Class: Source

Overview

A Source is the metadata that identifies the origin of some information/data.

The primary purpose of Source metadata is to allow the user to find the source, that’s all.

See en.wikipedia.org/wiki/BibTeX for a definition of attributes, in nearly all cases they are 1:1 with the TW model. We use this github.com/inukshuk/bibtex-ruby awesomeness. See github.com/inukshuk/bibtex-ruby/tree/master/lib/bibtex/entry, in particular rdf_converter.rb for the types of field managed.

Direct Known Subclasses

Bibtex, Human, Verbatim

Defined Under Namespace

Classes: Bibtex, Human, Verbatim

Constant Summary collapse

ALTERNATE_VALUES_FOR =
[
:address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
:publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

Constants included from SoftValidation

SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS

Instance Attribute Summary collapse

Attributes included from Housekeeping::Users

#by

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Shared::IsData

#errors_excepting, #full_error_messages_excepting, #identical, #is_community?, #is_destroyable?, #is_editable?, #is_in_use?, #is_in_users_projects?, #metamorphosize, #similar

Methods included from SoftValidation

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

Methods included from Shared::HasPapertrail

#attribute_updated, #attribute_updater

Methods included from Shared::Tags

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

Methods included from Shared::Notes

#concatenated_notes_string, #reject_notes

Methods included from Shared::Identifiers

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

Methods included from Shared::Documentation

#document_array=, #documented?, #reject_documentation, #reject_documents

Methods included from Shared::DataAttributes

#import_attributes, #internal_attributes, #keyword_value_hash, #reject_data_attributes

Methods included from Shared::AlternateValues

#all_values_for, #alternate_valued?

Methods included from Housekeeping::Timestamps

#data_breakdown_for_chartkick_recent

Methods included from Housekeeping::Users

#set_created_by_id, #set_updated_by_id

Methods inherited from ApplicationRecord

transaction_with_retry

Instance Attribute Details

#abstractString

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#addressString

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#annoteString

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#authorString

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#bibtex_typeString

Returns alias for “type” in the bibtex framework see en.wikipedia.org/wiki/BibTeX#Field_types.

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#booktitleString

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#cachedString

Returns calculated full citation, searched again in “full text”.

Returns:

  • (String)

    calculated full citation, searched again in “full text”



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#cached_author_stringString

Returns calculated author string.

Returns:

  • (String)

    calculated author string



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#cached_nomenclature_dateDateTime

Returns Date sensu nomenclature algorithm in TaxonWorks (see Utilities::Dates).

Returns:

  • (DateTime)

    Date sensu nomenclature algorithm in TaxonWorks (see Utilities::Dates)



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#chapterString

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#crossrefString

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#dayInteger

Returns the calendar day (1-31).

Returns:

  • (Integer)

    the calendar day (1-31)



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#doiString

Returns When provided also cloned to an Identifier::Global. See en.wikipedia.org/wiki/BibTeX#Field_types.

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#editionString

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#editorString

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#howpublishedString

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#institutionString

Returns:



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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]
  def nomenclature_date
    Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
  end

  # @return [Source]
  def clone
    s = dup

    m = "[CLONE of #{id}] "

    case type
    when 'Source::Verbatim'
      s.verbatim = m + verbatim.to_s
    when 'Source::Bibtex'
      s.title = m + title.to_s
    end

    roles.reload.each do |r|
      s.roles << Role.new(person: r.person, type: r.type, position: r.position )
    end

    s.year_suffix = nil
    s.save
    s
  end

  protected

  # Defined in subclasses
  # @return [Nil]
  def set_cached
  end

  # Defined in subclasses
  def get_cached
  end

    # @param [Hash] attributed
  # @return [Boolean]
  def reject_project_sources(attributed)
    return true if attributed['project_id'].blank?
    return true if ProjectSource.where(project_id: attributed['project_id'], source_id: id).any?
  end

  def sv_cached_names
    true # see validation in subclasses
  end

  def sv_fix_cached_names
    begin
      Source.transaction do
        self.set_cached
      end
      true
    rescue
      false
    end
  end

  def sv_stated_year
    soft_validations.add(
      :base, "'Stated year' is not needed if identical to 'year'; applying the Fix will delete it",
      success_message: "'Stated year' was deleted",
      failure_message:  "Failed to delete 'Stated year'") if year.to_s == stated_year.to_s
  end

  def sv_fix_stated_year
    begin
      Source.transaction do
        self.stated_year = nil
        self.save
      end
      true
    rescue
      false
    end
  end


  def sv_html_tags
    if title.present?
      str = title.squish.gsub(/\<i>[^<>]*?<\/i>/, '')
      soft_validations.add(:title, 'The title contains unmatched html tags') if str.include?('<i>') || str.include?('</i>')
    end
end
end

#isbnString

TODO:

Returns:

  • (String)


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

class Source < ApplicationRecord
  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Documentation
  include Shared::Identifiers
  include Shared::Notes
  include Shared::SharedAcrossProjects
  include Shared::Tags
  include Shared::HasPapertrail
  include SoftValidation
  include Shared::IsData

  ignore_whitespace_on(:verbatim_contents)

  ALTERNATE_VALUES_FOR = [
    :address, :annote, :booktitle, :edition, :editor, :institution, :journal, :note, :organization,
    :publisher, :school, :title, :doi, :abstract, :language, :translator, :author, :url].freeze

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

  # Keep this order for citations/topics
  has_many :citations, inverse_of: :source, dependent: :restrict_with_error
  has_many :origin_citations, -> {where(citations: {is_original: true})}, class_name: 'Citation', dependent: :restrict_with_error, inverse_of: :source
  has_many :citation_topics, through: :citations, inverse_of: :source
  has_many :topics, through: :citation_topics, inverse_of: :sources

  # !! must be below has_many :citations
  # has_many :asserted_distributions, through: :citations, source: :citation_object, source_type: 'AssertedDistribution'

  has_many :project_sources, dependent: :destroy
  has_many :projects, through: :project_sources

  after_save :set_cached

  validates_presence_of :type
  validates :type, inclusion: {in: ['Source::Bibtex', 'Source::Human', 'Source::Verbatim']} # TODO: not needed

  accepts_nested_attributes_for :project_sources, reject_if: :reject_project_sources

  soft_validate(
    :sv_cached_names,
    set: :cached_names,
    fix: :sv_fix_cached_names,
    name: 'Cached names',
    description: 'Check if cached values need to be updated' )

  soft_validate(
    :sv_stated_year,
    set: :stated_year,
    fix: :sv_fix_stated_year,
    name: 'Stated year',
    description: "'Stated year' is not needed if identical to 'year'" )

  soft_validate(
    :sv_html_tags,
    set: :html_tags,
    name: 'html tags',
    description: 'Check if html has both open and close tags' )

    # Redirect type here
  # @param [String] file
  # @return [[Array, message]]
  #   TODO: return a more informative response?
  def self.batch_preview(file)
    begin
      bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
      sources = []
      bibliography.each do |record|
        a = Source::Bibtex.new_from_bibtex(record)
        sources.push(a)
      end
      return sources, nil
    rescue BibTeX::ParseError => e
      return [], e.message
    rescue
      raise
    end
  end

  # @return [String]
  #   A string that represents the authors last_names and year (no suffix)
  def author_year
    return 'not yet calculated' if new_record?
    [cached_author_string, year].compact.join(', ')
  end

    # @param [String] file
  # @return [Array, Boolean]
  def self.batch_create(file)
    sources = []
    valid = 0
    begin
      # error_msg = []
      Source.transaction do
        bibliography = BibTeX::Bibliography.parse(file.read.force_encoding('UTF-8'), filter: :latex)
        bibliography.each do |record|
          a = Source::Bibtex.new_from_bibtex(record)
          if a.valid?
            if a.save
              valid += 1
            end
          else
            # error_msg = a.errors.messages.to_s
          end
          sources.push(a)
        end
      end
    rescue
      return false
    end
    return {records: sources, count: valid}
  end

  # @param used_on [String] a model name
  # @return [Scope]
  #    the max 10 most recently used (1 week, could parameterize) TaxonName, as used
  def self.used_recently(user_id, project_id, used_on = 'TaxonName')
    Source.select('sources.id').
      joins(:citations)
          .where(citations: {updated_by_id: user_id,
                 project_id:,
                 citation_object_type: used_on,
                 updated_at: 1.week.ago..})
         .order('citations.updated_at DESC')
       .pluck(:id).uniq
  end

  # @params target [String] a citable model name
  # @return [Hash] sources optimized for user selection
  def self.select_optimized(user_id, project_id, target = 'TaxonName')
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: Source.pinned_by(user_id).where(pinboard_items: {project_id:}).to_a,
      recent: []
    }

    if r.empty?
      h[:recent] = Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a
      h[:quick] = Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a
    else
      h[:recent] =
        (Source.where(created_by_id: user_id, updated_at: 2.hours.ago..Time.now )
        .order('created_at DESC')
        .limit(5).order(:cached).to_a +
      Source.where('"sources"."id" IN (?)', r.first(6) ).to_a).uniq
      h[:quick] = ( Source.pinned_by(user_id).pinboard_inserted.where(pinboard_items: {project_id:}).to_a +
                   Source.where('"sources"."id" IN (?)', r.first(4) ).to_a).uniq
    end

    h
  end

  # @return [Array]
  #    objects this source is linked to through citations
  def cited_objects
    self.citations.collect { |t| t.citation_object }
  end

  # @return [Boolean]
  def is_bibtex?
    type == 'Source::Bibtex'
  end

  # @return [Boolean]
  def is_in_project?(project_id)
    projects.where(id: project_id).any?
  end

  # Month handling allows values from bibtex like 'may' to be handled
  # @return [Date]