Class: Autoselect::TaxonName::CatalogueOfLifeCreator

Inherits:
Object
  • Object
show all
Defined in:
lib/autoselect/taxon_name/catalogue_of_life_creator.rb

Overview

Creates TaxonName records (and CoL URI identifiers) from a Catalog of Life selection.

Receives an ordered list of rows (distal → proximal, i.e. kingdom first, target last). Each row represents one name to be created or an existing TaxonName to be used as a parent. Rows with a taxonworks_id are already in the project; if the row immediately above them was just inserted (a rank CoL reports that TaxonWorks' real chain skipped), the existing record is reparented onto it. Otherwise it's used as-is as a parent anchor and left untouched, and it must already be under the row above it - if not, the rows don't describe TaxonWorks' real lineage and the request is refused.

Defined Under Namespace

Classes: CreationError

Instance Method Summary collapse

Constructor Details

#initialize(rows:, project_id:, user_id:, col_code: nil) ⇒ CatalogueOfLifeCreator

Returns a new instance of CatalogueOfLifeCreator.



49
50
51
52
53
54
# File 'lib/autoselect/taxon_name/catalogue_of_life_creator.rb', line 49

def initialize(rows:, project_id:, user_id:, col_code: nil)
  @rows = rows
  @project_id = project_id.to_i
  @user_id = user_id.to_i
  @col_code = col_code.to_s.downcase.presence
end

Instance Method Details

#call ⇒ Hash

Returns { taxon_name_id: Integer, created_ids: Array }.

Returns:

  • (Hash) —

    { taxon_name_id: Integer, created_ids: Array }

Raises:

  • (CreationError) —

    if any record fails validation; the transaction is fully rolled back.



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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/autoselect/taxon_name/catalogue_of_life_creator.rb', line 59

def call
  parent_id = project_root_id
  # Whether parent_id is a row created in this call (a skipped
  # unknown-rank row leaves it as it was).
  parent_created = false
  created_ids = []

  matched_ids = @rows.filter_map { |row|
    row[:taxonworks_id].presence&.to_i
  }
  current_parent_ids = ::TaxonName
    .where(id: matched_ids, project_id: @project_id)
    .pluck(:id, :parent_id).to_h

  unknown_ids = matched_ids - current_parent_ids.keys
  if unknown_ids.any?
    raise CreationError.new("Unknown TaxonName id(s) for this project: #{unknown_ids.join(', ')}")
  end

  refuse_non_accepted()
  # An ancestor at a rank TaxonWorks doesn't have is skipped below, but
  # the target can't be: the result is whatever was created last, so
  # skipping it would hand back its parent as the record.
  refuse_unknown_target_rank()

  ::ActiveRecord::Base.transaction do
    @rows.each do |row|
      if row[:taxonworks_id].present?
        existing_id = row[:taxonworks_id].to_i

        if current_parent_ids[existing_id] != parent_id
          existing = ::TaxonName.where(project_id: @project_id).find(existing_id)

          # Only a row just created directly above (a rank CoL reports
          # that TaxonWorks' real chain skipped) reattaches this existing
          # record. Any other mismatch means the rows don't describe
          # TaxonWorks' real lineage (a stale modal, a hand-made
          # request) - refuse rather than move a curated record.
          if !parent_created
            raise CreationError.new(
              "#{existing.cached} isn't under the record above it in " \
                'this request; the classification may have changed - ' \
                'reopen and try again.',
              col_name: row[:col_name],
              col_id: row[:col_id]
            )
          end

          begin
            existing.update!(parent_id:)
          rescue ::ActiveRecord::RecordInvalid => e
            raise CreationError.new(
              "Reparenting #{existing.cached}: #{e.record.errors.full_messages.join(', ')}",
              col_name: row[:col_name],
              col_id: row[:col_id]
            )
          end
        end

        parent_id = existing_id
        parent_created = false
        next
      end

      rank_class = resolve_rank_class(row[:col_rank])

      # Skip rows whose rank is entirely unknown to TaxonWorks — we cannot
      # create a valid Protonym without a rank_class (e.g. CoL-only ranks
      # like 'domain').
      next if rank_class.nil?

      author, year = ::Vendor::Colrapi::NameTranslation
        .split_authorship(row[:col_authorship], row[:col_year])

      begin
        tn = ::TaxonName.create!(
          type: 'Protonym',
          name: ::Vendor::Colrapi::NameTranslation
            .protonym_name(row[:col_name], row[:col_rank]),
          parent_id:,
          rank_class:,
          verbatim_author: author,
          year_of_publication: year,
          project_id: @project_id,
          by: @user_id,
        )
      rescue ::ActiveRecord::RecordInvalid => e
        raise CreationError.new(
          e.record.errors.full_messages.join(', '),
          col_name: row[:col_name],
          col_id: row[:col_id]
        )
      end

      create_col_identifier(tn, row) if row[:col_id].present?

      created_ids << tn.id
      parent_id = tn.id
      parent_created = true
    end
  end

  { taxon_name_id: parent_id, created_ids: }
end

#create_col_identifier(taxon_name, row) ⇒ Object (private)

Records the CoL taxon id as a ChecklistBank URI identifier on the freshly-created name.



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/autoselect/taxon_name/catalogue_of_life_creator.rb', line 173

def create_col_identifier(taxon_name, row)
  ::Identifier::Global::Uri::ChecklistBank.create!(
    taxon_id: row[:col_id],
    dataset_id: row[:dataset_id],
    identifier_object: taxon_name,
    project_id: @project_id,
    by: @user_id
  )
rescue ::ActiveRecord::RecordInvalid => e
  raise CreationError.new(
    "CoL identifier for #{row[:col_name]}: " \
      "#{e.record.errors.full_messages.join(', ')}",
    col_name: row[:col_name],
    col_id: row[:col_id]
  )
end

#project_root_id ⇒ Object (private)

Returns the TaxonName id of the project's Root node.



167
168
169
# File 'lib/autoselect/taxon_name/catalogue_of_life_creator.rb', line 167

def project_root_id
  ::Project.find(@project_id).root_taxon_name.id
end

#refuse_non_accepted ⇒ Object (private)

CoL gives a non-accepted name (synonym, misapplied, ...) only its accepted name's classification, nothing of its own, so there's no sound place to create it. The modal already disables Create for it; this is the server-side guard. A row with a taxonworks_id isn't created, so an existing name is still used.

Raises:



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/autoselect/taxon_name/catalogue_of_life_creator.rb', line 195

def refuse_non_accepted
  row = @rows.find do |r|
    r[:taxonworks_id].blank? && r[:col_status].present? &&
      !::Vendor::Colrapi::COL_ACCEPTED_STATUSES
        .include?(r[:col_status].to_s.downcase)
  end
  return if row.nil?

  raise CreationError.new(
    "#{row[:col_name]} is a #{row[:col_status]} in Catalogue of Life, " \
      "which has no classification of its own for it - it can't be " \
      'created from here.',
    col_name: row[:col_name],
    col_id: row[:col_id]
  )
end

#refuse_unknown_target_rank ⇒ Object (private)

Raises:



212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/autoselect/taxon_name/catalogue_of_life_creator.rb', line 212

def refuse_unknown_target_rank
  target = @rows.last
  return if target.nil? || target[:taxonworks_id].present?
  return if resolve_rank_class(target[:col_rank])

  raise CreationError.new(
    "Rank #{target[:col_rank]} isn't a TaxonWorks rank - " \
      "#{target[:col_name]} can't be created.",
    col_name: target[:col_name],
    col_id: target[:col_id]
  )
end

#resolve_rank_class(rank_string) ⇒ Object (private)

Maps a human-readable CoL rank string to a TaxonWorks rank_class string. Returns nil for unknown ranks.



227
228
229
# File 'lib/autoselect/taxon_name/catalogue_of_life_creator.rb', line 227

def resolve_rank_class(rank_string)
  ::Vendor::Colrapi.rank_class_for(rank_string, col_code: @col_code)
end