Class: Autoselect::TaxonName::ColCreator

Inherits:
Object
  • Object
show all
Defined in:
lib/autoselect/taxon_name/col_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.

TODO: Rename to remove abbreviation CatalogueOfLifeCreator

Defined Under Namespace

Classes: CreationError

Constant Summary collapse

COL_BASE_URI =

TODO:

'https://www.catalogueoflife.org/data/taxon/'.freeze

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of ColCreator.



55
56
57
58
59
60
# File 'lib/autoselect/taxon_name/col_creator.rb', line 55

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.



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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/autoselect/taxon_name/col_creator.rb', line 65

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

      if row[:col_id].present?
        begin
          ::Identifier::Global::Uri::ChecklistBank.create!(
            taxon_id: row[:col_id],
            dataset_id: row[:dataset_id],
            identifier_object: tn,
            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
      end

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

  { taxon_name_id: parent_id, created_ids: }
end

#project_root_id ⇒ Object (private)

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



189
190
191
# File 'lib/autoselect/taxon_name/col_creator.rb', line 189

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:



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/autoselect/taxon_name/col_creator.rb', line 198

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:



215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/autoselect/taxon_name/col_creator.rb', line 215

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.



230
231
232
# File 'lib/autoselect/taxon_name/col_creator.rb', line 230

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