Class: Source::Bibtex

Inherits:
Source
  • Object
show all
Includes:
SoftValidation
Defined in:
app/models/source/bibtex.rb

Overview

Bibtex - Subclass of Source that represents most references.

TaxonWorks(TW) relies on the bibtex-ruby gem to input or output BibTeX bibliographies, and has a strict list of required fields. TW itself only requires that :bibtex_type be valid and that one of the attributes in TW_REQ_FIELDS be defined. This allows a rapid input of incomplete data, but also means that not all TW Source::Bibtex objects can be added to a BibTeX bibliography.

The following information is taken from BibTeXing, by Oren Patashnik, February 8, 1988 ftp.math.purdue.edu/mirrors/ctan.org/biblio/bibtex/contrib/doc/btxdoc.pdf (and snippets are cut from this document for the attribute descriptions)

BibTeX fields in a BibTex bibliography are treated in one of three ways:

REQUIRED

Omitting the field will produce a warning message and, rarely, a badly formatted bibliography entry.

If the required information is not

meaningful, you are using the wrong entry type. However, if the required
information is meaningful but, say, already included is some other field,
simply ignore the warning.
OPTIONAL

The field's information will be used if present, but can be omitted

without causing any formatting problems. You should include the optional
     field if it will help the reader.
IGNORED

The field is ignored. BibTEX ignores any field that is not required or

optional, so you can include any fields you want in a bib file entry. It's a

good idea to put all relevant information about a reference in its bib file
entry - even information that may never appear in the bibliography.

TW will add all non-standard or housekeeping attributes to the bibliography even though the data may be ignored.

Author:

  • Elizabeth Frank <eef@illinois.edu> INHS University of IL

Constant Summary collapse

TW_REQ_FIELDS =

region constants TW required fields (must have one of these fields filled in)

[
    :author,
    :editor,
    :booktitle,
    :title,
    :url,
    :journal,
    :year,
    :stated_year
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SoftValidation

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

Methods included from Housekeeping::Users

#alive?, #set_created_by_id, #set_updated_by_id

Instance Attribute Details

#abstractObject

include Shared::Notable in source.rb



# File 'app/models/source/bibtex.rb', line 207

#address#String?

BibTeX standard field (optional for types: book, inbook, incollection, inproceedings, manual, mastersthesis, phdthesis, proceedings, techreport) Usually the address of the publisher or other type of institution. For major publishing houses, van Leunen recommends omitting the information entirely. For small publishers, on the other hand, you can help the reader by giving the complete address.

Returns:

  • (#String)

    the address

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).first.identifier
  end

  def issn=(value)
    write_attribute(:issn, value)
    #TODO if there is already an 'Identifier::Global::Issn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
  end
  def issn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:issn).first.identifier
  end

#TODO if language is set => set language_id
#endregion getters & setters

  # turn bibtex URL field into a Ruby URI object
  def url_as_uri 
    URI(self.url) unless self.url.blank?
  end

#region has_<attribute>? section
  def has_authors? # is there a bibtex author or author roles?
    return true if !(self.author.blank?)
    # author attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.authors.count > 0) ? (return true) : (return false)
  end

  def has_editors?
    return true if !(self.editor.blank?)
    # editor attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.editors.count > 0) ? (return true) : (return false)
  end

  def has_writer? # contains either an author or editor
    (has_authors?) || (has_editors?) ? true : false
  end

  def has_some_year? # is there a year or stated year?
    return true if !(self.year.blank?) || !(self.stated_year.blank?)
    false
  end

#endregion has_<attribute>? section

  #region time/date related
  # @return[Time] alias for nomenclature_date, computed if not yet saved
  def date
    set_nomenclature_date if !self.persisted?
    self.nomenclature_date
  end

  def set_nomenclature_date
    self.nomenclature_date = Utilities::Dates.nomenclature_date(self.day, self.month, self.year) 
  end
  #todo move the test for nomenclature_date to spec/lib/utilities/dates_spec.rb

#endregion    time/date related

  protected

   def set_cached_values

    bx_entry = self.to_bibtex
    if bx_entry.key.blank? then
      bx_entry.key = 'tmpID'
    end
    key = bx_entry.key
    #bx_entry.key = 'tmpID'
    bx_bibliography = BibTeX::Bibliography.new()
    bx_bibliography.add(bx_entry)

    cp = CiteProc::Processor.new(style: 'apa', format: 'text')
     cp.import bx_bibliography.to_citeproc

=begin
    cp << bx_bibliography.to_citeproc
    cp.process(bx_bibliography[self.id].to_citeproc)

    self.cached = CiteProc.process bx_entry.to_citeproc, style: 'apa', format: 'text'
=end

     # format cached = full reference
    self.cached = cp.render(:bibliography, id: key)[0]
    # self.cached = cp.render(:bibliography, id: 'tmpID')[0]
    # format cached_author_srting = either the bibtex author or the last names of all the normalized authors.
     self.cached_author_string = authority_name
  end


#region hard validations

# replaced with inclusion validation
#   def check_bibtex_type # must have a valid bibtex_type
#     errors.add(:bibtex_type, 'not a valid bibtex type') if !::VALID_BIBTEX_TYPES.include?(self.bibtex_type)
#   end

  def check_has_field # must have at least one of the required fields (TW_REQ_FIELDS)
    valid = false
    TW_REQ_FIELDS.each do |i| # for each i in the required fields list
      if !self[i].blank?
        valid = true
        break
      end
    end
    # if i is not nil and not == "", it's validly_published
    #if (!self[i].nil?) && (self[i] != '')
    #  return true
    #end
    errors.add(:base, 'no core data provided') if !valid
                      # return false # none of the required fields have a value
  end

#endregion  hard validations

#region Soft_validation_methods
  def sv_has_authors
    if !(has_authors?)
      soft_validations.add(:author, 'There is no author associated with this source.')
    end
  end

  def sv_contains_a_writer # neither author nor editor
    if !has_writer?
      soft_validations.add(:author, 'There is neither an author,nor editor associated with this source.')
      soft_validations.add(:editor, 'There is neither an author,nor editor associated with this source.')
    end

  end

  def sv_has_title
    if (self.title.blank?)
      soft_validations.add(:title, 'There is no title associated with this source.')
    end
  end

  def sv_has_some_type_of_year
    if (!has_some_year?)
      soft_validations.add(:year, 'There is no year or stated year associated with this source.')
      soft_validations.add(:stated_year, 'There is no or stated year year associated with this source.')
    end
  end

  def sv_year_exists
    if (year.blank?)
      soft_validations.add(:year, 'There is no year associated with this source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  def sv_missing_journal
    soft_validations.add(:bibtex_type, 'The source is missing a journal name.') if self.journal.blank?
  end

  def sv_is_article_missing_journal
    if (self.bibtex_type == 'article')
      if (self.journal.blank?)
        soft_validations.add(:bibtex_type, 'The article is missing a journal name.')
      end
    end
  end

  def sv_has_a_publisher
    if (self.publisher.blank?)
      soft_validations.add(:publisher, 'There is no publisher associated with this source.')
    end
  end

  def sv_has_booktitle
    if (self.booktitle.blank?)
      soft_validations.add(:booktitle, 'There is no book title associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:chapter, 'There is neither a chapter nor pages with this source.')
      soft_validations.add(:pages, 'There is neither a chapter nor pages with this source.')
    end
  end

  def sv_has_school
    if (self.school.blank?)
      soft_validations.add(:school, 'There is no school associated with this thesis.')
    end
  end

  def sv_has_institution
    if (self.institution.blank?)
      soft_validations.add(:institution, 'There is not institution associated with this tech report.')
    end
  end

  def sv_has_note
     # TODO we may need to check of a note in the TW sense as well - has_note? above.
     if (self.note.blank?) && (self.notes.count = 0)
       soft_validations.add(:note, 'There is no note associated with this source.')
     end
  end

  def sv_missing_required_bibtex_fields
    case self.bibtex_type
      when 'article' #:article       => [:author,:title,:journal,:year]
        sv_has_authors
        sv_has_title
        sv_is_article_missing_journal
        sv_year_exists
      when 'book' #:book          => [[:author,:editor],:title,:publisher,:year]
        sv_contains_a_writer
        sv_has_title
        sv_has_a_publisher
        sv_year_exists
      when 'booklet' #    :booklet       => [:title],
        sv_has_title
      when 'conference' #    :conference    => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'inbook' #    :inbook        => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year],
        sv_contains_a_writer
        sv_has_title
        sv_is_contained_has_chapter_or_pages
        sv_has_a_publisher
        sv_year_exists
      when 'incollection' #    :incollection  => [:author,:title,:booktitle,:publisher,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_has_a_publisher
        sv_year_exists
      when 'inproceedings' #    :inproceedings => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'manual' #    :manual        => [:title],
        sv_has_title
      when 'mastersthesis' #    :mastersthesis => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      #    :misc          => [],  (no required fields)
      when 'phdthesis' #    :phdthesis     => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      when 'proceedings' #    :proceedings   => [:title,:year],
        sv_has_title
        sv_year_exists
      when 'techreport' #    :techreport    => [:author,:title,:institution,:year],
        sv_has_authors
        sv_has_title
        sv_has_institution
        sv_year_exists
      when 'unpublished' #    :unpublished   => [:author,:title,:note]
        sv_has_authors
        sv_has_title
        sv_has_note
    end
  end

#endregion   Soft_validation_methods

end

#annoteString?

BibTeX standard field (ignored by standard processors) An annotation. It is not used by the standard bibliography styles, but may be used by others that produce an annotated bibliography.

Returns:

  • (String)

    the annotation

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).first.identifier
  end

  def issn=(value)
    write_attribute(:issn, value)
    #TODO if there is already an 'Identifier::Global::Issn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
  end
  def issn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:issn).first.identifier
  end

#TODO if language is set => set language_id
#endregion getters & setters

  # turn bibtex URL field into a Ruby URI object
  def url_as_uri 
    URI(self.url) unless self.url.blank?
  end

#region has_<attribute>? section
  def has_authors? # is there a bibtex author or author roles?
    return true if !(self.author.blank?)
    # author attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.authors.count > 0) ? (return true) : (return false)
  end

  def has_editors?
    return true if !(self.editor.blank?)
    # editor attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.editors.count > 0) ? (return true) : (return false)
  end

  def has_writer? # contains either an author or editor
    (has_authors?) || (has_editors?) ? true : false
  end

  def has_some_year? # is there a year or stated year?
    return true if !(self.year.blank?) || !(self.stated_year.blank?)
    false
  end

#endregion has_<attribute>? section

  #region time/date related
  # @return[Time] alias for nomenclature_date, computed if not yet saved
  def date
    set_nomenclature_date if !self.persisted?
    self.nomenclature_date
  end

  def set_nomenclature_date
    self.nomenclature_date = Utilities::Dates.nomenclature_date(self.day, self.month, self.year) 
  end
  #todo move the test for nomenclature_date to spec/lib/utilities/dates_spec.rb

#endregion    time/date related

  protected

   def set_cached_values

    bx_entry = self.to_bibtex
    if bx_entry.key.blank? then
      bx_entry.key = 'tmpID'
    end
    key = bx_entry.key
    #bx_entry.key = 'tmpID'
    bx_bibliography = BibTeX::Bibliography.new()
    bx_bibliography.add(bx_entry)

    cp = CiteProc::Processor.new(style: 'apa', format: 'text')
     cp.import bx_bibliography.to_citeproc

=begin
    cp << bx_bibliography.to_citeproc
    cp.process(bx_bibliography[self.id].to_citeproc)

    self.cached = CiteProc.process bx_entry.to_citeproc, style: 'apa', format: 'text'
=end

     # format cached = full reference
    self.cached = cp.render(:bibliography, id: key)[0]
    # self.cached = cp.render(:bibliography, id: 'tmpID')[0]
    # format cached_author_srting = either the bibtex author or the last names of all the normalized authors.
     self.cached_author_string = authority_name
  end


#region hard validations

# replaced with inclusion validation
#   def check_bibtex_type # must have a valid bibtex_type
#     errors.add(:bibtex_type, 'not a valid bibtex type') if !::VALID_BIBTEX_TYPES.include?(self.bibtex_type)
#   end

  def check_has_field # must have at least one of the required fields (TW_REQ_FIELDS)
    valid = false
    TW_REQ_FIELDS.each do |i| # for each i in the required fields list
      if !self[i].blank?
        valid = true
        break
      end
    end
    # if i is not nil and not == "", it's validly_published
    #if (!self[i].nil?) && (self[i] != '')
    #  return true
    #end
    errors.add(:base, 'no core data provided') if !valid
                      # return false # none of the required fields have a value
  end

#endregion  hard validations

#region Soft_validation_methods
  def sv_has_authors
    if !(has_authors?)
      soft_validations.add(:author, 'There is no author associated with this source.')
    end
  end

  def sv_contains_a_writer # neither author nor editor
    if !has_writer?
      soft_validations.add(:author, 'There is neither an author,nor editor associated with this source.')
      soft_validations.add(:editor, 'There is neither an author,nor editor associated with this source.')
    end

  end

  def sv_has_title
    if (self.title.blank?)
      soft_validations.add(:title, 'There is no title associated with this source.')
    end
  end

  def sv_has_some_type_of_year
    if (!has_some_year?)
      soft_validations.add(:year, 'There is no year or stated year associated with this source.')
      soft_validations.add(:stated_year, 'There is no or stated year year associated with this source.')
    end
  end

  def sv_year_exists
    if (year.blank?)
      soft_validations.add(:year, 'There is no year associated with this source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  def sv_missing_journal
    soft_validations.add(:bibtex_type, 'The source is missing a journal name.') if self.journal.blank?
  end

  def sv_is_article_missing_journal
    if (self.bibtex_type == 'article')
      if (self.journal.blank?)
        soft_validations.add(:bibtex_type, 'The article is missing a journal name.')
      end
    end
  end

  def sv_has_a_publisher
    if (self.publisher.blank?)
      soft_validations.add(:publisher, 'There is no publisher associated with this source.')
    end
  end

  def sv_has_booktitle
    if (self.booktitle.blank?)
      soft_validations.add(:booktitle, 'There is no book title associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:chapter, 'There is neither a chapter nor pages with this source.')
      soft_validations.add(:pages, 'There is neither a chapter nor pages with this source.')
    end
  end

  def sv_has_school
    if (self.school.blank?)
      soft_validations.add(:school, 'There is no school associated with this thesis.')
    end
  end

  def sv_has_institution
    if (self.institution.blank?)
      soft_validations.add(:institution, 'There is not institution associated with this tech report.')
    end
  end

  def sv_has_note
     # TODO we may need to check of a note in the TW sense as well - has_note? above.
     if (self.note.blank?) && (self.notes.count = 0)
       soft_validations.add(:note, 'There is no note associated with this source.')
     end
  end

  def sv_missing_required_bibtex_fields
    case self.bibtex_type
      when 'article' #:article       => [:author,:title,:journal,:year]
        sv_has_authors
        sv_has_title
        sv_is_article_missing_journal
        sv_year_exists
      when 'book' #:book          => [[:author,:editor],:title,:publisher,:year]
        sv_contains_a_writer
        sv_has_title
        sv_has_a_publisher
        sv_year_exists
      when 'booklet' #    :booklet       => [:title],
        sv_has_title
      when 'conference' #    :conference    => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'inbook' #    :inbook        => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year],
        sv_contains_a_writer
        sv_has_title
        sv_is_contained_has_chapter_or_pages
        sv_has_a_publisher
        sv_year_exists
      when 'incollection' #    :incollection  => [:author,:title,:booktitle,:publisher,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_has_a_publisher
        sv_year_exists
      when 'inproceedings' #    :inproceedings => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'manual' #    :manual        => [:title],
        sv_has_title
      when 'mastersthesis' #    :mastersthesis => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      #    :misc          => [],  (no required fields)
      when 'phdthesis' #    :phdthesis     => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      when 'proceedings' #    :proceedings   => [:title,:year],
        sv_has_title
        sv_year_exists
      when 'techreport' #    :techreport    => [:author,:title,:institution,:year],
        sv_has_authors
        sv_has_title
        sv_has_institution
        sv_year_exists
      when 'unpublished' #    :unpublished   => [:author,:title,:note]
        sv_has_authors
        sv_has_title
        sv_has_note
    end
  end

#endregion   Soft_validation_methods

end

#authorString?

BibTeX standard field (required for types: )(optional for types:) A TW required attribute (TW requires a value in one of any of the required attributes.) The name(s) of the author(s), in the format described in the LaTeX book. Names should be formatted as “Last name, FirstName MiddleName”. FirstName and MiddleName can be initials. If there are multiple authors, each author name should be separated by the word “ and ”. It should be noted that all the names before the comma are treated as a single last name.

Returns:

  • (String)

    the list of author names in BibTeX format

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).first.identifier
  end

  def issn=(value)
    write_attribute(:issn, value)
    #TODO if there is already an 'Identifier::Global::Issn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
  end
  def issn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:issn).first.identifier
  end

#TODO if language is set => set language_id
#endregion getters & setters

  # turn bibtex URL field into a Ruby URI object
  def url_as_uri 
    URI(self.url) unless self.url.blank?
  end

#region has_<attribute>? section
  def has_authors? # is there a bibtex author or author roles?
    return true if !(self.author.blank?)
    # author attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.authors.count > 0) ? (return true) : (return false)
  end

  def has_editors?
    return true if !(self.editor.blank?)
    # editor attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.editors.count > 0) ? (return true) : (return false)
  end

  def has_writer? # contains either an author or editor
    (has_authors?) || (has_editors?) ? true : false
  end

  def has_some_year? # is there a year or stated year?
    return true if !(self.year.blank?) || !(self.stated_year.blank?)
    false
  end

#endregion has_<attribute>? section

  #region time/date related
  # @return[Time] alias for nomenclature_date, computed if not yet saved
  def date
    set_nomenclature_date if !self.persisted?
    self.nomenclature_date
  end

  def set_nomenclature_date
    self.nomenclature_date = Utilities::Dates.nomenclature_date(self.day, self.month, self.year) 
  end
  #todo move the test for nomenclature_date to spec/lib/utilities/dates_spec.rb

#endregion    time/date related

  protected

   def set_cached_values

    bx_entry = self.to_bibtex
    if bx_entry.key.blank? then
      bx_entry.key = 'tmpID'
    end
    key = bx_entry.key
    #bx_entry.key = 'tmpID'
    bx_bibliography = BibTeX::Bibliography.new()
    bx_bibliography.add(bx_entry)

    cp = CiteProc::Processor.new(style: 'apa', format: 'text')
     cp.import bx_bibliography.to_citeproc

=begin
    cp << bx_bibliography.to_citeproc
    cp.process(bx_bibliography[self.id].to_citeproc)

    self.cached = CiteProc.process bx_entry.to_citeproc, style: 'apa', format: 'text'
=end

     # format cached = full reference
    self.cached = cp.render(:bibliography, id: key)[0]
    # self.cached = cp.render(:bibliography, id: 'tmpID')[0]
    # format cached_author_srting = either the bibtex author or the last names of all the normalized authors.
     self.cached_author_string = authority_name
  end


#region hard validations

# replaced with inclusion validation
#   def check_bibtex_type # must have a valid bibtex_type
#     errors.add(:bibtex_type, 'not a valid bibtex type') if !::VALID_BIBTEX_TYPES.include?(self.bibtex_type)
#   end

  def check_has_field # must have at least one of the required fields (TW_REQ_FIELDS)
    valid = false
    TW_REQ_FIELDS.each do |i| # for each i in the required fields list
      if !self[i].blank?
        valid = true
        break
      end
    end
    # if i is not nil and not == "", it's validly_published
    #if (!self[i].nil?) && (self[i] != '')
    #  return true
    #end
    errors.add(:base, 'no core data provided') if !valid
                      # return false # none of the required fields have a value
  end

#endregion  hard validations

#region Soft_validation_methods
  def sv_has_authors
    if !(has_authors?)
      soft_validations.add(:author, 'There is no author associated with this source.')
    end
  end

  def sv_contains_a_writer # neither author nor editor
    if !has_writer?
      soft_validations.add(:author, 'There is neither an author,nor editor associated with this source.')
      soft_validations.add(:editor, 'There is neither an author,nor editor associated with this source.')
    end

  end

  def sv_has_title
    if (self.title.blank?)
      soft_validations.add(:title, 'There is no title associated with this source.')
    end
  end

  def sv_has_some_type_of_year
    if (!has_some_year?)
      soft_validations.add(:year, 'There is no year or stated year associated with this source.')
      soft_validations.add(:stated_year, 'There is no or stated year year associated with this source.')
    end
  end

  def sv_year_exists
    if (year.blank?)
      soft_validations.add(:year, 'There is no year associated with this source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  def sv_missing_journal
    soft_validations.add(:bibtex_type, 'The source is missing a journal name.') if self.journal.blank?
  end

  def sv_is_article_missing_journal
    if (self.bibtex_type == 'article')
      if (self.journal.blank?)
        soft_validations.add(:bibtex_type, 'The article is missing a journal name.')
      end
    end
  end

  def sv_has_a_publisher
    if (self.publisher.blank?)
      soft_validations.add(:publisher, 'There is no publisher associated with this source.')
    end
  end

  def sv_has_booktitle
    if (self.booktitle.blank?)
      soft_validations.add(:booktitle, 'There is no book title associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:chapter, 'There is neither a chapter nor pages with this source.')
      soft_validations.add(:pages, 'There is neither a chapter nor pages with this source.')
    end
  end

  def sv_has_school
    if (self.school.blank?)
      soft_validations.add(:school, 'There is no school associated with this thesis.')
    end
  end

  def sv_has_institution
    if (self.institution.blank?)
      soft_validations.add(:institution, 'There is not institution associated with this tech report.')
    end
  end

  def sv_has_note
     # TODO we may need to check of a note in the TW sense as well - has_note? above.
     if (self.note.blank?) && (self.notes.count = 0)
       soft_validations.add(:note, 'There is no note associated with this source.')
     end
  end

  def sv_missing_required_bibtex_fields
    case self.bibtex_type
      when 'article' #:article       => [:author,:title,:journal,:year]
        sv_has_authors
        sv_has_title
        sv_is_article_missing_journal
        sv_year_exists
      when 'book' #:book          => [[:author,:editor],:title,:publisher,:year]
        sv_contains_a_writer
        sv_has_title
        sv_has_a_publisher
        sv_year_exists
      when 'booklet' #    :booklet       => [:title],
        sv_has_title
      when 'conference' #    :conference    => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'inbook' #    :inbook        => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year],
        sv_contains_a_writer
        sv_has_title
        sv_is_contained_has_chapter_or_pages
        sv_has_a_publisher
        sv_year_exists
      when 'incollection' #    :incollection  => [:author,:title,:booktitle,:publisher,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_has_a_publisher
        sv_year_exists
      when 'inproceedings' #    :inproceedings => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'manual' #    :manual        => [:title],
        sv_has_title
      when 'mastersthesis' #    :mastersthesis => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      #    :misc          => [],  (no required fields)
      when 'phdthesis' #    :phdthesis     => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      when 'proceedings' #    :proceedings   => [:title,:year],
        sv_has_title
        sv_year_exists
      when 'techreport' #    :techreport    => [:author,:title,:institution,:year],
        sv_has_authors
        sv_has_title
        sv_has_institution
        sv_year_exists
      when 'unpublished' #    :unpublished   => [:author,:title,:note]
        sv_has_authors
        sv_has_title
        sv_has_note
    end
  end

#endregion   Soft_validation_methods

end

#bibtex_typeObject

include Shared::Notable in source.rb



# File 'app/models/source/bibtex.rb', line 207

#booktitlenil

BibTeX standard field (required for types: )(optional for types:) A TW required attribute (TW requires a value in one of the required attributes.) Title of a book, part of which is being cited. See the LaTEX book for how to type titles. For book entries, use the title field instead. @return the title of the book

Returns:

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).first.identifier
  end

  def issn=(value)
    write_attribute(:issn, value)
    #TODO if there is already an 'Identifier::Global::Issn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
  end
  def issn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:issn).first.identifier
  end

#TODO if language is set => set language_id
#endregion getters & setters

  # turn bibtex URL field into a Ruby URI object
  def url_as_uri 
    URI(self.url) unless self.url.blank?
  end

#region has_<attribute>? section
  def has_authors? # is there a bibtex author or author roles?
    return true if !(self.author.blank?)
    # author attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.authors.count > 0) ? (return true) : (return false)
  end

  def has_editors?
    return true if !(self.editor.blank?)
    # editor attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.editors.count > 0) ? (return true) : (return false)
  end

  def has_writer? # contains either an author or editor
    (has_authors?) || (has_editors?) ? true : false
  end

  def has_some_year? # is there a year or stated year?
    return true if !(self.year.blank?) || !(self.stated_year.blank?)
    false
  end

#endregion has_<attribute>? section

  #region time/date related
  # @return[Time] alias for nomenclature_date, computed if not yet saved
  def date
    set_nomenclature_date if !self.persisted?
    self.nomenclature_date
  end

  def set_nomenclature_date
    self.nomenclature_date = Utilities::Dates.nomenclature_date(self.day, self.month, self.year) 
  end
  #todo move the test for nomenclature_date to spec/lib/utilities/dates_spec.rb

#endregion    time/date related

  protected

   def set_cached_values

    bx_entry = self.to_bibtex
    if bx_entry.key.blank? then
      bx_entry.key = 'tmpID'
    end
    key = bx_entry.key
    #bx_entry.key = 'tmpID'
    bx_bibliography = BibTeX::Bibliography.new()
    bx_bibliography.add(bx_entry)

    cp = CiteProc::Processor.new(style: 'apa', format: 'text')
     cp.import bx_bibliography.to_citeproc

=begin
    cp << bx_bibliography.to_citeproc
    cp.process(bx_bibliography[self.id].to_citeproc)

    self.cached = CiteProc.process bx_entry.to_citeproc, style: 'apa', format: 'text'
=end

     # format cached = full reference
    self.cached = cp.render(:bibliography, id: key)[0]
    # self.cached = cp.render(:bibliography, id: 'tmpID')[0]
    # format cached_author_srting = either the bibtex author or the last names of all the normalized authors.
     self.cached_author_string = authority_name
  end


#region hard validations

# replaced with inclusion validation
#   def check_bibtex_type # must have a valid bibtex_type
#     errors.add(:bibtex_type, 'not a valid bibtex type') if !::VALID_BIBTEX_TYPES.include?(self.bibtex_type)
#   end

  def check_has_field # must have at least one of the required fields (TW_REQ_FIELDS)
    valid = false
    TW_REQ_FIELDS.each do |i| # for each i in the required fields list
      if !self[i].blank?
        valid = true
        break
      end
    end
    # if i is not nil and not == "", it's validly_published
    #if (!self[i].nil?) && (self[i] != '')
    #  return true
    #end
    errors.add(:base, 'no core data provided') if !valid
                      # return false # none of the required fields have a value
  end

#endregion  hard validations

#region Soft_validation_methods
  def sv_has_authors
    if !(has_authors?)
      soft_validations.add(:author, 'There is no author associated with this source.')
    end
  end

  def sv_contains_a_writer # neither author nor editor
    if !has_writer?
      soft_validations.add(:author, 'There is neither an author,nor editor associated with this source.')
      soft_validations.add(:editor, 'There is neither an author,nor editor associated with this source.')
    end

  end

  def sv_has_title
    if (self.title.blank?)
      soft_validations.add(:title, 'There is no title associated with this source.')
    end
  end

  def sv_has_some_type_of_year
    if (!has_some_year?)
      soft_validations.add(:year, 'There is no year or stated year associated with this source.')
      soft_validations.add(:stated_year, 'There is no or stated year year associated with this source.')
    end
  end

  def sv_year_exists
    if (year.blank?)
      soft_validations.add(:year, 'There is no year associated with this source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  def sv_missing_journal
    soft_validations.add(:bibtex_type, 'The source is missing a journal name.') if self.journal.blank?
  end

  def sv_is_article_missing_journal
    if (self.bibtex_type == 'article')
      if (self.journal.blank?)
        soft_validations.add(:bibtex_type, 'The article is missing a journal name.')
      end
    end
  end

  def sv_has_a_publisher
    if (self.publisher.blank?)
      soft_validations.add(:publisher, 'There is no publisher associated with this source.')
    end
  end

  def sv_has_booktitle
    if (self.booktitle.blank?)
      soft_validations.add(:booktitle, 'There is no book title associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:chapter, 'There is neither a chapter nor pages with this source.')
      soft_validations.add(:pages, 'There is neither a chapter nor pages with this source.')
    end
  end

  def sv_has_school
    if (self.school.blank?)
      soft_validations.add(:school, 'There is no school associated with this thesis.')
    end
  end

  def sv_has_institution
    if (self.institution.blank?)
      soft_validations.add(:institution, 'There is not institution associated with this tech report.')
    end
  end

  def sv_has_note
     # TODO we may need to check of a note in the TW sense as well - has_note? above.
     if (self.note.blank?) && (self.notes.count = 0)
       soft_validations.add(:note, 'There is no note associated with this source.')
     end
  end

  def sv_missing_required_bibtex_fields
    case self.bibtex_type
      when 'article' #:article       => [:author,:title,:journal,:year]
        sv_has_authors
        sv_has_title
        sv_is_article_missing_journal
        sv_year_exists
      when 'book' #:book          => [[:author,:editor],:title,:publisher,:year]
        sv_contains_a_writer
        sv_has_title
        sv_has_a_publisher
        sv_year_exists
      when 'booklet' #    :booklet       => [:title],
        sv_has_title
      when 'conference' #    :conference    => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'inbook' #    :inbook        => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year],
        sv_contains_a_writer
        sv_has_title
        sv_is_contained_has_chapter_or_pages
        sv_has_a_publisher
        sv_year_exists
      when 'incollection' #    :incollection  => [:author,:title,:booktitle,:publisher,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_has_a_publisher
        sv_year_exists
      when 'inproceedings' #    :inproceedings => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'manual' #    :manual        => [:title],
        sv_has_title
      when 'mastersthesis' #    :mastersthesis => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      #    :misc          => [],  (no required fields)
      when 'phdthesis' #    :phdthesis     => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      when 'proceedings' #    :proceedings   => [:title,:year],
        sv_has_title
        sv_year_exists
      when 'techreport' #    :techreport    => [:author,:title,:institution,:year],
        sv_has_authors
        sv_has_title
        sv_has_institution
        sv_year_exists
      when 'unpublished' #    :unpublished   => [:author,:title,:note]
        sv_has_authors
        sv_has_title
        sv_has_note
    end
  end

#endregion   Soft_validation_methods

end

#cachedObject

include Shared::Notable in source.rb



# File 'app/models/source/bibtex.rb', line 207

#cached_author_stringObject

include Shared::Notable in source.rb



# File 'app/models/source/bibtex.rb', line 207

#chapterString?

BibTeX standard field (required for types: )(optional for types:) A chapter (or section or whatever) number.

Returns:

  • (String)

    the chapter or section number.

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).first.identifier
  end

  def issn=(value)
    write_attribute(:issn, value)
    #TODO if there is already an 'Identifier::Global::Issn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
  end
  def issn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:issn).first.identifier
  end

#TODO if language is set => set language_id
#endregion getters & setters

  # turn bibtex URL field into a Ruby URI object
  def url_as_uri 
    URI(self.url) unless self.url.blank?
  end

#region has_<attribute>? section
  def has_authors? # is there a bibtex author or author roles?
    return true if !(self.author.blank?)
    # author attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.authors.count > 0) ? (return true) : (return false)
  end

  def has_editors?
    return true if !(self.editor.blank?)
    # editor attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.editors.count > 0) ? (return true) : (return false)
  end

  def has_writer? # contains either an author or editor
    (has_authors?) || (has_editors?) ? true : false
  end

  def has_some_year? # is there a year or stated year?
    return true if !(self.year.blank?) || !(self.stated_year.blank?)
    false
  end

#endregion has_<attribute>? section

  #region time/date related
  # @return[Time] alias for nomenclature_date, computed if not yet saved
  def date
    set_nomenclature_date if !self.persisted?
    self.nomenclature_date
  end

  def set_nomenclature_date
    self.nomenclature_date = Utilities::Dates.nomenclature_date(self.day, self.month, self.year) 
  end
  #todo move the test for nomenclature_date to spec/lib/utilities/dates_spec.rb

#endregion    time/date related

  protected

   def set_cached_values

    bx_entry = self.to_bibtex
    if bx_entry.key.blank? then
      bx_entry.key = 'tmpID'
    end
    key = bx_entry.key
    #bx_entry.key = 'tmpID'
    bx_bibliography = BibTeX::Bibliography.new()
    bx_bibliography.add(bx_entry)

    cp = CiteProc::Processor.new(style: 'apa', format: 'text')
     cp.import bx_bibliography.to_citeproc

=begin
    cp << bx_bibliography.to_citeproc
    cp.process(bx_bibliography[self.id].to_citeproc)

    self.cached = CiteProc.process bx_entry.to_citeproc, style: 'apa', format: 'text'
=end

     # format cached = full reference
    self.cached = cp.render(:bibliography, id: key)[0]
    # self.cached = cp.render(:bibliography, id: 'tmpID')[0]
    # format cached_author_srting = either the bibtex author or the last names of all the normalized authors.
     self.cached_author_string = authority_name
  end


#region hard validations

# replaced with inclusion validation
#   def check_bibtex_type # must have a valid bibtex_type
#     errors.add(:bibtex_type, 'not a valid bibtex type') if !::VALID_BIBTEX_TYPES.include?(self.bibtex_type)
#   end

  def check_has_field # must have at least one of the required fields (TW_REQ_FIELDS)
    valid = false
    TW_REQ_FIELDS.each do |i| # for each i in the required fields list
      if !self[i].blank?
        valid = true
        break
      end
    end
    # if i is not nil and not == "", it's validly_published
    #if (!self[i].nil?) && (self[i] != '')
    #  return true
    #end
    errors.add(:base, 'no core data provided') if !valid
                      # return false # none of the required fields have a value
  end

#endregion  hard validations

#region Soft_validation_methods
  def sv_has_authors
    if !(has_authors?)
      soft_validations.add(:author, 'There is no author associated with this source.')
    end
  end

  def sv_contains_a_writer # neither author nor editor
    if !has_writer?
      soft_validations.add(:author, 'There is neither an author,nor editor associated with this source.')
      soft_validations.add(:editor, 'There is neither an author,nor editor associated with this source.')
    end

  end

  def sv_has_title
    if (self.title.blank?)
      soft_validations.add(:title, 'There is no title associated with this source.')
    end
  end

  def sv_has_some_type_of_year
    if (!has_some_year?)
      soft_validations.add(:year, 'There is no year or stated year associated with this source.')
      soft_validations.add(:stated_year, 'There is no or stated year year associated with this source.')
    end
  end

  def sv_year_exists
    if (year.blank?)
      soft_validations.add(:year, 'There is no year associated with this source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  def sv_missing_journal
    soft_validations.add(:bibtex_type, 'The source is missing a journal name.') if self.journal.blank?
  end

  def sv_is_article_missing_journal
    if (self.bibtex_type == 'article')
      if (self.journal.blank?)
        soft_validations.add(:bibtex_type, 'The article is missing a journal name.')
      end
    end
  end

  def sv_has_a_publisher
    if (self.publisher.blank?)
      soft_validations.add(:publisher, 'There is no publisher associated with this source.')
    end
  end

  def sv_has_booktitle
    if (self.booktitle.blank?)
      soft_validations.add(:booktitle, 'There is no book title associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:chapter, 'There is neither a chapter nor pages with this source.')
      soft_validations.add(:pages, 'There is neither a chapter nor pages with this source.')
    end
  end

  def sv_has_school
    if (self.school.blank?)
      soft_validations.add(:school, 'There is no school associated with this thesis.')
    end
  end

  def sv_has_institution
    if (self.institution.blank?)
      soft_validations.add(:institution, 'There is not institution associated with this tech report.')
    end
  end

  def sv_has_note
     # TODO we may need to check of a note in the TW sense as well - has_note? above.
     if (self.note.blank?) && (self.notes.count = 0)
       soft_validations.add(:note, 'There is no note associated with this source.')
     end
  end

  def sv_missing_required_bibtex_fields
    case self.bibtex_type
      when 'article' #:article       => [:author,:title,:journal,:year]
        sv_has_authors
        sv_has_title
        sv_is_article_missing_journal
        sv_year_exists
      when 'book' #:book          => [[:author,:editor],:title,:publisher,:year]
        sv_contains_a_writer
        sv_has_title
        sv_has_a_publisher
        sv_year_exists
      when 'booklet' #    :booklet       => [:title],
        sv_has_title
      when 'conference' #    :conference    => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'inbook' #    :inbook        => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year],
        sv_contains_a_writer
        sv_has_title
        sv_is_contained_has_chapter_or_pages
        sv_has_a_publisher
        sv_year_exists
      when 'incollection' #    :incollection  => [:author,:title,:booktitle,:publisher,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_has_a_publisher
        sv_year_exists
      when 'inproceedings' #    :inproceedings => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'manual' #    :manual        => [:title],
        sv_has_title
      when 'mastersthesis' #    :mastersthesis => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      #    :misc          => [],  (no required fields)
      when 'phdthesis' #    :phdthesis     => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      when 'proceedings' #    :proceedings   => [:title,:year],
        sv_has_title
        sv_year_exists
      when 'techreport' #    :techreport    => [:author,:title,:institution,:year],
        sv_has_authors
        sv_has_title
        sv_has_institution
        sv_year_exists
      when 'unpublished' #    :unpublished   => [:author,:title,:note]
        sv_has_authors
        sv_has_title
        sv_has_note
    end
  end

#endregion   Soft_validation_methods

end

#contentsObject

include Shared::Notable in source.rb



# File 'app/models/source/bibtex.rb', line 207

include Shared::Notable in source.rb



# File 'app/models/source/bibtex.rb', line 207

#created_atObject

include Shared::Notable in source.rb



# File 'app/models/source/bibtex.rb', line 207

#created_by - not yet implemented(-) ⇒ Object

include Shared::Notable in source.rb



# File 'app/models/source/bibtex.rb', line 207

#crossrefnil

BibTeX standard field (ignored by standard processors) The database key(key attribute) of the entry being cross referenced. This attribute is only set (and saved) during the import process, and is only relevant in a specific bibliography. @return the key of the cross referenced source

Returns:

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).first.identifier
  end

  def issn=(value)
    write_attribute(:issn, value)
    #TODO if there is already an 'Identifier::Global::Issn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
  end
  def issn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:issn).first.identifier
  end

#TODO if language is set => set language_id
#endregion getters & setters

  # turn bibtex URL field into a Ruby URI object
  def url_as_uri 
    URI(self.url) unless self.url.blank?
  end

#region has_<attribute>? section
  def has_authors? # is there a bibtex author or author roles?
    return true if !(self.author.blank?)
    # author attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.authors.count > 0) ? (return true) : (return false)
  end

  def has_editors?
    return true if !(self.editor.blank?)
    # editor attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.editors.count > 0) ? (return true) : (return false)
  end

  def has_writer? # contains either an author or editor
    (has_authors?) || (has_editors?) ? true : false
  end

  def has_some_year? # is there a year or stated year?
    return true if !(self.year.blank?) || !(self.stated_year.blank?)
    false
  end

#endregion has_<attribute>? section

  #region time/date related
  # @return[Time] alias for nomenclature_date, computed if not yet saved
  def date
    set_nomenclature_date if !self.persisted?
    self.nomenclature_date
  end

  def set_nomenclature_date
    self.nomenclature_date = Utilities::Dates.nomenclature_date(self.day, self.month, self.year) 
  end
  #todo move the test for nomenclature_date to spec/lib/utilities/dates_spec.rb

#endregion    time/date related

  protected

   def set_cached_values

    bx_entry = self.to_bibtex
    if bx_entry.key.blank? then
      bx_entry.key = 'tmpID'
    end
    key = bx_entry.key
    #bx_entry.key = 'tmpID'
    bx_bibliography = BibTeX::Bibliography.new()
    bx_bibliography.add(bx_entry)

    cp = CiteProc::Processor.new(style: 'apa', format: 'text')
     cp.import bx_bibliography.to_citeproc

=begin
    cp << bx_bibliography.to_citeproc
    cp.process(bx_bibliography[self.id].to_citeproc)

    self.cached = CiteProc.process bx_entry.to_citeproc, style: 'apa', format: 'text'
=end

     # format cached = full reference
    self.cached = cp.render(:bibliography, id: key)[0]
    # self.cached = cp.render(:bibliography, id: 'tmpID')[0]
    # format cached_author_srting = either the bibtex author or the last names of all the normalized authors.
     self.cached_author_string = authority_name
  end


#region hard validations

# replaced with inclusion validation
#   def check_bibtex_type # must have a valid bibtex_type
#     errors.add(:bibtex_type, 'not a valid bibtex type') if !::VALID_BIBTEX_TYPES.include?(self.bibtex_type)
#   end

  def check_has_field # must have at least one of the required fields (TW_REQ_FIELDS)
    valid = false
    TW_REQ_FIELDS.each do |i| # for each i in the required fields list
      if !self[i].blank?
        valid = true
        break
      end
    end
    # if i is not nil and not == "", it's validly_published
    #if (!self[i].nil?) && (self[i] != '')
    #  return true
    #end
    errors.add(:base, 'no core data provided') if !valid
                      # return false # none of the required fields have a value
  end

#endregion  hard validations

#region Soft_validation_methods
  def sv_has_authors
    if !(has_authors?)
      soft_validations.add(:author, 'There is no author associated with this source.')
    end
  end

  def sv_contains_a_writer # neither author nor editor
    if !has_writer?
      soft_validations.add(:author, 'There is neither an author,nor editor associated with this source.')
      soft_validations.add(:editor, 'There is neither an author,nor editor associated with this source.')
    end

  end

  def sv_has_title
    if (self.title.blank?)
      soft_validations.add(:title, 'There is no title associated with this source.')
    end
  end

  def sv_has_some_type_of_year
    if (!has_some_year?)
      soft_validations.add(:year, 'There is no year or stated year associated with this source.')
      soft_validations.add(:stated_year, 'There is no or stated year year associated with this source.')
    end
  end

  def sv_year_exists
    if (year.blank?)
      soft_validations.add(:year, 'There is no year associated with this source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  def sv_missing_journal
    soft_validations.add(:bibtex_type, 'The source is missing a journal name.') if self.journal.blank?
  end

  def sv_is_article_missing_journal
    if (self.bibtex_type == 'article')
      if (self.journal.blank?)
        soft_validations.add(:bibtex_type, 'The article is missing a journal name.')
      end
    end
  end

  def sv_has_a_publisher
    if (self.publisher.blank?)
      soft_validations.add(:publisher, 'There is no publisher associated with this source.')
    end
  end

  def sv_has_booktitle
    if (self.booktitle.blank?)
      soft_validations.add(:booktitle, 'There is no book title associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:chapter, 'There is neither a chapter nor pages with this source.')
      soft_validations.add(:pages, 'There is neither a chapter nor pages with this source.')
    end
  end

  def sv_has_school
    if (self.school.blank?)
      soft_validations.add(:school, 'There is no school associated with this thesis.')
    end
  end

  def sv_has_institution
    if (self.institution.blank?)
      soft_validations.add(:institution, 'There is not institution associated with this tech report.')
    end
  end

  def sv_has_note
     # TODO we may need to check of a note in the TW sense as well - has_note? above.
     if (self.note.blank?) && (self.notes.count = 0)
       soft_validations.add(:note, 'There is no note associated with this source.')
     end
  end

  def sv_missing_required_bibtex_fields
    case self.bibtex_type
      when 'article' #:article       => [:author,:title,:journal,:year]
        sv_has_authors
        sv_has_title
        sv_is_article_missing_journal
        sv_year_exists
      when 'book' #:book          => [[:author,:editor],:title,:publisher,:year]
        sv_contains_a_writer
        sv_has_title
        sv_has_a_publisher
        sv_year_exists
      when 'booklet' #    :booklet       => [:title],
        sv_has_title
      when 'conference' #    :conference    => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'inbook' #    :inbook        => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year],
        sv_contains_a_writer
        sv_has_title
        sv_is_contained_has_chapter_or_pages
        sv_has_a_publisher
        sv_year_exists
      when 'incollection' #    :incollection  => [:author,:title,:booktitle,:publisher,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_has_a_publisher
        sv_year_exists
      when 'inproceedings' #    :inproceedings => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'manual' #    :manual        => [:title],
        sv_has_title
      when 'mastersthesis' #    :mastersthesis => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      #    :misc          => [],  (no required fields)
      when 'phdthesis' #    :phdthesis     => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      when 'proceedings' #    :proceedings   => [:title,:year],
        sv_has_title
        sv_year_exists
      when 'techreport' #    :techreport    => [:author,:title,:institution,:year],
        sv_has_authors
        sv_has_title
        sv_has_institution
        sv_year_exists
      when 'unpublished' #    :unpublished   => [:author,:title,:note]
        sv_has_authors
        sv_has_title
        sv_has_note
    end
  end

#endregion   Soft_validation_methods

end

#dayObject

If day is present there must be a month and day must be valid for the month.



# File 'app/models/source/bibtex.rb', line 207

#doi - not implemented yet(-) ⇒ Object

Used by bibtex-ruby gem method identifier



# File 'app/models/source/bibtex.rb', line 207

#editionnil

BibTeX standard field (required for types: )(optional for types:) The edition of a book(for example, “Second”). This should be an ordinal, and should have the first letter capitalized, as shown here; the standard styles convert to lower case when necessary. @return the edition of the book

Returns:

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).first.identifier
  end

  def issn=(value)
    write_attribute(:issn, value)
    #TODO if there is already an 'Identifier::Global::Issn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
  end
  def issn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:issn).first.identifier
  end

#TODO if language is set => set language_id
#endregion getters & setters

  # turn bibtex URL field into a Ruby URI object
  def url_as_uri 
    URI(self.url) unless self.url.blank?
  end

#region has_<attribute>? section
  def has_authors? # is there a bibtex author or author roles?
    return true if !(self.author.blank?)
    # author attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.authors.count > 0) ? (return true) : (return false)
  end

  def has_editors?
    return true if !(self.editor.blank?)
    # editor attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.editors.count > 0) ? (return true) : (return false)
  end

  def has_writer? # contains either an author or editor
    (has_authors?) || (has_editors?) ? true : false
  end

  def has_some_year? # is there a year or stated year?
    return true if !(self.year.blank?) || !(self.stated_year.blank?)
    false
  end

#endregion has_<attribute>? section

  #region time/date related
  # @return[Time] alias for nomenclature_date, computed if not yet saved
  def date
    set_nomenclature_date if !self.persisted?
    self.nomenclature_date
  end

  def set_nomenclature_date
    self.nomenclature_date = Utilities::Dates.nomenclature_date(self.day, self.month, self.year) 
  end
  #todo move the test for nomenclature_date to spec/lib/utilities/dates_spec.rb

#endregion    time/date related

  protected

   def set_cached_values

    bx_entry = self.to_bibtex
    if bx_entry.key.blank? then
      bx_entry.key = 'tmpID'
    end
    key = bx_entry.key
    #bx_entry.key = 'tmpID'
    bx_bibliography = BibTeX::Bibliography.new()
    bx_bibliography.add(bx_entry)

    cp = CiteProc::Processor.new(style: 'apa', format: 'text')
     cp.import bx_bibliography.to_citeproc

=begin
    cp << bx_bibliography.to_citeproc
    cp.process(bx_bibliography[self.id].to_citeproc)

    self.cached = CiteProc.process bx_entry.to_citeproc, style: 'apa', format: 'text'
=end

     # format cached = full reference
    self.cached = cp.render(:bibliography, id: key)[0]
    # self.cached = cp.render(:bibliography, id: 'tmpID')[0]
    # format cached_author_srting = either the bibtex author or the last names of all the normalized authors.
     self.cached_author_string = authority_name
  end


#region hard validations

# replaced with inclusion validation
#   def check_bibtex_type # must have a valid bibtex_type
#     errors.add(:bibtex_type, 'not a valid bibtex type') if !::VALID_BIBTEX_TYPES.include?(self.bibtex_type)
#   end

  def check_has_field # must have at least one of the required fields (TW_REQ_FIELDS)
    valid = false
    TW_REQ_FIELDS.each do |i| # for each i in the required fields list
      if !self[i].blank?
        valid = true
        break
      end
    end
    # if i is not nil and not == "", it's validly_published
    #if (!self[i].nil?) && (self[i] != '')
    #  return true
    #end
    errors.add(:base, 'no core data provided') if !valid
                      # return false # none of the required fields have a value
  end

#endregion  hard validations

#region Soft_validation_methods
  def sv_has_authors
    if !(has_authors?)
      soft_validations.add(:author, 'There is no author associated with this source.')
    end
  end

  def sv_contains_a_writer # neither author nor editor
    if !has_writer?
      soft_validations.add(:author, 'There is neither an author,nor editor associated with this source.')
      soft_validations.add(:editor, 'There is neither an author,nor editor associated with this source.')
    end

  end

  def sv_has_title
    if (self.title.blank?)
      soft_validations.add(:title, 'There is no title associated with this source.')
    end
  end

  def sv_has_some_type_of_year
    if (!has_some_year?)
      soft_validations.add(:year, 'There is no year or stated year associated with this source.')
      soft_validations.add(:stated_year, 'There is no or stated year year associated with this source.')
    end
  end

  def sv_year_exists
    if (year.blank?)
      soft_validations.add(:year, 'There is no year associated with this source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  def sv_missing_journal
    soft_validations.add(:bibtex_type, 'The source is missing a journal name.') if self.journal.blank?
  end

  def sv_is_article_missing_journal
    if (self.bibtex_type == 'article')
      if (self.journal.blank?)
        soft_validations.add(:bibtex_type, 'The article is missing a journal name.')
      end
    end
  end

  def sv_has_a_publisher
    if (self.publisher.blank?)
      soft_validations.add(:publisher, 'There is no publisher associated with this source.')
    end
  end

  def sv_has_booktitle
    if (self.booktitle.blank?)
      soft_validations.add(:booktitle, 'There is no book title associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:chapter, 'There is neither a chapter nor pages with this source.')
      soft_validations.add(:pages, 'There is neither a chapter nor pages with this source.')
    end
  end

  def sv_has_school
    if (self.school.blank?)
      soft_validations.add(:school, 'There is no school associated with this thesis.')
    end
  end

  def sv_has_institution
    if (self.institution.blank?)
      soft_validations.add(:institution, 'There is not institution associated with this tech report.')
    end
  end

  def sv_has_note
     # TODO we may need to check of a note in the TW sense as well - has_note? above.
     if (self.note.blank?) && (self.notes.count = 0)
       soft_validations.add(:note, 'There is no note associated with this source.')
     end
  end

  def sv_missing_required_bibtex_fields
    case self.bibtex_type
      when 'article' #:article       => [:author,:title,:journal,:year]
        sv_has_authors
        sv_has_title
        sv_is_article_missing_journal
        sv_year_exists
      when 'book' #:book          => [[:author,:editor],:title,:publisher,:year]
        sv_contains_a_writer
        sv_has_title
        sv_has_a_publisher
        sv_year_exists
      when 'booklet' #    :booklet       => [:title],
        sv_has_title
      when 'conference' #    :conference    => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'inbook' #    :inbook        => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year],
        sv_contains_a_writer
        sv_has_title
        sv_is_contained_has_chapter_or_pages
        sv_has_a_publisher
        sv_year_exists
      when 'incollection' #    :incollection  => [:author,:title,:booktitle,:publisher,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_has_a_publisher
        sv_year_exists
      when 'inproceedings' #    :inproceedings => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'manual' #    :manual        => [:title],
        sv_has_title
      when 'mastersthesis' #    :mastersthesis => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      #    :misc          => [],  (no required fields)
      when 'phdthesis' #    :phdthesis     => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      when 'proceedings' #    :proceedings   => [:title,:year],
        sv_has_title
        sv_year_exists
      when 'techreport' #    :techreport    => [:author,:title,:institution,:year],
        sv_has_authors
        sv_has_title
        sv_has_institution
        sv_year_exists
      when 'unpublished' #    :unpublished   => [:author,:title,:note]
        sv_has_authors
        sv_has_title
        sv_has_note
    end
  end

#endregion   Soft_validation_methods

end

#editorString?

BibTeX standard field (required for types: )(optional for types:) A TW required attribute (TW requires a value in one of any of the required attributes.) The name(s) of the editor(s), in the format described in the LaTeX book. Names should be formatted as “Last name, FirstName MiddleName”. FirstName and MiddleName can be initials. If there are multiple editors, each editor name should be separated by the word “ and ”. It should be noted that all the names before the comma are treated as a single last name.

If there is also an author field, then the editor field gives the editor of the book or collection in which the reference appears.

Returns:

  • (String)

    the list of editor names in BibTeX format

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).first.identifier
  end

  def issn=(value)
    write_attribute(:issn, value)
    #TODO if there is already an 'Identifier::Global::Issn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
  end
  def issn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:issn).first.identifier
  end

#TODO if language is set => set language_id
#endregion getters & setters

  # turn bibtex URL field into a Ruby URI object
  def url_as_uri 
    URI(self.url) unless self.url.blank?
  end

#region has_<attribute>? section
  def has_authors? # is there a bibtex author or author roles?
    return true if !(self.author.blank?)
    # author attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.authors.count > 0) ? (return true) : (return false)
  end

  def has_editors?
    return true if !(self.editor.blank?)
    # editor attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.editors.count > 0) ? (return true) : (return false)
  end

  def has_writer? # contains either an author or editor
    (has_authors?) || (has_editors?) ? true : false
  end

  def has_some_year? # is there a year or stated year?
    return true if !(self.year.blank?) || !(self.stated_year.blank?)
    false
  end

#endregion has_<attribute>? section

  #region time/date related
  # @return[Time] alias for nomenclature_date, computed if not yet saved
  def date
    set_nomenclature_date if !self.persisted?
    self.nomenclature_date
  end

  def set_nomenclature_date
    self.nomenclature_date = Utilities::Dates.nomenclature_date(self.day, self.month, self.year) 
  end
  #todo move the test for nomenclature_date to spec/lib/utilities/dates_spec.rb

#endregion    time/date related

  protected

   def set_cached_values

    bx_entry = self.to_bibtex
    if bx_entry.key.blank? then
      bx_entry.key = 'tmpID'
    end
    key = bx_entry.key
    #bx_entry.key = 'tmpID'
    bx_bibliography = BibTeX::Bibliography.new()
    bx_bibliography.add(bx_entry)

    cp = CiteProc::Processor.new(style: 'apa', format: 'text')
     cp.import bx_bibliography.to_citeproc

=begin
    cp << bx_bibliography.to_citeproc
    cp.process(bx_bibliography[self.id].to_citeproc)

    self.cached = CiteProc.process bx_entry.to_citeproc, style: 'apa', format: 'text'
=end

     # format cached = full reference
    self.cached = cp.render(:bibliography, id: key)[0]
    # self.cached = cp.render(:bibliography, id: 'tmpID')[0]
    # format cached_author_srting = either the bibtex author or the last names of all the normalized authors.
     self.cached_author_string = authority_name
  end


#region hard validations

# replaced with inclusion validation
#   def check_bibtex_type # must have a valid bibtex_type
#     errors.add(:bibtex_type, 'not a valid bibtex type') if !::VALID_BIBTEX_TYPES.include?(self.bibtex_type)
#   end

  def check_has_field # must have at least one of the required fields (TW_REQ_FIELDS)
    valid = false
    TW_REQ_FIELDS.each do |i| # for each i in the required fields list
      if !self[i].blank?
        valid = true
        break
      end
    end
    # if i is not nil and not == "", it's validly_published
    #if (!self[i].nil?) && (self[i] != '')
    #  return true
    #end
    errors.add(:base, 'no core data provided') if !valid
                      # return false # none of the required fields have a value
  end

#endregion  hard validations

#region Soft_validation_methods
  def sv_has_authors
    if !(has_authors?)
      soft_validations.add(:author, 'There is no author associated with this source.')
    end
  end

  def sv_contains_a_writer # neither author nor editor
    if !has_writer?
      soft_validations.add(:author, 'There is neither an author,nor editor associated with this source.')
      soft_validations.add(:editor, 'There is neither an author,nor editor associated with this source.')
    end

  end

  def sv_has_title
    if (self.title.blank?)
      soft_validations.add(:title, 'There is no title associated with this source.')
    end
  end

  def sv_has_some_type_of_year
    if (!has_some_year?)
      soft_validations.add(:year, 'There is no year or stated year associated with this source.')
      soft_validations.add(:stated_year, 'There is no or stated year year associated with this source.')
    end
  end

  def sv_year_exists
    if (year.blank?)
      soft_validations.add(:year, 'There is no year associated with this source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  def sv_missing_journal
    soft_validations.add(:bibtex_type, 'The source is missing a journal name.') if self.journal.blank?
  end

  def sv_is_article_missing_journal
    if (self.bibtex_type == 'article')
      if (self.journal.blank?)
        soft_validations.add(:bibtex_type, 'The article is missing a journal name.')
      end
    end
  end

  def sv_has_a_publisher
    if (self.publisher.blank?)
      soft_validations.add(:publisher, 'There is no publisher associated with this source.')
    end
  end

  def sv_has_booktitle
    if (self.booktitle.blank?)
      soft_validations.add(:booktitle, 'There is no book title associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:chapter, 'There is neither a chapter nor pages with this source.')
      soft_validations.add(:pages, 'There is neither a chapter nor pages with this source.')
    end
  end

  def sv_has_school
    if (self.school.blank?)
      soft_validations.add(:school, 'There is no school associated with this thesis.')
    end
  end

  def sv_has_institution
    if (self.institution.blank?)
      soft_validations.add(:institution, 'There is not institution associated with this tech report.')
    end
  end

  def sv_has_note
     # TODO we may need to check of a note in the TW sense as well - has_note? above.
     if (self.note.blank?) && (self.notes.count = 0)
       soft_validations.add(:note, 'There is no note associated with this source.')
     end
  end

  def sv_missing_required_bibtex_fields
    case self.bibtex_type
      when 'article' #:article       => [:author,:title,:journal,:year]
        sv_has_authors
        sv_has_title
        sv_is_article_missing_journal
        sv_year_exists
      when 'book' #:book          => [[:author,:editor],:title,:publisher,:year]
        sv_contains_a_writer
        sv_has_title
        sv_has_a_publisher
        sv_year_exists
      when 'booklet' #    :booklet       => [:title],
        sv_has_title
      when 'conference' #    :conference    => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'inbook' #    :inbook        => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year],
        sv_contains_a_writer
        sv_has_title
        sv_is_contained_has_chapter_or_pages
        sv_has_a_publisher
        sv_year_exists
      when 'incollection' #    :incollection  => [:author,:title,:booktitle,:publisher,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_has_a_publisher
        sv_year_exists
      when 'inproceedings' #    :inproceedings => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'manual' #    :manual        => [:title],
        sv_has_title
      when 'mastersthesis' #    :mastersthesis => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      #    :misc          => [],  (no required fields)
      when 'phdthesis' #    :phdthesis     => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      when 'proceedings' #    :proceedings   => [:title,:year],
        sv_has_title
        sv_year_exists
      when 'techreport' #    :techreport    => [:author,:title,:institution,:year],
        sv_has_authors
        sv_has_title
        sv_has_institution
        sv_year_exists
      when 'unpublished' #    :unpublished   => [:author,:title,:note]
        sv_has_authors
        sv_has_title
        sv_has_note
    end
  end

#endregion   Soft_validation_methods

end

#howpublishednil

BibTeX standard field (required for types: )(optional for types:) How something unusual has been published. The first word should be capitalized. @return a description of how this source was published

Returns:

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).first.identifier
  end

  def issn=(value)
    write_attribute(:issn, value)
    #TODO if there is already an 'Identifier::Global::Issn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
  end
  def issn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:issn).first.identifier
  end

#TODO if language is set => set language_id
#endregion getters & setters

  # turn bibtex URL field into a Ruby URI object
  def url_as_uri 
    URI(self.url) unless self.url.blank?
  end

#region has_<attribute>? section
  def has_authors? # is there a bibtex author or author roles?
    return true if !(self.author.blank?)
    # author attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.authors.count > 0) ? (return true) : (return false)
  end

  def has_editors?
    return true if !(self.editor.blank?)
    # editor attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.editors.count > 0) ? (return true) : (return false)
  end

  def has_writer? # contains either an author or editor
    (has_authors?) || (has_editors?) ? true : false
  end

  def has_some_year? # is there a year or stated year?
    return true if !(self.year.blank?) || !(self.stated_year.blank?)
    false
  end

#endregion has_<attribute>? section

  #region time/date related
  # @return[Time] alias for nomenclature_date, computed if not yet saved
  def date
    set_nomenclature_date if !self.persisted?
    self.nomenclature_date
  end

  def set_nomenclature_date
    self.nomenclature_date = Utilities::Dates.nomenclature_date(self.day, self.month, self.year) 
  end
  #todo move the test for nomenclature_date to spec/lib/utilities/dates_spec.rb

#endregion    time/date related

  protected

   def set_cached_values

    bx_entry = self.to_bibtex
    if bx_entry.key.blank? then
      bx_entry.key = 'tmpID'
    end
    key = bx_entry.key
    #bx_entry.key = 'tmpID'
    bx_bibliography = BibTeX::Bibliography.new()
    bx_bibliography.add(bx_entry)

    cp = CiteProc::Processor.new(style: 'apa', format: 'text')
     cp.import bx_bibliography.to_citeproc

=begin
    cp << bx_bibliography.to_citeproc
    cp.process(bx_bibliography[self.id].to_citeproc)

    self.cached = CiteProc.process bx_entry.to_citeproc, style: 'apa', format: 'text'
=end

     # format cached = full reference
    self.cached = cp.render(:bibliography, id: key)[0]
    # self.cached = cp.render(:bibliography, id: 'tmpID')[0]
    # format cached_author_srting = either the bibtex author or the last names of all the normalized authors.
     self.cached_author_string = authority_name
  end


#region hard validations

# replaced with inclusion validation
#   def check_bibtex_type # must have a valid bibtex_type
#     errors.add(:bibtex_type, 'not a valid bibtex type') if !::VALID_BIBTEX_TYPES.include?(self.bibtex_type)
#   end

  def check_has_field # must have at least one of the required fields (TW_REQ_FIELDS)
    valid = false
    TW_REQ_FIELDS.each do |i| # for each i in the required fields list
      if !self[i].blank?
        valid = true
        break
      end
    end
    # if i is not nil and not == "", it's validly_published
    #if (!self[i].nil?) && (self[i] != '')
    #  return true
    #end
    errors.add(:base, 'no core data provided') if !valid
                      # return false # none of the required fields have a value
  end

#endregion  hard validations

#region Soft_validation_methods
  def sv_has_authors
    if !(has_authors?)
      soft_validations.add(:author, 'There is no author associated with this source.')
    end
  end

  def sv_contains_a_writer # neither author nor editor
    if !has_writer?
      soft_validations.add(:author, 'There is neither an author,nor editor associated with this source.')
      soft_validations.add(:editor, 'There is neither an author,nor editor associated with this source.')
    end

  end

  def sv_has_title
    if (self.title.blank?)
      soft_validations.add(:title, 'There is no title associated with this source.')
    end
  end

  def sv_has_some_type_of_year
    if (!has_some_year?)
      soft_validations.add(:year, 'There is no year or stated year associated with this source.')
      soft_validations.add(:stated_year, 'There is no or stated year year associated with this source.')
    end
  end

  def sv_year_exists
    if (year.blank?)
      soft_validations.add(:year, 'There is no year associated with this source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  def sv_missing_journal
    soft_validations.add(:bibtex_type, 'The source is missing a journal name.') if self.journal.blank?
  end

  def sv_is_article_missing_journal
    if (self.bibtex_type == 'article')
      if (self.journal.blank?)
        soft_validations.add(:bibtex_type, 'The article is missing a journal name.')
      end
    end
  end

  def sv_has_a_publisher
    if (self.publisher.blank?)
      soft_validations.add(:publisher, 'There is no publisher associated with this source.')
    end
  end

  def sv_has_booktitle
    if (self.booktitle.blank?)
      soft_validations.add(:booktitle, 'There is no book title associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:chapter, 'There is neither a chapter nor pages with this source.')
      soft_validations.add(:pages, 'There is neither a chapter nor pages with this source.')
    end
  end

  def sv_has_school
    if (self.school.blank?)
      soft_validations.add(:school, 'There is no school associated with this thesis.')
    end
  end

  def sv_has_institution
    if (self.institution.blank?)
      soft_validations.add(:institution, 'There is not institution associated with this tech report.')
    end
  end

  def sv_has_note
     # TODO we may need to check of a note in the TW sense as well - has_note? above.
     if (self.note.blank?) && (self.notes.count = 0)
       soft_validations.add(:note, 'There is no note associated with this source.')
     end
  end

  def sv_missing_required_bibtex_fields
    case self.bibtex_type
      when 'article' #:article       => [:author,:title,:journal,:year]
        sv_has_authors
        sv_has_title
        sv_is_article_missing_journal
        sv_year_exists
      when 'book' #:book          => [[:author,:editor],:title,:publisher,:year]
        sv_contains_a_writer
        sv_has_title
        sv_has_a_publisher
        sv_year_exists
      when 'booklet' #    :booklet       => [:title],
        sv_has_title
      when 'conference' #    :conference    => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'inbook' #    :inbook        => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year],
        sv_contains_a_writer
        sv_has_title
        sv_is_contained_has_chapter_or_pages
        sv_has_a_publisher
        sv_year_exists
      when 'incollection' #    :incollection  => [:author,:title,:booktitle,:publisher,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_has_a_publisher
        sv_year_exists
      when 'inproceedings' #    :inproceedings => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'manual' #    :manual        => [:title],
        sv_has_title
      when 'mastersthesis' #    :mastersthesis => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      #    :misc          => [],  (no required fields)
      when 'phdthesis' #    :phdthesis     => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      when 'proceedings' #    :proceedings   => [:title,:year],
        sv_has_title
        sv_year_exists
      when 'techreport' #    :techreport    => [:author,:title,:institution,:year],
        sv_has_authors
        sv_has_title
        sv_has_institution
        sv_year_exists
      when 'unpublished' #    :unpublished   => [:author,:title,:note]
        sv_has_authors
        sv_has_title
        sv_has_note
    end
  end

#endregion   Soft_validation_methods

end

#institutionnil

BibTeX standard field (required for types: )(optional for types:) The sponsoring institution of a technical report @return the name of the institution publishing this source

Returns:

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).first.identifier
  end

  def issn=(value)
    write_attribute(:issn, value)
    #TODO if there is already an 'Identifier::Global::Issn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
  end
  def issn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:issn).first.identifier
  end

#TODO if language is set => set language_id
#endregion getters & setters

  # turn bibtex URL field into a Ruby URI object
  def url_as_uri 
    URI(self.url) unless self.url.blank?
  end

#region has_<attribute>? section
  def has_authors? # is there a bibtex author or author roles?
    return true if !(self.author.blank?)
    # author attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.authors.count > 0) ? (return true) : (return false)
  end

  def has_editors?
    return true if !(self.editor.blank?)
    # editor attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.editors.count > 0) ? (return true) : (return false)
  end

  def has_writer? # contains either an author or editor
    (has_authors?) || (has_editors?) ? true : false
  end

  def has_some_year? # is there a year or stated year?
    return true if !(self.year.blank?) || !(self.stated_year.blank?)
    false
  end

#endregion has_<attribute>? section

  #region time/date related
  # @return[Time] alias for nomenclature_date, computed if not yet saved
  def date
    set_nomenclature_date if !self.persisted?
    self.nomenclature_date
  end

  def set_nomenclature_date
    self.nomenclature_date = Utilities::Dates.nomenclature_date(self.day, self.month, self.year) 
  end
  #todo move the test for nomenclature_date to spec/lib/utilities/dates_spec.rb

#endregion    time/date related

  protected

   def set_cached_values

    bx_entry = self.to_bibtex
    if bx_entry.key.blank? then
      bx_entry.key = 'tmpID'
    end
    key = bx_entry.key
    #bx_entry.key = 'tmpID'
    bx_bibliography = BibTeX::Bibliography.new()
    bx_bibliography.add(bx_entry)

    cp = CiteProc::Processor.new(style: 'apa', format: 'text')
     cp.import bx_bibliography.to_citeproc

=begin
    cp << bx_bibliography.to_citeproc
    cp.process(bx_bibliography[self.id].to_citeproc)

    self.cached = CiteProc.process bx_entry.to_citeproc, style: 'apa', format: 'text'
=end

     # format cached = full reference
    self.cached = cp.render(:bibliography, id: key)[0]
    # self.cached = cp.render(:bibliography, id: 'tmpID')[0]
    # format cached_author_srting = either the bibtex author or the last names of all the normalized authors.
     self.cached_author_string = authority_name
  end


#region hard validations

# replaced with inclusion validation
#   def check_bibtex_type # must have a valid bibtex_type
#     errors.add(:bibtex_type, 'not a valid bibtex type') if !::VALID_BIBTEX_TYPES.include?(self.bibtex_type)
#   end

  def check_has_field # must have at least one of the required fields (TW_REQ_FIELDS)
    valid = false
    TW_REQ_FIELDS.each do |i| # for each i in the required fields list
      if !self[i].blank?
        valid = true
        break
      end
    end
    # if i is not nil and not == "", it's validly_published
    #if (!self[i].nil?) && (self[i] != '')
    #  return true
    #end
    errors.add(:base, 'no core data provided') if !valid
                      # return false # none of the required fields have a value
  end

#endregion  hard validations

#region Soft_validation_methods
  def sv_has_authors
    if !(has_authors?)
      soft_validations.add(:author, 'There is no author associated with this source.')
    end
  end

  def sv_contains_a_writer # neither author nor editor
    if !has_writer?
      soft_validations.add(:author, 'There is neither an author,nor editor associated with this source.')
      soft_validations.add(:editor, 'There is neither an author,nor editor associated with this source.')
    end

  end

  def sv_has_title
    if (self.title.blank?)
      soft_validations.add(:title, 'There is no title associated with this source.')
    end
  end

  def sv_has_some_type_of_year
    if (!has_some_year?)
      soft_validations.add(:year, 'There is no year or stated year associated with this source.')
      soft_validations.add(:stated_year, 'There is no or stated year year associated with this source.')
    end
  end

  def sv_year_exists
    if (year.blank?)
      soft_validations.add(:year, 'There is no year associated with this source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  def sv_missing_journal
    soft_validations.add(:bibtex_type, 'The source is missing a journal name.') if self.journal.blank?
  end

  def sv_is_article_missing_journal
    if (self.bibtex_type == 'article')
      if (self.journal.blank?)
        soft_validations.add(:bibtex_type, 'The article is missing a journal name.')
      end
    end
  end

  def sv_has_a_publisher
    if (self.publisher.blank?)
      soft_validations.add(:publisher, 'There is no publisher associated with this source.')
    end
  end

  def sv_has_booktitle
    if (self.booktitle.blank?)
      soft_validations.add(:booktitle, 'There is no book title associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:chapter, 'There is neither a chapter nor pages with this source.')
      soft_validations.add(:pages, 'There is neither a chapter nor pages with this source.')
    end
  end

  def sv_has_school
    if (self.school.blank?)
      soft_validations.add(:school, 'There is no school associated with this thesis.')
    end
  end

  def sv_has_institution
    if (self.institution.blank?)
      soft_validations.add(:institution, 'There is not institution associated with this tech report.')
    end
  end

  def sv_has_note
     # TODO we may need to check of a note in the TW sense as well - has_note? above.
     if (self.note.blank?) && (self.notes.count = 0)
       soft_validations.add(:note, 'There is no note associated with this source.')
     end
  end

  def sv_missing_required_bibtex_fields
    case self.bibtex_type
      when 'article' #:article       => [:author,:title,:journal,:year]
        sv_has_authors
        sv_has_title
        sv_is_article_missing_journal
        sv_year_exists
      when 'book' #:book          => [[:author,:editor],:title,:publisher,:year]
        sv_contains_a_writer
        sv_has_title
        sv_has_a_publisher
        sv_year_exists
      when 'booklet' #    :booklet       => [:title],
        sv_has_title
      when 'conference' #    :conference    => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'inbook' #    :inbook        => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year],
        sv_contains_a_writer
        sv_has_title
        sv_is_contained_has_chapter_or_pages
        sv_has_a_publisher
        sv_year_exists
      when 'incollection' #    :incollection  => [:author,:title,:booktitle,:publisher,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_has_a_publisher
        sv_year_exists
      when 'inproceedings' #    :inproceedings => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'manual' #    :manual        => [:title],
        sv_has_title
      when 'mastersthesis' #    :mastersthesis => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      #    :misc          => [],  (no required fields)
      when 'phdthesis' #    :phdthesis     => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      when 'proceedings' #    :proceedings   => [:title,:year],
        sv_has_title
        sv_year_exists
      when 'techreport' #    :techreport    => [:author,:title,:institution,:year],
        sv_has_authors
        sv_has_title
        sv_has_institution
        sv_year_exists
      when 'unpublished' #    :unpublished   => [:author,:title,:note]
        sv_has_authors
        sv_has_title
        sv_has_note
    end
  end

#endregion   Soft_validation_methods

end

#ISBNObject

Used by bibtex-ruby gem method identifier



# File 'app/models/source/bibtex.rb', line 207

#ISSNObject

Used by bibtex-ruby gem method identifier



# File 'app/models/source/bibtex.rb', line 207

#journalnil

BibTeX standard field (required for types: )(optional for types:) A TW required attribute (TW requires a value in one of the required attributes.) A journal name. Many BibTeX processors have standardized abbreviations for many journals which would be listed in your local BibTeX processor guide. Once this attribute has been normalized against TW Serials, this attribute will contain the full journal name as defined by the Serial object. If you want a preferred abbreviation associated with with this journal, add the abbreviation the serial object. @return the name of the journal (serial) associated with this source

Returns:

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).first.identifier
  end

  def issn=(value)
    write_attribute(:issn, value)
    #TODO if there is already an 'Identifier::Global::Issn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
  end
  def issn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:issn).first.identifier
  end

#TODO if language is set => set language_id
#endregion getters & setters

  # turn bibtex URL field into a Ruby URI object
  def url_as_uri 
    URI(self.url) unless self.url.blank?
  end

#region has_<attribute>? section
  def has_authors? # is there a bibtex author or author roles?
    return true if !(self.author.blank?)
    # author attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.authors.count > 0) ? (return true) : (return false)
  end

  def has_editors?
    return true if !(self.editor.blank?)
    # editor attribute is empty
    return false if self.new_record?
    # self exists in the db
    (self.editors.count > 0) ? (return true) : (return false)
  end

  def has_writer? # contains either an author or editor
    (has_authors?) || (has_editors?) ? true : false
  end

  def has_some_year? # is there a year or stated year?
    return true if !(self.year.blank?) || !(self.stated_year.blank?)
    false
  end

#endregion has_<attribute>? section

  #region time/date related
  # @return[Time] alias for nomenclature_date, computed if not yet saved
  def date
    set_nomenclature_date if !self.persisted?
    self.nomenclature_date
  end

  def set_nomenclature_date
    self.nomenclature_date = Utilities::Dates.nomenclature_date(self.day, self.month, self.year) 
  end
  #todo move the test for nomenclature_date to spec/lib/utilities/dates_spec.rb

#endregion    time/date related

  protected

   def set_cached_values

    bx_entry = self.to_bibtex
    if bx_entry.key.blank? then
      bx_entry.key = 'tmpID'
    end
    key = bx_entry.key
    #bx_entry.key = 'tmpID'
    bx_bibliography = BibTeX::Bibliography.new()
    bx_bibliography.add(bx_entry)

    cp = CiteProc::Processor.new(style: 'apa', format: 'text')
     cp.import bx_bibliography.to_citeproc

=begin
    cp << bx_bibliography.to_citeproc
    cp.process(bx_bibliography[self.id].to_citeproc)

    self.cached = CiteProc.process bx_entry.to_citeproc, style: 'apa', format: 'text'
=end

     # format cached = full reference
    self.cached = cp.render(:bibliography, id: key)[0]
    # self.cached = cp.render(:bibliography, id: 'tmpID')[0]
    # format cached_author_srting = either the bibtex author or the last names of all the normalized authors.
     self.cached_author_string = authority_name
  end


#region hard validations

# replaced with inclusion validation
#   def check_bibtex_type # must have a valid bibtex_type
#     errors.add(:bibtex_type, 'not a valid bibtex type') if !::VALID_BIBTEX_TYPES.include?(self.bibtex_type)
#   end

  def check_has_field # must have at least one of the required fields (TW_REQ_FIELDS)
    valid = false
    TW_REQ_FIELDS.each do |i| # for each i in the required fields list
      if !self[i].blank?
        valid = true
        break
      end
    end
    # if i is not nil and not == "", it's validly_published
    #if (!self[i].nil?) && (self[i] != '')
    #  return true
    #end
    errors.add(:base, 'no core data provided') if !valid
                      # return false # none of the required fields have a value
  end

#endregion  hard validations

#region Soft_validation_methods
  def sv_has_authors
    if !(has_authors?)
      soft_validations.add(:author, 'There is no author associated with this source.')
    end
  end

  def sv_contains_a_writer # neither author nor editor
    if !has_writer?
      soft_validations.add(:author, 'There is neither an author,nor editor associated with this source.')
      soft_validations.add(:editor, 'There is neither an author,nor editor associated with this source.')
    end

  end

  def sv_has_title
    if (self.title.blank?)
      soft_validations.add(:title, 'There is no title associated with this source.')
    end
  end

  def sv_has_some_type_of_year
    if (!has_some_year?)
      soft_validations.add(:year, 'There is no year or stated year associated with this source.')
      soft_validations.add(:stated_year, 'There is no or stated year year associated with this source.')
    end
  end

  def sv_year_exists
    if (year.blank?)
      soft_validations.add(:year, 'There is no year associated with this source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  def sv_missing_journal
    soft_validations.add(:bibtex_type, 'The source is missing a journal name.') if self.journal.blank?
  end

  def sv_is_article_missing_journal
    if (self.bibtex_type == 'article')
      if (self.journal.blank?)
        soft_validations.add(:bibtex_type, 'The article is missing a journal name.')
      end
    end
  end

  def sv_has_a_publisher
    if (self.publisher.blank?)
      soft_validations.add(:publisher, 'There is no publisher associated with this source.')
    end
  end

  def sv_has_booktitle
    if (self.booktitle.blank?)
      soft_validations.add(:booktitle, 'There is no book title associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:chapter, 'There is neither a chapter nor pages with this source.')
      soft_validations.add(:pages, 'There is neither a chapter nor pages with this source.')
    end
  end

  def sv_has_school
    if (self.school.blank?)
      soft_validations.add(:school, 'There is no school associated with this thesis.')
    end
  end

  def sv_has_institution
    if (self.institution.blank?)
      soft_validations.add(:institution, 'There is not institution associated with this tech report.')
    end
  end

  def sv_has_note
     # TODO we may need to check of a note in the TW sense as well - has_note? above.
     if (self.note.blank?) && (self.notes.count = 0)
       soft_validations.add(:note, 'There is no note associated with this source.')
     end
  end

  def sv_missing_required_bibtex_fields
    case self.bibtex_type
      when 'article' #:article       => [:author,:title,:journal,:year]
        sv_has_authors
        sv_has_title
        sv_is_article_missing_journal
        sv_year_exists
      when 'book' #:book          => [[:author,:editor],:title,:publisher,:year]
        sv_contains_a_writer
        sv_has_title
        sv_has_a_publisher
        sv_year_exists
      when 'booklet' #    :booklet       => [:title],
        sv_has_title
      when 'conference' #    :conference    => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'inbook' #    :inbook        => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year],
        sv_contains_a_writer
        sv_has_title
        sv_is_contained_has_chapter_or_pages
        sv_has_a_publisher
        sv_year_exists
      when 'incollection' #    :incollection  => [:author,:title,:booktitle,:publisher,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_has_a_publisher
        sv_year_exists
      when 'inproceedings' #    :inproceedings => [:author,:title,:booktitle,:year],
        sv_has_authors
        sv_has_title
        sv_has_booktitle
        sv_year_exists
      when 'manual' #    :manual        => [:title],
        sv_has_title
      when 'mastersthesis' #    :mastersthesis => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      #    :misc          => [],  (no required fields)
      when 'phdthesis' #    :phdthesis     => [:author,:title,:school,:year],
        sv_has_authors
        sv_has_title
        sv_has_school
        sv_year_exists
      when 'proceedings' #    :proceedings   => [:title,:year],
        sv_has_title
        sv_year_exists
      when 'techreport' #    :techreport    => [:author,:title,:institution,:year],
        sv_has_authors
        sv_has_title
        sv_has_institution
        sv_year_exists
      when 'unpublished' #    :unpublished   => [:author,:title,:note]
        sv_has_authors
        sv_has_title
        sv_has_note
    end
  end

#endregion   Soft_validation_methods

end

#keynil

BibTeX standard field (may be used in a bibliography for alphabetizing & cross referencing) Used by bibtex-ruby gem method identifier as a default value when no other identifier is present. Used for alphabetizing, cross referencing, and creating a label when the “author” information is missing. This field should not be confused with the key that appears in the cite (BibTeX/LaTeX)command and at the beginning of the bibliography entry.

This attribute is only set (and saved) during the import process. It may be generated for output when a bibtex-ruby bibliography is created, but is unlikely to be save to the db. @return the key of this source

Returns:

  • (nil)

    means the attribute is not stored in the database.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'app/models/source/bibtex.rb', line 204

class Source::Bibtex < Source
  include SoftValidation

#  include Shared::Notable in source.rb
#
# @!attribute publisher
# @!attribute school
# @!attribute series
# @!attribute title
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute type
# @!attribute translator - not yet implemented
#   bibtex-ruby gem supports translator, it's not clear whether TW will or not.
# @!attribute volume
# @!attribute year
#   A TW required attribute (TW requires a value in one of the required attributes.)
#   Year must be between 1000 and now + 2 years inclusive
# @!attribute URL
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute doi - not implemented yet
#   Used by bibtex-ruby gem method identifier
# @!attribute ISBN
#   Used by bibtex-ruby gem method identifier
# @!attribute ISSN
#   Used by bibtex-ruby gem method identifier
# @!attribute LCCN
# @!attribute abstract
# @!attribute keywords
# @!attribute price
# @!attribute copyright
# @!attribute language
# @!attribute contents
# @!attribute stated_year
#   A TW required attribute (TW requires a value in one of the required attributes.)
# @!attribute verbatim
# @!attribute cached
# @!attribute cached_author_string
# @!attribute created_at
# @!attribute created_by - not yet implemented
# @!attribute updated_at
# @!attribute updated_by - not yet implemented
# @!attribute bibtex_type
# @!attribute day
#   If day is present there must be a month and day must be valid for the month.
#
# @!group associations
# @!endgroup
# @!group identifiers
# @!endgroup
# 
  # TODO add linkage to serials ==> belongs_to serial
  # TODO :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }
  has_many :author_roles, class_name: 'SourceAuthor', as: :role_object
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, class_name: 'SourceEditor', as: :role_object # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person

#region validations
  # TODO: refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_inclusion_of :bibtex_type,
    in: ::VALID_BIBTEX_TYPES,
    message: '%{value} is not a valid source type'
  validates_presence_of :year,
    if: '!month.nil?',
    message: 'year is required when month is provided'
  validates_numericality_of :year,
    only_integer: true, greater_than: 999,
    less_than_or_equal_to: Time.now.year + 2,
    allow_nil: true,
    message: 'year must be an integer greater than 999 and no more than 2 years in the future'
  validates_presence_of :month, 
    if: '!day.nil?',
    message: 'month is required when day is provided'
  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_nil: true,
    message: ' month'
  validates_numericality_of :day,
    allow_nil: true,
    only_integer: true,
    greater_than: 0,
    less_than_or_equal_to: Proc.new { |a| Time.utc(a.year, a.month).end_of_month.day },
    :unless => 'year.nil? || month.nil?',
    message: '%{value} is not a valid day for the month provided'

#  validates :url, :format => /\A#{URI::regexp}\z/, allow_nil: true  # this line is essentially the same as below
  # but isn't as clear. Note that both validations allow multiple urls strung together with a ',' provided
  # no spaces are included.
  validates :url, :format => { :with => URI::regexp(%w(http https ftp)),
            message: "[%{value}] is not a valid URL"}, allow_nil: true

  before_validation :check_has_field
  before_save :set_nomenclature_date, :set_cached_values

#endregion validations

  # nil is last by default, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:nomenclature_date) } 

#region soft_validate setup calls

  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_contains_a_writer, set: :recommended_fields)
  soft_validate(:sv_has_title, set: :recommended_fields)
  soft_validate(:sv_has_some_type_of_year, set: :recommended_fields)
  soft_validate(:sv_is_article_missing_journal, set: :recommended_fields)
#  soft_validate(:sv_has_url, set: :recommended_fields) # probably should be sv_has_identifier instead of sv_has_url
  soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields)

#endregion

#region constants
# TW required fields (must have one of these fields filled in)
  TW_REQ_FIELDS = [
      :author,
      :editor,
      :booktitle,
      :title,
      :url,
      :journal,
      :year,
      :stated_year
  ] # either year or stated_year is acceptable
#endregion

 accepts_nested_attributes_for :notes

 #region ruby-bibtex related

  def to_bibtex # outputs BibTeX::Entry equivalent to me.
    b = BibTeX::Entry.new(type: self.bibtex_type)
    ::BIBTEX_FIELDS.each do |f|
      if !(f == :bibtex_type) && (!self[f].blank?)
        b[f] = self.send(f)
      end
    end
    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    # TODO add conversion of identifiers to ruby-bibtex fields, & notations to notes field.

    b.key = self.id
    b
  end

  def valid_bibtex?
    self.to_bibtex.valid?
  end

  def self.new_from_bibtex(bibtex_entry)
# TODO On input, convert ruby-bibtex.url to an identifier & ruby-bibtex.note to a notation
    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    bibtex_entry.fields.each do |key, value|
      v = value.to_s.strip
      s.send("#{key}=", v) # = v
    end
    s
  end

  # @return[String] A string that represents the year with suffix as seen in a BibTeX bibliography.
  #   returns "" if neither :year or :year_suffix are set.
  def year_with_suffix
    self[:year].to_s + self[:year_suffix].to_s
  end

  def create_related_people
    return false if !self.valid? ||
        self.new_record? ||
        (self.author.blank? && self.editor.blank?) ||
        self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names
    bibtex.names.each do |a|
      p = Source::Bibtex.bibtex_author_to_person(a) # p is a TW person

      # TODO: These are required in present FactoryGirl tests, but not in production,
      # factor out when FactoryGirl + Housekeeping issues are resolved.
      p.creator = self.creator
      p.updater = self.updater

      if bibtex.author
        self.authors << p if bibtex.author.include?(a)
      end
      if bibtex.editor
        self.editors << p if bibtex.editor.include?(a)
      end
    end
    return true
  end

  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    Person.new(
        first_name: bibtex_author.first,
        prefix:     bibtex_author.prefix,
        last_name:  bibtex_author.last,
        suffix:     bibtex_author.suffix)
  end

#endregion  ruby-bibtex related

#region getters & setters
  def authority_name
    # TODO need to use full last name with suffix not just last_name
    case  self.authors.count
      when 0
        return ((self.author.blank?) ? '' : self.author)
        #return self.author # return author or ''
      when 1
        return (authors[0].last_name)
      else
        # authors[0..-2].join(", ") + " & #{authors.last.last_name}"
        p_array = Array.new
        for i in 0..(self.authors.count-1) do
          p_array.push(self.authors[i].last_name)
        end
        p_array.to_sentence(:last_word_connector =>' & ')
    end
  end


  def year=(value)
    if value.class == String
      value =~ /\A(\d\d\d\d)([a-zA-Z]*)\z/
      write_attribute(:year, $1.to_i) if $1
      write_attribute(:year_suffix, $2) if $2
      write_attribute(:year, value) if self.year.blank?
    else
      write_attribute(:year, value) 
    end
  end

  def month=(value)
    v = Utilities::Dates::SHORT_MONTH_FILTER[value]
    v = v.to_s if !v.nil?
    write_attribute(:month, v)
  end

  def note=(value)
    write_attribute(:note, value)
    self.notes.build({text: value + ' [Created on import from BibTeX.]'} ) if self.new_record?
  end 

  def isbn=(value)
    write_attribute(:isbn, value)
    #TODO if there is already an 'Identifier::Global::Isbn' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
  end
  def isbn
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:isbn).first.identifier
  end

  def doi=(value)
    write_attribute(:doi, value)
    #TODO if there is already an 'Identifier::Global::Doi' update instead of add
    self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
  end
  def doi
    # This relies on the identifier class to enforce a single version of any identifier
    self.identifiers.of_type(:doi).fir