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::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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#issnString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#journalString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#keyString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#languageString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#language_idInteger

Returns The TaxonWorks normalization of language to Language.

Returns:

  • (Integer)

    The TaxonWorks normalization of language to Language.



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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#monthString

Returns see en.wikipedia.org/wiki/BibTeX#Field_types

stored as a three letter value, see ::VALID_BIBTEX_MONTHS.

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#no_year_suffix_validationBoolean?

Returns When true, cached values are not built.

Returns:

  • (Boolean, nil)

    When true, cached values are not built



211
212
213
# File 'app/models/source.rb', line 211

def no_year_suffix_validation
  @no_year_suffix_validation
end

#noteString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#numberString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#organizationString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#pagesString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#publisherString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#schoolString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#serial_idInteger

Returns The TaxonWorks Serial.

Returns:

  • (Integer)

    The TaxonWorks Serial



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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#seriesString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#stated_yearString

Returns See source/bibtex.rb TODO: Why is this character but year is int?.

Returns:

  • (String)

    See source/bibtex.rb TODO: Why is this character but year is int?



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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#titleString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#translatorString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#typeString

Returns An exception to the 1:1 modelling. We retain for Rails STI usage. Either Source::Verbatim or Source::Bibtex. The former can only consist of a single field (the full citation as a string). The latter is a Bibtex model. See “bibtex_type” for the bibtex attribute “type”.

Returns:

  • (String)

    An exception to the 1:1 modelling. We retain for Rails STI usage. Either Source::Verbatim or Source::Bibtex. The former can only consist of a single field (the full citation as a string). The latter is a Bibtex model. See “bibtex_type” for the bibtex attribute “type”.



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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#urlString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#verbatimString

Returns the full citation, used only for type = SourceVerbatim.

Returns:

  • (String)

    the full citation, used only for type = SourceVerbatim



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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#verbatim_contentsString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#verbatim_keywordsString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#volumeString

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#yearInteger

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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

#year_suffixString

Returns Arbitrary user-provided suffix to the year. Use is highly discouraged.

Returns:

  • (String)

    Arbitrary user-provided suffix to the year. Use is highly discouraged.



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
# 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
  # !! Must not have Shared::Depictions

  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

  has_many :project_sources, inverse_of: :source, dependent: :destroy
  has_many :projects, inverse_of: :sources, 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

Class Method Details

.batch_create(file) ⇒ Array, Boolean

Returns:

  • (Array, Boolean)


278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'app/models/source.rb', line 278

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

.batch_preview(file) ⇒ [Array, message]

Returns TODO: return a more informative response?.

Parameters:

  • file (String)

Returns:

  • ([Array, message])

    TODO: return a more informative response?



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'app/models/source.rb', line 253

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

.select_optimized(user_id, project_id, target = 'TaxonName') ⇒ Hash

Returns sources optimized for user selection.

Returns:

  • (Hash)

    sources optimized for user selection



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

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

.used_recently(user_id, project_id, used_on = 'TaxonName') ⇒ Scope

Returns the max 10 most recently used (1 week, could parameterize) TaxonName, as used.

Parameters:

  • used_on (String) (defaults to: 'TaxonName')

    a model name

Returns:

  • (Scope)

    the max 10 most recently used (1 week, could parameterize) TaxonName, as used



306
307
308
309
310
311
312
313
314
315
# File 'app/models/source.rb', line 306

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

Instance Method Details

#author_yearString

Returns A string that represents the authors last_names and year (no suffix).

Returns:

  • (String)

    A string that represents the authors last_names and year (no suffix)



271
272
273
274
# File 'app/models/source.rb', line 271

def author_year
  return 'not yet calculated' if new_record?
  [cached_author_string, year].compact.join(', ')
end

#cited_objectsArray

Returns objects this source is linked to through citations.

Returns:

  • (Array)

    objects this source is linked to through citations



347
348
349
# File 'app/models/source.rb', line 347

def cited_objects
  self.citations.collect { |t| t.citation_object }
end

#cloneSource

Returns:



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

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

#get_cachedObject (protected)

Defined in subclasses



397
398
# File 'app/models/source.rb', line 397

def get_cached
end

#is_bibtex?Boolean

Returns:

  • (Boolean)


352
353
354
# File 'app/models/source.rb', line 352

def is_bibtex?
  type == 'Source::Bibtex'
end

#is_in_project?(project_id) ⇒ Boolean

Returns:

  • (Boolean)


357
358
359
# File 'app/models/source.rb', line 357

def is_in_project?(project_id)
  projects.where(id: project_id).any?
end

#nomenclature_dateDate

Month handling allows values from bibtex like ‘may’ to be handled

Returns:

  • (Date)


363
364
365
# File 'app/models/source.rb', line 363

def nomenclature_date
  Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), year)&.to_date
end

#reject_project_sources(attributed) ⇒ Boolean (protected)

Returns:

  • (Boolean)


402
403
404
405
# File 'app/models/source.rb', line 402

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

#set_cachedNil (protected)

Defined in subclasses

Returns:

  • (Nil)


393
394
# File 'app/models/source.rb', line 393

def set_cached
end

#sv_cached_namesObject (protected)



407
408
409
# File 'app/models/source.rb', line 407

def sv_cached_names
  true # see validation in subclasses
end

#sv_fix_cached_namesObject (protected)



411
412
413
414
415
416
417
418
419
420
# File 'app/models/source.rb', line 411

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

#sv_fix_stated_yearObject (protected)



429
430
431
432
433
434
435
436
437
438
439
# File 'app/models/source.rb', line 429

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

#sv_html_tagsObject (protected)



442
443
444
445
446
447
# File 'app/models/source.rb', line 442

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

#sv_stated_yearObject (protected)



422
423
424
425
426
427
# File 'app/models/source.rb', line 422

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