Class: CollectionObject

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
DwcExtensions, GlobalID::Identification, Housekeeping, Shared::Citable, Shared::Confidence, Shared::Containable, Shared::DataAttributes, Shared::Depictions, Shared::HasRoles, Shared::Identifiable, Shared::IsData, Shared::IsDwcOccurrence, Shared::Loanable, Shared::Notable, Shared::OriginRelationship, Shared::Taggable, SoftValidation
Defined in:
app/models/collection_object.rb

Overview

A CollectionObject is on or more physical things that have been collected. Enumerating how many things (@!total) is a task of the curator.

A CollectiongObjects immediate disposition is handled through its relation to containers. Containers can be nested, labeled, and interally subdivided as necessary.

Direct Known Subclasses

BiologicalCollectionObject

Defined Under Namespace

Modules: DwcExtensions Classes: BiologicalCollectionObject

Constant Summary

CO_OTU_HEADERS =
%w{OTU OTU\ name Family Genus Species Country State County Locality Latitude Longitude}.freeze
BUFFERED_ATTRIBUTES =
buffered_collecting_event buffered_determinations buffered_other_labels}.freeze

Constants included from DwcExtensions

DwcExtensions::DWC_OCCURRENCE_MAP

Constants included from SoftValidation

SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS

Instance Attribute Summary (collapse)

Attributes included from DwcExtensions

#taxonomy

Class Method Summary (collapse)

Instance Method Summary (collapse)

Methods included from DwcExtensions

#dwc_catalog_number, #dwc_country, #dwc_county, #dwc_event_date, #dwc_event_time, #dwc_family, #dwc_genus, #dwc_georeference_protocol, #dwc_institution_code, #dwc_institution_id, #dwc_latitude, #dwc_locality, #dwc_longitude, #dwc_nomenclatural_code, #dwc_preparations, #dwc_scientific_name, #dwc_specific_epithet, #dwc_state_province, #dwc_taxon_name_authorship, #dwc_verbatim_locality, #set_taxonomy

Methods included from SoftValidation

#clear_soft_validations, #fix_soft_validations, #soft_fixed?, #soft_valid?, #soft_validate, #soft_validated?, #soft_validations

Methods included from Housekeeping

#has_polymorphic_relationship?

Instance Attribute Details

- (Date) accessioned_at

The date when the object was accessioned to the Repository (not necessarily it's current disposition!). If present Repository must be present.

Returns:

  • (Date)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'app/models/collection_object.rb', line 59

class CollectionObject < ActiveRecord::Base

  include GlobalID::Identification
  include Housekeeping
  include Shared::Citable
  include Shared::Containable
  include Shared::DataAttributes
  include Shared::Loanable
  include Shared::HasRoles
  include Shared::Identifiable
  include Shared::Notable
  include Shared::Taggable
  include Shared::Depictions
  include Shared::OriginRelationship
  include Shared::Confidence
  include Shared::IsData
  include SoftValidation

  include Shared::IsDwcOccurrence
  include CollectionObject::DwcExtensions 

  is_origin_for :collection_objects
  has_paper_trail :on => [:update] 

  CO_OTU_HEADERS      = %w{OTU OTU\ name Family Genus Species Country State County Locality Latitude Longitude}.freeze
  BUFFERED_ATTRIBUTES = %i{buffered_collecting_event buffered_determinations buffered_other_labels}.freeze

  # @return [Boolean]
  #  When true, cached values are not built
  attr_accessor :no_cached

  after_save :add_to_dwc_occurrence, if: '!self.no_cached'
  
  # Otu delegations
  delegate :name, to: :current_otu, prefix: :otu, allow_nil: true # could be Otu#otu_name?
  delegate :id, to: :current_otu, prefix: :otu, allow_nil: true

  # Identifier delegations
  delegate :cached, to: :preferred_catalog_number, prefix: :catalog_number, allow_nil: true

  # CollectingEvent delegations
  delegate :map_center, to: :collecting_event, prefix: :collecting_event, allow_nil: true

  # Repository delegations
  delegate :acronym, to: :repository, prefix: :repository, allow_nil: true
  delegate :url, to: :repository, prefix: :repository, allow_nil: true 

  # Preparation delegations
  delegate :name, to: :preparation_type, prefix: :preparation_type, allow_nil: true

  has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object
  has_one :accession_provider, through: :accession_provider_role, source: :person
  has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object
  has_one :deaccession_recipient, through: :deaccession_recipient_role, source: :person

  has_many :derived_collection_objects, inverse_of: :collection_object
  has_many :collection_object_observations, through: :derived_collection_objects, inverse_of: :collection_objects
  has_many :sqed_depictions, through: :depictions

  # This must come before taxon determinations !!
  has_many :otus, through: :taxon_determinations, inverse_of: :collection_objects

  has_many :taxon_names, through: :otus

  # This is a problem, but here for the forseeable future for nested attributes purporses.
  has_many :taxon_determinations, foreign_key: :biological_collection_object_id, inverse_of: :biological_collection_object

  has_many :type_designations, class_name: 'TypeMaterial', foreign_key: :biological_object_id, inverse_of: :material

  belongs_to :collecting_event, inverse_of: :collection_objects
  belongs_to :preparation_type, inverse_of: :collection_objects
  belongs_to :ranged_lot_category, inverse_of: :ranged_lots
  belongs_to :repository, inverse_of: :collection_objects

  has_many :georeferences, through: :collecting_event
  has_many :geographic_items, through: :georeferences

  accepts_nested_attributes_for :otus, allow_destroy: true
  accepts_nested_attributes_for :taxon_determinations, allow_destroy: true, reject_if: :reject_taxon_determinations
  accepts_nested_attributes_for :collecting_event, allow_destroy: true, reject_if: :reject_collecting_event

  validates_presence_of :type
  validate :check_that_either_total_or_ranged_lot_category_id_is_present
  validate :check_that_both_of_category_and_total_are_not_present

  before_validation :assign_type_if_total_or_ranged_lot_category_id_provided

  soft_validate(:sv_missing_accession_fields, set: :missing_accession_fields)
  soft_validate(:sv_missing_deaccession_fields, set: :missing_deaccession_fields)

  def preferred_catalog_number
    Identifier::Local::CatalogNumber.where(identifier_object: self).first 
  end

  def missing_determination
    # see BiologicalCollectionObject
  end

  def self.find_for_autocomplete(params)
    Queries::BiologicalCollectionObjectAutocompleteQuery.new(params[:term]).all.where(project_id: params[:project_id]).includes(taxon_determinations: [:determiners]).limit(50)
  end

  # TODO: move to a helper
  def self.breakdown_status(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)

    breakdown = {
      total_objects:     collection_objects.length,
      collecting_events: {},
      determinations:    {},
      bio_overview:      []
    }

    breakdown.merge!(breakdown_buffered(collection_objects))

    collection_objects.each do |co|
      breakdown[:collecting_events].merge!(co => co.collecting_event) if co.collecting_event
      breakdown[:determinations].merge!(co => co.taxon_determinations) if co.taxon_determinations.any?
      breakdown[:bio_overview].push([co.total, co.biocuration_classes.collect { |a| a.name }])
    end

    breakdown
  end

  # @return [Hash]
  #   a unque list of buffered_ values observed in the collection objects passed
  def self.breakdown_buffered(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)
    breakdown          = {}
    categories         = BUFFERED_ATTRIBUTES

    categories.each do |c|
      breakdown[c] = []
    end

    categories.each do |c|
      collection_objects.each do |co|
        breakdown[c].push co.send(c)
      end
    end

    categories.each do |c|
      breakdown[c].uniq!
    end

    breakdown
  end

  # return [Boolean]
  #    True if instance is a subclass of BiologicalCollectionObject
  def biological?
    self.class <= BiologicalCollectionObject ? true : false
  end

  def annotations
    h = annotations_hash
    (h['biocuration classifications'] = self.biocuration_classes) if self.biological? && self.biocuration_classifications.any?
    h
  end
  
  # @param [String] rank
  # @return [String] if a determination exists, and the Otu in the determination has a taxon name then return the taxon name at the rank supplied
  def name_at_rank_string(rank)
    current_taxon_name.try(:ancestor_at_rank, rank).try(:cached_html)
  end

  # @param [Scope] scope of selected CollectionObjects
  # @param [Hash] col_defs selected headers and types
  # @param [Hash] table_data (optional)
  # @return [CSV] tab-separated data
  # Generate the CSV (tab-separated) data for the file to be sent, substitute for new-lines and tabs
  def self.generate_report_download(scope, col_defs, table_data = nil)
    CSV.generate do |csv|
      row = CO_OTU_HEADERS
      unless col_defs.nil?
        %w(ce co bc).each { |column_type|
          items = []
          unless col_defs[column_type.to_sym].nil?
            unless col_defs[column_type.to_sym][:in].nil?
              items.push(col_defs[column_type.to_sym][:in].keys)
            end
            unless col_defs[column_type.to_sym][:im].nil?
              items.push(col_defs[column_type.to_sym][:im].keys)
            end
          end
          row += items.flatten
        }
      end
      csv << row
      if table_data.nil?
        scope.order(id: :asc).each do |c_o|
          row = [c_o.otu_id,
                 c_o.otu_name,
                 c_o.name_at_rank_string(:family),
                 c_o.name_at_rank_string(:genus),
                 c_o.name_at_rank_string(:species),
                 c_o.collecting_event.country_name,
                 c_o.collecting_event.state_name,
                 c_o.collecting_event.county_name,
                 c_o.collecting_event.verbatim_locality,
                 c_o.collecting_event.georeference_latitude.to_s,
                 c_o.collecting_event.georeference_longitude.to_s
          ]
          row += ce_attributes(c_o, col_defs)
          row += co_attributes(c_o, col_defs)
          row += bc_attributes(c_o, col_defs)
          csv << row.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }

        end
      else
        table_data.each { |_key, value|
          csv << value.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }
        }
      end
    end
  end


  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.earliest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:end_date_year)

    return '1700/01/01' if a.nil? && b.nil?

    d = nil

    if a && b
      if a < b
        d = a
      end
    else
      d = a || b
    end
    d.to_s + '/01/01'
  end

  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.latest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:end_date_year)

    c = Time.now.strftime("%Y/%m/%d")

    return c if a.nil? && b.nil?

    d = nil

    if a && b
      if a > b
        d = a
      end
    else
      d = a || b
    end

    d.to_s + '/12/31'
  end

  # TODO: Clarify this.
  # CAREFULL - this isn't _in_, this is *with*, if it was in it would be spatial query, not a join(:geographic_items)
  #
  # Find all collection objects which have collecting events which have georeferences which have geographic_items which
  # are located within the geographic item supplied
  # @param [GeographicItem] geographic_item_id
  # @return [Scope] of CollectionObject
  def self.in_geographic_item(geographic_item, limit, steps = false)
    geographic_item_id = geographic_item.id
    if steps
      gi     = GeographicItem.find(geographic_item_id)
      # find the geographic_items inside gi
      step_1 = GeographicItem.is_contained_by('any', gi) # .pluck(:id)
      # find the georeferences from the geographic_items
      step_2 = step_1.map(&:georeferences).uniq.flatten
      # find the collecting events connected to the georeferences
      step_3 = step_2.map(&:collecting_event).uniq.flatten
      # find the collection objects associated with the collecting events
      step_4 = step_3.map(&:collection_objects).flatten.map(&:id).uniq
      retval = CollectionObject.where(id: step_4.sort)
    else
      retval = CollectionObject.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)).limit(limit).includes(:data_attributes, :collecting_event)
    end
    retval
  end

  def self.selected_column_names
    @selected_column_names = {ce: {in: {}, im: {}},
                              co: {in: {}, im: {}},
                              bc: {in: {}, im: {}}
    } if @selected_column_names.nil?
    @selected_column_names
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collecting events
  # decode which headers to be displayed for collecting events
  def self.ce_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:ce][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:ce][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.ce_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.collecting_event.internal_attributes
      all_import_das   = collection_object.collecting_event.import_attributes
      group            = collection[:ce]
      unless group.nil?
        group.keys.each { |type_key|
          group[type_key.to_sym].keys.each { |header|
            this_val = nil
            case type_key.to_sym
              when :in
                all_internal_das.each { |da|
                  if da.predicate.name == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              when :im
                all_import_das.each { |da|
                  if da.import_predicate == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              else
            end
          }
        }
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collection objects
  # decode which headers to be displayed for collection objects
  def self.co_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:co][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:co][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.co_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.internal_attributes
      all_import_das   = collection_object.import_attributes
      group            = collection[:co]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = nil
              all_internal_das.each { |da|
                if da.predicate.name == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
        unless group.empty?
          unless group[:im].empty?
            group[:im].keys.each { |header|
              this_val = nil
              all_import_das.each { |da|
                if da.import_predicate == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for biocuration classifications
  # decode which headers to be displayed for biocuration classifications
  def self.bc_headers(project_id)
    CollectionObject.selected_column_names
    # add selectable column names (unselected) to the column name list list
    BiocurationClass.where(project_id: project_id).map(&:name).each { |column_name|
      @selected_column_names[:bc][:in][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.bc_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      group = collection[:bc]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = collection_object.biocuration_classes.map(&:name).include?(header) ? '1' : '0'
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Array] collecting_event_ids (e.g., from CollectingEvent.in_date_range)
  # @param [Array] area_object_ids (e.g., from GeographicItem.gather_selected_data())
  # @return [Scope] of intersection of collecting events (usually by date range)
  #   and collection objects (usually by inclusion in geographic areas/items)
  def self.from_collecting_events(collecting_event_ids, area_object_ids, area_set, project_id)
    collecting_events_clause = {collecting_event_id: collecting_event_ids, project: project_id}
    area_objects_clause = {id: area_object_ids, project: project_id}

    if (collecting_event_ids.empty?)
      collecting_events_clause = {project: project_id}
    end

    if (area_object_ids.empty?)
      area_objects_clause = {}
      if (area_set)
        area_objects_clause = 'false'
      end
    end
    
    retval = CollectionObject.joins(:collecting_event)
                 .where(collecting_events_clause)
                 .where(area_objects_clause)
    retval
  end

  # @param [Hash] search_start_date string in form 'yyyy/mm/dd'
  # @param [Hash] search_end_date string in form 'yyyy/mm/dd'
  # @param [Hash] partial_overlap 'on' or 'off'
  # @return [Scope] of selected collection objects through collecting events with georeferences, remember to scope to project!
  def self.in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on')
    allow_partial = (partial_overlap.downcase == 'off' ? false : true) # TODO: Just get the correct values from the form!
    where_sql = CollectingEvent.date_sql_from_dates(search_start_date, search_end_date, allow_partial)
    joins(:collecting_event).where(where_sql)
  end

  def sv_missing_accession_fields
    soft_validations.add(:accessioned_at, 'Date is not selected') if self.accessioned_at.nil? && !self.accession_provider.nil?
    soft_validations.add(:base, 'Provider is not selected') if !self.accessioned_at.nil? && self.accession_provider.nil?
  end

  def sv_missing_deaccession_fields
    soft_validations.add(:deaccessioned_at, 'Date is not selected') if self.deaccessioned_at.nil? && !self.deaccession_reason.blank?
    soft_validations.add(:base, 'Recipient is not selected') if self.deaccession_recipient.nil? && self.deaccession_reason && self.deaccessioned_at
    soft_validations.add(:deaccession_reason, 'Reason is is not defined') if self.deaccession_reason.blank? && self.deaccession_recipient && self.deaccessioned_at
  end

  def sv_missing_collecting_event
    # see biological_collection_object
  end

  def sv_missing_preparation_type
    # see biological_collection_object
  end

  def sv_missing_repository
    # see biological_collection_object
  end

  protected

  def add_to_dwc_occurrence
    get_dwc_occurrence
  end
  handle_asynchronously :add_to_dwc_occurrence, run_at: Proc.new { 20.seconds.from_now }

  def check_that_both_of_category_and_total_are_not_present
    errors.add(:ranged_lot_category_id, 'Both ranged_lot_category and total can not be set') if !ranged_lot_category_id.blank? && !total.blank?
  end

  def check_that_either_total_or_ranged_lot_category_id_is_present
    errors.add(:base, 'Either total or a ranged lot category must be provided') if ranged_lot_category_id.blank? && total.blank?
  end

  def assign_type_if_total_or_ranged_lot_category_id_provided
    if self.total == 1
      self.type = 'Specimen'
    elsif self.total.to_i > 1
      self.type = 'Lot'
    elsif total.nil? && !ranged_lot_category_id.blank?
      self.type = 'RangedLot'
    end
    true
  end

  def reject_taxon_determinations(attributed)

    a = attributed['otu_id']
    b = attributed['otu_attributes'] 

    return true if !a.present? && !b.present?

    if a.present?
      return true if b.present? && ( b['name'].present? || b['taxon_name_id'].present? ) # not both
      return false 
    end

    if b.present?
      return true if !b['name'].present? && !b['taxon_name_id'].present?
    end 

    false
  end


  def reject_collecting_event(attributed)
    reject = true
    CollectingEvent.data_attributes.each do |a|
      if !attributed[a].blank?
        reject = false
        break
      end
    end
    # !! does not account for georeferences_attributes!
    reject
  end

end

- (String) buffered_collecting_event

An incoming, typically verbatim, block of data typically as seens as a locality/method/etc. label. All buffered_ attributes are written but not intended to be deleted or otherwise updated. Buffered_ attributes are typically only used in rapid data capture, primarily in historical situations.

Returns:

  • (String)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'app/models/collection_object.rb', line 59

class CollectionObject < ActiveRecord::Base

  include GlobalID::Identification
  include Housekeeping
  include Shared::Citable
  include Shared::Containable
  include Shared::DataAttributes
  include Shared::Loanable
  include Shared::HasRoles
  include Shared::Identifiable
  include Shared::Notable
  include Shared::Taggable
  include Shared::Depictions
  include Shared::OriginRelationship
  include Shared::Confidence
  include Shared::IsData
  include SoftValidation

  include Shared::IsDwcOccurrence
  include CollectionObject::DwcExtensions 

  is_origin_for :collection_objects
  has_paper_trail :on => [:update] 

  CO_OTU_HEADERS      = %w{OTU OTU\ name Family Genus Species Country State County Locality Latitude Longitude}.freeze
  BUFFERED_ATTRIBUTES = %i{buffered_collecting_event buffered_determinations buffered_other_labels}.freeze

  # @return [Boolean]
  #  When true, cached values are not built
  attr_accessor :no_cached

  after_save :add_to_dwc_occurrence, if: '!self.no_cached'
  
  # Otu delegations
  delegate :name, to: :current_otu, prefix: :otu, allow_nil: true # could be Otu#otu_name?
  delegate :id, to: :current_otu, prefix: :otu, allow_nil: true

  # Identifier delegations
  delegate :cached, to: :preferred_catalog_number, prefix: :catalog_number, allow_nil: true

  # CollectingEvent delegations
  delegate :map_center, to: :collecting_event, prefix: :collecting_event, allow_nil: true

  # Repository delegations
  delegate :acronym, to: :repository, prefix: :repository, allow_nil: true
  delegate :url, to: :repository, prefix: :repository, allow_nil: true 

  # Preparation delegations
  delegate :name, to: :preparation_type, prefix: :preparation_type, allow_nil: true

  has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object
  has_one :accession_provider, through: :accession_provider_role, source: :person
  has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object
  has_one :deaccession_recipient, through: :deaccession_recipient_role, source: :person

  has_many :derived_collection_objects, inverse_of: :collection_object
  has_many :collection_object_observations, through: :derived_collection_objects, inverse_of: :collection_objects
  has_many :sqed_depictions, through: :depictions

  # This must come before taxon determinations !!
  has_many :otus, through: :taxon_determinations, inverse_of: :collection_objects

  has_many :taxon_names, through: :otus

  # This is a problem, but here for the forseeable future for nested attributes purporses.
  has_many :taxon_determinations, foreign_key: :biological_collection_object_id, inverse_of: :biological_collection_object

  has_many :type_designations, class_name: 'TypeMaterial', foreign_key: :biological_object_id, inverse_of: :material

  belongs_to :collecting_event, inverse_of: :collection_objects
  belongs_to :preparation_type, inverse_of: :collection_objects
  belongs_to :ranged_lot_category, inverse_of: :ranged_lots
  belongs_to :repository, inverse_of: :collection_objects

  has_many :georeferences, through: :collecting_event
  has_many :geographic_items, through: :georeferences

  accepts_nested_attributes_for :otus, allow_destroy: true
  accepts_nested_attributes_for :taxon_determinations, allow_destroy: true, reject_if: :reject_taxon_determinations
  accepts_nested_attributes_for :collecting_event, allow_destroy: true, reject_if: :reject_collecting_event

  validates_presence_of :type
  validate :check_that_either_total_or_ranged_lot_category_id_is_present
  validate :check_that_both_of_category_and_total_are_not_present

  before_validation :assign_type_if_total_or_ranged_lot_category_id_provided

  soft_validate(:sv_missing_accession_fields, set: :missing_accession_fields)
  soft_validate(:sv_missing_deaccession_fields, set: :missing_deaccession_fields)

  def preferred_catalog_number
    Identifier::Local::CatalogNumber.where(identifier_object: self).first 
  end

  def missing_determination
    # see BiologicalCollectionObject
  end

  def self.find_for_autocomplete(params)
    Queries::BiologicalCollectionObjectAutocompleteQuery.new(params[:term]).all.where(project_id: params[:project_id]).includes(taxon_determinations: [:determiners]).limit(50)
  end

  # TODO: move to a helper
  def self.breakdown_status(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)

    breakdown = {
      total_objects:     collection_objects.length,
      collecting_events: {},
      determinations:    {},
      bio_overview:      []
    }

    breakdown.merge!(breakdown_buffered(collection_objects))

    collection_objects.each do |co|
      breakdown[:collecting_events].merge!(co => co.collecting_event) if co.collecting_event
      breakdown[:determinations].merge!(co => co.taxon_determinations) if co.taxon_determinations.any?
      breakdown[:bio_overview].push([co.total, co.biocuration_classes.collect { |a| a.name }])
    end

    breakdown
  end

  # @return [Hash]
  #   a unque list of buffered_ values observed in the collection objects passed
  def self.breakdown_buffered(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)
    breakdown          = {}
    categories         = BUFFERED_ATTRIBUTES

    categories.each do |c|
      breakdown[c] = []
    end

    categories.each do |c|
      collection_objects.each do |co|
        breakdown[c].push co.send(c)
      end
    end

    categories.each do |c|
      breakdown[c].uniq!
    end

    breakdown
  end

  # return [Boolean]
  #    True if instance is a subclass of BiologicalCollectionObject
  def biological?
    self.class <= BiologicalCollectionObject ? true : false
  end

  def annotations
    h = annotations_hash
    (h['biocuration classifications'] = self.biocuration_classes) if self.biological? && self.biocuration_classifications.any?
    h
  end
  
  # @param [String] rank
  # @return [String] if a determination exists, and the Otu in the determination has a taxon name then return the taxon name at the rank supplied
  def name_at_rank_string(rank)
    current_taxon_name.try(:ancestor_at_rank, rank).try(:cached_html)
  end

  # @param [Scope] scope of selected CollectionObjects
  # @param [Hash] col_defs selected headers and types
  # @param [Hash] table_data (optional)
  # @return [CSV] tab-separated data
  # Generate the CSV (tab-separated) data for the file to be sent, substitute for new-lines and tabs
  def self.generate_report_download(scope, col_defs, table_data = nil)
    CSV.generate do |csv|
      row = CO_OTU_HEADERS
      unless col_defs.nil?
        %w(ce co bc).each { |column_type|
          items = []
          unless col_defs[column_type.to_sym].nil?
            unless col_defs[column_type.to_sym][:in].nil?
              items.push(col_defs[column_type.to_sym][:in].keys)
            end
            unless col_defs[column_type.to_sym][:im].nil?
              items.push(col_defs[column_type.to_sym][:im].keys)
            end
          end
          row += items.flatten
        }
      end
      csv << row
      if table_data.nil?
        scope.order(id: :asc).each do |c_o|
          row = [c_o.otu_id,
                 c_o.otu_name,
                 c_o.name_at_rank_string(:family),
                 c_o.name_at_rank_string(:genus),
                 c_o.name_at_rank_string(:species),
                 c_o.collecting_event.country_name,
                 c_o.collecting_event.state_name,
                 c_o.collecting_event.county_name,
                 c_o.collecting_event.verbatim_locality,
                 c_o.collecting_event.georeference_latitude.to_s,
                 c_o.collecting_event.georeference_longitude.to_s
          ]
          row += ce_attributes(c_o, col_defs)
          row += co_attributes(c_o, col_defs)
          row += bc_attributes(c_o, col_defs)
          csv << row.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }

        end
      else
        table_data.each { |_key, value|
          csv << value.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }
        }
      end
    end
  end


  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.earliest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:end_date_year)

    return '1700/01/01' if a.nil? && b.nil?

    d = nil

    if a && b
      if a < b
        d = a
      end
    else
      d = a || b
    end
    d.to_s + '/01/01'
  end

  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.latest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:end_date_year)

    c = Time.now.strftime("%Y/%m/%d")

    return c if a.nil? && b.nil?

    d = nil

    if a && b
      if a > b
        d = a
      end
    else
      d = a || b
    end

    d.to_s + '/12/31'
  end

  # TODO: Clarify this.
  # CAREFULL - this isn't _in_, this is *with*, if it was in it would be spatial query, not a join(:geographic_items)
  #
  # Find all collection objects which have collecting events which have georeferences which have geographic_items which
  # are located within the geographic item supplied
  # @param [GeographicItem] geographic_item_id
  # @return [Scope] of CollectionObject
  def self.in_geographic_item(geographic_item, limit, steps = false)
    geographic_item_id = geographic_item.id
    if steps
      gi     = GeographicItem.find(geographic_item_id)
      # find the geographic_items inside gi
      step_1 = GeographicItem.is_contained_by('any', gi) # .pluck(:id)
      # find the georeferences from the geographic_items
      step_2 = step_1.map(&:georeferences).uniq.flatten
      # find the collecting events connected to the georeferences
      step_3 = step_2.map(&:collecting_event).uniq.flatten
      # find the collection objects associated with the collecting events
      step_4 = step_3.map(&:collection_objects).flatten.map(&:id).uniq
      retval = CollectionObject.where(id: step_4.sort)
    else
      retval = CollectionObject.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)).limit(limit).includes(:data_attributes, :collecting_event)
    end
    retval
  end

  def self.selected_column_names
    @selected_column_names = {ce: {in: {}, im: {}},
                              co: {in: {}, im: {}},
                              bc: {in: {}, im: {}}
    } if @selected_column_names.nil?
    @selected_column_names
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collecting events
  # decode which headers to be displayed for collecting events
  def self.ce_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:ce][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:ce][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.ce_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.collecting_event.internal_attributes
      all_import_das   = collection_object.collecting_event.import_attributes
      group            = collection[:ce]
      unless group.nil?
        group.keys.each { |type_key|
          group[type_key.to_sym].keys.each { |header|
            this_val = nil
            case type_key.to_sym
              when :in
                all_internal_das.each { |da|
                  if da.predicate.name == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              when :im
                all_import_das.each { |da|
                  if da.import_predicate == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              else
            end
          }
        }
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collection objects
  # decode which headers to be displayed for collection objects
  def self.co_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:co][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:co][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.co_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.internal_attributes
      all_import_das   = collection_object.import_attributes
      group            = collection[:co]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = nil
              all_internal_das.each { |da|
                if da.predicate.name == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
        unless group.empty?
          unless group[:im].empty?
            group[:im].keys.each { |header|
              this_val = nil
              all_import_das.each { |da|
                if da.import_predicate == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for biocuration classifications
  # decode which headers to be displayed for biocuration classifications
  def self.bc_headers(project_id)
    CollectionObject.selected_column_names
    # add selectable column names (unselected) to the column name list list
    BiocurationClass.where(project_id: project_id).map(&:name).each { |column_name|
      @selected_column_names[:bc][:in][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.bc_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      group = collection[:bc]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = collection_object.biocuration_classes.map(&:name).include?(header) ? '1' : '0'
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Array] collecting_event_ids (e.g., from CollectingEvent.in_date_range)
  # @param [Array] area_object_ids (e.g., from GeographicItem.gather_selected_data())
  # @return [Scope] of intersection of collecting events (usually by date range)
  #   and collection objects (usually by inclusion in geographic areas/items)
  def self.from_collecting_events(collecting_event_ids, area_object_ids, area_set, project_id)
    collecting_events_clause = {collecting_event_id: collecting_event_ids, project: project_id}
    area_objects_clause = {id: area_object_ids, project: project_id}

    if (collecting_event_ids.empty?)
      collecting_events_clause = {project: project_id}
    end

    if (area_object_ids.empty?)
      area_objects_clause = {}
      if (area_set)
        area_objects_clause = 'false'
      end
    end
    
    retval = CollectionObject.joins(:collecting_event)
                 .where(collecting_events_clause)
                 .where(area_objects_clause)
    retval
  end

  # @param [Hash] search_start_date string in form 'yyyy/mm/dd'
  # @param [Hash] search_end_date string in form 'yyyy/mm/dd'
  # @param [Hash] partial_overlap 'on' or 'off'
  # @return [Scope] of selected collection objects through collecting events with georeferences, remember to scope to project!
  def self.in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on')
    allow_partial = (partial_overlap.downcase == 'off' ? false : true) # TODO: Just get the correct values from the form!
    where_sql = CollectingEvent.date_sql_from_dates(search_start_date, search_end_date, allow_partial)
    joins(:collecting_event).where(where_sql)
  end

  def sv_missing_accession_fields
    soft_validations.add(:accessioned_at, 'Date is not selected') if self.accessioned_at.nil? && !self.accession_provider.nil?
    soft_validations.add(:base, 'Provider is not selected') if !self.accessioned_at.nil? && self.accession_provider.nil?
  end

  def sv_missing_deaccession_fields
    soft_validations.add(:deaccessioned_at, 'Date is not selected') if self.deaccessioned_at.nil? && !self.deaccession_reason.blank?
    soft_validations.add(:base, 'Recipient is not selected') if self.deaccession_recipient.nil? && self.deaccession_reason && self.deaccessioned_at
    soft_validations.add(:deaccession_reason, 'Reason is is not defined') if self.deaccession_reason.blank? && self.deaccession_recipient && self.deaccessioned_at
  end

  def sv_missing_collecting_event
    # see biological_collection_object
  end

  def sv_missing_preparation_type
    # see biological_collection_object
  end

  def sv_missing_repository
    # see biological_collection_object
  end

  protected

  def add_to_dwc_occurrence
    get_dwc_occurrence
  end
  handle_asynchronously :add_to_dwc_occurrence, run_at: Proc.new { 20.seconds.from_now }

  def check_that_both_of_category_and_total_are_not_present
    errors.add(:ranged_lot_category_id, 'Both ranged_lot_category and total can not be set') if !ranged_lot_category_id.blank? && !total.blank?
  end

  def check_that_either_total_or_ranged_lot_category_id_is_present
    errors.add(:base, 'Either total or a ranged lot category must be provided') if ranged_lot_category_id.blank? && total.blank?
  end

  def assign_type_if_total_or_ranged_lot_category_id_provided
    if self.total == 1
      self.type = 'Specimen'
    elsif self.total.to_i > 1
      self.type = 'Lot'
    elsif total.nil? && !ranged_lot_category_id.blank?
      self.type = 'RangedLot'
    end
    true
  end

  def reject_taxon_determinations(attributed)

    a = attributed['otu_id']
    b = attributed['otu_attributes'] 

    return true if !a.present? && !b.present?

    if a.present?
      return true if b.present? && ( b['name'].present? || b['taxon_name_id'].present? ) # not both
      return false 
    end

    if b.present?
      return true if !b['name'].present? && !b['taxon_name_id'].present?
    end 

    false
  end


  def reject_collecting_event(attributed)
    reject = true
    CollectingEvent.data_attributes.each do |a|
      if !attributed[a].blank?
        reject = false
        break
      end
    end
    # !! does not account for georeferences_attributes!
    reject
  end

end

- (String) buffered_determinations

An incoming, typically verbatim, block of data typically as seen a taxonomic determination label. All buffered_ attributes are written but not intended to be deleted or otherwise updated. Buffered_ attributes are typically only used in rapid data capture, primarily in historical situations.

Returns:

  • (String)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'app/models/collection_object.rb', line 59

class CollectionObject < ActiveRecord::Base

  include GlobalID::Identification
  include Housekeeping
  include Shared::Citable
  include Shared::Containable
  include Shared::DataAttributes
  include Shared::Loanable
  include Shared::HasRoles
  include Shared::Identifiable
  include Shared::Notable
  include Shared::Taggable
  include Shared::Depictions
  include Shared::OriginRelationship
  include Shared::Confidence
  include Shared::IsData
  include SoftValidation

  include Shared::IsDwcOccurrence
  include CollectionObject::DwcExtensions 

  is_origin_for :collection_objects
  has_paper_trail :on => [:update] 

  CO_OTU_HEADERS      = %w{OTU OTU\ name Family Genus Species Country State County Locality Latitude Longitude}.freeze
  BUFFERED_ATTRIBUTES = %i{buffered_collecting_event buffered_determinations buffered_other_labels}.freeze

  # @return [Boolean]
  #  When true, cached values are not built
  attr_accessor :no_cached

  after_save :add_to_dwc_occurrence, if: '!self.no_cached'
  
  # Otu delegations
  delegate :name, to: :current_otu, prefix: :otu, allow_nil: true # could be Otu#otu_name?
  delegate :id, to: :current_otu, prefix: :otu, allow_nil: true

  # Identifier delegations
  delegate :cached, to: :preferred_catalog_number, prefix: :catalog_number, allow_nil: true

  # CollectingEvent delegations
  delegate :map_center, to: :collecting_event, prefix: :collecting_event, allow_nil: true

  # Repository delegations
  delegate :acronym, to: :repository, prefix: :repository, allow_nil: true
  delegate :url, to: :repository, prefix: :repository, allow_nil: true 

  # Preparation delegations
  delegate :name, to: :preparation_type, prefix: :preparation_type, allow_nil: true

  has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object
  has_one :accession_provider, through: :accession_provider_role, source: :person
  has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object
  has_one :deaccession_recipient, through: :deaccession_recipient_role, source: :person

  has_many :derived_collection_objects, inverse_of: :collection_object
  has_many :collection_object_observations, through: :derived_collection_objects, inverse_of: :collection_objects
  has_many :sqed_depictions, through: :depictions

  # This must come before taxon determinations !!
  has_many :otus, through: :taxon_determinations, inverse_of: :collection_objects

  has_many :taxon_names, through: :otus

  # This is a problem, but here for the forseeable future for nested attributes purporses.
  has_many :taxon_determinations, foreign_key: :biological_collection_object_id, inverse_of: :biological_collection_object

  has_many :type_designations, class_name: 'TypeMaterial', foreign_key: :biological_object_id, inverse_of: :material

  belongs_to :collecting_event, inverse_of: :collection_objects
  belongs_to :preparation_type, inverse_of: :collection_objects
  belongs_to :ranged_lot_category, inverse_of: :ranged_lots
  belongs_to :repository, inverse_of: :collection_objects

  has_many :georeferences, through: :collecting_event
  has_many :geographic_items, through: :georeferences

  accepts_nested_attributes_for :otus, allow_destroy: true
  accepts_nested_attributes_for :taxon_determinations, allow_destroy: true, reject_if: :reject_taxon_determinations
  accepts_nested_attributes_for :collecting_event, allow_destroy: true, reject_if: :reject_collecting_event

  validates_presence_of :type
  validate :check_that_either_total_or_ranged_lot_category_id_is_present
  validate :check_that_both_of_category_and_total_are_not_present

  before_validation :assign_type_if_total_or_ranged_lot_category_id_provided

  soft_validate(:sv_missing_accession_fields, set: :missing_accession_fields)
  soft_validate(:sv_missing_deaccession_fields, set: :missing_deaccession_fields)

  def preferred_catalog_number
    Identifier::Local::CatalogNumber.where(identifier_object: self).first 
  end

  def missing_determination
    # see BiologicalCollectionObject
  end

  def self.find_for_autocomplete(params)
    Queries::BiologicalCollectionObjectAutocompleteQuery.new(params[:term]).all.where(project_id: params[:project_id]).includes(taxon_determinations: [:determiners]).limit(50)
  end

  # TODO: move to a helper
  def self.breakdown_status(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)

    breakdown = {
      total_objects:     collection_objects.length,
      collecting_events: {},
      determinations:    {},
      bio_overview:      []
    }

    breakdown.merge!(breakdown_buffered(collection_objects))

    collection_objects.each do |co|
      breakdown[:collecting_events].merge!(co => co.collecting_event) if co.collecting_event
      breakdown[:determinations].merge!(co => co.taxon_determinations) if co.taxon_determinations.any?
      breakdown[:bio_overview].push([co.total, co.biocuration_classes.collect { |a| a.name }])
    end

    breakdown
  end

  # @return [Hash]
  #   a unque list of buffered_ values observed in the collection objects passed
  def self.breakdown_buffered(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)
    breakdown          = {}
    categories         = BUFFERED_ATTRIBUTES

    categories.each do |c|
      breakdown[c] = []
    end

    categories.each do |c|
      collection_objects.each do |co|
        breakdown[c].push co.send(c)
      end
    end

    categories.each do |c|
      breakdown[c].uniq!
    end

    breakdown
  end

  # return [Boolean]
  #    True if instance is a subclass of BiologicalCollectionObject
  def biological?
    self.class <= BiologicalCollectionObject ? true : false
  end

  def annotations
    h = annotations_hash
    (h['biocuration classifications'] = self.biocuration_classes) if self.biological? && self.biocuration_classifications.any?
    h
  end
  
  # @param [String] rank
  # @return [String] if a determination exists, and the Otu in the determination has a taxon name then return the taxon name at the rank supplied
  def name_at_rank_string(rank)
    current_taxon_name.try(:ancestor_at_rank, rank).try(:cached_html)
  end

  # @param [Scope] scope of selected CollectionObjects
  # @param [Hash] col_defs selected headers and types
  # @param [Hash] table_data (optional)
  # @return [CSV] tab-separated data
  # Generate the CSV (tab-separated) data for the file to be sent, substitute for new-lines and tabs
  def self.generate_report_download(scope, col_defs, table_data = nil)
    CSV.generate do |csv|
      row = CO_OTU_HEADERS
      unless col_defs.nil?
        %w(ce co bc).each { |column_type|
          items = []
          unless col_defs[column_type.to_sym].nil?
            unless col_defs[column_type.to_sym][:in].nil?
              items.push(col_defs[column_type.to_sym][:in].keys)
            end
            unless col_defs[column_type.to_sym][:im].nil?
              items.push(col_defs[column_type.to_sym][:im].keys)
            end
          end
          row += items.flatten
        }
      end
      csv << row
      if table_data.nil?
        scope.order(id: :asc).each do |c_o|
          row = [c_o.otu_id,
                 c_o.otu_name,
                 c_o.name_at_rank_string(:family),
                 c_o.name_at_rank_string(:genus),
                 c_o.name_at_rank_string(:species),
                 c_o.collecting_event.country_name,
                 c_o.collecting_event.state_name,
                 c_o.collecting_event.county_name,
                 c_o.collecting_event.verbatim_locality,
                 c_o.collecting_event.georeference_latitude.to_s,
                 c_o.collecting_event.georeference_longitude.to_s
          ]
          row += ce_attributes(c_o, col_defs)
          row += co_attributes(c_o, col_defs)
          row += bc_attributes(c_o, col_defs)
          csv << row.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }

        end
      else
        table_data.each { |_key, value|
          csv << value.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }
        }
      end
    end
  end


  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.earliest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:end_date_year)

    return '1700/01/01' if a.nil? && b.nil?

    d = nil

    if a && b
      if a < b
        d = a
      end
    else
      d = a || b
    end
    d.to_s + '/01/01'
  end

  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.latest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:end_date_year)

    c = Time.now.strftime("%Y/%m/%d")

    return c if a.nil? && b.nil?

    d = nil

    if a && b
      if a > b
        d = a
      end
    else
      d = a || b
    end

    d.to_s + '/12/31'
  end

  # TODO: Clarify this.
  # CAREFULL - this isn't _in_, this is *with*, if it was in it would be spatial query, not a join(:geographic_items)
  #
  # Find all collection objects which have collecting events which have georeferences which have geographic_items which
  # are located within the geographic item supplied
  # @param [GeographicItem] geographic_item_id
  # @return [Scope] of CollectionObject
  def self.in_geographic_item(geographic_item, limit, steps = false)
    geographic_item_id = geographic_item.id
    if steps
      gi     = GeographicItem.find(geographic_item_id)
      # find the geographic_items inside gi
      step_1 = GeographicItem.is_contained_by('any', gi) # .pluck(:id)
      # find the georeferences from the geographic_items
      step_2 = step_1.map(&:georeferences).uniq.flatten
      # find the collecting events connected to the georeferences
      step_3 = step_2.map(&:collecting_event).uniq.flatten
      # find the collection objects associated with the collecting events
      step_4 = step_3.map(&:collection_objects).flatten.map(&:id).uniq
      retval = CollectionObject.where(id: step_4.sort)
    else
      retval = CollectionObject.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)).limit(limit).includes(:data_attributes, :collecting_event)
    end
    retval
  end

  def self.selected_column_names
    @selected_column_names = {ce: {in: {}, im: {}},
                              co: {in: {}, im: {}},
                              bc: {in: {}, im: {}}
    } if @selected_column_names.nil?
    @selected_column_names
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collecting events
  # decode which headers to be displayed for collecting events
  def self.ce_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:ce][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:ce][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.ce_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.collecting_event.internal_attributes
      all_import_das   = collection_object.collecting_event.import_attributes
      group            = collection[:ce]
      unless group.nil?
        group.keys.each { |type_key|
          group[type_key.to_sym].keys.each { |header|
            this_val = nil
            case type_key.to_sym
              when :in
                all_internal_das.each { |da|
                  if da.predicate.name == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              when :im
                all_import_das.each { |da|
                  if da.import_predicate == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              else
            end
          }
        }
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collection objects
  # decode which headers to be displayed for collection objects
  def self.co_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:co][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:co][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.co_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.internal_attributes
      all_import_das   = collection_object.import_attributes
      group            = collection[:co]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = nil
              all_internal_das.each { |da|
                if da.predicate.name == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
        unless group.empty?
          unless group[:im].empty?
            group[:im].keys.each { |header|
              this_val = nil
              all_import_das.each { |da|
                if da.import_predicate == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for biocuration classifications
  # decode which headers to be displayed for biocuration classifications
  def self.bc_headers(project_id)
    CollectionObject.selected_column_names
    # add selectable column names (unselected) to the column name list list
    BiocurationClass.where(project_id: project_id).map(&:name).each { |column_name|
      @selected_column_names[:bc][:in][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.bc_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      group = collection[:bc]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = collection_object.biocuration_classes.map(&:name).include?(header) ? '1' : '0'
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Array] collecting_event_ids (e.g., from CollectingEvent.in_date_range)
  # @param [Array] area_object_ids (e.g., from GeographicItem.gather_selected_data())
  # @return [Scope] of intersection of collecting events (usually by date range)
  #   and collection objects (usually by inclusion in geographic areas/items)
  def self.from_collecting_events(collecting_event_ids, area_object_ids, area_set, project_id)
    collecting_events_clause = {collecting_event_id: collecting_event_ids, project: project_id}
    area_objects_clause = {id: area_object_ids, project: project_id}

    if (collecting_event_ids.empty?)
      collecting_events_clause = {project: project_id}
    end

    if (area_object_ids.empty?)
      area_objects_clause = {}
      if (area_set)
        area_objects_clause = 'false'
      end
    end
    
    retval = CollectionObject.joins(:collecting_event)
                 .where(collecting_events_clause)
                 .where(area_objects_clause)
    retval
  end

  # @param [Hash] search_start_date string in form 'yyyy/mm/dd'
  # @param [Hash] search_end_date string in form 'yyyy/mm/dd'
  # @param [Hash] partial_overlap 'on' or 'off'
  # @return [Scope] of selected collection objects through collecting events with georeferences, remember to scope to project!
  def self.in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on')
    allow_partial = (partial_overlap.downcase == 'off' ? false : true) # TODO: Just get the correct values from the form!
    where_sql = CollectingEvent.date_sql_from_dates(search_start_date, search_end_date, allow_partial)
    joins(:collecting_event).where(where_sql)
  end

  def sv_missing_accession_fields
    soft_validations.add(:accessioned_at, 'Date is not selected') if self.accessioned_at.nil? && !self.accession_provider.nil?
    soft_validations.add(:base, 'Provider is not selected') if !self.accessioned_at.nil? && self.accession_provider.nil?
  end

  def sv_missing_deaccession_fields
    soft_validations.add(:deaccessioned_at, 'Date is not selected') if self.deaccessioned_at.nil? && !self.deaccession_reason.blank?
    soft_validations.add(:base, 'Recipient is not selected') if self.deaccession_recipient.nil? && self.deaccession_reason && self.deaccessioned_at
    soft_validations.add(:deaccession_reason, 'Reason is is not defined') if self.deaccession_reason.blank? && self.deaccession_recipient && self.deaccessioned_at
  end

  def sv_missing_collecting_event
    # see biological_collection_object
  end

  def sv_missing_preparation_type
    # see biological_collection_object
  end

  def sv_missing_repository
    # see biological_collection_object
  end

  protected

  def add_to_dwc_occurrence
    get_dwc_occurrence
  end
  handle_asynchronously :add_to_dwc_occurrence, run_at: Proc.new { 20.seconds.from_now }

  def check_that_both_of_category_and_total_are_not_present
    errors.add(:ranged_lot_category_id, 'Both ranged_lot_category and total can not be set') if !ranged_lot_category_id.blank? && !total.blank?
  end

  def check_that_either_total_or_ranged_lot_category_id_is_present
    errors.add(:base, 'Either total or a ranged lot category must be provided') if ranged_lot_category_id.blank? && total.blank?
  end

  def assign_type_if_total_or_ranged_lot_category_id_provided
    if self.total == 1
      self.type = 'Specimen'
    elsif self.total.to_i > 1
      self.type = 'Lot'
    elsif total.nil? && !ranged_lot_category_id.blank?
      self.type = 'RangedLot'
    end
    true
  end

  def reject_taxon_determinations(attributed)

    a = attributed['otu_id']
    b = attributed['otu_attributes'] 

    return true if !a.present? && !b.present?

    if a.present?
      return true if b.present? && ( b['name'].present? || b['taxon_name_id'].present? ) # not both
      return false 
    end

    if b.present?
      return true if !b['name'].present? && !b['taxon_name_id'].present?
    end 

    false
  end


  def reject_collecting_event(attributed)
    reject = true
    CollectingEvent.data_attributes.each do |a|
      if !attributed[a].blank?
        reject = false
        break
      end
    end
    # !! does not account for georeferences_attributes!
    reject
  end

end

- (String) buffered_other_labels

An incoming, typically verbatim, block of data, as typically found on label that is unrelated to determinations or collecting events. All buffered_ attributes are written but not intended to be deleted or otherwise updated. Buffered_ attributes are typically only used in rapid data capture, primarily in historical situations.

Returns:

  • (String)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'app/models/collection_object.rb', line 59

class CollectionObject < ActiveRecord::Base

  include GlobalID::Identification
  include Housekeeping
  include Shared::Citable
  include Shared::Containable
  include Shared::DataAttributes
  include Shared::Loanable
  include Shared::HasRoles
  include Shared::Identifiable
  include Shared::Notable
  include Shared::Taggable
  include Shared::Depictions
  include Shared::OriginRelationship
  include Shared::Confidence
  include Shared::IsData
  include SoftValidation

  include Shared::IsDwcOccurrence
  include CollectionObject::DwcExtensions 

  is_origin_for :collection_objects
  has_paper_trail :on => [:update] 

  CO_OTU_HEADERS      = %w{OTU OTU\ name Family Genus Species Country State County Locality Latitude Longitude}.freeze
  BUFFERED_ATTRIBUTES = %i{buffered_collecting_event buffered_determinations buffered_other_labels}.freeze

  # @return [Boolean]
  #  When true, cached values are not built
  attr_accessor :no_cached

  after_save :add_to_dwc_occurrence, if: '!self.no_cached'
  
  # Otu delegations
  delegate :name, to: :current_otu, prefix: :otu, allow_nil: true # could be Otu#otu_name?
  delegate :id, to: :current_otu, prefix: :otu, allow_nil: true

  # Identifier delegations
  delegate :cached, to: :preferred_catalog_number, prefix: :catalog_number, allow_nil: true

  # CollectingEvent delegations
  delegate :map_center, to: :collecting_event, prefix: :collecting_event, allow_nil: true

  # Repository delegations
  delegate :acronym, to: :repository, prefix: :repository, allow_nil: true
  delegate :url, to: :repository, prefix: :repository, allow_nil: true 

  # Preparation delegations
  delegate :name, to: :preparation_type, prefix: :preparation_type, allow_nil: true

  has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object
  has_one :accession_provider, through: :accession_provider_role, source: :person
  has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object
  has_one :deaccession_recipient, through: :deaccession_recipient_role, source: :person

  has_many :derived_collection_objects, inverse_of: :collection_object
  has_many :collection_object_observations, through: :derived_collection_objects, inverse_of: :collection_objects
  has_many :sqed_depictions, through: :depictions

  # This must come before taxon determinations !!
  has_many :otus, through: :taxon_determinations, inverse_of: :collection_objects

  has_many :taxon_names, through: :otus

  # This is a problem, but here for the forseeable future for nested attributes purporses.
  has_many :taxon_determinations, foreign_key: :biological_collection_object_id, inverse_of: :biological_collection_object

  has_many :type_designations, class_name: 'TypeMaterial', foreign_key: :biological_object_id, inverse_of: :material

  belongs_to :collecting_event, inverse_of: :collection_objects
  belongs_to :preparation_type, inverse_of: :collection_objects
  belongs_to :ranged_lot_category, inverse_of: :ranged_lots
  belongs_to :repository, inverse_of: :collection_objects

  has_many :georeferences, through: :collecting_event
  has_many :geographic_items, through: :georeferences

  accepts_nested_attributes_for :otus, allow_destroy: true
  accepts_nested_attributes_for :taxon_determinations, allow_destroy: true, reject_if: :reject_taxon_determinations
  accepts_nested_attributes_for :collecting_event, allow_destroy: true, reject_if: :reject_collecting_event

  validates_presence_of :type
  validate :check_that_either_total_or_ranged_lot_category_id_is_present
  validate :check_that_both_of_category_and_total_are_not_present

  before_validation :assign_type_if_total_or_ranged_lot_category_id_provided

  soft_validate(:sv_missing_accession_fields, set: :missing_accession_fields)
  soft_validate(:sv_missing_deaccession_fields, set: :missing_deaccession_fields)

  def preferred_catalog_number
    Identifier::Local::CatalogNumber.where(identifier_object: self).first 
  end

  def missing_determination
    # see BiologicalCollectionObject
  end

  def self.find_for_autocomplete(params)
    Queries::BiologicalCollectionObjectAutocompleteQuery.new(params[:term]).all.where(project_id: params[:project_id]).includes(taxon_determinations: [:determiners]).limit(50)
  end

  # TODO: move to a helper
  def self.breakdown_status(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)

    breakdown = {
      total_objects:     collection_objects.length,
      collecting_events: {},
      determinations:    {},
      bio_overview:      []
    }

    breakdown.merge!(breakdown_buffered(collection_objects))

    collection_objects.each do |co|
      breakdown[:collecting_events].merge!(co => co.collecting_event) if co.collecting_event
      breakdown[:determinations].merge!(co => co.taxon_determinations) if co.taxon_determinations.any?
      breakdown[:bio_overview].push([co.total, co.biocuration_classes.collect { |a| a.name }])
    end

    breakdown
  end

  # @return [Hash]
  #   a unque list of buffered_ values observed in the collection objects passed
  def self.breakdown_buffered(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)
    breakdown          = {}
    categories         = BUFFERED_ATTRIBUTES

    categories.each do |c|
      breakdown[c] = []
    end

    categories.each do |c|
      collection_objects.each do |co|
        breakdown[c].push co.send(c)
      end
    end

    categories.each do |c|
      breakdown[c].uniq!
    end

    breakdown
  end

  # return [Boolean]
  #    True if instance is a subclass of BiologicalCollectionObject
  def biological?
    self.class <= BiologicalCollectionObject ? true : false
  end

  def annotations
    h = annotations_hash
    (h['biocuration classifications'] = self.biocuration_classes) if self.biological? && self.biocuration_classifications.any?
    h
  end
  
  # @param [String] rank
  # @return [String] if a determination exists, and the Otu in the determination has a taxon name then return the taxon name at the rank supplied
  def name_at_rank_string(rank)
    current_taxon_name.try(:ancestor_at_rank, rank).try(:cached_html)
  end

  # @param [Scope] scope of selected CollectionObjects
  # @param [Hash] col_defs selected headers and types
  # @param [Hash] table_data (optional)
  # @return [CSV] tab-separated data
  # Generate the CSV (tab-separated) data for the file to be sent, substitute for new-lines and tabs
  def self.generate_report_download(scope, col_defs, table_data = nil)
    CSV.generate do |csv|
      row = CO_OTU_HEADERS
      unless col_defs.nil?
        %w(ce co bc).each { |column_type|
          items = []
          unless col_defs[column_type.to_sym].nil?
            unless col_defs[column_type.to_sym][:in].nil?
              items.push(col_defs[column_type.to_sym][:in].keys)
            end
            unless col_defs[column_type.to_sym][:im].nil?
              items.push(col_defs[column_type.to_sym][:im].keys)
            end
          end
          row += items.flatten
        }
      end
      csv << row
      if table_data.nil?
        scope.order(id: :asc).each do |c_o|
          row = [c_o.otu_id,
                 c_o.otu_name,
                 c_o.name_at_rank_string(:family),
                 c_o.name_at_rank_string(:genus),
                 c_o.name_at_rank_string(:species),
                 c_o.collecting_event.country_name,
                 c_o.collecting_event.state_name,
                 c_o.collecting_event.county_name,
                 c_o.collecting_event.verbatim_locality,
                 c_o.collecting_event.georeference_latitude.to_s,
                 c_o.collecting_event.georeference_longitude.to_s
          ]
          row += ce_attributes(c_o, col_defs)
          row += co_attributes(c_o, col_defs)
          row += bc_attributes(c_o, col_defs)
          csv << row.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }

        end
      else
        table_data.each { |_key, value|
          csv << value.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }
        }
      end
    end
  end


  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.earliest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:end_date_year)

    return '1700/01/01' if a.nil? && b.nil?

    d = nil

    if a && b
      if a < b
        d = a
      end
    else
      d = a || b
    end
    d.to_s + '/01/01'
  end

  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.latest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:end_date_year)

    c = Time.now.strftime("%Y/%m/%d")

    return c if a.nil? && b.nil?

    d = nil

    if a && b
      if a > b
        d = a
      end
    else
      d = a || b
    end

    d.to_s + '/12/31'
  end

  # TODO: Clarify this.
  # CAREFULL - this isn't _in_, this is *with*, if it was in it would be spatial query, not a join(:geographic_items)
  #
  # Find all collection objects which have collecting events which have georeferences which have geographic_items which
  # are located within the geographic item supplied
  # @param [GeographicItem] geographic_item_id
  # @return [Scope] of CollectionObject
  def self.in_geographic_item(geographic_item, limit, steps = false)
    geographic_item_id = geographic_item.id
    if steps
      gi     = GeographicItem.find(geographic_item_id)
      # find the geographic_items inside gi
      step_1 = GeographicItem.is_contained_by('any', gi) # .pluck(:id)
      # find the georeferences from the geographic_items
      step_2 = step_1.map(&:georeferences).uniq.flatten
      # find the collecting events connected to the georeferences
      step_3 = step_2.map(&:collecting_event).uniq.flatten
      # find the collection objects associated with the collecting events
      step_4 = step_3.map(&:collection_objects).flatten.map(&:id).uniq
      retval = CollectionObject.where(id: step_4.sort)
    else
      retval = CollectionObject.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)).limit(limit).includes(:data_attributes, :collecting_event)
    end
    retval
  end

  def self.selected_column_names
    @selected_column_names = {ce: {in: {}, im: {}},
                              co: {in: {}, im: {}},
                              bc: {in: {}, im: {}}
    } if @selected_column_names.nil?
    @selected_column_names
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collecting events
  # decode which headers to be displayed for collecting events
  def self.ce_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:ce][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:ce][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.ce_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.collecting_event.internal_attributes
      all_import_das   = collection_object.collecting_event.import_attributes
      group            = collection[:ce]
      unless group.nil?
        group.keys.each { |type_key|
          group[type_key.to_sym].keys.each { |header|
            this_val = nil
            case type_key.to_sym
              when :in
                all_internal_das.each { |da|
                  if da.predicate.name == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              when :im
                all_import_das.each { |da|
                  if da.import_predicate == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              else
            end
          }
        }
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collection objects
  # decode which headers to be displayed for collection objects
  def self.co_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:co][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:co][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.co_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.internal_attributes
      all_import_das   = collection_object.import_attributes
      group            = collection[:co]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = nil
              all_internal_das.each { |da|
                if da.predicate.name == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
        unless group.empty?
          unless group[:im].empty?
            group[:im].keys.each { |header|
              this_val = nil
              all_import_das.each { |da|
                if da.import_predicate == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for biocuration classifications
  # decode which headers to be displayed for biocuration classifications
  def self.bc_headers(project_id)
    CollectionObject.selected_column_names
    # add selectable column names (unselected) to the column name list list
    BiocurationClass.where(project_id: project_id).map(&:name).each { |column_name|
      @selected_column_names[:bc][:in][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.bc_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      group = collection[:bc]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = collection_object.biocuration_classes.map(&:name).include?(header) ? '1' : '0'
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Array] collecting_event_ids (e.g., from CollectingEvent.in_date_range)
  # @param [Array] area_object_ids (e.g., from GeographicItem.gather_selected_data())
  # @return [Scope] of intersection of collecting events (usually by date range)
  #   and collection objects (usually by inclusion in geographic areas/items)
  def self.from_collecting_events(collecting_event_ids, area_object_ids, area_set, project_id)
    collecting_events_clause = {collecting_event_id: collecting_event_ids, project: project_id}
    area_objects_clause = {id: area_object_ids, project: project_id}

    if (collecting_event_ids.empty?)
      collecting_events_clause = {project: project_id}
    end

    if (area_object_ids.empty?)
      area_objects_clause = {}
      if (area_set)
        area_objects_clause = 'false'
      end
    end
    
    retval = CollectionObject.joins(:collecting_event)
                 .where(collecting_events_clause)
                 .where(area_objects_clause)
    retval
  end

  # @param [Hash] search_start_date string in form 'yyyy/mm/dd'
  # @param [Hash] search_end_date string in form 'yyyy/mm/dd'
  # @param [Hash] partial_overlap 'on' or 'off'
  # @return [Scope] of selected collection objects through collecting events with georeferences, remember to scope to project!
  def self.in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on')
    allow_partial = (partial_overlap.downcase == 'off' ? false : true) # TODO: Just get the correct values from the form!
    where_sql = CollectingEvent.date_sql_from_dates(search_start_date, search_end_date, allow_partial)
    joins(:collecting_event).where(where_sql)
  end

  def sv_missing_accession_fields
    soft_validations.add(:accessioned_at, 'Date is not selected') if self.accessioned_at.nil? && !self.accession_provider.nil?
    soft_validations.add(:base, 'Provider is not selected') if !self.accessioned_at.nil? && self.accession_provider.nil?
  end

  def sv_missing_deaccession_fields
    soft_validations.add(:deaccessioned_at, 'Date is not selected') if self.deaccessioned_at.nil? && !self.deaccession_reason.blank?
    soft_validations.add(:base, 'Recipient is not selected') if self.deaccession_recipient.nil? && self.deaccession_reason && self.deaccessioned_at
    soft_validations.add(:deaccession_reason, 'Reason is is not defined') if self.deaccession_reason.blank? && self.deaccession_recipient && self.deaccessioned_at
  end

  def sv_missing_collecting_event
    # see biological_collection_object
  end

  def sv_missing_preparation_type
    # see biological_collection_object
  end

  def sv_missing_repository
    # see biological_collection_object
  end

  protected

  def add_to_dwc_occurrence
    get_dwc_occurrence
  end
  handle_asynchronously :add_to_dwc_occurrence, run_at: Proc.new { 20.seconds.from_now }

  def check_that_both_of_category_and_total_are_not_present
    errors.add(:ranged_lot_category_id, 'Both ranged_lot_category and total can not be set') if !ranged_lot_category_id.blank? && !total.blank?
  end

  def check_that_either_total_or_ranged_lot_category_id_is_present
    errors.add(:base, 'Either total or a ranged lot category must be provided') if ranged_lot_category_id.blank? && total.blank?
  end

  def assign_type_if_total_or_ranged_lot_category_id_provided
    if self.total == 1
      self.type = 'Specimen'
    elsif self.total.to_i > 1
      self.type = 'Lot'
    elsif total.nil? && !ranged_lot_category_id.blank?
      self.type = 'RangedLot'
    end
    true
  end

  def reject_taxon_determinations(attributed)

    a = attributed['otu_id']
    b = attributed['otu_attributes'] 

    return true if !a.present? && !b.present?

    if a.present?
      return true if b.present? && ( b['name'].present? || b['taxon_name_id'].present? ) # not both
      return false 
    end

    if b.present?
      return true if !b['name'].present? && !b['taxon_name_id'].present?
    end 

    false
  end


  def reject_collecting_event(attributed)
    reject = true
    CollectingEvent.data_attributes.each do |a|
      if !attributed[a].blank?
        reject = false
        break
      end
    end
    # !! does not account for georeferences_attributes!
    reject
  end

end

- (Integer) collecting_event_id

The id of the collecting event from whence this object came. See CollectingEvent.

Returns:

  • (Integer)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'app/models/collection_object.rb', line 59

class CollectionObject < ActiveRecord::Base

  include GlobalID::Identification
  include Housekeeping
  include Shared::Citable
  include Shared::Containable
  include Shared::DataAttributes
  include Shared::Loanable
  include Shared::HasRoles
  include Shared::Identifiable
  include Shared::Notable
  include Shared::Taggable
  include Shared::Depictions
  include Shared::OriginRelationship
  include Shared::Confidence
  include Shared::IsData
  include SoftValidation

  include Shared::IsDwcOccurrence
  include CollectionObject::DwcExtensions 

  is_origin_for :collection_objects
  has_paper_trail :on => [:update] 

  CO_OTU_HEADERS      = %w{OTU OTU\ name Family Genus Species Country State County Locality Latitude Longitude}.freeze
  BUFFERED_ATTRIBUTES = %i{buffered_collecting_event buffered_determinations buffered_other_labels}.freeze

  # @return [Boolean]
  #  When true, cached values are not built
  attr_accessor :no_cached

  after_save :add_to_dwc_occurrence, if: '!self.no_cached'
  
  # Otu delegations
  delegate :name, to: :current_otu, prefix: :otu, allow_nil: true # could be Otu#otu_name?
  delegate :id, to: :current_otu, prefix: :otu, allow_nil: true

  # Identifier delegations
  delegate :cached, to: :preferred_catalog_number, prefix: :catalog_number, allow_nil: true

  # CollectingEvent delegations
  delegate :map_center, to: :collecting_event, prefix: :collecting_event, allow_nil: true

  # Repository delegations
  delegate :acronym, to: :repository, prefix: :repository, allow_nil: true
  delegate :url, to: :repository, prefix: :repository, allow_nil: true 

  # Preparation delegations
  delegate :name, to: :preparation_type, prefix: :preparation_type, allow_nil: true

  has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object
  has_one :accession_provider, through: :accession_provider_role, source: :person
  has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object
  has_one :deaccession_recipient, through: :deaccession_recipient_role, source: :person

  has_many :derived_collection_objects, inverse_of: :collection_object
  has_many :collection_object_observations, through: :derived_collection_objects, inverse_of: :collection_objects
  has_many :sqed_depictions, through: :depictions

  # This must come before taxon determinations !!
  has_many :otus, through: :taxon_determinations, inverse_of: :collection_objects

  has_many :taxon_names, through: :otus

  # This is a problem, but here for the forseeable future for nested attributes purporses.
  has_many :taxon_determinations, foreign_key: :biological_collection_object_id, inverse_of: :biological_collection_object

  has_many :type_designations, class_name: 'TypeMaterial', foreign_key: :biological_object_id, inverse_of: :material

  belongs_to :collecting_event, inverse_of: :collection_objects
  belongs_to :preparation_type, inverse_of: :collection_objects
  belongs_to :ranged_lot_category, inverse_of: :ranged_lots
  belongs_to :repository, inverse_of: :collection_objects

  has_many :georeferences, through: :collecting_event
  has_many :geographic_items, through: :georeferences

  accepts_nested_attributes_for :otus, allow_destroy: true
  accepts_nested_attributes_for :taxon_determinations, allow_destroy: true, reject_if: :reject_taxon_determinations
  accepts_nested_attributes_for :collecting_event, allow_destroy: true, reject_if: :reject_collecting_event

  validates_presence_of :type
  validate :check_that_either_total_or_ranged_lot_category_id_is_present
  validate :check_that_both_of_category_and_total_are_not_present

  before_validation :assign_type_if_total_or_ranged_lot_category_id_provided

  soft_validate(:sv_missing_accession_fields, set: :missing_accession_fields)
  soft_validate(:sv_missing_deaccession_fields, set: :missing_deaccession_fields)

  def preferred_catalog_number
    Identifier::Local::CatalogNumber.where(identifier_object: self).first 
  end

  def missing_determination
    # see BiologicalCollectionObject
  end

  def self.find_for_autocomplete(params)
    Queries::BiologicalCollectionObjectAutocompleteQuery.new(params[:term]).all.where(project_id: params[:project_id]).includes(taxon_determinations: [:determiners]).limit(50)
  end

  # TODO: move to a helper
  def self.breakdown_status(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)

    breakdown = {
      total_objects:     collection_objects.length,
      collecting_events: {},
      determinations:    {},
      bio_overview:      []
    }

    breakdown.merge!(breakdown_buffered(collection_objects))

    collection_objects.each do |co|
      breakdown[:collecting_events].merge!(co => co.collecting_event) if co.collecting_event
      breakdown[:determinations].merge!(co => co.taxon_determinations) if co.taxon_determinations.any?
      breakdown[:bio_overview].push([co.total, co.biocuration_classes.collect { |a| a.name }])
    end

    breakdown
  end

  # @return [Hash]
  #   a unque list of buffered_ values observed in the collection objects passed
  def self.breakdown_buffered(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)
    breakdown          = {}
    categories         = BUFFERED_ATTRIBUTES

    categories.each do |c|
      breakdown[c] = []
    end

    categories.each do |c|
      collection_objects.each do |co|
        breakdown[c].push co.send(c)
      end
    end

    categories.each do |c|
      breakdown[c].uniq!
    end

    breakdown
  end

  # return [Boolean]
  #    True if instance is a subclass of BiologicalCollectionObject
  def biological?
    self.class <= BiologicalCollectionObject ? true : false
  end

  def annotations
    h = annotations_hash
    (h['biocuration classifications'] = self.biocuration_classes) if self.biological? && self.biocuration_classifications.any?
    h
  end
  
  # @param [String] rank
  # @return [String] if a determination exists, and the Otu in the determination has a taxon name then return the taxon name at the rank supplied
  def name_at_rank_string(rank)
    current_taxon_name.try(:ancestor_at_rank, rank).try(:cached_html)
  end

  # @param [Scope] scope of selected CollectionObjects
  # @param [Hash] col_defs selected headers and types
  # @param [Hash] table_data (optional)
  # @return [CSV] tab-separated data
  # Generate the CSV (tab-separated) data for the file to be sent, substitute for new-lines and tabs
  def self.generate_report_download(scope, col_defs, table_data = nil)
    CSV.generate do |csv|
      row = CO_OTU_HEADERS
      unless col_defs.nil?
        %w(ce co bc).each { |column_type|
          items = []
          unless col_defs[column_type.to_sym].nil?
            unless col_defs[column_type.to_sym][:in].nil?
              items.push(col_defs[column_type.to_sym][:in].keys)
            end
            unless col_defs[column_type.to_sym][:im].nil?
              items.push(col_defs[column_type.to_sym][:im].keys)
            end
          end
          row += items.flatten
        }
      end
      csv << row
      if table_data.nil?
        scope.order(id: :asc).each do |c_o|
          row = [c_o.otu_id,
                 c_o.otu_name,
                 c_o.name_at_rank_string(:family),
                 c_o.name_at_rank_string(:genus),
                 c_o.name_at_rank_string(:species),
                 c_o.collecting_event.country_name,
                 c_o.collecting_event.state_name,
                 c_o.collecting_event.county_name,
                 c_o.collecting_event.verbatim_locality,
                 c_o.collecting_event.georeference_latitude.to_s,
                 c_o.collecting_event.georeference_longitude.to_s
          ]
          row += ce_attributes(c_o, col_defs)
          row += co_attributes(c_o, col_defs)
          row += bc_attributes(c_o, col_defs)
          csv << row.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }

        end
      else
        table_data.each { |_key, value|
          csv << value.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }
        }
      end
    end
  end


  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.earliest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:end_date_year)

    return '1700/01/01' if a.nil? && b.nil?

    d = nil

    if a && b
      if a < b
        d = a
      end
    else
      d = a || b
    end
    d.to_s + '/01/01'
  end

  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.latest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:end_date_year)

    c = Time.now.strftime("%Y/%m/%d")

    return c if a.nil? && b.nil?

    d = nil

    if a && b
      if a > b
        d = a
      end
    else
      d = a || b
    end

    d.to_s + '/12/31'
  end

  # TODO: Clarify this.
  # CAREFULL - this isn't _in_, this is *with*, if it was in it would be spatial query, not a join(:geographic_items)
  #
  # Find all collection objects which have collecting events which have georeferences which have geographic_items which
  # are located within the geographic item supplied
  # @param [GeographicItem] geographic_item_id
  # @return [Scope] of CollectionObject
  def self.in_geographic_item(geographic_item, limit, steps = false)
    geographic_item_id = geographic_item.id
    if steps
      gi     = GeographicItem.find(geographic_item_id)
      # find the geographic_items inside gi
      step_1 = GeographicItem.is_contained_by('any', gi) # .pluck(:id)
      # find the georeferences from the geographic_items
      step_2 = step_1.map(&:georeferences).uniq.flatten
      # find the collecting events connected to the georeferences
      step_3 = step_2.map(&:collecting_event).uniq.flatten
      # find the collection objects associated with the collecting events
      step_4 = step_3.map(&:collection_objects).flatten.map(&:id).uniq
      retval = CollectionObject.where(id: step_4.sort)
    else
      retval = CollectionObject.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)).limit(limit).includes(:data_attributes, :collecting_event)
    end
    retval
  end

  def self.selected_column_names
    @selected_column_names = {ce: {in: {}, im: {}},
                              co: {in: {}, im: {}},
                              bc: {in: {}, im: {}}
    } if @selected_column_names.nil?
    @selected_column_names
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collecting events
  # decode which headers to be displayed for collecting events
  def self.ce_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:ce][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:ce][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.ce_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.collecting_event.internal_attributes
      all_import_das   = collection_object.collecting_event.import_attributes
      group            = collection[:ce]
      unless group.nil?
        group.keys.each { |type_key|
          group[type_key.to_sym].keys.each { |header|
            this_val = nil
            case type_key.to_sym
              when :in
                all_internal_das.each { |da|
                  if da.predicate.name == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              when :im
                all_import_das.each { |da|
                  if da.import_predicate == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              else
            end
          }
        }
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collection objects
  # decode which headers to be displayed for collection objects
  def self.co_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:co][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:co][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.co_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.internal_attributes
      all_import_das   = collection_object.import_attributes
      group            = collection[:co]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = nil
              all_internal_das.each { |da|
                if da.predicate.name == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
        unless group.empty?
          unless group[:im].empty?
            group[:im].keys.each { |header|
              this_val = nil
              all_import_das.each { |da|
                if da.import_predicate == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for biocuration classifications
  # decode which headers to be displayed for biocuration classifications
  def self.bc_headers(project_id)
    CollectionObject.selected_column_names
    # add selectable column names (unselected) to the column name list list
    BiocurationClass.where(project_id: project_id).map(&:name).each { |column_name|
      @selected_column_names[:bc][:in][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.bc_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      group = collection[:bc]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = collection_object.biocuration_classes.map(&:name).include?(header) ? '1' : '0'
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Array] collecting_event_ids (e.g., from CollectingEvent.in_date_range)
  # @param [Array] area_object_ids (e.g., from GeographicItem.gather_selected_data())
  # @return [Scope] of intersection of collecting events (usually by date range)
  #   and collection objects (usually by inclusion in geographic areas/items)
  def self.from_collecting_events(collecting_event_ids, area_object_ids, area_set, project_id)
    collecting_events_clause = {collecting_event_id: collecting_event_ids, project: project_id}
    area_objects_clause = {id: area_object_ids, project: project_id}

    if (collecting_event_ids.empty?)
      collecting_events_clause = {project: project_id}
    end

    if (area_object_ids.empty?)
      area_objects_clause = {}
      if (area_set)
        area_objects_clause = 'false'
      end
    end
    
    retval = CollectionObject.joins(:collecting_event)
                 .where(collecting_events_clause)
                 .where(area_objects_clause)
    retval
  end

  # @param [Hash] search_start_date string in form 'yyyy/mm/dd'
  # @param [Hash] search_end_date string in form 'yyyy/mm/dd'
  # @param [Hash] partial_overlap 'on' or 'off'
  # @return [Scope] of selected collection objects through collecting events with georeferences, remember to scope to project!
  def self.in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on')
    allow_partial = (partial_overlap.downcase == 'off' ? false : true) # TODO: Just get the correct values from the form!
    where_sql = CollectingEvent.date_sql_from_dates(search_start_date, search_end_date, allow_partial)
    joins(:collecting_event).where(where_sql)
  end

  def sv_missing_accession_fields
    soft_validations.add(:accessioned_at, 'Date is not selected') if self.accessioned_at.nil? && !self.accession_provider.nil?
    soft_validations.add(:base, 'Provider is not selected') if !self.accessioned_at.nil? && self.accession_provider.nil?
  end

  def sv_missing_deaccession_fields
    soft_validations.add(:deaccessioned_at, 'Date is not selected') if self.deaccessioned_at.nil? && !self.deaccession_reason.blank?
    soft_validations.add(:base, 'Recipient is not selected') if self.deaccession_recipient.nil? && self.deaccession_reason && self.deaccessioned_at
    soft_validations.add(:deaccession_reason, 'Reason is is not defined') if self.deaccession_reason.blank? && self.deaccession_recipient && self.deaccessioned_at
  end

  def sv_missing_collecting_event
    # see biological_collection_object
  end

  def sv_missing_preparation_type
    # see biological_collection_object
  end

  def sv_missing_repository
    # see biological_collection_object
  end

  protected

  def add_to_dwc_occurrence
    get_dwc_occurrence
  end
  handle_asynchronously :add_to_dwc_occurrence, run_at: Proc.new { 20.seconds.from_now }

  def check_that_both_of_category_and_total_are_not_present
    errors.add(:ranged_lot_category_id, 'Both ranged_lot_category and total can not be set') if !ranged_lot_category_id.blank? && !total.blank?
  end

  def check_that_either_total_or_ranged_lot_category_id_is_present
    errors.add(:base, 'Either total or a ranged lot category must be provided') if ranged_lot_category_id.blank? && total.blank?
  end

  def assign_type_if_total_or_ranged_lot_category_id_provided
    if self.total == 1
      self.type = 'Specimen'
    elsif self.total.to_i > 1
      self.type = 'Lot'
    elsif total.nil? && !ranged_lot_category_id.blank?
      self.type = 'RangedLot'
    end
    true
  end

  def reject_taxon_determinations(attributed)

    a = attributed['otu_id']
    b = attributed['otu_attributes'] 

    return true if !a.present? && !b.present?

    if a.present?
      return true if b.present? && ( b['name'].present? || b['taxon_name_id'].present? ) # not both
      return false 
    end

    if b.present?
      return true if !b['name'].present? && !b['taxon_name_id'].present?
    end 

    false
  end


  def reject_collecting_event(attributed)
    reject = true
    CollectingEvent.data_attributes.each do |a|
      if !attributed[a].blank?
        reject = false
        break
      end
    end
    # !! does not account for georeferences_attributes!
    reject
  end

end

- (String) deaccession_reason

A free text explanation of why the object was removed from tracking.

Returns:

  • (String)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'app/models/collection_object.rb', line 59

class CollectionObject < ActiveRecord::Base

  include GlobalID::Identification
  include Housekeeping
  include Shared::Citable
  include Shared::Containable
  include Shared::DataAttributes
  include Shared::Loanable
  include Shared::HasRoles
  include Shared::Identifiable
  include Shared::Notable
  include Shared::Taggable
  include Shared::Depictions
  include Shared::OriginRelationship
  include Shared::Confidence
  include Shared::IsData
  include SoftValidation

  include Shared::IsDwcOccurrence
  include CollectionObject::DwcExtensions 

  is_origin_for :collection_objects
  has_paper_trail :on => [:update] 

  CO_OTU_HEADERS      = %w{OTU OTU\ name Family Genus Species Country State County Locality Latitude Longitude}.freeze
  BUFFERED_ATTRIBUTES = %i{buffered_collecting_event buffered_determinations buffered_other_labels}.freeze

  # @return [Boolean]
  #  When true, cached values are not built
  attr_accessor :no_cached

  after_save :add_to_dwc_occurrence, if: '!self.no_cached'
  
  # Otu delegations
  delegate :name, to: :current_otu, prefix: :otu, allow_nil: true # could be Otu#otu_name?
  delegate :id, to: :current_otu, prefix: :otu, allow_nil: true

  # Identifier delegations
  delegate :cached, to: :preferred_catalog_number, prefix: :catalog_number, allow_nil: true

  # CollectingEvent delegations
  delegate :map_center, to: :collecting_event, prefix: :collecting_event, allow_nil: true

  # Repository delegations
  delegate :acronym, to: :repository, prefix: :repository, allow_nil: true
  delegate :url, to: :repository, prefix: :repository, allow_nil: true 

  # Preparation delegations
  delegate :name, to: :preparation_type, prefix: :preparation_type, allow_nil: true

  has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object
  has_one :accession_provider, through: :accession_provider_role, source: :person
  has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object
  has_one :deaccession_recipient, through: :deaccession_recipient_role, source: :person

  has_many :derived_collection_objects, inverse_of: :collection_object
  has_many :collection_object_observations, through: :derived_collection_objects, inverse_of: :collection_objects
  has_many :sqed_depictions, through: :depictions

  # This must come before taxon determinations !!
  has_many :otus, through: :taxon_determinations, inverse_of: :collection_objects

  has_many :taxon_names, through: :otus

  # This is a problem, but here for the forseeable future for nested attributes purporses.
  has_many :taxon_determinations, foreign_key: :biological_collection_object_id, inverse_of: :biological_collection_object

  has_many :type_designations, class_name: 'TypeMaterial', foreign_key: :biological_object_id, inverse_of: :material

  belongs_to :collecting_event, inverse_of: :collection_objects
  belongs_to :preparation_type, inverse_of: :collection_objects
  belongs_to :ranged_lot_category, inverse_of: :ranged_lots
  belongs_to :repository, inverse_of: :collection_objects

  has_many :georeferences, through: :collecting_event
  has_many :geographic_items, through: :georeferences

  accepts_nested_attributes_for :otus, allow_destroy: true
  accepts_nested_attributes_for :taxon_determinations, allow_destroy: true, reject_if: :reject_taxon_determinations
  accepts_nested_attributes_for :collecting_event, allow_destroy: true, reject_if: :reject_collecting_event

  validates_presence_of :type
  validate :check_that_either_total_or_ranged_lot_category_id_is_present
  validate :check_that_both_of_category_and_total_are_not_present

  before_validation :assign_type_if_total_or_ranged_lot_category_id_provided

  soft_validate(:sv_missing_accession_fields, set: :missing_accession_fields)
  soft_validate(:sv_missing_deaccession_fields, set: :missing_deaccession_fields)

  def preferred_catalog_number
    Identifier::Local::CatalogNumber.where(identifier_object: self).first 
  end

  def missing_determination
    # see BiologicalCollectionObject
  end

  def self.find_for_autocomplete(params)
    Queries::BiologicalCollectionObjectAutocompleteQuery.new(params[:term]).all.where(project_id: params[:project_id]).includes(taxon_determinations: [:determiners]).limit(50)
  end

  # TODO: move to a helper
  def self.breakdown_status(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)

    breakdown = {
      total_objects:     collection_objects.length,
      collecting_events: {},
      determinations:    {},
      bio_overview:      []
    }

    breakdown.merge!(breakdown_buffered(collection_objects))

    collection_objects.each do |co|
      breakdown[:collecting_events].merge!(co => co.collecting_event) if co.collecting_event
      breakdown[:determinations].merge!(co => co.taxon_determinations) if co.taxon_determinations.any?
      breakdown[:bio_overview].push([co.total, co.biocuration_classes.collect { |a| a.name }])
    end

    breakdown
  end

  # @return [Hash]
  #   a unque list of buffered_ values observed in the collection objects passed
  def self.breakdown_buffered(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)
    breakdown          = {}
    categories         = BUFFERED_ATTRIBUTES

    categories.each do |c|
      breakdown[c] = []
    end

    categories.each do |c|
      collection_objects.each do |co|
        breakdown[c].push co.send(c)
      end
    end

    categories.each do |c|
      breakdown[c].uniq!
    end

    breakdown
  end

  # return [Boolean]
  #    True if instance is a subclass of BiologicalCollectionObject
  def biological?
    self.class <= BiologicalCollectionObject ? true : false
  end

  def annotations
    h = annotations_hash
    (h['biocuration classifications'] = self.biocuration_classes) if self.biological? && self.biocuration_classifications.any?
    h
  end
  
  # @param [String] rank
  # @return [String] if a determination exists, and the Otu in the determination has a taxon name then return the taxon name at the rank supplied
  def name_at_rank_string(rank)
    current_taxon_name.try(:ancestor_at_rank, rank).try(:cached_html)
  end

  # @param [Scope] scope of selected CollectionObjects
  # @param [Hash] col_defs selected headers and types
  # @param [Hash] table_data (optional)
  # @return [CSV] tab-separated data
  # Generate the CSV (tab-separated) data for the file to be sent, substitute for new-lines and tabs
  def self.generate_report_download(scope, col_defs, table_data = nil)
    CSV.generate do |csv|
      row = CO_OTU_HEADERS
      unless col_defs.nil?
        %w(ce co bc).each { |column_type|
          items = []
          unless col_defs[column_type.to_sym].nil?
            unless col_defs[column_type.to_sym][:in].nil?
              items.push(col_defs[column_type.to_sym][:in].keys)
            end
            unless col_defs[column_type.to_sym][:im].nil?
              items.push(col_defs[column_type.to_sym][:im].keys)
            end
          end
          row += items.flatten
        }
      end
      csv << row
      if table_data.nil?
        scope.order(id: :asc).each do |c_o|
          row = [c_o.otu_id,
                 c_o.otu_name,
                 c_o.name_at_rank_string(:family),
                 c_o.name_at_rank_string(:genus),
                 c_o.name_at_rank_string(:species),
                 c_o.collecting_event.country_name,
                 c_o.collecting_event.state_name,
                 c_o.collecting_event.county_name,
                 c_o.collecting_event.verbatim_locality,
                 c_o.collecting_event.georeference_latitude.to_s,
                 c_o.collecting_event.georeference_longitude.to_s
          ]
          row += ce_attributes(c_o, col_defs)
          row += co_attributes(c_o, col_defs)
          row += bc_attributes(c_o, col_defs)
          csv << row.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }

        end
      else
        table_data.each { |_key, value|
          csv << value.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }
        }
      end
    end
  end


  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.earliest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:end_date_year)

    return '1700/01/01' if a.nil? && b.nil?

    d = nil

    if a && b
      if a < b
        d = a
      end
    else
      d = a || b
    end
    d.to_s + '/01/01'
  end

  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.latest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:end_date_year)

    c = Time.now.strftime("%Y/%m/%d")

    return c if a.nil? && b.nil?

    d = nil

    if a && b
      if a > b
        d = a
      end
    else
      d = a || b
    end

    d.to_s + '/12/31'
  end

  # TODO: Clarify this.
  # CAREFULL - this isn't _in_, this is *with*, if it was in it would be spatial query, not a join(:geographic_items)
  #
  # Find all collection objects which have collecting events which have georeferences which have geographic_items which
  # are located within the geographic item supplied
  # @param [GeographicItem] geographic_item_id
  # @return [Scope] of CollectionObject
  def self.in_geographic_item(geographic_item, limit, steps = false)
    geographic_item_id = geographic_item.id
    if steps
      gi     = GeographicItem.find(geographic_item_id)
      # find the geographic_items inside gi
      step_1 = GeographicItem.is_contained_by('any', gi) # .pluck(:id)
      # find the georeferences from the geographic_items
      step_2 = step_1.map(&:georeferences).uniq.flatten
      # find the collecting events connected to the georeferences
      step_3 = step_2.map(&:collecting_event).uniq.flatten
      # find the collection objects associated with the collecting events
      step_4 = step_3.map(&:collection_objects).flatten.map(&:id).uniq
      retval = CollectionObject.where(id: step_4.sort)
    else
      retval = CollectionObject.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)).limit(limit).includes(:data_attributes, :collecting_event)
    end
    retval
  end

  def self.selected_column_names
    @selected_column_names = {ce: {in: {}, im: {}},
                              co: {in: {}, im: {}},
                              bc: {in: {}, im: {}}
    } if @selected_column_names.nil?
    @selected_column_names
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collecting events
  # decode which headers to be displayed for collecting events
  def self.ce_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:ce][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:ce][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.ce_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.collecting_event.internal_attributes
      all_import_das   = collection_object.collecting_event.import_attributes
      group            = collection[:ce]
      unless group.nil?
        group.keys.each { |type_key|
          group[type_key.to_sym].keys.each { |header|
            this_val = nil
            case type_key.to_sym
              when :in
                all_internal_das.each { |da|
                  if da.predicate.name == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              when :im
                all_import_das.each { |da|
                  if da.import_predicate == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              else
            end
          }
        }
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collection objects
  # decode which headers to be displayed for collection objects
  def self.co_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:co][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:co][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.co_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.internal_attributes
      all_import_das   = collection_object.import_attributes
      group            = collection[:co]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = nil
              all_internal_das.each { |da|
                if da.predicate.name == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
        unless group.empty?
          unless group[:im].empty?
            group[:im].keys.each { |header|
              this_val = nil
              all_import_das.each { |da|
                if da.import_predicate == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for biocuration classifications
  # decode which headers to be displayed for biocuration classifications
  def self.bc_headers(project_id)
    CollectionObject.selected_column_names
    # add selectable column names (unselected) to the column name list list
    BiocurationClass.where(project_id: project_id).map(&:name).each { |column_name|
      @selected_column_names[:bc][:in][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.bc_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      group = collection[:bc]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = collection_object.biocuration_classes.map(&:name).include?(header) ? '1' : '0'
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Array] collecting_event_ids (e.g., from CollectingEvent.in_date_range)
  # @param [Array] area_object_ids (e.g., from GeographicItem.gather_selected_data())
  # @return [Scope] of intersection of collecting events (usually by date range)
  #   and collection objects (usually by inclusion in geographic areas/items)
  def self.from_collecting_events(collecting_event_ids, area_object_ids, area_set, project_id)
    collecting_events_clause = {collecting_event_id: collecting_event_ids, project: project_id}
    area_objects_clause = {id: area_object_ids, project: project_id}

    if (collecting_event_ids.empty?)
      collecting_events_clause = {project: project_id}
    end

    if (area_object_ids.empty?)
      area_objects_clause = {}
      if (area_set)
        area_objects_clause = 'false'
      end
    end
    
    retval = CollectionObject.joins(:collecting_event)
                 .where(collecting_events_clause)
                 .where(area_objects_clause)
    retval
  end

  # @param [Hash] search_start_date string in form 'yyyy/mm/dd'
  # @param [Hash] search_end_date string in form 'yyyy/mm/dd'
  # @param [Hash] partial_overlap 'on' or 'off'
  # @return [Scope] of selected collection objects through collecting events with georeferences, remember to scope to project!
  def self.in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on')
    allow_partial = (partial_overlap.downcase == 'off' ? false : true) # TODO: Just get the correct values from the form!
    where_sql = CollectingEvent.date_sql_from_dates(search_start_date, search_end_date, allow_partial)
    joins(:collecting_event).where(where_sql)
  end

  def sv_missing_accession_fields
    soft_validations.add(:accessioned_at, 'Date is not selected') if self.accessioned_at.nil? && !self.accession_provider.nil?
    soft_validations.add(:base, 'Provider is not selected') if !self.accessioned_at.nil? && self.accession_provider.nil?
  end

  def sv_missing_deaccession_fields
    soft_validations.add(:deaccessioned_at, 'Date is not selected') if self.deaccessioned_at.nil? && !self.deaccession_reason.blank?
    soft_validations.add(:base, 'Recipient is not selected') if self.deaccession_recipient.nil? && self.deaccession_reason && self.deaccessioned_at
    soft_validations.add(:deaccession_reason, 'Reason is is not defined') if self.deaccession_reason.blank? && self.deaccession_recipient && self.deaccessioned_at
  end

  def sv_missing_collecting_event
    # see biological_collection_object
  end

  def sv_missing_preparation_type
    # see biological_collection_object
  end

  def sv_missing_repository
    # see biological_collection_object
  end

  protected

  def add_to_dwc_occurrence
    get_dwc_occurrence
  end
  handle_asynchronously :add_to_dwc_occurrence, run_at: Proc.new { 20.seconds.from_now }

  def check_that_both_of_category_and_total_are_not_present
    errors.add(:ranged_lot_category_id, 'Both ranged_lot_category and total can not be set') if !ranged_lot_category_id.blank? && !total.blank?
  end

  def check_that_either_total_or_ranged_lot_category_id_is_present
    errors.add(:base, 'Either total or a ranged lot category must be provided') if ranged_lot_category_id.blank? && total.blank?
  end

  def assign_type_if_total_or_ranged_lot_category_id_provided
    if self.total == 1
      self.type = 'Specimen'
    elsif self.total.to_i > 1
      self.type = 'Lot'
    elsif total.nil? && !ranged_lot_category_id.blank?
      self.type = 'RangedLot'
    end
    true
  end

  def reject_taxon_determinations(attributed)

    a = attributed['otu_id']
    b = attributed['otu_attributes'] 

    return true if !a.present? && !b.present?

    if a.present?
      return true if b.present? && ( b['name'].present? || b['taxon_name_id'].present? ) # not both
      return false 
    end

    if b.present?
      return true if !b['name'].present? && !b['taxon_name_id'].present?
    end 

    false
  end


  def reject_collecting_event(attributed)
    reject = true
    CollectingEvent.data_attributes.each do |a|
      if !attributed[a].blank?
        reject = false
        break
      end
    end
    # !! does not account for georeferences_attributes!
    reject
  end

end

- (Date) deaccessioned_at

The date when the object was removed from tracking. If provide then Repository must be null?! TODO: resolve

Returns:

  • (Date)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'app/models/collection_object.rb', line 59

class CollectionObject < ActiveRecord::Base

  include GlobalID::Identification
  include Housekeeping
  include Shared::Citable
  include Shared::Containable
  include Shared::DataAttributes
  include Shared::Loanable
  include Shared::HasRoles
  include Shared::Identifiable
  include Shared::Notable
  include Shared::Taggable
  include Shared::Depictions
  include Shared::OriginRelationship
  include Shared::Confidence
  include Shared::IsData
  include SoftValidation

  include Shared::IsDwcOccurrence
  include CollectionObject::DwcExtensions 

  is_origin_for :collection_objects
  has_paper_trail :on => [:update] 

  CO_OTU_HEADERS      = %w{OTU OTU\ name Family Genus Species Country State County Locality Latitude Longitude}.freeze
  BUFFERED_ATTRIBUTES = %i{buffered_collecting_event buffered_determinations buffered_other_labels}.freeze

  # @return [Boolean]
  #  When true, cached values are not built
  attr_accessor :no_cached

  after_save :add_to_dwc_occurrence, if: '!self.no_cached'
  
  # Otu delegations
  delegate :name, to: :current_otu, prefix: :otu, allow_nil: true # could be Otu#otu_name?
  delegate :id, to: :current_otu, prefix: :otu, allow_nil: true

  # Identifier delegations
  delegate :cached, to: :preferred_catalog_number, prefix: :catalog_number, allow_nil: true

  # CollectingEvent delegations
  delegate :map_center, to: :collecting_event, prefix: :collecting_event, allow_nil: true

  # Repository delegations
  delegate :acronym, to: :repository, prefix: :repository, allow_nil: true
  delegate :url, to: :repository, prefix: :repository, allow_nil: true 

  # Preparation delegations
  delegate :name, to: :preparation_type, prefix: :preparation_type, allow_nil: true

  has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object
  has_one :accession_provider, through: :accession_provider_role, source: :person
  has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object
  has_one :deaccession_recipient, through: :deaccession_recipient_role, source: :person

  has_many :derived_collection_objects, inverse_of: :collection_object
  has_many :collection_object_observations, through: :derived_collection_objects, inverse_of: :collection_objects
  has_many :sqed_depictions, through: :depictions

  # This must come before taxon determinations !!
  has_many :otus, through: :taxon_determinations, inverse_of: :collection_objects

  has_many :taxon_names, through: :otus

  # This is a problem, but here for the forseeable future for nested attributes purporses.
  has_many :taxon_determinations, foreign_key: :biological_collection_object_id, inverse_of: :biological_collection_object

  has_many :type_designations, class_name: 'TypeMaterial', foreign_key: :biological_object_id, inverse_of: :material

  belongs_to :collecting_event, inverse_of: :collection_objects
  belongs_to :preparation_type, inverse_of: :collection_objects
  belongs_to :ranged_lot_category, inverse_of: :ranged_lots
  belongs_to :repository, inverse_of: :collection_objects

  has_many :georeferences, through: :collecting_event
  has_many :geographic_items, through: :georeferences

  accepts_nested_attributes_for :otus, allow_destroy: true
  accepts_nested_attributes_for :taxon_determinations, allow_destroy: true, reject_if: :reject_taxon_determinations
  accepts_nested_attributes_for :collecting_event, allow_destroy: true, reject_if: :reject_collecting_event

  validates_presence_of :type
  validate :check_that_either_total_or_ranged_lot_category_id_is_present
  validate :check_that_both_of_category_and_total_are_not_present

  before_validation :assign_type_if_total_or_ranged_lot_category_id_provided

  soft_validate(:sv_missing_accession_fields, set: :missing_accession_fields)
  soft_validate(:sv_missing_deaccession_fields, set: :missing_deaccession_fields)

  def preferred_catalog_number
    Identifier::Local::CatalogNumber.where(identifier_object: self).first 
  end

  def missing_determination
    # see BiologicalCollectionObject
  end

  def self.find_for_autocomplete(params)
    Queries::BiologicalCollectionObjectAutocompleteQuery.new(params[:term]).all.where(project_id: params[:project_id]).includes(taxon_determinations: [:determiners]).limit(50)
  end

  # TODO: move to a helper
  def self.breakdown_status(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)

    breakdown = {
      total_objects:     collection_objects.length,
      collecting_events: {},
      determinations:    {},
      bio_overview:      []
    }

    breakdown.merge!(breakdown_buffered(collection_objects))

    collection_objects.each do |co|
      breakdown[:collecting_events].merge!(co => co.collecting_event) if co.collecting_event
      breakdown[:determinations].merge!(co => co.taxon_determinations) if co.taxon_determinations.any?
      breakdown[:bio_overview].push([co.total, co.biocuration_classes.collect { |a| a.name }])
    end

    breakdown
  end

  # @return [Hash]
  #   a unque list of buffered_ values observed in the collection objects passed
  def self.breakdown_buffered(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)
    breakdown          = {}
    categories         = BUFFERED_ATTRIBUTES

    categories.each do |c|
      breakdown[c] = []
    end

    categories.each do |c|
      collection_objects.each do |co|
        breakdown[c].push co.send(c)
      end
    end

    categories.each do |c|
      breakdown[c].uniq!
    end

    breakdown
  end

  # return [Boolean]
  #    True if instance is a subclass of BiologicalCollectionObject
  def biological?
    self.class <= BiologicalCollectionObject ? true : false
  end

  def annotations
    h = annotations_hash
    (h['biocuration classifications'] = self.biocuration_classes) if self.biological? && self.biocuration_classifications.any?
    h
  end
  
  # @param [String] rank
  # @return [String] if a determination exists, and the Otu in the determination has a taxon name then return the taxon name at the rank supplied
  def name_at_rank_string(rank)
    current_taxon_name.try(:ancestor_at_rank, rank).try(:cached_html)
  end

  # @param [Scope] scope of selected CollectionObjects
  # @param [Hash] col_defs selected headers and types
  # @param [Hash] table_data (optional)
  # @return [CSV] tab-separated data
  # Generate the CSV (tab-separated) data for the file to be sent, substitute for new-lines and tabs
  def self.generate_report_download(scope, col_defs, table_data = nil)
    CSV.generate do |csv|
      row = CO_OTU_HEADERS
      unless col_defs.nil?
        %w(ce co bc).each { |column_type|
          items = []
          unless col_defs[column_type.to_sym].nil?
            unless col_defs[column_type.to_sym][:in].nil?
              items.push(col_defs[column_type.to_sym][:in].keys)
            end
            unless col_defs[column_type.to_sym][:im].nil?
              items.push(col_defs[column_type.to_sym][:im].keys)
            end
          end
          row += items.flatten
        }
      end
      csv << row
      if table_data.nil?
        scope.order(id: :asc).each do |c_o|
          row = [c_o.otu_id,
                 c_o.otu_name,
                 c_o.name_at_rank_string(:family),
                 c_o.name_at_rank_string(:genus),
                 c_o.name_at_rank_string(:species),
                 c_o.collecting_event.country_name,
                 c_o.collecting_event.state_name,
                 c_o.collecting_event.county_name,
                 c_o.collecting_event.verbatim_locality,
                 c_o.collecting_event.georeference_latitude.to_s,
                 c_o.collecting_event.georeference_longitude.to_s
          ]
          row += ce_attributes(c_o, col_defs)
          row += co_attributes(c_o, col_defs)
          row += bc_attributes(c_o, col_defs)
          csv << row.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }

        end
      else
        table_data.each { |_key, value|
          csv << value.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }
        }
      end
    end
  end


  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.earliest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:end_date_year)

    return '1700/01/01' if a.nil? && b.nil?

    d = nil

    if a && b
      if a < b
        d = a
      end
    else
      d = a || b
    end
    d.to_s + '/01/01'
  end

  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.latest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:end_date_year)

    c = Time.now.strftime("%Y/%m/%d")

    return c if a.nil? && b.nil?

    d = nil

    if a && b
      if a > b
        d = a
      end
    else
      d = a || b
    end

    d.to_s + '/12/31'
  end

  # TODO: Clarify this.
  # CAREFULL - this isn't _in_, this is *with*, if it was in it would be spatial query, not a join(:geographic_items)
  #
  # Find all collection objects which have collecting events which have georeferences which have geographic_items which
  # are located within the geographic item supplied
  # @param [GeographicItem] geographic_item_id
  # @return [Scope] of CollectionObject
  def self.in_geographic_item(geographic_item, limit, steps = false)
    geographic_item_id = geographic_item.id
    if steps
      gi     = GeographicItem.find(geographic_item_id)
      # find the geographic_items inside gi
      step_1 = GeographicItem.is_contained_by('any', gi) # .pluck(:id)
      # find the georeferences from the geographic_items
      step_2 = step_1.map(&:georeferences).uniq.flatten
      # find the collecting events connected to the georeferences
      step_3 = step_2.map(&:collecting_event).uniq.flatten
      # find the collection objects associated with the collecting events
      step_4 = step_3.map(&:collection_objects).flatten.map(&:id).uniq
      retval = CollectionObject.where(id: step_4.sort)
    else
      retval = CollectionObject.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)).limit(limit).includes(:data_attributes, :collecting_event)
    end
    retval
  end

  def self.selected_column_names
    @selected_column_names = {ce: {in: {}, im: {}},
                              co: {in: {}, im: {}},
                              bc: {in: {}, im: {}}
    } if @selected_column_names.nil?
    @selected_column_names
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collecting events
  # decode which headers to be displayed for collecting events
  def self.ce_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:ce][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:ce][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.ce_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.collecting_event.internal_attributes
      all_import_das   = collection_object.collecting_event.import_attributes
      group            = collection[:ce]
      unless group.nil?
        group.keys.each { |type_key|
          group[type_key.to_sym].keys.each { |header|
            this_val = nil
            case type_key.to_sym
              when :in
                all_internal_das.each { |da|
                  if da.predicate.name == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              when :im
                all_import_das.each { |da|
                  if da.import_predicate == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              else
            end
          }
        }
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collection objects
  # decode which headers to be displayed for collection objects
  def self.co_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:co][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:co][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.co_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.internal_attributes
      all_import_das   = collection_object.import_attributes
      group            = collection[:co]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = nil
              all_internal_das.each { |da|
                if da.predicate.name == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
        unless group.empty?
          unless group[:im].empty?
            group[:im].keys.each { |header|
              this_val = nil
              all_import_das.each { |da|
                if da.import_predicate == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for biocuration classifications
  # decode which headers to be displayed for biocuration classifications
  def self.bc_headers(project_id)
    CollectionObject.selected_column_names
    # add selectable column names (unselected) to the column name list list
    BiocurationClass.where(project_id: project_id).map(&:name).each { |column_name|
      @selected_column_names[:bc][:in][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.bc_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      group = collection[:bc]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = collection_object.biocuration_classes.map(&:name).include?(header) ? '1' : '0'
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Array] collecting_event_ids (e.g., from CollectingEvent.in_date_range)
  # @param [Array] area_object_ids (e.g., from GeographicItem.gather_selected_data())
  # @return [Scope] of intersection of collecting events (usually by date range)
  #   and collection objects (usually by inclusion in geographic areas/items)
  def self.from_collecting_events(collecting_event_ids, area_object_ids, area_set, project_id)
    collecting_events_clause = {collecting_event_id: collecting_event_ids, project: project_id}
    area_objects_clause = {id: area_object_ids, project: project_id}

    if (collecting_event_ids.empty?)
      collecting_events_clause = {project: project_id}
    end

    if (area_object_ids.empty?)
      area_objects_clause = {}
      if (area_set)
        area_objects_clause = 'false'
      end
    end
    
    retval = CollectionObject.joins(:collecting_event)
                 .where(collecting_events_clause)
                 .where(area_objects_clause)
    retval
  end

  # @param [Hash] search_start_date string in form 'yyyy/mm/dd'
  # @param [Hash] search_end_date string in form 'yyyy/mm/dd'
  # @param [Hash] partial_overlap 'on' or 'off'
  # @return [Scope] of selected collection objects through collecting events with georeferences, remember to scope to project!
  def self.in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on')
    allow_partial = (partial_overlap.downcase == 'off' ? false : true) # TODO: Just get the correct values from the form!
    where_sql = CollectingEvent.date_sql_from_dates(search_start_date, search_end_date, allow_partial)
    joins(:collecting_event).where(where_sql)
  end

  def sv_missing_accession_fields
    soft_validations.add(:accessioned_at, 'Date is not selected') if self.accessioned_at.nil? && !self.accession_provider.nil?
    soft_validations.add(:base, 'Provider is not selected') if !self.accessioned_at.nil? && self.accession_provider.nil?
  end

  def sv_missing_deaccession_fields
    soft_validations.add(:deaccessioned_at, 'Date is not selected') if self.deaccessioned_at.nil? && !self.deaccession_reason.blank?
    soft_validations.add(:base, 'Recipient is not selected') if self.deaccession_recipient.nil? && self.deaccession_reason && self.deaccessioned_at
    soft_validations.add(:deaccession_reason, 'Reason is is not defined') if self.deaccession_reason.blank? && self.deaccession_recipient && self.deaccessioned_at
  end

  def sv_missing_collecting_event
    # see biological_collection_object
  end

  def sv_missing_preparation_type
    # see biological_collection_object
  end

  def sv_missing_repository
    # see biological_collection_object
  end

  protected

  def add_to_dwc_occurrence
    get_dwc_occurrence
  end
  handle_asynchronously :add_to_dwc_occurrence, run_at: Proc.new { 20.seconds.from_now }

  def check_that_both_of_category_and_total_are_not_present
    errors.add(:ranged_lot_category_id, 'Both ranged_lot_category and total can not be set') if !ranged_lot_category_id.blank? && !total.blank?
  end

  def check_that_either_total_or_ranged_lot_category_id_is_present
    errors.add(:base, 'Either total or a ranged lot category must be provided') if ranged_lot_category_id.blank? && total.blank?
  end

  def assign_type_if_total_or_ranged_lot_category_id_provided
    if self.total == 1
      self.type = 'Specimen'
    elsif self.total.to_i > 1
      self.type = 'Lot'
    elsif total.nil? && !ranged_lot_category_id.blank?
      self.type = 'RangedLot'
    end
    true
  end

  def reject_taxon_determinations(attributed)

    a = attributed['otu_id']
    b = attributed['otu_attributes'] 

    return true if !a.present? && !b.present?

    if a.present?
      return true if b.present? && ( b['name'].present? || b['taxon_name_id'].present? ) # not both
      return false 
    end

    if b.present?
      return true if !b['name'].present? && !b['taxon_name_id'].present?
    end 

    false
  end


  def reject_collecting_event(attributed)
    reject = true
    CollectingEvent.data_attributes.each do |a|
      if !attributed[a].blank?
        reject = false
        break
      end
    end
    # !! does not account for georeferences_attributes!
    reject
  end

end

- (Boolean) no_cached

Returns When true, cached values are not built

Returns:

  • (Boolean)

    When true, cached values are not built



88
89
90
# File 'app/models/collection_object.rb', line 88

def no_cached
  @no_cached
end

- (Integer) preparation_type_id

How the collection object was prepared. Draws from a controlled set of values shared by all projects. For example “slide mounted”. See PreparationType.

Returns:

  • (Integer)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'app/models/collection_object.rb', line 59

class CollectionObject < ActiveRecord::Base

  include GlobalID::Identification
  include Housekeeping
  include Shared::Citable
  include Shared::Containable
  include Shared::DataAttributes
  include Shared::Loanable
  include Shared::HasRoles
  include Shared::Identifiable
  include Shared::Notable
  include Shared::Taggable
  include Shared::Depictions
  include Shared::OriginRelationship
  include Shared::Confidence
  include Shared::IsData
  include SoftValidation

  include Shared::IsDwcOccurrence
  include CollectionObject::DwcExtensions 

  is_origin_for :collection_objects
  has_paper_trail :on => [:update] 

  CO_OTU_HEADERS      = %w{OTU OTU\ name Family Genus Species Country State County Locality Latitude Longitude}.freeze
  BUFFERED_ATTRIBUTES = %i{buffered_collecting_event buffered_determinations buffered_other_labels}.freeze

  # @return [Boolean]
  #  When true, cached values are not built
  attr_accessor :no_cached

  after_save :add_to_dwc_occurrence, if: '!self.no_cached'
  
  # Otu delegations
  delegate :name, to: :current_otu, prefix: :otu, allow_nil: true # could be Otu#otu_name?
  delegate :id, to: :current_otu, prefix: :otu, allow_nil: true

  # Identifier delegations
  delegate :cached, to: :preferred_catalog_number, prefix: :catalog_number, allow_nil: true

  # CollectingEvent delegations
  delegate :map_center, to: :collecting_event, prefix: :collecting_event, allow_nil: true

  # Repository delegations
  delegate :acronym, to: :repository, prefix: :repository, allow_nil: true
  delegate :url, to: :repository, prefix: :repository, allow_nil: true 

  # Preparation delegations
  delegate :name, to: :preparation_type, prefix: :preparation_type, allow_nil: true

  has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object
  has_one :accession_provider, through: :accession_provider_role, source: :person
  has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object
  has_one :deaccession_recipient, through: :deaccession_recipient_role, source: :person

  has_many :derived_collection_objects, inverse_of: :collection_object
  has_many :collection_object_observations, through: :derived_collection_objects, inverse_of: :collection_objects
  has_many :sqed_depictions, through: :depictions

  # This must come before taxon determinations !!
  has_many :otus, through: :taxon_determinations, inverse_of: :collection_objects

  has_many :taxon_names, through: :otus

  # This is a problem, but here for the forseeable future for nested attributes purporses.
  has_many :taxon_determinations, foreign_key: :biological_collection_object_id, inverse_of: :biological_collection_object

  has_many :type_designations, class_name: 'TypeMaterial', foreign_key: :biological_object_id, inverse_of: :material

  belongs_to :collecting_event, inverse_of: :collection_objects
  belongs_to :preparation_type, inverse_of: :collection_objects
  belongs_to :ranged_lot_category, inverse_of: :ranged_lots
  belongs_to :repository, inverse_of: :collection_objects

  has_many :georeferences, through: :collecting_event
  has_many :geographic_items, through: :georeferences

  accepts_nested_attributes_for :otus, allow_destroy: true
  accepts_nested_attributes_for :taxon_determinations, allow_destroy: true, reject_if: :reject_taxon_determinations
  accepts_nested_attributes_for :collecting_event, allow_destroy: true, reject_if: :reject_collecting_event

  validates_presence_of :type
  validate :check_that_either_total_or_ranged_lot_category_id_is_present
  validate :check_that_both_of_category_and_total_are_not_present

  before_validation :assign_type_if_total_or_ranged_lot_category_id_provided

  soft_validate(:sv_missing_accession_fields, set: :missing_accession_fields)
  soft_validate(:sv_missing_deaccession_fields, set: :missing_deaccession_fields)

  def preferred_catalog_number
    Identifier::Local::CatalogNumber.where(identifier_object: self).first 
  end

  def missing_determination
    # see BiologicalCollectionObject
  end

  def self.find_for_autocomplete(params)
    Queries::BiologicalCollectionObjectAutocompleteQuery.new(params[:term]).all.where(project_id: params[:project_id]).includes(taxon_determinations: [:determiners]).limit(50)
  end

  # TODO: move to a helper
  def self.breakdown_status(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)

    breakdown = {
      total_objects:     collection_objects.length,
      collecting_events: {},
      determinations:    {},
      bio_overview:      []
    }

    breakdown.merge!(breakdown_buffered(collection_objects))

    collection_objects.each do |co|
      breakdown[:collecting_events].merge!(co => co.collecting_event) if co.collecting_event
      breakdown[:determinations].merge!(co => co.taxon_determinations) if co.taxon_determinations.any?
      breakdown[:bio_overview].push([co.total, co.biocuration_classes.collect { |a| a.name }])
    end

    breakdown
  end

  # @return [Hash]
  #   a unque list of buffered_ values observed in the collection objects passed
  def self.breakdown_buffered(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)
    breakdown          = {}
    categories         = BUFFERED_ATTRIBUTES

    categories.each do |c|
      breakdown[c] = []
    end

    categories.each do |c|
      collection_objects.each do |co|
        breakdown[c].push co.send(c)
      end
    end

    categories.each do |c|
      breakdown[c].uniq!
    end

    breakdown
  end

  # return [Boolean]
  #    True if instance is a subclass of BiologicalCollectionObject
  def biological?
    self.class <= BiologicalCollectionObject ? true : false
  end

  def annotations
    h = annotations_hash
    (h['biocuration classifications'] = self.biocuration_classes) if self.biological? && self.biocuration_classifications.any?
    h
  end
  
  # @param [String] rank
  # @return [String] if a determination exists, and the Otu in the determination has a taxon name then return the taxon name at the rank supplied
  def name_at_rank_string(rank)
    current_taxon_name.try(:ancestor_at_rank, rank).try(:cached_html)
  end

  # @param [Scope] scope of selected CollectionObjects
  # @param [Hash] col_defs selected headers and types
  # @param [Hash] table_data (optional)
  # @return [CSV] tab-separated data
  # Generate the CSV (tab-separated) data for the file to be sent, substitute for new-lines and tabs
  def self.generate_report_download(scope, col_defs, table_data = nil)
    CSV.generate do |csv|
      row = CO_OTU_HEADERS
      unless col_defs.nil?
        %w(ce co bc).each { |column_type|
          items = []
          unless col_defs[column_type.to_sym].nil?
            unless col_defs[column_type.to_sym][:in].nil?
              items.push(col_defs[column_type.to_sym][:in].keys)
            end
            unless col_defs[column_type.to_sym][:im].nil?
              items.push(col_defs[column_type.to_sym][:im].keys)
            end
          end
          row += items.flatten
        }
      end
      csv << row
      if table_data.nil?
        scope.order(id: :asc).each do |c_o|
          row = [c_o.otu_id,
                 c_o.otu_name,
                 c_o.name_at_rank_string(:family),
                 c_o.name_at_rank_string(:genus),
                 c_o.name_at_rank_string(:species),
                 c_o.collecting_event.country_name,
                 c_o.collecting_event.state_name,
                 c_o.collecting_event.county_name,
                 c_o.collecting_event.verbatim_locality,
                 c_o.collecting_event.georeference_latitude.to_s,
                 c_o.collecting_event.georeference_longitude.to_s
          ]
          row += ce_attributes(c_o, col_defs)
          row += co_attributes(c_o, col_defs)
          row += bc_attributes(c_o, col_defs)
          csv << row.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }

        end
      else
        table_data.each { |_key, value|
          csv << value.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }
        }
      end
    end
  end


  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.earliest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:end_date_year)

    return '1700/01/01' if a.nil? && b.nil?

    d = nil

    if a && b
      if a < b
        d = a
      end
    else
      d = a || b
    end
    d.to_s + '/01/01'
  end

  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.latest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:end_date_year)

    c = Time.now.strftime("%Y/%m/%d")

    return c if a.nil? && b.nil?

    d = nil

    if a && b
      if a > b
        d = a
      end
    else
      d = a || b
    end

    d.to_s + '/12/31'
  end

  # TODO: Clarify this.
  # CAREFULL - this isn't _in_, this is *with*, if it was in it would be spatial query, not a join(:geographic_items)
  #
  # Find all collection objects which have collecting events which have georeferences which have geographic_items which
  # are located within the geographic item supplied
  # @param [GeographicItem] geographic_item_id
  # @return [Scope] of CollectionObject
  def self.in_geographic_item(geographic_item, limit, steps = false)
    geographic_item_id = geographic_item.id
    if steps
      gi     = GeographicItem.find(geographic_item_id)
      # find the geographic_items inside gi
      step_1 = GeographicItem.is_contained_by('any', gi) # .pluck(:id)
      # find the georeferences from the geographic_items
      step_2 = step_1.map(&:georeferences).uniq.flatten
      # find the collecting events connected to the georeferences
      step_3 = step_2.map(&:collecting_event).uniq.flatten
      # find the collection objects associated with the collecting events
      step_4 = step_3.map(&:collection_objects).flatten.map(&:id).uniq
      retval = CollectionObject.where(id: step_4.sort)
    else
      retval = CollectionObject.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)).limit(limit).includes(:data_attributes, :collecting_event)
    end
    retval
  end

  def self.selected_column_names
    @selected_column_names = {ce: {in: {}, im: {}},
                              co: {in: {}, im: {}},
                              bc: {in: {}, im: {}}
    } if @selected_column_names.nil?
    @selected_column_names
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collecting events
  # decode which headers to be displayed for collecting events
  def self.ce_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:ce][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:ce][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.ce_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.collecting_event.internal_attributes
      all_import_das   = collection_object.collecting_event.import_attributes
      group            = collection[:ce]
      unless group.nil?
        group.keys.each { |type_key|
          group[type_key.to_sym].keys.each { |header|
            this_val = nil
            case type_key.to_sym
              when :in
                all_internal_das.each { |da|
                  if da.predicate.name == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              when :im
                all_import_das.each { |da|
                  if da.import_predicate == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              else
            end
          }
        }
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collection objects
  # decode which headers to be displayed for collection objects
  def self.co_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:co][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:co][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.co_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.internal_attributes
      all_import_das   = collection_object.import_attributes
      group            = collection[:co]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = nil
              all_internal_das.each { |da|
                if da.predicate.name == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
        unless group.empty?
          unless group[:im].empty?
            group[:im].keys.each { |header|
              this_val = nil
              all_import_das.each { |da|
                if da.import_predicate == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for biocuration classifications
  # decode which headers to be displayed for biocuration classifications
  def self.bc_headers(project_id)
    CollectionObject.selected_column_names
    # add selectable column names (unselected) to the column name list list
    BiocurationClass.where(project_id: project_id).map(&:name).each { |column_name|
      @selected_column_names[:bc][:in][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.bc_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      group = collection[:bc]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].keys.each { |header|
              this_val = collection_object.biocuration_classes.map(&:name).include?(header) ? '1' : '0'
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Array] collecting_event_ids (e.g., from CollectingEvent.in_date_range)
  # @param [Array] area_object_ids (e.g., from GeographicItem.gather_selected_data())
  # @return [Scope] of intersection of collecting events (usually by date range)
  #   and collection objects (usually by inclusion in geographic areas/items)
  def self.from_collecting_events(collecting_event_ids, area_object_ids, area_set, project_id)
    collecting_events_clause = {collecting_event_id: collecting_event_ids, project: project_id}
    area_objects_clause = {id: area_object_ids, project: project_id}

    if (collecting_event_ids.empty?)
      collecting_events_clause = {project: project_id}
    end

    if (area_object_ids.empty?)
      area_objects_clause = {}
      if (area_set)
        area_objects_clause = 'false'
      end
    end
    
    retval = CollectionObject.joins(:collecting_event)
                 .where(collecting_events_clause)
                 .where(area_objects_clause)
    retval
  end

  # @param [Hash] search_start_date string in form 'yyyy/mm/dd'
  # @param [Hash] search_end_date string in form 'yyyy/mm/dd'
  # @param [Hash] partial_overlap 'on' or 'off'
  # @return [Scope] of selected collection objects through collecting events with georeferences, remember to scope to project!
  def self.in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on')
    allow_partial = (partial_overlap.downcase == 'off' ? false : true) # TODO: Just get the correct values from the form!
    where_sql = CollectingEvent.date_sql_from_dates(search_start_date, search_end_date, allow_partial)
    joins(:collecting_event).where(where_sql)
  end

  def sv_missing_accession_fields
    soft_validations.add(:accessioned_at, 'Date is not selected') if self.accessioned_at.nil? && !self.accession_provider.nil?
    soft_validations.add(:base, 'Provider is not selected') if !self.accessioned_at.nil? && self.accession_provider.nil?
  end

  def sv_missing_deaccession_fields
    soft_validations.add(:deaccessioned_at, 'Date is not selected') if self.deaccessioned_at.nil? && !self.deaccession_reason.blank?
    soft_validations.add(:base, 'Recipient is not selected') if self.deaccession_recipient.nil? && self.deaccession_reason && self.deaccessioned_at
    soft_validations.add(:deaccession_reason, 'Reason is is not defined') if self.deaccession_reason.blank? && self.deaccession_recipient && self.deaccessioned_at
  end

  def sv_missing_collecting_event
    # see biological_collection_object
  end

  def sv_missing_preparation_type
    # see biological_collection_object
  end

  def sv_missing_repository
    # see biological_collection_object
  end

  protected

  def add_to_dwc_occurrence
    get_dwc_occurrence
  end
  handle_asynchronously :add_to_dwc_occurrence, run_at: Proc.new { 20.seconds.from_now }

  def check_that_both_of_category_and_total_are_not_present
    errors.add(:ranged_lot_category_id, 'Both ranged_lot_category and total can not be set') if !ranged_lot_category_id.blank? && !total.blank?
  end

  def check_that_either_total_or_ranged_lot_category_id_is_present
    errors.add(:base, 'Either total or a ranged lot category must be provided') if ranged_lot_category_id.blank? && total.blank?
  end

  def assign_type_if_total_or_ranged_lot_category_id_provided
    if self.total == 1
      self.type = 'Specimen'
    elsif self.total.to_i > 1
      self.type = 'Lot'
    elsif total.nil? && !ranged_lot_category_id.blank?
      self.type = 'RangedLot'
    end
    true
  end

  def reject_taxon_determinations(attributed)

    a = attributed['otu_id']
    b = attributed['otu_attributes'] 

    return true if !a.present? && !b.present?

    if a.present?
      return true if b.present? && ( b['name'].present? || b['taxon_name_id'].present? ) # not both
      return false 
    end

    if b.present?
      return true if !b['name'].present? && !b['taxon_name_id'].present?
    end 

    false
  end


  def reject_collecting_event(attributed)
    reject = true
    CollectingEvent.data_attributes.each do |a|
      if !attributed[a].blank?
        reject = false
        break
      end
    end
    # !! does not account for georeferences_attributes!
    reject
  end

end

- (Integer) project_id

the project ID

Returns:

  • (Integer)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'app/models/collection_object.rb', line 59

class CollectionObject < ActiveRecord::Base

  include GlobalID::Identification
  include Housekeeping
  include Shared::Citable
  include Shared::Containable
  include Shared::DataAttributes
  include Shared::Loanable
  include Shared::HasRoles
  include Shared::Identifiable
  include Shared::Notable
  include Shared::Taggable
  include Shared::Depictions
  include Shared::OriginRelationship
  include Shared::Confidence
  include Shared::IsData
  include SoftValidation

  include Shared::IsDwcOccurrence
  include CollectionObject::DwcExtensions 

  is_origin_for :collection_objects
  has_paper_trail :on => [:update] 

  CO_OTU_HEADERS      = %w{OTU OTU\ name Family Genus Species Country State County Locality Latitude Longitude}.freeze
  BUFFERED_ATTRIBUTES = %i{buffered_collecting_event buffered_determinations buffered_other_labels}.freeze

  # @return [Boolean]
  #  When true, cached values are not built
  attr_accessor :no_cached

  after_save :add_to_dwc_occurrence, if: '!self.no_cached'
  
  # Otu delegations
  delegate :name, to: :current_otu, prefix: :otu, allow_nil: true # could be Otu#otu_name?
  delegate :id, to: :current_otu, prefix: :otu, allow_nil: true

  # Identifier delegations
  delegate :cached, to: :preferred_catalog_number, prefix: :catalog_number, allow_nil: true

  # CollectingEvent delegations
  delegate :map_center, to: :collecting_event, prefix: :collecting_event, allow_nil: true

  # Repository delegations
  delegate :acronym, to: :repository, prefix: :repository, allow_nil: true
  delegate :url, to: :repository, prefix: :repository, allow_nil: true 

  # Preparation delegations
  delegate :name, to: :preparation_type, prefix: :preparation_type, allow_nil: true

  has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object
  has_one :accession_provider, through: :accession_provider_role, source: :person
  has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object
  has_one :deaccession_recipient, through: :deaccession_recipient_role, source: :person

  has_many :derived_collection_objects, inverse_of: :collection_object
  has_many :collection_object_observations, through: :derived_collection_objects, inverse_of: :collection_objects
  has_many :sqed_depictions, through: :depictions

  # This must come before taxon determinations !!
  has_many :otus, through: :taxon_determinations, inverse_of: :collection_objects

  has_many :taxon_names, through: :otus

  # This is a problem, but here for the forseeable future for nested attributes purporses.
  has_many :taxon_determinations, foreign_key: :biological_collection_object_id, inverse_of: :biological_collection_object

  has_many :type_designations, class_name: 'TypeMaterial', foreign_key: :biological_object_id, inverse_of: :material

  belongs_to :collecting_event, inverse_of: :collection_objects
  belongs_to :preparation_type, inverse_of: :collection_objects
  belongs_to :ranged_lot_category, inverse_of: :ranged_lots
  belongs_to :repository, inverse_of: :collection_objects

  has_many :georeferences, through: :collecting_event
  has_many :geographic_items, through: :georeferences

  accepts_nested_attributes_for :otus, allow_destroy: true
  accepts_nested_attributes_for :taxon_determinations, allow_destroy: true, reject_if: :reject_taxon_determinations
  accepts_nested_attributes_for :collecting_event, allow_destroy: true, reject_if: :reject_collecting_event

  validates_presence_of :type
  validate :check_that_either_total_or_ranged_lot_category_id_is_present
  validate :check_that_both_of_category_and_total_are_not_present

  before_validation :assign_type_if_total_or_ranged_lot_category_id_provided

  soft_validate(:sv_missing_accession_fields, set: :missing_accession_fields)
  soft_validate(:sv_missing_deaccession_fields, set: :missing_deaccession_fields)

  def preferred_catalog_number
    Identifier::Local::CatalogNumber.where(identifier_object: self).first 
  end

  def missing_determination
    # see BiologicalCollectionObject
  end

  def self.find_for_autocomplete(params)
    Queries::BiologicalCollectionObjectAutocompleteQuery.new(params[:term]).all.where(project_id: params[:project_id]).includes(taxon_determinations: [:determiners]).limit(50)
  end

  # TODO: move to a helper
  def self.breakdown_status(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)

    breakdown = {
      total_objects:     collection_objects.length,
      collecting_events: {},
      determinations:    {},
      bio_overview:      []
    }

    breakdown.merge!(breakdown_buffered(collection_objects))

    collection_objects.each do |co|
      breakdown[:collecting_events].merge!(co => co.collecting_event) if co.collecting_event
      breakdown[:determinations].merge!(co => co.taxon_determinations) if co.taxon_determinations.any?
      breakdown[:bio_overview].push([co.total, co.biocuration_classes.collect { |a| a.name }])
    end

    breakdown
  end

  # @return [Hash]
  #   a unque list of buffered_ values observed in the collection objects passed
  def self.breakdown_buffered(collection_objects)
    collection_objects = [collection_objects] if !(collection_objects.class == Array)
    breakdown          = {}
    categories         = BUFFERED_ATTRIBUTES

    categories.each do |c|
      breakdown[c] = []
    end

    categories.each do |c|
      collection_objects.each do |co|
        breakdown[c].push co.send(c)
      end
    end

    categories.each do |c|
      breakdown[c].uniq!
    end

    breakdown
  end

  # return [Boolean]
  #    True if instance is a subclass of BiologicalCollectionObject
  def biological?
    self.class <= BiologicalCollectionObject ? true : false
  end

  def annotations
    h = annotations_hash
    (h['biocuration classifications'] = self.biocuration_classes) if self.biological? && self.biocuration_classifications.any?
    h
  end
  
  # @param [String] rank
  # @return [String] if a determination exists, and the Otu in the determination has a taxon name then return the taxon name at the rank supplied
  def name_at_rank_string(rank)
    current_taxon_name.try(:ancestor_at_rank, rank).try(:cached_html)
  end

  # @param [Scope] scope of selected CollectionObjects
  # @param [Hash] col_defs selected headers and types
  # @param [Hash] table_data (optional)
  # @return [CSV] tab-separated data
  # Generate the CSV (tab-separated) data for the file to be sent, substitute for new-lines and tabs
  def self.generate_report_download(scope, col_defs, table_data = nil)
    CSV.generate do |csv|
      row = CO_OTU_HEADERS
      unless col_defs.nil?
        %w(ce co bc).each { |column_type|
          items = []
          unless col_defs[column_type.to_sym].nil?
            unless col_defs[column_type.to_sym][:in].nil?
              items.push(col_defs[column_type.to_sym][:in].keys)
            end
            unless col_defs[column_type.to_sym][:im].nil?
              items.push(col_defs[column_type.to_sym][:im].keys)
            end
          end
          row += items.flatten
        }
      end
      csv << row
      if table_data.nil?
        scope.order(id: :asc).each do |c_o|
          row = [c_o.otu_id,
                 c_o.otu_name,
                 c_o.name_at_rank_string(:family),
                 c_o.name_at_rank_string(:genus),
                 c_o.name_at_rank_string(:species),
                 c_o.collecting_event.country_name,
                 c_o.collecting_event.state_name,
                 c_o.collecting_event.county_name,
                 c_o.collecting_event.verbatim_locality,
                 c_o.collecting_event.georeference_latitude.to_s,
                 c_o.collecting_event.georeference_longitude.to_s
          ]
          row += ce_attributes(c_o, col_defs)
          row += co_attributes(c_o, col_defs)
          row += bc_attributes(c_o, col_defs)
          csv << row.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }

        end
      else
        table_data.each { |_key, value|
          csv << value.collect { |item|
            item.to_s.gsub(/\n/, '\n').gsub(/\t/, '\t')
          }
        }
      end
    end
  end


  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.earliest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).minimum(:end_date_year)

    return '1700/01/01' if a.nil? && b.nil?

    d = nil

    if a && b
      if a < b
        d = a
      end
    else
      d = a || b
    end
    d.to_s + '/01/01'
  end

  # TODO: this should be refactored to be collection object centric AFTER 
  # it is spec'd
  def self.latest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id: project_id).maximum(:end_date_year)

    c = Time.now.strftime("%Y/%m/%d")

    return c if a.nil? && b.nil?

    d = nil

    if a && b
      if a > b
        d = a
      end
    else
      d = a || b
    end

    d.to_s + '/12/31'
  end

  # TODO: Clarify this.
  # CAREFULL - this isn't _in_, this is *with*, if it was in it would be spatial query, not a join(:geographic_items)
  #
  # Find all collection objects which have collecting events which have georeferences which have geographic_items which
  # are located within the geographic item supplied
  # @param [GeographicItem] geographic_item_id
  # @return [Scope] of CollectionObject
  def self.in_geographic_item(geographic_item, limit, steps = false)
    geographic_item_id = geographic_item.id
    if steps
      gi     = GeographicItem.find(geographic_item_id)
      # find the geographic_items inside gi
      step_1 = GeographicItem.is_contained_by('any', gi) # .pluck(:id)
      # find the georeferences from the geographic_items
      step_2 = step_1.map(&:georeferences).uniq.flatten
      # find the collecting events connected to the georeferences
      step_3 = step_2.map(&:collecting_event).uniq.flatten
      # find the collection objects associated with the collecting events
      step_4 = step_3.map(&:collection_objects).flatten.map(&:id).uniq
      retval = CollectionObject.where(id: step_4.sort)
    else
      retval = CollectionObject.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)).limit(limit).includes(:data_attributes, :collecting_event)
    end
    retval
  end

  def self.selected_column_names
    @selected_column_names = {ce: {in: {}, im: {}},
                              co: {in: {}, im: {}},
                              bc: {in: {}, im: {}}
    } if @selected_column_names.nil?
    @selected_column_names
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collecting events
  # decode which headers to be displayed for collecting events
  def self.ce_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:ce][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id: project_id, attribute_subject_type: 'CollectingEvent')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
      @selected_column_names[:ce][:im][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.ce_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.collecting_event.internal_attributes
      all_import_das   = collection_object.collecting_event.import_attributes
      group            = collection[:ce]
      unless group.nil?
        group.keys.each { |type_key|
          group[type_key.to_sym].keys.each { |header|
            this_val = nil
            case type_key.to_sym
              when :in
                all_internal_das.each { |da|
                  if da.predicate.name == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              when :im
                all_import_das.each { |da|
                  if da.import_predicate == header
                    this_val = da.value
                    break
                  end
                }
                retval.push(this_val) # push one value (nil or not) for each selected header
              else
            end
          }
        }
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collection objects
  # decode which headers to be displayed for collection objects
  def self.co_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id: project_id, attribute_subject_type: 'CollectionObject')
                 .distinct
                 .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:co][:in][column_name] = {checked: '0'}