Class: AssertedEnvironment

Overview

An AssertedEnvironment is the assertion that an environment, drawn from the Environment Ontology (ENVO), characterizes some record - see ENVIRONMENT_ASSERTABLE_TYPES for what those records can be.

Unlike AnatomicalPart, AssertedEnvironment does not accept a free-text name - only ENVO-backed uri/uri_label pairs are permitted (see Vendor::Envo). This is deliberate - we want to see how well ENVO alone can hold up rather than accumulate uncontrolled local terms.

position provides a simple ordering, not a category - each larger position is understood to be some refinement of the ones before it (e.g. "terrestrial biome" then "temperate forest biome").

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Shared::IsData

#errors_excepting, #full_error_messages_excepting, #identical, #is_community?, #is_in_use?, #similar

Methods included from Shared::Tags

#reject_tags, #tag_with, #tagged?, #tagged_with?

Methods included from Shared::Notes

#concatenated_notes_string, #reject_notes

Methods included from Shared::Identifiers

#dwc_occurrence_id, #identified?, #next_by_identifier, #previous_by_identifier, #reject_identifiers, #uuid, #visible_identifiers_for

Methods included from Shared::DataAttributes

#import_attributes, #internal_attributes, #keyword_value_hash, #reject_data_attributes

Methods included from Shared::Citations

#cited?, #mark_citations_for_destruction, #nomenclature_date, #origin_citation_source_id, #reject_citations, #requires_citation?, #sources_by_topic_id

Methods included from Housekeeping

#has_polymorphic_relationship?

Methods inherited from ApplicationRecord

transaction_with_retry

Instance Attribute Details

#asserted_environment_object_id ⇒ Integer

polymorphic object ID

Returns:

  • (Integer)


43
44
45
46
47
48
49
50
51
52
53
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
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'app/models/asserted_environment.rb', line 43

class AssertedEnvironment < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Identifiers
  # Override the :uri defined by Identifiers, which otherwise shadows the
  # asserted_environment's own `uri` column/validations. Bad, same as
  # AnatomicalPart.
  def uri
    self[:uri]
  end
  include Shared::Notes
  include Shared::Tags
  include Shared::DwcOccurrenceHooks
  include Shared::IsData
  include Shared::BatchByFilterScope

  acts_as_list scope: [:project_id, :asserted_environment_object_type, :asserted_environment_object_id]

  belongs_to :asserted_environment_object, polymorphic: true

  after_save :set_cached

  validates_presence_of :asserted_environment_object
  validates_presence_of :uri
  validates_presence_of :uri_label
  validate :asserted_environment_object_has_allowed_type
  validate :uri_is_an_envo_term
  validates_uniqueness_of :uri, scope: [
    :project_id, :asserted_environment_object_type, :asserted_environment_object_id
  ]

  # @return [Scope]
  #   DwcOccurrence records potentially affected by this asserted environment;
  #   only CollectingEvent assertions feed dwc:habitat (see
  #   Shared::Dwc::CollectingEventExtensions#dwc_habitat), so other object
  #   types have no corresponding DwcOccurrence records to rebuild.
  def dwc_occurrences
    return DwcOccurrence.none unless asserted_environment_object_type == 'CollectingEvent'

    a = DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .where(co: {collecting_event_id: asserted_environment_object_id})

    b = DwcOccurrence
      .joins("JOIN field_occurrences fo on dwc_occurrence_object_id = fo.id AND dwc_occurrence_object_type = 'FieldOccurrence'")
      .where(fo: {collecting_event_id: asserted_environment_object_id})

    ::Queries.union(DwcOccurrence, [a, b])
  end

  # mode :add - assert the ENVO term in params (uri, uri_label) on every object
  # returned by query, optionally with a citation (params[:citation], like
  # { source_id:, pages:, is_original: }).
  #
  # If the object already has an asserted environment for the term then the
  # citation, when provided and not already present, is added to that
  # existing asserted environment, otherwise the object is not updated.
  #
  # mode :remove - destroy the asserted environment for the ENVO term in
  # params (uri) on every object returned by query; objects without one are
  # not updated.
  #
  # updated are AssertedEnvironment ids (destroyed ones for :remove),
  # not_updated are object ids.
  def self.process_batch_by_filter_scope(
    batch_response: nil, query: nil, hash_query: nil, mode: nil, params: nil,
    async: nil, project_id: nil, user_id: nil,
    called_from_async: false
  )
    # Don't call async from async (the point is we do the same processing in
    # async and not in async, and this function handles both that processing and
    # making the async call, so it's this much janky).
    async = false if called_from_async == true
    r = batch_response

    mode = mode.to_sym
    if ![:add, :remove].include?(mode)
      r.errors["unsupported mode '#{mode}'"] = 1
      return r
    end

    object_type = query.klass.base_class.name
    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(object_type)
      r.errors["#{object_type} can not have asserted environments"] = 1
      return r
    end

    if params[:uri].blank?
      r.errors['uri is required'] = 1
      return r
    end

    if mode == :add && params[:uri_label].blank?
      r.errors['uri_label is required'] = 1
      return r
    end

    if async && !called_from_async
      BatchByFilterScopeJob.perform_later(
        klass: self.name,
        hash_query:,
        mode:,
        params:,
        project_id:,
        user_id:
      )
    elsif mode == :add
      citation_params = params[:citation]&.to_h&.symbolize_keys&.compact_blank

      query.find_each do |o|
        add_by_batch(o, params[:uri], params[:uri_label], citation_params, r)
      end
    else
      remove_by_batch(query, params[:uri], r)
    end

    r
  end

  # @param citation_params [Hash, nil] attributes for a new Citation
  # @param r [BatchResponse] updated with the result for object
  def self.add_by_batch(object, uri, uri_label, citation_params, r)
    existing = AssertedEnvironment.find_by(
      asserted_environment_object: object, uri:
    )

    if existing
      if citation_params.blank?
        r.not_updated.push object.id
        r.validation_errors['asserted environment already exists'] += 1
        return
      end

      citation = existing.citations.create(citation_params)
      if citation.persisted?
        r.updated.push existing.id
      else
        r.not_updated.push object.id
        citation.errors.full_messages.each { |msg|
          r.validation_errors["citation not added to existing asserted environment: #{msg}"] += 1
        }
      end

      return
    end

    a = AssertedEnvironment.create(
      asserted_environment_object: object,
      uri:,
      uri_label:,
      citations_attributes: citation_params.present? ? [citation_params] : []
    )

    if a.persisted?
      r.updated.push a.id
    else
      r.not_updated.push object.id
      a.errors.full_messages.each { |msg|
        r.validation_errors[msg] += 1
      }
    end
  end

  # @param r [BatchResponse] updated with the result for each object in query
  def self.remove_by_batch(query, uri, r)
    existing_by_object_id = AssertedEnvironment
      .where(asserted_environment_object: query, uri:)
      .index_by(&:asserted_environment_object_id)

    query.find_each do |o|
      a = existing_by_object_id[o.id]

      if a.nil?
        r.not_updated.push o.id
        r.validation_errors['no asserted environment for this term'] += 1
      elsif a.destroy
        r.updated.push a.id
      else
        r.not_updated.push o.id
        a.errors.full_messages.each { |msg|
          r.validation_errors[msg] += 1
        }
      end
    end
  end

  protected

  def set_cached
    update_column(:cached, uri_label)
  end

  def asserted_environment_object_has_allowed_type
    t = asserted_environment_object_type&.to_s # STRING (not symbol)

    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(t)
      errors.add(t || :base, " - the type of this asserted environment's object can only be one of #{ENVIRONMENT_ASSERTABLE_TYPES}, not '#{t}'")
    end
  end

  def uri_is_an_envo_term
    return if uri.blank?

    errors.add(:uri, 'must be an ENVO term URI') unless Vendor::Envo.valid_uri?(uri)
  end
end

#asserted_environment_object_type ⇒ String

polymorphic object type

Returns:

  • (String)


43
44
45
46
47
48
49
50
51
52
53
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
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'app/models/asserted_environment.rb', line 43

class AssertedEnvironment < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Identifiers
  # Override the :uri defined by Identifiers, which otherwise shadows the
  # asserted_environment's own `uri` column/validations. Bad, same as
  # AnatomicalPart.
  def uri
    self[:uri]
  end
  include Shared::Notes
  include Shared::Tags
  include Shared::DwcOccurrenceHooks
  include Shared::IsData
  include Shared::BatchByFilterScope

  acts_as_list scope: [:project_id, :asserted_environment_object_type, :asserted_environment_object_id]

  belongs_to :asserted_environment_object, polymorphic: true

  after_save :set_cached

  validates_presence_of :asserted_environment_object
  validates_presence_of :uri
  validates_presence_of :uri_label
  validate :asserted_environment_object_has_allowed_type
  validate :uri_is_an_envo_term
  validates_uniqueness_of :uri, scope: [
    :project_id, :asserted_environment_object_type, :asserted_environment_object_id
  ]

  # @return [Scope]
  #   DwcOccurrence records potentially affected by this asserted environment;
  #   only CollectingEvent assertions feed dwc:habitat (see
  #   Shared::Dwc::CollectingEventExtensions#dwc_habitat), so other object
  #   types have no corresponding DwcOccurrence records to rebuild.
  def dwc_occurrences
    return DwcOccurrence.none unless asserted_environment_object_type == 'CollectingEvent'

    a = DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .where(co: {collecting_event_id: asserted_environment_object_id})

    b = DwcOccurrence
      .joins("JOIN field_occurrences fo on dwc_occurrence_object_id = fo.id AND dwc_occurrence_object_type = 'FieldOccurrence'")
      .where(fo: {collecting_event_id: asserted_environment_object_id})

    ::Queries.union(DwcOccurrence, [a, b])
  end

  # mode :add - assert the ENVO term in params (uri, uri_label) on every object
  # returned by query, optionally with a citation (params[:citation], like
  # { source_id:, pages:, is_original: }).
  #
  # If the object already has an asserted environment for the term then the
  # citation, when provided and not already present, is added to that
  # existing asserted environment, otherwise the object is not updated.
  #
  # mode :remove - destroy the asserted environment for the ENVO term in
  # params (uri) on every object returned by query; objects without one are
  # not updated.
  #
  # updated are AssertedEnvironment ids (destroyed ones for :remove),
  # not_updated are object ids.
  def self.process_batch_by_filter_scope(
    batch_response: nil, query: nil, hash_query: nil, mode: nil, params: nil,
    async: nil, project_id: nil, user_id: nil,
    called_from_async: false
  )
    # Don't call async from async (the point is we do the same processing in
    # async and not in async, and this function handles both that processing and
    # making the async call, so it's this much janky).
    async = false if called_from_async == true
    r = batch_response

    mode = mode.to_sym
    if ![:add, :remove].include?(mode)
      r.errors["unsupported mode '#{mode}'"] = 1
      return r
    end

    object_type = query.klass.base_class.name
    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(object_type)
      r.errors["#{object_type} can not have asserted environments"] = 1
      return r
    end

    if params[:uri].blank?
      r.errors['uri is required'] = 1
      return r
    end

    if mode == :add && params[:uri_label].blank?
      r.errors['uri_label is required'] = 1
      return r
    end

    if async && !called_from_async
      BatchByFilterScopeJob.perform_later(
        klass: self.name,
        hash_query:,
        mode:,
        params:,
        project_id:,
        user_id:
      )
    elsif mode == :add
      citation_params = params[:citation]&.to_h&.symbolize_keys&.compact_blank

      query.find_each do |o|
        add_by_batch(o, params[:uri], params[:uri_label], citation_params, r)
      end
    else
      remove_by_batch(query, params[:uri], r)
    end

    r
  end

  # @param citation_params [Hash, nil] attributes for a new Citation
  # @param r [BatchResponse] updated with the result for object
  def self.add_by_batch(object, uri, uri_label, citation_params, r)
    existing = AssertedEnvironment.find_by(
      asserted_environment_object: object, uri:
    )

    if existing
      if citation_params.blank?
        r.not_updated.push object.id
        r.validation_errors['asserted environment already exists'] += 1
        return
      end

      citation = existing.citations.create(citation_params)
      if citation.persisted?
        r.updated.push existing.id
      else
        r.not_updated.push object.id
        citation.errors.full_messages.each { |msg|
          r.validation_errors["citation not added to existing asserted environment: #{msg}"] += 1
        }
      end

      return
    end

    a = AssertedEnvironment.create(
      asserted_environment_object: object,
      uri:,
      uri_label:,
      citations_attributes: citation_params.present? ? [citation_params] : []
    )

    if a.persisted?
      r.updated.push a.id
    else
      r.not_updated.push object.id
      a.errors.full_messages.each { |msg|
        r.validation_errors[msg] += 1
      }
    end
  end

  # @param r [BatchResponse] updated with the result for each object in query
  def self.remove_by_batch(query, uri, r)
    existing_by_object_id = AssertedEnvironment
      .where(asserted_environment_object: query, uri:)
      .index_by(&:asserted_environment_object_id)

    query.find_each do |o|
      a = existing_by_object_id[o.id]

      if a.nil?
        r.not_updated.push o.id
        r.validation_errors['no asserted environment for this term'] += 1
      elsif a.destroy
        r.updated.push a.id
      else
        r.not_updated.push o.id
        a.errors.full_messages.each { |msg|
          r.validation_errors[msg] += 1
        }
      end
    end
  end

  protected

  def set_cached
    update_column(:cached, uri_label)
  end

  def asserted_environment_object_has_allowed_type
    t = asserted_environment_object_type&.to_s # STRING (not symbol)

    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(t)
      errors.add(t || :base, " - the type of this asserted environment's object can only be one of #{ENVIRONMENT_ASSERTABLE_TYPES}, not '#{t}'")
    end
  end

  def uri_is_an_envo_term
    return if uri.blank?

    errors.add(:uri, 'must be an ENVO term URI') unless Vendor::Envo.valid_uri?(uri)
  end
end

#cached ⇒ String

cached uri_label

Returns:

  • (String)


43
44
45
46
47
48
49
50
51
52
53
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
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'app/models/asserted_environment.rb', line 43

class AssertedEnvironment < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Identifiers
  # Override the :uri defined by Identifiers, which otherwise shadows the
  # asserted_environment's own `uri` column/validations. Bad, same as
  # AnatomicalPart.
  def uri
    self[:uri]
  end
  include Shared::Notes
  include Shared::Tags
  include Shared::DwcOccurrenceHooks
  include Shared::IsData
  include Shared::BatchByFilterScope

  acts_as_list scope: [:project_id, :asserted_environment_object_type, :asserted_environment_object_id]

  belongs_to :asserted_environment_object, polymorphic: true

  after_save :set_cached

  validates_presence_of :asserted_environment_object
  validates_presence_of :uri
  validates_presence_of :uri_label
  validate :asserted_environment_object_has_allowed_type
  validate :uri_is_an_envo_term
  validates_uniqueness_of :uri, scope: [
    :project_id, :asserted_environment_object_type, :asserted_environment_object_id
  ]

  # @return [Scope]
  #   DwcOccurrence records potentially affected by this asserted environment;
  #   only CollectingEvent assertions feed dwc:habitat (see
  #   Shared::Dwc::CollectingEventExtensions#dwc_habitat), so other object
  #   types have no corresponding DwcOccurrence records to rebuild.
  def dwc_occurrences
    return DwcOccurrence.none unless asserted_environment_object_type == 'CollectingEvent'

    a = DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .where(co: {collecting_event_id: asserted_environment_object_id})

    b = DwcOccurrence
      .joins("JOIN field_occurrences fo on dwc_occurrence_object_id = fo.id AND dwc_occurrence_object_type = 'FieldOccurrence'")
      .where(fo: {collecting_event_id: asserted_environment_object_id})

    ::Queries.union(DwcOccurrence, [a, b])
  end

  # mode :add - assert the ENVO term in params (uri, uri_label) on every object
  # returned by query, optionally with a citation (params[:citation], like
  # { source_id:, pages:, is_original: }).
  #
  # If the object already has an asserted environment for the term then the
  # citation, when provided and not already present, is added to that
  # existing asserted environment, otherwise the object is not updated.
  #
  # mode :remove - destroy the asserted environment for the ENVO term in
  # params (uri) on every object returned by query; objects without one are
  # not updated.
  #
  # updated are AssertedEnvironment ids (destroyed ones for :remove),
  # not_updated are object ids.
  def self.process_batch_by_filter_scope(
    batch_response: nil, query: nil, hash_query: nil, mode: nil, params: nil,
    async: nil, project_id: nil, user_id: nil,
    called_from_async: false
  )
    # Don't call async from async (the point is we do the same processing in
    # async and not in async, and this function handles both that processing and
    # making the async call, so it's this much janky).
    async = false if called_from_async == true
    r = batch_response

    mode = mode.to_sym
    if ![:add, :remove].include?(mode)
      r.errors["unsupported mode '#{mode}'"] = 1
      return r
    end

    object_type = query.klass.base_class.name
    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(object_type)
      r.errors["#{object_type} can not have asserted environments"] = 1
      return r
    end

    if params[:uri].blank?
      r.errors['uri is required'] = 1
      return r
    end

    if mode == :add && params[:uri_label].blank?
      r.errors['uri_label is required'] = 1
      return r
    end

    if async && !called_from_async
      BatchByFilterScopeJob.perform_later(
        klass: self.name,
        hash_query:,
        mode:,
        params:,
        project_id:,
        user_id:
      )
    elsif mode == :add
      citation_params = params[:citation]&.to_h&.symbolize_keys&.compact_blank

      query.find_each do |o|
        add_by_batch(o, params[:uri], params[:uri_label], citation_params, r)
      end
    else
      remove_by_batch(query, params[:uri], r)
    end

    r
  end

  # @param citation_params [Hash, nil] attributes for a new Citation
  # @param r [BatchResponse] updated with the result for object
  def self.add_by_batch(object, uri, uri_label, citation_params, r)
    existing = AssertedEnvironment.find_by(
      asserted_environment_object: object, uri:
    )

    if existing
      if citation_params.blank?
        r.not_updated.push object.id
        r.validation_errors['asserted environment already exists'] += 1
        return
      end

      citation = existing.citations.create(citation_params)
      if citation.persisted?
        r.updated.push existing.id
      else
        r.not_updated.push object.id
        citation.errors.full_messages.each { |msg|
          r.validation_errors["citation not added to existing asserted environment: #{msg}"] += 1
        }
      end

      return
    end

    a = AssertedEnvironment.create(
      asserted_environment_object: object,
      uri:,
      uri_label:,
      citations_attributes: citation_params.present? ? [citation_params] : []
    )

    if a.persisted?
      r.updated.push a.id
    else
      r.not_updated.push object.id
      a.errors.full_messages.each { |msg|
        r.validation_errors[msg] += 1
      }
    end
  end

  # @param r [BatchResponse] updated with the result for each object in query
  def self.remove_by_batch(query, uri, r)
    existing_by_object_id = AssertedEnvironment
      .where(asserted_environment_object: query, uri:)
      .index_by(&:asserted_environment_object_id)

    query.find_each do |o|
      a = existing_by_object_id[o.id]

      if a.nil?
        r.not_updated.push o.id
        r.validation_errors['no asserted environment for this term'] += 1
      elsif a.destroy
        r.updated.push a.id
      else
        r.not_updated.push o.id
        a.errors.full_messages.each { |msg|
          r.validation_errors[msg] += 1
        }
      end
    end
  end

  protected

  def set_cached
    update_column(:cached, uri_label)
  end

  def asserted_environment_object_has_allowed_type
    t = asserted_environment_object_type&.to_s # STRING (not symbol)

    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(t)
      errors.add(t || :base, " - the type of this asserted environment's object can only be one of #{ENVIRONMENT_ASSERTABLE_TYPES}, not '#{t}'")
    end
  end

  def uri_is_an_envo_term
    return if uri.blank?

    errors.add(:uri, 'must be an ENVO term URI') unless Vendor::Envo.valid_uri?(uri)
  end
end

#position ⇒ Integer

for acts_as_list, scoped to the asserted environment object; larger values are refinements of smaller ones

Returns:

  • (Integer)


43
44
45
46
47
48
49
50
51
52
53
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
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'app/models/asserted_environment.rb', line 43

class AssertedEnvironment < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Identifiers
  # Override the :uri defined by Identifiers, which otherwise shadows the
  # asserted_environment's own `uri` column/validations. Bad, same as
  # AnatomicalPart.
  def uri
    self[:uri]
  end
  include Shared::Notes
  include Shared::Tags
  include Shared::DwcOccurrenceHooks
  include Shared::IsData
  include Shared::BatchByFilterScope

  acts_as_list scope: [:project_id, :asserted_environment_object_type, :asserted_environment_object_id]

  belongs_to :asserted_environment_object, polymorphic: true

  after_save :set_cached

  validates_presence_of :asserted_environment_object
  validates_presence_of :uri
  validates_presence_of :uri_label
  validate :asserted_environment_object_has_allowed_type
  validate :uri_is_an_envo_term
  validates_uniqueness_of :uri, scope: [
    :project_id, :asserted_environment_object_type, :asserted_environment_object_id
  ]

  # @return [Scope]
  #   DwcOccurrence records potentially affected by this asserted environment;
  #   only CollectingEvent assertions feed dwc:habitat (see
  #   Shared::Dwc::CollectingEventExtensions#dwc_habitat), so other object
  #   types have no corresponding DwcOccurrence records to rebuild.
  def dwc_occurrences
    return DwcOccurrence.none unless asserted_environment_object_type == 'CollectingEvent'

    a = DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .where(co: {collecting_event_id: asserted_environment_object_id})

    b = DwcOccurrence
      .joins("JOIN field_occurrences fo on dwc_occurrence_object_id = fo.id AND dwc_occurrence_object_type = 'FieldOccurrence'")
      .where(fo: {collecting_event_id: asserted_environment_object_id})

    ::Queries.union(DwcOccurrence, [a, b])
  end

  # mode :add - assert the ENVO term in params (uri, uri_label) on every object
  # returned by query, optionally with a citation (params[:citation], like
  # { source_id:, pages:, is_original: }).
  #
  # If the object already has an asserted environment for the term then the
  # citation, when provided and not already present, is added to that
  # existing asserted environment, otherwise the object is not updated.
  #
  # mode :remove - destroy the asserted environment for the ENVO term in
  # params (uri) on every object returned by query; objects without one are
  # not updated.
  #
  # updated are AssertedEnvironment ids (destroyed ones for :remove),
  # not_updated are object ids.
  def self.process_batch_by_filter_scope(
    batch_response: nil, query: nil, hash_query: nil, mode: nil, params: nil,
    async: nil, project_id: nil, user_id: nil,
    called_from_async: false
  )
    # Don't call async from async (the point is we do the same processing in
    # async and not in async, and this function handles both that processing and
    # making the async call, so it's this much janky).
    async = false if called_from_async == true
    r = batch_response

    mode = mode.to_sym
    if ![:add, :remove].include?(mode)
      r.errors["unsupported mode '#{mode}'"] = 1
      return r
    end

    object_type = query.klass.base_class.name
    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(object_type)
      r.errors["#{object_type} can not have asserted environments"] = 1
      return r
    end

    if params[:uri].blank?
      r.errors['uri is required'] = 1
      return r
    end

    if mode == :add && params[:uri_label].blank?
      r.errors['uri_label is required'] = 1
      return r
    end

    if async && !called_from_async
      BatchByFilterScopeJob.perform_later(
        klass: self.name,
        hash_query:,
        mode:,
        params:,
        project_id:,
        user_id:
      )
    elsif mode == :add
      citation_params = params[:citation]&.to_h&.symbolize_keys&.compact_blank

      query.find_each do |o|
        add_by_batch(o, params[:uri], params[:uri_label], citation_params, r)
      end
    else
      remove_by_batch(query, params[:uri], r)
    end

    r
  end

  # @param citation_params [Hash, nil] attributes for a new Citation
  # @param r [BatchResponse] updated with the result for object
  def self.add_by_batch(object, uri, uri_label, citation_params, r)
    existing = AssertedEnvironment.find_by(
      asserted_environment_object: object, uri:
    )

    if existing
      if citation_params.blank?
        r.not_updated.push object.id
        r.validation_errors['asserted environment already exists'] += 1
        return
      end

      citation = existing.citations.create(citation_params)
      if citation.persisted?
        r.updated.push existing.id
      else
        r.not_updated.push object.id
        citation.errors.full_messages.each { |msg|
          r.validation_errors["citation not added to existing asserted environment: #{msg}"] += 1
        }
      end

      return
    end

    a = AssertedEnvironment.create(
      asserted_environment_object: object,
      uri:,
      uri_label:,
      citations_attributes: citation_params.present? ? [citation_params] : []
    )

    if a.persisted?
      r.updated.push a.id
    else
      r.not_updated.push object.id
      a.errors.full_messages.each { |msg|
        r.validation_errors[msg] += 1
      }
    end
  end

  # @param r [BatchResponse] updated with the result for each object in query
  def self.remove_by_batch(query, uri, r)
    existing_by_object_id = AssertedEnvironment
      .where(asserted_environment_object: query, uri:)
      .index_by(&:asserted_environment_object_id)

    query.find_each do |o|
      a = existing_by_object_id[o.id]

      if a.nil?
        r.not_updated.push o.id
        r.validation_errors['no asserted environment for this term'] += 1
      elsif a.destroy
        r.updated.push a.id
      else
        r.not_updated.push o.id
        a.errors.full_messages.each { |msg|
          r.validation_errors[msg] += 1
        }
      end
    end
  end

  protected

  def set_cached
    update_column(:cached, uri_label)
  end

  def asserted_environment_object_has_allowed_type
    t = asserted_environment_object_type&.to_s # STRING (not symbol)

    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(t)
      errors.add(t || :base, " - the type of this asserted environment's object can only be one of #{ENVIRONMENT_ASSERTABLE_TYPES}, not '#{t}'")
    end
  end

  def uri_is_an_envo_term
    return if uri.blank?

    errors.add(:uri, 'must be an ENVO term URI') unless Vendor::Envo.valid_uri?(uri)
  end
end

#project_id ⇒ Integer

the project ID

Returns:

  • (Integer)


43
44
45
46
47
48
49
50
51
52
53
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
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'app/models/asserted_environment.rb', line 43

class AssertedEnvironment < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Identifiers
  # Override the :uri defined by Identifiers, which otherwise shadows the
  # asserted_environment's own `uri` column/validations. Bad, same as
  # AnatomicalPart.
  def uri
    self[:uri]
  end
  include Shared::Notes
  include Shared::Tags
  include Shared::DwcOccurrenceHooks
  include Shared::IsData
  include Shared::BatchByFilterScope

  acts_as_list scope: [:project_id, :asserted_environment_object_type, :asserted_environment_object_id]

  belongs_to :asserted_environment_object, polymorphic: true

  after_save :set_cached

  validates_presence_of :asserted_environment_object
  validates_presence_of :uri
  validates_presence_of :uri_label
  validate :asserted_environment_object_has_allowed_type
  validate :uri_is_an_envo_term
  validates_uniqueness_of :uri, scope: [
    :project_id, :asserted_environment_object_type, :asserted_environment_object_id
  ]

  # @return [Scope]
  #   DwcOccurrence records potentially affected by this asserted environment;
  #   only CollectingEvent assertions feed dwc:habitat (see
  #   Shared::Dwc::CollectingEventExtensions#dwc_habitat), so other object
  #   types have no corresponding DwcOccurrence records to rebuild.
  def dwc_occurrences
    return DwcOccurrence.none unless asserted_environment_object_type == 'CollectingEvent'

    a = DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .where(co: {collecting_event_id: asserted_environment_object_id})

    b = DwcOccurrence
      .joins("JOIN field_occurrences fo on dwc_occurrence_object_id = fo.id AND dwc_occurrence_object_type = 'FieldOccurrence'")
      .where(fo: {collecting_event_id: asserted_environment_object_id})

    ::Queries.union(DwcOccurrence, [a, b])
  end

  # mode :add - assert the ENVO term in params (uri, uri_label) on every object
  # returned by query, optionally with a citation (params[:citation], like
  # { source_id:, pages:, is_original: }).
  #
  # If the object already has an asserted environment for the term then the
  # citation, when provided and not already present, is added to that
  # existing asserted environment, otherwise the object is not updated.
  #
  # mode :remove - destroy the asserted environment for the ENVO term in
  # params (uri) on every object returned by query; objects without one are
  # not updated.
  #
  # updated are AssertedEnvironment ids (destroyed ones for :remove),
  # not_updated are object ids.
  def self.process_batch_by_filter_scope(
    batch_response: nil, query: nil, hash_query: nil, mode: nil, params: nil,
    async: nil, project_id: nil, user_id: nil,
    called_from_async: false
  )
    # Don't call async from async (the point is we do the same processing in
    # async and not in async, and this function handles both that processing and
    # making the async call, so it's this much janky).
    async = false if called_from_async == true
    r = batch_response

    mode = mode.to_sym
    if ![:add, :remove].include?(mode)
      r.errors["unsupported mode '#{mode}'"] = 1
      return r
    end

    object_type = query.klass.base_class.name
    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(object_type)
      r.errors["#{object_type} can not have asserted environments"] = 1
      return r
    end

    if params[:uri].blank?
      r.errors['uri is required'] = 1
      return r
    end

    if mode == :add && params[:uri_label].blank?
      r.errors['uri_label is required'] = 1
      return r
    end

    if async && !called_from_async
      BatchByFilterScopeJob.perform_later(
        klass: self.name,
        hash_query:,
        mode:,
        params:,
        project_id:,
        user_id:
      )
    elsif mode == :add
      citation_params = params[:citation]&.to_h&.symbolize_keys&.compact_blank

      query.find_each do |o|
        add_by_batch(o, params[:uri], params[:uri_label], citation_params, r)
      end
    else
      remove_by_batch(query, params[:uri], r)
    end

    r
  end

  # @param citation_params [Hash, nil] attributes for a new Citation
  # @param r [BatchResponse] updated with the result for object
  def self.add_by_batch(object, uri, uri_label, citation_params, r)
    existing = AssertedEnvironment.find_by(
      asserted_environment_object: object, uri:
    )

    if existing
      if citation_params.blank?
        r.not_updated.push object.id
        r.validation_errors['asserted environment already exists'] += 1
        return
      end

      citation = existing.citations.create(citation_params)
      if citation.persisted?
        r.updated.push existing.id
      else
        r.not_updated.push object.id
        citation.errors.full_messages.each { |msg|
          r.validation_errors["citation not added to existing asserted environment: #{msg}"] += 1
        }
      end

      return
    end

    a = AssertedEnvironment.create(
      asserted_environment_object: object,
      uri:,
      uri_label:,
      citations_attributes: citation_params.present? ? [citation_params] : []
    )

    if a.persisted?
      r.updated.push a.id
    else
      r.not_updated.push object.id
      a.errors.full_messages.each { |msg|
        r.validation_errors[msg] += 1
      }
    end
  end

  # @param r [BatchResponse] updated with the result for each object in query
  def self.remove_by_batch(query, uri, r)
    existing_by_object_id = AssertedEnvironment
      .where(asserted_environment_object: query, uri:)
      .index_by(&:asserted_environment_object_id)

    query.find_each do |o|
      a = existing_by_object_id[o.id]

      if a.nil?
        r.not_updated.push o.id
        r.validation_errors['no asserted environment for this term'] += 1
      elsif a.destroy
        r.updated.push a.id
      else
        r.not_updated.push o.id
        a.errors.full_messages.each { |msg|
          r.validation_errors[msg] += 1
        }
      end
    end
  end

  protected

  def set_cached
    update_column(:cached, uri_label)
  end

  def asserted_environment_object_has_allowed_type
    t = asserted_environment_object_type&.to_s # STRING (not symbol)

    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(t)
      errors.add(t || :base, " - the type of this asserted environment's object can only be one of #{ENVIRONMENT_ASSERTABLE_TYPES}, not '#{t}'")
    end
  end

  def uri_is_an_envo_term
    return if uri.blank?

    errors.add(:uri, 'must be an ENVO term URI') unless Vendor::Envo.valid_uri?(uri)
  end
end

#uri ⇒ Object

Override the :uri defined by Identifiers, which otherwise shadows the asserted_environment's own uri column/validations. Bad, same as AnatomicalPart.



43
44
45
46
47
48
49
50
51
52
53
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
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'app/models/asserted_environment.rb', line 43

class AssertedEnvironment < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Identifiers
  # Override the :uri defined by Identifiers, which otherwise shadows the
  # asserted_environment's own `uri` column/validations. Bad, same as
  # AnatomicalPart.
  def uri
    self[:uri]
  end
  include Shared::Notes
  include Shared::Tags
  include Shared::DwcOccurrenceHooks
  include Shared::IsData
  include Shared::BatchByFilterScope

  acts_as_list scope: [:project_id, :asserted_environment_object_type, :asserted_environment_object_id]

  belongs_to :asserted_environment_object, polymorphic: true

  after_save :set_cached

  validates_presence_of :asserted_environment_object
  validates_presence_of :uri
  validates_presence_of :uri_label
  validate :asserted_environment_object_has_allowed_type
  validate :uri_is_an_envo_term
  validates_uniqueness_of :uri, scope: [
    :project_id, :asserted_environment_object_type, :asserted_environment_object_id
  ]

  # @return [Scope]
  #   DwcOccurrence records potentially affected by this asserted environment;
  #   only CollectingEvent assertions feed dwc:habitat (see
  #   Shared::Dwc::CollectingEventExtensions#dwc_habitat), so other object
  #   types have no corresponding DwcOccurrence records to rebuild.
  def dwc_occurrences
    return DwcOccurrence.none unless asserted_environment_object_type == 'CollectingEvent'

    a = DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .where(co: {collecting_event_id: asserted_environment_object_id})

    b = DwcOccurrence
      .joins("JOIN field_occurrences fo on dwc_occurrence_object_id = fo.id AND dwc_occurrence_object_type = 'FieldOccurrence'")
      .where(fo: {collecting_event_id: asserted_environment_object_id})

    ::Queries.union(DwcOccurrence, [a, b])
  end

  # mode :add - assert the ENVO term in params (uri, uri_label) on every object
  # returned by query, optionally with a citation (params[:citation], like
  # { source_id:, pages:, is_original: }).
  #
  # If the object already has an asserted environment for the term then the
  # citation, when provided and not already present, is added to that
  # existing asserted environment, otherwise the object is not updated.
  #
  # mode :remove - destroy the asserted environment for the ENVO term in
  # params (uri) on every object returned by query; objects without one are
  # not updated.
  #
  # updated are AssertedEnvironment ids (destroyed ones for :remove),
  # not_updated are object ids.
  def self.process_batch_by_filter_scope(
    batch_response: nil, query: nil, hash_query: nil, mode: nil, params: nil,
    async: nil, project_id: nil, user_id: nil,
    called_from_async: false
  )
    # Don't call async from async (the point is we do the same processing in
    # async and not in async, and this function handles both that processing and
    # making the async call, so it's this much janky).
    async = false if called_from_async == true
    r = batch_response

    mode = mode.to_sym
    if ![:add, :remove].include?(mode)
      r.errors["unsupported mode '#{mode}'"] = 1
      return r
    end

    object_type = query.klass.base_class.name
    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(object_type)
      r.errors["#{object_type} can not have asserted environments"] = 1
      return r
    end

    if params[:uri].blank?
      r.errors['uri is required'] = 1
      return r
    end

    if mode == :add && params[:uri_label].blank?
      r.errors['uri_label is required'] = 1
      return r
    end

    if async && !called_from_async
      BatchByFilterScopeJob.perform_later(
        klass: self.name,
        hash_query:,
        mode:,
        params:,
        project_id:,
        user_id:
      )
    elsif mode == :add
      citation_params = params[:citation]&.to_h&.symbolize_keys&.compact_blank

      query.find_each do |o|
        add_by_batch(o, params[:uri], params[:uri_label], citation_params, r)
      end
    else
      remove_by_batch(query, params[:uri], r)
    end

    r
  end

  # @param citation_params [Hash, nil] attributes for a new Citation
  # @param r [BatchResponse] updated with the result for object
  def self.add_by_batch(object, uri, uri_label, citation_params, r)
    existing = AssertedEnvironment.find_by(
      asserted_environment_object: object, uri:
    )

    if existing
      if citation_params.blank?
        r.not_updated.push object.id
        r.validation_errors['asserted environment already exists'] += 1
        return
      end

      citation = existing.citations.create(citation_params)
      if citation.persisted?
        r.updated.push existing.id
      else
        r.not_updated.push object.id
        citation.errors.full_messages.each { |msg|
          r.validation_errors["citation not added to existing asserted environment: #{msg}"] += 1
        }
      end

      return
    end

    a = AssertedEnvironment.create(
      asserted_environment_object: object,
      uri:,
      uri_label:,
      citations_attributes: citation_params.present? ? [citation_params] : []
    )

    if a.persisted?
      r.updated.push a.id
    else
      r.not_updated.push object.id
      a.errors.full_messages.each { |msg|
        r.validation_errors[msg] += 1
      }
    end
  end

  # @param r [BatchResponse] updated with the result for each object in query
  def self.remove_by_batch(query, uri, r)
    existing_by_object_id = AssertedEnvironment
      .where(asserted_environment_object: query, uri:)
      .index_by(&:asserted_environment_object_id)

    query.find_each do |o|
      a = existing_by_object_id[o.id]

      if a.nil?
        r.not_updated.push o.id
        r.validation_errors['no asserted environment for this term'] += 1
      elsif a.destroy
        r.updated.push a.id
      else
        r.not_updated.push o.id
        a.errors.full_messages.each { |msg|
          r.validation_errors[msg] += 1
        }
      end
    end
  end

  protected

  def set_cached
    update_column(:cached, uri_label)
  end

  def asserted_environment_object_has_allowed_type
    t = asserted_environment_object_type&.to_s # STRING (not symbol)

    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(t)
      errors.add(t || :base, " - the type of this asserted environment's object can only be one of #{ENVIRONMENT_ASSERTABLE_TYPES}, not '#{t}'")
    end
  end

  def uri_is_an_envo_term
    return if uri.blank?

    errors.add(:uri, 'must be an ENVO term URI') unless Vendor::Envo.valid_uri?(uri)
  end
end

#uri_label ⇒ String

the label of the ENVO term at uri

Returns:

  • (String)


43
44
45
46
47
48
49
50
51
52
53
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
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'app/models/asserted_environment.rb', line 43

class AssertedEnvironment < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::DataAttributes
  include Shared::Identifiers
  # Override the :uri defined by Identifiers, which otherwise shadows the
  # asserted_environment's own `uri` column/validations. Bad, same as
  # AnatomicalPart.
  def uri
    self[:uri]
  end
  include Shared::Notes
  include Shared::Tags
  include Shared::DwcOccurrenceHooks
  include Shared::IsData
  include Shared::BatchByFilterScope

  acts_as_list scope: [:project_id, :asserted_environment_object_type, :asserted_environment_object_id]

  belongs_to :asserted_environment_object, polymorphic: true

  after_save :set_cached

  validates_presence_of :asserted_environment_object
  validates_presence_of :uri
  validates_presence_of :uri_label
  validate :asserted_environment_object_has_allowed_type
  validate :uri_is_an_envo_term
  validates_uniqueness_of :uri, scope: [
    :project_id, :asserted_environment_object_type, :asserted_environment_object_id
  ]

  # @return [Scope]
  #   DwcOccurrence records potentially affected by this asserted environment;
  #   only CollectingEvent assertions feed dwc:habitat (see
  #   Shared::Dwc::CollectingEventExtensions#dwc_habitat), so other object
  #   types have no corresponding DwcOccurrence records to rebuild.
  def dwc_occurrences
    return DwcOccurrence.none unless asserted_environment_object_type == 'CollectingEvent'

    a = DwcOccurrence
      .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
      .where(co: {collecting_event_id: asserted_environment_object_id})

    b = DwcOccurrence
      .joins("JOIN field_occurrences fo on dwc_occurrence_object_id = fo.id AND dwc_occurrence_object_type = 'FieldOccurrence'")
      .where(fo: {collecting_event_id: asserted_environment_object_id})

    ::Queries.union(DwcOccurrence, [a, b])
  end

  # mode :add - assert the ENVO term in params (uri, uri_label) on every object
  # returned by query, optionally with a citation (params[:citation], like
  # { source_id:, pages:, is_original: }).
  #
  # If the object already has an asserted environment for the term then the
  # citation, when provided and not already present, is added to that
  # existing asserted environment, otherwise the object is not updated.
  #
  # mode :remove - destroy the asserted environment for the ENVO term in
  # params (uri) on every object returned by query; objects without one are
  # not updated.
  #
  # updated are AssertedEnvironment ids (destroyed ones for :remove),
  # not_updated are object ids.
  def self.process_batch_by_filter_scope(
    batch_response: nil, query: nil, hash_query: nil, mode: nil, params: nil,
    async: nil, project_id: nil, user_id: nil,
    called_from_async: false
  )
    # Don't call async from async (the point is we do the same processing in
    # async and not in async, and this function handles both that processing and
    # making the async call, so it's this much janky).
    async = false if called_from_async == true
    r = batch_response

    mode = mode.to_sym
    if ![:add, :remove].include?(mode)
      r.errors["unsupported mode '#{mode}'"] = 1
      return r
    end

    object_type = query.klass.base_class.name
    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(object_type)
      r.errors["#{object_type} can not have asserted environments"] = 1
      return r
    end

    if params[:uri].blank?
      r.errors['uri is required'] = 1
      return r
    end

    if mode == :add && params[:uri_label].blank?
      r.errors['uri_label is required'] = 1
      return r
    end

    if async && !called_from_async
      BatchByFilterScopeJob.perform_later(
        klass: self.name,
        hash_query:,
        mode:,
        params:,
        project_id:,
        user_id:
      )
    elsif mode == :add
      citation_params = params[:citation]&.to_h&.symbolize_keys&.compact_blank

      query.find_each do |o|
        add_by_batch(o, params[:uri], params[:uri_label], citation_params, r)
      end
    else
      remove_by_batch(query, params[:uri], r)
    end

    r
  end

  # @param citation_params [Hash, nil] attributes for a new Citation
  # @param r [BatchResponse] updated with the result for object
  def self.add_by_batch(object, uri, uri_label, citation_params, r)
    existing = AssertedEnvironment.find_by(
      asserted_environment_object: object, uri:
    )

    if existing
      if citation_params.blank?
        r.not_updated.push object.id
        r.validation_errors['asserted environment already exists'] += 1
        return
      end

      citation = existing.citations.create(citation_params)
      if citation.persisted?
        r.updated.push existing.id
      else
        r.not_updated.push object.id
        citation.errors.full_messages.each { |msg|
          r.validation_errors["citation not added to existing asserted environment: #{msg}"] += 1
        }
      end

      return
    end

    a = AssertedEnvironment.create(
      asserted_environment_object: object,
      uri:,
      uri_label:,
      citations_attributes: citation_params.present? ? [citation_params] : []
    )

    if a.persisted?
      r.updated.push a.id
    else
      r.not_updated.push object.id
      a.errors.full_messages.each { |msg|
        r.validation_errors[msg] += 1
      }
    end
  end

  # @param r [BatchResponse] updated with the result for each object in query
  def self.remove_by_batch(query, uri, r)
    existing_by_object_id = AssertedEnvironment
      .where(asserted_environment_object: query, uri:)
      .index_by(&:asserted_environment_object_id)

    query.find_each do |o|
      a = existing_by_object_id[o.id]

      if a.nil?
        r.not_updated.push o.id
        r.validation_errors['no asserted environment for this term'] += 1
      elsif a.destroy
        r.updated.push a.id
      else
        r.not_updated.push o.id
        a.errors.full_messages.each { |msg|
          r.validation_errors[msg] += 1
        }
      end
    end
  end

  protected

  def set_cached
    update_column(:cached, uri_label)
  end

  def asserted_environment_object_has_allowed_type
    t = asserted_environment_object_type&.to_s # STRING (not symbol)

    if !ENVIRONMENT_ASSERTABLE_TYPES.include?(t)
      errors.add(t || :base, " - the type of this asserted environment's object can only be one of #{ENVIRONMENT_ASSERTABLE_TYPES}, not '#{t}'")
    end
  end

  def uri_is_an_envo_term
    return if uri.blank?

    errors.add(:uri, 'must be an ENVO term URI') unless Vendor::Envo.valid_uri?(uri)
  end
end

Class Method Details

.add_by_batch(object, uri, uri_label, citation_params, r) ⇒ Object

Parameters:

  • citation_params (Hash, nil) —

    attributes for a new Citation

  • r (BatchResponse) —

    updated with the result for object



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'app/models/asserted_environment.rb', line 165

def self.add_by_batch(object, uri, uri_label, citation_params, r)
  existing = AssertedEnvironment.find_by(
    asserted_environment_object: object, uri:
  )

  if existing
    if citation_params.blank?
      r.not_updated.push object.id
      r.validation_errors['asserted environment already exists'] += 1
      return
    end

    citation = existing.citations.create(citation_params)
    if citation.persisted?
      r.updated.push existing.id
    else
      r.not_updated.push object.id
      citation.errors.full_messages.each { |msg|
        r.validation_errors["citation not added to existing asserted environment: #{msg}"] += 1
      }
    end

    return
  end

  a = AssertedEnvironment.create(
    asserted_environment_object: object,
    uri:,
    uri_label:,
    citations_attributes: citation_params.present? ? [citation_params] : []
  )

  if a.persisted?
    r.updated.push a.id
  else
    r.not_updated.push object.id
    a.errors.full_messages.each { |msg|
      r.validation_errors[msg] += 1
    }
  end
end

.process_batch_by_filter_scope(batch_response: nil, query: nil, hash_query: nil, mode: nil, params: nil, async: nil, project_id: nil, user_id: nil, called_from_async: false) ⇒ Object

mode :add - assert the ENVO term in params (uri, uri_label) on every object returned by query, optionally with a citation (params, like { source_id:, pages:, is_original: }).

If the object already has an asserted environment for the term then the citation, when provided and not already present, is added to that existing asserted environment, otherwise the object is not updated.

mode :remove - destroy the asserted environment for the ENVO term in params (uri) on every object returned by query; objects without one are not updated.

updated are AssertedEnvironment ids (destroyed ones for :remove), not_updated are object ids.



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
# File 'app/models/asserted_environment.rb', line 108

def self.process_batch_by_filter_scope(
  batch_response: nil, query: nil, hash_query: nil, mode: nil, params: nil,
  async: nil, project_id: nil, user_id: nil,
  called_from_async: false
)
  # Don't call async from async (the point is we do the same processing in
  # async and not in async, and this function handles both that processing and
  # making the async call, so it's this much janky).
  async = false if called_from_async == true
  r = batch_response

  mode = mode.to_sym
  if ![:add, :remove].include?(mode)
    r.errors["unsupported mode '#{mode}'"] = 1
    return r
  end

  object_type = query.klass.base_class.name
  if !ENVIRONMENT_ASSERTABLE_TYPES.include?(object_type)
    r.errors["#{object_type} can not have asserted environments"] = 1
    return r
  end

  if params[:uri].blank?
    r.errors['uri is required'] = 1
    return r
  end

  if mode == :add && params[:uri_label].blank?
    r.errors['uri_label is required'] = 1
    return r
  end

  if async && !called_from_async
    BatchByFilterScopeJob.perform_later(
      klass: self.name,
      hash_query:,
      mode:,
      params:,
      project_id:,
      user_id:
    )
  elsif mode == :add
    citation_params = params[:citation]&.to_h&.symbolize_keys&.compact_blank

    query.find_each do |o|
      add_by_batch(o, params[:uri], params[:uri_label], citation_params, r)
    end
  else
    remove_by_batch(query, params[:uri], r)
  end

  r
end

.remove_by_batch(query, uri, r) ⇒ Object

Parameters:

  • r (BatchResponse) —

    updated with the result for each object in query



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'app/models/asserted_environment.rb', line 208

def self.remove_by_batch(query, uri, r)
  existing_by_object_id = AssertedEnvironment
    .where(asserted_environment_object: query, uri:)
    .index_by(&:asserted_environment_object_id)

  query.find_each do |o|
    a = existing_by_object_id[o.id]

    if a.nil?
      r.not_updated.push o.id
      r.validation_errors['no asserted environment for this term'] += 1
    elsif a.destroy
      r.updated.push a.id
    else
      r.not_updated.push o.id
      a.errors.full_messages.each { |msg|
        r.validation_errors[msg] += 1
      }
    end
  end
end

Instance Method Details

#asserted_environment_object_has_allowed_type ⇒ Object (protected)



236
237
238
239
240
241
242
# File 'app/models/asserted_environment.rb', line 236

def asserted_environment_object_has_allowed_type
  t = asserted_environment_object_type&.to_s # STRING (not symbol)

  if !ENVIRONMENT_ASSERTABLE_TYPES.include?(t)
    errors.add(t || :base, " - the type of this asserted environment's object can only be one of #{ENVIRONMENT_ASSERTABLE_TYPES}, not '#{t}'")
  end
end

#dwc_occurrences ⇒ Scope

Returns DwcOccurrence records potentially affected by this asserted environment; only CollectingEvent assertions feed dwc:habitat (see Shared::Dwc::CollectingEventExtensions#dwc_habitat), so other object types have no corresponding DwcOccurrence records to rebuild.

Returns:

  • (Scope) —

    DwcOccurrence records potentially affected by this asserted environment; only CollectingEvent assertions feed dwc:habitat (see Shared::Dwc::CollectingEventExtensions#dwc_habitat), so other object types have no corresponding DwcOccurrence records to rebuild.



80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'app/models/asserted_environment.rb', line 80

def dwc_occurrences
  return DwcOccurrence.none unless asserted_environment_object_type == 'CollectingEvent'

  a = DwcOccurrence
    .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'")
    .where(co: {collecting_event_id: asserted_environment_object_id})

  b = DwcOccurrence
    .joins("JOIN field_occurrences fo on dwc_occurrence_object_id = fo.id AND dwc_occurrence_object_type = 'FieldOccurrence'")
    .where(fo: {collecting_event_id: asserted_environment_object_id})

  ::Queries.union(DwcOccurrence, [a, b])
end

#set_cached ⇒ Object (protected)



232
233
234
# File 'app/models/asserted_environment.rb', line 232

def set_cached
  update_column(:cached, uri_label)
end

#uri_is_an_envo_term ⇒ Object (protected)



244
245
246
247
248
# File 'app/models/asserted_environment.rb', line 244

def uri_is_an_envo_term
  return if uri.blank?

  errors.add(:uri, 'must be an ENVO term URI') unless Vendor::Envo.valid_uri?(uri)
end