Module: Vendor::Nasturtium

Defined in:
lib/vendor/nasturtium.rb

Overview

Possible Extensions

  • CE with a iNat global UUID
  • Bonus set GA for CE based on string matching
  • predict_otu

A middle-layer wrapper between Nasturtium and TaxonWorks

Constant Summary collapse

INAT_LICENSE_CODE_TO_TW_LICENSE =

Maps iNaturalist photo license_code values to CREATIVE_COMMONS_LICENSES keys. Derived from the inat_codes entries in CREATIVE_COMMONS_LICENSES. iNat codes not in this map (e.g. nil, 'arr') are not importable.

CREATIVE_COMMONS_LICENSES
.each_with_object({}) { |(k, v), h| h[v[:inat_code]] = k if v[:inat_code] }
.freeze
INAT_API_TIMEOUT =

Seconds before a synchronous iNat API call is abandoned.

15
SUBGENUS_RANK_CLASSES =
CODES_WITH_SUBGENUS.map { |code| Ranks.lookup(code, :subgenus) }.freeze
MATCH_CANDIDATES_LIMIT =

High enough that an ambiguous match's candidates are never truncated.

100

Class Method Summary collapse

Class Method Details

.build_image!(obs_photo, result:, observed_year: nil, person_cache: nil) ⇒ Image

Build and save an Image (with Attribution, copyright holder Person, and iNat identifier) from an iNat photo hash. Raises on failure so the caller's savepoint can roll back.

Parameters:

  • photo (Hash) —

    the 'photo' object from an iNat observation_photo

  • result (Hash) —

    the full Nasturtium observation result (for ORCID matching)

  • observed_year (Integer, nil) (defaults to: nil) —

    year of observation, used as copyright year

  • person_cache (Hash, nil) (defaults to: nil) —

    per-import-run cache (see .dedupe_person)

Returns:



502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/vendor/nasturtium.rb', line 502

def self.build_image!(obs_photo, result:, observed_year: nil, person_cache: nil)
  photo = obs_photo['photo']
  license_key = INAT_LICENSE_CODE_TO_TW_LICENSE[photo['license_code']]

  copyright_person = stub_copyright_person(result, media: photo, person_cache:)
  copyright_person.save! if copyright_person.new_record?

  attribution = Attribution.new(
    license: license_key,
    copyright_year: observed_year,
    copyright_holder_roles: [
      AttributionCopyrightHolder.new(person: copyright_person)
    ]
  )

  image_url = large_photo_url(photo['url'])
  tempfile = download_to_tempfile(image_url)
  begin
    image = Image.new(image_file: tempfile)
    image.attribution = attribution
    if obs_photo['uuid'].present?
      image.identifiers << Identifier::Global::Uuid::InaturalistObservationPhoto.new(
        identifier: obs_photo['uuid']
      )
    end
    begin
      image.save!
    rescue ActiveRecord::RecordInvalid
      existing = Image.find_by(image_file_fingerprint: image.image_file_fingerprint)
      raise unless existing
      image = existing
    end
  ensure
    tempfile.close!
  end

  image
end

.build_sound!(obs_sound, result:, observed_year: nil, person_cache: nil) ⇒ Sound

Build and save a Sound (with Attribution, copyright holder Person, and iNat identifier) from an iNat observation_sound hash. Raises on failure so the caller's savepoint can roll back.

Parameters:

  • obs_sound (Hash) —

    the outer observation_sound object (carries uuid)

  • result (Hash) —

    the full Nasturtium observation result (for ORCID matching)

  • observed_year (Integer, nil) (defaults to: nil) —

    year of observation, used as copyright year

  • person_cache (Hash, nil) (defaults to: nil) —

    per-import-run cache (see .dedupe_person)

Returns:



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/vendor/nasturtium.rb', line 385

def self.build_sound!(obs_sound, result:, observed_year: nil, person_cache: nil)
  sound_data  = obs_sound['sound']
  license_key = INAT_LICENSE_CODE_TO_TW_LICENSE[sound_data['license_code']]

  copyright_person = stub_copyright_person(result, media: sound_data, person_cache:)
  copyright_person.save! if copyright_person.new_record?

  attribution = Attribution.new(
    license: license_key,
    copyright_year: observed_year,
    copyright_holder_roles: [
      AttributionCopyrightHolder.new(person: copyright_person)
    ]
  )

  sound = Sound.new(name: sound_data['original_filename'].presence || obs_sound['uuid'])
  tempfile = download_to_tempfile(sound_data['file_url'])
  begin
    sound.sound_file.attach(
      io: File.open(tempfile.path),
      filename: tempfile.original_filename,
      content_type: Marcel::MimeType.for(Pathname.new(tempfile.path), name: tempfile.original_filename)
    )
  ensure
    tempfile.close!
  end
  sound.attribution = attribution
  if obs_sound['uuid'].present?
    sound.identifiers << Identifier::Global::Uuid::InaturalistObservationSound.new(
      identifier: obs_sound['uuid']
    )
  end
  begin
    sound.save!
  rescue ActiveRecord::RecordInvalid
    existing = obs_sound['uuid'].present? &&
      Identifier::Global::Uuid::InaturalistObservationSound.find_by(identifier: obs_sound['uuid'])
    raise unless existing
    sound = existing.identifier_object
  end

  sound
end

.by_observation_ids(ids) ⇒ Array<Hash>

Fetch multiple observations in a single iNat API request. IDs are joined as a comma-separated string because Faraday serializes Ruby arrays as id=...&id=... which the iNat API does not accept.

Parameters:

  • ids (Array<String>) —

    iNat observation integer IDs

Returns:

  • (Array<Hash>) —

    Nasturtium result hashes (only found observations are returned)

Raises:

  • (Timeout::Error) —

    if the iNat API does not respond within INAT_API_TIMEOUT seconds



32
33
34
35
36
37
38
# File 'lib/vendor/nasturtium.rb', line 32

def self.by_observation_ids(ids)
  return [] if ids.blank?

  Timeout.timeout(INAT_API_TIMEOUT) do
    ::Nasturtium.observations(id: ids.join(','), per_page: ids.size)['results']
  end
end

.dedupe_person(person, cache, result = nil) ⇒ Person

Reuse a single Person within one import run for an observer / copyright holder that would otherwise be built repeatedly (e.g. the same person as both georeferencer and copyright holder, or across multiple photos in the batch). Deduplication across separate import runs is intentionally not attempted.

Parameters:

  • person (Person) —

    a matched (persisted) or freshly built (new) Person

  • cache (Hash, nil) —

    the run cache; when nil no deduplication is done

  • result (Hash, nil) (defaults to: nil) —

    the Nasturtium result person was built directly from (i.e. person is that result's user), used for an exact cache key. Omit when person has no such structured identity at all — e.g. a third-party photo credit (see .stub_copyright_person) — falls back to name-based matching, the only option left in that case.

Returns:



128
129
130
131
132
133
134
135
136
# File 'lib/vendor/nasturtium.rb', line 128

def self.dedupe_person(person, cache, result = nil)
  return person if cache.nil?

  key = person_identity_key(person, result)
  cached = cache[key]
  return cached if cached && (cached.new_record? || Person.exists?(cached.id))

  cache[key] = person
end

.download_to_tempfile(url) ⇒ Tempfile

Download a remote image to a Tempfile with the original_filename method that Paperclip expects.

Parameters:

  • url (String)

Returns:

  • (Tempfile)


564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/vendor/nasturtium.rb', line 564

def self.download_to_tempfile(url)
  uri_path = URI.parse(url).path
  tempfile = Tempfile.new(['inat_media', File.extname(uri_path)], binmode: true)

  URI.open(url, 'rb') { |io| tempfile.write(io.read) }
  tempfile.rewind

  basename = File.basename(uri_path)
  tempfile.define_singleton_method(:original_filename) { basename }

  tempfile
end

.large_photo_url(photo_url) ⇒ String

Returns URL for the large version.

Parameters:

  • photo_url (String) —

    iNat square thumbnail URL

Returns:

  • (String) —

    URL for the large version



553
554
555
556
557
# File 'lib/vendor/nasturtium.rb', line 553

def self.large_photo_url(photo_url)
  return nil if photo_url.blank?

  photo_url.sub('/square.', '/large.')
end

.match_taxon_name(name, rank:, project_id:) ⇒ TaxonName?

iNat names never include a subgenus, while TaxonName#cached does ('Aus bus' vs 'Aus (Bus) bus'), so species-group names are matched with the subgenus ignored, and an iNat subgenus ('Bus') by its bare name.

When the name matches more than one taxon (e.g. a valid name and a junior homonym), the one valid name is taken, as iNat names are (almost always) meant as the current valid name.

Parameters:

  • name (String) —

    the iNat taxon name

  • rank (String, nil) —

    the iNat taxon rank

  • project_id (Integer)

Returns:

  • (TaxonName, nil) —

    nil when there is no match, or an ambiguous match without exactly one valid name



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/vendor/nasturtium.rb', line 270

def self.match_taxon_name(name, rank:, project_id:)
  candidates = if rank == 'subgenus'
    ::TaxonName.where(project_id:, name:, rank_class: SUBGENUS_RANK_CLASSES).to_a
  else
    match = ::Match::Otu::TaxonName.new(
      names: [name], project_id:,
      try_without_subgenus: true, try_without_subgenus_after_exact_match: true,
      candidates: MATCH_CANDIDATES_LIMIT
    ).call.first
    return nil if !match[:matched]
    return match[:taxon_name] if !match[:ambiguous]

    match[:candidates]
  end

  return candidates.first if candidates.one?

  valid = candidates.select { |tn| tn.cached_valid_taxon_name_id == tn.id }
  valid.one? ? valid.first : nil
end

.observer_identification_uuid(result) ⇒ String?

Find the UUID of the observer's current identification on the observation. Only the observer's own identifications are considered; community taxon has no UUID.

Parameters:

  • result (Hash) —

    a Nasturtium result

Returns:

  • (String, nil) —

    UUID string, or nil if not found



196
197
198
199
200
201
202
203
204
205
# File 'lib/vendor/nasturtium.rb', line 196

def self.observer_identification_uuid(result)
  user_id = result.dig('user', 'id')
  return nil if user_id.blank?

  ident = (result['identifications'] || []).find do |i|
    i.dig('user', 'id') == user_id && i['current']
  end

  ident&.dig('uuid')
end

.otu_for_taxon_name(taxon_name, project_id:) ⇒ Otu?

Returns the TaxonName's only OTU, else its only unnamed OTU, else a new OTU on it when it has none; nil when there is no single choice.

Parameters:

  • taxon_name (TaxonName)
  • project_id (Integer)

Returns:

  • (Otu, nil) —

    the TaxonName's only OTU, else its only unnamed OTU, else a new OTU on it when it has none; nil when there is no single choice



295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/vendor/nasturtium.rb', line 295

def self.otu_for_taxon_name(taxon_name, project_id:)
  otus = Otu.where(project_id:, taxon_name_id: taxon_name.id).to_a

  case otus.size
  when 0
    Otu.new(taxon_name:)
  when 1
    otus.first
  else
    unnamed = otus.select { |o| o.name.blank? }
    unnamed.one? ? unnamed.first : nil
  end
end

.parse_attribution_name(attribution) ⇒ String?

Parse the photographer's name out of an iNat-generated attribution string, e.g. "(c) Kim, Hyun-tae, some rights reserved (CC BY-NC-SA)" => "Kim, Hyun-tae" "(c) Jane Doe, all rights reserved" => "Jane Doe" "no rights reserved" (CC0 - no name given) => nil

Parameters:

  • attribution (String, nil)

Returns:

  • (String, nil)


485
486
487
488
489
490
491
492
# File 'lib/vendor/nasturtium.rb', line 485

def self.parse_attribution_name(attribution)
  return nil if attribution.blank?

  m = attribution.match(/\A\(c\)\s+(.+),\s*(?:some|all)\s+rights reserved/)
  return nil unless m

  m[1].squeeze(' ').strip
end

.permitted_photos(result) ⇒ Array<Hash>

Returns the iNat observation_photos that carry a CC or PD license importable into TW.

Parameters:

  • result (Hash) —

    a Nasturtium result

Returns:

  • (Array<Hash>) —

    observation_photo hashes (the outer object, which carries uuid)



349
350
351
352
353
354
355
356
357
358
359
# File 'lib/vendor/nasturtium.rb', line 349

def self.permitted_photos(result)
  return [] if result.blank?

  (result['observation_photos'] || []).filter_map do |obs_photo|
    photo = obs_photo['photo']
    next if photo.blank?
    next unless INAT_LICENSE_CODE_TO_TW_LICENSE.key?(photo['license_code'])

    obs_photo
  end
end

.permitted_sounds(result) ⇒ Array<Hash>

Returns the iNat observation_sounds that carry a CC or PD license importable into TW.

Parameters:

  • result (Hash) —

    a Nasturtium result

Returns:

  • (Array<Hash>) —

    observation_sound hashes (the outer object, which carries uuid)



365
366
367
368
369
370
371
372
373
374
375
# File 'lib/vendor/nasturtium.rb', line 365

def self.permitted_sounds(result)
  return [] if result.blank?

  (result['observation_sounds'] || []).filter_map do |obs_sound|
    sound = obs_sound['sound']
    next if sound.blank?
    next unless INAT_LICENSE_CODE_TO_TW_LICENSE.key?(sound['license_code'])

    obs_sound
  end
end

.person_by_orcid(result) ⇒ Person?

Attempt to find a Person in TW by the observer's ORCID. Returns nil if iNat provides no ORCID or no matching Person exists.

Parameters:

  • result (Hash) —

    a Nasturtium result

Returns:



155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/vendor/nasturtium.rb', line 155

def self.person_by_orcid(result)
  orcid = result.dig('user', 'orcid')
  return nil if orcid.blank?

  # iNat may return the bare ID (0000-0001-2345-6789) or a full URL; normalise to URL form
  orcid_url = orcid.start_with?('http') ? orcid : "https://orcid.org/#{orcid}"

  Person
    .joins(:identifiers)
    .where(identifiers: { type: 'Identifier::Global::Orcid', cached: orcid_url })
    .first
end

.person_from_display_name(name) ⇒ Person::Unvetted

Build a Person::Unvetted from a display name string.

Parameters:

  • name (String, nil)

Returns:



544
545
546
547
548
549
# File 'lib/vendor/nasturtium.rb', line 544

def self.person_from_display_name(name)
  return Person::Unvetted.new(last_name: 'Undetermined iNaturalist user') if name.blank?
  return Person::Unvetted.new(last_name: name) unless name.include?(' ')

  Person.parse_to_people(name).first || Person::Unvetted.new(last_name: name)
end

.person_identity_key(person, result) ⇒ String

Returns a within-run identity key.

Parameters:

  • person (Person)
  • result (Hash, nil) —

    the Nasturtium result person was built directly from

Returns:

  • (String) —

    a within-run identity key



141
142
143
144
145
146
147
148
# File 'lib/vendor/nasturtium.rb', line 141

def self.person_identity_key(person, result)
  return "id:#{person.id}" if person.persisted?

  user_id = result&.dig('user', 'id')
  return "inat_user:#{user_id}" if user_id.present?

  "name:#{[person.first_name, person.last_name].filter_map { |s| s&.strip&.downcase&.presence }.join(' ')}"
end

.stub_biocuration_classes(result, project_id:) ⇒ Array<BiocurationClass>

Find BiocurationClass records in the project that match iNat annotations on the observation, via INAT_ANNOTATION_LABEL_TO_DWC_URI → BiocurationGroup URI. Only annotations with a DwC mapping are considered; unmatched annotations are skipped.

Parameters:

  • result (Hash) —

    a Nasturtium result

  • project_id (Integer)

Returns:



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/vendor/nasturtium.rb', line 316

def self.stub_biocuration_classes(result, project_id:)
  annotations = (result['annotations'] || []).select do |a|
    a.dig('controlled_attribute', 'label').present? &&
      a.dig('controlled_value', 'label').present? &&
      INAT_ANNOTATION_LABEL_TO_DWC_URI.key?(a.dig('controlled_attribute', 'label'))
  end
  return [] if annotations.empty?

  relevant_uris = annotations.map { |a| INAT_ANNOTATION_LABEL_TO_DWC_URI[a.dig('controlled_attribute', 'label')] }.uniq
  group_ids_by_uri = BiocurationGroup
    .where(project_id:, uri: relevant_uris)
    .pluck(:uri, :id)
    .each_with_object(Hash.new { |h, k| h[k] = [] }) { |(uri, id), h| h[uri] << id }

  annotations.filter_map do |annotation|
    term_label  = annotation.dig('controlled_attribute', 'label')
    value_label = annotation.dig('controlled_value', 'label')

    group_ids = group_ids_by_uri[INAT_ANNOTATION_LABEL_TO_DWC_URI[term_label]]
    next if group_ids.blank?

    BiocurationClass
      .where(project_id:)
      .joins(:tags)
      .where(tags: { keyword_id: group_ids })
      .find_by('lower(controlled_vocabulary_terms.name) = lower(?)', value_label)
  end.compact
end

.stub_collecting_event(result, guess_as_locality: true, person_cache: nil) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/vendor/nasturtium.rb', line 54

def self.stub_collecting_event(result, guess_as_locality: true, person_cache: nil)
  return nil if result.blank?

  d = result['observed_on_details']

  p = {
    verbatim_collectors: result.dig('user', 'name').presence || result.dig('user', 'login').presence,
    verbatim_date: result['observed_on_string'].presence,
    start_date_day: d['day'],
    start_date_month: d['month'],
    start_date_year: d['year'],
  }

  if (t = result['time_observed_at']).present?
    parsed = Time.parse(t)
    p[:time_start_hour]   = parsed.hour
    p[:time_start_minute] = parsed.min
    p[:time_start_second] = parsed.sec if parsed.sec > 0
  else
    p[:time_start_hour] = d['hour'] unless d['hour'].nil?
  end

  p[:verbatim_locality] = result['place_guess'] if guess_as_locality

  ce = CollectingEvent.new(
    p.merge(
      georeferences: [stub_georeference(result)].compact,
    )
  )

  collector = stub_collector(result, person_cache:)
  ce.collector_roles.build(person: collector) if collector

  ce
end

.stub_collector(result, person_cache: nil) ⇒ Person

iNat has no notion of a "collector" distinct from the observer — the observation's own user is the collector.

Parameters:

  • result (Hash) —

    a Nasturtium result

  • person_cache (Hash, nil) (defaults to: nil) —

    per-import-run cache (see .dedupe_person)

Returns:



96
97
98
# File 'lib/vendor/nasturtium.rb', line 96

def self.stub_collector(result, person_cache: nil)
  stub_observer_person(result, person_cache:)
end

Find or build the copyright holder Person for a photo or sound.

iNat auto-generates attribution from the uploader's account in every case we've observed against the live API — except for photos imported into iNat from an external source (e.g. Flickr), where iNat's own attribution_name uses that source's native_realname/native_username instead, crediting the original photographer rather than the iNat account that imported it. Those two fields are deliberately excluded from the public API's JSON output, so the rendered attribution string is the only way to recover that identity — there's no structured field we could use instead. So we check whether the parsed name actually is the observer rather than assuming it.

Strategy (in order):

1. If the attribution names the observer (or gives no name at all, e.g. CC0's
 "no rights reserved") — ORCID match first, then key off the observer's exact
 iNat identity.
2. Otherwise, the attribution names someone else Nasturtium has no ORCID/id
 for (a native_realname/native_username credit) — build a Person::Unvetted
 from that name, weakly deduped by name within this run since that's all we
 have to go on.

Parameters:

  • result (Hash) —

    the full Nasturtium observation result (used for ORCID/user id lookup)

  • media (Hash) —

    the photo or sound hash (used for attribution string fallback)

  • person_cache (Hash, nil) (defaults to: nil) —

    per-import-run cache (see .dedupe_person)

Returns:



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# File 'lib/vendor/nasturtium.rb', line 454

def self.stub_copyright_person(result, media:, person_cache: nil)
  copyright_name = parse_attribution_name(media['attribution'])
  observer_names = [result.dig('user', 'name'), result.dig('user', 'login')].map(&:presence).compact

  names_someone_else = copyright_name.present? &&
    observer_names.none? { |n| n.casecmp?(copyright_name) }

  if names_someone_else
    # Attribution names someone other than the observer (a native_realname/
    # native_username credit) — no exact identity available, weak name dedup only.
    return dedupe_person(person_from_display_name(copyright_name), person_cache)
  end

  matched = person_by_orcid(result)
  return dedupe_person(matched, person_cache, result) if matched

  # Neither the attribution nor the observer's own account gives us a name at
  # all (e.g. CC0 with a blank user.name/login) - person_from_display_name
  # falls back to a placeholder in that case.
  person = person_from_display_name(copyright_name || observer_names.first)

  dedupe_person(person, person_cache, result)
end

.stub_georeference(result) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/vendor/nasturtium.rb', line 168

def self.stub_georeference(result)
  return nil if result.blank?

  # Skip georeference for obscured observations — iNat jitters coordinates
  # within a ~22km bounding box, exceeding TW's 10km error_radius maximum.
  return nil if result['obscured']

  # Skip if the reported accuracy itself exceeds TW's 10km error_radius limit.
  accuracy = result['positional_accuracy']
  return nil if accuracy.present? && accuracy > 10_000

  c = result.dig('geojson', 'coordinates')

  return nil if c.blank?

  Georeference::Inaturalist.new(
    error_radius: result['positional_accuracy'],
    geographic_item: GeographicItem.new(
      geography: Gis::FACTORY.parse_wkt("POINT(#{c.first} #{c.second})")
    )
  )
end

.stub_identifier(result) ⇒ Identifier::Global::Uuid::InaturalistObservation?

Parameters:

  • result (Hash) —

    a Nasturtium result

Returns:



209
210
211
212
213
214
215
216
# File 'lib/vendor/nasturtium.rb', line 209

def self.stub_identifier(result)
  return nil if result.blank?

  uuid = result['uuid']
  return nil if uuid.blank?

  Identifier::Global::Uuid::InaturalistObservation.new(identifier: uuid)
end

.stub_observer_person(result, person_cache: nil) ⇒ Person

Find or build the observer as a Person. Strategy: ORCID match first, then Person::Unvetted from user.name or user.login. Used as collector on CollectingEvent, determiner on TaxonDetermination, and georeferencer on Georeference.

Parameters:

  • result (Hash) —

    a Nasturtium result

  • person_cache (Hash, nil) (defaults to: nil) —

    per-import-run cache (see .dedupe_person)

Returns:



108
109
110
111
112
113
# File 'lib/vendor/nasturtium.rb', line 108

def self.stub_observer_person(result, person_cache: nil)
  person = person_by_orcid(result) ||
    person_from_display_name(result.dig('user', 'name').presence || result.dig('user', 'login'))

  dedupe_person(person, person_cache, result)
end

.stub_otu(result, project_id:, match_by_name: false, use_community_taxon: true) ⇒ Otu?

Find or build an OTU for the iNat taxon.

Parameters:

  • result (Hash) —

    a Nasturtium result

  • project_id (Integer)
  • match_by_name (Boolean) (defaults to: false) —

    if true, look for an existing OTU in the project first:

    1. an OTU on a TaxonName matching the iNat name - subgenus ignored, see
     .match_taxon_name - or a new OTU on that TaxonName if it has none
    2. an OTU whose name is the iNat name
  • use_community_taxon (Boolean) (defaults to: true) —

    if true, use the community consensus taxon (community_taxon, falling back to taxon); if false, use the observation taxon (taxon), which is the observer's own most recent ID when no community consensus exists

Returns:



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/vendor/nasturtium.rb', line 232

def self.stub_otu(result, project_id:, match_by_name: false, use_community_taxon: true)
  taxon = self.taxon(result, use_community_taxon:)
  taxon_name = taxon&.dig('name')
  return nil if taxon_name.blank?

  if match_by_name
    matched_taxon_name = match_taxon_name(taxon_name, rank: taxon['rank'], project_id:)

    if matched_taxon_name
      otu = otu_for_taxon_name(matched_taxon_name, project_id:)
      return otu if otu
    end

    existing = Otu.where(project_id:, name: taxon_name).order(:id).first
    return existing if existing
  end

  Otu.new(name: taxon_name)
end

.taxon(result, use_community_taxon: true) ⇒ Hash?

Returns the iNat taxon (community_taxon, falling back to taxon, or just taxon when use_community_taxon is false).

Returns:

  • (Hash, nil) —

    the iNat taxon (community_taxon, falling back to taxon, or just taxon when use_community_taxon is false)



42
43
44
45
46
47
48
# File 'lib/vendor/nasturtium.rb', line 42

def self.taxon(result, use_community_taxon: true)
  if use_community_taxon && result.dig('community_taxon', 'name').present?
    result['community_taxon']
  else
    result['taxon']
  end
end

.taxon_name(result, use_community_taxon: true) ⇒ Object



50
51
52
# File 'lib/vendor/nasturtium.rb', line 50

def self.taxon_name(result, use_community_taxon: true)
  taxon(result, use_community_taxon:)&.dig('name')
end