Class: LoanItem

Inherits:
ApplicationRecord show all
Includes:
Housekeeping, Shared::BatchByFilterScope, Shared::DataAttributes, Shared::IsData, Shared::Notes, Shared::Tags
Defined in:
app/models/loan_item.rb

Overview

A loan item is a CollectionObject, Container, or historical reference to something that has been loaned via (Otu)

Thanks to neanderslob.com/2015/11/03/polymorphic-associations-the-smart-way-using-global-ids/ for global_entity.

Constant Summary collapse

STATUS =
['Destroyed', 'Donated', 'Lost', 'Retained', 'Returned'].freeze

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::DataAttributes

#import_attributes, #internal_attributes, #keyword_value_hash, #reject_data_attributes

Methods included from Housekeeping

#has_polymorphic_relationship?

Methods inherited from ApplicationRecord

transaction_with_retry

Instance Attribute Details

#date_returnedDateTime

The date the item was returned.

Returns:

  • (DateTime)


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

class LoanItem < ApplicationRecord
  acts_as_list scope: [:loan, :project_id]

  include Housekeeping
  include Shared::DataAttributes
  include Shared::Notes
  include Shared::Tags
  include Shared::IsData
  include Shared::BatchByFilterScope

  attr_accessor :date_returned_jquery

  STATUS = ['Destroyed', 'Donated', 'Lost', 'Retained', 'Returned'].freeze

  belongs_to :loan, inverse_of: :loan_items
  belongs_to :loan_item_object, polymorphic: true

  validates_presence_of :loan_item_object

  validates :loan, presence: true

  # validates_uniqueness_of :loan, scope: [:loan_item_object_type, :loan_item_object_id]

  validate :total_provided_only_when_otu

  validate :loan_object_is_loanable

  validate :available_for_loan

  validates_uniqueness_of :loan_id, scope: [:loan_item_object_id, :loan_item_object_type], if: -> { loan_item_object_type == 'CollectionObject' }

  validates_inclusion_of :disposition, in: STATUS, if: -> {disposition.present?}

  def global_entity
    self.loan_item_object.to_global_id if self.loan_item_object.present?
  end

  def global_entity=(entity)
    self.loan_item_object = GlobalID::Locator.locate entity
  end

  def date_returned_jquery=(date)
    self.date_returned = date.gsub(/(\d+)\/(\d+)\/(\d+)/, '\2/\1/\3')
  end

  def date_returned_jquery
    self.date_returned
  end

  def returned?
    date_returned.present?
  end

  # @return [Integer, nil]
  #   the total items this loan line item represent
  # TODO: this does not factor in nested items in a container
  def total_items
    case loan_item_object_type
      when 'Otu'
        total ? total : nil
      when 'Container'
        t = 0
        loan_item_object.all_contained_objects.each do |o|
          if o.kind_of?(::CollectionObject)
            t += o.total
          end
        end
        t
      when 'CollectionObject'
        loan_item_object.total.to_i
      else
        nil
    end
  end

  # @return [Array]
  #   all objects that can have a taxon determination applied to them for this loan item
  def determinable_objects
    # this loan item which may be a container, an OTU, or a collection object
    case loan_item_object_type
    when /contain/i # if this item is a container, dig into the container for the collection objects themselves
      loan_item_object.collection_objects
    when /object/i # if this item is a collection object, just add the object
      [loan_item_object]
    when /otu/i # not strictly needed, but helps keep track of what the loan_item is.
      [] # can't use an OTU as a determination object.
    end
  end

  # @params :ids -> an ID of a loan_item
  def self.batch_determine_loan_items(ids: [], params: {})
    return false if ids.empty?
    # these objects will be created/persisted to be used for each of the loan items identified by the input ids
    td = TaxonDetermination.new(params) # build a td from the input data

    begin
      LoanItem.transaction do
        item_list = [] # Array of objects that can have a taxon determination
        LoanItem.where(id: ids).each do |li|
          item_list.push li.determinable_objects
        end

        item_list.flatten!

        first = item_list.pop
        td.taxon_determination_object = first
        td.save! # create and save the first one so we can dup it in the next step

        item_list.each do |item|
          n = td.dup
          n.determiners << td.determiners
          n.taxon_determination_object = item
          n.save
          n.move_to_top
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    true
  end

  # @param batch_response [BatchResponse]
  # @param query [ActiveRecord::Relation] the filtered collection objects
  # @param hash_query [Hash] the filter query as a hash (for async)
  # @param mode [Symbol] :add
  # @param params [Hash] must include :loan_id
  # @param async [Boolean]
  # @param project_id [Integer]
  # @param user_id [Integer]
  # @param called_from_async [Boolean]
  # @return [BatchResponse]
  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
  )
    async = false if called_from_async == true
    r = batch_response

    case mode.to_sym
    when :add
      loan_id = params[:loan_id]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.new(
            loan_item_object: collection_object,
            loan_id:,
            project_id:,
            by: user_id
          )

          if loan_item.save
            r.updated.push loan_item.id
          else
            r.not_updated.push collection_object.id
            loan_item.errors.full_messages.each do |msg|
              r.validation_errors[msg] += 1
            end
          end
        end
      end

    when :return
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            if loan_item.update(disposition:, date_returned:)
              r.updated.push loan_item.id
            else
              r.not_updated.push collection_object.id
              loan_item.errors.full_messages.each do |msg|
                r.validation_errors[msg] += 1
              end
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end

    when :move
      loan_id = params[:loan_id]
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            loan_item.loan_item_object.association(:loan_item).reset
            new_loan_item = loan_item.close_and_move(loan_id, date_returned, disposition, user_id)

            if new_loan_item
              r.updated.push new_loan_item.id
            else
              r.not_updated.push collection_object.id
              r.validation_errors['either return or move of loan item failed'] += 1
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end
    end

    r
  end

  # TODO: param handling is currently all kinds of "meh"
  def self.batch_create(params)
    case params[:batch_type]
    when 'tags'
      batch_create_from_tags(params[:keyword_id], params[:klass], params[:loan_id])
    when 'pinboard'
      batch_create_from_pinboard(params[:loan_id], params[:project_id], params[:user_id], params[:klass])
    when 'collection_object_filter'
      batch_create_from_collection_object_filter(
        params[:loan_id],
        params[:project_id],
        params[:user_id],
        params[:collection_object_query])
    end
  end

  # @return [Hash]
  def self.batch_move(params)
    return false if params[:loan_id].blank? || params[:disposition].blank? || params[:user_id].blank? || params[:date_returned].blank?

    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])
    return false if a.all.count == 0

    moved = []
    unmoved = []

    begin
      a.all.each do |co|
        new_loan_item = nil

        # Only match open loan items
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)

          new_loan_item = b.close_and_move(params[:loan_id], params[:date_returned], params[:disposition], params[:user_id])

          if new_loan_item.nil?
            unmoved.push b
          else
            moved.push new_loan_item
          end
        end
      end

    rescue ActiveRecord::RecordInvalid => e
     # raise e
    end

    return { moved:, unmoved: }
  end

  # @param param[:collection_object_query] required
  #
  # Return all CollectionObjects matching the query. Does not yet work with OtuQuery
  def self.batch_return(params)
    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])

    return false if a.all.count == 0

    returned = []
    unreturned = []

    begin
      a.all.each do |co|
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)
          begin
            b.update!(disposition: params[:disposition], date_returned: params[:date_returned])
            returned.push b
          rescue ActiveRecord::RecordInvalid
            unreturned.push b
          end
        end
      end
    end
    return {returned:, unreturned:}
  end

  def close_and_move(to_loan_id, date_returned, disposition, user_id)
    return nil if to_loan_id.blank?

    new_loan_item = nil
    LoanItem.transaction do
      begin
        update!(date_returned:, disposition:)

        # Reset cached association so on_loan? returns correct value
        loan_item_object.association(:loan_item).reset

        new_loan_item = LoanItem.create!(
          project_id:,
          loan_item_object:,
          loan_id: to_loan_id
          )

      rescue ActiveRecord::RecordInvalid => e
        #raise e
      end
    end
    new_loan_item
  end

  def self.batch_create_from_collection_object_filter(loan_id, project_id, user_id, collection_object_filter)
    created = []
    query = Queries::CollectionObject::Filter.new(collection_object_filter)
    LoanItem.transaction do
      begin
        query.all.each do |co|
          i = LoanItem.create!(loan_item_object: co, by: user_id, loan_id:, project_id:)
          if i.persisted?
            created.push i
          end
        end
      rescue ActiveRecord::RecordInvalid => e
        # raise e
      end
    end
    return created
  end

  def self.batch_create_from_tags(keyword_id, klass, loan_id)
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:tags).where(tags: {keyword_id:}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          Tag.where(keyword_id:).where(tag_object_type: ['Container', 'Otu', 'CollectionObject']).distinct.all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.tag_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end


  def self.batch_create_from_pinboard(loan_id, project_id, user_id, klass)
    return false if loan_id.blank? || project_id.blank? || user_id.blank?
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:pinboard_items).where(pinboard_items: {user_id:, project_id:, pinned_object_type: klass}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          PinboardItem.where(project_id:, user_id:, pinned_object_type: ['Container', 'Otu', 'CollectionObject']).all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.pinned_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end

  protected

  # Whether this class of objects is in fact loanable, not
  # whether it's on loan or not.
  def object_loanable_check
    loan_item_object && loan_item_object.respond_to?(:is_loanable?)
  end

  # Code, not out-on-loan check!
  def loan_object_is_loanable
    if !persisted? # if it is, then this check should not be necessary
      if !object_loanable_check
        errors.add(:loan_item_object, 'is not loanble')
      end
    end
  end

  def total_provided_only_when_otu
    errors.add(:total, 'only providable when item is an OTU.') if total && loan_item_object_type != 'Otu'
  end

  # Is not already in a loan item if CollectionObject/Container
  def available_for_loan
    if !persisted? # if it is, then this check should not be necessary
      if object_loanable_check
        if loan_item_object_type == 'Otu'
          true
        else
          if loan_item_object.on_loan? # takes into account Containers!
            errors.add(:loan_item_object, 'is already on loan')
          end
        end
      end
    end
  end

end

#date_returned_jqueryObject

Returns the value of attribute date_returned_jquery.



48
49
50
# File 'app/models/loan_item.rb', line 48

def date_returned_jquery
  @date_returned_jquery
end

#dispositionString

Returns an evolving controlled vocabulary used to differentiate loan object status when it differs from that of the overal loan, see LoanItem::STATUS.

Returns:

  • (String)

    an evolving controlled vocabulary used to differentiate loan object status when it differs from that of the overal loan, see LoanItem::STATUS



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

class LoanItem < ApplicationRecord
  acts_as_list scope: [:loan, :project_id]

  include Housekeeping
  include Shared::DataAttributes
  include Shared::Notes
  include Shared::Tags
  include Shared::IsData
  include Shared::BatchByFilterScope

  attr_accessor :date_returned_jquery

  STATUS = ['Destroyed', 'Donated', 'Lost', 'Retained', 'Returned'].freeze

  belongs_to :loan, inverse_of: :loan_items
  belongs_to :loan_item_object, polymorphic: true

  validates_presence_of :loan_item_object

  validates :loan, presence: true

  # validates_uniqueness_of :loan, scope: [:loan_item_object_type, :loan_item_object_id]

  validate :total_provided_only_when_otu

  validate :loan_object_is_loanable

  validate :available_for_loan

  validates_uniqueness_of :loan_id, scope: [:loan_item_object_id, :loan_item_object_type], if: -> { loan_item_object_type == 'CollectionObject' }

  validates_inclusion_of :disposition, in: STATUS, if: -> {disposition.present?}

  def global_entity
    self.loan_item_object.to_global_id if self.loan_item_object.present?
  end

  def global_entity=(entity)
    self.loan_item_object = GlobalID::Locator.locate entity
  end

  def date_returned_jquery=(date)
    self.date_returned = date.gsub(/(\d+)\/(\d+)\/(\d+)/, '\2/\1/\3')
  end

  def date_returned_jquery
    self.date_returned
  end

  def returned?
    date_returned.present?
  end

  # @return [Integer, nil]
  #   the total items this loan line item represent
  # TODO: this does not factor in nested items in a container
  def total_items
    case loan_item_object_type
      when 'Otu'
        total ? total : nil
      when 'Container'
        t = 0
        loan_item_object.all_contained_objects.each do |o|
          if o.kind_of?(::CollectionObject)
            t += o.total
          end
        end
        t
      when 'CollectionObject'
        loan_item_object.total.to_i
      else
        nil
    end
  end

  # @return [Array]
  #   all objects that can have a taxon determination applied to them for this loan item
  def determinable_objects
    # this loan item which may be a container, an OTU, or a collection object
    case loan_item_object_type
    when /contain/i # if this item is a container, dig into the container for the collection objects themselves
      loan_item_object.collection_objects
    when /object/i # if this item is a collection object, just add the object
      [loan_item_object]
    when /otu/i # not strictly needed, but helps keep track of what the loan_item is.
      [] # can't use an OTU as a determination object.
    end
  end

  # @params :ids -> an ID of a loan_item
  def self.batch_determine_loan_items(ids: [], params: {})
    return false if ids.empty?
    # these objects will be created/persisted to be used for each of the loan items identified by the input ids
    td = TaxonDetermination.new(params) # build a td from the input data

    begin
      LoanItem.transaction do
        item_list = [] # Array of objects that can have a taxon determination
        LoanItem.where(id: ids).each do |li|
          item_list.push li.determinable_objects
        end

        item_list.flatten!

        first = item_list.pop
        td.taxon_determination_object = first
        td.save! # create and save the first one so we can dup it in the next step

        item_list.each do |item|
          n = td.dup
          n.determiners << td.determiners
          n.taxon_determination_object = item
          n.save
          n.move_to_top
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    true
  end

  # @param batch_response [BatchResponse]
  # @param query [ActiveRecord::Relation] the filtered collection objects
  # @param hash_query [Hash] the filter query as a hash (for async)
  # @param mode [Symbol] :add
  # @param params [Hash] must include :loan_id
  # @param async [Boolean]
  # @param project_id [Integer]
  # @param user_id [Integer]
  # @param called_from_async [Boolean]
  # @return [BatchResponse]
  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
  )
    async = false if called_from_async == true
    r = batch_response

    case mode.to_sym
    when :add
      loan_id = params[:loan_id]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.new(
            loan_item_object: collection_object,
            loan_id:,
            project_id:,
            by: user_id
          )

          if loan_item.save
            r.updated.push loan_item.id
          else
            r.not_updated.push collection_object.id
            loan_item.errors.full_messages.each do |msg|
              r.validation_errors[msg] += 1
            end
          end
        end
      end

    when :return
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            if loan_item.update(disposition:, date_returned:)
              r.updated.push loan_item.id
            else
              r.not_updated.push collection_object.id
              loan_item.errors.full_messages.each do |msg|
                r.validation_errors[msg] += 1
              end
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end

    when :move
      loan_id = params[:loan_id]
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            loan_item.loan_item_object.association(:loan_item).reset
            new_loan_item = loan_item.close_and_move(loan_id, date_returned, disposition, user_id)

            if new_loan_item
              r.updated.push new_loan_item.id
            else
              r.not_updated.push collection_object.id
              r.validation_errors['either return or move of loan item failed'] += 1
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end
    end

    r
  end

  # TODO: param handling is currently all kinds of "meh"
  def self.batch_create(params)
    case params[:batch_type]
    when 'tags'
      batch_create_from_tags(params[:keyword_id], params[:klass], params[:loan_id])
    when 'pinboard'
      batch_create_from_pinboard(params[:loan_id], params[:project_id], params[:user_id], params[:klass])
    when 'collection_object_filter'
      batch_create_from_collection_object_filter(
        params[:loan_id],
        params[:project_id],
        params[:user_id],
        params[:collection_object_query])
    end
  end

  # @return [Hash]
  def self.batch_move(params)
    return false if params[:loan_id].blank? || params[:disposition].blank? || params[:user_id].blank? || params[:date_returned].blank?

    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])
    return false if a.all.count == 0

    moved = []
    unmoved = []

    begin
      a.all.each do |co|
        new_loan_item = nil

        # Only match open loan items
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)

          new_loan_item = b.close_and_move(params[:loan_id], params[:date_returned], params[:disposition], params[:user_id])

          if new_loan_item.nil?
            unmoved.push b
          else
            moved.push new_loan_item
          end
        end
      end

    rescue ActiveRecord::RecordInvalid => e
     # raise e
    end

    return { moved:, unmoved: }
  end

  # @param param[:collection_object_query] required
  #
  # Return all CollectionObjects matching the query. Does not yet work with OtuQuery
  def self.batch_return(params)
    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])

    return false if a.all.count == 0

    returned = []
    unreturned = []

    begin
      a.all.each do |co|
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)
          begin
            b.update!(disposition: params[:disposition], date_returned: params[:date_returned])
            returned.push b
          rescue ActiveRecord::RecordInvalid
            unreturned.push b
          end
        end
      end
    end
    return {returned:, unreturned:}
  end

  def close_and_move(to_loan_id, date_returned, disposition, user_id)
    return nil if to_loan_id.blank?

    new_loan_item = nil
    LoanItem.transaction do
      begin
        update!(date_returned:, disposition:)

        # Reset cached association so on_loan? returns correct value
        loan_item_object.association(:loan_item).reset

        new_loan_item = LoanItem.create!(
          project_id:,
          loan_item_object:,
          loan_id: to_loan_id
          )

      rescue ActiveRecord::RecordInvalid => e
        #raise e
      end
    end
    new_loan_item
  end

  def self.batch_create_from_collection_object_filter(loan_id, project_id, user_id, collection_object_filter)
    created = []
    query = Queries::CollectionObject::Filter.new(collection_object_filter)
    LoanItem.transaction do
      begin
        query.all.each do |co|
          i = LoanItem.create!(loan_item_object: co, by: user_id, loan_id:, project_id:)
          if i.persisted?
            created.push i
          end
        end
      rescue ActiveRecord::RecordInvalid => e
        # raise e
      end
    end
    return created
  end

  def self.batch_create_from_tags(keyword_id, klass, loan_id)
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:tags).where(tags: {keyword_id:}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          Tag.where(keyword_id:).where(tag_object_type: ['Container', 'Otu', 'CollectionObject']).distinct.all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.tag_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end


  def self.batch_create_from_pinboard(loan_id, project_id, user_id, klass)
    return false if loan_id.blank? || project_id.blank? || user_id.blank?
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:pinboard_items).where(pinboard_items: {user_id:, project_id:, pinned_object_type: klass}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          PinboardItem.where(project_id:, user_id:, pinned_object_type: ['Container', 'Otu', 'CollectionObject']).all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.pinned_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end

  protected

  # Whether this class of objects is in fact loanable, not
  # whether it's on loan or not.
  def object_loanable_check
    loan_item_object && loan_item_object.respond_to?(:is_loanable?)
  end

  # Code, not out-on-loan check!
  def loan_object_is_loanable
    if !persisted? # if it is, then this check should not be necessary
      if !object_loanable_check
        errors.add(:loan_item_object, 'is not loanble')
      end
    end
  end

  def total_provided_only_when_otu
    errors.add(:total, 'only providable when item is an OTU.') if total && loan_item_object_type != 'Otu'
  end

  # Is not already in a loan item if CollectionObject/Container
  def available_for_loan
    if !persisted? # if it is, then this check should not be necessary
      if object_loanable_check
        if loan_item_object_type == 'Otu'
          true
        else
          if loan_item_object.on_loan? # takes into account Containers!
            errors.add(:loan_item_object, 'is already on loan')
          end
        end
      end
    end
  end

end

#loan_idInteger

Id of the loan

Returns:

  • (Integer)


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

class LoanItem < ApplicationRecord
  acts_as_list scope: [:loan, :project_id]

  include Housekeeping
  include Shared::DataAttributes
  include Shared::Notes
  include Shared::Tags
  include Shared::IsData
  include Shared::BatchByFilterScope

  attr_accessor :date_returned_jquery

  STATUS = ['Destroyed', 'Donated', 'Lost', 'Retained', 'Returned'].freeze

  belongs_to :loan, inverse_of: :loan_items
  belongs_to :loan_item_object, polymorphic: true

  validates_presence_of :loan_item_object

  validates :loan, presence: true

  # validates_uniqueness_of :loan, scope: [:loan_item_object_type, :loan_item_object_id]

  validate :total_provided_only_when_otu

  validate :loan_object_is_loanable

  validate :available_for_loan

  validates_uniqueness_of :loan_id, scope: [:loan_item_object_id, :loan_item_object_type], if: -> { loan_item_object_type == 'CollectionObject' }

  validates_inclusion_of :disposition, in: STATUS, if: -> {disposition.present?}

  def global_entity
    self.loan_item_object.to_global_id if self.loan_item_object.present?
  end

  def global_entity=(entity)
    self.loan_item_object = GlobalID::Locator.locate entity
  end

  def date_returned_jquery=(date)
    self.date_returned = date.gsub(/(\d+)\/(\d+)\/(\d+)/, '\2/\1/\3')
  end

  def date_returned_jquery
    self.date_returned
  end

  def returned?
    date_returned.present?
  end

  # @return [Integer, nil]
  #   the total items this loan line item represent
  # TODO: this does not factor in nested items in a container
  def total_items
    case loan_item_object_type
      when 'Otu'
        total ? total : nil
      when 'Container'
        t = 0
        loan_item_object.all_contained_objects.each do |o|
          if o.kind_of?(::CollectionObject)
            t += o.total
          end
        end
        t
      when 'CollectionObject'
        loan_item_object.total.to_i
      else
        nil
    end
  end

  # @return [Array]
  #   all objects that can have a taxon determination applied to them for this loan item
  def determinable_objects
    # this loan item which may be a container, an OTU, or a collection object
    case loan_item_object_type
    when /contain/i # if this item is a container, dig into the container for the collection objects themselves
      loan_item_object.collection_objects
    when /object/i # if this item is a collection object, just add the object
      [loan_item_object]
    when /otu/i # not strictly needed, but helps keep track of what the loan_item is.
      [] # can't use an OTU as a determination object.
    end
  end

  # @params :ids -> an ID of a loan_item
  def self.batch_determine_loan_items(ids: [], params: {})
    return false if ids.empty?
    # these objects will be created/persisted to be used for each of the loan items identified by the input ids
    td = TaxonDetermination.new(params) # build a td from the input data

    begin
      LoanItem.transaction do
        item_list = [] # Array of objects that can have a taxon determination
        LoanItem.where(id: ids).each do |li|
          item_list.push li.determinable_objects
        end

        item_list.flatten!

        first = item_list.pop
        td.taxon_determination_object = first
        td.save! # create and save the first one so we can dup it in the next step

        item_list.each do |item|
          n = td.dup
          n.determiners << td.determiners
          n.taxon_determination_object = item
          n.save
          n.move_to_top
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    true
  end

  # @param batch_response [BatchResponse]
  # @param query [ActiveRecord::Relation] the filtered collection objects
  # @param hash_query [Hash] the filter query as a hash (for async)
  # @param mode [Symbol] :add
  # @param params [Hash] must include :loan_id
  # @param async [Boolean]
  # @param project_id [Integer]
  # @param user_id [Integer]
  # @param called_from_async [Boolean]
  # @return [BatchResponse]
  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
  )
    async = false if called_from_async == true
    r = batch_response

    case mode.to_sym
    when :add
      loan_id = params[:loan_id]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.new(
            loan_item_object: collection_object,
            loan_id:,
            project_id:,
            by: user_id
          )

          if loan_item.save
            r.updated.push loan_item.id
          else
            r.not_updated.push collection_object.id
            loan_item.errors.full_messages.each do |msg|
              r.validation_errors[msg] += 1
            end
          end
        end
      end

    when :return
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            if loan_item.update(disposition:, date_returned:)
              r.updated.push loan_item.id
            else
              r.not_updated.push collection_object.id
              loan_item.errors.full_messages.each do |msg|
                r.validation_errors[msg] += 1
              end
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end

    when :move
      loan_id = params[:loan_id]
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            loan_item.loan_item_object.association(:loan_item).reset
            new_loan_item = loan_item.close_and_move(loan_id, date_returned, disposition, user_id)

            if new_loan_item
              r.updated.push new_loan_item.id
            else
              r.not_updated.push collection_object.id
              r.validation_errors['either return or move of loan item failed'] += 1
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end
    end

    r
  end

  # TODO: param handling is currently all kinds of "meh"
  def self.batch_create(params)
    case params[:batch_type]
    when 'tags'
      batch_create_from_tags(params[:keyword_id], params[:klass], params[:loan_id])
    when 'pinboard'
      batch_create_from_pinboard(params[:loan_id], params[:project_id], params[:user_id], params[:klass])
    when 'collection_object_filter'
      batch_create_from_collection_object_filter(
        params[:loan_id],
        params[:project_id],
        params[:user_id],
        params[:collection_object_query])
    end
  end

  # @return [Hash]
  def self.batch_move(params)
    return false if params[:loan_id].blank? || params[:disposition].blank? || params[:user_id].blank? || params[:date_returned].blank?

    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])
    return false if a.all.count == 0

    moved = []
    unmoved = []

    begin
      a.all.each do |co|
        new_loan_item = nil

        # Only match open loan items
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)

          new_loan_item = b.close_and_move(params[:loan_id], params[:date_returned], params[:disposition], params[:user_id])

          if new_loan_item.nil?
            unmoved.push b
          else
            moved.push new_loan_item
          end
        end
      end

    rescue ActiveRecord::RecordInvalid => e
     # raise e
    end

    return { moved:, unmoved: }
  end

  # @param param[:collection_object_query] required
  #
  # Return all CollectionObjects matching the query. Does not yet work with OtuQuery
  def self.batch_return(params)
    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])

    return false if a.all.count == 0

    returned = []
    unreturned = []

    begin
      a.all.each do |co|
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)
          begin
            b.update!(disposition: params[:disposition], date_returned: params[:date_returned])
            returned.push b
          rescue ActiveRecord::RecordInvalid
            unreturned.push b
          end
        end
      end
    end
    return {returned:, unreturned:}
  end

  def close_and_move(to_loan_id, date_returned, disposition, user_id)
    return nil if to_loan_id.blank?

    new_loan_item = nil
    LoanItem.transaction do
      begin
        update!(date_returned:, disposition:)

        # Reset cached association so on_loan? returns correct value
        loan_item_object.association(:loan_item).reset

        new_loan_item = LoanItem.create!(
          project_id:,
          loan_item_object:,
          loan_id: to_loan_id
          )

      rescue ActiveRecord::RecordInvalid => e
        #raise e
      end
    end
    new_loan_item
  end

  def self.batch_create_from_collection_object_filter(loan_id, project_id, user_id, collection_object_filter)
    created = []
    query = Queries::CollectionObject::Filter.new(collection_object_filter)
    LoanItem.transaction do
      begin
        query.all.each do |co|
          i = LoanItem.create!(loan_item_object: co, by: user_id, loan_id:, project_id:)
          if i.persisted?
            created.push i
          end
        end
      rescue ActiveRecord::RecordInvalid => e
        # raise e
      end
    end
    return created
  end

  def self.batch_create_from_tags(keyword_id, klass, loan_id)
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:tags).where(tags: {keyword_id:}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          Tag.where(keyword_id:).where(tag_object_type: ['Container', 'Otu', 'CollectionObject']).distinct.all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.tag_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end


  def self.batch_create_from_pinboard(loan_id, project_id, user_id, klass)
    return false if loan_id.blank? || project_id.blank? || user_id.blank?
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:pinboard_items).where(pinboard_items: {user_id:, project_id:, pinned_object_type: klass}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          PinboardItem.where(project_id:, user_id:, pinned_object_type: ['Container', 'Otu', 'CollectionObject']).all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.pinned_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end

  protected

  # Whether this class of objects is in fact loanable, not
  # whether it's on loan or not.
  def object_loanable_check
    loan_item_object && loan_item_object.respond_to?(:is_loanable?)
  end

  # Code, not out-on-loan check!
  def loan_object_is_loanable
    if !persisted? # if it is, then this check should not be necessary
      if !object_loanable_check
        errors.add(:loan_item_object, 'is not loanble')
      end
    end
  end

  def total_provided_only_when_otu
    errors.add(:total, 'only providable when item is an OTU.') if total && loan_item_object_type != 'Otu'
  end

  # Is not already in a loan item if CollectionObject/Container
  def available_for_loan
    if !persisted? # if it is, then this check should not be necessary
      if object_loanable_check
        if loan_item_object_type == 'Otu'
          true
        else
          if loan_item_object.on_loan? # takes into account Containers!
            errors.add(:loan_item_object, 'is already on loan')
          end
        end
      end
    end
  end

end

#loan_item_object_idInteger

Polymorphic, the id of the Container, CollectionObject or Otu

Returns:

  • (Integer)


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

class LoanItem < ApplicationRecord
  acts_as_list scope: [:loan, :project_id]

  include Housekeeping
  include Shared::DataAttributes
  include Shared::Notes
  include Shared::Tags
  include Shared::IsData
  include Shared::BatchByFilterScope

  attr_accessor :date_returned_jquery

  STATUS = ['Destroyed', 'Donated', 'Lost', 'Retained', 'Returned'].freeze

  belongs_to :loan, inverse_of: :loan_items
  belongs_to :loan_item_object, polymorphic: true

  validates_presence_of :loan_item_object

  validates :loan, presence: true

  # validates_uniqueness_of :loan, scope: [:loan_item_object_type, :loan_item_object_id]

  validate :total_provided_only_when_otu

  validate :loan_object_is_loanable

  validate :available_for_loan

  validates_uniqueness_of :loan_id, scope: [:loan_item_object_id, :loan_item_object_type], if: -> { loan_item_object_type == 'CollectionObject' }

  validates_inclusion_of :disposition, in: STATUS, if: -> {disposition.present?}

  def global_entity
    self.loan_item_object.to_global_id if self.loan_item_object.present?
  end

  def global_entity=(entity)
    self.loan_item_object = GlobalID::Locator.locate entity
  end

  def date_returned_jquery=(date)
    self.date_returned = date.gsub(/(\d+)\/(\d+)\/(\d+)/, '\2/\1/\3')
  end

  def date_returned_jquery
    self.date_returned
  end

  def returned?
    date_returned.present?
  end

  # @return [Integer, nil]
  #   the total items this loan line item represent
  # TODO: this does not factor in nested items in a container
  def total_items
    case loan_item_object_type
      when 'Otu'
        total ? total : nil
      when 'Container'
        t = 0
        loan_item_object.all_contained_objects.each do |o|
          if o.kind_of?(::CollectionObject)
            t += o.total
          end
        end
        t
      when 'CollectionObject'
        loan_item_object.total.to_i
      else
        nil
    end
  end

  # @return [Array]
  #   all objects that can have a taxon determination applied to them for this loan item
  def determinable_objects
    # this loan item which may be a container, an OTU, or a collection object
    case loan_item_object_type
    when /contain/i # if this item is a container, dig into the container for the collection objects themselves
      loan_item_object.collection_objects
    when /object/i # if this item is a collection object, just add the object
      [loan_item_object]
    when /otu/i # not strictly needed, but helps keep track of what the loan_item is.
      [] # can't use an OTU as a determination object.
    end
  end

  # @params :ids -> an ID of a loan_item
  def self.batch_determine_loan_items(ids: [], params: {})
    return false if ids.empty?
    # these objects will be created/persisted to be used for each of the loan items identified by the input ids
    td = TaxonDetermination.new(params) # build a td from the input data

    begin
      LoanItem.transaction do
        item_list = [] # Array of objects that can have a taxon determination
        LoanItem.where(id: ids).each do |li|
          item_list.push li.determinable_objects
        end

        item_list.flatten!

        first = item_list.pop
        td.taxon_determination_object = first
        td.save! # create and save the first one so we can dup it in the next step

        item_list.each do |item|
          n = td.dup
          n.determiners << td.determiners
          n.taxon_determination_object = item
          n.save
          n.move_to_top
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    true
  end

  # @param batch_response [BatchResponse]
  # @param query [ActiveRecord::Relation] the filtered collection objects
  # @param hash_query [Hash] the filter query as a hash (for async)
  # @param mode [Symbol] :add
  # @param params [Hash] must include :loan_id
  # @param async [Boolean]
  # @param project_id [Integer]
  # @param user_id [Integer]
  # @param called_from_async [Boolean]
  # @return [BatchResponse]
  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
  )
    async = false if called_from_async == true
    r = batch_response

    case mode.to_sym
    when :add
      loan_id = params[:loan_id]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.new(
            loan_item_object: collection_object,
            loan_id:,
            project_id:,
            by: user_id
          )

          if loan_item.save
            r.updated.push loan_item.id
          else
            r.not_updated.push collection_object.id
            loan_item.errors.full_messages.each do |msg|
              r.validation_errors[msg] += 1
            end
          end
        end
      end

    when :return
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            if loan_item.update(disposition:, date_returned:)
              r.updated.push loan_item.id
            else
              r.not_updated.push collection_object.id
              loan_item.errors.full_messages.each do |msg|
                r.validation_errors[msg] += 1
              end
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end

    when :move
      loan_id = params[:loan_id]
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            loan_item.loan_item_object.association(:loan_item).reset
            new_loan_item = loan_item.close_and_move(loan_id, date_returned, disposition, user_id)

            if new_loan_item
              r.updated.push new_loan_item.id
            else
              r.not_updated.push collection_object.id
              r.validation_errors['either return or move of loan item failed'] += 1
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end
    end

    r
  end

  # TODO: param handling is currently all kinds of "meh"
  def self.batch_create(params)
    case params[:batch_type]
    when 'tags'
      batch_create_from_tags(params[:keyword_id], params[:klass], params[:loan_id])
    when 'pinboard'
      batch_create_from_pinboard(params[:loan_id], params[:project_id], params[:user_id], params[:klass])
    when 'collection_object_filter'
      batch_create_from_collection_object_filter(
        params[:loan_id],
        params[:project_id],
        params[:user_id],
        params[:collection_object_query])
    end
  end

  # @return [Hash]
  def self.batch_move(params)
    return false if params[:loan_id].blank? || params[:disposition].blank? || params[:user_id].blank? || params[:date_returned].blank?

    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])
    return false if a.all.count == 0

    moved = []
    unmoved = []

    begin
      a.all.each do |co|
        new_loan_item = nil

        # Only match open loan items
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)

          new_loan_item = b.close_and_move(params[:loan_id], params[:date_returned], params[:disposition], params[:user_id])

          if new_loan_item.nil?
            unmoved.push b
          else
            moved.push new_loan_item
          end
        end
      end

    rescue ActiveRecord::RecordInvalid => e
     # raise e
    end

    return { moved:, unmoved: }
  end

  # @param param[:collection_object_query] required
  #
  # Return all CollectionObjects matching the query. Does not yet work with OtuQuery
  def self.batch_return(params)
    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])

    return false if a.all.count == 0

    returned = []
    unreturned = []

    begin
      a.all.each do |co|
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)
          begin
            b.update!(disposition: params[:disposition], date_returned: params[:date_returned])
            returned.push b
          rescue ActiveRecord::RecordInvalid
            unreturned.push b
          end
        end
      end
    end
    return {returned:, unreturned:}
  end

  def close_and_move(to_loan_id, date_returned, disposition, user_id)
    return nil if to_loan_id.blank?

    new_loan_item = nil
    LoanItem.transaction do
      begin
        update!(date_returned:, disposition:)

        # Reset cached association so on_loan? returns correct value
        loan_item_object.association(:loan_item).reset

        new_loan_item = LoanItem.create!(
          project_id:,
          loan_item_object:,
          loan_id: to_loan_id
          )

      rescue ActiveRecord::RecordInvalid => e
        #raise e
      end
    end
    new_loan_item
  end

  def self.batch_create_from_collection_object_filter(loan_id, project_id, user_id, collection_object_filter)
    created = []
    query = Queries::CollectionObject::Filter.new(collection_object_filter)
    LoanItem.transaction do
      begin
        query.all.each do |co|
          i = LoanItem.create!(loan_item_object: co, by: user_id, loan_id:, project_id:)
          if i.persisted?
            created.push i
          end
        end
      rescue ActiveRecord::RecordInvalid => e
        # raise e
      end
    end
    return created
  end

  def self.batch_create_from_tags(keyword_id, klass, loan_id)
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:tags).where(tags: {keyword_id:}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          Tag.where(keyword_id:).where(tag_object_type: ['Container', 'Otu', 'CollectionObject']).distinct.all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.tag_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end


  def self.batch_create_from_pinboard(loan_id, project_id, user_id, klass)
    return false if loan_id.blank? || project_id.blank? || user_id.blank?
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:pinboard_items).where(pinboard_items: {user_id:, project_id:, pinned_object_type: klass}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          PinboardItem.where(project_id:, user_id:, pinned_object_type: ['Container', 'Otu', 'CollectionObject']).all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.pinned_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end

  protected

  # Whether this class of objects is in fact loanable, not
  # whether it's on loan or not.
  def object_loanable_check
    loan_item_object && loan_item_object.respond_to?(:is_loanable?)
  end

  # Code, not out-on-loan check!
  def loan_object_is_loanable
    if !persisted? # if it is, then this check should not be necessary
      if !object_loanable_check
        errors.add(:loan_item_object, 'is not loanble')
      end
    end
  end

  def total_provided_only_when_otu
    errors.add(:total, 'only providable when item is an OTU.') if total && loan_item_object_type != 'Otu'
  end

  # Is not already in a loan item if CollectionObject/Container
  def available_for_loan
    if !persisted? # if it is, then this check should not be necessary
      if object_loanable_check
        if loan_item_object_type == 'Otu'
          true
        else
          if loan_item_object.on_loan? # takes into account Containers!
            errors.add(:loan_item_object, 'is already on loan')
          end
        end
      end
    end
  end

end

#loan_item_object_typeString

Polymorphic- one of Container, CollectionObject, or Otu

Returns:

  • (String)


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

class LoanItem < ApplicationRecord
  acts_as_list scope: [:loan, :project_id]

  include Housekeeping
  include Shared::DataAttributes
  include Shared::Notes
  include Shared::Tags
  include Shared::IsData
  include Shared::BatchByFilterScope

  attr_accessor :date_returned_jquery

  STATUS = ['Destroyed', 'Donated', 'Lost', 'Retained', 'Returned'].freeze

  belongs_to :loan, inverse_of: :loan_items
  belongs_to :loan_item_object, polymorphic: true

  validates_presence_of :loan_item_object

  validates :loan, presence: true

  # validates_uniqueness_of :loan, scope: [:loan_item_object_type, :loan_item_object_id]

  validate :total_provided_only_when_otu

  validate :loan_object_is_loanable

  validate :available_for_loan

  validates_uniqueness_of :loan_id, scope: [:loan_item_object_id, :loan_item_object_type], if: -> { loan_item_object_type == 'CollectionObject' }

  validates_inclusion_of :disposition, in: STATUS, if: -> {disposition.present?}

  def global_entity
    self.loan_item_object.to_global_id if self.loan_item_object.present?
  end

  def global_entity=(entity)
    self.loan_item_object = GlobalID::Locator.locate entity
  end

  def date_returned_jquery=(date)
    self.date_returned = date.gsub(/(\d+)\/(\d+)\/(\d+)/, '\2/\1/\3')
  end

  def date_returned_jquery
    self.date_returned
  end

  def returned?
    date_returned.present?
  end

  # @return [Integer, nil]
  #   the total items this loan line item represent
  # TODO: this does not factor in nested items in a container
  def total_items
    case loan_item_object_type
      when 'Otu'
        total ? total : nil
      when 'Container'
        t = 0
        loan_item_object.all_contained_objects.each do |o|
          if o.kind_of?(::CollectionObject)
            t += o.total
          end
        end
        t
      when 'CollectionObject'
        loan_item_object.total.to_i
      else
        nil
    end
  end

  # @return [Array]
  #   all objects that can have a taxon determination applied to them for this loan item
  def determinable_objects
    # this loan item which may be a container, an OTU, or a collection object
    case loan_item_object_type
    when /contain/i # if this item is a container, dig into the container for the collection objects themselves
      loan_item_object.collection_objects
    when /object/i # if this item is a collection object, just add the object
      [loan_item_object]
    when /otu/i # not strictly needed, but helps keep track of what the loan_item is.
      [] # can't use an OTU as a determination object.
    end
  end

  # @params :ids -> an ID of a loan_item
  def self.batch_determine_loan_items(ids: [], params: {})
    return false if ids.empty?
    # these objects will be created/persisted to be used for each of the loan items identified by the input ids
    td = TaxonDetermination.new(params) # build a td from the input data

    begin
      LoanItem.transaction do
        item_list = [] # Array of objects that can have a taxon determination
        LoanItem.where(id: ids).each do |li|
          item_list.push li.determinable_objects
        end

        item_list.flatten!

        first = item_list.pop
        td.taxon_determination_object = first
        td.save! # create and save the first one so we can dup it in the next step

        item_list.each do |item|
          n = td.dup
          n.determiners << td.determiners
          n.taxon_determination_object = item
          n.save
          n.move_to_top
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    true
  end

  # @param batch_response [BatchResponse]
  # @param query [ActiveRecord::Relation] the filtered collection objects
  # @param hash_query [Hash] the filter query as a hash (for async)
  # @param mode [Symbol] :add
  # @param params [Hash] must include :loan_id
  # @param async [Boolean]
  # @param project_id [Integer]
  # @param user_id [Integer]
  # @param called_from_async [Boolean]
  # @return [BatchResponse]
  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
  )
    async = false if called_from_async == true
    r = batch_response

    case mode.to_sym
    when :add
      loan_id = params[:loan_id]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.new(
            loan_item_object: collection_object,
            loan_id:,
            project_id:,
            by: user_id
          )

          if loan_item.save
            r.updated.push loan_item.id
          else
            r.not_updated.push collection_object.id
            loan_item.errors.full_messages.each do |msg|
              r.validation_errors[msg] += 1
            end
          end
        end
      end

    when :return
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            if loan_item.update(disposition:, date_returned:)
              r.updated.push loan_item.id
            else
              r.not_updated.push collection_object.id
              loan_item.errors.full_messages.each do |msg|
                r.validation_errors[msg] += 1
              end
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end

    when :move
      loan_id = params[:loan_id]
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            loan_item.loan_item_object.association(:loan_item).reset
            new_loan_item = loan_item.close_and_move(loan_id, date_returned, disposition, user_id)

            if new_loan_item
              r.updated.push new_loan_item.id
            else
              r.not_updated.push collection_object.id
              r.validation_errors['either return or move of loan item failed'] += 1
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end
    end

    r
  end

  # TODO: param handling is currently all kinds of "meh"
  def self.batch_create(params)
    case params[:batch_type]
    when 'tags'
      batch_create_from_tags(params[:keyword_id], params[:klass], params[:loan_id])
    when 'pinboard'
      batch_create_from_pinboard(params[:loan_id], params[:project_id], params[:user_id], params[:klass])
    when 'collection_object_filter'
      batch_create_from_collection_object_filter(
        params[:loan_id],
        params[:project_id],
        params[:user_id],
        params[:collection_object_query])
    end
  end

  # @return [Hash]
  def self.batch_move(params)
    return false if params[:loan_id].blank? || params[:disposition].blank? || params[:user_id].blank? || params[:date_returned].blank?

    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])
    return false if a.all.count == 0

    moved = []
    unmoved = []

    begin
      a.all.each do |co|
        new_loan_item = nil

        # Only match open loan items
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)

          new_loan_item = b.close_and_move(params[:loan_id], params[:date_returned], params[:disposition], params[:user_id])

          if new_loan_item.nil?
            unmoved.push b
          else
            moved.push new_loan_item
          end
        end
      end

    rescue ActiveRecord::RecordInvalid => e
     # raise e
    end

    return { moved:, unmoved: }
  end

  # @param param[:collection_object_query] required
  #
  # Return all CollectionObjects matching the query. Does not yet work with OtuQuery
  def self.batch_return(params)
    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])

    return false if a.all.count == 0

    returned = []
    unreturned = []

    begin
      a.all.each do |co|
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)
          begin
            b.update!(disposition: params[:disposition], date_returned: params[:date_returned])
            returned.push b
          rescue ActiveRecord::RecordInvalid
            unreturned.push b
          end
        end
      end
    end
    return {returned:, unreturned:}
  end

  def close_and_move(to_loan_id, date_returned, disposition, user_id)
    return nil if to_loan_id.blank?

    new_loan_item = nil
    LoanItem.transaction do
      begin
        update!(date_returned:, disposition:)

        # Reset cached association so on_loan? returns correct value
        loan_item_object.association(:loan_item).reset

        new_loan_item = LoanItem.create!(
          project_id:,
          loan_item_object:,
          loan_id: to_loan_id
          )

      rescue ActiveRecord::RecordInvalid => e
        #raise e
      end
    end
    new_loan_item
  end

  def self.batch_create_from_collection_object_filter(loan_id, project_id, user_id, collection_object_filter)
    created = []
    query = Queries::CollectionObject::Filter.new(collection_object_filter)
    LoanItem.transaction do
      begin
        query.all.each do |co|
          i = LoanItem.create!(loan_item_object: co, by: user_id, loan_id:, project_id:)
          if i.persisted?
            created.push i
          end
        end
      rescue ActiveRecord::RecordInvalid => e
        # raise e
      end
    end
    return created
  end

  def self.batch_create_from_tags(keyword_id, klass, loan_id)
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:tags).where(tags: {keyword_id:}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          Tag.where(keyword_id:).where(tag_object_type: ['Container', 'Otu', 'CollectionObject']).distinct.all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.tag_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end


  def self.batch_create_from_pinboard(loan_id, project_id, user_id, klass)
    return false if loan_id.blank? || project_id.blank? || user_id.blank?
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:pinboard_items).where(pinboard_items: {user_id:, project_id:, pinned_object_type: klass}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          PinboardItem.where(project_id:, user_id:, pinned_object_type: ['Container', 'Otu', 'CollectionObject']).all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.pinned_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end

  protected

  # Whether this class of objects is in fact loanable, not
  # whether it's on loan or not.
  def object_loanable_check
    loan_item_object && loan_item_object.respond_to?(:is_loanable?)
  end

  # Code, not out-on-loan check!
  def loan_object_is_loanable
    if !persisted? # if it is, then this check should not be necessary
      if !object_loanable_check
        errors.add(:loan_item_object, 'is not loanble')
      end
    end
  end

  def total_provided_only_when_otu
    errors.add(:total, 'only providable when item is an OTU.') if total && loan_item_object_type != 'Otu'
  end

  # Is not already in a loan item if CollectionObject/Container
  def available_for_loan
    if !persisted? # if it is, then this check should not be necessary
      if object_loanable_check
        if loan_item_object_type == 'Otu'
          true
        else
          if loan_item_object.on_loan? # takes into account Containers!
            errors.add(:loan_item_object, 'is already on loan')
          end
        end
      end
    end
  end

end

#positionInteger

Returns Sorts the items in relation to the loan.

Returns:

  • (Integer)

    Sorts the items in relation to the loan.



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

class LoanItem < ApplicationRecord
  acts_as_list scope: [:loan, :project_id]

  include Housekeeping
  include Shared::DataAttributes
  include Shared::Notes
  include Shared::Tags
  include Shared::IsData
  include Shared::BatchByFilterScope

  attr_accessor :date_returned_jquery

  STATUS = ['Destroyed', 'Donated', 'Lost', 'Retained', 'Returned'].freeze

  belongs_to :loan, inverse_of: :loan_items
  belongs_to :loan_item_object, polymorphic: true

  validates_presence_of :loan_item_object

  validates :loan, presence: true

  # validates_uniqueness_of :loan, scope: [:loan_item_object_type, :loan_item_object_id]

  validate :total_provided_only_when_otu

  validate :loan_object_is_loanable

  validate :available_for_loan

  validates_uniqueness_of :loan_id, scope: [:loan_item_object_id, :loan_item_object_type], if: -> { loan_item_object_type == 'CollectionObject' }

  validates_inclusion_of :disposition, in: STATUS, if: -> {disposition.present?}

  def global_entity
    self.loan_item_object.to_global_id if self.loan_item_object.present?
  end

  def global_entity=(entity)
    self.loan_item_object = GlobalID::Locator.locate entity
  end

  def date_returned_jquery=(date)
    self.date_returned = date.gsub(/(\d+)\/(\d+)\/(\d+)/, '\2/\1/\3')
  end

  def date_returned_jquery
    self.date_returned
  end

  def returned?
    date_returned.present?
  end

  # @return [Integer, nil]
  #   the total items this loan line item represent
  # TODO: this does not factor in nested items in a container
  def total_items
    case loan_item_object_type
      when 'Otu'
        total ? total : nil
      when 'Container'
        t = 0
        loan_item_object.all_contained_objects.each do |o|
          if o.kind_of?(::CollectionObject)
            t += o.total
          end
        end
        t
      when 'CollectionObject'
        loan_item_object.total.to_i
      else
        nil
    end
  end

  # @return [Array]
  #   all objects that can have a taxon determination applied to them for this loan item
  def determinable_objects
    # this loan item which may be a container, an OTU, or a collection object
    case loan_item_object_type
    when /contain/i # if this item is a container, dig into the container for the collection objects themselves
      loan_item_object.collection_objects
    when /object/i # if this item is a collection object, just add the object
      [loan_item_object]
    when /otu/i # not strictly needed, but helps keep track of what the loan_item is.
      [] # can't use an OTU as a determination object.
    end
  end

  # @params :ids -> an ID of a loan_item
  def self.batch_determine_loan_items(ids: [], params: {})
    return false if ids.empty?
    # these objects will be created/persisted to be used for each of the loan items identified by the input ids
    td = TaxonDetermination.new(params) # build a td from the input data

    begin
      LoanItem.transaction do
        item_list = [] # Array of objects that can have a taxon determination
        LoanItem.where(id: ids).each do |li|
          item_list.push li.determinable_objects
        end

        item_list.flatten!

        first = item_list.pop
        td.taxon_determination_object = first
        td.save! # create and save the first one so we can dup it in the next step

        item_list.each do |item|
          n = td.dup
          n.determiners << td.determiners
          n.taxon_determination_object = item
          n.save
          n.move_to_top
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    true
  end

  # @param batch_response [BatchResponse]
  # @param query [ActiveRecord::Relation] the filtered collection objects
  # @param hash_query [Hash] the filter query as a hash (for async)
  # @param mode [Symbol] :add
  # @param params [Hash] must include :loan_id
  # @param async [Boolean]
  # @param project_id [Integer]
  # @param user_id [Integer]
  # @param called_from_async [Boolean]
  # @return [BatchResponse]
  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
  )
    async = false if called_from_async == true
    r = batch_response

    case mode.to_sym
    when :add
      loan_id = params[:loan_id]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.new(
            loan_item_object: collection_object,
            loan_id:,
            project_id:,
            by: user_id
          )

          if loan_item.save
            r.updated.push loan_item.id
          else
            r.not_updated.push collection_object.id
            loan_item.errors.full_messages.each do |msg|
              r.validation_errors[msg] += 1
            end
          end
        end
      end

    when :return
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            if loan_item.update(disposition:, date_returned:)
              r.updated.push loan_item.id
            else
              r.not_updated.push collection_object.id
              loan_item.errors.full_messages.each do |msg|
                r.validation_errors[msg] += 1
              end
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end

    when :move
      loan_id = params[:loan_id]
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            loan_item.loan_item_object.association(:loan_item).reset
            new_loan_item = loan_item.close_and_move(loan_id, date_returned, disposition, user_id)

            if new_loan_item
              r.updated.push new_loan_item.id
            else
              r.not_updated.push collection_object.id
              r.validation_errors['either return or move of loan item failed'] += 1
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end
    end

    r
  end

  # TODO: param handling is currently all kinds of "meh"
  def self.batch_create(params)
    case params[:batch_type]
    when 'tags'
      batch_create_from_tags(params[:keyword_id], params[:klass], params[:loan_id])
    when 'pinboard'
      batch_create_from_pinboard(params[:loan_id], params[:project_id], params[:user_id], params[:klass])
    when 'collection_object_filter'
      batch_create_from_collection_object_filter(
        params[:loan_id],
        params[:project_id],
        params[:user_id],
        params[:collection_object_query])
    end
  end

  # @return [Hash]
  def self.batch_move(params)
    return false if params[:loan_id].blank? || params[:disposition].blank? || params[:user_id].blank? || params[:date_returned].blank?

    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])
    return false if a.all.count == 0

    moved = []
    unmoved = []

    begin
      a.all.each do |co|
        new_loan_item = nil

        # Only match open loan items
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)

          new_loan_item = b.close_and_move(params[:loan_id], params[:date_returned], params[:disposition], params[:user_id])

          if new_loan_item.nil?
            unmoved.push b
          else
            moved.push new_loan_item
          end
        end
      end

    rescue ActiveRecord::RecordInvalid => e
     # raise e
    end

    return { moved:, unmoved: }
  end

  # @param param[:collection_object_query] required
  #
  # Return all CollectionObjects matching the query. Does not yet work with OtuQuery
  def self.batch_return(params)
    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])

    return false if a.all.count == 0

    returned = []
    unreturned = []

    begin
      a.all.each do |co|
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)
          begin
            b.update!(disposition: params[:disposition], date_returned: params[:date_returned])
            returned.push b
          rescue ActiveRecord::RecordInvalid
            unreturned.push b
          end
        end
      end
    end
    return {returned:, unreturned:}
  end

  def close_and_move(to_loan_id, date_returned, disposition, user_id)
    return nil if to_loan_id.blank?

    new_loan_item = nil
    LoanItem.transaction do
      begin
        update!(date_returned:, disposition:)

        # Reset cached association so on_loan? returns correct value
        loan_item_object.association(:loan_item).reset

        new_loan_item = LoanItem.create!(
          project_id:,
          loan_item_object:,
          loan_id: to_loan_id
          )

      rescue ActiveRecord::RecordInvalid => e
        #raise e
      end
    end
    new_loan_item
  end

  def self.batch_create_from_collection_object_filter(loan_id, project_id, user_id, collection_object_filter)
    created = []
    query = Queries::CollectionObject::Filter.new(collection_object_filter)
    LoanItem.transaction do
      begin
        query.all.each do |co|
          i = LoanItem.create!(loan_item_object: co, by: user_id, loan_id:, project_id:)
          if i.persisted?
            created.push i
          end
        end
      rescue ActiveRecord::RecordInvalid => e
        # raise e
      end
    end
    return created
  end

  def self.batch_create_from_tags(keyword_id, klass, loan_id)
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:tags).where(tags: {keyword_id:}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          Tag.where(keyword_id:).where(tag_object_type: ['Container', 'Otu', 'CollectionObject']).distinct.all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.tag_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end


  def self.batch_create_from_pinboard(loan_id, project_id, user_id, klass)
    return false if loan_id.blank? || project_id.blank? || user_id.blank?
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:pinboard_items).where(pinboard_items: {user_id:, project_id:, pinned_object_type: klass}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          PinboardItem.where(project_id:, user_id:, pinned_object_type: ['Container', 'Otu', 'CollectionObject']).all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.pinned_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end

  protected

  # Whether this class of objects is in fact loanable, not
  # whether it's on loan or not.
  def object_loanable_check
    loan_item_object && loan_item_object.respond_to?(:is_loanable?)
  end

  # Code, not out-on-loan check!
  def loan_object_is_loanable
    if !persisted? # if it is, then this check should not be necessary
      if !object_loanable_check
        errors.add(:loan_item_object, 'is not loanble')
      end
    end
  end

  def total_provided_only_when_otu
    errors.add(:total, 'only providable when item is an OTU.') if total && loan_item_object_type != 'Otu'
  end

  # Is not already in a loan item if CollectionObject/Container
  def available_for_loan
    if !persisted? # if it is, then this check should not be necessary
      if object_loanable_check
        if loan_item_object_type == 'Otu'
          true
        else
          if loan_item_object.on_loan? # takes into account Containers!
            errors.add(:loan_item_object, 'is already on loan')
          end
        end
      end
    end
  end

end

#project_idInteger

the project ID

Returns:

  • (Integer)


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

class LoanItem < ApplicationRecord
  acts_as_list scope: [:loan, :project_id]

  include Housekeeping
  include Shared::DataAttributes
  include Shared::Notes
  include Shared::Tags
  include Shared::IsData
  include Shared::BatchByFilterScope

  attr_accessor :date_returned_jquery

  STATUS = ['Destroyed', 'Donated', 'Lost', 'Retained', 'Returned'].freeze

  belongs_to :loan, inverse_of: :loan_items
  belongs_to :loan_item_object, polymorphic: true

  validates_presence_of :loan_item_object

  validates :loan, presence: true

  # validates_uniqueness_of :loan, scope: [:loan_item_object_type, :loan_item_object_id]

  validate :total_provided_only_when_otu

  validate :loan_object_is_loanable

  validate :available_for_loan

  validates_uniqueness_of :loan_id, scope: [:loan_item_object_id, :loan_item_object_type], if: -> { loan_item_object_type == 'CollectionObject' }

  validates_inclusion_of :disposition, in: STATUS, if: -> {disposition.present?}

  def global_entity
    self.loan_item_object.to_global_id if self.loan_item_object.present?
  end

  def global_entity=(entity)
    self.loan_item_object = GlobalID::Locator.locate entity
  end

  def date_returned_jquery=(date)
    self.date_returned = date.gsub(/(\d+)\/(\d+)\/(\d+)/, '\2/\1/\3')
  end

  def date_returned_jquery
    self.date_returned
  end

  def returned?
    date_returned.present?
  end

  # @return [Integer, nil]
  #   the total items this loan line item represent
  # TODO: this does not factor in nested items in a container
  def total_items
    case loan_item_object_type
      when 'Otu'
        total ? total : nil
      when 'Container'
        t = 0
        loan_item_object.all_contained_objects.each do |o|
          if o.kind_of?(::CollectionObject)
            t += o.total
          end
        end
        t
      when 'CollectionObject'
        loan_item_object.total.to_i
      else
        nil
    end
  end

  # @return [Array]
  #   all objects that can have a taxon determination applied to them for this loan item
  def determinable_objects
    # this loan item which may be a container, an OTU, or a collection object
    case loan_item_object_type
    when /contain/i # if this item is a container, dig into the container for the collection objects themselves
      loan_item_object.collection_objects
    when /object/i # if this item is a collection object, just add the object
      [loan_item_object]
    when /otu/i # not strictly needed, but helps keep track of what the loan_item is.
      [] # can't use an OTU as a determination object.
    end
  end

  # @params :ids -> an ID of a loan_item
  def self.batch_determine_loan_items(ids: [], params: {})
    return false if ids.empty?
    # these objects will be created/persisted to be used for each of the loan items identified by the input ids
    td = TaxonDetermination.new(params) # build a td from the input data

    begin
      LoanItem.transaction do
        item_list = [] # Array of objects that can have a taxon determination
        LoanItem.where(id: ids).each do |li|
          item_list.push li.determinable_objects
        end

        item_list.flatten!

        first = item_list.pop
        td.taxon_determination_object = first
        td.save! # create and save the first one so we can dup it in the next step

        item_list.each do |item|
          n = td.dup
          n.determiners << td.determiners
          n.taxon_determination_object = item
          n.save
          n.move_to_top
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    true
  end

  # @param batch_response [BatchResponse]
  # @param query [ActiveRecord::Relation] the filtered collection objects
  # @param hash_query [Hash] the filter query as a hash (for async)
  # @param mode [Symbol] :add
  # @param params [Hash] must include :loan_id
  # @param async [Boolean]
  # @param project_id [Integer]
  # @param user_id [Integer]
  # @param called_from_async [Boolean]
  # @return [BatchResponse]
  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
  )
    async = false if called_from_async == true
    r = batch_response

    case mode.to_sym
    when :add
      loan_id = params[:loan_id]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.new(
            loan_item_object: collection_object,
            loan_id:,
            project_id:,
            by: user_id
          )

          if loan_item.save
            r.updated.push loan_item.id
          else
            r.not_updated.push collection_object.id
            loan_item.errors.full_messages.each do |msg|
              r.validation_errors[msg] += 1
            end
          end
        end
      end

    when :return
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            if loan_item.update(disposition:, date_returned:)
              r.updated.push loan_item.id
            else
              r.not_updated.push collection_object.id
              loan_item.errors.full_messages.each do |msg|
                r.validation_errors[msg] += 1
              end
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end

    when :move
      loan_id = params[:loan_id]
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            loan_item.loan_item_object.association(:loan_item).reset
            new_loan_item = loan_item.close_and_move(loan_id, date_returned, disposition, user_id)

            if new_loan_item
              r.updated.push new_loan_item.id
            else
              r.not_updated.push collection_object.id
              r.validation_errors['either return or move of loan item failed'] += 1
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end
    end

    r
  end

  # TODO: param handling is currently all kinds of "meh"
  def self.batch_create(params)
    case params[:batch_type]
    when 'tags'
      batch_create_from_tags(params[:keyword_id], params[:klass], params[:loan_id])
    when 'pinboard'
      batch_create_from_pinboard(params[:loan_id], params[:project_id], params[:user_id], params[:klass])
    when 'collection_object_filter'
      batch_create_from_collection_object_filter(
        params[:loan_id],
        params[:project_id],
        params[:user_id],
        params[:collection_object_query])
    end
  end

  # @return [Hash]
  def self.batch_move(params)
    return false if params[:loan_id].blank? || params[:disposition].blank? || params[:user_id].blank? || params[:date_returned].blank?

    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])
    return false if a.all.count == 0

    moved = []
    unmoved = []

    begin
      a.all.each do |co|
        new_loan_item = nil

        # Only match open loan items
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)

          new_loan_item = b.close_and_move(params[:loan_id], params[:date_returned], params[:disposition], params[:user_id])

          if new_loan_item.nil?
            unmoved.push b
          else
            moved.push new_loan_item
          end
        end
      end

    rescue ActiveRecord::RecordInvalid => e
     # raise e
    end

    return { moved:, unmoved: }
  end

  # @param param[:collection_object_query] required
  #
  # Return all CollectionObjects matching the query. Does not yet work with OtuQuery
  def self.batch_return(params)
    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])

    return false if a.all.count == 0

    returned = []
    unreturned = []

    begin
      a.all.each do |co|
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)
          begin
            b.update!(disposition: params[:disposition], date_returned: params[:date_returned])
            returned.push b
          rescue ActiveRecord::RecordInvalid
            unreturned.push b
          end
        end
      end
    end
    return {returned:, unreturned:}
  end

  def close_and_move(to_loan_id, date_returned, disposition, user_id)
    return nil if to_loan_id.blank?

    new_loan_item = nil
    LoanItem.transaction do
      begin
        update!(date_returned:, disposition:)

        # Reset cached association so on_loan? returns correct value
        loan_item_object.association(:loan_item).reset

        new_loan_item = LoanItem.create!(
          project_id:,
          loan_item_object:,
          loan_id: to_loan_id
          )

      rescue ActiveRecord::RecordInvalid => e
        #raise e
      end
    end
    new_loan_item
  end

  def self.batch_create_from_collection_object_filter(loan_id, project_id, user_id, collection_object_filter)
    created = []
    query = Queries::CollectionObject::Filter.new(collection_object_filter)
    LoanItem.transaction do
      begin
        query.all.each do |co|
          i = LoanItem.create!(loan_item_object: co, by: user_id, loan_id:, project_id:)
          if i.persisted?
            created.push i
          end
        end
      rescue ActiveRecord::RecordInvalid => e
        # raise e
      end
    end
    return created
  end

  def self.batch_create_from_tags(keyword_id, klass, loan_id)
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:tags).where(tags: {keyword_id:}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          Tag.where(keyword_id:).where(tag_object_type: ['Container', 'Otu', 'CollectionObject']).distinct.all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.tag_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end


  def self.batch_create_from_pinboard(loan_id, project_id, user_id, klass)
    return false if loan_id.blank? || project_id.blank? || user_id.blank?
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:pinboard_items).where(pinboard_items: {user_id:, project_id:, pinned_object_type: klass}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          PinboardItem.where(project_id:, user_id:, pinned_object_type: ['Container', 'Otu', 'CollectionObject']).all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.pinned_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end

  protected

  # Whether this class of objects is in fact loanable, not
  # whether it's on loan or not.
  def object_loanable_check
    loan_item_object && loan_item_object.respond_to?(:is_loanable?)
  end

  # Code, not out-on-loan check!
  def loan_object_is_loanable
    if !persisted? # if it is, then this check should not be necessary
      if !object_loanable_check
        errors.add(:loan_item_object, 'is not loanble')
      end
    end
  end

  def total_provided_only_when_otu
    errors.add(:total, 'only providable when item is an OTU.') if total && loan_item_object_type != 'Otu'
  end

  # Is not already in a loan item if CollectionObject/Container
  def available_for_loan
    if !persisted? # if it is, then this check should not be necessary
      if object_loanable_check
        if loan_item_object_type == 'Otu'
          true
        else
          if loan_item_object.on_loan? # takes into account Containers!
            errors.add(:loan_item_object, 'is already on loan')
          end
        end
      end
    end
  end

end

#totalInteger

Returns when type is OTU an arbitrary total can be provided.

Returns:

  • (Integer)

    when type is OTU an arbitrary total can be provided



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

class LoanItem < ApplicationRecord
  acts_as_list scope: [:loan, :project_id]

  include Housekeeping
  include Shared::DataAttributes
  include Shared::Notes
  include Shared::Tags
  include Shared::IsData
  include Shared::BatchByFilterScope

  attr_accessor :date_returned_jquery

  STATUS = ['Destroyed', 'Donated', 'Lost', 'Retained', 'Returned'].freeze

  belongs_to :loan, inverse_of: :loan_items
  belongs_to :loan_item_object, polymorphic: true

  validates_presence_of :loan_item_object

  validates :loan, presence: true

  # validates_uniqueness_of :loan, scope: [:loan_item_object_type, :loan_item_object_id]

  validate :total_provided_only_when_otu

  validate :loan_object_is_loanable

  validate :available_for_loan

  validates_uniqueness_of :loan_id, scope: [:loan_item_object_id, :loan_item_object_type], if: -> { loan_item_object_type == 'CollectionObject' }

  validates_inclusion_of :disposition, in: STATUS, if: -> {disposition.present?}

  def global_entity
    self.loan_item_object.to_global_id if self.loan_item_object.present?
  end

  def global_entity=(entity)
    self.loan_item_object = GlobalID::Locator.locate entity
  end

  def date_returned_jquery=(date)
    self.date_returned = date.gsub(/(\d+)\/(\d+)\/(\d+)/, '\2/\1/\3')
  end

  def date_returned_jquery
    self.date_returned
  end

  def returned?
    date_returned.present?
  end

  # @return [Integer, nil]
  #   the total items this loan line item represent
  # TODO: this does not factor in nested items in a container
  def total_items
    case loan_item_object_type
      when 'Otu'
        total ? total : nil
      when 'Container'
        t = 0
        loan_item_object.all_contained_objects.each do |o|
          if o.kind_of?(::CollectionObject)
            t += o.total
          end
        end
        t
      when 'CollectionObject'
        loan_item_object.total.to_i
      else
        nil
    end
  end

  # @return [Array]
  #   all objects that can have a taxon determination applied to them for this loan item
  def determinable_objects
    # this loan item which may be a container, an OTU, or a collection object
    case loan_item_object_type
    when /contain/i # if this item is a container, dig into the container for the collection objects themselves
      loan_item_object.collection_objects
    when /object/i # if this item is a collection object, just add the object
      [loan_item_object]
    when /otu/i # not strictly needed, but helps keep track of what the loan_item is.
      [] # can't use an OTU as a determination object.
    end
  end

  # @params :ids -> an ID of a loan_item
  def self.batch_determine_loan_items(ids: [], params: {})
    return false if ids.empty?
    # these objects will be created/persisted to be used for each of the loan items identified by the input ids
    td = TaxonDetermination.new(params) # build a td from the input data

    begin
      LoanItem.transaction do
        item_list = [] # Array of objects that can have a taxon determination
        LoanItem.where(id: ids).each do |li|
          item_list.push li.determinable_objects
        end

        item_list.flatten!

        first = item_list.pop
        td.taxon_determination_object = first
        td.save! # create and save the first one so we can dup it in the next step

        item_list.each do |item|
          n = td.dup
          n.determiners << td.determiners
          n.taxon_determination_object = item
          n.save
          n.move_to_top
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    true
  end

  # @param batch_response [BatchResponse]
  # @param query [ActiveRecord::Relation] the filtered collection objects
  # @param hash_query [Hash] the filter query as a hash (for async)
  # @param mode [Symbol] :add
  # @param params [Hash] must include :loan_id
  # @param async [Boolean]
  # @param project_id [Integer]
  # @param user_id [Integer]
  # @param called_from_async [Boolean]
  # @return [BatchResponse]
  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
  )
    async = false if called_from_async == true
    r = batch_response

    case mode.to_sym
    when :add
      loan_id = params[:loan_id]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.new(
            loan_item_object: collection_object,
            loan_id:,
            project_id:,
            by: user_id
          )

          if loan_item.save
            r.updated.push loan_item.id
          else
            r.not_updated.push collection_object.id
            loan_item.errors.full_messages.each do |msg|
              r.validation_errors[msg] += 1
            end
          end
        end
      end

    when :return
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            if loan_item.update(disposition:, date_returned:)
              r.updated.push loan_item.id
            else
              r.not_updated.push collection_object.id
              loan_item.errors.full_messages.each do |msg|
                r.validation_errors[msg] += 1
              end
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end

    when :move
      loan_id = params[:loan_id]
      disposition = params[:disposition]
      date_returned = params[:date_returned]

      if loan_id.blank?
        r.errors['no loan_id provided'] = 1
        return r
      end

      if disposition.blank?
        r.errors['no disposition provided'] = 1
        return r
      end

      if date_returned.blank?
        r.errors['no date_returned provided'] = 1
        return r
      end

      if async && !called_from_async
        BatchByFilterScopeJob.perform_later(
          klass: self.name,
          hash_query:,
          mode:,
          params:,
          project_id:,
          user_id:
        )
      else
        query.find_each do |collection_object|
          loan_item = LoanItem.where(date_returned: nil)
            .find_by(loan_item_object: collection_object, project_id:)

          if loan_item
            loan_item.loan_item_object.association(:loan_item).reset
            new_loan_item = loan_item.close_and_move(loan_id, date_returned, disposition, user_id)

            if new_loan_item
              r.updated.push new_loan_item.id
            else
              r.not_updated.push collection_object.id
              r.validation_errors['either return or move of loan item failed'] += 1
            end
          else
            r.not_updated.push collection_object.id
            r.validation_errors['not currently on loan'] += 1
          end
        end
      end
    end

    r
  end

  # TODO: param handling is currently all kinds of "meh"
  def self.batch_create(params)
    case params[:batch_type]
    when 'tags'
      batch_create_from_tags(params[:keyword_id], params[:klass], params[:loan_id])
    when 'pinboard'
      batch_create_from_pinboard(params[:loan_id], params[:project_id], params[:user_id], params[:klass])
    when 'collection_object_filter'
      batch_create_from_collection_object_filter(
        params[:loan_id],
        params[:project_id],
        params[:user_id],
        params[:collection_object_query])
    end
  end

  # @return [Hash]
  def self.batch_move(params)
    return false if params[:loan_id].blank? || params[:disposition].blank? || params[:user_id].blank? || params[:date_returned].blank?

    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])
    return false if a.all.count == 0

    moved = []
    unmoved = []

    begin
      a.all.each do |co|
        new_loan_item = nil

        # Only match open loan items
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)

          new_loan_item = b.close_and_move(params[:loan_id], params[:date_returned], params[:disposition], params[:user_id])

          if new_loan_item.nil?
            unmoved.push b
          else
            moved.push new_loan_item
          end
        end
      end

    rescue ActiveRecord::RecordInvalid => e
     # raise e
    end

    return { moved:, unmoved: }
  end

  # @param param[:collection_object_query] required
  #
  # Return all CollectionObjects matching the query. Does not yet work with OtuQuery
  def self.batch_return(params)
    a = Queries::CollectionObject::Filter.new(params[:collection_object_query])

    return false if a.all.count == 0

    returned = []
    unreturned = []

    begin
      a.all.each do |co|
        if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)
          begin
            b.update!(disposition: params[:disposition], date_returned: params[:date_returned])
            returned.push b
          rescue ActiveRecord::RecordInvalid
            unreturned.push b
          end
        end
      end
    end
    return {returned:, unreturned:}
  end

  def close_and_move(to_loan_id, date_returned, disposition, user_id)
    return nil if to_loan_id.blank?

    new_loan_item = nil
    LoanItem.transaction do
      begin
        update!(date_returned:, disposition:)

        # Reset cached association so on_loan? returns correct value
        loan_item_object.association(:loan_item).reset

        new_loan_item = LoanItem.create!(
          project_id:,
          loan_item_object:,
          loan_id: to_loan_id
          )

      rescue ActiveRecord::RecordInvalid => e
        #raise e
      end
    end
    new_loan_item
  end

  def self.batch_create_from_collection_object_filter(loan_id, project_id, user_id, collection_object_filter)
    created = []
    query = Queries::CollectionObject::Filter.new(collection_object_filter)
    LoanItem.transaction do
      begin
        query.all.each do |co|
          i = LoanItem.create!(loan_item_object: co, by: user_id, loan_id:, project_id:)
          if i.persisted?
            created.push i
          end
        end
      rescue ActiveRecord::RecordInvalid => e
        # raise e
      end
    end
    return created
  end

  def self.batch_create_from_tags(keyword_id, klass, loan_id)
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:tags).where(tags: {keyword_id:}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          Tag.where(keyword_id:).where(tag_object_type: ['Container', 'Otu', 'CollectionObject']).distinct.all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.tag_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end


  def self.batch_create_from_pinboard(loan_id, project_id, user_id, klass)
    return false if loan_id.blank? || project_id.blank? || user_id.blank?
    created = []
    LoanItem.transaction do
      begin
        if klass
          klass.constantize.joins(:pinboard_items).where(pinboard_items: {user_id:, project_id:, pinned_object_type: klass}).each do |o|
            created.push LoanItem.create!(loan_item_object: o, loan_id:)
          end
        else
          PinboardItem.where(project_id:, user_id:, pinned_object_type: ['Container', 'Otu', 'CollectionObject']).all.each do |o|
            created.push LoanItem.create!(loan_item_object: o.pinned_object, loan_id:)
          end
        end
      rescue ActiveRecord::RecordInvalid
        return false
      end
    end
    return created
  end

  protected

  # Whether this class of objects is in fact loanable, not
  # whether it's on loan or not.
  def object_loanable_check
    loan_item_object && loan_item_object.respond_to?(:is_loanable?)
  end

  # Code, not out-on-loan check!
  def loan_object_is_loanable
    if !persisted? # if it is, then this check should not be necessary
      if !object_loanable_check
        errors.add(:loan_item_object, 'is not loanble')
      end
    end
  end

  def total_provided_only_when_otu
    errors.add(:total, 'only providable when item is an OTU.') if total && loan_item_object_type != 'Otu'
  end

  # Is not already in a loan item if CollectionObject/Container
  def available_for_loan
    if !persisted? # if it is, then this check should not be necessary
      if object_loanable_check
        if loan_item_object_type == 'Otu'
          true
        else
          if loan_item_object.on_loan? # takes into account Containers!
            errors.add(:loan_item_object, 'is already on loan')
          end
        end
      end
    end
  end

end

Class Method Details

.batch_create(params) ⇒ Object

TODO: param handling is currently all kinds of “meh”



316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'app/models/loan_item.rb', line 316

def self.batch_create(params)
  case params[:batch_type]
  when 'tags'
    batch_create_from_tags(params[:keyword_id], params[:klass], params[:loan_id])
  when 'pinboard'
    batch_create_from_pinboard(params[:loan_id], params[:project_id], params[:user_id], params[:klass])
  when 'collection_object_filter'
    batch_create_from_collection_object_filter(
      params[:loan_id],
      params[:project_id],
      params[:user_id],
      params[:collection_object_query])
  end
end

.batch_create_from_collection_object_filter(loan_id, project_id, user_id, collection_object_filter) ⇒ Object



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# File 'app/models/loan_item.rb', line 415

def self.batch_create_from_collection_object_filter(loan_id, project_id, user_id, collection_object_filter)
  created = []
  query = Queries::CollectionObject::Filter.new(collection_object_filter)
  LoanItem.transaction do
    begin
      query.all.each do |co|
        i = LoanItem.create!(loan_item_object: co, by: user_id, loan_id:, project_id:)
        if i.persisted?
          created.push i
        end
      end
    rescue ActiveRecord::RecordInvalid => e
      # raise e
    end
  end
  return created
end

.batch_create_from_pinboard(loan_id, project_id, user_id, klass) ⇒ Object



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'app/models/loan_item.rb', line 454

def self.batch_create_from_pinboard(loan_id, project_id, user_id, klass)
  return false if loan_id.blank? || project_id.blank? || user_id.blank?
  created = []
  LoanItem.transaction do
    begin
      if klass
        klass.constantize.joins(:pinboard_items).where(pinboard_items: {user_id:, project_id:, pinned_object_type: klass}).each do |o|
          created.push LoanItem.create!(loan_item_object: o, loan_id:)
        end
      else
        PinboardItem.where(project_id:, user_id:, pinned_object_type: ['Container', 'Otu', 'CollectionObject']).all.each do |o|
          created.push LoanItem.create!(loan_item_object: o.pinned_object, loan_id:)
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
  end
  return created
end

.batch_create_from_tags(keyword_id, klass, loan_id) ⇒ Object



433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'app/models/loan_item.rb', line 433

def self.batch_create_from_tags(keyword_id, klass, loan_id)
  created = []
  LoanItem.transaction do
    begin
      if klass
        klass.constantize.joins(:tags).where(tags: {keyword_id:}).each do |o|
          created.push LoanItem.create!(loan_item_object: o, loan_id:)
        end
      else
        Tag.where(keyword_id:).where(tag_object_type: ['Container', 'Otu', 'CollectionObject']).distinct.all.each do |o|
          created.push LoanItem.create!(loan_item_object: o.tag_object, loan_id:)
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
  end
  return created
end

.batch_determine_loan_items(ids: [], params: {}) ⇒ Object



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

def self.batch_determine_loan_items(ids: [], params: {})
  return false if ids.empty?
  # these objects will be created/persisted to be used for each of the loan items identified by the input ids
  td = TaxonDetermination.new(params) # build a td from the input data

  begin
    LoanItem.transaction do
      item_list = [] # Array of objects that can have a taxon determination
      LoanItem.where(id: ids).each do |li|
        item_list.push li.determinable_objects
      end

      item_list.flatten!

      first = item_list.pop
      td.taxon_determination_object = first
      td.save! # create and save the first one so we can dup it in the next step

      item_list.each do |item|
        n = td.dup
        n.determiners << td.determiners
        n.taxon_determination_object = item
        n.save
        n.move_to_top
      end
    end
  rescue ActiveRecord::RecordInvalid
    return false
  end
  true
end

.batch_move(params) ⇒ Hash

Returns:

  • (Hash)


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

def self.batch_move(params)
  return false if params[:loan_id].blank? || params[:disposition].blank? || params[:user_id].blank? || params[:date_returned].blank?

  a = Queries::CollectionObject::Filter.new(params[:collection_object_query])
  return false if a.all.count == 0

  moved = []
  unmoved = []

  begin
    a.all.each do |co|
      new_loan_item = nil

      # Only match open loan items
      if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)

        new_loan_item = b.close_and_move(params[:loan_id], params[:date_returned], params[:disposition], params[:user_id])

        if new_loan_item.nil?
          unmoved.push b
        else
          moved.push new_loan_item
        end
      end
    end

  rescue ActiveRecord::RecordInvalid => e
   # raise e
  end

  return { moved:, unmoved: }
end

.batch_return(params) ⇒ Object

Return all CollectionObjects matching the query. Does not yet work with OtuQuery

Parameters:

  • param (:collection_object_query)

    required



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'app/models/loan_item.rb', line 368

def self.batch_return(params)
  a = Queries::CollectionObject::Filter.new(params[:collection_object_query])

  return false if a.all.count == 0

  returned = []
  unreturned = []

  begin
    a.all.each do |co|
      if b = LoanItem.where(disposition: nil).find_by(loan_item_object: co, project_id: co.project_id)
        begin
          b.update!(disposition: params[:disposition], date_returned: params[:date_returned])
          returned.push b
        rescue ActiveRecord::RecordInvalid
          unreturned.push b
        end
      end
    end
  end
  return {returned:, unreturned:}
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) ⇒ BatchResponse

Parameters:

  • batch_response (BatchResponse) (defaults to: nil)
  • query (ActiveRecord::Relation) (defaults to: nil)

    the filtered collection objects

  • hash_query (Hash) (defaults to: nil)

    the filter query as a hash (for async)

  • mode (Symbol) (defaults to: nil)

    :add

  • params (Hash) (defaults to: nil)

    must include :loan_id

  • async (Boolean) (defaults to: nil)
  • project_id (Integer) (defaults to: nil)
  • user_id (Integer) (defaults to: nil)
  • called_from_async (Boolean) (defaults to: false)

Returns:



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

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
)
  async = false if called_from_async == true
  r = batch_response

  case mode.to_sym
  when :add
    loan_id = params[:loan_id]

    if loan_id.blank?
      r.errors['no loan_id provided'] = 1
      return r
    end

    if async && !called_from_async
      BatchByFilterScopeJob.perform_later(
        klass: self.name,
        hash_query:,
        mode:,
        params:,
        project_id:,
        user_id:
      )
    else
      query.find_each do |collection_object|
        loan_item = LoanItem.new(
          loan_item_object: collection_object,
          loan_id:,
          project_id:,
          by: user_id
        )

        if loan_item.save
          r.updated.push loan_item.id
        else
          r.not_updated.push collection_object.id
          loan_item.errors.full_messages.each do |msg|
            r.validation_errors[msg] += 1
          end
        end
      end
    end

  when :return
    disposition = params[:disposition]
    date_returned = params[:date_returned]

    if disposition.blank?
      r.errors['no disposition provided'] = 1
      return r
    end

    if date_returned.blank?
      r.errors['no date_returned provided'] = 1
      return r
    end

    if async && !called_from_async
      BatchByFilterScopeJob.perform_later(
        klass: self.name,
        hash_query:,
        mode:,
        params:,
        project_id:,
        user_id:
      )
    else
      query.find_each do |collection_object|
        loan_item = LoanItem.where(date_returned: nil)
          .find_by(loan_item_object: collection_object, project_id:)

        if loan_item
          if loan_item.update(disposition:, date_returned:)
            r.updated.push loan_item.id
          else
            r.not_updated.push collection_object.id
            loan_item.errors.full_messages.each do |msg|
              r.validation_errors[msg] += 1
            end
          end
        else
          r.not_updated.push collection_object.id
          r.validation_errors['not currently on loan'] += 1
        end
      end
    end

  when :move
    loan_id = params[:loan_id]
    disposition = params[:disposition]
    date_returned = params[:date_returned]

    if loan_id.blank?
      r.errors['no loan_id provided'] = 1
      return r
    end

    if disposition.blank?
      r.errors['no disposition provided'] = 1
      return r
    end

    if date_returned.blank?
      r.errors['no date_returned provided'] = 1
      return r
    end

    if async && !called_from_async
      BatchByFilterScopeJob.perform_later(
        klass: self.name,
        hash_query:,
        mode:,
        params:,
        project_id:,
        user_id:
      )
    else
      query.find_each do |collection_object|
        loan_item = LoanItem.where(date_returned: nil)
          .find_by(loan_item_object: collection_object, project_id:)

        if loan_item
          loan_item.loan_item_object.association(:loan_item).reset
          new_loan_item = loan_item.close_and_move(loan_id, date_returned, disposition, user_id)

          if new_loan_item
            r.updated.push new_loan_item.id
          else
            r.not_updated.push collection_object.id
            r.validation_errors['either return or move of loan item failed'] += 1
          end
        else
          r.not_updated.push collection_object.id
          r.validation_errors['not currently on loan'] += 1
        end
      end
    end
  end

  r
end

Instance Method Details

#available_for_loanObject (protected)

Is not already in a loan item if CollectionObject/Container



497
498
499
500
501
502
503
504
505
506
507
508
509
# File 'app/models/loan_item.rb', line 497

def available_for_loan
  if !persisted? # if it is, then this check should not be necessary
    if object_loanable_check
      if loan_item_object_type == 'Otu'
        true
      else
        if loan_item_object.on_loan? # takes into account Containers!
          errors.add(:loan_item_object, 'is already on loan')
        end
      end
    end
  end
end

#close_and_move(to_loan_id, date_returned, disposition, user_id) ⇒ Object



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# File 'app/models/loan_item.rb', line 391

def close_and_move(to_loan_id, date_returned, disposition, user_id)
  return nil if to_loan_id.blank?

  new_loan_item = nil
  LoanItem.transaction do
    begin
      update!(date_returned:, disposition:)

      # Reset cached association so on_loan? returns correct value
      loan_item_object.association(:loan_item).reset

      new_loan_item = LoanItem.create!(
        project_id:,
        loan_item_object:,
        loan_id: to_loan_id
        )

    rescue ActiveRecord::RecordInvalid => e
      #raise e
    end
  end
  new_loan_item
end

#determinable_objectsArray

Returns all objects that can have a taxon determination applied to them for this loan item.

Returns:

  • (Array)

    all objects that can have a taxon determination applied to them for this loan item



115
116
117
118
119
120
121
122
123
124
125
# File 'app/models/loan_item.rb', line 115

def determinable_objects
  # this loan item which may be a container, an OTU, or a collection object
  case loan_item_object_type
  when /contain/i # if this item is a container, dig into the container for the collection objects themselves
    loan_item_object.collection_objects
  when /object/i # if this item is a collection object, just add the object
    [loan_item_object]
  when /otu/i # not strictly needed, but helps keep track of what the loan_item is.
    [] # can't use an OTU as a determination object.
  end
end

#global_entityObject



71
72
73
# File 'app/models/loan_item.rb', line 71

def global_entity
  self.loan_item_object.to_global_id if self.loan_item_object.present?
end

#global_entity=(entity) ⇒ Object



75
76
77
# File 'app/models/loan_item.rb', line 75

def global_entity=(entity)
  self.loan_item_object = GlobalID::Locator.locate entity
end

#loan_object_is_loanableObject (protected)

Code, not out-on-loan check!



484
485
486
487
488
489
490
# File 'app/models/loan_item.rb', line 484

def loan_object_is_loanable
  if !persisted? # if it is, then this check should not be necessary
    if !object_loanable_check
      errors.add(:loan_item_object, 'is not loanble')
    end
  end
end

#object_loanable_checkObject (protected)

Whether this class of objects is in fact loanable, not whether it’s on loan or not.



479
480
481
# File 'app/models/loan_item.rb', line 479

def object_loanable_check
  loan_item_object && loan_item_object.respond_to?(:is_loanable?)
end

#returned?Boolean

Returns:

  • (Boolean)


87
88
89
# File 'app/models/loan_item.rb', line 87

def returned?
  date_returned.present?
end

#total_itemsInteger?

TODO: this does not factor in nested items in a container

Returns:

  • (Integer, nil)

    the total items this loan line item represent



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'app/models/loan_item.rb', line 94

def total_items
  case loan_item_object_type
    when 'Otu'
      total ? total : nil
    when 'Container'
      t = 0
      loan_item_object.all_contained_objects.each do |o|
        if o.kind_of?(::CollectionObject)
          t += o.total
        end
      end
      t
    when 'CollectionObject'
      loan_item_object.total.to_i
    else
      nil
  end
end

#total_provided_only_when_otuObject (protected)



492
493
494
# File 'app/models/loan_item.rb', line 492

def total_provided_only_when_otu
  errors.add(:total, 'only providable when item is an OTU.') if total && loan_item_object_type != 'Otu'
end