Class: Attribution
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Attribution
- Includes:
- Housekeeping, Shared::BatchByFilterScope, Shared::Confidences, Shared::IsData, Shared::Notes, Shared::PolymorphicAnnotator, Shared::Tags
- Defined in:
- app/models/attribution.rb
Overview
An attribution is an explicit assertion of who is responsible for different attributes of the content of tied data.
Constant Summary collapse
- ATTRIBUTION_ROLES =
TODO: Consider DRYing with Source roles.
[ :creator, :editor, :owner, :copyright_holder ].freeze
Instance Attribute Summary collapse
-
#attribution_object_id ⇒ Integer
Polymorphic attribution object id.
-
#attribution_object_type ⇒ String
Polymorphic attribution object type.
-
#copyright_year ⇒ Integer
4 digit year of copyright.
-
#license ⇒ String
A creative-commons copyright.
Class Method Summary collapse
-
.apply_add_attribution(attribution, new_attribution) ⇒ Object
private
This is add, so only add data from new_attribution that will be ‘new’ in attribution (the not-new case is ‘replace’).
-
.apply_remove_attribution(attribution, remove_attribution) ⇒ Object
private
Removes those attribution elements from remove_attribution that exist on attribution.
-
.apply_replace_attribution(attribution, replace_attribution, new_attribution) ⇒ Object
private
Assumes 1:1 replacement (enforced by UI): - If replacing license, new_attrs must have a license - If replacing copyright_year, new_attrs must have a copyright_year - If replacing a role, new_attrs must have exactly one role of the same type.
- .attribution_data_present?(attribution) ⇒ Boolean private
- .attribution_roles(attribution) ⇒ Object private
-
.attribution_scope_for(query, attribution) ⇒ Scope
private
data that matches attribution.
-
.attrs_to_clear(attribution, remove_attrs) ⇒ Object
private
Returns a hash with :license and/or :copyright_year set to nil for those that match between attribution and remove_attrs.
- .matchable_roles(attribution) ⇒ Object private
-
.matching_role_ids(attribution, remove_roles) ⇒ Object
private
Returns an array of role IDs from attribution that match remove_roles.
- .operation_types(attribution) ⇒ Object private
- .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
-
.role_replacement_actions(attribution, replace_roles, new_roles) ⇒ Object
private
Returns [roles_to_update, roles_to_delete] arrays for role replacement.
- .validate_single_attribution(attribution, response) ⇒ Object private
- .validate_single_replace(replace_attribution, to_attribution, response) ⇒ Object private
Instance Method Summary collapse
- #some_data_provided ⇒ Object protected
- #some_roles_present ⇒ Object protected
Methods included from Shared::PolymorphicAnnotator
#annotated_object_is_persisted?
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::Confidences
Methods included from Shared::Notes
#concatenated_notes_string, #reject_notes
Methods included from Housekeeping
#has_polymorphic_relationship?
Methods inherited from ApplicationRecord
Instance Attribute Details
#attribution_object_id ⇒ Integer
Returns Polymorphic attribution object id.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 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 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 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 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 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 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 |
# File 'app/models/attribution.rb', line 18 class Attribution < ApplicationRecord include Housekeeping include Shared::BatchByFilterScope include Shared::Notes include Shared::Confidences include Shared::Tags include Shared::IsData include Shared::PolymorphicAnnotator polymorphic_annotates('attribution_object', inverse_of: :attribution) # TODO: Consider DRYing with Source roles. ATTRIBUTION_ROLES = [ :creator, :editor, :owner, :copyright_holder ].freeze ATTRIBUTION_ROLES.each do |r| role_name = "#{r}_roles".to_sym role_person = "attribution_#{r.to_s.pluralize}".to_sym role_organization = "attribution_organization_#{r.to_s.pluralize}".to_sym has_many role_name, -> { order('roles.position ASC') }, class_name: "Attribution#{r.to_s.camelize}", as: :role_object, inverse_of: :role_object has_many role_person, -> { order('roles.position ASC') }, through: role_name, source: :person, validate: true has_many role_organization, -> { order('roles.position ASC') }, through: role_name, source: :organization, validate: true accepts_nested_attributes_for role_name, allow_destroy: true accepts_nested_attributes_for role_person accepts_nested_attributes_for role_organization end validates :attribution_object_id, uniqueness: { scope: [:attribution_object_type, :project_id] } validates :license, inclusion: {in: CREATIVE_COMMONS_LICENSES.keys}, allow_nil: true validates :copyright_year, date_year: { min_year: 1000, max_year: Time.zone.now.year + 5, message: 'must be an integer greater than 999 and no more than 5 years in the future'} validate :some_data_provided 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 case mode.to_sym when :add attribution = params[:attribution].presence || {} unless attribution_data_present?(attribution) r.errors['no attribution provided'] = 1 return r end unless validate_single_attribution(attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_object_type = query.klass.name query.find_in_batches do |records| record_ids = records.map(&:id) existing_map = Attribution.where( attribution_object_id: record_ids, attribution_object_type: ).index_by(&:attribution_object_id) records.each do |record| existing = existing_map[record.id] if existing result = apply_add_attribution(existing, attribution) if result == :updated r.updated.push existing.id else r.not_updated.push existing.id end else o_params = attribution.merge({ attribution_object_id: record.id, attribution_object_type: }) created = Attribution.create(o_params) if created.valid? r.updated.push created.id else r.not_updated.push nil # no id to add end end end end end when :remove attribution = params[:attribution].presence || {} unless attribution_data_present?(attribution) r.errors['no attribution criteria provided'] = 1 return r end unless validate_single_attribution(attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_scope_for(query, attribution).find_each do |o| result = apply_remove_attribution(o, attribution) if result == :updated || result == :destroyed r.updated.push o.id else r.not_updated.push o.id end end missing = r.total_attempted - r.updated.length - r.not_updated.length r.not_updated.concat([nil] * missing) if missing.positive? end when :replace replace_attribution = params[:replace_attribution].presence || {} to_attribution = params[:attribution].presence || {} unless attribution_data_present?(replace_attribution) r.errors['no replace attribution criteria provided'] = 1 return r end unless attribution_data_present?(to_attribution) r.errors['no replacement attribution provided'] = 1 return r end unless validate_single_replace(replace_attribution, to_attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_scope_for(query, replace_attribution).find_each do |o| result = apply_replace_attribution(o, replace_attribution, to_attribution) if result == :updated || result == :destroyed r.updated.push o.id else r.not_updated.push o.id end end missing = r.total_attempted - r.updated.length - r.not_updated.length r.not_updated.concat([nil] * missing) if missing.positive? end end r end protected def some_roles_present ATTRIBUTION_ROLES.each do |r| return true if send("#{r}_roles".to_sym).any? end if self.roles.any? self.roles.each do |r| return true if r.type.present? && (r.person_id.present? || r.organization_id.present?) end end false end def some_data_provided if license.blank? && copyright_year.blank? && !some_roles_present errors.add(:base, 'no attribution metadata') end end private def self.attribution_data_present?(attribution) attrs = attribution.to_h.symbolize_keys roles = attribution_roles(attrs) attrs[:license].present? || attrs[:copyright_year].present? || roles.any? end def self.attribution_roles(attribution) attrs = attribution.to_h.symbolize_keys roles = attrs[:roles_attributes] || [] Array(roles) .map { |role| role&.to_h&.symbolize_keys } .compact end def self.operation_types(attribution) attrs = attribution.to_h.symbolize_keys roles = matchable_roles(attrs) types = [] types << :license if attrs[:license].present? types << :copyright_year if attrs[:copyright_year].present? types << :roles if roles.any? [types, roles] end def self.validate_single_attribution(attribution, response) types, roles = operation_types(attribution) if types.empty? response.errors['no valid attribution attribute or role provided'] = 1 return false end if types.length > 1 response.errors['only one attribution attribute or role may be specified'] = 1 return false end if types.first == :roles && roles.length != 1 response.errors['exactly one role must be provided'] = 1 return false end true end def self.validate_single_replace(replace_attribution, to_attribution, response) replace_types, replace_roles = operation_types(replace_attribution) to_types, to_roles = operation_types(to_attribution) if replace_types.length != 1 || to_types.length != 1 response.errors['replace must target exactly one attribute or role'] = 1 return false end if replace_types.first != to_types.first response.errors['replace attribute type must match replacement type'] = 1 return false end if replace_types.first == :roles if replace_roles.length != 1 || to_roles.length != 1 response.errors['exactly one role must be provided for replacement'] = 1 return false end if replace_roles.first[:type] != to_roles.first[:type] response.errors['role replacement types must match'] = 1 return false end end true end def self.matchable_roles(attribution) attribution_roles(attribution).select do |role| role[:type].present? && (role[:person_id].present? || role[:organization_id].present?) end end # @param query [Scope] the base scope to filter # @param attribution [Hash, ActionController::Parameters] # @return [Scope] of all Attributions associated with query that have some # data that matches attribution. def self.attribution_scope_for(query, attribution) attrs = attribution.to_h.symbolize_keys scope = Attribution.includes(:roles).where( attribution_object_id: query.select(:id), attribution_object_type: query.klass.name ) scope = scope.where(license: attrs[:license]) if attrs[:license].present? scope = scope.where(copyright_year: attrs[:copyright_year]) if attrs[:copyright_year].present? matchable_roles(attrs).each do |role| role_type = role[:type] next if role_type.blank? role_scope = Role.where(role_object_type: 'Attribution', type: role_type) if role[:person_id].present? role_scope = role_scope.where(person_id: role[:person_id]) elsif role[:organization_id].present? role_scope = role_scope.where(organization_id: role[:organization_id]) else next end scope = scope.where(id: role_scope.select(:role_object_id)) end scope end # Assumes 1:1 replacement (enforced by UI): # - If replacing license, new_attrs must have a license # - If replacing copyright_year, new_attrs must have a copyright_year # - If replacing a role, new_attrs must have exactly one role of the same type def self.apply_replace_attribution( attribution, replace_attribution, new_attribution ) replace_attrs = replace_attribution.to_h.symbolize_keys new_attrs = new_attribution.to_h.symbolize_keys update_payload = {} update_payload[:license] = new_attrs[:license] if new_attrs[:license].present? && replace_attrs[:license].present? && replace_attrs[:license] == attribution.license update_payload[:copyright_year] = new_attrs[:copyright_year] if new_attrs[:copyright_year].present? && replace_attrs[:copyright_year].present? && replace_attrs[:copyright_year] == attribution.copyright_year replace_roles = matchable_roles(replace_attrs) new_roles = matchable_roles(new_attrs) roles_to_update, roles_to_delete = role_replacement_actions(attribution, replace_roles, new_roles) return :noop if update_payload.empty? && roles_to_update.empty? && roles_to_delete.empty? Attribution.transaction do roles_to_update.each do |item| role_update = {} if item[:new_role][:person_id].present? role_update[:person_id] = item[:new_role][:person_id] role_update[:organization_id] = nil elsif item[:new_role][:organization_id].present? role_update[:organization_id] = item[:new_role][:organization_id] role_update[:person_id] = nil end unless item[:role].update(role_update) raise ActiveRecord::Rollback end end roles_to_delete.each do |role| unless role.destroy raise ActiveRecord::Rollback end end unless update_payload.empty? unless attribution.update(update_payload) raise ActiveRecord::Rollback end end end attribution.errors.any? ? :invalid : :updated end # Returns [roles_to_update, roles_to_delete] arrays for role replacement. # roles_to_update contains { role:, new_role: } hashes for roles to update in # place. roles_to_delete contains roles to delete (when "to" role already # exists). # Raises TaxonWorks::Error if replace_roles and new_roles are mismatched or # have duplicates. def self.role_replacement_actions(attribution, replace_roles, new_roles) if replace_roles.size != new_roles.size raise TaxonWorks::Error, "Mismatched role replacement: each 'from' role must have a corresponding 'to' role - from: #{replace_roles.inspect}, to: #{new_roles.inspect}" end from_keys = replace_roles.map { |r| [r[:type], r[:person_id], r[:organization_id]] } if from_keys.size != from_keys.uniq.size raise TaxonWorks::Error, "Duplicate 'from' role in replacement request: #{replace_roles.inspect}" end roles_to_update = [] roles_to_delete = [] replace_roles.each_with_index do |replace_role, index| new_role = new_roles[index] from_role = attribution.roles.find do |role| role.type == replace_role[:type] && ((replace_role[:person_id].present? && role.person_id == replace_role[:person_id]) || (replace_role[:organization_id].present? && role.organization_id == replace_role[:organization_id])) end next unless from_role to_role_exists = attribution.roles.any? do |role| role.type == new_role[:type] && ((new_role[:person_id].present? && role.person_id == new_role[:person_id]) || (new_role[:organization_id].present? && role.organization_id == new_role[:organization_id])) end if to_role_exists # Target already exists, just delete the "from" role. roles_to_delete << from_role else roles_to_update << { role: from_role, new_role: new_role } end end [roles_to_update, roles_to_delete] end # Returns a hash with :license and/or :copyright_year set to nil for those # that match between attribution and remove_attrs. def self.attrs_to_clear(attribution, remove_attrs) payload = {} payload[:license] = nil if remove_attrs[:license].present? && remove_attrs[:license] == attribution.license payload[:copyright_year] = nil if remove_attrs[:copyright_year].present? && remove_attrs[:copyright_year] == attribution.copyright_year payload end # Returns an array of role IDs from attribution that match remove_roles. def self.matching_role_ids(attribution, remove_roles) return [] unless remove_roles.any? ids = [] attribution.roles.each do |role| remove_roles.each do |remove_role| next unless role.type == remove_role[:type] if remove_role[:person_id].present? && role.person_id == remove_role[:person_id] ids << role.id elsif remove_role[:organization_id].present? && role.organization_id == remove_role[:organization_id] ids << role.id end end end ids.uniq end # Removes those attribution elements from remove_attribution that exist on # attribution. def self.apply_remove_attribution(attribution, remove_attribution) remove_attrs = remove_attribution.to_h.symbolize_keys remove_roles = matchable_roles(remove_attrs) update_payload = attrs_to_clear(attribution, remove_attrs) roles_to_remove = matching_role_ids(attribution, remove_roles) return :noop if update_payload.empty? && roles_to_remove.empty? final_license = update_payload.key?(:license) ? nil : attribution.license final_year = update_payload.key?(:copyright_year) ? nil : attribution.copyright_year final_roles_present = if roles_to_remove.any? (attribution.roles.size - roles_to_remove.size) > 0 else attribution.roles.any? end should_destroy = final_license.blank? && final_year.blank? && !final_roles_present result = nil Attribution.transaction do if roles_to_remove.any? destroyed_roles = attribution.roles.where(id: roles_to_remove).destroy_all # destroy_all fails silently, so check its work. unless destroyed_roles.all?(&:destroyed?) raise ActiveRecord::Rollback end end if should_destroy if attribution.destroy result = :destroyed else raise ActiveRecord::Rollback end else unless update_payload.empty? unless attribution.update(update_payload) raise ActiveRecord::Rollback end end result = :updated end end result || :invalid end # This is add, so only add data from new_attribution that will be 'new' in # attribution (the not-new case is 'replace'). def self.apply_add_attribution(attribution, new_attribution) attrs = new_attribution.to_h.symbolize_keys roles_attributes = attribution_roles(attrs) update_payload = {} update_payload[:license] = attrs[:license] if attribution.license.blank? && attrs[:license].present? update_payload[:copyright_year] = attrs[:copyright_year] if attribution.copyright_year.blank? && attrs[:copyright_year].present? if roles_attributes.any? existing_roles = attribution.roles.map do |role| [role.type, role.person_id, role.organization_id] end.to_set new_roles = roles_attributes.map do |role| role.slice(:type, :person_id, :organization_id).compact end # Roles are has_many, so we add as long as the new role doesn't already # exist. unique_roles = new_roles.reject do |role| existing_roles.include?([role[:type], role[:person_id], role[:organization_id]]) end if unique_roles.any? update_payload[:roles_attributes] = unique_roles end end return :noop if update_payload.empty? attribution.update(update_payload) attribution.errors.any? ? :invalid : :updated end end |
#attribution_object_type ⇒ String
Returns Polymorphic attribution object type.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 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 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 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 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 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 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 |
# File 'app/models/attribution.rb', line 18 class Attribution < ApplicationRecord include Housekeeping include Shared::BatchByFilterScope include Shared::Notes include Shared::Confidences include Shared::Tags include Shared::IsData include Shared::PolymorphicAnnotator polymorphic_annotates('attribution_object', inverse_of: :attribution) # TODO: Consider DRYing with Source roles. ATTRIBUTION_ROLES = [ :creator, :editor, :owner, :copyright_holder ].freeze ATTRIBUTION_ROLES.each do |r| role_name = "#{r}_roles".to_sym role_person = "attribution_#{r.to_s.pluralize}".to_sym role_organization = "attribution_organization_#{r.to_s.pluralize}".to_sym has_many role_name, -> { order('roles.position ASC') }, class_name: "Attribution#{r.to_s.camelize}", as: :role_object, inverse_of: :role_object has_many role_person, -> { order('roles.position ASC') }, through: role_name, source: :person, validate: true has_many role_organization, -> { order('roles.position ASC') }, through: role_name, source: :organization, validate: true accepts_nested_attributes_for role_name, allow_destroy: true accepts_nested_attributes_for role_person accepts_nested_attributes_for role_organization end validates :attribution_object_id, uniqueness: { scope: [:attribution_object_type, :project_id] } validates :license, inclusion: {in: CREATIVE_COMMONS_LICENSES.keys}, allow_nil: true validates :copyright_year, date_year: { min_year: 1000, max_year: Time.zone.now.year + 5, message: 'must be an integer greater than 999 and no more than 5 years in the future'} validate :some_data_provided 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 case mode.to_sym when :add attribution = params[:attribution].presence || {} unless attribution_data_present?(attribution) r.errors['no attribution provided'] = 1 return r end unless validate_single_attribution(attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_object_type = query.klass.name query.find_in_batches do |records| record_ids = records.map(&:id) existing_map = Attribution.where( attribution_object_id: record_ids, attribution_object_type: ).index_by(&:attribution_object_id) records.each do |record| existing = existing_map[record.id] if existing result = apply_add_attribution(existing, attribution) if result == :updated r.updated.push existing.id else r.not_updated.push existing.id end else o_params = attribution.merge({ attribution_object_id: record.id, attribution_object_type: }) created = Attribution.create(o_params) if created.valid? r.updated.push created.id else r.not_updated.push nil # no id to add end end end end end when :remove attribution = params[:attribution].presence || {} unless attribution_data_present?(attribution) r.errors['no attribution criteria provided'] = 1 return r end unless validate_single_attribution(attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_scope_for(query, attribution).find_each do |o| result = apply_remove_attribution(o, attribution) if result == :updated || result == :destroyed r.updated.push o.id else r.not_updated.push o.id end end missing = r.total_attempted - r.updated.length - r.not_updated.length r.not_updated.concat([nil] * missing) if missing.positive? end when :replace replace_attribution = params[:replace_attribution].presence || {} to_attribution = params[:attribution].presence || {} unless attribution_data_present?(replace_attribution) r.errors['no replace attribution criteria provided'] = 1 return r end unless attribution_data_present?(to_attribution) r.errors['no replacement attribution provided'] = 1 return r end unless validate_single_replace(replace_attribution, to_attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_scope_for(query, replace_attribution).find_each do |o| result = apply_replace_attribution(o, replace_attribution, to_attribution) if result == :updated || result == :destroyed r.updated.push o.id else r.not_updated.push o.id end end missing = r.total_attempted - r.updated.length - r.not_updated.length r.not_updated.concat([nil] * missing) if missing.positive? end end r end protected def some_roles_present ATTRIBUTION_ROLES.each do |r| return true if send("#{r}_roles".to_sym).any? end if self.roles.any? self.roles.each do |r| return true if r.type.present? && (r.person_id.present? || r.organization_id.present?) end end false end def some_data_provided if license.blank? && copyright_year.blank? && !some_roles_present errors.add(:base, 'no attribution metadata') end end private def self.attribution_data_present?(attribution) attrs = attribution.to_h.symbolize_keys roles = attribution_roles(attrs) attrs[:license].present? || attrs[:copyright_year].present? || roles.any? end def self.attribution_roles(attribution) attrs = attribution.to_h.symbolize_keys roles = attrs[:roles_attributes] || [] Array(roles) .map { |role| role&.to_h&.symbolize_keys } .compact end def self.operation_types(attribution) attrs = attribution.to_h.symbolize_keys roles = matchable_roles(attrs) types = [] types << :license if attrs[:license].present? types << :copyright_year if attrs[:copyright_year].present? types << :roles if roles.any? [types, roles] end def self.validate_single_attribution(attribution, response) types, roles = operation_types(attribution) if types.empty? response.errors['no valid attribution attribute or role provided'] = 1 return false end if types.length > 1 response.errors['only one attribution attribute or role may be specified'] = 1 return false end if types.first == :roles && roles.length != 1 response.errors['exactly one role must be provided'] = 1 return false end true end def self.validate_single_replace(replace_attribution, to_attribution, response) replace_types, replace_roles = operation_types(replace_attribution) to_types, to_roles = operation_types(to_attribution) if replace_types.length != 1 || to_types.length != 1 response.errors['replace must target exactly one attribute or role'] = 1 return false end if replace_types.first != to_types.first response.errors['replace attribute type must match replacement type'] = 1 return false end if replace_types.first == :roles if replace_roles.length != 1 || to_roles.length != 1 response.errors['exactly one role must be provided for replacement'] = 1 return false end if replace_roles.first[:type] != to_roles.first[:type] response.errors['role replacement types must match'] = 1 return false end end true end def self.matchable_roles(attribution) attribution_roles(attribution).select do |role| role[:type].present? && (role[:person_id].present? || role[:organization_id].present?) end end # @param query [Scope] the base scope to filter # @param attribution [Hash, ActionController::Parameters] # @return [Scope] of all Attributions associated with query that have some # data that matches attribution. def self.attribution_scope_for(query, attribution) attrs = attribution.to_h.symbolize_keys scope = Attribution.includes(:roles).where( attribution_object_id: query.select(:id), attribution_object_type: query.klass.name ) scope = scope.where(license: attrs[:license]) if attrs[:license].present? scope = scope.where(copyright_year: attrs[:copyright_year]) if attrs[:copyright_year].present? matchable_roles(attrs).each do |role| role_type = role[:type] next if role_type.blank? role_scope = Role.where(role_object_type: 'Attribution', type: role_type) if role[:person_id].present? role_scope = role_scope.where(person_id: role[:person_id]) elsif role[:organization_id].present? role_scope = role_scope.where(organization_id: role[:organization_id]) else next end scope = scope.where(id: role_scope.select(:role_object_id)) end scope end # Assumes 1:1 replacement (enforced by UI): # - If replacing license, new_attrs must have a license # - If replacing copyright_year, new_attrs must have a copyright_year # - If replacing a role, new_attrs must have exactly one role of the same type def self.apply_replace_attribution( attribution, replace_attribution, new_attribution ) replace_attrs = replace_attribution.to_h.symbolize_keys new_attrs = new_attribution.to_h.symbolize_keys update_payload = {} update_payload[:license] = new_attrs[:license] if new_attrs[:license].present? && replace_attrs[:license].present? && replace_attrs[:license] == attribution.license update_payload[:copyright_year] = new_attrs[:copyright_year] if new_attrs[:copyright_year].present? && replace_attrs[:copyright_year].present? && replace_attrs[:copyright_year] == attribution.copyright_year replace_roles = matchable_roles(replace_attrs) new_roles = matchable_roles(new_attrs) roles_to_update, roles_to_delete = role_replacement_actions(attribution, replace_roles, new_roles) return :noop if update_payload.empty? && roles_to_update.empty? && roles_to_delete.empty? Attribution.transaction do roles_to_update.each do |item| role_update = {} if item[:new_role][:person_id].present? role_update[:person_id] = item[:new_role][:person_id] role_update[:organization_id] = nil elsif item[:new_role][:organization_id].present? role_update[:organization_id] = item[:new_role][:organization_id] role_update[:person_id] = nil end unless item[:role].update(role_update) raise ActiveRecord::Rollback end end roles_to_delete.each do |role| unless role.destroy raise ActiveRecord::Rollback end end unless update_payload.empty? unless attribution.update(update_payload) raise ActiveRecord::Rollback end end end attribution.errors.any? ? :invalid : :updated end # Returns [roles_to_update, roles_to_delete] arrays for role replacement. # roles_to_update contains { role:, new_role: } hashes for roles to update in # place. roles_to_delete contains roles to delete (when "to" role already # exists). # Raises TaxonWorks::Error if replace_roles and new_roles are mismatched or # have duplicates. def self.role_replacement_actions(attribution, replace_roles, new_roles) if replace_roles.size != new_roles.size raise TaxonWorks::Error, "Mismatched role replacement: each 'from' role must have a corresponding 'to' role - from: #{replace_roles.inspect}, to: #{new_roles.inspect}" end from_keys = replace_roles.map { |r| [r[:type], r[:person_id], r[:organization_id]] } if from_keys.size != from_keys.uniq.size raise TaxonWorks::Error, "Duplicate 'from' role in replacement request: #{replace_roles.inspect}" end roles_to_update = [] roles_to_delete = [] replace_roles.each_with_index do |replace_role, index| new_role = new_roles[index] from_role = attribution.roles.find do |role| role.type == replace_role[:type] && ((replace_role[:person_id].present? && role.person_id == replace_role[:person_id]) || (replace_role[:organization_id].present? && role.organization_id == replace_role[:organization_id])) end next unless from_role to_role_exists = attribution.roles.any? do |role| role.type == new_role[:type] && ((new_role[:person_id].present? && role.person_id == new_role[:person_id]) || (new_role[:organization_id].present? && role.organization_id == new_role[:organization_id])) end if to_role_exists # Target already exists, just delete the "from" role. roles_to_delete << from_role else roles_to_update << { role: from_role, new_role: new_role } end end [roles_to_update, roles_to_delete] end # Returns a hash with :license and/or :copyright_year set to nil for those # that match between attribution and remove_attrs. def self.attrs_to_clear(attribution, remove_attrs) payload = {} payload[:license] = nil if remove_attrs[:license].present? && remove_attrs[:license] == attribution.license payload[:copyright_year] = nil if remove_attrs[:copyright_year].present? && remove_attrs[:copyright_year] == attribution.copyright_year payload end # Returns an array of role IDs from attribution that match remove_roles. def self.matching_role_ids(attribution, remove_roles) return [] unless remove_roles.any? ids = [] attribution.roles.each do |role| remove_roles.each do |remove_role| next unless role.type == remove_role[:type] if remove_role[:person_id].present? && role.person_id == remove_role[:person_id] ids << role.id elsif remove_role[:organization_id].present? && role.organization_id == remove_role[:organization_id] ids << role.id end end end ids.uniq end # Removes those attribution elements from remove_attribution that exist on # attribution. def self.apply_remove_attribution(attribution, remove_attribution) remove_attrs = remove_attribution.to_h.symbolize_keys remove_roles = matchable_roles(remove_attrs) update_payload = attrs_to_clear(attribution, remove_attrs) roles_to_remove = matching_role_ids(attribution, remove_roles) return :noop if update_payload.empty? && roles_to_remove.empty? final_license = update_payload.key?(:license) ? nil : attribution.license final_year = update_payload.key?(:copyright_year) ? nil : attribution.copyright_year final_roles_present = if roles_to_remove.any? (attribution.roles.size - roles_to_remove.size) > 0 else attribution.roles.any? end should_destroy = final_license.blank? && final_year.blank? && !final_roles_present result = nil Attribution.transaction do if roles_to_remove.any? destroyed_roles = attribution.roles.where(id: roles_to_remove).destroy_all # destroy_all fails silently, so check its work. unless destroyed_roles.all?(&:destroyed?) raise ActiveRecord::Rollback end end if should_destroy if attribution.destroy result = :destroyed else raise ActiveRecord::Rollback end else unless update_payload.empty? unless attribution.update(update_payload) raise ActiveRecord::Rollback end end result = :updated end end result || :invalid end # This is add, so only add data from new_attribution that will be 'new' in # attribution (the not-new case is 'replace'). def self.apply_add_attribution(attribution, new_attribution) attrs = new_attribution.to_h.symbolize_keys roles_attributes = attribution_roles(attrs) update_payload = {} update_payload[:license] = attrs[:license] if attribution.license.blank? && attrs[:license].present? update_payload[:copyright_year] = attrs[:copyright_year] if attribution.copyright_year.blank? && attrs[:copyright_year].present? if roles_attributes.any? existing_roles = attribution.roles.map do |role| [role.type, role.person_id, role.organization_id] end.to_set new_roles = roles_attributes.map do |role| role.slice(:type, :person_id, :organization_id).compact end # Roles are has_many, so we add as long as the new role doesn't already # exist. unique_roles = new_roles.reject do |role| existing_roles.include?([role[:type], role[:person_id], role[:organization_id]]) end if unique_roles.any? update_payload[:roles_attributes] = unique_roles end end return :noop if update_payload.empty? attribution.update(update_payload) attribution.errors.any? ? :invalid : :updated end end |
#copyright_year ⇒ Integer
Returns 4 digit year of copyright.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 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 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 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 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 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 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 |
# File 'app/models/attribution.rb', line 18 class Attribution < ApplicationRecord include Housekeeping include Shared::BatchByFilterScope include Shared::Notes include Shared::Confidences include Shared::Tags include Shared::IsData include Shared::PolymorphicAnnotator polymorphic_annotates('attribution_object', inverse_of: :attribution) # TODO: Consider DRYing with Source roles. ATTRIBUTION_ROLES = [ :creator, :editor, :owner, :copyright_holder ].freeze ATTRIBUTION_ROLES.each do |r| role_name = "#{r}_roles".to_sym role_person = "attribution_#{r.to_s.pluralize}".to_sym role_organization = "attribution_organization_#{r.to_s.pluralize}".to_sym has_many role_name, -> { order('roles.position ASC') }, class_name: "Attribution#{r.to_s.camelize}", as: :role_object, inverse_of: :role_object has_many role_person, -> { order('roles.position ASC') }, through: role_name, source: :person, validate: true has_many role_organization, -> { order('roles.position ASC') }, through: role_name, source: :organization, validate: true accepts_nested_attributes_for role_name, allow_destroy: true accepts_nested_attributes_for role_person accepts_nested_attributes_for role_organization end validates :attribution_object_id, uniqueness: { scope: [:attribution_object_type, :project_id] } validates :license, inclusion: {in: CREATIVE_COMMONS_LICENSES.keys}, allow_nil: true validates :copyright_year, date_year: { min_year: 1000, max_year: Time.zone.now.year + 5, message: 'must be an integer greater than 999 and no more than 5 years in the future'} validate :some_data_provided 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 case mode.to_sym when :add attribution = params[:attribution].presence || {} unless attribution_data_present?(attribution) r.errors['no attribution provided'] = 1 return r end unless validate_single_attribution(attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_object_type = query.klass.name query.find_in_batches do |records| record_ids = records.map(&:id) existing_map = Attribution.where( attribution_object_id: record_ids, attribution_object_type: ).index_by(&:attribution_object_id) records.each do |record| existing = existing_map[record.id] if existing result = apply_add_attribution(existing, attribution) if result == :updated r.updated.push existing.id else r.not_updated.push existing.id end else o_params = attribution.merge({ attribution_object_id: record.id, attribution_object_type: }) created = Attribution.create(o_params) if created.valid? r.updated.push created.id else r.not_updated.push nil # no id to add end end end end end when :remove attribution = params[:attribution].presence || {} unless attribution_data_present?(attribution) r.errors['no attribution criteria provided'] = 1 return r end unless validate_single_attribution(attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_scope_for(query, attribution).find_each do |o| result = apply_remove_attribution(o, attribution) if result == :updated || result == :destroyed r.updated.push o.id else r.not_updated.push o.id end end missing = r.total_attempted - r.updated.length - r.not_updated.length r.not_updated.concat([nil] * missing) if missing.positive? end when :replace replace_attribution = params[:replace_attribution].presence || {} to_attribution = params[:attribution].presence || {} unless attribution_data_present?(replace_attribution) r.errors['no replace attribution criteria provided'] = 1 return r end unless attribution_data_present?(to_attribution) r.errors['no replacement attribution provided'] = 1 return r end unless validate_single_replace(replace_attribution, to_attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_scope_for(query, replace_attribution).find_each do |o| result = apply_replace_attribution(o, replace_attribution, to_attribution) if result == :updated || result == :destroyed r.updated.push o.id else r.not_updated.push o.id end end missing = r.total_attempted - r.updated.length - r.not_updated.length r.not_updated.concat([nil] * missing) if missing.positive? end end r end protected def some_roles_present ATTRIBUTION_ROLES.each do |r| return true if send("#{r}_roles".to_sym).any? end if self.roles.any? self.roles.each do |r| return true if r.type.present? && (r.person_id.present? || r.organization_id.present?) end end false end def some_data_provided if license.blank? && copyright_year.blank? && !some_roles_present errors.add(:base, 'no attribution metadata') end end private def self.attribution_data_present?(attribution) attrs = attribution.to_h.symbolize_keys roles = attribution_roles(attrs) attrs[:license].present? || attrs[:copyright_year].present? || roles.any? end def self.attribution_roles(attribution) attrs = attribution.to_h.symbolize_keys roles = attrs[:roles_attributes] || [] Array(roles) .map { |role| role&.to_h&.symbolize_keys } .compact end def self.operation_types(attribution) attrs = attribution.to_h.symbolize_keys roles = matchable_roles(attrs) types = [] types << :license if attrs[:license].present? types << :copyright_year if attrs[:copyright_year].present? types << :roles if roles.any? [types, roles] end def self.validate_single_attribution(attribution, response) types, roles = operation_types(attribution) if types.empty? response.errors['no valid attribution attribute or role provided'] = 1 return false end if types.length > 1 response.errors['only one attribution attribute or role may be specified'] = 1 return false end if types.first == :roles && roles.length != 1 response.errors['exactly one role must be provided'] = 1 return false end true end def self.validate_single_replace(replace_attribution, to_attribution, response) replace_types, replace_roles = operation_types(replace_attribution) to_types, to_roles = operation_types(to_attribution) if replace_types.length != 1 || to_types.length != 1 response.errors['replace must target exactly one attribute or role'] = 1 return false end if replace_types.first != to_types.first response.errors['replace attribute type must match replacement type'] = 1 return false end if replace_types.first == :roles if replace_roles.length != 1 || to_roles.length != 1 response.errors['exactly one role must be provided for replacement'] = 1 return false end if replace_roles.first[:type] != to_roles.first[:type] response.errors['role replacement types must match'] = 1 return false end end true end def self.matchable_roles(attribution) attribution_roles(attribution).select do |role| role[:type].present? && (role[:person_id].present? || role[:organization_id].present?) end end # @param query [Scope] the base scope to filter # @param attribution [Hash, ActionController::Parameters] # @return [Scope] of all Attributions associated with query that have some # data that matches attribution. def self.attribution_scope_for(query, attribution) attrs = attribution.to_h.symbolize_keys scope = Attribution.includes(:roles).where( attribution_object_id: query.select(:id), attribution_object_type: query.klass.name ) scope = scope.where(license: attrs[:license]) if attrs[:license].present? scope = scope.where(copyright_year: attrs[:copyright_year]) if attrs[:copyright_year].present? matchable_roles(attrs).each do |role| role_type = role[:type] next if role_type.blank? role_scope = Role.where(role_object_type: 'Attribution', type: role_type) if role[:person_id].present? role_scope = role_scope.where(person_id: role[:person_id]) elsif role[:organization_id].present? role_scope = role_scope.where(organization_id: role[:organization_id]) else next end scope = scope.where(id: role_scope.select(:role_object_id)) end scope end # Assumes 1:1 replacement (enforced by UI): # - If replacing license, new_attrs must have a license # - If replacing copyright_year, new_attrs must have a copyright_year # - If replacing a role, new_attrs must have exactly one role of the same type def self.apply_replace_attribution( attribution, replace_attribution, new_attribution ) replace_attrs = replace_attribution.to_h.symbolize_keys new_attrs = new_attribution.to_h.symbolize_keys update_payload = {} update_payload[:license] = new_attrs[:license] if new_attrs[:license].present? && replace_attrs[:license].present? && replace_attrs[:license] == attribution.license update_payload[:copyright_year] = new_attrs[:copyright_year] if new_attrs[:copyright_year].present? && replace_attrs[:copyright_year].present? && replace_attrs[:copyright_year] == attribution.copyright_year replace_roles = matchable_roles(replace_attrs) new_roles = matchable_roles(new_attrs) roles_to_update, roles_to_delete = role_replacement_actions(attribution, replace_roles, new_roles) return :noop if update_payload.empty? && roles_to_update.empty? && roles_to_delete.empty? Attribution.transaction do roles_to_update.each do |item| role_update = {} if item[:new_role][:person_id].present? role_update[:person_id] = item[:new_role][:person_id] role_update[:organization_id] = nil elsif item[:new_role][:organization_id].present? role_update[:organization_id] = item[:new_role][:organization_id] role_update[:person_id] = nil end unless item[:role].update(role_update) raise ActiveRecord::Rollback end end roles_to_delete.each do |role| unless role.destroy raise ActiveRecord::Rollback end end unless update_payload.empty? unless attribution.update(update_payload) raise ActiveRecord::Rollback end end end attribution.errors.any? ? :invalid : :updated end # Returns [roles_to_update, roles_to_delete] arrays for role replacement. # roles_to_update contains { role:, new_role: } hashes for roles to update in # place. roles_to_delete contains roles to delete (when "to" role already # exists). # Raises TaxonWorks::Error if replace_roles and new_roles are mismatched or # have duplicates. def self.role_replacement_actions(attribution, replace_roles, new_roles) if replace_roles.size != new_roles.size raise TaxonWorks::Error, "Mismatched role replacement: each 'from' role must have a corresponding 'to' role - from: #{replace_roles.inspect}, to: #{new_roles.inspect}" end from_keys = replace_roles.map { |r| [r[:type], r[:person_id], r[:organization_id]] } if from_keys.size != from_keys.uniq.size raise TaxonWorks::Error, "Duplicate 'from' role in replacement request: #{replace_roles.inspect}" end roles_to_update = [] roles_to_delete = [] replace_roles.each_with_index do |replace_role, index| new_role = new_roles[index] from_role = attribution.roles.find do |role| role.type == replace_role[:type] && ((replace_role[:person_id].present? && role.person_id == replace_role[:person_id]) || (replace_role[:organization_id].present? && role.organization_id == replace_role[:organization_id])) end next unless from_role to_role_exists = attribution.roles.any? do |role| role.type == new_role[:type] && ((new_role[:person_id].present? && role.person_id == new_role[:person_id]) || (new_role[:organization_id].present? && role.organization_id == new_role[:organization_id])) end if to_role_exists # Target already exists, just delete the "from" role. roles_to_delete << from_role else roles_to_update << { role: from_role, new_role: new_role } end end [roles_to_update, roles_to_delete] end # Returns a hash with :license and/or :copyright_year set to nil for those # that match between attribution and remove_attrs. def self.attrs_to_clear(attribution, remove_attrs) payload = {} payload[:license] = nil if remove_attrs[:license].present? && remove_attrs[:license] == attribution.license payload[:copyright_year] = nil if remove_attrs[:copyright_year].present? && remove_attrs[:copyright_year] == attribution.copyright_year payload end # Returns an array of role IDs from attribution that match remove_roles. def self.matching_role_ids(attribution, remove_roles) return [] unless remove_roles.any? ids = [] attribution.roles.each do |role| remove_roles.each do |remove_role| next unless role.type == remove_role[:type] if remove_role[:person_id].present? && role.person_id == remove_role[:person_id] ids << role.id elsif remove_role[:organization_id].present? && role.organization_id == remove_role[:organization_id] ids << role.id end end end ids.uniq end # Removes those attribution elements from remove_attribution that exist on # attribution. def self.apply_remove_attribution(attribution, remove_attribution) remove_attrs = remove_attribution.to_h.symbolize_keys remove_roles = matchable_roles(remove_attrs) update_payload = attrs_to_clear(attribution, remove_attrs) roles_to_remove = matching_role_ids(attribution, remove_roles) return :noop if update_payload.empty? && roles_to_remove.empty? final_license = update_payload.key?(:license) ? nil : attribution.license final_year = update_payload.key?(:copyright_year) ? nil : attribution.copyright_year final_roles_present = if roles_to_remove.any? (attribution.roles.size - roles_to_remove.size) > 0 else attribution.roles.any? end should_destroy = final_license.blank? && final_year.blank? && !final_roles_present result = nil Attribution.transaction do if roles_to_remove.any? destroyed_roles = attribution.roles.where(id: roles_to_remove).destroy_all # destroy_all fails silently, so check its work. unless destroyed_roles.all?(&:destroyed?) raise ActiveRecord::Rollback end end if should_destroy if attribution.destroy result = :destroyed else raise ActiveRecord::Rollback end else unless update_payload.empty? unless attribution.update(update_payload) raise ActiveRecord::Rollback end end result = :updated end end result || :invalid end # This is add, so only add data from new_attribution that will be 'new' in # attribution (the not-new case is 'replace'). def self.apply_add_attribution(attribution, new_attribution) attrs = new_attribution.to_h.symbolize_keys roles_attributes = attribution_roles(attrs) update_payload = {} update_payload[:license] = attrs[:license] if attribution.license.blank? && attrs[:license].present? update_payload[:copyright_year] = attrs[:copyright_year] if attribution.copyright_year.blank? && attrs[:copyright_year].present? if roles_attributes.any? existing_roles = attribution.roles.map do |role| [role.type, role.person_id, role.organization_id] end.to_set new_roles = roles_attributes.map do |role| role.slice(:type, :person_id, :organization_id).compact end # Roles are has_many, so we add as long as the new role doesn't already # exist. unique_roles = new_roles.reject do |role| existing_roles.include?([role[:type], role[:person_id], role[:organization_id]]) end if unique_roles.any? update_payload[:roles_attributes] = unique_roles end end return :noop if update_payload.empty? attribution.update(update_payload) attribution.errors.any? ? :invalid : :updated end end |
#license ⇒ String
Returns A creative-commons copyright.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 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 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 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 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 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 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 |
# File 'app/models/attribution.rb', line 18 class Attribution < ApplicationRecord include Housekeeping include Shared::BatchByFilterScope include Shared::Notes include Shared::Confidences include Shared::Tags include Shared::IsData include Shared::PolymorphicAnnotator polymorphic_annotates('attribution_object', inverse_of: :attribution) # TODO: Consider DRYing with Source roles. ATTRIBUTION_ROLES = [ :creator, :editor, :owner, :copyright_holder ].freeze ATTRIBUTION_ROLES.each do |r| role_name = "#{r}_roles".to_sym role_person = "attribution_#{r.to_s.pluralize}".to_sym role_organization = "attribution_organization_#{r.to_s.pluralize}".to_sym has_many role_name, -> { order('roles.position ASC') }, class_name: "Attribution#{r.to_s.camelize}", as: :role_object, inverse_of: :role_object has_many role_person, -> { order('roles.position ASC') }, through: role_name, source: :person, validate: true has_many role_organization, -> { order('roles.position ASC') }, through: role_name, source: :organization, validate: true accepts_nested_attributes_for role_name, allow_destroy: true accepts_nested_attributes_for role_person accepts_nested_attributes_for role_organization end validates :attribution_object_id, uniqueness: { scope: [:attribution_object_type, :project_id] } validates :license, inclusion: {in: CREATIVE_COMMONS_LICENSES.keys}, allow_nil: true validates :copyright_year, date_year: { min_year: 1000, max_year: Time.zone.now.year + 5, message: 'must be an integer greater than 999 and no more than 5 years in the future'} validate :some_data_provided 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 case mode.to_sym when :add attribution = params[:attribution].presence || {} unless attribution_data_present?(attribution) r.errors['no attribution provided'] = 1 return r end unless validate_single_attribution(attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_object_type = query.klass.name query.find_in_batches do |records| record_ids = records.map(&:id) existing_map = Attribution.where( attribution_object_id: record_ids, attribution_object_type: ).index_by(&:attribution_object_id) records.each do |record| existing = existing_map[record.id] if existing result = apply_add_attribution(existing, attribution) if result == :updated r.updated.push existing.id else r.not_updated.push existing.id end else o_params = attribution.merge({ attribution_object_id: record.id, attribution_object_type: }) created = Attribution.create(o_params) if created.valid? r.updated.push created.id else r.not_updated.push nil # no id to add end end end end end when :remove attribution = params[:attribution].presence || {} unless attribution_data_present?(attribution) r.errors['no attribution criteria provided'] = 1 return r end unless validate_single_attribution(attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_scope_for(query, attribution).find_each do |o| result = apply_remove_attribution(o, attribution) if result == :updated || result == :destroyed r.updated.push o.id else r.not_updated.push o.id end end missing = r.total_attempted - r.updated.length - r.not_updated.length r.not_updated.concat([nil] * missing) if missing.positive? end when :replace replace_attribution = params[:replace_attribution].presence || {} to_attribution = params[:attribution].presence || {} unless attribution_data_present?(replace_attribution) r.errors['no replace attribution criteria provided'] = 1 return r end unless attribution_data_present?(to_attribution) r.errors['no replacement attribution provided'] = 1 return r end unless validate_single_replace(replace_attribution, to_attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_scope_for(query, replace_attribution).find_each do |o| result = apply_replace_attribution(o, replace_attribution, to_attribution) if result == :updated || result == :destroyed r.updated.push o.id else r.not_updated.push o.id end end missing = r.total_attempted - r.updated.length - r.not_updated.length r.not_updated.concat([nil] * missing) if missing.positive? end end r end protected def some_roles_present ATTRIBUTION_ROLES.each do |r| return true if send("#{r}_roles".to_sym).any? end if self.roles.any? self.roles.each do |r| return true if r.type.present? && (r.person_id.present? || r.organization_id.present?) end end false end def some_data_provided if license.blank? && copyright_year.blank? && !some_roles_present errors.add(:base, 'no attribution metadata') end end private def self.attribution_data_present?(attribution) attrs = attribution.to_h.symbolize_keys roles = attribution_roles(attrs) attrs[:license].present? || attrs[:copyright_year].present? || roles.any? end def self.attribution_roles(attribution) attrs = attribution.to_h.symbolize_keys roles = attrs[:roles_attributes] || [] Array(roles) .map { |role| role&.to_h&.symbolize_keys } .compact end def self.operation_types(attribution) attrs = attribution.to_h.symbolize_keys roles = matchable_roles(attrs) types = [] types << :license if attrs[:license].present? types << :copyright_year if attrs[:copyright_year].present? types << :roles if roles.any? [types, roles] end def self.validate_single_attribution(attribution, response) types, roles = operation_types(attribution) if types.empty? response.errors['no valid attribution attribute or role provided'] = 1 return false end if types.length > 1 response.errors['only one attribution attribute or role may be specified'] = 1 return false end if types.first == :roles && roles.length != 1 response.errors['exactly one role must be provided'] = 1 return false end true end def self.validate_single_replace(replace_attribution, to_attribution, response) replace_types, replace_roles = operation_types(replace_attribution) to_types, to_roles = operation_types(to_attribution) if replace_types.length != 1 || to_types.length != 1 response.errors['replace must target exactly one attribute or role'] = 1 return false end if replace_types.first != to_types.first response.errors['replace attribute type must match replacement type'] = 1 return false end if replace_types.first == :roles if replace_roles.length != 1 || to_roles.length != 1 response.errors['exactly one role must be provided for replacement'] = 1 return false end if replace_roles.first[:type] != to_roles.first[:type] response.errors['role replacement types must match'] = 1 return false end end true end def self.matchable_roles(attribution) attribution_roles(attribution).select do |role| role[:type].present? && (role[:person_id].present? || role[:organization_id].present?) end end # @param query [Scope] the base scope to filter # @param attribution [Hash, ActionController::Parameters] # @return [Scope] of all Attributions associated with query that have some # data that matches attribution. def self.attribution_scope_for(query, attribution) attrs = attribution.to_h.symbolize_keys scope = Attribution.includes(:roles).where( attribution_object_id: query.select(:id), attribution_object_type: query.klass.name ) scope = scope.where(license: attrs[:license]) if attrs[:license].present? scope = scope.where(copyright_year: attrs[:copyright_year]) if attrs[:copyright_year].present? matchable_roles(attrs).each do |role| role_type = role[:type] next if role_type.blank? role_scope = Role.where(role_object_type: 'Attribution', type: role_type) if role[:person_id].present? role_scope = role_scope.where(person_id: role[:person_id]) elsif role[:organization_id].present? role_scope = role_scope.where(organization_id: role[:organization_id]) else next end scope = scope.where(id: role_scope.select(:role_object_id)) end scope end # Assumes 1:1 replacement (enforced by UI): # - If replacing license, new_attrs must have a license # - If replacing copyright_year, new_attrs must have a copyright_year # - If replacing a role, new_attrs must have exactly one role of the same type def self.apply_replace_attribution( attribution, replace_attribution, new_attribution ) replace_attrs = replace_attribution.to_h.symbolize_keys new_attrs = new_attribution.to_h.symbolize_keys update_payload = {} update_payload[:license] = new_attrs[:license] if new_attrs[:license].present? && replace_attrs[:license].present? && replace_attrs[:license] == attribution.license update_payload[:copyright_year] = new_attrs[:copyright_year] if new_attrs[:copyright_year].present? && replace_attrs[:copyright_year].present? && replace_attrs[:copyright_year] == attribution.copyright_year replace_roles = matchable_roles(replace_attrs) new_roles = matchable_roles(new_attrs) roles_to_update, roles_to_delete = role_replacement_actions(attribution, replace_roles, new_roles) return :noop if update_payload.empty? && roles_to_update.empty? && roles_to_delete.empty? Attribution.transaction do roles_to_update.each do |item| role_update = {} if item[:new_role][:person_id].present? role_update[:person_id] = item[:new_role][:person_id] role_update[:organization_id] = nil elsif item[:new_role][:organization_id].present? role_update[:organization_id] = item[:new_role][:organization_id] role_update[:person_id] = nil end unless item[:role].update(role_update) raise ActiveRecord::Rollback end end roles_to_delete.each do |role| unless role.destroy raise ActiveRecord::Rollback end end unless update_payload.empty? unless attribution.update(update_payload) raise ActiveRecord::Rollback end end end attribution.errors.any? ? :invalid : :updated end # Returns [roles_to_update, roles_to_delete] arrays for role replacement. # roles_to_update contains { role:, new_role: } hashes for roles to update in # place. roles_to_delete contains roles to delete (when "to" role already # exists). # Raises TaxonWorks::Error if replace_roles and new_roles are mismatched or # have duplicates. def self.role_replacement_actions(attribution, replace_roles, new_roles) if replace_roles.size != new_roles.size raise TaxonWorks::Error, "Mismatched role replacement: each 'from' role must have a corresponding 'to' role - from: #{replace_roles.inspect}, to: #{new_roles.inspect}" end from_keys = replace_roles.map { |r| [r[:type], r[:person_id], r[:organization_id]] } if from_keys.size != from_keys.uniq.size raise TaxonWorks::Error, "Duplicate 'from' role in replacement request: #{replace_roles.inspect}" end roles_to_update = [] roles_to_delete = [] replace_roles.each_with_index do |replace_role, index| new_role = new_roles[index] from_role = attribution.roles.find do |role| role.type == replace_role[:type] && ((replace_role[:person_id].present? && role.person_id == replace_role[:person_id]) || (replace_role[:organization_id].present? && role.organization_id == replace_role[:organization_id])) end next unless from_role to_role_exists = attribution.roles.any? do |role| role.type == new_role[:type] && ((new_role[:person_id].present? && role.person_id == new_role[:person_id]) || (new_role[:organization_id].present? && role.organization_id == new_role[:organization_id])) end if to_role_exists # Target already exists, just delete the "from" role. roles_to_delete << from_role else roles_to_update << { role: from_role, new_role: new_role } end end [roles_to_update, roles_to_delete] end # Returns a hash with :license and/or :copyright_year set to nil for those # that match between attribution and remove_attrs. def self.attrs_to_clear(attribution, remove_attrs) payload = {} payload[:license] = nil if remove_attrs[:license].present? && remove_attrs[:license] == attribution.license payload[:copyright_year] = nil if remove_attrs[:copyright_year].present? && remove_attrs[:copyright_year] == attribution.copyright_year payload end # Returns an array of role IDs from attribution that match remove_roles. def self.matching_role_ids(attribution, remove_roles) return [] unless remove_roles.any? ids = [] attribution.roles.each do |role| remove_roles.each do |remove_role| next unless role.type == remove_role[:type] if remove_role[:person_id].present? && role.person_id == remove_role[:person_id] ids << role.id elsif remove_role[:organization_id].present? && role.organization_id == remove_role[:organization_id] ids << role.id end end end ids.uniq end # Removes those attribution elements from remove_attribution that exist on # attribution. def self.apply_remove_attribution(attribution, remove_attribution) remove_attrs = remove_attribution.to_h.symbolize_keys remove_roles = matchable_roles(remove_attrs) update_payload = attrs_to_clear(attribution, remove_attrs) roles_to_remove = matching_role_ids(attribution, remove_roles) return :noop if update_payload.empty? && roles_to_remove.empty? final_license = update_payload.key?(:license) ? nil : attribution.license final_year = update_payload.key?(:copyright_year) ? nil : attribution.copyright_year final_roles_present = if roles_to_remove.any? (attribution.roles.size - roles_to_remove.size) > 0 else attribution.roles.any? end should_destroy = final_license.blank? && final_year.blank? && !final_roles_present result = nil Attribution.transaction do if roles_to_remove.any? destroyed_roles = attribution.roles.where(id: roles_to_remove).destroy_all # destroy_all fails silently, so check its work. unless destroyed_roles.all?(&:destroyed?) raise ActiveRecord::Rollback end end if should_destroy if attribution.destroy result = :destroyed else raise ActiveRecord::Rollback end else unless update_payload.empty? unless attribution.update(update_payload) raise ActiveRecord::Rollback end end result = :updated end end result || :invalid end # This is add, so only add data from new_attribution that will be 'new' in # attribution (the not-new case is 'replace'). def self.apply_add_attribution(attribution, new_attribution) attrs = new_attribution.to_h.symbolize_keys roles_attributes = attribution_roles(attrs) update_payload = {} update_payload[:license] = attrs[:license] if attribution.license.blank? && attrs[:license].present? update_payload[:copyright_year] = attrs[:copyright_year] if attribution.copyright_year.blank? && attrs[:copyright_year].present? if roles_attributes.any? existing_roles = attribution.roles.map do |role| [role.type, role.person_id, role.organization_id] end.to_set new_roles = roles_attributes.map do |role| role.slice(:type, :person_id, :organization_id).compact end # Roles are has_many, so we add as long as the new role doesn't already # exist. unique_roles = new_roles.reject do |role| existing_roles.include?([role[:type], role[:person_id], role[:organization_id]]) end if unique_roles.any? update_payload[:roles_attributes] = unique_roles end end return :noop if update_payload.empty? attribution.update(update_payload) attribution.errors.any? ? :invalid : :updated end end |
Class Method Details
.apply_add_attribution(attribution, new_attribution) ⇒ Object (private)
This is add, so only add data from new_attribution that will be ‘new’ in attribution (the not-new case is ‘replace’).
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 |
# File 'app/models/attribution.rb', line 524 def self.apply_add_attribution(attribution, new_attribution) attrs = new_attribution.to_h.symbolize_keys roles_attributes = attribution_roles(attrs) update_payload = {} update_payload[:license] = attrs[:license] if attribution.license.blank? && attrs[:license].present? update_payload[:copyright_year] = attrs[:copyright_year] if attribution.copyright_year.blank? && attrs[:copyright_year].present? if roles_attributes.any? existing_roles = attribution.roles.map do |role| [role.type, role.person_id, role.organization_id] end.to_set new_roles = roles_attributes.map do |role| role.slice(:type, :person_id, :organization_id).compact end # Roles are has_many, so we add as long as the new role doesn't already # exist. unique_roles = new_roles.reject do |role| existing_roles.include?([role[:type], role[:person_id], role[:organization_id]]) end if unique_roles.any? update_payload[:roles_attributes] = unique_roles end end return :noop if update_payload.empty? attribution.update(update_payload) attribution.errors.any? ? :invalid : :updated end |
.apply_remove_attribution(attribution, remove_attribution) ⇒ Object (private)
Removes those attribution elements from remove_attribution that exist on attribution.
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 |
# File 'app/models/attribution.rb', line 473 def self.apply_remove_attribution(attribution, remove_attribution) remove_attrs = remove_attribution.to_h.symbolize_keys remove_roles = matchable_roles(remove_attrs) update_payload = attrs_to_clear(attribution, remove_attrs) roles_to_remove = matching_role_ids(attribution, remove_roles) return :noop if update_payload.empty? && roles_to_remove.empty? final_license = update_payload.key?(:license) ? nil : attribution.license final_year = update_payload.key?(:copyright_year) ? nil : attribution.copyright_year final_roles_present = if roles_to_remove.any? (attribution.roles.size - roles_to_remove.size) > 0 else attribution.roles.any? end should_destroy = final_license.blank? && final_year.blank? && !final_roles_present result = nil Attribution.transaction do if roles_to_remove.any? destroyed_roles = attribution.roles.where(id: roles_to_remove).destroy_all # destroy_all fails silently, so check its work. unless destroyed_roles.all?(&:destroyed?) raise ActiveRecord::Rollback end end if should_destroy if attribution.destroy result = :destroyed else raise ActiveRecord::Rollback end else unless update_payload.empty? unless attribution.update(update_payload) raise ActiveRecord::Rollback end end result = :updated end end result || :invalid end |
.apply_replace_attribution(attribution, replace_attribution, new_attribution) ⇒ Object (private)
Assumes 1:1 replacement (enforced by UI):
-
If replacing license, new_attrs must have a license
-
If replacing copyright_year, new_attrs must have a copyright_year
-
If replacing a role, new_attrs must have exactly one role of the same type
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 |
# File 'app/models/attribution.rb', line 348 def self.apply_replace_attribution( attribution, replace_attribution, new_attribution ) replace_attrs = replace_attribution.to_h.symbolize_keys new_attrs = new_attribution.to_h.symbolize_keys update_payload = {} update_payload[:license] = new_attrs[:license] if new_attrs[:license].present? && replace_attrs[:license].present? && replace_attrs[:license] == attribution.license update_payload[:copyright_year] = new_attrs[:copyright_year] if new_attrs[:copyright_year].present? && replace_attrs[:copyright_year].present? && replace_attrs[:copyright_year] == attribution.copyright_year replace_roles = matchable_roles(replace_attrs) new_roles = matchable_roles(new_attrs) roles_to_update, roles_to_delete = role_replacement_actions(attribution, replace_roles, new_roles) return :noop if update_payload.empty? && roles_to_update.empty? && roles_to_delete.empty? Attribution.transaction do roles_to_update.each do |item| role_update = {} if item[:new_role][:person_id].present? role_update[:person_id] = item[:new_role][:person_id] role_update[:organization_id] = nil elsif item[:new_role][:organization_id].present? role_update[:organization_id] = item[:new_role][:organization_id] role_update[:person_id] = nil end unless item[:role].update(role_update) raise ActiveRecord::Rollback end end roles_to_delete.each do |role| unless role.destroy raise ActiveRecord::Rollback end end unless update_payload.empty? unless attribution.update(update_payload) raise ActiveRecord::Rollback end end end attribution.errors.any? ? :invalid : :updated end |
.attribution_data_present?(attribution) ⇒ Boolean (private)
227 228 229 230 231 232 |
# File 'app/models/attribution.rb', line 227 def self.attribution_data_present?(attribution) attrs = attribution.to_h.symbolize_keys roles = attribution_roles(attrs) attrs[:license].present? || attrs[:copyright_year].present? || roles.any? end |
.attribution_roles(attribution) ⇒ Object (private)
234 235 236 237 238 239 240 241 |
# File 'app/models/attribution.rb', line 234 def self.attribution_roles(attribution) attrs = attribution.to_h.symbolize_keys roles = attrs[:roles_attributes] || [] Array(roles) .map { |role| role&.to_h&.symbolize_keys } .compact end |
.attribution_scope_for(query, attribution) ⇒ Scope (private)
data that matches attribution.
315 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 |
# File 'app/models/attribution.rb', line 315 def self.attribution_scope_for(query, attribution) attrs = attribution.to_h.symbolize_keys scope = Attribution.includes(:roles).where( attribution_object_id: query.select(:id), attribution_object_type: query.klass.name ) scope = scope.where(license: attrs[:license]) if attrs[:license].present? scope = scope.where(copyright_year: attrs[:copyright_year]) if attrs[:copyright_year].present? matchable_roles(attrs).each do |role| role_type = role[:type] next if role_type.blank? role_scope = Role.where(role_object_type: 'Attribution', type: role_type) if role[:person_id].present? role_scope = role_scope.where(person_id: role[:person_id]) elsif role[:organization_id].present? role_scope = role_scope.where(organization_id: role[:organization_id]) else next end scope = scope.where(id: role_scope.select(:role_object_id)) end scope end |
.attrs_to_clear(attribution, remove_attrs) ⇒ Object (private)
Returns a hash with :license and/or :copyright_year set to nil for those that match between attribution and remove_attrs.
446 447 448 449 450 451 |
# File 'app/models/attribution.rb', line 446 def self.attrs_to_clear(attribution, remove_attrs) payload = {} payload[:license] = nil if remove_attrs[:license].present? && remove_attrs[:license] == attribution.license payload[:copyright_year] = nil if remove_attrs[:copyright_year].present? && remove_attrs[:copyright_year] == attribution.copyright_year payload end |
.matchable_roles(attribution) ⇒ Object (private)
305 306 307 308 309 |
# File 'app/models/attribution.rb', line 305 def self.matchable_roles(attribution) attribution_roles(attribution).select do |role| role[:type].present? && (role[:person_id].present? || role[:organization_id].present?) end end |
.matching_role_ids(attribution, remove_roles) ⇒ Object (private)
Returns an array of role IDs from attribution that match remove_roles.
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 |
# File 'app/models/attribution.rb', line 454 def self.matching_role_ids(attribution, remove_roles) return [] unless remove_roles.any? ids = [] attribution.roles.each do |role| remove_roles.each do |remove_role| next unless role.type == remove_role[:type] if remove_role[:person_id].present? && role.person_id == remove_role[:person_id] ids << role.id elsif remove_role[:organization_id].present? && role.organization_id == remove_role[:organization_id] ids << role.id end end end ids.uniq end |
.operation_types(attribution) ⇒ Object (private)
243 244 245 246 247 248 249 250 251 252 253 |
# File 'app/models/attribution.rb', line 243 def self.operation_types(attribution) attrs = attribution.to_h.symbolize_keys roles = matchable_roles(attrs) types = [] types << :license if attrs[:license].present? types << :copyright_year if attrs[:copyright_year].present? types << :roles if roles.any? [types, roles] 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
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 |
# File 'app/models/attribution.rb', line 61 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 case mode.to_sym when :add attribution = params[:attribution].presence || {} unless attribution_data_present?(attribution) r.errors['no attribution provided'] = 1 return r end unless validate_single_attribution(attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_object_type = query.klass.name query.find_in_batches do |records| record_ids = records.map(&:id) existing_map = Attribution.where( attribution_object_id: record_ids, attribution_object_type: ).index_by(&:attribution_object_id) records.each do |record| existing = existing_map[record.id] if existing result = apply_add_attribution(existing, attribution) if result == :updated r.updated.push existing.id else r.not_updated.push existing.id end else o_params = attribution.merge({ attribution_object_id: record.id, attribution_object_type: }) created = Attribution.create(o_params) if created.valid? r.updated.push created.id else r.not_updated.push nil # no id to add end end end end end when :remove attribution = params[:attribution].presence || {} unless attribution_data_present?(attribution) r.errors['no attribution criteria provided'] = 1 return r end unless validate_single_attribution(attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_scope_for(query, attribution).find_each do |o| result = apply_remove_attribution(o, attribution) if result == :updated || result == :destroyed r.updated.push o.id else r.not_updated.push o.id end end missing = r.total_attempted - r.updated.length - r.not_updated.length r.not_updated.concat([nil] * missing) if missing.positive? end when :replace replace_attribution = params[:replace_attribution].presence || {} to_attribution = params[:attribution].presence || {} unless attribution_data_present?(replace_attribution) r.errors['no replace attribution criteria provided'] = 1 return r end unless attribution_data_present?(to_attribution) r.errors['no replacement attribution provided'] = 1 return r end unless validate_single_replace(replace_attribution, to_attribution, r) return r end if async && !called_from_async BatchByFilterScopeJob.perform_later( klass: self.name, hash_query:, mode:, params:, project_id:, user_id: ) else attribution_scope_for(query, replace_attribution).find_each do |o| result = apply_replace_attribution(o, replace_attribution, to_attribution) if result == :updated || result == :destroyed r.updated.push o.id else r.not_updated.push o.id end end missing = r.total_attempted - r.updated.length - r.not_updated.length r.not_updated.concat([nil] * missing) if missing.positive? end end r end |
.role_replacement_actions(attribution, replace_roles, new_roles) ⇒ Object (private)
Returns [roles_to_update, roles_to_delete] arrays for role replacement. roles_to_update contains { role:, new_role: } hashes for roles to update in place. roles_to_delete contains roles to delete (when “to” role already exists). Raises TaxonWorks::Error if replace_roles and new_roles are mismatched or have duplicates.
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 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 |
# File 'app/models/attribution.rb', line 403 def self.role_replacement_actions(attribution, replace_roles, new_roles) if replace_roles.size != new_roles.size raise TaxonWorks::Error, "Mismatched role replacement: each 'from' role must have a corresponding 'to' role - from: #{replace_roles.inspect}, to: #{new_roles.inspect}" end from_keys = replace_roles.map { |r| [r[:type], r[:person_id], r[:organization_id]] } if from_keys.size != from_keys.uniq.size raise TaxonWorks::Error, "Duplicate 'from' role in replacement request: #{replace_roles.inspect}" end roles_to_update = [] roles_to_delete = [] replace_roles.each_with_index do |replace_role, index| new_role = new_roles[index] from_role = attribution.roles.find do |role| role.type == replace_role[:type] && ((replace_role[:person_id].present? && role.person_id == replace_role[:person_id]) || (replace_role[:organization_id].present? && role.organization_id == replace_role[:organization_id])) end next unless from_role to_role_exists = attribution.roles.any? do |role| role.type == new_role[:type] && ((new_role[:person_id].present? && role.person_id == new_role[:person_id]) || (new_role[:organization_id].present? && role.organization_id == new_role[:organization_id])) end if to_role_exists # Target already exists, just delete the "from" role. roles_to_delete << from_role else roles_to_update << { role: from_role, new_role: new_role } end end [roles_to_update, roles_to_delete] end |
.validate_single_attribution(attribution, response) ⇒ Object (private)
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'app/models/attribution.rb', line 255 def self.validate_single_attribution(attribution, response) types, roles = operation_types(attribution) if types.empty? response.errors['no valid attribution attribute or role provided'] = 1 return false end if types.length > 1 response.errors['only one attribution attribute or role may be specified'] = 1 return false end if types.first == :roles && roles.length != 1 response.errors['exactly one role must be provided'] = 1 return false end true end |
.validate_single_replace(replace_attribution, to_attribution, response) ⇒ Object (private)
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'app/models/attribution.rb', line 276 def self.validate_single_replace(replace_attribution, to_attribution, response) replace_types, replace_roles = operation_types(replace_attribution) to_types, to_roles = operation_types(to_attribution) if replace_types.length != 1 || to_types.length != 1 response.errors['replace must target exactly one attribute or role'] = 1 return false end if replace_types.first != to_types.first response.errors['replace attribute type must match replacement type'] = 1 return false end if replace_types.first == :roles if replace_roles.length != 1 || to_roles.length != 1 response.errors['exactly one role must be provided for replacement'] = 1 return false end if replace_roles.first[:type] != to_roles.first[:type] response.errors['role replacement types must match'] = 1 return false end end true end |
Instance Method Details
#some_data_provided ⇒ Object (protected)
219 220 221 222 223 |
# File 'app/models/attribution.rb', line 219 def some_data_provided if license.blank? && copyright_year.blank? && !some_roles_present errors.add(:base, 'no attribution metadata') end end |
#some_roles_present ⇒ Object (protected)
205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'app/models/attribution.rb', line 205 def some_roles_present ATTRIBUTION_ROLES.each do |r| return true if send("#{r}_roles".to_sym).any? end if self.roles.any? self.roles.each do |r| return true if r.type.present? && (r.person_id.present? || r.organization_id.present?) end end false end |