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.

Cached values are formatted using the 'zootaxa' style from 'csl/styles'

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_REQUIRED_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.

Dates in Source Bibtex:

It is common for there two be two (or more) dates associated with the origin of a source:
  1) If you only have reference to a single value, it goes in year (month, day)
  2) If you have reference to two year values, the actual year of publication goes in year, and the stated year of publication goes in stated_year.
  3) If you have month or day publication, they go in month or day.

We do not track stated_month or stated_day if they are present in addition to actual month and actual day.

Bibtex has month.

Bibtex does not have day.

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

Author:

Constant Summary

TW_REQUIRED_FIELDS =

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

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

Constants included from SoftValidation

SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS

Constants inherited from Source

ALTERNATE_VALUES_FOR

Instance Attribute Summary (collapse)

Attributes inherited from Source

#serial_id

Attributes included from Housekeeping::Users

#by

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 inherited from Source

batch_create, batch_preview, #cited_objects, find_for_autocomplete, generate_download, #is_bibtex?, #nearest_by_levenshtein, new_from_citation, new_from_doi, #reject_project_sources

Methods included from Housekeeping::Timestamps

#data_breakdown_for_chartkick_recent

Methods included from Housekeeping::Users

#set_created_by_id, #set_updated_by_id

Instance Attribute Details

- (String) abstract

TODO:

Returns:

  • (String)


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
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'app/models/source/bibtex.rb', line 312

class Source::Bibtex < Source
  include SoftValidation

  attr_accessor :authors_to_create

  # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }

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

  belongs_to :serial, inverse_of: :sources
  belongs_to :source_language, class_name: "Language", foreign_key: :language_id, inverse_of: :sources
  # above to handle clash with bibtex language field.

  has_many :author_roles, -> { order('roles.position ASC') }, class_name: 'SourceAuthor', as: :role_object, validate: true
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person, validate: true # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor', as: :role_object, validate: true # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true
  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, allow_destroy: true

  before_validation :create_authors, if: '!authors_to_create.nil?'
  before_validation :check_has_field
  before_save :set_cached_nomenclature_date

  #region validations
  validates_inclusion_of :bibtex_type,
                         in:      ::VALID_BIBTEX_TYPES,
                         message: '%{value} is not a valid source type'

  validates_presence_of :year,
                        if:      '!month.blank? || !stated_year.blank?',
                        message: 'year is required when month or stated_year is provided'

  # @todo refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_numericality_of :year,
                            only_integer:          true, greater_than: 999,
                            less_than_or_equal_to: Time.now.year + 2,
                            allow_blank:           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_blank: true,
                         message:     ' month'

  validates_numericality_of :day,
                            allow_blank:           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 => {:with    => URI::regexp(%w(http https ftp)),
                              message: "[%{value}] is not a valid URL"}, allow_blank: true

  #endregion validations

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_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_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 ruby-bibtex related

  def journal
    [read_attribute(:journal), (self.serial.blank? ? nil : self.serial.name)].compact.first
  end

  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [BibTeX::Entry]
  #   entry equivalent to self
  def to_bibtex
    b = BibTeX::Entry.new(:bibtex_type => self[:bibtex_type])
    ::BIBTEX_FIELDS.each do |f|
      if (!self.send(f).blank?) && !(f == :bibtex_type)
        b[f] = self.send(f)
      end
    end

    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    unless self.verbatim_keywords.blank?
      b[:keywords] = self.verbatim_keywords
    end

    b[:note] = concatenated_notes_string if !concatenated_notes_string.blank? # see Notable
    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = self.identifiers.of_type(:uri)
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = self.identifiers.of_type(:isbn)
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = self.identifiers.of_type(:doi)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    b.author = self.compute_bibtex_names('author') unless (!self.authors.any? && self.author.blank?)
    b.editor = self.compute_bibtex_names('editor') unless (!self.editors.any? && self.editor.blank?)

    b.key    = self.id unless self.new_record? # id.blank?
    b
  end

  # @param type [String] either author or editor
  # @return [String]
  #   the bibtex version of the name strings created from the TW people
  #   BibTeX format is 'lastname, firstname and lastname,firstname and lastname, firstname'
  #   For a name list not joined by multiple 'and's, use compute_human_names
  def compute_bibtex_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.bibtex_name
      else
        return self.send(methods).collect { |a| a.bibtex_name }.join(' and ')
    end
  end

  # @param type [String] either author or editor
  # @return[String]
  #   A human readable version of the person list
  #   'firstname lastname, firstname lastname, & firstname lastname'
  def compute_human_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.name
      else
        return self.send(methods).collect { |a| a.name }.to_sentence(last_word_connector: ' & ')
    end
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    self.to_bibtex.valid?
  end

  # Instantiates a Source::Bibtex instance from a BibTeX::Entry
  # Note: note conversion is handled in note setter.
  #       identifiers are handled in associated setter.
  # !! Unrecognized attributes are added as import attributes.
  #
  # Usage:
  #    a = BibTeX::Entry.new(bibtex_type: 'book', title: 'Foos of Bar America', author: 'Smith, James', year: 1921)
  #    b = Source::Bibtex.new(a)
  # 
  # @param bibtex_entry [BibTex::Entry] the BibTex::Entry to convert 
  # @return [Source::BibTex.new] a new instance 
  # @todo annote to project specific note?
  # @todo if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title
  # serial with alternate_value on name .count = 1 assign .first
  # before validate assign serial if matching & not doesn't have a serial currently assigned.
  # @todo if there is an ISSN it should look up to see it the serial already exists.
  def self.new_from_bibtex(bibtex_entry = nil)

    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s                 = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    import_attributes = []
    bibtex_entry.fields.each do |key, value|
      if key == :keywords
        s.verbatim_keywords = value
        next
      end
      v = value.to_s.strip
      if s.respond_to?(key.to_sym)
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end
    s.data_attributes_attributes = import_attributes
    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

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

  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now
  def create_related_people_and_roles
    return false if !self.valid? ||
      self.new_record? ||
      (self.author.blank? && self.editor.blank?) ||
      self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names

    begin
      Role.transaction do
        unless bibtex.authors.blank?
          bibtex.authors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceAuthor.create!(role_object: self, person: p)
          end
        end

        if !bibtex.editors.blank?
          bibtex.editors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceEditor.create!(role_object: self, person: p)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      raise
    end
    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

  # @todo create related Serials

  #endregion ruby-bibtex related

  #region getters & setters

  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

  # Used only on import from BibTeX records
  def note=(value)
    write_attribute(:note, value)
    if !self.note.blank? && self.new_record?
      if value.include?('||')
        a = value.split(/||/)
        a.each do |n|
          self.notes.build({text: n + ' [Created on import from BibTeX.]'})
        end
      else
        self.notes.build({text: value + ' [Created on import from BibTeX.]'})
      end
    end
  end

  # @return [String]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized people records before bibtex author string
  def authority_name
    if self.authors.count == 0 # no normalized people, use string, !! not .any? because of in-memory setting?!
      if self.author.blank?
        return ('')
      else
        b = self.to_bibtex
        b.parse_names
        return b.author.tokens.collect{ |t| t.last }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
      end
    else # use normalized records 
      return self.authors.collect{ |a| a.full_last_name }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
    end
  end

  def isbn=(value)
    write_attribute(:isbn, value)
    unless value.blank?
      if tw_isbn = self.identifiers.where(type: 'Identifier::Global::Isbn').first
        if tw_isbn.identifier != value
          tw_isbn.destroy
          self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
      end
    end
  end

  def isbn
    identifier_string_of_type(:isbn)
  end

  def doi=(value)
    write_attribute(:doi, value)
    unless value.blank?
      if tw_doi = self.identifiers.where(type: 'Identifier::Global::Doi').first
        if tw_doi.identifier != value
          tw_doi.destroy
          self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
      end
    end
  end

  def doi
    identifier_string_of_type(:doi)
  end

  # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case
  # we need to set the serial based on ISSN.
  def issn=(value)
    write_attribute(:issn, value)
    unless value.blank?
      tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first
      unless tw_issn.nil? || tw_issn.identifier != value
        tw_issn.destroy
      end
      self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
    end
  end

  def issn
    identifier_string_of_type(:issn)
  end

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

  # @return [Identifier]
  #   the identifier of this type, relies on Identifier to enforce has_one for Global identifiers,
  #   !! behaviour for Identifier::Local types may be unexpected
  def identifier_string_of_type(type)
    identifiers.of_type(type).first.try(:identifier)
  end

  # @todo if language is set => set language_id
  # def language=(value)
  #
  # end
  #endregion getters & setters

  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? # nothing saved yet, so no author roles are saved yet
    # 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

  #region time/date related

  # @return [Date] 
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def date
    set_cached_nomenclature_date if !self.persisted?
    self.cached_nomenclature_date
  end

  # @return [Integer]
  #  The effective year of publication as per nomenclatural rules
  def nomenclature_year
    date.year if date
  end

  def set_cached_nomenclature_date
    self.cached_nomenclature_date = Utilities::Dates.nomenclature_date(
      self.day,
      Utilities::Dates.month_index(self.month), # this allows values from bibtex like 'may' to be handled 
      self.year
    )
  end

  #endregion    time/date related


  # @return [BibTex::Bibliography]
  #   initialized with this source as an entry
  def bibtex_bibliography
    bx_entry = to_bibtex
    bx_entry.year = '0000' if bx_entry.year.blank? # cludge to fix render problem with year
    b = BibTeX::Bibliography.new
    b.add(bx_entry)
    b
  end

  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text')
    cp = CiteProc::Processor.new(style: style, format: format) # There is a problem with the zootaxa format and letters!
    cp.import(bibtex_bibliography.to_citeproc)
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end 

  # @return [String]
  #   a full representation, using bibtex
  # String must be length > 0
  def cached_string(format = 'text')
    return nil  unless (format == 'text') || (format == 'html')
    str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant
    str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  protected

  def create_authors
    begin
      Person.transaction do
        authors_to_create.each do |shs|
          p = Person.create!(shs)
          self.author_roles.build(person: p)
        end
      end
    rescue
      errors.add(:base, 'invalid author parameters')
    end
  end

  # set cached values and copies active record relations into bibtex values
  def set_cached
    if self.errors.empty?
      tmp                       = cached_string('text')
      self.cached               = tmp
      self.cached_author_string = authority_name

      if self.authors.size > 0
        self.author = self.compute_bibtex_names('author')
      end

      if self.editors.size > 0
        self.editor = self.compute_bibtex_names('editor')
      end
    end
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  def check_has_field
    valid = false
    TW_REQUIRED_FIELDS.each do |i|
      if !self[i].blank?
        valid = true
        break
      end
    end
    #TODO This test for auth doesn't work with a new record.
    if (self.authors.count > 0 || self.editors.count > 0 || !self.serial.nil?)
      valid = true
    end
    if !valid
      errors.add(:base,
                 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year'
      )
    end
  end

  #endregion  hard validations

  #region Soft_validation_methods
  def sv_has_authors
    # only used in validating BibTeX output
    if !(has_authors?)
      soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.')
    end
  end

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

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

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

  def sv_year_exists
    # only used in validating BibTeX output
    if year.blank?
      soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  # def sv_missing_journal
  # never used
  #   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? and self.serial.blank?
        soft_validations.add(:bibtex_type, 'This article is missing a journal name or serial.')
      end
    end
  end

  def sv_has_a_publisher
    if self.publisher.blank?
      soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.')
    end
  end

  def sv_has_booktitle
    if self.booktitle.blank?
      soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')

      #  soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
      # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
    end
  end

  def sv_has_school
    if self.school.blank?
      soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.')
    end
  end

  def sv_has_institution
    if self.institution.blank?
      soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.')
    end
  end

  def sv_has_note
    if (self.note.blank?) && (!self.notes.any?)
      soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished 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

- (#String?) address

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.



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
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'app/models/source/bibtex.rb', line 312

class Source::Bibtex < Source
  include SoftValidation

  attr_accessor :authors_to_create

  # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }

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

  belongs_to :serial, inverse_of: :sources
  belongs_to :source_language, class_name: "Language", foreign_key: :language_id, inverse_of: :sources
  # above to handle clash with bibtex language field.

  has_many :author_roles, -> { order('roles.position ASC') }, class_name: 'SourceAuthor', as: :role_object, validate: true
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person, validate: true # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor', as: :role_object, validate: true # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true
  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, allow_destroy: true

  before_validation :create_authors, if: '!authors_to_create.nil?'
  before_validation :check_has_field
  before_save :set_cached_nomenclature_date

  #region validations
  validates_inclusion_of :bibtex_type,
                         in:      ::VALID_BIBTEX_TYPES,
                         message: '%{value} is not a valid source type'

  validates_presence_of :year,
                        if:      '!month.blank? || !stated_year.blank?',
                        message: 'year is required when month or stated_year is provided'

  # @todo refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_numericality_of :year,
                            only_integer:          true, greater_than: 999,
                            less_than_or_equal_to: Time.now.year + 2,
                            allow_blank:           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_blank: true,
                         message:     ' month'

  validates_numericality_of :day,
                            allow_blank:           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 => {:with    => URI::regexp(%w(http https ftp)),
                              message: "[%{value}] is not a valid URL"}, allow_blank: true

  #endregion validations

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_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_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 ruby-bibtex related

  def journal
    [read_attribute(:journal), (self.serial.blank? ? nil : self.serial.name)].compact.first
  end

  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [BibTeX::Entry]
  #   entry equivalent to self
  def to_bibtex
    b = BibTeX::Entry.new(:bibtex_type => self[:bibtex_type])
    ::BIBTEX_FIELDS.each do |f|
      if (!self.send(f).blank?) && !(f == :bibtex_type)
        b[f] = self.send(f)
      end
    end

    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    unless self.verbatim_keywords.blank?
      b[:keywords] = self.verbatim_keywords
    end

    b[:note] = concatenated_notes_string if !concatenated_notes_string.blank? # see Notable
    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = self.identifiers.of_type(:uri)
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = self.identifiers.of_type(:isbn)
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = self.identifiers.of_type(:doi)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    b.author = self.compute_bibtex_names('author') unless (!self.authors.any? && self.author.blank?)
    b.editor = self.compute_bibtex_names('editor') unless (!self.editors.any? && self.editor.blank?)

    b.key    = self.id unless self.new_record? # id.blank?
    b
  end

  # @param type [String] either author or editor
  # @return [String]
  #   the bibtex version of the name strings created from the TW people
  #   BibTeX format is 'lastname, firstname and lastname,firstname and lastname, firstname'
  #   For a name list not joined by multiple 'and's, use compute_human_names
  def compute_bibtex_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.bibtex_name
      else
        return self.send(methods).collect { |a| a.bibtex_name }.join(' and ')
    end
  end

  # @param type [String] either author or editor
  # @return[String]
  #   A human readable version of the person list
  #   'firstname lastname, firstname lastname, & firstname lastname'
  def compute_human_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.name
      else
        return self.send(methods).collect { |a| a.name }.to_sentence(last_word_connector: ' & ')
    end
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    self.to_bibtex.valid?
  end

  # Instantiates a Source::Bibtex instance from a BibTeX::Entry
  # Note: note conversion is handled in note setter.
  #       identifiers are handled in associated setter.
  # !! Unrecognized attributes are added as import attributes.
  #
  # Usage:
  #    a = BibTeX::Entry.new(bibtex_type: 'book', title: 'Foos of Bar America', author: 'Smith, James', year: 1921)
  #    b = Source::Bibtex.new(a)
  # 
  # @param bibtex_entry [BibTex::Entry] the BibTex::Entry to convert 
  # @return [Source::BibTex.new] a new instance 
  # @todo annote to project specific note?
  # @todo if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title
  # serial with alternate_value on name .count = 1 assign .first
  # before validate assign serial if matching & not doesn't have a serial currently assigned.
  # @todo if there is an ISSN it should look up to see it the serial already exists.
  def self.new_from_bibtex(bibtex_entry = nil)

    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s                 = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    import_attributes = []
    bibtex_entry.fields.each do |key, value|
      if key == :keywords
        s.verbatim_keywords = value
        next
      end
      v = value.to_s.strip
      if s.respond_to?(key.to_sym)
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end
    s.data_attributes_attributes = import_attributes
    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

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

  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now
  def create_related_people_and_roles
    return false if !self.valid? ||
      self.new_record? ||
      (self.author.blank? && self.editor.blank?) ||
      self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names

    begin
      Role.transaction do
        unless bibtex.authors.blank?
          bibtex.authors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceAuthor.create!(role_object: self, person: p)
          end
        end

        if !bibtex.editors.blank?
          bibtex.editors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceEditor.create!(role_object: self, person: p)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      raise
    end
    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

  # @todo create related Serials

  #endregion ruby-bibtex related

  #region getters & setters

  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

  # Used only on import from BibTeX records
  def note=(value)
    write_attribute(:note, value)
    if !self.note.blank? && self.new_record?
      if value.include?('||')
        a = value.split(/||/)
        a.each do |n|
          self.notes.build({text: n + ' [Created on import from BibTeX.]'})
        end
      else
        self.notes.build({text: value + ' [Created on import from BibTeX.]'})
      end
    end
  end

  # @return [String]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized people records before bibtex author string
  def authority_name
    if self.authors.count == 0 # no normalized people, use string, !! not .any? because of in-memory setting?!
      if self.author.blank?
        return ('')
      else
        b = self.to_bibtex
        b.parse_names
        return b.author.tokens.collect{ |t| t.last }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
      end
    else # use normalized records 
      return self.authors.collect{ |a| a.full_last_name }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
    end
  end

  def isbn=(value)
    write_attribute(:isbn, value)
    unless value.blank?
      if tw_isbn = self.identifiers.where(type: 'Identifier::Global::Isbn').first
        if tw_isbn.identifier != value
          tw_isbn.destroy
          self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
      end
    end
  end

  def isbn
    identifier_string_of_type(:isbn)
  end

  def doi=(value)
    write_attribute(:doi, value)
    unless value.blank?
      if tw_doi = self.identifiers.where(type: 'Identifier::Global::Doi').first
        if tw_doi.identifier != value
          tw_doi.destroy
          self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
      end
    end
  end

  def doi
    identifier_string_of_type(:doi)
  end

  # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case
  # we need to set the serial based on ISSN.
  def issn=(value)
    write_attribute(:issn, value)
    unless value.blank?
      tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first
      unless tw_issn.nil? || tw_issn.identifier != value
        tw_issn.destroy
      end
      self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
    end
  end

  def issn
    identifier_string_of_type(:issn)
  end

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

  # @return [Identifier]
  #   the identifier of this type, relies on Identifier to enforce has_one for Global identifiers,
  #   !! behaviour for Identifier::Local types may be unexpected
  def identifier_string_of_type(type)
    identifiers.of_type(type).first.try(:identifier)
  end

  # @todo if language is set => set language_id
  # def language=(value)
  #
  # end
  #endregion getters & setters

  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? # nothing saved yet, so no author roles are saved yet
    # 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

  #region time/date related

  # @return [Date] 
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def date
    set_cached_nomenclature_date if !self.persisted?
    self.cached_nomenclature_date
  end

  # @return [Integer]
  #  The effective year of publication as per nomenclatural rules
  def nomenclature_year
    date.year if date
  end

  def set_cached_nomenclature_date
    self.cached_nomenclature_date = Utilities::Dates.nomenclature_date(
      self.day,
      Utilities::Dates.month_index(self.month), # this allows values from bibtex like 'may' to be handled 
      self.year
    )
  end

  #endregion    time/date related


  # @return [BibTex::Bibliography]
  #   initialized with this source as an entry
  def bibtex_bibliography
    bx_entry = to_bibtex
    bx_entry.year = '0000' if bx_entry.year.blank? # cludge to fix render problem with year
    b = BibTeX::Bibliography.new
    b.add(bx_entry)
    b
  end

  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text')
    cp = CiteProc::Processor.new(style: style, format: format) # There is a problem with the zootaxa format and letters!
    cp.import(bibtex_bibliography.to_citeproc)
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end 

  # @return [String]
  #   a full representation, using bibtex
  # String must be length > 0
  def cached_string(format = 'text')
    return nil  unless (format == 'text') || (format == 'html')
    str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant
    str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  protected

  def create_authors
    begin
      Person.transaction do
        authors_to_create.each do |shs|
          p = Person.create!(shs)
          self.author_roles.build(person: p)
        end
      end
    rescue
      errors.add(:base, 'invalid author parameters')
    end
  end

  # set cached values and copies active record relations into bibtex values
  def set_cached
    if self.errors.empty?
      tmp                       = cached_string('text')
      self.cached               = tmp
      self.cached_author_string = authority_name

      if self.authors.size > 0
        self.author = self.compute_bibtex_names('author')
      end

      if self.editors.size > 0
        self.editor = self.compute_bibtex_names('editor')
      end
    end
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  def check_has_field
    valid = false
    TW_REQUIRED_FIELDS.each do |i|
      if !self[i].blank?
        valid = true
        break
      end
    end
    #TODO This test for auth doesn't work with a new record.
    if (self.authors.count > 0 || self.editors.count > 0 || !self.serial.nil?)
      valid = true
    end
    if !valid
      errors.add(:base,
                 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year'
      )
    end
  end

  #endregion  hard validations

  #region Soft_validation_methods
  def sv_has_authors
    # only used in validating BibTeX output
    if !(has_authors?)
      soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.')
    end
  end

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

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

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

  def sv_year_exists
    # only used in validating BibTeX output
    if year.blank?
      soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  # def sv_missing_journal
  # never used
  #   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? and self.serial.blank?
        soft_validations.add(:bibtex_type, 'This article is missing a journal name or serial.')
      end
    end
  end

  def sv_has_a_publisher
    if self.publisher.blank?
      soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.')
    end
  end

  def sv_has_booktitle
    if self.booktitle.blank?
      soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')

      #  soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
      # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
    end
  end

  def sv_has_school
    if self.school.blank?
      soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.')
    end
  end

  def sv_has_institution
    if self.institution.blank?
      soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.')
    end
  end

  def sv_has_note
    if (self.note.blank?) && (!self.notes.any?)
      soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished 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

- (String?) annote

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. (compare to a note which is any additional information which may be useful to the reader) In most cases these are personal annotations; TW will translate these into notes with a specified project so they will only be visible within the project where the note was made. <== Under debate with Matt.

Returns:

  • (String)

    the annotation

  • (nil)

    means the attribute is not stored in the database.



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
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'app/models/source/bibtex.rb', line 312

class Source::Bibtex < Source
  include SoftValidation

  attr_accessor :authors_to_create

  # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }

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

  belongs_to :serial, inverse_of: :sources
  belongs_to :source_language, class_name: "Language", foreign_key: :language_id, inverse_of: :sources
  # above to handle clash with bibtex language field.

  has_many :author_roles, -> { order('roles.position ASC') }, class_name: 'SourceAuthor', as: :role_object, validate: true
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person, validate: true # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor', as: :role_object, validate: true # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true
  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, allow_destroy: true

  before_validation :create_authors, if: '!authors_to_create.nil?'
  before_validation :check_has_field
  before_save :set_cached_nomenclature_date

  #region validations
  validates_inclusion_of :bibtex_type,
                         in:      ::VALID_BIBTEX_TYPES,
                         message: '%{value} is not a valid source type'

  validates_presence_of :year,
                        if:      '!month.blank? || !stated_year.blank?',
                        message: 'year is required when month or stated_year is provided'

  # @todo refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_numericality_of :year,
                            only_integer:          true, greater_than: 999,
                            less_than_or_equal_to: Time.now.year + 2,
                            allow_blank:           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_blank: true,
                         message:     ' month'

  validates_numericality_of :day,
                            allow_blank:           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 => {:with    => URI::regexp(%w(http https ftp)),
                              message: "[%{value}] is not a valid URL"}, allow_blank: true

  #endregion validations

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_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_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 ruby-bibtex related

  def journal
    [read_attribute(:journal), (self.serial.blank? ? nil : self.serial.name)].compact.first
  end

  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [BibTeX::Entry]
  #   entry equivalent to self
  def to_bibtex
    b = BibTeX::Entry.new(:bibtex_type => self[:bibtex_type])
    ::BIBTEX_FIELDS.each do |f|
      if (!self.send(f).blank?) && !(f == :bibtex_type)
        b[f] = self.send(f)
      end
    end

    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    unless self.verbatim_keywords.blank?
      b[:keywords] = self.verbatim_keywords
    end

    b[:note] = concatenated_notes_string if !concatenated_notes_string.blank? # see Notable
    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = self.identifiers.of_type(:uri)
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = self.identifiers.of_type(:isbn)
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = self.identifiers.of_type(:doi)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    b.author = self.compute_bibtex_names('author') unless (!self.authors.any? && self.author.blank?)
    b.editor = self.compute_bibtex_names('editor') unless (!self.editors.any? && self.editor.blank?)

    b.key    = self.id unless self.new_record? # id.blank?
    b
  end

  # @param type [String] either author or editor
  # @return [String]
  #   the bibtex version of the name strings created from the TW people
  #   BibTeX format is 'lastname, firstname and lastname,firstname and lastname, firstname'
  #   For a name list not joined by multiple 'and's, use compute_human_names
  def compute_bibtex_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.bibtex_name
      else
        return self.send(methods).collect { |a| a.bibtex_name }.join(' and ')
    end
  end

  # @param type [String] either author or editor
  # @return[String]
  #   A human readable version of the person list
  #   'firstname lastname, firstname lastname, & firstname lastname'
  def compute_human_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.name
      else
        return self.send(methods).collect { |a| a.name }.to_sentence(last_word_connector: ' & ')
    end
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    self.to_bibtex.valid?
  end

  # Instantiates a Source::Bibtex instance from a BibTeX::Entry
  # Note: note conversion is handled in note setter.
  #       identifiers are handled in associated setter.
  # !! Unrecognized attributes are added as import attributes.
  #
  # Usage:
  #    a = BibTeX::Entry.new(bibtex_type: 'book', title: 'Foos of Bar America', author: 'Smith, James', year: 1921)
  #    b = Source::Bibtex.new(a)
  # 
  # @param bibtex_entry [BibTex::Entry] the BibTex::Entry to convert 
  # @return [Source::BibTex.new] a new instance 
  # @todo annote to project specific note?
  # @todo if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title
  # serial with alternate_value on name .count = 1 assign .first
  # before validate assign serial if matching & not doesn't have a serial currently assigned.
  # @todo if there is an ISSN it should look up to see it the serial already exists.
  def self.new_from_bibtex(bibtex_entry = nil)

    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s                 = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    import_attributes = []
    bibtex_entry.fields.each do |key, value|
      if key == :keywords
        s.verbatim_keywords = value
        next
      end
      v = value.to_s.strip
      if s.respond_to?(key.to_sym)
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end
    s.data_attributes_attributes = import_attributes
    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

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

  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now
  def create_related_people_and_roles
    return false if !self.valid? ||
      self.new_record? ||
      (self.author.blank? && self.editor.blank?) ||
      self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names

    begin
      Role.transaction do
        unless bibtex.authors.blank?
          bibtex.authors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceAuthor.create!(role_object: self, person: p)
          end
        end

        if !bibtex.editors.blank?
          bibtex.editors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceEditor.create!(role_object: self, person: p)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      raise
    end
    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

  # @todo create related Serials

  #endregion ruby-bibtex related

  #region getters & setters

  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

  # Used only on import from BibTeX records
  def note=(value)
    write_attribute(:note, value)
    if !self.note.blank? && self.new_record?
      if value.include?('||')
        a = value.split(/||/)
        a.each do |n|
          self.notes.build({text: n + ' [Created on import from BibTeX.]'})
        end
      else
        self.notes.build({text: value + ' [Created on import from BibTeX.]'})
      end
    end
  end

  # @return [String]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized people records before bibtex author string
  def authority_name
    if self.authors.count == 0 # no normalized people, use string, !! not .any? because of in-memory setting?!
      if self.author.blank?
        return ('')
      else
        b = self.to_bibtex
        b.parse_names
        return b.author.tokens.collect{ |t| t.last }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
      end
    else # use normalized records 
      return self.authors.collect{ |a| a.full_last_name }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
    end
  end

  def isbn=(value)
    write_attribute(:isbn, value)
    unless value.blank?
      if tw_isbn = self.identifiers.where(type: 'Identifier::Global::Isbn').first
        if tw_isbn.identifier != value
          tw_isbn.destroy
          self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
      end
    end
  end

  def isbn
    identifier_string_of_type(:isbn)
  end

  def doi=(value)
    write_attribute(:doi, value)
    unless value.blank?
      if tw_doi = self.identifiers.where(type: 'Identifier::Global::Doi').first
        if tw_doi.identifier != value
          tw_doi.destroy
          self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
      end
    end
  end

  def doi
    identifier_string_of_type(:doi)
  end

  # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case
  # we need to set the serial based on ISSN.
  def issn=(value)
    write_attribute(:issn, value)
    unless value.blank?
      tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first
      unless tw_issn.nil? || tw_issn.identifier != value
        tw_issn.destroy
      end
      self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
    end
  end

  def issn
    identifier_string_of_type(:issn)
  end

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

  # @return [Identifier]
  #   the identifier of this type, relies on Identifier to enforce has_one for Global identifiers,
  #   !! behaviour for Identifier::Local types may be unexpected
  def identifier_string_of_type(type)
    identifiers.of_type(type).first.try(:identifier)
  end

  # @todo if language is set => set language_id
  # def language=(value)
  #
  # end
  #endregion getters & setters

  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? # nothing saved yet, so no author roles are saved yet
    # 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

  #region time/date related

  # @return [Date] 
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def date
    set_cached_nomenclature_date if !self.persisted?
    self.cached_nomenclature_date
  end

  # @return [Integer]
  #  The effective year of publication as per nomenclatural rules
  def nomenclature_year
    date.year if date
  end

  def set_cached_nomenclature_date
    self.cached_nomenclature_date = Utilities::Dates.nomenclature_date(
      self.day,
      Utilities::Dates.month_index(self.month), # this allows values from bibtex like 'may' to be handled 
      self.year
    )
  end

  #endregion    time/date related


  # @return [BibTex::Bibliography]
  #   initialized with this source as an entry
  def bibtex_bibliography
    bx_entry = to_bibtex
    bx_entry.year = '0000' if bx_entry.year.blank? # cludge to fix render problem with year
    b = BibTeX::Bibliography.new
    b.add(bx_entry)
    b
  end

  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text')
    cp = CiteProc::Processor.new(style: style, format: format) # There is a problem with the zootaxa format and letters!
    cp.import(bibtex_bibliography.to_citeproc)
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end 

  # @return [String]
  #   a full representation, using bibtex
  # String must be length > 0
  def cached_string(format = 'text')
    return nil  unless (format == 'text') || (format == 'html')
    str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant
    str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  protected

  def create_authors
    begin
      Person.transaction do
        authors_to_create.each do |shs|
          p = Person.create!(shs)
          self.author_roles.build(person: p)
        end
      end
    rescue
      errors.add(:base, 'invalid author parameters')
    end
  end

  # set cached values and copies active record relations into bibtex values
  def set_cached
    if self.errors.empty?
      tmp                       = cached_string('text')
      self.cached               = tmp
      self.cached_author_string = authority_name

      if self.authors.size > 0
        self.author = self.compute_bibtex_names('author')
      end

      if self.editors.size > 0
        self.editor = self.compute_bibtex_names('editor')
      end
    end
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  def check_has_field
    valid = false
    TW_REQUIRED_FIELDS.each do |i|
      if !self[i].blank?
        valid = true
        break
      end
    end
    #TODO This test for auth doesn't work with a new record.
    if (self.authors.count > 0 || self.editors.count > 0 || !self.serial.nil?)
      valid = true
    end
    if !valid
      errors.add(:base,
                 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year'
      )
    end
  end

  #endregion  hard validations

  #region Soft_validation_methods
  def sv_has_authors
    # only used in validating BibTeX output
    if !(has_authors?)
      soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.')
    end
  end

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

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

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

  def sv_year_exists
    # only used in validating BibTeX output
    if year.blank?
      soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  # def sv_missing_journal
  # never used
  #   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? and self.serial.blank?
        soft_validations.add(:bibtex_type, 'This article is missing a journal name or serial.')
      end
    end
  end

  def sv_has_a_publisher
    if self.publisher.blank?
      soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.')
    end
  end

  def sv_has_booktitle
    if self.booktitle.blank?
      soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')

      #  soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
      # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
    end
  end

  def sv_has_school
    if self.school.blank?
      soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.')
    end
  end

  def sv_has_institution
    if self.institution.blank?
      soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.')
    end
  end

  def sv_has_note
    if (self.note.blank?) && (!self.notes.any?)
      soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished 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

- (String?) author

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.



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
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'app/models/source/bibtex.rb', line 312

class Source::Bibtex < Source
  include SoftValidation

  attr_accessor :authors_to_create

  # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }

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

  belongs_to :serial, inverse_of: :sources
  belongs_to :source_language, class_name: "Language", foreign_key: :language_id, inverse_of: :sources
  # above to handle clash with bibtex language field.

  has_many :author_roles, -> { order('roles.position ASC') }, class_name: 'SourceAuthor', as: :role_object, validate: true
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person, validate: true # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor', as: :role_object, validate: true # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true
  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, allow_destroy: true

  before_validation :create_authors, if: '!authors_to_create.nil?'
  before_validation :check_has_field
  before_save :set_cached_nomenclature_date

  #region validations
  validates_inclusion_of :bibtex_type,
                         in:      ::VALID_BIBTEX_TYPES,
                         message: '%{value} is not a valid source type'

  validates_presence_of :year,
                        if:      '!month.blank? || !stated_year.blank?',
                        message: 'year is required when month or stated_year is provided'

  # @todo refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_numericality_of :year,
                            only_integer:          true, greater_than: 999,
                            less_than_or_equal_to: Time.now.year + 2,
                            allow_blank:           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_blank: true,
                         message:     ' month'

  validates_numericality_of :day,
                            allow_blank:           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 => {:with    => URI::regexp(%w(http https ftp)),
                              message: "[%{value}] is not a valid URL"}, allow_blank: true

  #endregion validations

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_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_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 ruby-bibtex related

  def journal
    [read_attribute(:journal), (self.serial.blank? ? nil : self.serial.name)].compact.first
  end

  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [BibTeX::Entry]
  #   entry equivalent to self
  def to_bibtex
    b = BibTeX::Entry.new(:bibtex_type => self[:bibtex_type])
    ::BIBTEX_FIELDS.each do |f|
      if (!self.send(f).blank?) && !(f == :bibtex_type)
        b[f] = self.send(f)
      end
    end

    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    unless self.verbatim_keywords.blank?
      b[:keywords] = self.verbatim_keywords
    end

    b[:note] = concatenated_notes_string if !concatenated_notes_string.blank? # see Notable
    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = self.identifiers.of_type(:uri)
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = self.identifiers.of_type(:isbn)
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = self.identifiers.of_type(:doi)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    b.author = self.compute_bibtex_names('author') unless (!self.authors.any? && self.author.blank?)
    b.editor = self.compute_bibtex_names('editor') unless (!self.editors.any? && self.editor.blank?)

    b.key    = self.id unless self.new_record? # id.blank?
    b
  end

  # @param type [String] either author or editor
  # @return [String]
  #   the bibtex version of the name strings created from the TW people
  #   BibTeX format is 'lastname, firstname and lastname,firstname and lastname, firstname'
  #   For a name list not joined by multiple 'and's, use compute_human_names
  def compute_bibtex_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.bibtex_name
      else
        return self.send(methods).collect { |a| a.bibtex_name }.join(' and ')
    end
  end

  # @param type [String] either author or editor
  # @return[String]
  #   A human readable version of the person list
  #   'firstname lastname, firstname lastname, & firstname lastname'
  def compute_human_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.name
      else
        return self.send(methods).collect { |a| a.name }.to_sentence(last_word_connector: ' & ')
    end
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    self.to_bibtex.valid?
  end

  # Instantiates a Source::Bibtex instance from a BibTeX::Entry
  # Note: note conversion is handled in note setter.
  #       identifiers are handled in associated setter.
  # !! Unrecognized attributes are added as import attributes.
  #
  # Usage:
  #    a = BibTeX::Entry.new(bibtex_type: 'book', title: 'Foos of Bar America', author: 'Smith, James', year: 1921)
  #    b = Source::Bibtex.new(a)
  # 
  # @param bibtex_entry [BibTex::Entry] the BibTex::Entry to convert 
  # @return [Source::BibTex.new] a new instance 
  # @todo annote to project specific note?
  # @todo if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title
  # serial with alternate_value on name .count = 1 assign .first
  # before validate assign serial if matching & not doesn't have a serial currently assigned.
  # @todo if there is an ISSN it should look up to see it the serial already exists.
  def self.new_from_bibtex(bibtex_entry = nil)

    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s                 = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    import_attributes = []
    bibtex_entry.fields.each do |key, value|
      if key == :keywords
        s.verbatim_keywords = value
        next
      end
      v = value.to_s.strip
      if s.respond_to?(key.to_sym)
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end
    s.data_attributes_attributes = import_attributes
    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

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

  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now
  def create_related_people_and_roles
    return false if !self.valid? ||
      self.new_record? ||
      (self.author.blank? && self.editor.blank?) ||
      self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names

    begin
      Role.transaction do
        unless bibtex.authors.blank?
          bibtex.authors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceAuthor.create!(role_object: self, person: p)
          end
        end

        if !bibtex.editors.blank?
          bibtex.editors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceEditor.create!(role_object: self, person: p)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      raise
    end
    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

  # @todo create related Serials

  #endregion ruby-bibtex related

  #region getters & setters

  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

  # Used only on import from BibTeX records
  def note=(value)
    write_attribute(:note, value)
    if !self.note.blank? && self.new_record?
      if value.include?('||')
        a = value.split(/||/)
        a.each do |n|
          self.notes.build({text: n + ' [Created on import from BibTeX.]'})
        end
      else
        self.notes.build({text: value + ' [Created on import from BibTeX.]'})
      end
    end
  end

  # @return [String]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized people records before bibtex author string
  def authority_name
    if self.authors.count == 0 # no normalized people, use string, !! not .any? because of in-memory setting?!
      if self.author.blank?
        return ('')
      else
        b = self.to_bibtex
        b.parse_names
        return b.author.tokens.collect{ |t| t.last }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
      end
    else # use normalized records 
      return self.authors.collect{ |a| a.full_last_name }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
    end
  end

  def isbn=(value)
    write_attribute(:isbn, value)
    unless value.blank?
      if tw_isbn = self.identifiers.where(type: 'Identifier::Global::Isbn').first
        if tw_isbn.identifier != value
          tw_isbn.destroy
          self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
      end
    end
  end

  def isbn
    identifier_string_of_type(:isbn)
  end

  def doi=(value)
    write_attribute(:doi, value)
    unless value.blank?
      if tw_doi = self.identifiers.where(type: 'Identifier::Global::Doi').first
        if tw_doi.identifier != value
          tw_doi.destroy
          self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
      end
    end
  end

  def doi
    identifier_string_of_type(:doi)
  end

  # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case
  # we need to set the serial based on ISSN.
  def issn=(value)
    write_attribute(:issn, value)
    unless value.blank?
      tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first
      unless tw_issn.nil? || tw_issn.identifier != value
        tw_issn.destroy
      end
      self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
    end
  end

  def issn
    identifier_string_of_type(:issn)
  end

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

  # @return [Identifier]
  #   the identifier of this type, relies on Identifier to enforce has_one for Global identifiers,
  #   !! behaviour for Identifier::Local types may be unexpected
  def identifier_string_of_type(type)
    identifiers.of_type(type).first.try(:identifier)
  end

  # @todo if language is set => set language_id
  # def language=(value)
  #
  # end
  #endregion getters & setters

  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? # nothing saved yet, so no author roles are saved yet
    # 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

  #region time/date related

  # @return [Date] 
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def date
    set_cached_nomenclature_date if !self.persisted?
    self.cached_nomenclature_date
  end

  # @return [Integer]
  #  The effective year of publication as per nomenclatural rules
  def nomenclature_year
    date.year if date
  end

  def set_cached_nomenclature_date
    self.cached_nomenclature_date = Utilities::Dates.nomenclature_date(
      self.day,
      Utilities::Dates.month_index(self.month), # this allows values from bibtex like 'may' to be handled 
      self.year
    )
  end

  #endregion    time/date related


  # @return [BibTex::Bibliography]
  #   initialized with this source as an entry
  def bibtex_bibliography
    bx_entry = to_bibtex
    bx_entry.year = '0000' if bx_entry.year.blank? # cludge to fix render problem with year
    b = BibTeX::Bibliography.new
    b.add(bx_entry)
    b
  end

  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text')
    cp = CiteProc::Processor.new(style: style, format: format) # There is a problem with the zootaxa format and letters!
    cp.import(bibtex_bibliography.to_citeproc)
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end 

  # @return [String]
  #   a full representation, using bibtex
  # String must be length > 0
  def cached_string(format = 'text')
    return nil  unless (format == 'text') || (format == 'html')
    str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant
    str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  protected

  def create_authors
    begin
      Person.transaction do
        authors_to_create.each do |shs|
          p = Person.create!(shs)
          self.author_roles.build(person: p)
        end
      end
    rescue
      errors.add(:base, 'invalid author parameters')
    end
  end

  # set cached values and copies active record relations into bibtex values
  def set_cached
    if self.errors.empty?
      tmp                       = cached_string('text')
      self.cached               = tmp
      self.cached_author_string = authority_name

      if self.authors.size > 0
        self.author = self.compute_bibtex_names('author')
      end

      if self.editors.size > 0
        self.editor = self.compute_bibtex_names('editor')
      end
    end
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  def check_has_field
    valid = false
    TW_REQUIRED_FIELDS.each do |i|
      if !self[i].blank?
        valid = true
        break
      end
    end
    #TODO This test for auth doesn't work with a new record.
    if (self.authors.count > 0 || self.editors.count > 0 || !self.serial.nil?)
      valid = true
    end
    if !valid
      errors.add(:base,
                 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year'
      )
    end
  end

  #endregion  hard validations

  #region Soft_validation_methods
  def sv_has_authors
    # only used in validating BibTeX output
    if !(has_authors?)
      soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.')
    end
  end

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

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

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

  def sv_year_exists
    # only used in validating BibTeX output
    if year.blank?
      soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  # def sv_missing_journal
  # never used
  #   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? and self.serial.blank?
        soft_validations.add(:bibtex_type, 'This article is missing a journal name or serial.')
      end
    end
  end

  def sv_has_a_publisher
    if self.publisher.blank?
      soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.')
    end
  end

  def sv_has_booktitle
    if self.booktitle.blank?
      soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')

      #  soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
      # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
    end
  end

  def sv_has_school
    if self.school.blank?
      soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.')
    end
  end

  def sv_has_institution
    if self.institution.blank?
      soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.')
    end
  end

  def sv_has_note
    if (self.note.blank?) && (!self.notes.any?)
      soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished 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

- (Object) authors_to_create

Returns the value of attribute authors_to_create



315
316
317
# File 'app/models/source/bibtex.rb', line 315

def authors_to_create
  @authors_to_create
end

- (String) bibtex_type

TODO:

Returns:

  • (String)


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
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'app/models/source/bibtex.rb', line 312

class Source::Bibtex < Source
  include SoftValidation

  attr_accessor :authors_to_create

  # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }

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

  belongs_to :serial, inverse_of: :sources
  belongs_to :source_language, class_name: "Language", foreign_key: :language_id, inverse_of: :sources
  # above to handle clash with bibtex language field.

  has_many :author_roles, -> { order('roles.position ASC') }, class_name: 'SourceAuthor', as: :role_object, validate: true
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person, validate: true # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor', as: :role_object, validate: true # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true
  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, allow_destroy: true

  before_validation :create_authors, if: '!authors_to_create.nil?'
  before_validation :check_has_field
  before_save :set_cached_nomenclature_date

  #region validations
  validates_inclusion_of :bibtex_type,
                         in:      ::VALID_BIBTEX_TYPES,
                         message: '%{value} is not a valid source type'

  validates_presence_of :year,
                        if:      '!month.blank? || !stated_year.blank?',
                        message: 'year is required when month or stated_year is provided'

  # @todo refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_numericality_of :year,
                            only_integer:          true, greater_than: 999,
                            less_than_or_equal_to: Time.now.year + 2,
                            allow_blank:           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_blank: true,
                         message:     ' month'

  validates_numericality_of :day,
                            allow_blank:           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 => {:with    => URI::regexp(%w(http https ftp)),
                              message: "[%{value}] is not a valid URL"}, allow_blank: true

  #endregion validations

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_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_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 ruby-bibtex related

  def journal
    [read_attribute(:journal), (self.serial.blank? ? nil : self.serial.name)].compact.first
  end

  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [BibTeX::Entry]
  #   entry equivalent to self
  def to_bibtex
    b = BibTeX::Entry.new(:bibtex_type => self[:bibtex_type])
    ::BIBTEX_FIELDS.each do |f|
      if (!self.send(f).blank?) && !(f == :bibtex_type)
        b[f] = self.send(f)
      end
    end

    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    unless self.verbatim_keywords.blank?
      b[:keywords] = self.verbatim_keywords
    end

    b[:note] = concatenated_notes_string if !concatenated_notes_string.blank? # see Notable
    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = self.identifiers.of_type(:uri)
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = self.identifiers.of_type(:isbn)
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = self.identifiers.of_type(:doi)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    b.author = self.compute_bibtex_names('author') unless (!self.authors.any? && self.author.blank?)
    b.editor = self.compute_bibtex_names('editor') unless (!self.editors.any? && self.editor.blank?)

    b.key    = self.id unless self.new_record? # id.blank?
    b
  end

  # @param type [String] either author or editor
  # @return [String]
  #   the bibtex version of the name strings created from the TW people
  #   BibTeX format is 'lastname, firstname and lastname,firstname and lastname, firstname'
  #   For a name list not joined by multiple 'and's, use compute_human_names
  def compute_bibtex_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.bibtex_name
      else
        return self.send(methods).collect { |a| a.bibtex_name }.join(' and ')
    end
  end

  # @param type [String] either author or editor
  # @return[String]
  #   A human readable version of the person list
  #   'firstname lastname, firstname lastname, & firstname lastname'
  def compute_human_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.name
      else
        return self.send(methods).collect { |a| a.name }.to_sentence(last_word_connector: ' & ')
    end
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    self.to_bibtex.valid?
  end

  # Instantiates a Source::Bibtex instance from a BibTeX::Entry
  # Note: note conversion is handled in note setter.
  #       identifiers are handled in associated setter.
  # !! Unrecognized attributes are added as import attributes.
  #
  # Usage:
  #    a = BibTeX::Entry.new(bibtex_type: 'book', title: 'Foos of Bar America', author: 'Smith, James', year: 1921)
  #    b = Source::Bibtex.new(a)
  # 
  # @param bibtex_entry [BibTex::Entry] the BibTex::Entry to convert 
  # @return [Source::BibTex.new] a new instance 
  # @todo annote to project specific note?
  # @todo if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title
  # serial with alternate_value on name .count = 1 assign .first
  # before validate assign serial if matching & not doesn't have a serial currently assigned.
  # @todo if there is an ISSN it should look up to see it the serial already exists.
  def self.new_from_bibtex(bibtex_entry = nil)

    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s                 = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    import_attributes = []
    bibtex_entry.fields.each do |key, value|
      if key == :keywords
        s.verbatim_keywords = value
        next
      end
      v = value.to_s.strip
      if s.respond_to?(key.to_sym)
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end
    s.data_attributes_attributes = import_attributes
    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

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

  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now
  def create_related_people_and_roles
    return false if !self.valid? ||
      self.new_record? ||
      (self.author.blank? && self.editor.blank?) ||
      self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names

    begin
      Role.transaction do
        unless bibtex.authors.blank?
          bibtex.authors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceAuthor.create!(role_object: self, person: p)
          end
        end

        if !bibtex.editors.blank?
          bibtex.editors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceEditor.create!(role_object: self, person: p)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      raise
    end
    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

  # @todo create related Serials

  #endregion ruby-bibtex related

  #region getters & setters

  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

  # Used only on import from BibTeX records
  def note=(value)
    write_attribute(:note, value)
    if !self.note.blank? && self.new_record?
      if value.include?('||')
        a = value.split(/||/)
        a.each do |n|
          self.notes.build({text: n + ' [Created on import from BibTeX.]'})
        end
      else
        self.notes.build({text: value + ' [Created on import from BibTeX.]'})
      end
    end
  end

  # @return [String]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized people records before bibtex author string
  def authority_name
    if self.authors.count == 0 # no normalized people, use string, !! not .any? because of in-memory setting?!
      if self.author.blank?
        return ('')
      else
        b = self.to_bibtex
        b.parse_names
        return b.author.tokens.collect{ |t| t.last }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
      end
    else # use normalized records 
      return self.authors.collect{ |a| a.full_last_name }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
    end
  end

  def isbn=(value)
    write_attribute(:isbn, value)
    unless value.blank?
      if tw_isbn = self.identifiers.where(type: 'Identifier::Global::Isbn').first
        if tw_isbn.identifier != value
          tw_isbn.destroy
          self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
      end
    end
  end

  def isbn
    identifier_string_of_type(:isbn)
  end

  def doi=(value)
    write_attribute(:doi, value)
    unless value.blank?
      if tw_doi = self.identifiers.where(type: 'Identifier::Global::Doi').first
        if tw_doi.identifier != value
          tw_doi.destroy
          self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
      end
    end
  end

  def doi
    identifier_string_of_type(:doi)
  end

  # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case
  # we need to set the serial based on ISSN.
  def issn=(value)
    write_attribute(:issn, value)
    unless value.blank?
      tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first
      unless tw_issn.nil? || tw_issn.identifier != value
        tw_issn.destroy
      end
      self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
    end
  end

  def issn
    identifier_string_of_type(:issn)
  end

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

  # @return [Identifier]
  #   the identifier of this type, relies on Identifier to enforce has_one for Global identifiers,
  #   !! behaviour for Identifier::Local types may be unexpected
  def identifier_string_of_type(type)
    identifiers.of_type(type).first.try(:identifier)
  end

  # @todo if language is set => set language_id
  # def language=(value)
  #
  # end
  #endregion getters & setters

  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? # nothing saved yet, so no author roles are saved yet
    # 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

  #region time/date related

  # @return [Date] 
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def date
    set_cached_nomenclature_date if !self.persisted?
    self.cached_nomenclature_date
  end

  # @return [Integer]
  #  The effective year of publication as per nomenclatural rules
  def nomenclature_year
    date.year if date
  end

  def set_cached_nomenclature_date
    self.cached_nomenclature_date = Utilities::Dates.nomenclature_date(
      self.day,
      Utilities::Dates.month_index(self.month), # this allows values from bibtex like 'may' to be handled 
      self.year
    )
  end

  #endregion    time/date related


  # @return [BibTex::Bibliography]
  #   initialized with this source as an entry
  def bibtex_bibliography
    bx_entry = to_bibtex
    bx_entry.year = '0000' if bx_entry.year.blank? # cludge to fix render problem with year
    b = BibTeX::Bibliography.new
    b.add(bx_entry)
    b
  end

  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text')
    cp = CiteProc::Processor.new(style: style, format: format) # There is a problem with the zootaxa format and letters!
    cp.import(bibtex_bibliography.to_citeproc)
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end 

  # @return [String]
  #   a full representation, using bibtex
  # String must be length > 0
  def cached_string(format = 'text')
    return nil  unless (format == 'text') || (format == 'html')
    str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant
    str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  protected

  def create_authors
    begin
      Person.transaction do
        authors_to_create.each do |shs|
          p = Person.create!(shs)
          self.author_roles.build(person: p)
        end
      end
    rescue
      errors.add(:base, 'invalid author parameters')
    end
  end

  # set cached values and copies active record relations into bibtex values
  def set_cached
    if self.errors.empty?
      tmp                       = cached_string('text')
      self.cached               = tmp
      self.cached_author_string = authority_name

      if self.authors.size > 0
        self.author = self.compute_bibtex_names('author')
      end

      if self.editors.size > 0
        self.editor = self.compute_bibtex_names('editor')
      end
    end
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  def check_has_field
    valid = false
    TW_REQUIRED_FIELDS.each do |i|
      if !self[i].blank?
        valid = true
        break
      end
    end
    #TODO This test for auth doesn't work with a new record.
    if (self.authors.count > 0 || self.editors.count > 0 || !self.serial.nil?)
      valid = true
    end
    if !valid
      errors.add(:base,
                 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year'
      )
    end
  end

  #endregion  hard validations

  #region Soft_validation_methods
  def sv_has_authors
    # only used in validating BibTeX output
    if !(has_authors?)
      soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.')
    end
  end

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

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

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

  def sv_year_exists
    # only used in validating BibTeX output
    if year.blank?
      soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  # def sv_missing_journal
  # never used
  #   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? and self.serial.blank?
        soft_validations.add(:bibtex_type, 'This article is missing a journal name or serial.')
      end
    end
  end

  def sv_has_a_publisher
    if self.publisher.blank?
      soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.')
    end
  end

  def sv_has_booktitle
    if self.booktitle.blank?
      soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')

      #  soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
      # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
    end
  end

  def sv_has_school
    if self.school.blank?
      soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.')
    end
  end

  def sv_has_institution
    if self.institution.blank?
      soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.')
    end
  end

  def sv_has_note
    if (self.note.blank?) && (!self.notes.any?)
      soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished 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

- (nil) booktitle

@return the title of the book 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.

Returns:

  • (nil)

    means the attribute is not stored in the database.



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
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'app/models/source/bibtex.rb', line 312

class Source::Bibtex < Source
  include SoftValidation

  attr_accessor :authors_to_create

  # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }

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

  belongs_to :serial, inverse_of: :sources
  belongs_to :source_language, class_name: "Language", foreign_key: :language_id, inverse_of: :sources
  # above to handle clash with bibtex language field.

  has_many :author_roles, -> { order('roles.position ASC') }, class_name: 'SourceAuthor', as: :role_object, validate: true
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person, validate: true # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor', as: :role_object, validate: true # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true
  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, allow_destroy: true

  before_validation :create_authors, if: '!authors_to_create.nil?'
  before_validation :check_has_field
  before_save :set_cached_nomenclature_date

  #region validations
  validates_inclusion_of :bibtex_type,
                         in:      ::VALID_BIBTEX_TYPES,
                         message: '%{value} is not a valid source type'

  validates_presence_of :year,
                        if:      '!month.blank? || !stated_year.blank?',
                        message: 'year is required when month or stated_year is provided'

  # @todo refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_numericality_of :year,
                            only_integer:          true, greater_than: 999,
                            less_than_or_equal_to: Time.now.year + 2,
                            allow_blank:           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_blank: true,
                         message:     ' month'

  validates_numericality_of :day,
                            allow_blank:           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 => {:with    => URI::regexp(%w(http https ftp)),
                              message: "[%{value}] is not a valid URL"}, allow_blank: true

  #endregion validations

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_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_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 ruby-bibtex related

  def journal
    [read_attribute(:journal), (self.serial.blank? ? nil : self.serial.name)].compact.first
  end

  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [BibTeX::Entry]
  #   entry equivalent to self
  def to_bibtex
    b = BibTeX::Entry.new(:bibtex_type => self[:bibtex_type])
    ::BIBTEX_FIELDS.each do |f|
      if (!self.send(f).blank?) && !(f == :bibtex_type)
        b[f] = self.send(f)
      end
    end

    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    unless self.verbatim_keywords.blank?
      b[:keywords] = self.verbatim_keywords
    end

    b[:note] = concatenated_notes_string if !concatenated_notes_string.blank? # see Notable
    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = self.identifiers.of_type(:uri)
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = self.identifiers.of_type(:isbn)
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = self.identifiers.of_type(:doi)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    b.author = self.compute_bibtex_names('author') unless (!self.authors.any? && self.author.blank?)
    b.editor = self.compute_bibtex_names('editor') unless (!self.editors.any? && self.editor.blank?)

    b.key    = self.id unless self.new_record? # id.blank?
    b
  end

  # @param type [String] either author or editor
  # @return [String]
  #   the bibtex version of the name strings created from the TW people
  #   BibTeX format is 'lastname, firstname and lastname,firstname and lastname, firstname'
  #   For a name list not joined by multiple 'and's, use compute_human_names
  def compute_bibtex_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.bibtex_name
      else
        return self.send(methods).collect { |a| a.bibtex_name }.join(' and ')
    end
  end

  # @param type [String] either author or editor
  # @return[String]
  #   A human readable version of the person list
  #   'firstname lastname, firstname lastname, & firstname lastname'
  def compute_human_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.name
      else
        return self.send(methods).collect { |a| a.name }.to_sentence(last_word_connector: ' & ')
    end
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    self.to_bibtex.valid?
  end

  # Instantiates a Source::Bibtex instance from a BibTeX::Entry
  # Note: note conversion is handled in note setter.
  #       identifiers are handled in associated setter.
  # !! Unrecognized attributes are added as import attributes.
  #
  # Usage:
  #    a = BibTeX::Entry.new(bibtex_type: 'book', title: 'Foos of Bar America', author: 'Smith, James', year: 1921)
  #    b = Source::Bibtex.new(a)
  # 
  # @param bibtex_entry [BibTex::Entry] the BibTex::Entry to convert 
  # @return [Source::BibTex.new] a new instance 
  # @todo annote to project specific note?
  # @todo if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title
  # serial with alternate_value on name .count = 1 assign .first
  # before validate assign serial if matching & not doesn't have a serial currently assigned.
  # @todo if there is an ISSN it should look up to see it the serial already exists.
  def self.new_from_bibtex(bibtex_entry = nil)

    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s                 = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    import_attributes = []
    bibtex_entry.fields.each do |key, value|
      if key == :keywords
        s.verbatim_keywords = value
        next
      end
      v = value.to_s.strip
      if s.respond_to?(key.to_sym)
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end
    s.data_attributes_attributes = import_attributes
    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

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

  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now
  def create_related_people_and_roles
    return false if !self.valid? ||
      self.new_record? ||
      (self.author.blank? && self.editor.blank?) ||
      self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names

    begin
      Role.transaction do
        unless bibtex.authors.blank?
          bibtex.authors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceAuthor.create!(role_object: self, person: p)
          end
        end

        if !bibtex.editors.blank?
          bibtex.editors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceEditor.create!(role_object: self, person: p)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      raise
    end
    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

  # @todo create related Serials

  #endregion ruby-bibtex related

  #region getters & setters

  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

  # Used only on import from BibTeX records
  def note=(value)
    write_attribute(:note, value)
    if !self.note.blank? && self.new_record?
      if value.include?('||')
        a = value.split(/||/)
        a.each do |n|
          self.notes.build({text: n + ' [Created on import from BibTeX.]'})
        end
      else
        self.notes.build({text: value + ' [Created on import from BibTeX.]'})
      end
    end
  end

  # @return [String]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized people records before bibtex author string
  def authority_name
    if self.authors.count == 0 # no normalized people, use string, !! not .any? because of in-memory setting?!
      if self.author.blank?
        return ('')
      else
        b = self.to_bibtex
        b.parse_names
        return b.author.tokens.collect{ |t| t.last }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
      end
    else # use normalized records 
      return self.authors.collect{ |a| a.full_last_name }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
    end
  end

  def isbn=(value)
    write_attribute(:isbn, value)
    unless value.blank?
      if tw_isbn = self.identifiers.where(type: 'Identifier::Global::Isbn').first
        if tw_isbn.identifier != value
          tw_isbn.destroy
          self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
      end
    end
  end

  def isbn
    identifier_string_of_type(:isbn)
  end

  def doi=(value)
    write_attribute(:doi, value)
    unless value.blank?
      if tw_doi = self.identifiers.where(type: 'Identifier::Global::Doi').first
        if tw_doi.identifier != value
          tw_doi.destroy
          self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
      end
    end
  end

  def doi
    identifier_string_of_type(:doi)
  end

  # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case
  # we need to set the serial based on ISSN.
  def issn=(value)
    write_attribute(:issn, value)
    unless value.blank?
      tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first
      unless tw_issn.nil? || tw_issn.identifier != value
        tw_issn.destroy
      end
      self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
    end
  end

  def issn
    identifier_string_of_type(:issn)
  end

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

  # @return [Identifier]
  #   the identifier of this type, relies on Identifier to enforce has_one for Global identifiers,
  #   !! behaviour for Identifier::Local types may be unexpected
  def identifier_string_of_type(type)
    identifiers.of_type(type).first.try(:identifier)
  end

  # @todo if language is set => set language_id
  # def language=(value)
  #
  # end
  #endregion getters & setters

  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? # nothing saved yet, so no author roles are saved yet
    # 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

  #region time/date related

  # @return [Date] 
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def date
    set_cached_nomenclature_date if !self.persisted?
    self.cached_nomenclature_date
  end

  # @return [Integer]
  #  The effective year of publication as per nomenclatural rules
  def nomenclature_year
    date.year if date
  end

  def set_cached_nomenclature_date
    self.cached_nomenclature_date = Utilities::Dates.nomenclature_date(
      self.day,
      Utilities::Dates.month_index(self.month), # this allows values from bibtex like 'may' to be handled 
      self.year
    )
  end

  #endregion    time/date related


  # @return [BibTex::Bibliography]
  #   initialized with this source as an entry
  def bibtex_bibliography
    bx_entry = to_bibtex
    bx_entry.year = '0000' if bx_entry.year.blank? # cludge to fix render problem with year
    b = BibTeX::Bibliography.new
    b.add(bx_entry)
    b
  end

  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text')
    cp = CiteProc::Processor.new(style: style, format: format) # There is a problem with the zootaxa format and letters!
    cp.import(bibtex_bibliography.to_citeproc)
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end 

  # @return [String]
  #   a full representation, using bibtex
  # String must be length > 0
  def cached_string(format = 'text')
    return nil  unless (format == 'text') || (format == 'html')
    str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant
    str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  protected

  def create_authors
    begin
      Person.transaction do
        authors_to_create.each do |shs|
          p = Person.create!(shs)
          self.author_roles.build(person: p)
        end
      end
    rescue
      errors.add(:base, 'invalid author parameters')
    end
  end

  # set cached values and copies active record relations into bibtex values
  def set_cached
    if self.errors.empty?
      tmp                       = cached_string('text')
      self.cached               = tmp
      self.cached_author_string = authority_name

      if self.authors.size > 0
        self.author = self.compute_bibtex_names('author')
      end

      if self.editors.size > 0
        self.editor = self.compute_bibtex_names('editor')
      end
    end
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  def check_has_field
    valid = false
    TW_REQUIRED_FIELDS.each do |i|
      if !self[i].blank?
        valid = true
        break
      end
    end
    #TODO This test for auth doesn't work with a new record.
    if (self.authors.count > 0 || self.editors.count > 0 || !self.serial.nil?)
      valid = true
    end
    if !valid
      errors.add(:base,
                 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year'
      )
    end
  end

  #endregion  hard validations

  #region Soft_validation_methods
  def sv_has_authors
    # only used in validating BibTeX output
    if !(has_authors?)
      soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.')
    end
  end

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

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

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

  def sv_year_exists
    # only used in validating BibTeX output
    if year.blank?
      soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  # def sv_missing_journal
  # never used
  #   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? and self.serial.blank?
        soft_validations.add(:bibtex_type, 'This article is missing a journal name or serial.')
      end
    end
  end

  def sv_has_a_publisher
    if self.publisher.blank?
      soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.')
    end
  end

  def sv_has_booktitle
    if self.booktitle.blank?
      soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')

      #  soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
      # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
    end
  end

  def sv_has_school
    if self.school.blank?
      soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.')
    end
  end

  def sv_has_institution
    if self.institution.blank?
      soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.')
    end
  end

  def sv_has_note
    if (self.note.blank?) && (!self.notes.any?)
      soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished 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

- (String) cached

Non-Bibtex attribute that is cross-referenced.

Returns:

  • (String)


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
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'app/models/source/bibtex.rb', line 312

class Source::Bibtex < Source
  include SoftValidation

  attr_accessor :authors_to_create

  # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }

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

  belongs_to :serial, inverse_of: :sources
  belongs_to :source_language, class_name: "Language", foreign_key: :language_id, inverse_of: :sources
  # above to handle clash with bibtex language field.

  has_many :author_roles, -> { order('roles.position ASC') }, class_name: 'SourceAuthor', as: :role_object, validate: true
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person, validate: true # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor', as: :role_object, validate: true # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true
  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, allow_destroy: true

  before_validation :create_authors, if: '!authors_to_create.nil?'
  before_validation :check_has_field
  before_save :set_cached_nomenclature_date

  #region validations
  validates_inclusion_of :bibtex_type,
                         in:      ::VALID_BIBTEX_TYPES,
                         message: '%{value} is not a valid source type'

  validates_presence_of :year,
                        if:      '!month.blank? || !stated_year.blank?',
                        message: 'year is required when month or stated_year is provided'

  # @todo refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_numericality_of :year,
                            only_integer:          true, greater_than: 999,
                            less_than_or_equal_to: Time.now.year + 2,
                            allow_blank:           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_blank: true,
                         message:     ' month'

  validates_numericality_of :day,
                            allow_blank:           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 => {:with    => URI::regexp(%w(http https ftp)),
                              message: "[%{value}] is not a valid URL"}, allow_blank: true

  #endregion validations

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_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_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 ruby-bibtex related

  def journal
    [read_attribute(:journal), (self.serial.blank? ? nil : self.serial.name)].compact.first
  end

  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [BibTeX::Entry]
  #   entry equivalent to self
  def to_bibtex
    b = BibTeX::Entry.new(:bibtex_type => self[:bibtex_type])
    ::BIBTEX_FIELDS.each do |f|
      if (!self.send(f).blank?) && !(f == :bibtex_type)
        b[f] = self.send(f)
      end
    end

    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    unless self.verbatim_keywords.blank?
      b[:keywords] = self.verbatim_keywords
    end

    b[:note] = concatenated_notes_string if !concatenated_notes_string.blank? # see Notable
    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = self.identifiers.of_type(:uri)
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = self.identifiers.of_type(:isbn)
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = self.identifiers.of_type(:doi)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    b.author = self.compute_bibtex_names('author') unless (!self.authors.any? && self.author.blank?)
    b.editor = self.compute_bibtex_names('editor') unless (!self.editors.any? && self.editor.blank?)

    b.key    = self.id unless self.new_record? # id.blank?
    b
  end

  # @param type [String] either author or editor
  # @return [String]
  #   the bibtex version of the name strings created from the TW people
  #   BibTeX format is 'lastname, firstname and lastname,firstname and lastname, firstname'
  #   For a name list not joined by multiple 'and's, use compute_human_names
  def compute_bibtex_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.bibtex_name
      else
        return self.send(methods).collect { |a| a.bibtex_name }.join(' and ')
    end
  end

  # @param type [String] either author or editor
  # @return[String]
  #   A human readable version of the person list
  #   'firstname lastname, firstname lastname, & firstname lastname'
  def compute_human_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.name
      else
        return self.send(methods).collect { |a| a.name }.to_sentence(last_word_connector: ' & ')
    end
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    self.to_bibtex.valid?
  end

  # Instantiates a Source::Bibtex instance from a BibTeX::Entry
  # Note: note conversion is handled in note setter.
  #       identifiers are handled in associated setter.
  # !! Unrecognized attributes are added as import attributes.
  #
  # Usage:
  #    a = BibTeX::Entry.new(bibtex_type: 'book', title: 'Foos of Bar America', author: 'Smith, James', year: 1921)
  #    b = Source::Bibtex.new(a)
  # 
  # @param bibtex_entry [BibTex::Entry] the BibTex::Entry to convert 
  # @return [Source::BibTex.new] a new instance 
  # @todo annote to project specific note?
  # @todo if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title
  # serial with alternate_value on name .count = 1 assign .first
  # before validate assign serial if matching & not doesn't have a serial currently assigned.
  # @todo if there is an ISSN it should look up to see it the serial already exists.
  def self.new_from_bibtex(bibtex_entry = nil)

    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s                 = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    import_attributes = []
    bibtex_entry.fields.each do |key, value|
      if key == :keywords
        s.verbatim_keywords = value
        next
      end
      v = value.to_s.strip
      if s.respond_to?(key.to_sym)
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end
    s.data_attributes_attributes = import_attributes
    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

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

  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now
  def create_related_people_and_roles
    return false if !self.valid? ||
      self.new_record? ||
      (self.author.blank? && self.editor.blank?) ||
      self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names

    begin
      Role.transaction do
        unless bibtex.authors.blank?
          bibtex.authors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceAuthor.create!(role_object: self, person: p)
          end
        end

        if !bibtex.editors.blank?
          bibtex.editors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceEditor.create!(role_object: self, person: p)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      raise
    end
    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

  # @todo create related Serials

  #endregion ruby-bibtex related

  #region getters & setters

  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

  # Used only on import from BibTeX records
  def note=(value)
    write_attribute(:note, value)
    if !self.note.blank? && self.new_record?
      if value.include?('||')
        a = value.split(/||/)
        a.each do |n|
          self.notes.build({text: n + ' [Created on import from BibTeX.]'})
        end
      else
        self.notes.build({text: value + ' [Created on import from BibTeX.]'})
      end
    end
  end

  # @return [String]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized people records before bibtex author string
  def authority_name
    if self.authors.count == 0 # no normalized people, use string, !! not .any? because of in-memory setting?!
      if self.author.blank?
        return ('')
      else
        b = self.to_bibtex
        b.parse_names
        return b.author.tokens.collect{ |t| t.last }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
      end
    else # use normalized records 
      return self.authors.collect{ |a| a.full_last_name }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
    end
  end

  def isbn=(value)
    write_attribute(:isbn, value)
    unless value.blank?
      if tw_isbn = self.identifiers.where(type: 'Identifier::Global::Isbn').first
        if tw_isbn.identifier != value
          tw_isbn.destroy
          self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
      end
    end
  end

  def isbn
    identifier_string_of_type(:isbn)
  end

  def doi=(value)
    write_attribute(:doi, value)
    unless value.blank?
      if tw_doi = self.identifiers.where(type: 'Identifier::Global::Doi').first
        if tw_doi.identifier != value
          tw_doi.destroy
          self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
      end
    end
  end

  def doi
    identifier_string_of_type(:doi)
  end

  # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case
  # we need to set the serial based on ISSN.
  def issn=(value)
    write_attribute(:issn, value)
    unless value.blank?
      tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first
      unless tw_issn.nil? || tw_issn.identifier != value
        tw_issn.destroy
      end
      self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
    end
  end

  def issn
    identifier_string_of_type(:issn)
  end

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

  # @return [Identifier]
  #   the identifier of this type, relies on Identifier to enforce has_one for Global identifiers,
  #   !! behaviour for Identifier::Local types may be unexpected
  def identifier_string_of_type(type)
    identifiers.of_type(type).first.try(:identifier)
  end

  # @todo if language is set => set language_id
  # def language=(value)
  #
  # end
  #endregion getters & setters

  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? # nothing saved yet, so no author roles are saved yet
    # 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

  #region time/date related

  # @return [Date] 
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def date
    set_cached_nomenclature_date if !self.persisted?
    self.cached_nomenclature_date
  end

  # @return [Integer]
  #  The effective year of publication as per nomenclatural rules
  def nomenclature_year
    date.year if date
  end

  def set_cached_nomenclature_date
    self.cached_nomenclature_date = Utilities::Dates.nomenclature_date(
      self.day,
      Utilities::Dates.month_index(self.month), # this allows values from bibtex like 'may' to be handled 
      self.year
    )
  end

  #endregion    time/date related


  # @return [BibTex::Bibliography]
  #   initialized with this source as an entry
  def bibtex_bibliography
    bx_entry = to_bibtex
    bx_entry.year = '0000' if bx_entry.year.blank? # cludge to fix render problem with year
    b = BibTeX::Bibliography.new
    b.add(bx_entry)
    b
  end

  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text')
    cp = CiteProc::Processor.new(style: style, format: format) # There is a problem with the zootaxa format and letters!
    cp.import(bibtex_bibliography.to_citeproc)
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end 

  # @return [String]
  #   a full representation, using bibtex
  # String must be length > 0
  def cached_string(format = 'text')
    return nil  unless (format == 'text') || (format == 'html')
    str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant
    str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  protected

  def create_authors
    begin
      Person.transaction do
        authors_to_create.each do |shs|
          p = Person.create!(shs)
          self.author_roles.build(person: p)
        end
      end
    rescue
      errors.add(:base, 'invalid author parameters')
    end
  end

  # set cached values and copies active record relations into bibtex values
  def set_cached
    if self.errors.empty?
      tmp                       = cached_string('text')
      self.cached               = tmp
      self.cached_author_string = authority_name

      if self.authors.size > 0
        self.author = self.compute_bibtex_names('author')
      end

      if self.editors.size > 0
        self.editor = self.compute_bibtex_names('editor')
      end
    end
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  def check_has_field
    valid = false
    TW_REQUIRED_FIELDS.each do |i|
      if !self[i].blank?
        valid = true
        break
      end
    end
    #TODO This test for auth doesn't work with a new record.
    if (self.authors.count > 0 || self.editors.count > 0 || !self.serial.nil?)
      valid = true
    end
    if !valid
      errors.add(:base,
                 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year'
      )
    end
  end

  #endregion  hard validations

  #region Soft_validation_methods
  def sv_has_authors
    # only used in validating BibTeX output
    if !(has_authors?)
      soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.')
    end
  end

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

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

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

  def sv_year_exists
    # only used in validating BibTeX output
    if year.blank?
      soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  # def sv_missing_journal
  # never used
  #   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? and self.serial.blank?
        soft_validations.add(:bibtex_type, 'This article is missing a journal name or serial.')
      end
    end
  end

  def sv_has_a_publisher
    if self.publisher.blank?
      soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.')
    end
  end

  def sv_has_booktitle
    if self.booktitle.blank?
      soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')

      #  soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
      # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
    end
  end

  def sv_has_school
    if self.school.blank?
      soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.')
    end
  end

  def sv_has_institution
    if self.institution.blank?
      soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.')
    end
  end

  def sv_has_note
    if (self.note.blank?) && (!self.notes.any?)
      soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished 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

- (String) cached_author_string

Non-Bibtex attribute that is cross-referenced.

Returns:

  • (String)


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
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'app/models/source/bibtex.rb', line 312

class Source::Bibtex < Source
  include SoftValidation

  attr_accessor :authors_to_create

  # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }

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

  belongs_to :serial, inverse_of: :sources
  belongs_to :source_language, class_name: "Language", foreign_key: :language_id, inverse_of: :sources
  # above to handle clash with bibtex language field.

  has_many :author_roles, -> { order('roles.position ASC') }, class_name: 'SourceAuthor', as: :role_object, validate: true
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person, validate: true # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor', as: :role_object, validate: true # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true
  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, allow_destroy: true

  before_validation :create_authors, if: '!authors_to_create.nil?'
  before_validation :check_has_field
  before_save :set_cached_nomenclature_date

  #region validations
  validates_inclusion_of :bibtex_type,
                         in:      ::VALID_BIBTEX_TYPES,
                         message: '%{value} is not a valid source type'

  validates_presence_of :year,
                        if:      '!month.blank? || !stated_year.blank?',
                        message: 'year is required when month or stated_year is provided'

  # @todo refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_numericality_of :year,
                            only_integer:          true, greater_than: 999,
                            less_than_or_equal_to: Time.now.year + 2,
                            allow_blank:           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_blank: true,
                         message:     ' month'

  validates_numericality_of :day,
                            allow_blank:           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 => {:with    => URI::regexp(%w(http https ftp)),
                              message: "[%{value}] is not a valid URL"}, allow_blank: true

  #endregion validations

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_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_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 ruby-bibtex related

  def journal
    [read_attribute(:journal), (self.serial.blank? ? nil : self.serial.name)].compact.first
  end

  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [BibTeX::Entry]
  #   entry equivalent to self
  def to_bibtex
    b = BibTeX::Entry.new(:bibtex_type => self[:bibtex_type])
    ::BIBTEX_FIELDS.each do |f|
      if (!self.send(f).blank?) && !(f == :bibtex_type)
        b[f] = self.send(f)
      end
    end

    if !self.year_suffix.blank?
      b.year = self.year_with_suffix
    end

    unless self.verbatim_keywords.blank?
      b[:keywords] = self.verbatim_keywords
    end

    b[:note] = concatenated_notes_string if !concatenated_notes_string.blank? # see Notable
    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless self.serial.nil?
      b[:journal] = self.serial.name
      issns       = self.serial.identifiers.of_type(:issn)
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = self.identifiers.of_type(:uri)
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = self.identifiers.of_type(:isbn)
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = self.identifiers.of_type(:doi)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    b.author = self.compute_bibtex_names('author') unless (!self.authors.any? && self.author.blank?)
    b.editor = self.compute_bibtex_names('editor') unless (!self.editors.any? && self.editor.blank?)

    b.key    = self.id unless self.new_record? # id.blank?
    b
  end

  # @param type [String] either author or editor
  # @return [String]
  #   the bibtex version of the name strings created from the TW people
  #   BibTeX format is 'lastname, firstname and lastname,firstname and lastname, firstname'
  #   For a name list not joined by multiple 'and's, use compute_human_names
  def compute_bibtex_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.bibtex_name
      else
        return self.send(methods).collect { |a| a.bibtex_name }.join(' and ')
    end
  end

  # @param type [String] either author or editor
  # @return[String]
  #   A human readable version of the person list
  #   'firstname lastname, firstname lastname, & firstname lastname'
  def compute_human_names(type)
    method  = type
    methods = type + 's'
    case self.send(methods).size
      when 0
        return self.send(method)
      when 1
        return self.send(methods).first.name
      else
        return self.send(methods).collect { |a| a.name }.to_sentence(last_word_connector: ' & ')
    end
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    self.to_bibtex.valid?
  end

  # Instantiates a Source::Bibtex instance from a BibTeX::Entry
  # Note: note conversion is handled in note setter.
  #       identifiers are handled in associated setter.
  # !! Unrecognized attributes are added as import attributes.
  #
  # Usage:
  #    a = BibTeX::Entry.new(bibtex_type: 'book', title: 'Foos of Bar America', author: 'Smith, James', year: 1921)
  #    b = Source::Bibtex.new(a)
  # 
  # @param bibtex_entry [BibTex::Entry] the BibTex::Entry to convert 
  # @return [Source::BibTex.new] a new instance 
  # @todo annote to project specific note?
  # @todo if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title
  # serial with alternate_value on name .count = 1 assign .first
  # before validate assign serial if matching & not doesn't have a serial currently assigned.
  # @todo if there is an ISSN it should look up to see it the serial already exists.
  def self.new_from_bibtex(bibtex_entry = nil)

    return false if !bibtex_entry.kind_of?(::BibTeX::Entry)
    s                 = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s)
    import_attributes = []
    bibtex_entry.fields.each do |key, value|
      if key == :keywords
        s.verbatim_keywords = value
        next
      end
      v = value.to_s.strip
      if s.respond_to?(key.to_sym)
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end
    s.data_attributes_attributes = import_attributes
    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

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

  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now
  def create_related_people_and_roles
    return false if !self.valid? ||
      self.new_record? ||
      (self.author.blank? && self.editor.blank?) ||
      self.roles.count > 0

    bibtex = to_bibtex
    bibtex.parse_names

    begin
      Role.transaction do
        unless bibtex.authors.blank?
          bibtex.authors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceAuthor.create!(role_object: self, person: p)
          end
        end

        if !bibtex.editors.blank?
          bibtex.editors.each do |a|
            p = Source::Bibtex.bibtex_author_to_person(a)
            p.save!
            SourceEditor.create!(role_object: self, person: p)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      raise
    end
    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

  # @todo create related Serials

  #endregion ruby-bibtex related

  #region getters & setters

  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

  # Used only on import from BibTeX records
  def note=(value)
    write_attribute(:note, value)
    if !self.note.blank? && self.new_record?
      if value.include?('||')
        a = value.split(/||/)
        a.each do |n|
          self.notes.build({text: n + ' [Created on import from BibTeX.]'})
        end
      else
        self.notes.build({text: value + ' [Created on import from BibTeX.]'})
      end
    end
  end

  # @return [String]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized people records before bibtex author string
  def authority_name
    if self.authors.count == 0 # no normalized people, use string, !! not .any? because of in-memory setting?!
      if self.author.blank?
        return ('')
      else
        b = self.to_bibtex
        b.parse_names
        return b.author.tokens.collect{ |t| t.last }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
      end
    else # use normalized records 
      return self.authors.collect{ |a| a.full_last_name }.to_sentence(last_word_connector: ' & ', two_words_connector: ' & ')
    end
  end

  def isbn=(value)
    write_attribute(:isbn, value)
    unless value.blank?
      if tw_isbn = self.identifiers.where(type: 'Identifier::Global::Isbn').first
        if tw_isbn.identifier != value
          tw_isbn.destroy
          self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Isbn', identifier: value)
      end
    end
  end

  def isbn
    identifier_string_of_type(:isbn)
  end

  def doi=(value)
    write_attribute(:doi, value)
    unless value.blank?
      if tw_doi = self.identifiers.where(type: 'Identifier::Global::Doi').first
        if tw_doi.identifier != value
          tw_doi.destroy
          self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
        end
      else
        self.identifiers.build(type: 'Identifier::Global::Doi', identifier: value)
      end
    end
  end

  def doi
    identifier_string_of_type(:doi)
  end

  # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case
  # we need to set the serial based on ISSN.
  def issn=(value)
    write_attribute(:issn, value)
    unless value.blank?
      tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first
      unless tw_issn.nil? || tw_issn.identifier != value
        tw_issn.destroy
      end
      self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value)
    end
  end

  def issn
    identifier_string_of_type(:issn)
  end

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

  # @return [Identifier]
  #   the identifier of this type, relies on Identifier to enforce has_one for Global identifiers,
  #   !! behaviour for Identifier::Local types may be unexpected
  def identifier_string_of_type(type)
    identifiers.of_type(type).first.try(:identifier)
  end

  # @todo if language is set => set language_id
  # def language=(value)
  #
  # end
  #endregion getters & setters

  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? # nothing saved yet, so no author roles are saved yet
    # 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

  #region time/date related

  # @return [Date] 
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def date
    set_cached_nomenclature_date if !self.persisted?
    self.cached_nomenclature_date
  end

  # @return [Integer]
  #  The effective year of publication as per nomenclatural rules
  def nomenclature_year
    date.year if date
  end

  def set_cached_nomenclature_date
    self.cached_nomenclature_date = Utilities::Dates.nomenclature_date(
      self.day,
      Utilities::Dates.month_index(self.month), # this allows values from bibtex like 'may' to be handled 
      self.year
    )
  end

  #endregion    time/date related


  # @return [BibTex::Bibliography]
  #   initialized with this source as an entry
  def bibtex_bibliography
    bx_entry = to_bibtex
    bx_entry.year = '0000' if bx_entry.year.blank? # cludge to fix render problem with year
    b = BibTeX::Bibliography.new
    b.add(bx_entry)
    b
  end

  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text')
    cp = CiteProc::Processor.new(style: style, format: format) # There is a problem with the zootaxa format and letters!
    cp.import(bibtex_bibliography.to_citeproc)
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end 

  # @return [String]
  #   a full representation, using bibtex
  # String must be length > 0
  def cached_string(format = 'text')
    return nil  unless (format == 'text') || (format == 'html')
    str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant
    str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  protected

  def create_authors
    begin
      Person.transaction do
        authors_to_create.each do |shs|
          p = Person.create!(shs)
          self.author_roles.build(person: p)
        end
      end
    rescue
      errors.add(:base, 'invalid author parameters')
    end
  end

  # set cached values and copies active record relations into bibtex values
  def set_cached
    if self.errors.empty?
      tmp                       = cached_string('text')
      self.cached               = tmp
      self.cached_author_string = authority_name

      if self.authors.size > 0
        self.author = self.compute_bibtex_names('author')
      end

      if self.editors.size > 0
        self.editor = self.compute_bibtex_names('editor')
      end
    end
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  def check_has_field
    valid = false
    TW_REQUIRED_FIELDS.each do |i|
      if !self[i].blank?
        valid = true
        break
      end
    end
    #TODO This test for auth doesn't work with a new record.
    if (self.authors.count > 0 || self.editors.count > 0 || !self.serial.nil?)
      valid = true
    end
    if !valid
      errors.add(:base,
                 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year'
      )
    end
  end

  #endregion  hard validations

  #region Soft_validation_methods
  def sv_has_authors
    # only used in validating BibTeX output
    if !(has_authors?)
      soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.')
    end
  end

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

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

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

  def sv_year_exists
    # only used in validating BibTeX output
    if year.blank?
      soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.')
    elsif year < 1700
      soft_validations.add(:year, 'This year is prior to the 1700s')
    end
  end

  # def sv_missing_journal
  # never used
  #   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? and self.serial.blank?
        soft_validations.add(:bibtex_type, 'This article is missing a journal name or serial.')
      end
    end
  end

  def sv_has_a_publisher
    if self.publisher.blank?
      soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.')
    end
  end

  def sv_has_booktitle
    if self.booktitle.blank?
      soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.')
    end
  end

  def sv_is_contained_has_chapter_or_pages
    if self.chapter.blank? && self.pages.blank?
      soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')

      #  soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
      # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.')
    end
  end

  def sv_has_school
    if self.school.blank?
      soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.')
    end
  end

  def sv_has_institution
    if self.institution.blank?
      soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.')
    end
  end

  def sv_has_note
    if (self.note.blank?) && (!self.notes.any?)
      soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished 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

- (DateTime) cached_nomenclature_date

Returns the date of the publication for nomenclatural purposes

Returns:

  • (DateTime)

    the date of the publication for nomenclatural purposes



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
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'app/models/source/bibtex.rb', line 312

class Source::Bibtex < Source
  include SoftValidation

  attr_accessor :authors_to_create

  # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? }

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

  belongs_to :serial, inverse_of: :sources
  belongs_to :source_language, class_name: "Language", foreign_key: :language_id, inverse_of: :sources
  # above to handle clash with bibtex language field.

  has_many :author_roles, -> { order('roles.position ASC') }, class_name: 'SourceAuthor', as: :role_object, validate: true
  has_many :authors, -> { order('roles.position ASC') }, through: :author_roles, source: :person, validate: true # self.author & self.authors should match or one of them should be empty
  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor', as: :role_object, validate: true # ditto for self.editor & self.editors
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true
  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, allow_destroy: true

  before_validation :create_authors, if: '!authors_to_create.nil?'
  before_validation :check_has_field
  before_save :set_cached_nomenclature_date

  #region validations
  validates_inclusion_of :bibtex_type,
                         in:      ::VALID_BIBTEX_TYPES,
                         message: '%{value} is not a valid source type'

  validates_presence_of :year,
                        if:      '!month.blank? || !stated_year.blank?',
                        message: 'year is required when month or stated_year is provided'

  # @todo refactor out date validation methods so that they can be unified (TaxonDetermination, CollectingEvent)
  validates_numericality_of :year,
                            only_integer:          true, greater_than: 999,
                            less_than_or_equal_to: Time.now.year + 2,
                            allow_blank:           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_blank: true,
                         message:     ' month'