Class: Source::Bibtex

Inherits:
Source show all
Extended by:
SoftValidationExtensions::Klass
Includes:
Shared::OriginRelationship, SoftValidationExtensions::Instance
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.

Author:

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

  • Matt Yoder

Defined Under Namespace

Modules: SoftValidationExtensions

Constant Summary collapse

DEFAULT_CSL_STYLE =

Type will change

'taxonworks'
GRAPH_ENTRY_POINTS =
[:origin_relationships]
BIBTEX_REQUIRED_FIELDS =

Used in soft validation

{
  article: [:author, :title, :journal, :year],
  book: [:author, :editor, :title, :publisher, :year],
  booklet: [:title],
  conference: [:author, :title, :booktitle, :year],
  inbook: [:author, :editor, :title, :chapter, :pages, :publisher, :year],
  incollection: [:author, :title, :booktitle, :publisher, :year],
  inproceedings:  [:author, :title, :booktitle, :year],
  manual: [:title],
  mastersthesis: [:author, :title, :school, :year],
  misc: [],
  phdthesis: [:author, :title, :school, :year],
  proceedings: [:title, :year],
  techreport: [:author,:title,:institution, :year],
  unpublished: [:author, :title, :note]
}
TW_REQUIRED_FIELDS =

TW required fields (must have one of these fields filled in) either year or stated_year is acceptable

[:author, :editor, :booktitle, :title, :url, :journal, :year, :stated_year].freeze
IGNORE_SIMILAR =
[:verbatim, :cached, :cached_author_string, :cached_nomenclature_date].freeze
IGNORE_IDENTICAL =
IGNORE_SIMILAR.dup.freeze

Constants included from SoftValidationExtensions::Klass

SoftValidationExtensions::Klass::VALIDATIONS

Constants inherited from Source

ALTERNATE_VALUES_FOR

Constants included from SoftValidation

SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS

Instance Attribute Summary collapse

Attributes inherited from Source

#cached, #cached_author_string, #no_year_suffix_validation, #serial_id

Attributes included from Housekeeping::Users

#by

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SoftValidationExtensions::Instance

#sv_contains_a_writer, #sv_duplicate_title, #sv_electronic_only, #sv_has_authors, #sv_has_booktitle, #sv_has_chapter_or_pages, #sv_has_institution, #sv_has_note, #sv_has_publisher, #sv_has_school, #sv_has_some_type_of_year, #sv_has_title, #sv_has_year, #sv_is_article_missing_journal, #sv_match_fields?, #sv_missing_roles

Methods included from Shared::OriginRelationship

#new_objects, #old_objects, #reject_origin_relationships, #set_origin

Methods inherited from Source

#author_year, batch_create, batch_preview, #cited_objects, #clone, #is_bibtex?, #is_in_project?, #nomenclature_date, #reject_project_sources, select_optimized, #sv_fix_cached_names, #sv_fix_stated_year, #sv_html_tags, #sv_stated_year, used_recently

Methods included from SoftValidation

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

Methods included from Shared::HasPapertrail

#attribute_updated, #attribute_updater

Methods included from Shared::IsData

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

Methods included from Shared::Documentation

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

Methods included from Shared::Tags

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

Methods included from Shared::Notes

#concatenated_notes_string, #reject_notes

Methods included from Shared::Identifiers

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

Methods included from Shared::DataAttributes

#import_attributes, #internal_attributes, #keyword_value_hash, #reject_data_attributes

Methods included from Shared::AlternateValues

#all_values_for, #alternate_valued?

Methods included from Housekeeping::Timestamps

#data_breakdown_for_chartkick_recent

Methods included from Housekeeping::Users

#set_created_by_id, #set_updated_by_id

Methods inherited from ApplicationRecord

transaction_with_retry

Instance Attribute Details

#abstractString

TODO:

Returns:

  • (String)


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

class Source::Bibtex < Source

  # Type will change
  DEFAULT_CSL_STYLE = 'taxonworks'
  #DEFAULT_CSL_STYLE = 'zootaxa'

  attr_accessor :authors_to_create

  include Shared::OriginRelationship
  include Source::Bibtex::SoftValidationExtensions::Instance
  extend Source::Bibtex::SoftValidationExtensions::Klass

  is_origin_for 'Source::Bibtex', 'Source::Verbatim'
  originates_from 'Source::Bibtex', 'Source::Verbatim'

  GRAPH_ENTRY_POINTS = [:origin_relationships]

  # Used in soft validation
  BIBTEX_REQUIRED_FIELDS = {
    article: [:author, :title, :journal, :year],
    book: [:author, :editor, :title, :publisher, :year],
    booklet: [:title],
    conference: [:author, :title, :booktitle, :year],
    inbook: [:author, :editor, :title, :chapter, :pages, :publisher, :year],
    incollection: [:author, :title, :booktitle, :publisher, :year],
    inproceedings:  [:author, :title, :booktitle, :year],
    manual: [:title],
    mastersthesis: [:author, :title, :school, :year],
    misc: [],
    phdthesis: [:author, :title, :school, :year],
    proceedings: [:title, :year],
    techreport: [:author,:title,:institution, :year],
    unpublished: [:author, :title, :note]
  }

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

  IGNORE_SIMILAR = [:verbatim, :cached, :cached_author_string, :cached_nomenclature_date].freeze
  IGNORE_IDENTICAL = IGNORE_SIMILAR.dup.freeze

  belongs_to :serial, inverse_of: :sources

  # handle conflict with BibTex language field.
  belongs_to :source_language, class_name: 'Language', foreign_key: :language_id, inverse_of: :sources

  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

  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor',
    as: :role_object, validate: true
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true

  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, :serial, allow_destroy: true

  before_validation :create_authors, if: -> { !authors_to_create.nil? }
  before_validation :check_has_field

  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: 'is required when month or stated_year is provided'

  validates :year, date_year: {
    min_year: 1000, max_year: Time.now.year + 2,
    message: 'must be an integer greater than 999 and no more than 2 years in the future'}

  validates_presence_of :month,
    unless: -> { day.blank? },
    message: 'is required when day is provided'

  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_blank: true,
    message: ' month'

  validates :day, date_day: {year_sym: :year, month_sym: :month},
            unless: -> { year.blank? || month.blank? }

  validates :url, format: {
    with: URI::regexp(%w(http https ftp)),
    message: '[%{value}] is not a valid URL'}, allow_blank: true

  validate :italics_are_paired, unless: -> { title.blank? }
  validate :validate_year_suffix, unless: -> { self.no_year_suffix_validation || (self.type != 'Source::Bibtex') }

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_nomenclature_date) }

  # @param [BibTeX::Name] bibtex_author
  # @return [Person, Boolean] new person, or false
  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    p = Person.new(
      first_name: bibtex_author.first,
      prefix: bibtex_author.prefix,
      last_name: bibtex_author.last,
      suffix: bibtex_author.suffix)
    p.namecase_names
    p
  end

  # @return [Source::Bibtex.new]
  #   Adds errors if parse error exists. Note these
  # errors are lost if save/valid? is called again on the object.
  def self.new_from_bibtex_text(text = nil)
    source = Source::Bibtex.new
    begin
      a = BibTeX::Bibliography.parse(text, filter: :latex).first
      if a.class.name == 'BibTeX::Error'
        source.errors.add(:base, 'Unable to parse BibTeX entry. Possible error at end of: ' + a.content)
        return source
      else
        return new_from_bibtex(a)
      end
    rescue BibTeX::ParseError => e
      source.errors.add(:base, 'Unable to parse BibTeX entry: ' + e.to_s)
      return source
    end
  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_from_bibtex(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: Serial with alternate_value on name .count = 1 assign .first
  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|
      next if key == :serial # Raises if it hits the belongs_to

      if key == :keywords
        s.verbatim_keywords = value
        next
      end

      v = value.to_s.strip

      if s.respond_to?(key.to_sym) && key != :type
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end

    s.data_attributes_attributes = import_attributes

    # See issn=() for code matching to existing serials that preceeds this logic
    if s.serial_id.blank? && !bibtex_entry.fields[:journal].to_s.blank? && !bibtex_entry.fields[:issn].to_s.blank?
      a = {
        name: bibtex_entry.fields[:journal].to_s,
        publisher: bibtex_entry.fields[:publisher].to_s,
        identifiers_attributes: [ {
          identifier: bibtex_entry.fields[:issn].to_s,
          type: 'Identifier::Global::Issn'
        } ]
      }

      s.serial_attributes = a
    end
    s
  end

  # @return [Array] journal, nil or name
  def journal
    [read_attribute(:journal), (serial.blank? ? nil : serial.name)].compact.first
  end

  # @return [String]
  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    to_bibtex.valid?
  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
    [year, year_suffix].compact.join
  end

  # TODO: Not used
  #
  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now.
  #
  # @return [Array, Boolean] of names, or false
  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
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(bibtex)

    begin
      Role.transaction do
        if bibtex.authors
          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
          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

  #region getters & setters

  # @param [String, Integer] value
  # @return [Integer] value of year
  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

  # @param [String] value
  # @return [String]
  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
  # @param [String] value
  # @return [String]
  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

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def isbn
    identifier_string_of_type('Identifier::Global::Isbn')
  end

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def doi
    identifier_string_of_type('Identifier::Global::Doi')
  end

  # @param [String] value
  # @return [String]
  def issn=(value)
    # Only add ISSN if it is reasonable to assume its repeated
    # It is likely that most ISSN belong on Serials
    unless %w{article inbook phdthesis inproceedings}.include?(bibtex_type)
      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
    else
      # Do some work to assign a Serial if possible
      # Check for Journal by ISSN
      s = Serial.where(name: journal).first
      i = Identifier::Global::Issn.where(identifier: value, identifier_object_type: 'Serial').first

      # Found an Existing Serial identically named with an assigned Identical ISSN
      if !s.nil? && (s == i&.identifier_object)
        write_attribute(:serial_id, s.id)
      elsif i && s.nil? # Found a Serial with an Identifier, but not identically named, assign it anyway
        write_attribute(:serial_id, i.identifier_object_id)
      end
    end
  end

  # @return [String]
  def issn
    identifier_string_of_type('Identifier::Global::Issn')
  end

  # turn bibtex URL field into a Ruby URI object
  # @return [URI]
  def url_as_uri
    URI(self.url) if self.url.present?
  end

  # @param [String] type_value
  # @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_value)
    # Also handle in memory
    identifiers.each do |i|
      return i.identifier if i.type == type_value
    end
    nil
    # identifiers.where(type: type_value).first&.identifier
  end

 #endregion getters & setters

  # @return [Boolean]
  # is there a bibtex author or author roles?
  def has_authors?
    return true if !author.blank?
    return false if new_record?
    # self exists in the db
    authors.count > 0 ? true : false
  end

  # @return [Boolean]
  def has_editors?
    return true if editor
    # editor attribute is empty
    return false if new_record? # WHY!?
    # self exists in the db
    editors.count > 0 ? true : false
  end

  # @return [Boolean]
  #  true contains either an author or editor
  def has_writer?
    (has_authors? || has_editors?) ? true : false
  end

  # @return [Boolean]
  def has_some_year? # is there a year or stated year?
    return false if year.blank? && stated_year.blank?
    true
  end

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

  # @return [Date || Time] <sigh>
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def cached_nomenclature_date
    if !persisted?
      nomenclature_date
    else
      read_attribute(:cached_nomenclature_date)
    end
  end

  # rubocop:disable Metrics/MethodLength
  # @return [BibTeX::Entry, false]
  #   !! Entry equivalent to self, this should round-trip with no changes.
  def to_bibtex
    return false if bibtex_type.nil?
    b = BibTeX::Entry.new(bibtex_type: bibtex_type)

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank?
        b[f] = v
      end
    end

    b[:keywords] = verbatim_keywords unless verbatim_keywords.blank?
    b[:note] = concatenated_notes_string unless concatenated_notes_string.blank?

    unless serial.nil?
      b[:journal] = serial.name
      issns  = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless serial.nil?
      b[:journal] = serial.name
      issns = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = identifiers.where(type: 'Identifier::Global::Uri')
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = identifiers.where(type: 'Identifier::Global::Isbn')
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = identifiers.where(type: 'Identifier::Global::Doi') #.of_type(:isbn)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    # Overiden by `author` and `editor` if present
    b.author = get_bibtex_names('author') if author_roles.load.any? # unless (!authors.load.any? && author.blank?)
    b.editor = get_bibtex_names('editor') if editor_roles.load.any? # unless (!editors.load.any? && editor.blank?)

    # TODO: use global_id or replace with UUID or DOI if available
    b.key = id unless new_record?
    b
  end

  # @return Hash
  #   a to_citeproc with values updated for literal
  #   handling via `{}` in TaxonWorks
  def to_citeproc(normalize_names = true)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names

    a = b.to_citeproc

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank? && (v.to_s =~ /\A{(.*)}\z/)
        a[f.to_s] = {literal: $1}
      end
    end

    a['year-suffix'] = year_suffix unless year_suffix.blank?
    a['original-date'] = {"date-parts" => [[ stated_year ]]} if !stated_year.blank? && stated_year.to_s != year.to_s
    a['language'] = Language.find(language_id).english_name.to_s unless language_id.nil?
    a['translated-title'] = alternate_values.where(type: "AlternateValue::Translation", alternate_value_object_attribute: 'title').pluck(:value).first
    a['note'] = note
    a.reject! { |k| k == 'note' } if note.blank?
    a
  end

  # @return [String, nil]
  #  priority is Person, string
  #  !! Not the cached value !!
  def get_author
    a = authors.load
    if a.any?
      get_bibtex_names('author')
    else
      author.blank? ? nil : author
    end
  end

  # @return [BibTex::Bibliography]
  #   initialized with this Source as an entry
  def bibtex_bibliography
    TaxonWorks::Vendor::BibtexRuby.bibliography([self])
 end

  # @param [String] style
  # @param [String] format
  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text', normalize_names = true)
    s = ::TaxonWorks::Vendor::BibtexRuby.get_style(style)
    cp = CiteProc::Processor.new(style: s, format: format)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names
    cp.import( [to_citeproc(normalize_names)] )
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end

  # @param [String] format
  # @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(DEFAULT_CSL_STYLE, format)
    #str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  # @return [String, nil]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized People before BibTeX `author`
  #   !! This is NOT a legal BibTeX format  !!
  def authority_name(reload = true)
    reload ? authors.reload : authors.load
    if !authors.any? # no normalized people, use string, !! not .any? because of in-memory setting?!
      if author.blank?
        return nil
      else
        b = to_bibtex
        ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b)
        return Utilities::Strings.authorship_sentence(b.author.tokens.collect{ |t| t.last })
      end
    else # use normalized records
      #      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.full_last_name })
      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.last_name })
    end
  end

  # TODO: Replace with taxonworks.csl.  Move unsupported fields to
  # wrappers in vue rendering.
  # set cached values and copies active record relations into bibtex values
  # @return [Ignored]
  def set_cached
    if errors.empty?
      attributes_to_update = {}
      attributes_to_update[:author] = get_bibtex_names('author') if authors.reload.size > 0
      attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0
      attributes_to_update.merge!(
        cached: get_cached,
        cached_nomenclature_date: nomenclature_date,
        cached_author_string: authority_name(false)
      )
      update_columns(attributes_to_update)
    end
  end

  def get_cached
    if errors.empty?
      c = cached_string('html') # preserves our convention of <i>
      return c
    end
    nil
  end

  # @param [String] type either `author` or `editor`
  # @return [String]
  #   The BibTeX version of the name strings created from People
  #   BibTeX format is 'lastname, firstname and lastname, firstname and lastname, firstname'
  #   This only references People, i.e. `authors` and `editors`.
  #   !! Do not adapt to reference the BibTeX attributes `author` or `editor`
  def get_bibtex_names(role_type)
    # so, we can not reload here
    send("#{role_type}s").collect{ |a| a.bibtex_name}.join(' and ')
  end

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

  protected

  def validate_year_suffix
    a = get_author
    unless year_suffix.blank? || year.blank? || a.blank?
      if new_record?
        s = Source.where(author: a, year: year, year_suffix: year_suffix).first
      else
        s = Source.where(author: a, year: year, year_suffix: year_suffix).not_self(self).first
      end
      errors.add(:year_suffix, " '#{year_suffix}' is already used for #{a} #{year}") unless s.nil?
    end
  end

  def italics_are_paired
    l = title.scan('<i>')&.count
    r = title.scan('</i>')&.count
    errors.add(:title, 'italic markup is not paired') unless l == r
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  # @return [Ignored]
  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

  def sv_cached_names # this cannot be moved to soft_validation_extensions
    is_cached = true

    if (author.to_s != get_bibtex_names('author') && !get_bibtex_names('author').blank?) ||
        (editor.to_s != get_bibtex_names('editor') && !get_bibtex_names('editor').blank?) ||
        cached != get_cached ||
        cached_nomenclature_date != nomenclature_date ||
        cached_author_string.to_s != authority_name(false)
      is_cached = false
    end

    soft_validations.add(
      :base, 'Cached values should be updated',
      success_message: 'Cached values were updated',
      failure_message:  'Failed to update cached values') if !is_cached
  end
end

#address#String?

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

Returns:

  • (#String)

    the address

  • (nil)

    means the attribute is not stored in the database.



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

class Source::Bibtex < Source

  # Type will change
  DEFAULT_CSL_STYLE = 'taxonworks'
  #DEFAULT_CSL_STYLE = 'zootaxa'

  attr_accessor :authors_to_create

  include Shared::OriginRelationship
  include Source::Bibtex::SoftValidationExtensions::Instance
  extend Source::Bibtex::SoftValidationExtensions::Klass

  is_origin_for 'Source::Bibtex', 'Source::Verbatim'
  originates_from 'Source::Bibtex', 'Source::Verbatim'

  GRAPH_ENTRY_POINTS = [:origin_relationships]

  # Used in soft validation
  BIBTEX_REQUIRED_FIELDS = {
    article: [:author, :title, :journal, :year],
    book: [:author, :editor, :title, :publisher, :year],
    booklet: [:title],
    conference: [:author, :title, :booktitle, :year],
    inbook: [:author, :editor, :title, :chapter, :pages, :publisher, :year],
    incollection: [:author, :title, :booktitle, :publisher, :year],
    inproceedings:  [:author, :title, :booktitle, :year],
    manual: [:title],
    mastersthesis: [:author, :title, :school, :year],
    misc: [],
    phdthesis: [:author, :title, :school, :year],
    proceedings: [:title, :year],
    techreport: [:author,:title,:institution, :year],
    unpublished: [:author, :title, :note]
  }

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

  IGNORE_SIMILAR = [:verbatim, :cached, :cached_author_string, :cached_nomenclature_date].freeze
  IGNORE_IDENTICAL = IGNORE_SIMILAR.dup.freeze

  belongs_to :serial, inverse_of: :sources

  # handle conflict with BibTex language field.
  belongs_to :source_language, class_name: 'Language', foreign_key: :language_id, inverse_of: :sources

  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

  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor',
    as: :role_object, validate: true
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true

  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, :serial, allow_destroy: true

  before_validation :create_authors, if: -> { !authors_to_create.nil? }
  before_validation :check_has_field

  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: 'is required when month or stated_year is provided'

  validates :year, date_year: {
    min_year: 1000, max_year: Time.now.year + 2,
    message: 'must be an integer greater than 999 and no more than 2 years in the future'}

  validates_presence_of :month,
    unless: -> { day.blank? },
    message: 'is required when day is provided'

  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_blank: true,
    message: ' month'

  validates :day, date_day: {year_sym: :year, month_sym: :month},
            unless: -> { year.blank? || month.blank? }

  validates :url, format: {
    with: URI::regexp(%w(http https ftp)),
    message: '[%{value}] is not a valid URL'}, allow_blank: true

  validate :italics_are_paired, unless: -> { title.blank? }
  validate :validate_year_suffix, unless: -> { self.no_year_suffix_validation || (self.type != 'Source::Bibtex') }

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_nomenclature_date) }

  # @param [BibTeX::Name] bibtex_author
  # @return [Person, Boolean] new person, or false
  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    p = Person.new(
      first_name: bibtex_author.first,
      prefix: bibtex_author.prefix,
      last_name: bibtex_author.last,
      suffix: bibtex_author.suffix)
    p.namecase_names
    p
  end

  # @return [Source::Bibtex.new]
  #   Adds errors if parse error exists. Note these
  # errors are lost if save/valid? is called again on the object.
  def self.new_from_bibtex_text(text = nil)
    source = Source::Bibtex.new
    begin
      a = BibTeX::Bibliography.parse(text, filter: :latex).first
      if a.class.name == 'BibTeX::Error'
        source.errors.add(:base, 'Unable to parse BibTeX entry. Possible error at end of: ' + a.content)
        return source
      else
        return new_from_bibtex(a)
      end
    rescue BibTeX::ParseError => e
      source.errors.add(:base, 'Unable to parse BibTeX entry: ' + e.to_s)
      return source
    end
  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_from_bibtex(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: Serial with alternate_value on name .count = 1 assign .first
  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|
      next if key == :serial # Raises if it hits the belongs_to

      if key == :keywords
        s.verbatim_keywords = value
        next
      end

      v = value.to_s.strip

      if s.respond_to?(key.to_sym) && key != :type
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end

    s.data_attributes_attributes = import_attributes

    # See issn=() for code matching to existing serials that preceeds this logic
    if s.serial_id.blank? && !bibtex_entry.fields[:journal].to_s.blank? && !bibtex_entry.fields[:issn].to_s.blank?
      a = {
        name: bibtex_entry.fields[:journal].to_s,
        publisher: bibtex_entry.fields[:publisher].to_s,
        identifiers_attributes: [ {
          identifier: bibtex_entry.fields[:issn].to_s,
          type: 'Identifier::Global::Issn'
        } ]
      }

      s.serial_attributes = a
    end
    s
  end

  # @return [Array] journal, nil or name
  def journal
    [read_attribute(:journal), (serial.blank? ? nil : serial.name)].compact.first
  end

  # @return [String]
  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    to_bibtex.valid?
  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
    [year, year_suffix].compact.join
  end

  # TODO: Not used
  #
  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now.
  #
  # @return [Array, Boolean] of names, or false
  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
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(bibtex)

    begin
      Role.transaction do
        if bibtex.authors
          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
          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

  #region getters & setters

  # @param [String, Integer] value
  # @return [Integer] value of year
  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

  # @param [String] value
  # @return [String]
  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
  # @param [String] value
  # @return [String]
  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

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def isbn
    identifier_string_of_type('Identifier::Global::Isbn')
  end

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def doi
    identifier_string_of_type('Identifier::Global::Doi')
  end

  # @param [String] value
  # @return [String]
  def issn=(value)
    # Only add ISSN if it is reasonable to assume its repeated
    # It is likely that most ISSN belong on Serials
    unless %w{article inbook phdthesis inproceedings}.include?(bibtex_type)
      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
    else
      # Do some work to assign a Serial if possible
      # Check for Journal by ISSN
      s = Serial.where(name: journal).first
      i = Identifier::Global::Issn.where(identifier: value, identifier_object_type: 'Serial').first

      # Found an Existing Serial identically named with an assigned Identical ISSN
      if !s.nil? && (s == i&.identifier_object)
        write_attribute(:serial_id, s.id)
      elsif i && s.nil? # Found a Serial with an Identifier, but not identically named, assign it anyway
        write_attribute(:serial_id, i.identifier_object_id)
      end
    end
  end

  # @return [String]
  def issn
    identifier_string_of_type('Identifier::Global::Issn')
  end

  # turn bibtex URL field into a Ruby URI object
  # @return [URI]
  def url_as_uri
    URI(self.url) if self.url.present?
  end

  # @param [String] type_value
  # @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_value)
    # Also handle in memory
    identifiers.each do |i|
      return i.identifier if i.type == type_value
    end
    nil
    # identifiers.where(type: type_value).first&.identifier
  end

 #endregion getters & setters

  # @return [Boolean]
  # is there a bibtex author or author roles?
  def has_authors?
    return true if !author.blank?
    return false if new_record?
    # self exists in the db
    authors.count > 0 ? true : false
  end

  # @return [Boolean]
  def has_editors?
    return true if editor
    # editor attribute is empty
    return false if new_record? # WHY!?
    # self exists in the db
    editors.count > 0 ? true : false
  end

  # @return [Boolean]
  #  true contains either an author or editor
  def has_writer?
    (has_authors? || has_editors?) ? true : false
  end

  # @return [Boolean]
  def has_some_year? # is there a year or stated year?
    return false if year.blank? && stated_year.blank?
    true
  end

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

  # @return [Date || Time] <sigh>
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def cached_nomenclature_date
    if !persisted?
      nomenclature_date
    else
      read_attribute(:cached_nomenclature_date)
    end
  end

  # rubocop:disable Metrics/MethodLength
  # @return [BibTeX::Entry, false]
  #   !! Entry equivalent to self, this should round-trip with no changes.
  def to_bibtex
    return false if bibtex_type.nil?
    b = BibTeX::Entry.new(bibtex_type: bibtex_type)

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank?
        b[f] = v
      end
    end

    b[:keywords] = verbatim_keywords unless verbatim_keywords.blank?
    b[:note] = concatenated_notes_string unless concatenated_notes_string.blank?

    unless serial.nil?
      b[:journal] = serial.name
      issns  = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless serial.nil?
      b[:journal] = serial.name
      issns = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = identifiers.where(type: 'Identifier::Global::Uri')
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = identifiers.where(type: 'Identifier::Global::Isbn')
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = identifiers.where(type: 'Identifier::Global::Doi') #.of_type(:isbn)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    # Overiden by `author` and `editor` if present
    b.author = get_bibtex_names('author') if author_roles.load.any? # unless (!authors.load.any? && author.blank?)
    b.editor = get_bibtex_names('editor') if editor_roles.load.any? # unless (!editors.load.any? && editor.blank?)

    # TODO: use global_id or replace with UUID or DOI if available
    b.key = id unless new_record?
    b
  end

  # @return Hash
  #   a to_citeproc with values updated for literal
  #   handling via `{}` in TaxonWorks
  def to_citeproc(normalize_names = true)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names

    a = b.to_citeproc

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank? && (v.to_s =~ /\A{(.*)}\z/)
        a[f.to_s] = {literal: $1}
      end
    end

    a['year-suffix'] = year_suffix unless year_suffix.blank?
    a['original-date'] = {"date-parts" => [[ stated_year ]]} if !stated_year.blank? && stated_year.to_s != year.to_s
    a['language'] = Language.find(language_id).english_name.to_s unless language_id.nil?
    a['translated-title'] = alternate_values.where(type: "AlternateValue::Translation", alternate_value_object_attribute: 'title').pluck(:value).first
    a['note'] = note
    a.reject! { |k| k == 'note' } if note.blank?
    a
  end

  # @return [String, nil]
  #  priority is Person, string
  #  !! Not the cached value !!
  def get_author
    a = authors.load
    if a.any?
      get_bibtex_names('author')
    else
      author.blank? ? nil : author
    end
  end

  # @return [BibTex::Bibliography]
  #   initialized with this Source as an entry
  def bibtex_bibliography
    TaxonWorks::Vendor::BibtexRuby.bibliography([self])
 end

  # @param [String] style
  # @param [String] format
  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text', normalize_names = true)
    s = ::TaxonWorks::Vendor::BibtexRuby.get_style(style)
    cp = CiteProc::Processor.new(style: s, format: format)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names
    cp.import( [to_citeproc(normalize_names)] )
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end

  # @param [String] format
  # @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(DEFAULT_CSL_STYLE, format)
    #str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  # @return [String, nil]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized People before BibTeX `author`
  #   !! This is NOT a legal BibTeX format  !!
  def authority_name(reload = true)
    reload ? authors.reload : authors.load
    if !authors.any? # no normalized people, use string, !! not .any? because of in-memory setting?!
      if author.blank?
        return nil
      else
        b = to_bibtex
        ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b)
        return Utilities::Strings.authorship_sentence(b.author.tokens.collect{ |t| t.last })
      end
    else # use normalized records
      #      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.full_last_name })
      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.last_name })
    end
  end

  # TODO: Replace with taxonworks.csl.  Move unsupported fields to
  # wrappers in vue rendering.
  # set cached values and copies active record relations into bibtex values
  # @return [Ignored]
  def set_cached
    if errors.empty?
      attributes_to_update = {}
      attributes_to_update[:author] = get_bibtex_names('author') if authors.reload.size > 0
      attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0
      attributes_to_update.merge!(
        cached: get_cached,
        cached_nomenclature_date: nomenclature_date,
        cached_author_string: authority_name(false)
      )
      update_columns(attributes_to_update)
    end
  end

  def get_cached
    if errors.empty?
      c = cached_string('html') # preserves our convention of <i>
      return c
    end
    nil
  end

  # @param [String] type either `author` or `editor`
  # @return [String]
  #   The BibTeX version of the name strings created from People
  #   BibTeX format is 'lastname, firstname and lastname, firstname and lastname, firstname'
  #   This only references People, i.e. `authors` and `editors`.
  #   !! Do not adapt to reference the BibTeX attributes `author` or `editor`
  def get_bibtex_names(role_type)
    # so, we can not reload here
    send("#{role_type}s").collect{ |a| a.bibtex_name}.join(' and ')
  end

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

  protected

  def validate_year_suffix
    a = get_author
    unless year_suffix.blank? || year.blank? || a.blank?
      if new_record?
        s = Source.where(author: a, year: year, year_suffix: year_suffix).first
      else
        s = Source.where(author: a, year: year, year_suffix: year_suffix).not_self(self).first
      end
      errors.add(:year_suffix, " '#{year_suffix}' is already used for #{a} #{year}") unless s.nil?
    end
  end

  def italics_are_paired
    l = title.scan('<i>')&.count
    r = title.scan('</i>')&.count
    errors.add(:title, 'italic markup is not paired') unless l == r
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  # @return [Ignored]
  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

  def sv_cached_names # this cannot be moved to soft_validation_extensions
    is_cached = true

    if (author.to_s != get_bibtex_names('author') && !get_bibtex_names('author').blank?) ||
        (editor.to_s != get_bibtex_names('editor') && !get_bibtex_names('editor').blank?) ||
        cached != get_cached ||
        cached_nomenclature_date != nomenclature_date ||
        cached_author_string.to_s != authority_name(false)
      is_cached = false
    end

    soft_validations.add(
      :base, 'Cached values should be updated',
      success_message: 'Cached values were updated',
      failure_message:  'Failed to update cached values') if !is_cached
  end
end

#annoteString?

BibTeX standard field (ignored by standard processors) An annotation. It is not used by the standard bibliography styles, but may be used by others that produce an annotated bibliography. (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.



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

class Source::Bibtex < Source

  # Type will change
  DEFAULT_CSL_STYLE = 'taxonworks'
  #DEFAULT_CSL_STYLE = 'zootaxa'

  attr_accessor :authors_to_create

  include Shared::OriginRelationship
  include Source::Bibtex::SoftValidationExtensions::Instance
  extend Source::Bibtex::SoftValidationExtensions::Klass

  is_origin_for 'Source::Bibtex', 'Source::Verbatim'
  originates_from 'Source::Bibtex', 'Source::Verbatim'

  GRAPH_ENTRY_POINTS = [:origin_relationships]

  # Used in soft validation
  BIBTEX_REQUIRED_FIELDS = {
    article: [:author, :title, :journal, :year],
    book: [:author, :editor, :title, :publisher, :year],
    booklet: [:title],
    conference: [:author, :title, :booktitle, :year],
    inbook: [:author, :editor, :title, :chapter, :pages, :publisher, :year],
    incollection: [:author, :title, :booktitle, :publisher, :year],
    inproceedings:  [:author, :title, :booktitle, :year],
    manual: [:title],
    mastersthesis: [:author, :title, :school, :year],
    misc: [],
    phdthesis: [:author, :title, :school, :year],
    proceedings: [:title, :year],
    techreport: [:author,:title,:institution, :year],
    unpublished: [:author, :title, :note]
  }

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

  IGNORE_SIMILAR = [:verbatim, :cached, :cached_author_string, :cached_nomenclature_date].freeze
  IGNORE_IDENTICAL = IGNORE_SIMILAR.dup.freeze

  belongs_to :serial, inverse_of: :sources

  # handle conflict with BibTex language field.
  belongs_to :source_language, class_name: 'Language', foreign_key: :language_id, inverse_of: :sources

  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

  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor',
    as: :role_object, validate: true
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true

  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, :serial, allow_destroy: true

  before_validation :create_authors, if: -> { !authors_to_create.nil? }
  before_validation :check_has_field

  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: 'is required when month or stated_year is provided'

  validates :year, date_year: {
    min_year: 1000, max_year: Time.now.year + 2,
    message: 'must be an integer greater than 999 and no more than 2 years in the future'}

  validates_presence_of :month,
    unless: -> { day.blank? },
    message: 'is required when day is provided'

  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_blank: true,
    message: ' month'

  validates :day, date_day: {year_sym: :year, month_sym: :month},
            unless: -> { year.blank? || month.blank? }

  validates :url, format: {
    with: URI::regexp(%w(http https ftp)),
    message: '[%{value}] is not a valid URL'}, allow_blank: true

  validate :italics_are_paired, unless: -> { title.blank? }
  validate :validate_year_suffix, unless: -> { self.no_year_suffix_validation || (self.type != 'Source::Bibtex') }

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_nomenclature_date) }

  # @param [BibTeX::Name] bibtex_author
  # @return [Person, Boolean] new person, or false
  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    p = Person.new(
      first_name: bibtex_author.first,
      prefix: bibtex_author.prefix,
      last_name: bibtex_author.last,
      suffix: bibtex_author.suffix)
    p.namecase_names
    p
  end

  # @return [Source::Bibtex.new]
  #   Adds errors if parse error exists. Note these
  # errors are lost if save/valid? is called again on the object.
  def self.new_from_bibtex_text(text = nil)
    source = Source::Bibtex.new
    begin
      a = BibTeX::Bibliography.parse(text, filter: :latex).first
      if a.class.name == 'BibTeX::Error'
        source.errors.add(:base, 'Unable to parse BibTeX entry. Possible error at end of: ' + a.content)
        return source
      else
        return new_from_bibtex(a)
      end
    rescue BibTeX::ParseError => e
      source.errors.add(:base, 'Unable to parse BibTeX entry: ' + e.to_s)
      return source
    end
  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_from_bibtex(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: Serial with alternate_value on name .count = 1 assign .first
  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|
      next if key == :serial # Raises if it hits the belongs_to

      if key == :keywords
        s.verbatim_keywords = value
        next
      end

      v = value.to_s.strip

      if s.respond_to?(key.to_sym) && key != :type
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end

    s.data_attributes_attributes = import_attributes

    # See issn=() for code matching to existing serials that preceeds this logic
    if s.serial_id.blank? && !bibtex_entry.fields[:journal].to_s.blank? && !bibtex_entry.fields[:issn].to_s.blank?
      a = {
        name: bibtex_entry.fields[:journal].to_s,
        publisher: bibtex_entry.fields[:publisher].to_s,
        identifiers_attributes: [ {
          identifier: bibtex_entry.fields[:issn].to_s,
          type: 'Identifier::Global::Issn'
        } ]
      }

      s.serial_attributes = a
    end
    s
  end

  # @return [Array] journal, nil or name
  def journal
    [read_attribute(:journal), (serial.blank? ? nil : serial.name)].compact.first
  end

  # @return [String]
  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    to_bibtex.valid?
  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
    [year, year_suffix].compact.join
  end

  # TODO: Not used
  #
  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now.
  #
  # @return [Array, Boolean] of names, or false
  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
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(bibtex)

    begin
      Role.transaction do
        if bibtex.authors
          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
          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

  #region getters & setters

  # @param [String, Integer] value
  # @return [Integer] value of year
  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

  # @param [String] value
  # @return [String]
  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
  # @param [String] value
  # @return [String]
  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

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def isbn
    identifier_string_of_type('Identifier::Global::Isbn')
  end

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def doi
    identifier_string_of_type('Identifier::Global::Doi')
  end

  # @param [String] value
  # @return [String]
  def issn=(value)
    # Only add ISSN if it is reasonable to assume its repeated
    # It is likely that most ISSN belong on Serials
    unless %w{article inbook phdthesis inproceedings}.include?(bibtex_type)
      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
    else
      # Do some work to assign a Serial if possible
      # Check for Journal by ISSN
      s = Serial.where(name: journal).first
      i = Identifier::Global::Issn.where(identifier: value, identifier_object_type: 'Serial').first

      # Found an Existing Serial identically named with an assigned Identical ISSN
      if !s.nil? && (s == i&.identifier_object)
        write_attribute(:serial_id, s.id)
      elsif i && s.nil? # Found a Serial with an Identifier, but not identically named, assign it anyway
        write_attribute(:serial_id, i.identifier_object_id)
      end
    end
  end

  # @return [String]
  def issn
    identifier_string_of_type('Identifier::Global::Issn')
  end

  # turn bibtex URL field into a Ruby URI object
  # @return [URI]
  def url_as_uri
    URI(self.url) if self.url.present?
  end

  # @param [String] type_value
  # @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_value)
    # Also handle in memory
    identifiers.each do |i|
      return i.identifier if i.type == type_value
    end
    nil
    # identifiers.where(type: type_value).first&.identifier
  end

 #endregion getters & setters

  # @return [Boolean]
  # is there a bibtex author or author roles?
  def has_authors?
    return true if !author.blank?
    return false if new_record?
    # self exists in the db
    authors.count > 0 ? true : false
  end

  # @return [Boolean]
  def has_editors?
    return true if editor
    # editor attribute is empty
    return false if new_record? # WHY!?
    # self exists in the db
    editors.count > 0 ? true : false
  end

  # @return [Boolean]
  #  true contains either an author or editor
  def has_writer?
    (has_authors? || has_editors?) ? true : false
  end

  # @return [Boolean]
  def has_some_year? # is there a year or stated year?
    return false if year.blank? && stated_year.blank?
    true
  end

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

  # @return [Date || Time] <sigh>
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def cached_nomenclature_date
    if !persisted?
      nomenclature_date
    else
      read_attribute(:cached_nomenclature_date)
    end
  end

  # rubocop:disable Metrics/MethodLength
  # @return [BibTeX::Entry, false]
  #   !! Entry equivalent to self, this should round-trip with no changes.
  def to_bibtex
    return false if bibtex_type.nil?
    b = BibTeX::Entry.new(bibtex_type: bibtex_type)

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank?
        b[f] = v
      end
    end

    b[:keywords] = verbatim_keywords unless verbatim_keywords.blank?
    b[:note] = concatenated_notes_string unless concatenated_notes_string.blank?

    unless serial.nil?
      b[:journal] = serial.name
      issns  = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless serial.nil?
      b[:journal] = serial.name
      issns = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = identifiers.where(type: 'Identifier::Global::Uri')
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = identifiers.where(type: 'Identifier::Global::Isbn')
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = identifiers.where(type: 'Identifier::Global::Doi') #.of_type(:isbn)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    # Overiden by `author` and `editor` if present
    b.author = get_bibtex_names('author') if author_roles.load.any? # unless (!authors.load.any? && author.blank?)
    b.editor = get_bibtex_names('editor') if editor_roles.load.any? # unless (!editors.load.any? && editor.blank?)

    # TODO: use global_id or replace with UUID or DOI if available
    b.key = id unless new_record?
    b
  end

  # @return Hash
  #   a to_citeproc with values updated for literal
  #   handling via `{}` in TaxonWorks
  def to_citeproc(normalize_names = true)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names

    a = b.to_citeproc

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank? && (v.to_s =~ /\A{(.*)}\z/)
        a[f.to_s] = {literal: $1}
      end
    end

    a['year-suffix'] = year_suffix unless year_suffix.blank?
    a['original-date'] = {"date-parts" => [[ stated_year ]]} if !stated_year.blank? && stated_year.to_s != year.to_s
    a['language'] = Language.find(language_id).english_name.to_s unless language_id.nil?
    a['translated-title'] = alternate_values.where(type: "AlternateValue::Translation", alternate_value_object_attribute: 'title').pluck(:value).first
    a['note'] = note
    a.reject! { |k| k == 'note' } if note.blank?
    a
  end

  # @return [String, nil]
  #  priority is Person, string
  #  !! Not the cached value !!
  def get_author
    a = authors.load
    if a.any?
      get_bibtex_names('author')
    else
      author.blank? ? nil : author
    end
  end

  # @return [BibTex::Bibliography]
  #   initialized with this Source as an entry
  def bibtex_bibliography
    TaxonWorks::Vendor::BibtexRuby.bibliography([self])
 end

  # @param [String] style
  # @param [String] format
  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text', normalize_names = true)
    s = ::TaxonWorks::Vendor::BibtexRuby.get_style(style)
    cp = CiteProc::Processor.new(style: s, format: format)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names
    cp.import( [to_citeproc(normalize_names)] )
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end

  # @param [String] format
  # @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(DEFAULT_CSL_STYLE, format)
    #str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  # @return [String, nil]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized People before BibTeX `author`
  #   !! This is NOT a legal BibTeX format  !!
  def authority_name(reload = true)
    reload ? authors.reload : authors.load
    if !authors.any? # no normalized people, use string, !! not .any? because of in-memory setting?!
      if author.blank?
        return nil
      else
        b = to_bibtex
        ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b)
        return Utilities::Strings.authorship_sentence(b.author.tokens.collect{ |t| t.last })
      end
    else # use normalized records
      #      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.full_last_name })
      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.last_name })
    end
  end

  # TODO: Replace with taxonworks.csl.  Move unsupported fields to
  # wrappers in vue rendering.
  # set cached values and copies active record relations into bibtex values
  # @return [Ignored]
  def set_cached
    if errors.empty?
      attributes_to_update = {}
      attributes_to_update[:author] = get_bibtex_names('author') if authors.reload.size > 0
      attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0
      attributes_to_update.merge!(
        cached: get_cached,
        cached_nomenclature_date: nomenclature_date,
        cached_author_string: authority_name(false)
      )
      update_columns(attributes_to_update)
    end
  end

  def get_cached
    if errors.empty?
      c = cached_string('html') # preserves our convention of <i>
      return c
    end
    nil
  end

  # @param [String] type either `author` or `editor`
  # @return [String]
  #   The BibTeX version of the name strings created from People
  #   BibTeX format is 'lastname, firstname and lastname, firstname and lastname, firstname'
  #   This only references People, i.e. `authors` and `editors`.
  #   !! Do not adapt to reference the BibTeX attributes `author` or `editor`
  def get_bibtex_names(role_type)
    # so, we can not reload here
    send("#{role_type}s").collect{ |a| a.bibtex_name}.join(' and ')
  end

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

  protected

  def validate_year_suffix
    a = get_author
    unless year_suffix.blank? || year.blank? || a.blank?
      if new_record?
        s = Source.where(author: a, year: year, year_suffix: year_suffix).first
      else
        s = Source.where(author: a, year: year, year_suffix: year_suffix).not_self(self).first
      end
      errors.add(:year_suffix, " '#{year_suffix}' is already used for #{a} #{year}") unless s.nil?
    end
  end

  def italics_are_paired
    l = title.scan('<i>')&.count
    r = title.scan('</i>')&.count
    errors.add(:title, 'italic markup is not paired') unless l == r
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  # @return [Ignored]
  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

  def sv_cached_names # this cannot be moved to soft_validation_extensions
    is_cached = true

    if (author.to_s != get_bibtex_names('author') && !get_bibtex_names('author').blank?) ||
        (editor.to_s != get_bibtex_names('editor') && !get_bibtex_names('editor').blank?) ||
        cached != get_cached ||
        cached_nomenclature_date != nomenclature_date ||
        cached_author_string.to_s != authority_name(false)
      is_cached = false
    end

    soft_validations.add(
      :base, 'Cached values should be updated',
      success_message: 'Cached values were updated',
      failure_message:  'Failed to update cached values') if !is_cached
  end
end

#authorString?

“Last name, FirstName MiddleName”. FirstName and MiddleName can be initials. Additional authors are joined with ` and `. All names before the comma are treated as a single last name.

The contents of `author` follow the following rules:

  • `author` (a) and `authors` (People) (b) can both be used to generate the author string

  • if a & !b then `author` = a verbatim (and therefor may not match the BibTeX format)

  • if !a & b then `author` = b, collected and rendered in BibTeX format

  • if a & b then `author` = b, collected and rendered in BibTeX format on each update. !! Updates to `author` directly will be overwritten !!

`author` is automatically populated from `authors` if the latter is provided !! This is different behavious from TaxonName, where `verbatim_author` has priority over taxon_name_author (People) in rendering.

See also `cached_author_string`

Returns:

  • (String, nil)

    author names preferably rendered in BibTeX format,



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

class Source::Bibtex < Source

  # Type will change
  DEFAULT_CSL_STYLE = 'taxonworks'
  #DEFAULT_CSL_STYLE = 'zootaxa'

  attr_accessor :authors_to_create

  include Shared::OriginRelationship
  include Source::Bibtex::SoftValidationExtensions::Instance
  extend Source::Bibtex::SoftValidationExtensions::Klass

  is_origin_for 'Source::Bibtex', 'Source::Verbatim'
  originates_from 'Source::Bibtex', 'Source::Verbatim'

  GRAPH_ENTRY_POINTS = [:origin_relationships]

  # Used in soft validation
  BIBTEX_REQUIRED_FIELDS = {
    article: [:author, :title, :journal, :year],
    book: [:author, :editor, :title, :publisher, :year],
    booklet: [:title],
    conference: [:author, :title, :booktitle, :year],
    inbook: [:author, :editor, :title, :chapter, :pages, :publisher, :year],
    incollection: [:author, :title, :booktitle, :publisher, :year],
    inproceedings:  [:author, :title, :booktitle, :year],
    manual: [:title],
    mastersthesis: [:author, :title, :school, :year],
    misc: [],
    phdthesis: [:author, :title, :school, :year],
    proceedings: [:title, :year],
    techreport: [:author,:title,:institution, :year],
    unpublished: [:author, :title, :note]
  }

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

  IGNORE_SIMILAR = [:verbatim, :cached, :cached_author_string, :cached_nomenclature_date].freeze
  IGNORE_IDENTICAL = IGNORE_SIMILAR.dup.freeze

  belongs_to :serial, inverse_of: :sources

  # handle conflict with BibTex language field.
  belongs_to :source_language, class_name: 'Language', foreign_key: :language_id, inverse_of: :sources

  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

  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor',
    as: :role_object, validate: true
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true

  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, :serial, allow_destroy: true

  before_validation :create_authors, if: -> { !authors_to_create.nil? }
  before_validation :check_has_field

  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: 'is required when month or stated_year is provided'

  validates :year, date_year: {
    min_year: 1000, max_year: Time.now.year + 2,
    message: 'must be an integer greater than 999 and no more than 2 years in the future'}

  validates_presence_of :month,
    unless: -> { day.blank? },
    message: 'is required when day is provided'

  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_blank: true,
    message: ' month'

  validates :day, date_day: {year_sym: :year, month_sym: :month},
            unless: -> { year.blank? || month.blank? }

  validates :url, format: {
    with: URI::regexp(%w(http https ftp)),
    message: '[%{value}] is not a valid URL'}, allow_blank: true

  validate :italics_are_paired, unless: -> { title.blank? }
  validate :validate_year_suffix, unless: -> { self.no_year_suffix_validation || (self.type != 'Source::Bibtex') }

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_nomenclature_date) }

  # @param [BibTeX::Name] bibtex_author
  # @return [Person, Boolean] new person, or false
  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    p = Person.new(
      first_name: bibtex_author.first,
      prefix: bibtex_author.prefix,
      last_name: bibtex_author.last,
      suffix: bibtex_author.suffix)
    p.namecase_names
    p
  end

  # @return [Source::Bibtex.new]
  #   Adds errors if parse error exists. Note these
  # errors are lost if save/valid? is called again on the object.
  def self.new_from_bibtex_text(text = nil)
    source = Source::Bibtex.new
    begin
      a = BibTeX::Bibliography.parse(text, filter: :latex).first
      if a.class.name == 'BibTeX::Error'
        source.errors.add(:base, 'Unable to parse BibTeX entry. Possible error at end of: ' + a.content)
        return source
      else
        return new_from_bibtex(a)
      end
    rescue BibTeX::ParseError => e
      source.errors.add(:base, 'Unable to parse BibTeX entry: ' + e.to_s)
      return source
    end
  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_from_bibtex(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: Serial with alternate_value on name .count = 1 assign .first
  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|
      next if key == :serial # Raises if it hits the belongs_to

      if key == :keywords
        s.verbatim_keywords = value
        next
      end

      v = value.to_s.strip

      if s.respond_to?(key.to_sym) && key != :type
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end

    s.data_attributes_attributes = import_attributes

    # See issn=() for code matching to existing serials that preceeds this logic
    if s.serial_id.blank? && !bibtex_entry.fields[:journal].to_s.blank? && !bibtex_entry.fields[:issn].to_s.blank?
      a = {
        name: bibtex_entry.fields[:journal].to_s,
        publisher: bibtex_entry.fields[:publisher].to_s,
        identifiers_attributes: [ {
          identifier: bibtex_entry.fields[:issn].to_s,
          type: 'Identifier::Global::Issn'
        } ]
      }

      s.serial_attributes = a
    end
    s
  end

  # @return [Array] journal, nil or name
  def journal
    [read_attribute(:journal), (serial.blank? ? nil : serial.name)].compact.first
  end

  # @return [String]
  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    to_bibtex.valid?
  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
    [year, year_suffix].compact.join
  end

  # TODO: Not used
  #
  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now.
  #
  # @return [Array, Boolean] of names, or false
  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
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(bibtex)

    begin
      Role.transaction do
        if bibtex.authors
          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
          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

  #region getters & setters

  # @param [String, Integer] value
  # @return [Integer] value of year
  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

  # @param [String] value
  # @return [String]
  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
  # @param [String] value
  # @return [String]
  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

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def isbn
    identifier_string_of_type('Identifier::Global::Isbn')
  end

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def doi
    identifier_string_of_type('Identifier::Global::Doi')
  end

  # @param [String] value
  # @return [String]
  def issn=(value)
    # Only add ISSN if it is reasonable to assume its repeated
    # It is likely that most ISSN belong on Serials
    unless %w{article inbook phdthesis inproceedings}.include?(bibtex_type)
      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
    else
      # Do some work to assign a Serial if possible
      # Check for Journal by ISSN
      s = Serial.where(name: journal).first
      i = Identifier::Global::Issn.where(identifier: value, identifier_object_type: 'Serial').first

      # Found an Existing Serial identically named with an assigned Identical ISSN
      if !s.nil? && (s == i&.identifier_object)
        write_attribute(:serial_id, s.id)
      elsif i && s.nil? # Found a Serial with an Identifier, but not identically named, assign it anyway
        write_attribute(:serial_id, i.identifier_object_id)
      end
    end
  end

  # @return [String]
  def issn
    identifier_string_of_type('Identifier::Global::Issn')
  end

  # turn bibtex URL field into a Ruby URI object
  # @return [URI]
  def url_as_uri
    URI(self.url) if self.url.present?
  end

  # @param [String] type_value
  # @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_value)
    # Also handle in memory
    identifiers.each do |i|
      return i.identifier if i.type == type_value
    end
    nil
    # identifiers.where(type: type_value).first&.identifier
  end

 #endregion getters & setters

  # @return [Boolean]
  # is there a bibtex author or author roles?
  def has_authors?
    return true if !author.blank?
    return false if new_record?
    # self exists in the db
    authors.count > 0 ? true : false
  end

  # @return [Boolean]
  def has_editors?
    return true if editor
    # editor attribute is empty
    return false if new_record? # WHY!?
    # self exists in the db
    editors.count > 0 ? true : false
  end

  # @return [Boolean]
  #  true contains either an author or editor
  def has_writer?
    (has_authors? || has_editors?) ? true : false
  end

  # @return [Boolean]
  def has_some_year? # is there a year or stated year?
    return false if year.blank? && stated_year.blank?
    true
  end

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

  # @return [Date || Time] <sigh>
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def cached_nomenclature_date
    if !persisted?
      nomenclature_date
    else
      read_attribute(:cached_nomenclature_date)
    end
  end

  # rubocop:disable Metrics/MethodLength
  # @return [BibTeX::Entry, false]
  #   !! Entry equivalent to self, this should round-trip with no changes.
  def to_bibtex
    return false if bibtex_type.nil?
    b = BibTeX::Entry.new(bibtex_type: bibtex_type)

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank?
        b[f] = v
      end
    end

    b[:keywords] = verbatim_keywords unless verbatim_keywords.blank?
    b[:note] = concatenated_notes_string unless concatenated_notes_string.blank?

    unless serial.nil?
      b[:journal] = serial.name
      issns  = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless serial.nil?
      b[:journal] = serial.name
      issns = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = identifiers.where(type: 'Identifier::Global::Uri')
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = identifiers.where(type: 'Identifier::Global::Isbn')
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = identifiers.where(type: 'Identifier::Global::Doi') #.of_type(:isbn)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    # Overiden by `author` and `editor` if present
    b.author = get_bibtex_names('author') if author_roles.load.any? # unless (!authors.load.any? && author.blank?)
    b.editor = get_bibtex_names('editor') if editor_roles.load.any? # unless (!editors.load.any? && editor.blank?)

    # TODO: use global_id or replace with UUID or DOI if available
    b.key = id unless new_record?
    b
  end

  # @return Hash
  #   a to_citeproc with values updated for literal
  #   handling via `{}` in TaxonWorks
  def to_citeproc(normalize_names = true)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names

    a = b.to_citeproc

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank? && (v.to_s =~ /\A{(.*)}\z/)
        a[f.to_s] = {literal: $1}
      end
    end

    a['year-suffix'] = year_suffix unless year_suffix.blank?
    a['original-date'] = {"date-parts" => [[ stated_year ]]} if !stated_year.blank? && stated_year.to_s != year.to_s
    a['language'] = Language.find(language_id).english_name.to_s unless language_id.nil?
    a['translated-title'] = alternate_values.where(type: "AlternateValue::Translation", alternate_value_object_attribute: 'title').pluck(:value).first
    a['note'] = note
    a.reject! { |k| k == 'note' } if note.blank?
    a
  end

  # @return [String, nil]
  #  priority is Person, string
  #  !! Not the cached value !!
  def get_author
    a = authors.load
    if a.any?
      get_bibtex_names('author')
    else
      author.blank? ? nil : author
    end
  end

  # @return [BibTex::Bibliography]
  #   initialized with this Source as an entry
  def bibtex_bibliography
    TaxonWorks::Vendor::BibtexRuby.bibliography([self])
 end

  # @param [String] style
  # @param [String] format
  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text', normalize_names = true)
    s = ::TaxonWorks::Vendor::BibtexRuby.get_style(style)
    cp = CiteProc::Processor.new(style: s, format: format)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names
    cp.import( [to_citeproc(normalize_names)] )
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end

  # @param [String] format
  # @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(DEFAULT_CSL_STYLE, format)
    #str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  # @return [String, nil]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized People before BibTeX `author`
  #   !! This is NOT a legal BibTeX format  !!
  def authority_name(reload = true)
    reload ? authors.reload : authors.load
    if !authors.any? # no normalized people, use string, !! not .any? because of in-memory setting?!
      if author.blank?
        return nil
      else
        b = to_bibtex
        ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b)
        return Utilities::Strings.authorship_sentence(b.author.tokens.collect{ |t| t.last })
      end
    else # use normalized records
      #      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.full_last_name })
      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.last_name })
    end
  end

  # TODO: Replace with taxonworks.csl.  Move unsupported fields to
  # wrappers in vue rendering.
  # set cached values and copies active record relations into bibtex values
  # @return [Ignored]
  def set_cached
    if errors.empty?
      attributes_to_update = {}
      attributes_to_update[:author] = get_bibtex_names('author') if authors.reload.size > 0
      attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0
      attributes_to_update.merge!(
        cached: get_cached,
        cached_nomenclature_date: nomenclature_date,
        cached_author_string: authority_name(false)
      )
      update_columns(attributes_to_update)
    end
  end

  def get_cached
    if errors.empty?
      c = cached_string('html') # preserves our convention of <i>
      return c
    end
    nil
  end

  # @param [String] type either `author` or `editor`
  # @return [String]
  #   The BibTeX version of the name strings created from People
  #   BibTeX format is 'lastname, firstname and lastname, firstname and lastname, firstname'
  #   This only references People, i.e. `authors` and `editors`.
  #   !! Do not adapt to reference the BibTeX attributes `author` or `editor`
  def get_bibtex_names(role_type)
    # so, we can not reload here
    send("#{role_type}s").collect{ |a| a.bibtex_name}.join(' and ')
  end

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

  protected

  def validate_year_suffix
    a = get_author
    unless year_suffix.blank? || year.blank? || a.blank?
      if new_record?
        s = Source.where(author: a, year: year, year_suffix: year_suffix).first
      else
        s = Source.where(author: a, year: year, year_suffix: year_suffix).not_self(self).first
      end
      errors.add(:year_suffix, " '#{year_suffix}' is already used for #{a} #{year}") unless s.nil?
    end
  end

  def italics_are_paired
    l = title.scan('<i>')&.count
    r = title.scan('</i>')&.count
    errors.add(:title, 'italic markup is not paired') unless l == r
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  # @return [Ignored]
  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

  def sv_cached_names # this cannot be moved to soft_validation_extensions
    is_cached = true

    if (author.to_s != get_bibtex_names('author') && !get_bibtex_names('author').blank?) ||
        (editor.to_s != get_bibtex_names('editor') && !get_bibtex_names('editor').blank?) ||
        cached != get_cached ||
        cached_nomenclature_date != nomenclature_date ||
        cached_author_string.to_s != authority_name(false)
      is_cached = false
    end

    soft_validations.add(
      :base, 'Cached values should be updated',
      success_message: 'Cached values were updated',
      failure_message:  'Failed to update cached values') if !is_cached
  end
end

#authors_to_createObject

DEFAULT_CSL_STYLE = 'zootaxa'



308
309
310
# File 'app/models/source/bibtex.rb', line 308

def authors_to_create
  @authors_to_create
end

#bibtex_typeString

Returns config/initializers/constants/_controlled_vocabularies/bibtex_constants one of VALID_BIBTEX_TYPES (.rb, keys there are symbols).

Returns:

  • (String)

    config/initializers/constants/_controlled_vocabularies/bibtex_constants one of VALID_BIBTEX_TYPES (.rb, keys there are symbols)



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

class Source::Bibtex < Source

  # Type will change
  DEFAULT_CSL_STYLE = 'taxonworks'
  #DEFAULT_CSL_STYLE = 'zootaxa'

  attr_accessor :authors_to_create

  include Shared::OriginRelationship
  include Source::Bibtex::SoftValidationExtensions::Instance
  extend Source::Bibtex::SoftValidationExtensions::Klass

  is_origin_for 'Source::Bibtex', 'Source::Verbatim'
  originates_from 'Source::Bibtex', 'Source::Verbatim'

  GRAPH_ENTRY_POINTS = [:origin_relationships]

  # Used in soft validation
  BIBTEX_REQUIRED_FIELDS = {
    article: [:author, :title, :journal, :year],
    book: [:author, :editor, :title, :publisher, :year],
    booklet: [:title],
    conference: [:author, :title, :booktitle, :year],
    inbook: [:author, :editor, :title, :chapter, :pages, :publisher, :year],
    incollection: [:author, :title, :booktitle, :publisher, :year],
    inproceedings:  [:author, :title, :booktitle, :year],
    manual: [:title],
    mastersthesis: [:author, :title, :school, :year],
    misc: [],
    phdthesis: [:author, :title, :school, :year],
    proceedings: [:title, :year],
    techreport: [:author,:title,:institution, :year],
    unpublished: [:author, :title, :note]
  }

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

  IGNORE_SIMILAR = [:verbatim, :cached, :cached_author_string, :cached_nomenclature_date].freeze
  IGNORE_IDENTICAL = IGNORE_SIMILAR.dup.freeze

  belongs_to :serial, inverse_of: :sources

  # handle conflict with BibTex language field.
  belongs_to :source_language, class_name: 'Language', foreign_key: :language_id, inverse_of: :sources

  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

  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor',
    as: :role_object, validate: true
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true

  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, :serial, allow_destroy: true

  before_validation :create_authors, if: -> { !authors_to_create.nil? }
  before_validation :check_has_field

  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: 'is required when month or stated_year is provided'

  validates :year, date_year: {
    min_year: 1000, max_year: Time.now.year + 2,
    message: 'must be an integer greater than 999 and no more than 2 years in the future'}

  validates_presence_of :month,
    unless: -> { day.blank? },
    message: 'is required when day is provided'

  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_blank: true,
    message: ' month'

  validates :day, date_day: {year_sym: :year, month_sym: :month},
            unless: -> { year.blank? || month.blank? }

  validates :url, format: {
    with: URI::regexp(%w(http https ftp)),
    message: '[%{value}] is not a valid URL'}, allow_blank: true

  validate :italics_are_paired, unless: -> { title.blank? }
  validate :validate_year_suffix, unless: -> { self.no_year_suffix_validation || (self.type != 'Source::Bibtex') }

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_nomenclature_date) }

  # @param [BibTeX::Name] bibtex_author
  # @return [Person, Boolean] new person, or false
  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    p = Person.new(
      first_name: bibtex_author.first,
      prefix: bibtex_author.prefix,
      last_name: bibtex_author.last,
      suffix: bibtex_author.suffix)
    p.namecase_names
    p
  end

  # @return [Source::Bibtex.new]
  #   Adds errors if parse error exists. Note these
  # errors are lost if save/valid? is called again on the object.
  def self.new_from_bibtex_text(text = nil)
    source = Source::Bibtex.new
    begin
      a = BibTeX::Bibliography.parse(text, filter: :latex).first
      if a.class.name == 'BibTeX::Error'
        source.errors.add(:base, 'Unable to parse BibTeX entry. Possible error at end of: ' + a.content)
        return source
      else
        return new_from_bibtex(a)
      end
    rescue BibTeX::ParseError => e
      source.errors.add(:base, 'Unable to parse BibTeX entry: ' + e.to_s)
      return source
    end
  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_from_bibtex(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: Serial with alternate_value on name .count = 1 assign .first
  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|
      next if key == :serial # Raises if it hits the belongs_to

      if key == :keywords
        s.verbatim_keywords = value
        next
      end

      v = value.to_s.strip

      if s.respond_to?(key.to_sym) && key != :type
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end

    s.data_attributes_attributes = import_attributes

    # See issn=() for code matching to existing serials that preceeds this logic
    if s.serial_id.blank? && !bibtex_entry.fields[:journal].to_s.blank? && !bibtex_entry.fields[:issn].to_s.blank?
      a = {
        name: bibtex_entry.fields[:journal].to_s,
        publisher: bibtex_entry.fields[:publisher].to_s,
        identifiers_attributes: [ {
          identifier: bibtex_entry.fields[:issn].to_s,
          type: 'Identifier::Global::Issn'
        } ]
      }

      s.serial_attributes = a
    end
    s
  end

  # @return [Array] journal, nil or name
  def journal
    [read_attribute(:journal), (serial.blank? ? nil : serial.name)].compact.first
  end

  # @return [String]
  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    to_bibtex.valid?
  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
    [year, year_suffix].compact.join
  end

  # TODO: Not used
  #
  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now.
  #
  # @return [Array, Boolean] of names, or false
  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
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(bibtex)

    begin
      Role.transaction do
        if bibtex.authors
          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
          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

  #region getters & setters

  # @param [String, Integer] value
  # @return [Integer] value of year
  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

  # @param [String] value
  # @return [String]
  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
  # @param [String] value
  # @return [String]
  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

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def isbn
    identifier_string_of_type('Identifier::Global::Isbn')
  end

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def doi
    identifier_string_of_type('Identifier::Global::Doi')
  end

  # @param [String] value
  # @return [String]
  def issn=(value)
    # Only add ISSN if it is reasonable to assume its repeated
    # It is likely that most ISSN belong on Serials
    unless %w{article inbook phdthesis inproceedings}.include?(bibtex_type)
      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
    else
      # Do some work to assign a Serial if possible
      # Check for Journal by ISSN
      s = Serial.where(name: journal).first
      i = Identifier::Global::Issn.where(identifier: value, identifier_object_type: 'Serial').first

      # Found an Existing Serial identically named with an assigned Identical ISSN
      if !s.nil? && (s == i&.identifier_object)
        write_attribute(:serial_id, s.id)
      elsif i && s.nil? # Found a Serial with an Identifier, but not identically named, assign it anyway
        write_attribute(:serial_id, i.identifier_object_id)
      end
    end
  end

  # @return [String]
  def issn
    identifier_string_of_type('Identifier::Global::Issn')
  end

  # turn bibtex URL field into a Ruby URI object
  # @return [URI]
  def url_as_uri
    URI(self.url) if self.url.present?
  end

  # @param [String] type_value
  # @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_value)
    # Also handle in memory
    identifiers.each do |i|
      return i.identifier if i.type == type_value
    end
    nil
    # identifiers.where(type: type_value).first&.identifier
  end

 #endregion getters & setters

  # @return [Boolean]
  # is there a bibtex author or author roles?
  def has_authors?
    return true if !author.blank?
    return false if new_record?
    # self exists in the db
    authors.count > 0 ? true : false
  end

  # @return [Boolean]
  def has_editors?
    return true if editor
    # editor attribute is empty
    return false if new_record? # WHY!?
    # self exists in the db
    editors.count > 0 ? true : false
  end

  # @return [Boolean]
  #  true contains either an author or editor
  def has_writer?
    (has_authors? || has_editors?) ? true : false
  end

  # @return [Boolean]
  def has_some_year? # is there a year or stated year?
    return false if year.blank? && stated_year.blank?
    true
  end

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

  # @return [Date || Time] <sigh>
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def cached_nomenclature_date
    if !persisted?
      nomenclature_date
    else
      read_attribute(:cached_nomenclature_date)
    end
  end

  # rubocop:disable Metrics/MethodLength
  # @return [BibTeX::Entry, false]
  #   !! Entry equivalent to self, this should round-trip with no changes.
  def to_bibtex
    return false if bibtex_type.nil?
    b = BibTeX::Entry.new(bibtex_type: bibtex_type)

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank?
        b[f] = v
      end
    end

    b[:keywords] = verbatim_keywords unless verbatim_keywords.blank?
    b[:note] = concatenated_notes_string unless concatenated_notes_string.blank?

    unless serial.nil?
      b[:journal] = serial.name
      issns  = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless serial.nil?
      b[:journal] = serial.name
      issns = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = identifiers.where(type: 'Identifier::Global::Uri')
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = identifiers.where(type: 'Identifier::Global::Isbn')
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = identifiers.where(type: 'Identifier::Global::Doi') #.of_type(:isbn)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    # Overiden by `author` and `editor` if present
    b.author = get_bibtex_names('author') if author_roles.load.any? # unless (!authors.load.any? && author.blank?)
    b.editor = get_bibtex_names('editor') if editor_roles.load.any? # unless (!editors.load.any? && editor.blank?)

    # TODO: use global_id or replace with UUID or DOI if available
    b.key = id unless new_record?
    b
  end

  # @return Hash
  #   a to_citeproc with values updated for literal
  #   handling via `{}` in TaxonWorks
  def to_citeproc(normalize_names = true)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names

    a = b.to_citeproc

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank? && (v.to_s =~ /\A{(.*)}\z/)
        a[f.to_s] = {literal: $1}
      end
    end

    a['year-suffix'] = year_suffix unless year_suffix.blank?
    a['original-date'] = {"date-parts" => [[ stated_year ]]} if !stated_year.blank? && stated_year.to_s != year.to_s
    a['language'] = Language.find(language_id).english_name.to_s unless language_id.nil?
    a['translated-title'] = alternate_values.where(type: "AlternateValue::Translation", alternate_value_object_attribute: 'title').pluck(:value).first
    a['note'] = note
    a.reject! { |k| k == 'note' } if note.blank?
    a
  end

  # @return [String, nil]
  #  priority is Person, string
  #  !! Not the cached value !!
  def get_author
    a = authors.load
    if a.any?
      get_bibtex_names('author')
    else
      author.blank? ? nil : author
    end
  end

  # @return [BibTex::Bibliography]
  #   initialized with this Source as an entry
  def bibtex_bibliography
    TaxonWorks::Vendor::BibtexRuby.bibliography([self])
 end

  # @param [String] style
  # @param [String] format
  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text', normalize_names = true)
    s = ::TaxonWorks::Vendor::BibtexRuby.get_style(style)
    cp = CiteProc::Processor.new(style: s, format: format)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names
    cp.import( [to_citeproc(normalize_names)] )
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end

  # @param [String] format
  # @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(DEFAULT_CSL_STYLE, format)
    #str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  # @return [String, nil]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized People before BibTeX `author`
  #   !! This is NOT a legal BibTeX format  !!
  def authority_name(reload = true)
    reload ? authors.reload : authors.load
    if !authors.any? # no normalized people, use string, !! not .any? because of in-memory setting?!
      if author.blank?
        return nil
      else
        b = to_bibtex
        ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b)
        return Utilities::Strings.authorship_sentence(b.author.tokens.collect{ |t| t.last })
      end
    else # use normalized records
      #      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.full_last_name })
      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.last_name })
    end
  end

  # TODO: Replace with taxonworks.csl.  Move unsupported fields to
  # wrappers in vue rendering.
  # set cached values and copies active record relations into bibtex values
  # @return [Ignored]
  def set_cached
    if errors.empty?
      attributes_to_update = {}
      attributes_to_update[:author] = get_bibtex_names('author') if authors.reload.size > 0
      attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0
      attributes_to_update.merge!(
        cached: get_cached,
        cached_nomenclature_date: nomenclature_date,
        cached_author_string: authority_name(false)
      )
      update_columns(attributes_to_update)
    end
  end

  def get_cached
    if errors.empty?
      c = cached_string('html') # preserves our convention of <i>
      return c
    end
    nil
  end

  # @param [String] type either `author` or `editor`
  # @return [String]
  #   The BibTeX version of the name strings created from People
  #   BibTeX format is 'lastname, firstname and lastname, firstname and lastname, firstname'
  #   This only references People, i.e. `authors` and `editors`.
  #   !! Do not adapt to reference the BibTeX attributes `author` or `editor`
  def get_bibtex_names(role_type)
    # so, we can not reload here
    send("#{role_type}s").collect{ |a| a.bibtex_name}.join(' and ')
  end

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

  protected

  def validate_year_suffix
    a = get_author
    unless year_suffix.blank? || year.blank? || a.blank?
      if new_record?
        s = Source.where(author: a, year: year, year_suffix: year_suffix).first
      else
        s = Source.where(author: a, year: year, year_suffix: year_suffix).not_self(self).first
      end
      errors.add(:year_suffix, " '#{year_suffix}' is already used for #{a} #{year}") unless s.nil?
    end
  end

  def italics_are_paired
    l = title.scan('<i>')&.count
    r = title.scan('</i>')&.count
    errors.add(:title, 'italic markup is not paired') unless l == r
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  # @return [Ignored]
  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

  def sv_cached_names # this cannot be moved to soft_validation_extensions
    is_cached = true

    if (author.to_s != get_bibtex_names('author') && !get_bibtex_names('author').blank?) ||
        (editor.to_s != get_bibtex_names('editor') && !get_bibtex_names('editor').blank?) ||
        cached != get_cached ||
        cached_nomenclature_date != nomenclature_date ||
        cached_author_string.to_s != authority_name(false)
      is_cached = false
    end

    soft_validations.add(
      :base, 'Cached values should be updated',
      success_message: 'Cached values were updated',
      failure_message:  'Failed to update cached values') if !is_cached
  end
end

#booktitlenil

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



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

class Source::Bibtex < Source

  # Type will change
  DEFAULT_CSL_STYLE = 'taxonworks'
  #DEFAULT_CSL_STYLE = 'zootaxa'

  attr_accessor :authors_to_create

  include Shared::OriginRelationship
  include Source::Bibtex::SoftValidationExtensions::Instance
  extend Source::Bibtex::SoftValidationExtensions::Klass

  is_origin_for 'Source::Bibtex', 'Source::Verbatim'
  originates_from 'Source::Bibtex', 'Source::Verbatim'

  GRAPH_ENTRY_POINTS = [:origin_relationships]

  # Used in soft validation
  BIBTEX_REQUIRED_FIELDS = {
    article: [:author, :title, :journal, :year],
    book: [:author, :editor, :title, :publisher, :year],
    booklet: [:title],
    conference: [:author, :title, :booktitle, :year],
    inbook: [:author, :editor, :title, :chapter, :pages, :publisher, :year],
    incollection: [:author, :title, :booktitle, :publisher, :year],
    inproceedings:  [:author, :title, :booktitle, :year],
    manual: [:title],
    mastersthesis: [:author, :title, :school, :year],
    misc: [],
    phdthesis: [:author, :title, :school, :year],
    proceedings: [:title, :year],
    techreport: [:author,:title,:institution, :year],
    unpublished: [:author, :title, :note]
  }

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

  IGNORE_SIMILAR = [:verbatim, :cached, :cached_author_string, :cached_nomenclature_date].freeze
  IGNORE_IDENTICAL = IGNORE_SIMILAR.dup.freeze

  belongs_to :serial, inverse_of: :sources

  # handle conflict with BibTex language field.
  belongs_to :source_language, class_name: 'Language', foreign_key: :language_id, inverse_of: :sources

  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

  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor',
    as: :role_object, validate: true
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true

  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, :serial, allow_destroy: true

  before_validation :create_authors, if: -> { !authors_to_create.nil? }
  before_validation :check_has_field

  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: 'is required when month or stated_year is provided'

  validates :year, date_year: {
    min_year: 1000, max_year: Time.now.year + 2,
    message: 'must be an integer greater than 999 and no more than 2 years in the future'}

  validates_presence_of :month,
    unless: -> { day.blank? },
    message: 'is required when day is provided'

  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_blank: true,
    message: ' month'

  validates :day, date_day: {year_sym: :year, month_sym: :month},
            unless: -> { year.blank? || month.blank? }

  validates :url, format: {
    with: URI::regexp(%w(http https ftp)),
    message: '[%{value}] is not a valid URL'}, allow_blank: true

  validate :italics_are_paired, unless: -> { title.blank? }
  validate :validate_year_suffix, unless: -> { self.no_year_suffix_validation || (self.type != 'Source::Bibtex') }

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_nomenclature_date) }

  # @param [BibTeX::Name] bibtex_author
  # @return [Person, Boolean] new person, or false
  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    p = Person.new(
      first_name: bibtex_author.first,
      prefix: bibtex_author.prefix,
      last_name: bibtex_author.last,
      suffix: bibtex_author.suffix)
    p.namecase_names
    p
  end

  # @return [Source::Bibtex.new]
  #   Adds errors if parse error exists. Note these
  # errors are lost if save/valid? is called again on the object.
  def self.new_from_bibtex_text(text = nil)
    source = Source::Bibtex.new
    begin
      a = BibTeX::Bibliography.parse(text, filter: :latex).first
      if a.class.name == 'BibTeX::Error'
        source.errors.add(:base, 'Unable to parse BibTeX entry. Possible error at end of: ' + a.content)
        return source
      else
        return new_from_bibtex(a)
      end
    rescue BibTeX::ParseError => e
      source.errors.add(:base, 'Unable to parse BibTeX entry: ' + e.to_s)
      return source
    end
  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_from_bibtex(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: Serial with alternate_value on name .count = 1 assign .first
  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|
      next if key == :serial # Raises if it hits the belongs_to

      if key == :keywords
        s.verbatim_keywords = value
        next
      end

      v = value.to_s.strip

      if s.respond_to?(key.to_sym) && key != :type
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end

    s.data_attributes_attributes = import_attributes

    # See issn=() for code matching to existing serials that preceeds this logic
    if s.serial_id.blank? && !bibtex_entry.fields[:journal].to_s.blank? && !bibtex_entry.fields[:issn].to_s.blank?
      a = {
        name: bibtex_entry.fields[:journal].to_s,
        publisher: bibtex_entry.fields[:publisher].to_s,
        identifiers_attributes: [ {
          identifier: bibtex_entry.fields[:issn].to_s,
          type: 'Identifier::Global::Issn'
        } ]
      }

      s.serial_attributes = a
    end
    s
  end

  # @return [Array] journal, nil or name
  def journal
    [read_attribute(:journal), (serial.blank? ? nil : serial.name)].compact.first
  end

  # @return [String]
  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    to_bibtex.valid?
  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
    [year, year_suffix].compact.join
  end

  # TODO: Not used
  #
  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now.
  #
  # @return [Array, Boolean] of names, or false
  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
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(bibtex)

    begin
      Role.transaction do
        if bibtex.authors
          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
          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

  #region getters & setters

  # @param [String, Integer] value
  # @return [Integer] value of year
  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

  # @param [String] value
  # @return [String]
  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
  # @param [String] value
  # @return [String]
  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

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def isbn
    identifier_string_of_type('Identifier::Global::Isbn')
  end

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def doi
    identifier_string_of_type('Identifier::Global::Doi')
  end

  # @param [String] value
  # @return [String]
  def issn=(value)
    # Only add ISSN if it is reasonable to assume its repeated
    # It is likely that most ISSN belong on Serials
    unless %w{article inbook phdthesis inproceedings}.include?(bibtex_type)
      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
    else
      # Do some work to assign a Serial if possible
      # Check for Journal by ISSN
      s = Serial.where(name: journal).first
      i = Identifier::Global::Issn.where(identifier: value, identifier_object_type: 'Serial').first

      # Found an Existing Serial identically named with an assigned Identical ISSN
      if !s.nil? && (s == i&.identifier_object)
        write_attribute(:serial_id, s.id)
      elsif i && s.nil? # Found a Serial with an Identifier, but not identically named, assign it anyway
        write_attribute(:serial_id, i.identifier_object_id)
      end
    end
  end

  # @return [String]
  def issn
    identifier_string_of_type('Identifier::Global::Issn')
  end

  # turn bibtex URL field into a Ruby URI object
  # @return [URI]
  def url_as_uri
    URI(self.url) if self.url.present?
  end

  # @param [String] type_value
  # @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_value)
    # Also handle in memory
    identifiers.each do |i|
      return i.identifier if i.type == type_value
    end
    nil
    # identifiers.where(type: type_value).first&.identifier
  end

 #endregion getters & setters

  # @return [Boolean]
  # is there a bibtex author or author roles?
  def has_authors?
    return true if !author.blank?
    return false if new_record?
    # self exists in the db
    authors.count > 0 ? true : false
  end

  # @return [Boolean]
  def has_editors?
    return true if editor
    # editor attribute is empty
    return false if new_record? # WHY!?
    # self exists in the db
    editors.count > 0 ? true : false
  end

  # @return [Boolean]
  #  true contains either an author or editor
  def has_writer?
    (has_authors? || has_editors?) ? true : false
  end

  # @return [Boolean]
  def has_some_year? # is there a year or stated year?
    return false if year.blank? && stated_year.blank?
    true
  end

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

  # @return [Date || Time] <sigh>
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def cached_nomenclature_date
    if !persisted?
      nomenclature_date
    else
      read_attribute(:cached_nomenclature_date)
    end
  end

  # rubocop:disable Metrics/MethodLength
  # @return [BibTeX::Entry, false]
  #   !! Entry equivalent to self, this should round-trip with no changes.
  def to_bibtex
    return false if bibtex_type.nil?
    b = BibTeX::Entry.new(bibtex_type: bibtex_type)

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank?
        b[f] = v
      end
    end

    b[:keywords] = verbatim_keywords unless verbatim_keywords.blank?
    b[:note] = concatenated_notes_string unless concatenated_notes_string.blank?

    unless serial.nil?
      b[:journal] = serial.name
      issns  = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless serial.nil?
      b[:journal] = serial.name
      issns = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = identifiers.where(type: 'Identifier::Global::Uri')
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = identifiers.where(type: 'Identifier::Global::Isbn')
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = identifiers.where(type: 'Identifier::Global::Doi') #.of_type(:isbn)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    # Overiden by `author` and `editor` if present
    b.author = get_bibtex_names('author') if author_roles.load.any? # unless (!authors.load.any? && author.blank?)
    b.editor = get_bibtex_names('editor') if editor_roles.load.any? # unless (!editors.load.any? && editor.blank?)

    # TODO: use global_id or replace with UUID or DOI if available
    b.key = id unless new_record?
    b
  end

  # @return Hash
  #   a to_citeproc with values updated for literal
  #   handling via `{}` in TaxonWorks
  def to_citeproc(normalize_names = true)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names

    a = b.to_citeproc

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank? && (v.to_s =~ /\A{(.*)}\z/)
        a[f.to_s] = {literal: $1}
      end
    end

    a['year-suffix'] = year_suffix unless year_suffix.blank?
    a['original-date'] = {"date-parts" => [[ stated_year ]]} if !stated_year.blank? && stated_year.to_s != year.to_s
    a['language'] = Language.find(language_id).english_name.to_s unless language_id.nil?
    a['translated-title'] = alternate_values.where(type: "AlternateValue::Translation", alternate_value_object_attribute: 'title').pluck(:value).first
    a['note'] = note
    a.reject! { |k| k == 'note' } if note.blank?
    a
  end

  # @return [String, nil]
  #  priority is Person, string
  #  !! Not the cached value !!
  def get_author
    a = authors.load
    if a.any?
      get_bibtex_names('author')
    else
      author.blank? ? nil : author
    end
  end

  # @return [BibTex::Bibliography]
  #   initialized with this Source as an entry
  def bibtex_bibliography
    TaxonWorks::Vendor::BibtexRuby.bibliography([self])
 end

  # @param [String] style
  # @param [String] format
  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text', normalize_names = true)
    s = ::TaxonWorks::Vendor::BibtexRuby.get_style(style)
    cp = CiteProc::Processor.new(style: s, format: format)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names
    cp.import( [to_citeproc(normalize_names)] )
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end

  # @param [String] format
  # @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(DEFAULT_CSL_STYLE, format)
    #str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  # @return [String, nil]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized People before BibTeX `author`
  #   !! This is NOT a legal BibTeX format  !!
  def authority_name(reload = true)
    reload ? authors.reload : authors.load
    if !authors.any? # no normalized people, use string, !! not .any? because of in-memory setting?!
      if author.blank?
        return nil
      else
        b = to_bibtex
        ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b)
        return Utilities::Strings.authorship_sentence(b.author.tokens.collect{ |t| t.last })
      end
    else # use normalized records
      #      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.full_last_name })
      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.last_name })
    end
  end

  # TODO: Replace with taxonworks.csl.  Move unsupported fields to
  # wrappers in vue rendering.
  # set cached values and copies active record relations into bibtex values
  # @return [Ignored]
  def set_cached
    if errors.empty?
      attributes_to_update = {}
      attributes_to_update[:author] = get_bibtex_names('author') if authors.reload.size > 0
      attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0
      attributes_to_update.merge!(
        cached: get_cached,
        cached_nomenclature_date: nomenclature_date,
        cached_author_string: authority_name(false)
      )
      update_columns(attributes_to_update)
    end
  end

  def get_cached
    if errors.empty?
      c = cached_string('html') # preserves our convention of <i>
      return c
    end
    nil
  end

  # @param [String] type either `author` or `editor`
  # @return [String]
  #   The BibTeX version of the name strings created from People
  #   BibTeX format is 'lastname, firstname and lastname, firstname and lastname, firstname'
  #   This only references People, i.e. `authors` and `editors`.
  #   !! Do not adapt to reference the BibTeX attributes `author` or `editor`
  def get_bibtex_names(role_type)
    # so, we can not reload here
    send("#{role_type}s").collect{ |a| a.bibtex_name}.join(' and ')
  end

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

  protected

  def validate_year_suffix
    a = get_author
    unless year_suffix.blank? || year.blank? || a.blank?
      if new_record?
        s = Source.where(author: a, year: year, year_suffix: year_suffix).first
      else
        s = Source.where(author: a, year: year, year_suffix: year_suffix).not_self(self).first
      end
      errors.add(:year_suffix, " '#{year_suffix}' is already used for #{a} #{year}") unless s.nil?
    end
  end

  def italics_are_paired
    l = title.scan('<i>')&.count
    r = title.scan('</i>')&.count
    errors.add(:title, 'italic markup is not paired') unless l == r
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  # @return [Ignored]
  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

  def sv_cached_names # this cannot be moved to soft_validation_extensions
    is_cached = true

    if (author.to_s != get_bibtex_names('author') && !get_bibtex_names('author').blank?) ||
        (editor.to_s != get_bibtex_names('editor') && !get_bibtex_names('editor').blank?) ||
        cached != get_cached ||
        cached_nomenclature_date != nomenclature_date ||
        cached_author_string.to_s != authority_name(false)
      is_cached = false
    end

    soft_validations.add(
      :base, 'Cached values should be updated',
      success_message: 'Cached values were updated',
      failure_message:  'Failed to update cached values') if !is_cached
  end
end

#chapterString?

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

Returns:

  • (String)

    the chapter or section number.

  • (nil)

    means the attribute is not stored in the database.



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

class Source::Bibtex < Source

  # Type will change
  DEFAULT_CSL_STYLE = 'taxonworks'
  #DEFAULT_CSL_STYLE = 'zootaxa'

  attr_accessor :authors_to_create

  include Shared::OriginRelationship
  include Source::Bibtex::SoftValidationExtensions::Instance
  extend Source::Bibtex::SoftValidationExtensions::Klass

  is_origin_for 'Source::Bibtex', 'Source::Verbatim'
  originates_from 'Source::Bibtex', 'Source::Verbatim'

  GRAPH_ENTRY_POINTS = [:origin_relationships]

  # Used in soft validation
  BIBTEX_REQUIRED_FIELDS = {
    article: [:author, :title, :journal, :year],
    book: [:author, :editor, :title, :publisher, :year],
    booklet: [:title],
    conference: [:author, :title, :booktitle, :year],
    inbook: [:author, :editor, :title, :chapter, :pages, :publisher, :year],
    incollection: [:author, :title, :booktitle, :publisher, :year],
    inproceedings:  [:author, :title, :booktitle, :year],
    manual: [:title],
    mastersthesis: [:author, :title, :school, :year],
    misc: [],
    phdthesis: [:author, :title, :school, :year],
    proceedings: [:title, :year],
    techreport: [:author,:title,:institution, :year],
    unpublished: [:author, :title, :note]
  }

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

  IGNORE_SIMILAR = [:verbatim, :cached, :cached_author_string, :cached_nomenclature_date].freeze
  IGNORE_IDENTICAL = IGNORE_SIMILAR.dup.freeze

  belongs_to :serial, inverse_of: :sources

  # handle conflict with BibTex language field.
  belongs_to :source_language, class_name: 'Language', foreign_key: :language_id, inverse_of: :sources

  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

  has_many :editor_roles, -> { order('roles.position ASC') }, class_name: 'SourceEditor',
    as: :role_object, validate: true
  has_many :editors, -> { order('roles.position ASC') }, through: :editor_roles, source: :person, validate: true

  accepts_nested_attributes_for :authors, :editors, :author_roles, :editor_roles, :serial, allow_destroy: true

  before_validation :create_authors, if: -> { !authors_to_create.nil? }
  before_validation :check_has_field

  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: 'is required when month or stated_year is provided'

  validates :year, date_year: {
    min_year: 1000, max_year: Time.now.year + 2,
    message: 'must be an integer greater than 999 and no more than 2 years in the future'}

  validates_presence_of :month,
    unless: -> { day.blank? },
    message: 'is required when day is provided'

  validates_inclusion_of :month,
    in: ::VALID_BIBTEX_MONTHS,
    allow_blank: true,
    message: ' month'

  validates :day, date_day: {year_sym: :year, month_sym: :month},
            unless: -> { year.blank? || month.blank? }

  validates :url, format: {
    with: URI::regexp(%w(http https ftp)),
    message: '[%{value}] is not a valid URL'}, allow_blank: true

  validate :italics_are_paired, unless: -> { title.blank? }
  validate :validate_year_suffix, unless: -> { self.no_year_suffix_validation || (self.type != 'Source::Bibtex') }

  # includes nil last, exclude it explicitly with another condition if need be
  scope :order_by_nomenclature_date, -> { order(:cached_nomenclature_date) }

  # @param [BibTeX::Name] bibtex_author
  # @return [Person, Boolean] new person, or false
  def self.bibtex_author_to_person(bibtex_author)
    return false if bibtex_author.class != BibTeX::Name
    p = Person.new(
      first_name: bibtex_author.first,
      prefix: bibtex_author.prefix,
      last_name: bibtex_author.last,
      suffix: bibtex_author.suffix)
    p.namecase_names
    p
  end

  # @return [Source::Bibtex.new]
  #   Adds errors if parse error exists. Note these
  # errors are lost if save/valid? is called again on the object.
  def self.new_from_bibtex_text(text = nil)
    source = Source::Bibtex.new
    begin
      a = BibTeX::Bibliography.parse(text, filter: :latex).first
      if a.class.name == 'BibTeX::Error'
        source.errors.add(:base, 'Unable to parse BibTeX entry. Possible error at end of: ' + a.content)
        return source
      else
        return new_from_bibtex(a)
      end
    rescue BibTeX::ParseError => e
      source.errors.add(:base, 'Unable to parse BibTeX entry: ' + e.to_s)
      return source
    end
  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_from_bibtex(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: Serial with alternate_value on name .count = 1 assign .first
  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|
      next if key == :serial # Raises if it hits the belongs_to

      if key == :keywords
        s.verbatim_keywords = value
        next
      end

      v = value.to_s.strip

      if s.respond_to?(key.to_sym) && key != :type
        s.send("#{key}=", v)
      else
        import_attributes.push({import_predicate: key, value: v, type: 'ImportAttribute'})
      end
    end

    s.data_attributes_attributes = import_attributes

    # See issn=() for code matching to existing serials that preceeds this logic
    if s.serial_id.blank? && !bibtex_entry.fields[:journal].to_s.blank? && !bibtex_entry.fields[:issn].to_s.blank?
      a = {
        name: bibtex_entry.fields[:journal].to_s,
        publisher: bibtex_entry.fields[:publisher].to_s,
        identifiers_attributes: [ {
          identifier: bibtex_entry.fields[:issn].to_s,
          type: 'Identifier::Global::Issn'
        } ]
      }

      s.serial_attributes = a
    end
    s
  end

  # @return [Array] journal, nil or name
  def journal
    [read_attribute(:journal), (serial.blank? ? nil : serial.name)].compact.first
  end

  # @return [String]
  def verbatim_journal
    read_attribute(:journal)
  end

  # @return [Boolean]
  #   whether the BibTeX::Entry representation of this source is valid
  def valid_bibtex?
    to_bibtex.valid?
  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
    [year, year_suffix].compact.join
  end

  # TODO: Not used
  #
  # Modified from build, the issues with polymorphic has_many and build
  # are more than we want to tackle right now.
  #
  # @return [Array, Boolean] of names, or false
  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
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(bibtex)

    begin
      Role.transaction do
        if bibtex.authors
          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
          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

  #region getters & setters

  # @param [String, Integer] value
  # @return [Integer] value of year
  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

  # @param [String] value
  # @return [String]
  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
  # @param [String] value
  # @return [String]
  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

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def isbn
    identifier_string_of_type('Identifier::Global::Isbn')
  end

  # @param [String] value
  # @return [String]
  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

  # @return [String]
  def doi
    identifier_string_of_type('Identifier::Global::Doi')
  end

  # @param [String] value
  # @return [String]
  def issn=(value)
    # Only add ISSN if it is reasonable to assume its repeated
    # It is likely that most ISSN belong on Serials
    unless %w{article inbook phdthesis inproceedings}.include?(bibtex_type)
      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
    else
      # Do some work to assign a Serial if possible
      # Check for Journal by ISSN
      s = Serial.where(name: journal).first
      i = Identifier::Global::Issn.where(identifier: value, identifier_object_type: 'Serial').first

      # Found an Existing Serial identically named with an assigned Identical ISSN
      if !s.nil? && (s == i&.identifier_object)
        write_attribute(:serial_id, s.id)
      elsif i && s.nil? # Found a Serial with an Identifier, but not identically named, assign it anyway
        write_attribute(:serial_id, i.identifier_object_id)
      end
    end
  end

  # @return [String]
  def issn
    identifier_string_of_type('Identifier::Global::Issn')
  end

  # turn bibtex URL field into a Ruby URI object
  # @return [URI]
  def url_as_uri
    URI(self.url) if self.url.present?
  end

  # @param [String] type_value
  # @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_value)
    # Also handle in memory
    identifiers.each do |i|
      return i.identifier if i.type == type_value
    end
    nil
    # identifiers.where(type: type_value).first&.identifier
  end

 #endregion getters & setters

  # @return [Boolean]
  # is there a bibtex author or author roles?
  def has_authors?
    return true if !author.blank?
    return false if new_record?
    # self exists in the db
    authors.count > 0 ? true : false
  end

  # @return [Boolean]
  def has_editors?
    return true if editor
    # editor attribute is empty
    return false if new_record? # WHY!?
    # self exists in the db
    editors.count > 0 ? true : false
  end

  # @return [Boolean]
  #  true contains either an author or editor
  def has_writer?
    (has_authors? || has_editors?) ? true : false
  end

  # @return [Boolean]
  def has_some_year? # is there a year or stated year?
    return false if year.blank? && stated_year.blank?
    true
  end

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

  # @return [Date || Time] <sigh>
  #  An memoizer, getter for cached_nomenclature_date, computes if not .persisted?
  def cached_nomenclature_date
    if !persisted?
      nomenclature_date
    else
      read_attribute(:cached_nomenclature_date)
    end
  end

  # rubocop:disable Metrics/MethodLength
  # @return [BibTeX::Entry, false]
  #   !! Entry equivalent to self, this should round-trip with no changes.
  def to_bibtex
    return false if bibtex_type.nil?
    b = BibTeX::Entry.new(bibtex_type: bibtex_type)

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank?
        b[f] = v
      end
    end

    b[:keywords] = verbatim_keywords unless verbatim_keywords.blank?
    b[:note] = concatenated_notes_string unless concatenated_notes_string.blank?

    unless serial.nil?
      b[:journal] = serial.name
      issns  = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    unless serial.nil?
      b[:journal] = serial.name
      issns = serial.identifiers.where(type: 'Identifier::Global::Issn')
      unless issns.empty?
        b[:issn] = issns.first.identifier # assuming the serial has only 1 ISSN
      end
    end

    uris = identifiers.where(type: 'Identifier::Global::Uri')
    unless uris.empty?
      b[:url] = uris.first.identifier # TW only allows one URI per object
    end

    isbns = identifiers.where(type: 'Identifier::Global::Isbn')
    unless isbns.empty?
      b[:isbn] = isbns.first.identifier # TW only allows one ISBN per object
    end

    dois = identifiers.where(type: 'Identifier::Global::Doi') #.of_type(:isbn)
    unless dois.empty?
      b[:doi] = dois.first.identifier # TW only allows one DOI per object
    end

    # Overiden by `author` and `editor` if present
    b.author = get_bibtex_names('author') if author_roles.load.any? # unless (!authors.load.any? && author.blank?)
    b.editor = get_bibtex_names('editor') if editor_roles.load.any? # unless (!editors.load.any? && editor.blank?)

    # TODO: use global_id or replace with UUID or DOI if available
    b.key = id unless new_record?
    b
  end

  # @return Hash
  #   a to_citeproc with values updated for literal
  #   handling via `{}` in TaxonWorks
  def to_citeproc(normalize_names = true)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names

    a = b.to_citeproc

    ::BIBTEX_FIELDS.each do |f|
      next if f == :bibtex_type
      v = send(f)
      if !v.blank? && (v.to_s =~ /\A{(.*)}\z/)
        a[f.to_s] = {literal: $1}
      end
    end

    a['year-suffix'] = year_suffix unless year_suffix.blank?
    a['original-date'] = {"date-parts" => [[ stated_year ]]} if !stated_year.blank? && stated_year.to_s != year.to_s
    a['language'] = Language.find(language_id).english_name.to_s unless language_id.nil?
    a['translated-title'] = alternate_values.where(type: "AlternateValue::Translation", alternate_value_object_attribute: 'title').pluck(:value).first
    a['note'] = note
    a.reject! { |k| k == 'note' } if note.blank?
    a
  end

  # @return [String, nil]
  #  priority is Person, string
  #  !! Not the cached value !!
  def get_author
    a = authors.load
    if a.any?
      get_bibtex_names('author')
    else
      author.blank? ? nil : author
    end
  end

  # @return [BibTex::Bibliography]
  #   initialized with this Source as an entry
  def bibtex_bibliography
    TaxonWorks::Vendor::BibtexRuby.bibliography([self])
 end

  # @param [String] style
  # @param [String] format
  # @return [String]
  #   this source, rendered in the provided CSL style, as text
  def render_with_style(style = 'vancouver', format = 'text', normalize_names = true)
    s = ::TaxonWorks::Vendor::BibtexRuby.get_style(style)
    cp = CiteProc::Processor.new(style: s, format: format)
    b = to_bibtex
    ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b) if normalize_names
    cp.import( [to_citeproc(normalize_names)] )
    cp.render(:bibliography, id: cp.items.keys.first).first.strip
  end

  # @param [String] format
  # @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(DEFAULT_CSL_STYLE, format)
    #str.sub('(0ADAD)', '') # citeproc renders year 0000 as (0ADAD)
  end

  # @return [String, nil]
  #   last names formatted as displayed in nomenclatural authority (iczn), prioritizes
  #   normalized People before BibTeX `author`
  #   !! This is NOT a legal BibTeX format  !!
  def authority_name(reload = true)
    reload ? authors.reload : authors.load
    if !authors.any? # no normalized people, use string, !! not .any? because of in-memory setting?!
      if author.blank?
        return nil
      else
        b = to_bibtex
        ::TaxonWorks::Vendor::BibtexRuby.namecase_bibtex_entry(b)
        return Utilities::Strings.authorship_sentence(b.author.tokens.collect{ |t| t.last })
      end
    else # use normalized records
      #      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.full_last_name })
      return Utilities::Strings.authorship_sentence(authors.collect{ |a| a.last_name })
    end
  end

  # TODO: Replace with taxonworks.csl.  Move unsupported fields to
  # wrappers in vue rendering.
  # set cached values and copies active record relations into bibtex values
  # @return [Ignored]
  def set_cached
    if errors.empty?
      attributes_to_update = {}
      attributes_to_update[:author] = get_bibtex_names('author') if authors.reload.size > 0
      attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0
      attributes_to_update.merge!(
        cached: get_cached,
        cached_nomenclature_date: nomenclature_date,
        cached_author_string: authority_name(false)
      )
      update_columns(attributes_to_update)
    end
  end

  def get_cached
    if errors.empty?
      c = cached_string('html') # preserves our convention of <i>
      return c
    end
    nil
  end

  # @param [String] type either `author` or `editor`
  # @return [String]
  #   The BibTeX version of the name strings created from People
  #   BibTeX format is 'lastname, firstname and lastname, firstname and lastname, firstname'
  #   This only references People, i.e. `authors` and `editors`.
  #   !! Do not adapt to reference the BibTeX attributes `author` or `editor`
  def get_bibtex_names(role_type)
    # so, we can not reload here
    send("#{role_type}s").collect{ |a| a.bibtex_name}.join(' and ')
  end

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

  protected

  def validate_year_suffix
    a = get_author
    unless year_suffix.blank? || year.blank? || a.blank?
      if new_record?
        s = Source.where(author: a, year: year, year_suffix: year_suffix).first
      else
        s = Source.where(author: a, year: year, year_suffix: year_suffix).not_self(self).first
      end
      errors.add(:year_suffix, " '#{year_suffix}' is already used for #{a} #{year}") unless s.nil?
    end
  end

  def italics_are_paired
    l = title.scan('<i>')&.count
    r = title.scan('</i>')&.count
    errors.add(:title, 'italic markup is not paired') unless l == r
  end

  #region hard validations

  # must have at least one of the required fields (TW_REQUIRED_FIELDS)
  # @return [Ignored]
  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

  def sv_cached_names # this cannot be moved to soft_validation_extensions
    is_cached = true

    if (author.to_s != get_bibtex_names('author') && !get_bibtex_names('author').blank?) ||
        (editor.to_s != get_bibtex_names('editor') && !get_bibtex_names('editor').blank?) ||
        cached != get_cached ||
        cached_nomenclature_date != nomenclature_date ||
        cached_author_string.to_s != authority_name(false)
      is_cached = false
    end

    soft_validations.add(
      :base, 'Cached values should be updated',
      success_message: 'Cached values were updated',
      failure_message:  'Failed to update cached values') if !is_cached
  end
end
TODO:

Returns:

  • (String)


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

class Source::Bibtex < Source

  # Type will change
  DEFAULT_CSL_STYLE = 'taxonworks'
  #DEFAULT_CSL_STYLE = 'zootaxa'

  attr_accessor :authors_to_create

  include Shared::OriginRelationship
  include Source::Bibtex::SoftValidationExtensions::Instance
  extend Source::Bibtex::SoftValidationExtensions::Klass

  is_origin_for 'Source::Bibtex', 'Source::Verbatim'
  originates_from 'Source::Bibtex'