Class: Source::Bibtex
- Inherits:
-
Source
- Object
- ActiveRecord::Base
- ApplicationRecord
- Source
- Source::Bibtex
- Defined in:
- app/models/source/bibtex.rb
Overview
Bibtex - Subclass of Source that represents most references.
Cached values are formatted using the 'zootaxa' style from 'csl/styles'
TaxonWorks(TW) relies on the bibtex-ruby gem to input or output BibTeX bibliographies, and has a strict list of required fields. TW itself only requires that :bibtex_type be valid and that one of the attributes in TW_REQUIRED_FIELDS be defined. This allows a rapid input of incomplete data, but also means that not all TW Source::Bibtex objects can be added to a BibTeX bibliography.
The following information is taken from BibTeXing, by Oren Patashnik, February 8, 1988 ftp.math.purdue.edu/mirrors/ctan.org/biblio/bibtex/contrib/doc/btxdoc.pdf (and snippets are cut from this document for the attribute descriptions)
BibTeX fields in a BibTex bibliography are treated in one of three ways:
- REQUIRED
-
Omitting the field will produce a warning message and, rarely, a badly formatted bibliography entry. If the required information is not meaningful, you are using the wrong entry type. However, if the required information is meaningful but, say, already included is some other field, simply ignore the warning.
- OPTIONAL
-
The field's information will be used if present, but can be omitted
without causing any formatting problems. You should include the optional
field if it will help the reader.
- IGNORED
-
The field is ignored. BibTEX ignores any field that is not required or
optional, so you can include any fields you want in a bib file entry. It's a
good idea to put all relevant information about a reference in its bib file
entry - even information that may never appear in the bibliography.
Dates in Source Bibtex:
It is common for there two be two (or more) dates associated with the origin of a source:
1) If you only have reference to a single value, it goes in year (month, day)
2) If you have reference to two year values, the actual year of publication goes in year, and the stated year of publication goes in stated_year.
3) If you have month or day publication, they go in month or day.
We do not track stated_month or stated_day if they are present in addition to actual month and actual day.
BibTeX has month.
BibTeX does not have day.
TW will add all non-standard or housekeeping attributes to the bibliography even though the data may be ignored.
Constant Summary collapse
- 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 inherited from Source
Constants included from SoftValidation
SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS
Instance Attribute Summary collapse
- #abstract ⇒ String
-
#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.
-
#annote ⇒ String?
BibTeX standard field (ignored by standard processors) An annotation.
-
#author ⇒ String?
“Last name, FirstName MiddleName”.
-
#authors_to_create ⇒ Object
Returns the value of attribute authors_to_create.
-
#bibtex_type ⇒ String
One of VALID_BIBTEX_TYPES (config/initializers/constants/_controlled_vocabularies/bibtex_constants.rb, keys there are symbols).
-
#booktitle ⇒ nil
@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.
-
#chapter ⇒ String?
BibTeX standard field (required for types: )(optional for types:) A chapter (or section or whatever) number.
- #copyright ⇒ String
-
#crossref ⇒ nil
@return the key of the cross referenced source BibTeX standard field (ignored by standard processors) The database key(key attribute) of the entry being cross referenced.
-
#day ⇒ Integer
the actual publication month, NOT a BibTex standard field If day is present there must be a month and day must be valid for the month.
- #doi ⇒ String
-
#edition ⇒ nil
@return the edition of the book BibTeX standard field (required for types: )(optional for types:) The edition of a book(for example, “Second”).
- #editor ⇒ String
-
#howpublished ⇒ nil
@return a description of how this source was published BibTeX standard field (required for types: )(optional for types:) How something unusual has been published.
-
#institution ⇒ nil
@return the name of the institution publishing this source BibTeX standard field (required for types: )(optional for types:) The sponsoring institution of a technical report.
- #isbn ⇒ String
- #issn ⇒ String
-
#journal ⇒ Array
Journal, nil or name.
-
#key ⇒ String?
BibTeX standard field (may be used in a bibliography for alphabetizing & cross referencing) Used by bibtex-ruby gem method identifier as a default value when no other identifier is present.
-
#language ⇒ String
BibTeX field for the name of the language used.
-
#language_id ⇒ Integer
Language, from a controlled vocabulary.
-
#month ⇒ nil
@return The three-letter lower-case abbreviation for the month in which this source was published.
-
#note ⇒ nil
@return the BibTeX note associated with this source BibTeX standard field (required for types: unpublished)(optional for types:) Any additional information that can help the reader.
-
#number ⇒ nil
@return the number in a series, issue or technical report number associated with this source BibTeX standard field (required for types: )(optional for types:) The number of a journal, magazine, technical report, or of a work in a series.
-
#organization ⇒ nil
@return the organization associated with this source BibTeX standard field (required for types: )(optional for types:) The organization that sponsors a conference or that publishes a manual.
-
#pages ⇒ String
BibTeX standard field (required for types: )(optional for types:) One or more page numbers or range of numbers, such as 42–111 or 7,41,73–97 or 43+ (the `+' in this last example indicates pages following that don't form a simple range).
- #publisher ⇒ String
- #school ⇒ String
- #series ⇒ String
- #stated_year ⇒ String
-
#title ⇒ String
A TW required attribute (TW requires a value in one of the required attributes.).
-
#translator ⇒ String
bibtex-ruby gem supports translator, it's not clear whether TW will or not.
- #type ⇒ String
-
#url ⇒ String
A TW required attribute for certain bibtex_types (TW requires a value in one of the required attributes.).
-
#verbatim ⇒ String
Non-Bibtex attribute that is cross-referenced.
- #verbatim_contents ⇒ String
- #verbatim_keywords ⇒ String
- #volume ⇒ String
-
#year ⇒ Integer
the actual publication year.
-
#year_suffix ⇒ String
Like 1950a.
Attributes inherited from Source
#cached, #cached_author_string, #no_year_suffix_validation, #serial_id
Attributes included from Housekeeping::Users
Class Method Summary collapse
-
.bibtex_author_to_person(bibtex_author) ⇒ Person, Boolean
New person, or false.
-
.new_from_bibtex(bibtex_entry = nil) ⇒ Source::BibTex.new
Instantiates a Source::Bibtex instance from a BibTeX::Entry Note: * note conversion is handled in note setter.
- .new_from_bibtex_text(text = nil) ⇒ BibTeX::Entry
Instance Method Summary collapse
-
#author_year ⇒ String
A string that represents the authors last_names and year (no suffix).
-
#authority_name(reload = true) ⇒ String?
Last names formatted as displayed in nomenclatural authority (iczn), prioritizes normalized People before BibTeX `author` !! This is NOT a legal BibTeX format !!.
-
#bibtex_bibliography ⇒ BibTex::Bibliography
Initialized with this Source as an entry.
-
#cached_nomenclature_date ⇒ Date || Time
<sigh> An memoizer, getter for cached_nomenclature_date, computes if not .persisted?.
-
#cached_string(format = 'text') ⇒ String
String must be length > 0.
-
#check_has_field ⇒ Ignored
protected
must have at least one of the required fields (TW_REQUIRED_FIELDS).
- #create_authors ⇒ Ignored protected
-
#create_related_people_and_roles ⇒ Array, Boolean
Modified from build, the issues with polymorphic has_many and build are more than we want to tackle right now.
-
#get_author ⇒ String?
Priority is Person, string !! Not the cached value !!.
-
#get_bibtex_names(role_type) ⇒ String
protected
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.
-
#has_authors? ⇒ Boolean
is there a bibtex author or author roles?.
- #has_editors? ⇒ Boolean
- #has_some_year? ⇒ Boolean
-
#has_writer? ⇒ Boolean
True contains either an author or editor.
-
#identifier_string_of_type(type_value) ⇒ Identifier
The identifier of this type, relies on Identifier to enforce has_one for Global identifiers !! behaviour for Identifier::Local types may be unexpected.
- #italics_are_paired ⇒ Object protected
-
#namecase_bibtex_entry(bibtex_entry) ⇒ Object
Namecase all elements of all names.
-
#nomenclature_date ⇒ Time
Month handling allows values from bibtex like 'may' to be handled.
-
#nomenclature_year ⇒ Integer
The effective year of publication as per nomenclatural rules.
-
#render_with_style(style = 'vancouver', format = 'text', normalize_names = true) ⇒ String
This source, rendered in the provided CSL style, as text.
-
#serial_id(value) ⇒ Fixnum?
Non-Bibtex attribute that is cross-referenced.
-
#set_cached ⇒ Ignored
protected
set cached values and copies active record relations into bibtex values.
- #sv_contains_a_writer ⇒ Ignored protected
- #sv_duplicate_title ⇒ Ignored protected
- #sv_has_a_publisher ⇒ Ignored protected
- #sv_has_authors ⇒ Ignored protected
- #sv_has_booktitle ⇒ Ignored protected
- #sv_has_institution ⇒ Ignored protected
- #sv_has_note ⇒ Ignored protected
- #sv_has_school ⇒ Ignored protected
- #sv_has_some_type_of_year ⇒ Ignored protected
- #sv_has_title ⇒ Ignored protected
- #sv_is_article_missing_journal ⇒ Ignored protected
- #sv_is_contained_has_chapter_or_pages ⇒ Ignored protected
-
#sv_missing_required_bibtex_fields ⇒ Ignored
protected
rubocop:disable Metrics/MethodLength.
- #sv_missing_roles ⇒ Object protected
- #sv_year_exists ⇒ Ignored protected
-
#to_bibtex ⇒ BibTeX::Entry
rubocop:disable Metrics/MethodLength.
-
#url_as_uri ⇒ URI
turn bibtex URL field into a Ruby URI object.
-
#valid_bibtex? ⇒ Boolean
Whether the BibTeX::Entry representation of this source is valid.
- #validate_year_suffix ⇒ Object protected
- #verbatim_journal ⇒ String
-
#year_with_suffix ⇒ String
A string that represents the year with suffix as seen in a BibTeX bibliography.
Methods inherited from Source
batch_create, batch_preview, #cited_objects, #clone, #is_bibtex?, #is_in_project?, #reject_project_sources, select_optimized, used_recently
Methods included from SoftValidation
#clear_soft_validations, #fix_soft_validations, #soft_fixed?, #soft_valid?, #soft_validate, #soft_validated?, #soft_validations
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::HasRoles
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
#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
Instance Attribute Details
#abstract ⇒ String
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 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 |
# File 'app/models/source/bibtex.rb', line 306 class Source::Bibtex < Source attr_accessor :authors_to_create # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? } # TW required fields (must have one of these fields filled in) # 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, allow_destroy: true before_validation :create_authors, if: -> { !.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) } soft_validate(:sv_has_some_type_of_year, set: :recommended_fields, has_fix: false) soft_validate(:sv_contains_a_writer, set: :recommended_fields, has_fix: false) soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields, has_fix: false) soft_validate(:sv_duplicate_title, set: :duplicate_title, has_fix: false) soft_validate(:sv_missing_roles, set: :missing_roles, has_fix: false) # @param [BibTeX::Name] bibtex_author # @return [Person, Boolean] new person, or false def self.() return false if .class != BibTeX::Name p = Person.new( first_name: .first, prefix: .prefix, last_name: .last, suffix: .suffix) p.namecase_names p end # @return [BibTeX::Entry] def self.new_from_bibtex_text(text = nil) a = BibTeX::Bibliography.parse(text, filter: :latex).first new_from_bibtex(a) 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 if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title # serial with alternate_value on name .count = 1 assign .first # before validate assign serial if matching & not doesn't have a serial currently assigned. # @todo if there is an ISSN it should look up to see it the serial already exists. def self.new_from_bibtex(bibtex_entry = nil) return false if !bibtex_entry.kind_of?(::BibTeX::Entry) s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s) import_attributes = [] bibtex_entry.fields.each do |key, value| if key == :keywords s.verbatim_keywords = value next end v = value.to_s.strip if s.respond_to?(key.to_sym) && 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 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 # @return [String] A string that represents the authors last_names and year (no suffix) def return 'not yet calculated' if new_record? [, year].compact.join(', ') end # 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 return false if !self.valid? || self.new_record? || (self..blank? && self.editor.blank?) || self.roles.count > 0 bibtex = to_bibtex namecase_bibtex_entry(bibtex) begin Role.transaction do if bibtex. bibtex..each do |a| p = Source::Bibtex.(a) p.save! SourceAuthor.create!(role_object: self, person: p) end end if bibtex.editors bibtex.editors.each do |a| p = Source::Bibtex.(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 # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case # we need to set the serial based on ISSN. # @param [String] value # @return [String] def issn=(value) write_attribute(:issn, value) unless value.blank? tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first unless tw_issn.nil? || tw_issn.identifier != value tw_issn.destroy end self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value) end end # @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) unless self.url.blank? 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 return true if !.blank? return false if new_record? # self exists in the db .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_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 # Month handling allows values from bibtex like 'may' to be handled # @return [Time] def nomenclature_date Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), 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] # entry equivalent to self, this should round-trip with no changes def to_bibtex b = BibTeX::Entry.new(bibtex_type: bibtex_type) ::BIBTEX_FIELDS.each do |f| if (!self.send(f).blank?) && !(f == :bibtex_type) b[f] = self.send(f) end end b.year = year_with_suffix if !year_suffix.blank? b[:keywords] = verbatim_keywords unless verbatim_keywords.blank? b[:note] = concatenated_notes_string if !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. = get_bibtex_names('author') if .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?) b.key = id unless new_record? b end # @return [String, nil] # priority is Person, string # !! Not the cached value !! def a = .load if a.any? get_bibtex_names('author') else .blank? ? nil : end end # Namecase all elements of all names def namecase_bibtex_entry(bibtex_entry) bibtex_entry.parse_names bibtex_entry.names.each do |n| n.first = NameCase(n.first) if n.first n.last = NameCase(n.last) if n.last n.prefix = NameCase(n.prefix) if n.prefix n.suffix = NameCase(n.suffix) if n.suffix end true 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) cp = CiteProc::Processor.new(style: style, format: format) b = bibtex_bibliography namecase_bibtex_entry(b) if normalize_names cp.import(b.to_citeproc) 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 # # There (was) a problem with the zootaxa format and letters! # https://github.com/citation-style-language/styles/pull/2305 def cached_string(format = 'text') return nil unless (format == 'text') || (format == 'html') str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zookeys', format) # the current TaxonWorks default ... make a constant # str = render_with_style('entomological-society-of-america', format) # the current TaxonWorks default ... make a constant # str = render_with_style('florida-entomologist', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zoological-journal-of-the-linnean-society', format) # the current TaxonWorks default ... make a constant # str = render_with_style('systematic-biology', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-annotated-bibliography', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-fullnote-bibliography-16th-edition', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-library-list', format) # the current TaxonWorks default ... make a constant 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 (reload = true) reload ? .reload : .load if !.any? # no normalized people, use string, !! not .any? because of in-memory setting?! if .blank? return nil else b = to_bibtex namecase_bibtex_entry(b) return Utilities::Strings.(b..tokens.collect{ |t| t.last }) end else # use normalized records return Utilities::Strings.(.collect{ |a| a.full_last_name }) end end protected def validate_year_suffix a = 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 # @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 begin Person.transaction do .each do |shs| p = Person.create!(shs) .build(person: p) end end rescue errors.add(:base, 'invalid author parameters') end end # set cached values and copies active record relations into bibtex values # @return [Ignored] def set_cached if errors.empty? attributes_to_update = {} attributes_to_update[:author] = get_bibtex_names('author') if .reload.size > 0 attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0 c = cached_string('html') if bibtex_type == 'book' && !pages.blank? if pages.to_i.to_s == pages c = c + " #{pages} pp." else c = c + " #{pages}" end end n = [] n += [stated_year.to_s] if stated_year && year && stated_year != year n += ['in ' + Language.find(language_id).english_name.to_s] if language_id n += [note.to_s] if note c = c + " [#{n.join(', ')}]" unless n.empty? attributes_to_update.merge!( cached: c, cached_nomenclature_date: nomenclature_date, cached_author_string: (false) ) update_columns(attributes_to_update) end 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..count > 0 || self.editors.count > 0 || !self.serial.nil?) valid = true end if !valid errors.add( :base, 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year' ) end end #endregion hard validations # @return [Ignored] def sv_duplicate_title # only used in validating BibTeX output if Source.where(title: title).where.not(id: id).any? soft_validations.add(:title, 'Another source with this title exists, it may be duplicate') end end # @return [Ignored] def # only used in validating BibTeX output if !() soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.') end end # @return [Ignored] def sv_contains_a_writer # neither author nor editor if !has_writer? soft_validations.add(:base, 'There is neither author, nor editor associated with this source.') end end # @return [Ignored] def sv_has_title if title.blank? unless soft_validations..include?('There is no title associated with this source.') soft_validations.add(:title, 'There is no title associated with this source.') end end end # @return [Ignored] def sv_has_some_type_of_year if !has_some_year? soft_validations.add(:base, 'There is no year nor is there a stated year associated with this source.') end end # @return [Ignored] def sv_year_exists # only used in validating BibTeX output if year.blank? soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.') elsif year < 1700 soft_validations.add(:year, 'This year is prior to the 1700s.') end end # @return [Ignored] def sv_is_article_missing_journal if bibtex_type == 'article' if journal.nil? && serial_id.nil? soft_validations.add(:bibtex_type, 'This article is missing a journal name.') elsif serial_id.nil? soft_validations.add(:serial_id, 'This article is missing a serial.') end end end # @return [Ignored] def sv_has_a_publisher if publisher.blank? soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.') end end # @return [Ignored] def sv_has_booktitle if booktitle.blank? soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.') end end # @return [Ignored] def sv_is_contained_has_chapter_or_pages if chapter.blank? && pages.blank? soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') end end # @return [Ignored] def sv_has_school if school.blank? soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.') end end # @return [Ignored] def sv_has_institution if institution.blank? soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.') end end # @return [Ignored] def sv_has_note if (note.blank?) && (!notes.any?) soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished source.') end end # rubocop:disable Metrics/MethodLength # @return [Ignored] def sv_missing_required_bibtex_fields case bibtex_type when 'article' # :article => [:author,:title,:journal,:year] sv_has_title sv_is_article_missing_journal sv_year_exists when 'book' #: book => [[:author,:editor],:title,:publisher,:year] sv_contains_a_writer sv_has_title sv_has_a_publisher sv_year_exists when 'booklet' # :booklet => [:title], sv_has_title when 'conference' # :conference => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'inbook' # :inbook => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year], sv_contains_a_writer sv_has_title sv_is_contained_has_chapter_or_pages sv_has_a_publisher sv_year_exists when 'incollection' # :incollection => [:author,:title,:booktitle,:publisher,:year], sv_has_title sv_has_booktitle sv_has_a_publisher sv_year_exists when 'inproceedings' # :inproceedings => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'manual' # :manual => [:title], sv_has_title when 'mastersthesis' # :mastersthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists # :misc => [], (no required fields) when 'phdthesis' # :phdthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists when 'proceedings' # :proceedings => [:title,:year], sv_has_title sv_year_exists when 'techreport' # :techreport => [:author,:title,:institution,:year], sv_has_title sv_has_institution sv_year_exists when 'unpublished' # :unpublished => [:author,:title,:note] sv_has_title sv_has_note end end def sv_missing_roles soft_validations.add(:base, 'Author roles are not selected.') if self..empty? end # rubocop:enable Metrics/MethodLength 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.
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 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 |
# File 'app/models/source/bibtex.rb', line 306 class Source::Bibtex < Source attr_accessor :authors_to_create # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? } # TW required fields (must have one of these fields filled in) # 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, allow_destroy: true before_validation :create_authors, if: -> { !.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) } soft_validate(:sv_has_some_type_of_year, set: :recommended_fields, has_fix: false) soft_validate(:sv_contains_a_writer, set: :recommended_fields, has_fix: false) soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields, has_fix: false) soft_validate(:sv_duplicate_title, set: :duplicate_title, has_fix: false) soft_validate(:sv_missing_roles, set: :missing_roles, has_fix: false) # @param [BibTeX::Name] bibtex_author # @return [Person, Boolean] new person, or false def self.() return false if .class != BibTeX::Name p = Person.new( first_name: .first, prefix: .prefix, last_name: .last, suffix: .suffix) p.namecase_names p end # @return [BibTeX::Entry] def self.new_from_bibtex_text(text = nil) a = BibTeX::Bibliography.parse(text, filter: :latex).first new_from_bibtex(a) 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 if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title # serial with alternate_value on name .count = 1 assign .first # before validate assign serial if matching & not doesn't have a serial currently assigned. # @todo if there is an ISSN it should look up to see it the serial already exists. def self.new_from_bibtex(bibtex_entry = nil) return false if !bibtex_entry.kind_of?(::BibTeX::Entry) s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s) import_attributes = [] bibtex_entry.fields.each do |key, value| if key == :keywords s.verbatim_keywords = value next end v = value.to_s.strip if s.respond_to?(key.to_sym) && 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 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 # @return [String] A string that represents the authors last_names and year (no suffix) def return 'not yet calculated' if new_record? [, year].compact.join(', ') end # 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 return false if !self.valid? || self.new_record? || (self..blank? && self.editor.blank?) || self.roles.count > 0 bibtex = to_bibtex namecase_bibtex_entry(bibtex) begin Role.transaction do if bibtex. bibtex..each do |a| p = Source::Bibtex.(a) p.save! SourceAuthor.create!(role_object: self, person: p) end end if bibtex.editors bibtex.editors.each do |a| p = Source::Bibtex.(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 # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case # we need to set the serial based on ISSN. # @param [String] value # @return [String] def issn=(value) write_attribute(:issn, value) unless value.blank? tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first unless tw_issn.nil? || tw_issn.identifier != value tw_issn.destroy end self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value) end end # @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) unless self.url.blank? 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 return true if !.blank? return false if new_record? # self exists in the db .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_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 # Month handling allows values from bibtex like 'may' to be handled # @return [Time] def nomenclature_date Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), 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] # entry equivalent to self, this should round-trip with no changes def to_bibtex b = BibTeX::Entry.new(bibtex_type: bibtex_type) ::BIBTEX_FIELDS.each do |f| if (!self.send(f).blank?) && !(f == :bibtex_type) b[f] = self.send(f) end end b.year = year_with_suffix if !year_suffix.blank? b[:keywords] = verbatim_keywords unless verbatim_keywords.blank? b[:note] = concatenated_notes_string if !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. = get_bibtex_names('author') if .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?) b.key = id unless new_record? b end # @return [String, nil] # priority is Person, string # !! Not the cached value !! def a = .load if a.any? get_bibtex_names('author') else .blank? ? nil : end end # Namecase all elements of all names def namecase_bibtex_entry(bibtex_entry) bibtex_entry.parse_names bibtex_entry.names.each do |n| n.first = NameCase(n.first) if n.first n.last = NameCase(n.last) if n.last n.prefix = NameCase(n.prefix) if n.prefix n.suffix = NameCase(n.suffix) if n.suffix end true 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) cp = CiteProc::Processor.new(style: style, format: format) b = bibtex_bibliography namecase_bibtex_entry(b) if normalize_names cp.import(b.to_citeproc) 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 # # There (was) a problem with the zootaxa format and letters! # https://github.com/citation-style-language/styles/pull/2305 def cached_string(format = 'text') return nil unless (format == 'text') || (format == 'html') str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zookeys', format) # the current TaxonWorks default ... make a constant # str = render_with_style('entomological-society-of-america', format) # the current TaxonWorks default ... make a constant # str = render_with_style('florida-entomologist', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zoological-journal-of-the-linnean-society', format) # the current TaxonWorks default ... make a constant # str = render_with_style('systematic-biology', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-annotated-bibliography', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-fullnote-bibliography-16th-edition', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-library-list', format) # the current TaxonWorks default ... make a constant 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 (reload = true) reload ? .reload : .load if !.any? # no normalized people, use string, !! not .any? because of in-memory setting?! if .blank? return nil else b = to_bibtex namecase_bibtex_entry(b) return Utilities::Strings.(b..tokens.collect{ |t| t.last }) end else # use normalized records return Utilities::Strings.(.collect{ |a| a.full_last_name }) end end protected def validate_year_suffix a = 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 # @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 begin Person.transaction do .each do |shs| p = Person.create!(shs) .build(person: p) end end rescue errors.add(:base, 'invalid author parameters') end end # set cached values and copies active record relations into bibtex values # @return [Ignored] def set_cached if errors.empty? attributes_to_update = {} attributes_to_update[:author] = get_bibtex_names('author') if .reload.size > 0 attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0 c = cached_string('html') if bibtex_type == 'book' && !pages.blank? if pages.to_i.to_s == pages c = c + " #{pages} pp." else c = c + " #{pages}" end end n = [] n += [stated_year.to_s] if stated_year && year && stated_year != year n += ['in ' + Language.find(language_id).english_name.to_s] if language_id n += [note.to_s] if note c = c + " [#{n.join(', ')}]" unless n.empty? attributes_to_update.merge!( cached: c, cached_nomenclature_date: nomenclature_date, cached_author_string: (false) ) update_columns(attributes_to_update) end 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..count > 0 || self.editors.count > 0 || !self.serial.nil?) valid = true end if !valid errors.add( :base, 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year' ) end end #endregion hard validations # @return [Ignored] def sv_duplicate_title # only used in validating BibTeX output if Source.where(title: title).where.not(id: id).any? soft_validations.add(:title, 'Another source with this title exists, it may be duplicate') end end # @return [Ignored] def # only used in validating BibTeX output if !() soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.') end end # @return [Ignored] def sv_contains_a_writer # neither author nor editor if !has_writer? soft_validations.add(:base, 'There is neither author, nor editor associated with this source.') end end # @return [Ignored] def sv_has_title if title.blank? unless soft_validations..include?('There is no title associated with this source.') soft_validations.add(:title, 'There is no title associated with this source.') end end end # @return [Ignored] def sv_has_some_type_of_year if !has_some_year? soft_validations.add(:base, 'There is no year nor is there a stated year associated with this source.') end end # @return [Ignored] def sv_year_exists # only used in validating BibTeX output if year.blank? soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.') elsif year < 1700 soft_validations.add(:year, 'This year is prior to the 1700s.') end end # @return [Ignored] def sv_is_article_missing_journal if bibtex_type == 'article' if journal.nil? && serial_id.nil? soft_validations.add(:bibtex_type, 'This article is missing a journal name.') elsif serial_id.nil? soft_validations.add(:serial_id, 'This article is missing a serial.') end end end # @return [Ignored] def sv_has_a_publisher if publisher.blank? soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.') end end # @return [Ignored] def sv_has_booktitle if booktitle.blank? soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.') end end # @return [Ignored] def sv_is_contained_has_chapter_or_pages if chapter.blank? && pages.blank? soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') end end # @return [Ignored] def sv_has_school if school.blank? soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.') end end # @return [Ignored] def sv_has_institution if institution.blank? soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.') end end # @return [Ignored] def sv_has_note if (note.blank?) && (!notes.any?) soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished source.') end end # rubocop:disable Metrics/MethodLength # @return [Ignored] def sv_missing_required_bibtex_fields case bibtex_type when 'article' # :article => [:author,:title,:journal,:year] sv_has_title sv_is_article_missing_journal sv_year_exists when 'book' #: book => [[:author,:editor],:title,:publisher,:year] sv_contains_a_writer sv_has_title sv_has_a_publisher sv_year_exists when 'booklet' # :booklet => [:title], sv_has_title when 'conference' # :conference => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'inbook' # :inbook => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year], sv_contains_a_writer sv_has_title sv_is_contained_has_chapter_or_pages sv_has_a_publisher sv_year_exists when 'incollection' # :incollection => [:author,:title,:booktitle,:publisher,:year], sv_has_title sv_has_booktitle sv_has_a_publisher sv_year_exists when 'inproceedings' # :inproceedings => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'manual' # :manual => [:title], sv_has_title when 'mastersthesis' # :mastersthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists # :misc => [], (no required fields) when 'phdthesis' # :phdthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists when 'proceedings' # :proceedings => [:title,:year], sv_has_title sv_year_exists when 'techreport' # :techreport => [:author,:title,:institution,:year], sv_has_title sv_has_institution sv_year_exists when 'unpublished' # :unpublished => [:author,:title,:note] sv_has_title sv_has_note end end def sv_missing_roles soft_validations.add(:base, 'Author roles are not selected.') if self..empty? end # rubocop:enable Metrics/MethodLength end |
#annote ⇒ String?
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.
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 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 |
# File 'app/models/source/bibtex.rb', line 306 class Source::Bibtex < Source attr_accessor :authors_to_create # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? } # TW required fields (must have one of these fields filled in) # 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, allow_destroy: true before_validation :create_authors, if: -> { !.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) } soft_validate(:sv_has_some_type_of_year, set: :recommended_fields, has_fix: false) soft_validate(:sv_contains_a_writer, set: :recommended_fields, has_fix: false) soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields, has_fix: false) soft_validate(:sv_duplicate_title, set: :duplicate_title, has_fix: false) soft_validate(:sv_missing_roles, set: :missing_roles, has_fix: false) # @param [BibTeX::Name] bibtex_author # @return [Person, Boolean] new person, or false def self.() return false if .class != BibTeX::Name p = Person.new( first_name: .first, prefix: .prefix, last_name: .last, suffix: .suffix) p.namecase_names p end # @return [BibTeX::Entry] def self.new_from_bibtex_text(text = nil) a = BibTeX::Bibliography.parse(text, filter: :latex).first new_from_bibtex(a) 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 if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title # serial with alternate_value on name .count = 1 assign .first # before validate assign serial if matching & not doesn't have a serial currently assigned. # @todo if there is an ISSN it should look up to see it the serial already exists. def self.new_from_bibtex(bibtex_entry = nil) return false if !bibtex_entry.kind_of?(::BibTeX::Entry) s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s) import_attributes = [] bibtex_entry.fields.each do |key, value| if key == :keywords s.verbatim_keywords = value next end v = value.to_s.strip if s.respond_to?(key.to_sym) && 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 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 # @return [String] A string that represents the authors last_names and year (no suffix) def return 'not yet calculated' if new_record? [, year].compact.join(', ') end # 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 return false if !self.valid? || self.new_record? || (self..blank? && self.editor.blank?) || self.roles.count > 0 bibtex = to_bibtex namecase_bibtex_entry(bibtex) begin Role.transaction do if bibtex. bibtex..each do |a| p = Source::Bibtex.(a) p.save! SourceAuthor.create!(role_object: self, person: p) end end if bibtex.editors bibtex.editors.each do |a| p = Source::Bibtex.(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 # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case # we need to set the serial based on ISSN. # @param [String] value # @return [String] def issn=(value) write_attribute(:issn, value) unless value.blank? tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first unless tw_issn.nil? || tw_issn.identifier != value tw_issn.destroy end self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value) end end # @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) unless self.url.blank? 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 return true if !.blank? return false if new_record? # self exists in the db .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_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 # Month handling allows values from bibtex like 'may' to be handled # @return [Time] def nomenclature_date Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), 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] # entry equivalent to self, this should round-trip with no changes def to_bibtex b = BibTeX::Entry.new(bibtex_type: bibtex_type) ::BIBTEX_FIELDS.each do |f| if (!self.send(f).blank?) && !(f == :bibtex_type) b[f] = self.send(f) end end b.year = year_with_suffix if !year_suffix.blank? b[:keywords] = verbatim_keywords unless verbatim_keywords.blank? b[:note] = concatenated_notes_string if !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. = get_bibtex_names('author') if .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?) b.key = id unless new_record? b end # @return [String, nil] # priority is Person, string # !! Not the cached value !! def a = .load if a.any? get_bibtex_names('author') else .blank? ? nil : end end # Namecase all elements of all names def namecase_bibtex_entry(bibtex_entry) bibtex_entry.parse_names bibtex_entry.names.each do |n| n.first = NameCase(n.first) if n.first n.last = NameCase(n.last) if n.last n.prefix = NameCase(n.prefix) if n.prefix n.suffix = NameCase(n.suffix) if n.suffix end true 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) cp = CiteProc::Processor.new(style: style, format: format) b = bibtex_bibliography namecase_bibtex_entry(b) if normalize_names cp.import(b.to_citeproc) 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 # # There (was) a problem with the zootaxa format and letters! # https://github.com/citation-style-language/styles/pull/2305 def cached_string(format = 'text') return nil unless (format == 'text') || (format == 'html') str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zookeys', format) # the current TaxonWorks default ... make a constant # str = render_with_style('entomological-society-of-america', format) # the current TaxonWorks default ... make a constant # str = render_with_style('florida-entomologist', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zoological-journal-of-the-linnean-society', format) # the current TaxonWorks default ... make a constant # str = render_with_style('systematic-biology', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-annotated-bibliography', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-fullnote-bibliography-16th-edition', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-library-list', format) # the current TaxonWorks default ... make a constant 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 (reload = true) reload ? .reload : .load if !.any? # no normalized people, use string, !! not .any? because of in-memory setting?! if .blank? return nil else b = to_bibtex namecase_bibtex_entry(b) return Utilities::Strings.(b..tokens.collect{ |t| t.last }) end else # use normalized records return Utilities::Strings.(.collect{ |a| a.full_last_name }) end end protected def validate_year_suffix a = 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 # @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 begin Person.transaction do .each do |shs| p = Person.create!(shs) .build(person: p) end end rescue errors.add(:base, 'invalid author parameters') end end # set cached values and copies active record relations into bibtex values # @return [Ignored] def set_cached if errors.empty? attributes_to_update = {} attributes_to_update[:author] = get_bibtex_names('author') if .reload.size > 0 attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0 c = cached_string('html') if bibtex_type == 'book' && !pages.blank? if pages.to_i.to_s == pages c = c + " #{pages} pp." else c = c + " #{pages}" end end n = [] n += [stated_year.to_s] if stated_year && year && stated_year != year n += ['in ' + Language.find(language_id).english_name.to_s] if language_id n += [note.to_s] if note c = c + " [#{n.join(', ')}]" unless n.empty? attributes_to_update.merge!( cached: c, cached_nomenclature_date: nomenclature_date, cached_author_string: (false) ) update_columns(attributes_to_update) end 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..count > 0 || self.editors.count > 0 || !self.serial.nil?) valid = true end if !valid errors.add( :base, 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year' ) end end #endregion hard validations # @return [Ignored] def sv_duplicate_title # only used in validating BibTeX output if Source.where(title: title).where.not(id: id).any? soft_validations.add(:title, 'Another source with this title exists, it may be duplicate') end end # @return [Ignored] def # only used in validating BibTeX output if !() soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.') end end # @return [Ignored] def sv_contains_a_writer # neither author nor editor if !has_writer? soft_validations.add(:base, 'There is neither author, nor editor associated with this source.') end end # @return [Ignored] def sv_has_title if title.blank? unless soft_validations..include?('There is no title associated with this source.') soft_validations.add(:title, 'There is no title associated with this source.') end end end # @return [Ignored] def sv_has_some_type_of_year if !has_some_year? soft_validations.add(:base, 'There is no year nor is there a stated year associated with this source.') end end # @return [Ignored] def sv_year_exists # only used in validating BibTeX output if year.blank? soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.') elsif year < 1700 soft_validations.add(:year, 'This year is prior to the 1700s.') end end # @return [Ignored] def sv_is_article_missing_journal if bibtex_type == 'article' if journal.nil? && serial_id.nil? soft_validations.add(:bibtex_type, 'This article is missing a journal name.') elsif serial_id.nil? soft_validations.add(:serial_id, 'This article is missing a serial.') end end end # @return [Ignored] def sv_has_a_publisher if publisher.blank? soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.') end end # @return [Ignored] def sv_has_booktitle if booktitle.blank? soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.') end end # @return [Ignored] def sv_is_contained_has_chapter_or_pages if chapter.blank? && pages.blank? soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') end end # @return [Ignored] def sv_has_school if school.blank? soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.') end end # @return [Ignored] def sv_has_institution if institution.blank? soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.') end end # @return [Ignored] def sv_has_note if (note.blank?) && (!notes.any?) soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished source.') end end # rubocop:disable Metrics/MethodLength # @return [Ignored] def sv_missing_required_bibtex_fields case bibtex_type when 'article' # :article => [:author,:title,:journal,:year] sv_has_title sv_is_article_missing_journal sv_year_exists when 'book' #: book => [[:author,:editor],:title,:publisher,:year] sv_contains_a_writer sv_has_title sv_has_a_publisher sv_year_exists when 'booklet' # :booklet => [:title], sv_has_title when 'conference' # :conference => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'inbook' # :inbook => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year], sv_contains_a_writer sv_has_title sv_is_contained_has_chapter_or_pages sv_has_a_publisher sv_year_exists when 'incollection' # :incollection => [:author,:title,:booktitle,:publisher,:year], sv_has_title sv_has_booktitle sv_has_a_publisher sv_year_exists when 'inproceedings' # :inproceedings => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'manual' # :manual => [:title], sv_has_title when 'mastersthesis' # :mastersthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists # :misc => [], (no required fields) when 'phdthesis' # :phdthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists when 'proceedings' # :proceedings => [:title,:year], sv_has_title sv_year_exists when 'techreport' # :techreport => [:author,:title,:institution,:year], sv_has_title sv_has_institution sv_year_exists when 'unpublished' # :unpublished => [:author,:title,:note] sv_has_title sv_has_note end end def sv_missing_roles soft_validations.add(:base, 'Author roles are not selected.') if self..empty? end # rubocop:enable Metrics/MethodLength end |
#author ⇒ String?
“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`
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 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 |
# File 'app/models/source/bibtex.rb', line 306 class Source::Bibtex < Source attr_accessor :authors_to_create # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? } # TW required fields (must have one of these fields filled in) # 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, allow_destroy: true before_validation :create_authors, if: -> { !.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) } soft_validate(:sv_has_some_type_of_year, set: :recommended_fields, has_fix: false) soft_validate(:sv_contains_a_writer, set: :recommended_fields, has_fix: false) soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields, has_fix: false) soft_validate(:sv_duplicate_title, set: :duplicate_title, has_fix: false) soft_validate(:sv_missing_roles, set: :missing_roles, has_fix: false) # @param [BibTeX::Name] bibtex_author # @return [Person, Boolean] new person, or false def self.() return false if .class != BibTeX::Name p = Person.new( first_name: .first, prefix: .prefix, last_name: .last, suffix: .suffix) p.namecase_names p end # @return [BibTeX::Entry] def self.new_from_bibtex_text(text = nil) a = BibTeX::Bibliography.parse(text, filter: :latex).first new_from_bibtex(a) 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 if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title # serial with alternate_value on name .count = 1 assign .first # before validate assign serial if matching & not doesn't have a serial currently assigned. # @todo if there is an ISSN it should look up to see it the serial already exists. def self.new_from_bibtex(bibtex_entry = nil) return false if !bibtex_entry.kind_of?(::BibTeX::Entry) s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s) import_attributes = [] bibtex_entry.fields.each do |key, value| if key == :keywords s.verbatim_keywords = value next end v = value.to_s.strip if s.respond_to?(key.to_sym) && 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 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 # @return [String] A string that represents the authors last_names and year (no suffix) def return 'not yet calculated' if new_record? [, year].compact.join(', ') end # 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 return false if !self.valid? || self.new_record? || (self..blank? && self.editor.blank?) || self.roles.count > 0 bibtex = to_bibtex namecase_bibtex_entry(bibtex) begin Role.transaction do if bibtex. bibtex..each do |a| p = Source::Bibtex.(a) p.save! SourceAuthor.create!(role_object: self, person: p) end end if bibtex.editors bibtex.editors.each do |a| p = Source::Bibtex.(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 # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case # we need to set the serial based on ISSN. # @param [String] value # @return [String] def issn=(value) write_attribute(:issn, value) unless value.blank? tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first unless tw_issn.nil? || tw_issn.identifier != value tw_issn.destroy end self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value) end end # @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) unless self.url.blank? 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 return true if !.blank? return false if new_record? # self exists in the db .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_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 # Month handling allows values from bibtex like 'may' to be handled # @return [Time] def nomenclature_date Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), 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] # entry equivalent to self, this should round-trip with no changes def to_bibtex b = BibTeX::Entry.new(bibtex_type: bibtex_type) ::BIBTEX_FIELDS.each do |f| if (!self.send(f).blank?) && !(f == :bibtex_type) b[f] = self.send(f) end end b.year = year_with_suffix if !year_suffix.blank? b[:keywords] = verbatim_keywords unless verbatim_keywords.blank? b[:note] = concatenated_notes_string if !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. = get_bibtex_names('author') if .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?) b.key = id unless new_record? b end # @return [String, nil] # priority is Person, string # !! Not the cached value !! def a = .load if a.any? get_bibtex_names('author') else .blank? ? nil : end end # Namecase all elements of all names def namecase_bibtex_entry(bibtex_entry) bibtex_entry.parse_names bibtex_entry.names.each do |n| n.first = NameCase(n.first) if n.first n.last = NameCase(n.last) if n.last n.prefix = NameCase(n.prefix) if n.prefix n.suffix = NameCase(n.suffix) if n.suffix end true 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) cp = CiteProc::Processor.new(style: style, format: format) b = bibtex_bibliography namecase_bibtex_entry(b) if normalize_names cp.import(b.to_citeproc) 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 # # There (was) a problem with the zootaxa format and letters! # https://github.com/citation-style-language/styles/pull/2305 def cached_string(format = 'text') return nil unless (format == 'text') || (format == 'html') str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zookeys', format) # the current TaxonWorks default ... make a constant # str = render_with_style('entomological-society-of-america', format) # the current TaxonWorks default ... make a constant # str = render_with_style('florida-entomologist', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zoological-journal-of-the-linnean-society', format) # the current TaxonWorks default ... make a constant # str = render_with_style('systematic-biology', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-annotated-bibliography', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-fullnote-bibliography-16th-edition', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-library-list', format) # the current TaxonWorks default ... make a constant 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 (reload = true) reload ? .reload : .load if !.any? # no normalized people, use string, !! not .any? because of in-memory setting?! if .blank? return nil else b = to_bibtex namecase_bibtex_entry(b) return Utilities::Strings.(b..tokens.collect{ |t| t.last }) end else # use normalized records return Utilities::Strings.(.collect{ |a| a.full_last_name }) end end protected def validate_year_suffix a = 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 # @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 begin Person.transaction do .each do |shs| p = Person.create!(shs) .build(person: p) end end rescue errors.add(:base, 'invalid author parameters') end end # set cached values and copies active record relations into bibtex values # @return [Ignored] def set_cached if errors.empty? attributes_to_update = {} attributes_to_update[:author] = get_bibtex_names('author') if .reload.size > 0 attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0 c = cached_string('html') if bibtex_type == 'book' && !pages.blank? if pages.to_i.to_s == pages c = c + " #{pages} pp." else c = c + " #{pages}" end end n = [] n += [stated_year.to_s] if stated_year && year && stated_year != year n += ['in ' + Language.find(language_id).english_name.to_s] if language_id n += [note.to_s] if note c = c + " [#{n.join(', ')}]" unless n.empty? attributes_to_update.merge!( cached: c, cached_nomenclature_date: nomenclature_date, cached_author_string: (false) ) update_columns(attributes_to_update) end 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..count > 0 || self.editors.count > 0 || !self.serial.nil?) valid = true end if !valid errors.add( :base, 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year' ) end end #endregion hard validations # @return [Ignored] def sv_duplicate_title # only used in validating BibTeX output if Source.where(title: title).where.not(id: id).any? soft_validations.add(:title, 'Another source with this title exists, it may be duplicate') end end # @return [Ignored] def # only used in validating BibTeX output if !() soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.') end end # @return [Ignored] def sv_contains_a_writer # neither author nor editor if !has_writer? soft_validations.add(:base, 'There is neither author, nor editor associated with this source.') end end # @return [Ignored] def sv_has_title if title.blank? unless soft_validations..include?('There is no title associated with this source.') soft_validations.add(:title, 'There is no title associated with this source.') end end end # @return [Ignored] def sv_has_some_type_of_year if !has_some_year? soft_validations.add(:base, 'There is no year nor is there a stated year associated with this source.') end end # @return [Ignored] def sv_year_exists # only used in validating BibTeX output if year.blank? soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.') elsif year < 1700 soft_validations.add(:year, 'This year is prior to the 1700s.') end end # @return [Ignored] def sv_is_article_missing_journal if bibtex_type == 'article' if journal.nil? && serial_id.nil? soft_validations.add(:bibtex_type, 'This article is missing a journal name.') elsif serial_id.nil? soft_validations.add(:serial_id, 'This article is missing a serial.') end end end # @return [Ignored] def sv_has_a_publisher if publisher.blank? soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.') end end # @return [Ignored] def sv_has_booktitle if booktitle.blank? soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.') end end # @return [Ignored] def sv_is_contained_has_chapter_or_pages if chapter.blank? && pages.blank? soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') end end # @return [Ignored] def sv_has_school if school.blank? soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.') end end # @return [Ignored] def sv_has_institution if institution.blank? soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.') end end # @return [Ignored] def sv_has_note if (note.blank?) && (!notes.any?) soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished source.') end end # rubocop:disable Metrics/MethodLength # @return [Ignored] def sv_missing_required_bibtex_fields case bibtex_type when 'article' # :article => [:author,:title,:journal,:year] sv_has_title sv_is_article_missing_journal sv_year_exists when 'book' #: book => [[:author,:editor],:title,:publisher,:year] sv_contains_a_writer sv_has_title sv_has_a_publisher sv_year_exists when 'booklet' # :booklet => [:title], sv_has_title when 'conference' # :conference => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'inbook' # :inbook => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year], sv_contains_a_writer sv_has_title sv_is_contained_has_chapter_or_pages sv_has_a_publisher sv_year_exists when 'incollection' # :incollection => [:author,:title,:booktitle,:publisher,:year], sv_has_title sv_has_booktitle sv_has_a_publisher sv_year_exists when 'inproceedings' # :inproceedings => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'manual' # :manual => [:title], sv_has_title when 'mastersthesis' # :mastersthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists # :misc => [], (no required fields) when 'phdthesis' # :phdthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists when 'proceedings' # :proceedings => [:title,:year], sv_has_title sv_year_exists when 'techreport' # :techreport => [:author,:title,:institution,:year], sv_has_title sv_has_institution sv_year_exists when 'unpublished' # :unpublished => [:author,:title,:note] sv_has_title sv_has_note end end def sv_missing_roles soft_validations.add(:base, 'Author roles are not selected.') if self..empty? end # rubocop:enable Metrics/MethodLength end |
#authors_to_create ⇒ Object
Returns the value of attribute authors_to_create.
308 309 310 |
# File 'app/models/source/bibtex.rb', line 308 def @authors_to_create end |
#bibtex_type ⇒ String
Returns one of VALID_BIBTEX_TYPES (config/initializers/constants/_controlled_vocabularies/bibtex_constants.rb, keys there are symbols).
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 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 |
# File 'app/models/source/bibtex.rb', line 306 class Source::Bibtex < Source attr_accessor :authors_to_create # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? } # TW required fields (must have one of these fields filled in) # 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, allow_destroy: true before_validation :create_authors, if: -> { !.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) } soft_validate(:sv_has_some_type_of_year, set: :recommended_fields, has_fix: false) soft_validate(:sv_contains_a_writer, set: :recommended_fields, has_fix: false) soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields, has_fix: false) soft_validate(:sv_duplicate_title, set: :duplicate_title, has_fix: false) soft_validate(:sv_missing_roles, set: :missing_roles, has_fix: false) # @param [BibTeX::Name] bibtex_author # @return [Person, Boolean] new person, or false def self.() return false if .class != BibTeX::Name p = Person.new( first_name: .first, prefix: .prefix, last_name: .last, suffix: .suffix) p.namecase_names p end # @return [BibTeX::Entry] def self.new_from_bibtex_text(text = nil) a = BibTeX::Bibliography.parse(text, filter: :latex).first new_from_bibtex(a) 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 if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title # serial with alternate_value on name .count = 1 assign .first # before validate assign serial if matching & not doesn't have a serial currently assigned. # @todo if there is an ISSN it should look up to see it the serial already exists. def self.new_from_bibtex(bibtex_entry = nil) return false if !bibtex_entry.kind_of?(::BibTeX::Entry) s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s) import_attributes = [] bibtex_entry.fields.each do |key, value| if key == :keywords s.verbatim_keywords = value next end v = value.to_s.strip if s.respond_to?(key.to_sym) && 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 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 # @return [String] A string that represents the authors last_names and year (no suffix) def return 'not yet calculated' if new_record? [, year].compact.join(', ') end # 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 return false if !self.valid? || self.new_record? || (self..blank? && self.editor.blank?) || self.roles.count > 0 bibtex = to_bibtex namecase_bibtex_entry(bibtex) begin Role.transaction do if bibtex. bibtex..each do |a| p = Source::Bibtex.(a) p.save! SourceAuthor.create!(role_object: self, person: p) end end if bibtex.editors bibtex.editors.each do |a| p = Source::Bibtex.(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 # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case # we need to set the serial based on ISSN. # @param [String] value # @return [String] def issn=(value) write_attribute(:issn, value) unless value.blank? tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first unless tw_issn.nil? || tw_issn.identifier != value tw_issn.destroy end self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value) end end # @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) unless self.url.blank? 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 return true if !.blank? return false if new_record? # self exists in the db .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_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 # Month handling allows values from bibtex like 'may' to be handled # @return [Time] def nomenclature_date Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), 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] # entry equivalent to self, this should round-trip with no changes def to_bibtex b = BibTeX::Entry.new(bibtex_type: bibtex_type) ::BIBTEX_FIELDS.each do |f| if (!self.send(f).blank?) && !(f == :bibtex_type) b[f] = self.send(f) end end b.year = year_with_suffix if !year_suffix.blank? b[:keywords] = verbatim_keywords unless verbatim_keywords.blank? b[:note] = concatenated_notes_string if !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. = get_bibtex_names('author') if .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?) b.key = id unless new_record? b end # @return [String, nil] # priority is Person, string # !! Not the cached value !! def a = .load if a.any? get_bibtex_names('author') else .blank? ? nil : end end # Namecase all elements of all names def namecase_bibtex_entry(bibtex_entry) bibtex_entry.parse_names bibtex_entry.names.each do |n| n.first = NameCase(n.first) if n.first n.last = NameCase(n.last) if n.last n.prefix = NameCase(n.prefix) if n.prefix n.suffix = NameCase(n.suffix) if n.suffix end true 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) cp = CiteProc::Processor.new(style: style, format: format) b = bibtex_bibliography namecase_bibtex_entry(b) if normalize_names cp.import(b.to_citeproc) 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 # # There (was) a problem with the zootaxa format and letters! # https://github.com/citation-style-language/styles/pull/2305 def cached_string(format = 'text') return nil unless (format == 'text') || (format == 'html') str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zookeys', format) # the current TaxonWorks default ... make a constant # str = render_with_style('entomological-society-of-america', format) # the current TaxonWorks default ... make a constant # str = render_with_style('florida-entomologist', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zoological-journal-of-the-linnean-society', format) # the current TaxonWorks default ... make a constant # str = render_with_style('systematic-biology', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-annotated-bibliography', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-fullnote-bibliography-16th-edition', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-library-list', format) # the current TaxonWorks default ... make a constant 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 (reload = true) reload ? .reload : .load if !.any? # no normalized people, use string, !! not .any? because of in-memory setting?! if .blank? return nil else b = to_bibtex namecase_bibtex_entry(b) return Utilities::Strings.(b..tokens.collect{ |t| t.last }) end else # use normalized records return Utilities::Strings.(.collect{ |a| a.full_last_name }) end end protected def validate_year_suffix a = 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 # @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 begin Person.transaction do .each do |shs| p = Person.create!(shs) .build(person: p) end end rescue errors.add(:base, 'invalid author parameters') end end # set cached values and copies active record relations into bibtex values # @return [Ignored] def set_cached if errors.empty? attributes_to_update = {} attributes_to_update[:author] = get_bibtex_names('author') if .reload.size > 0 attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0 c = cached_string('html') if bibtex_type == 'book' && !pages.blank? if pages.to_i.to_s == pages c = c + " #{pages} pp." else c = c + " #{pages}" end end n = [] n += [stated_year.to_s] if stated_year && year && stated_year != year n += ['in ' + Language.find(language_id).english_name.to_s] if language_id n += [note.to_s] if note c = c + " [#{n.join(', ')}]" unless n.empty? attributes_to_update.merge!( cached: c, cached_nomenclature_date: nomenclature_date, cached_author_string: (false) ) update_columns(attributes_to_update) end 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..count > 0 || self.editors.count > 0 || !self.serial.nil?) valid = true end if !valid errors.add( :base, 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year' ) end end #endregion hard validations # @return [Ignored] def sv_duplicate_title # only used in validating BibTeX output if Source.where(title: title).where.not(id: id).any? soft_validations.add(:title, 'Another source with this title exists, it may be duplicate') end end # @return [Ignored] def # only used in validating BibTeX output if !() soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.') end end # @return [Ignored] def sv_contains_a_writer # neither author nor editor if !has_writer? soft_validations.add(:base, 'There is neither author, nor editor associated with this source.') end end # @return [Ignored] def sv_has_title if title.blank? unless soft_validations..include?('There is no title associated with this source.') soft_validations.add(:title, 'There is no title associated with this source.') end end end # @return [Ignored] def sv_has_some_type_of_year if !has_some_year? soft_validations.add(:base, 'There is no year nor is there a stated year associated with this source.') end end # @return [Ignored] def sv_year_exists # only used in validating BibTeX output if year.blank? soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.') elsif year < 1700 soft_validations.add(:year, 'This year is prior to the 1700s.') end end # @return [Ignored] def sv_is_article_missing_journal if bibtex_type == 'article' if journal.nil? && serial_id.nil? soft_validations.add(:bibtex_type, 'This article is missing a journal name.') elsif serial_id.nil? soft_validations.add(:serial_id, 'This article is missing a serial.') end end end # @return [Ignored] def sv_has_a_publisher if publisher.blank? soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.') end end # @return [Ignored] def sv_has_booktitle if booktitle.blank? soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.') end end # @return [Ignored] def sv_is_contained_has_chapter_or_pages if chapter.blank? && pages.blank? soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') end end # @return [Ignored] def sv_has_school if school.blank? soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.') end end # @return [Ignored] def sv_has_institution if institution.blank? soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.') end end # @return [Ignored] def sv_has_note if (note.blank?) && (!notes.any?) soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished source.') end end # rubocop:disable Metrics/MethodLength # @return [Ignored] def sv_missing_required_bibtex_fields case bibtex_type when 'article' # :article => [:author,:title,:journal,:year] sv_has_title sv_is_article_missing_journal sv_year_exists when 'book' #: book => [[:author,:editor],:title,:publisher,:year] sv_contains_a_writer sv_has_title sv_has_a_publisher sv_year_exists when 'booklet' # :booklet => [:title], sv_has_title when 'conference' # :conference => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'inbook' # :inbook => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year], sv_contains_a_writer sv_has_title sv_is_contained_has_chapter_or_pages sv_has_a_publisher sv_year_exists when 'incollection' # :incollection => [:author,:title,:booktitle,:publisher,:year], sv_has_title sv_has_booktitle sv_has_a_publisher sv_year_exists when 'inproceedings' # :inproceedings => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'manual' # :manual => [:title], sv_has_title when 'mastersthesis' # :mastersthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists # :misc => [], (no required fields) when 'phdthesis' # :phdthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists when 'proceedings' # :proceedings => [:title,:year], sv_has_title sv_year_exists when 'techreport' # :techreport => [:author,:title,:institution,:year], sv_has_title sv_has_institution sv_year_exists when 'unpublished' # :unpublished => [:author,:title,:note] sv_has_title sv_has_note end end def sv_missing_roles soft_validations.add(:base, 'Author roles are not selected.') if self..empty? end # rubocop:enable Metrics/MethodLength end |
#booktitle ⇒ nil
@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.
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 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 |
# File 'app/models/source/bibtex.rb', line 306 class Source::Bibtex < Source attr_accessor :authors_to_create # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? } # TW required fields (must have one of these fields filled in) # 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, allow_destroy: true before_validation :create_authors, if: -> { !.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) } soft_validate(:sv_has_some_type_of_year, set: :recommended_fields, has_fix: false) soft_validate(:sv_contains_a_writer, set: :recommended_fields, has_fix: false) soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields, has_fix: false) soft_validate(:sv_duplicate_title, set: :duplicate_title, has_fix: false) soft_validate(:sv_missing_roles, set: :missing_roles, has_fix: false) # @param [BibTeX::Name] bibtex_author # @return [Person, Boolean] new person, or false def self.() return false if .class != BibTeX::Name p = Person.new( first_name: .first, prefix: .prefix, last_name: .last, suffix: .suffix) p.namecase_names p end # @return [BibTeX::Entry] def self.new_from_bibtex_text(text = nil) a = BibTeX::Bibliography.parse(text, filter: :latex).first new_from_bibtex(a) 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 if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title # serial with alternate_value on name .count = 1 assign .first # before validate assign serial if matching & not doesn't have a serial currently assigned. # @todo if there is an ISSN it should look up to see it the serial already exists. def self.new_from_bibtex(bibtex_entry = nil) return false if !bibtex_entry.kind_of?(::BibTeX::Entry) s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s) import_attributes = [] bibtex_entry.fields.each do |key, value| if key == :keywords s.verbatim_keywords = value next end v = value.to_s.strip if s.respond_to?(key.to_sym) && 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 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 # @return [String] A string that represents the authors last_names and year (no suffix) def return 'not yet calculated' if new_record? [, year].compact.join(', ') end # 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 return false if !self.valid? || self.new_record? || (self..blank? && self.editor.blank?) || self.roles.count > 0 bibtex = to_bibtex namecase_bibtex_entry(bibtex) begin Role.transaction do if bibtex. bibtex..each do |a| p = Source::Bibtex.(a) p.save! SourceAuthor.create!(role_object: self, person: p) end end if bibtex.editors bibtex.editors.each do |a| p = Source::Bibtex.(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 # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case # we need to set the serial based on ISSN. # @param [String] value # @return [String] def issn=(value) write_attribute(:issn, value) unless value.blank? tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first unless tw_issn.nil? || tw_issn.identifier != value tw_issn.destroy end self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value) end end # @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) unless self.url.blank? 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 return true if !.blank? return false if new_record? # self exists in the db .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_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 # Month handling allows values from bibtex like 'may' to be handled # @return [Time] def nomenclature_date Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), 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] # entry equivalent to self, this should round-trip with no changes def to_bibtex b = BibTeX::Entry.new(bibtex_type: bibtex_type) ::BIBTEX_FIELDS.each do |f| if (!self.send(f).blank?) && !(f == :bibtex_type) b[f] = self.send(f) end end b.year = year_with_suffix if !year_suffix.blank? b[:keywords] = verbatim_keywords unless verbatim_keywords.blank? b[:note] = concatenated_notes_string if !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. = get_bibtex_names('author') if .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?) b.key = id unless new_record? b end # @return [String, nil] # priority is Person, string # !! Not the cached value !! def a = .load if a.any? get_bibtex_names('author') else .blank? ? nil : end end # Namecase all elements of all names def namecase_bibtex_entry(bibtex_entry) bibtex_entry.parse_names bibtex_entry.names.each do |n| n.first = NameCase(n.first) if n.first n.last = NameCase(n.last) if n.last n.prefix = NameCase(n.prefix) if n.prefix n.suffix = NameCase(n.suffix) if n.suffix end true 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) cp = CiteProc::Processor.new(style: style, format: format) b = bibtex_bibliography namecase_bibtex_entry(b) if normalize_names cp.import(b.to_citeproc) 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 # # There (was) a problem with the zootaxa format and letters! # https://github.com/citation-style-language/styles/pull/2305 def cached_string(format = 'text') return nil unless (format == 'text') || (format == 'html') str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zookeys', format) # the current TaxonWorks default ... make a constant # str = render_with_style('entomological-society-of-america', format) # the current TaxonWorks default ... make a constant # str = render_with_style('florida-entomologist', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zoological-journal-of-the-linnean-society', format) # the current TaxonWorks default ... make a constant # str = render_with_style('systematic-biology', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-annotated-bibliography', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-fullnote-bibliography-16th-edition', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-library-list', format) # the current TaxonWorks default ... make a constant 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 (reload = true) reload ? .reload : .load if !.any? # no normalized people, use string, !! not .any? because of in-memory setting?! if .blank? return nil else b = to_bibtex namecase_bibtex_entry(b) return Utilities::Strings.(b..tokens.collect{ |t| t.last }) end else # use normalized records return Utilities::Strings.(.collect{ |a| a.full_last_name }) end end protected def validate_year_suffix a = 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 # @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 begin Person.transaction do .each do |shs| p = Person.create!(shs) .build(person: p) end end rescue errors.add(:base, 'invalid author parameters') end end # set cached values and copies active record relations into bibtex values # @return [Ignored] def set_cached if errors.empty? attributes_to_update = {} attributes_to_update[:author] = get_bibtex_names('author') if .reload.size > 0 attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0 c = cached_string('html') if bibtex_type == 'book' && !pages.blank? if pages.to_i.to_s == pages c = c + " #{pages} pp." else c = c + " #{pages}" end end n = [] n += [stated_year.to_s] if stated_year && year && stated_year != year n += ['in ' + Language.find(language_id).english_name.to_s] if language_id n += [note.to_s] if note c = c + " [#{n.join(', ')}]" unless n.empty? attributes_to_update.merge!( cached: c, cached_nomenclature_date: nomenclature_date, cached_author_string: (false) ) update_columns(attributes_to_update) end 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..count > 0 || self.editors.count > 0 || !self.serial.nil?) valid = true end if !valid errors.add( :base, 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year' ) end end #endregion hard validations # @return [Ignored] def sv_duplicate_title # only used in validating BibTeX output if Source.where(title: title).where.not(id: id).any? soft_validations.add(:title, 'Another source with this title exists, it may be duplicate') end end # @return [Ignored] def # only used in validating BibTeX output if !() soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.') end end # @return [Ignored] def sv_contains_a_writer # neither author nor editor if !has_writer? soft_validations.add(:base, 'There is neither author, nor editor associated with this source.') end end # @return [Ignored] def sv_has_title if title.blank? unless soft_validations..include?('There is no title associated with this source.') soft_validations.add(:title, 'There is no title associated with this source.') end end end # @return [Ignored] def sv_has_some_type_of_year if !has_some_year? soft_validations.add(:base, 'There is no year nor is there a stated year associated with this source.') end end # @return [Ignored] def sv_year_exists # only used in validating BibTeX output if year.blank? soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.') elsif year < 1700 soft_validations.add(:year, 'This year is prior to the 1700s.') end end # @return [Ignored] def sv_is_article_missing_journal if bibtex_type == 'article' if journal.nil? && serial_id.nil? soft_validations.add(:bibtex_type, 'This article is missing a journal name.') elsif serial_id.nil? soft_validations.add(:serial_id, 'This article is missing a serial.') end end end # @return [Ignored] def sv_has_a_publisher if publisher.blank? soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.') end end # @return [Ignored] def sv_has_booktitle if booktitle.blank? soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.') end end # @return [Ignored] def sv_is_contained_has_chapter_or_pages if chapter.blank? && pages.blank? soft_validations.add(:bibtex_type, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:chapter, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') # soft_validations.add(:pages, 'Valid BibTeX requires either a chapter or pages with sources of type inbook.') end end # @return [Ignored] def sv_has_school if school.blank? soft_validations.add(:school, 'Valid BibTeX requires a school associated with any thesis.') end end # @return [Ignored] def sv_has_institution if institution.blank? soft_validations.add(:institution, 'Valid BibTeX requires an institution with a tech report.') end end # @return [Ignored] def sv_has_note if (note.blank?) && (!notes.any?) soft_validations.add(:note, 'Valid BibTeX requires a note with an unpublished source.') end end # rubocop:disable Metrics/MethodLength # @return [Ignored] def sv_missing_required_bibtex_fields case bibtex_type when 'article' # :article => [:author,:title,:journal,:year] sv_has_title sv_is_article_missing_journal sv_year_exists when 'book' #: book => [[:author,:editor],:title,:publisher,:year] sv_contains_a_writer sv_has_title sv_has_a_publisher sv_year_exists when 'booklet' # :booklet => [:title], sv_has_title when 'conference' # :conference => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'inbook' # :inbook => [[:author,:editor],:title,[:chapter,:pages],:publisher,:year], sv_contains_a_writer sv_has_title sv_is_contained_has_chapter_or_pages sv_has_a_publisher sv_year_exists when 'incollection' # :incollection => [:author,:title,:booktitle,:publisher,:year], sv_has_title sv_has_booktitle sv_has_a_publisher sv_year_exists when 'inproceedings' # :inproceedings => [:author,:title,:booktitle,:year], sv_has_title sv_has_booktitle sv_year_exists when 'manual' # :manual => [:title], sv_has_title when 'mastersthesis' # :mastersthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists # :misc => [], (no required fields) when 'phdthesis' # :phdthesis => [:author,:title,:school,:year], sv_has_title sv_has_school sv_year_exists when 'proceedings' # :proceedings => [:title,:year], sv_has_title sv_year_exists when 'techreport' # :techreport => [:author,:title,:institution,:year], sv_has_title sv_has_institution sv_year_exists when 'unpublished' # :unpublished => [:author,:title,:note] sv_has_title sv_has_note end end def sv_missing_roles soft_validations.add(:base, 'Author roles are not selected.') if self..empty? end # rubocop:enable Metrics/MethodLength end |
#chapter ⇒ String?
BibTeX standard field (required for types: )(optional for types:) A chapter (or section or whatever) number.
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 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 |
# File 'app/models/source/bibtex.rb', line 306 class Source::Bibtex < Source attr_accessor :authors_to_create # @todo :update_authors_editor_if_changed? if: Proc.new { |a| a.password.blank? } # TW required fields (must have one of these fields filled in) # 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, allow_destroy: true before_validation :create_authors, if: -> { !.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) } soft_validate(:sv_has_some_type_of_year, set: :recommended_fields, has_fix: false) soft_validate(:sv_contains_a_writer, set: :recommended_fields, has_fix: false) soft_validate(:sv_missing_required_bibtex_fields, set: :bibtex_fields, has_fix: false) soft_validate(:sv_duplicate_title, set: :duplicate_title, has_fix: false) soft_validate(:sv_missing_roles, set: :missing_roles, has_fix: false) # @param [BibTeX::Name] bibtex_author # @return [Person, Boolean] new person, or false def self.() return false if .class != BibTeX::Name p = Person.new( first_name: .first, prefix: .prefix, last_name: .last, suffix: .suffix) p.namecase_names p end # @return [BibTeX::Entry] def self.new_from_bibtex_text(text = nil) a = BibTeX::Bibliography.parse(text, filter: :latex).first new_from_bibtex(a) 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 if it finds one & only one match for serial assigns the serial ID, and if not it just store in journal title # serial with alternate_value on name .count = 1 assign .first # before validate assign serial if matching & not doesn't have a serial currently assigned. # @todo if there is an ISSN it should look up to see it the serial already exists. def self.new_from_bibtex(bibtex_entry = nil) return false if !bibtex_entry.kind_of?(::BibTeX::Entry) s = Source::Bibtex.new(bibtex_type: bibtex_entry.type.to_s) import_attributes = [] bibtex_entry.fields.each do |key, value| if key == :keywords s.verbatim_keywords = value next end v = value.to_s.strip if s.respond_to?(key.to_sym) && 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 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 # @return [String] A string that represents the authors last_names and year (no suffix) def return 'not yet calculated' if new_record? [, year].compact.join(', ') end # 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 return false if !self.valid? || self.new_record? || (self..blank? && self.editor.blank?) || self.roles.count > 0 bibtex = to_bibtex namecase_bibtex_entry(bibtex) begin Role.transaction do if bibtex. bibtex..each do |a| p = Source::Bibtex.(a) p.save! SourceAuthor.create!(role_object: self, person: p) end end if bibtex.editors bibtex.editors.each do |a| p = Source::Bibtex.(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 # @todo Are ISSN only Serials now? Maybe - the raw bibtex source may come in with an ISSN in which case # we need to set the serial based on ISSN. # @param [String] value # @return [String] def issn=(value) write_attribute(:issn, value) unless value.blank? tw_issn = self.identifiers.where(type: 'Identifier::Global::Issn').first unless tw_issn.nil? || tw_issn.identifier != value tw_issn.destroy end self.identifiers.build(type: 'Identifier::Global::Issn', identifier: value) end end # @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) unless self.url.blank? 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 return true if !.blank? return false if new_record? # self exists in the db .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_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 # Month handling allows values from bibtex like 'may' to be handled # @return [Time] def nomenclature_date Utilities::Dates.nomenclature_date(day, Utilities::Dates.month_index(month), 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] # entry equivalent to self, this should round-trip with no changes def to_bibtex b = BibTeX::Entry.new(bibtex_type: bibtex_type) ::BIBTEX_FIELDS.each do |f| if (!self.send(f).blank?) && !(f == :bibtex_type) b[f] = self.send(f) end end b.year = year_with_suffix if !year_suffix.blank? b[:keywords] = verbatim_keywords unless verbatim_keywords.blank? b[:note] = concatenated_notes_string if !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. = get_bibtex_names('author') if .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?) b.key = id unless new_record? b end # @return [String, nil] # priority is Person, string # !! Not the cached value !! def a = .load if a.any? get_bibtex_names('author') else .blank? ? nil : end end # Namecase all elements of all names def namecase_bibtex_entry(bibtex_entry) bibtex_entry.parse_names bibtex_entry.names.each do |n| n.first = NameCase(n.first) if n.first n.last = NameCase(n.last) if n.last n.prefix = NameCase(n.prefix) if n.prefix n.suffix = NameCase(n.suffix) if n.suffix end true 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) cp = CiteProc::Processor.new(style: style, format: format) b = bibtex_bibliography namecase_bibtex_entry(b) if normalize_names cp.import(b.to_citeproc) 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 # # There (was) a problem with the zootaxa format and letters! # https://github.com/citation-style-language/styles/pull/2305 def cached_string(format = 'text') return nil unless (format == 'text') || (format == 'html') str = render_with_style('zootaxa', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zookeys', format) # the current TaxonWorks default ... make a constant # str = render_with_style('entomological-society-of-america', format) # the current TaxonWorks default ... make a constant # str = render_with_style('florida-entomologist', format) # the current TaxonWorks default ... make a constant # str = render_with_style('zoological-journal-of-the-linnean-society', format) # the current TaxonWorks default ... make a constant # str = render_with_style('systematic-biology', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-annotated-bibliography', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-fullnote-bibliography-16th-edition', format) # the current TaxonWorks default ... make a constant # str = render_with_style('chicago-library-list', format) # the current TaxonWorks default ... make a constant 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 (reload = true) reload ? .reload : .load if !.any? # no normalized people, use string, !! not .any? because of in-memory setting?! if .blank? return nil else b = to_bibtex namecase_bibtex_entry(b) return Utilities::Strings.(b..tokens.collect{ |t| t.last }) end else # use normalized records return Utilities::Strings.(.collect{ |a| a.full_last_name }) end end protected def validate_year_suffix a = 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 # @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 begin Person.transaction do .each do |shs| p = Person.create!(shs) .build(person: p) end end rescue errors.add(:base, 'invalid author parameters') end end # set cached values and copies active record relations into bibtex values # @return [Ignored] def set_cached if errors.empty? attributes_to_update = {} attributes_to_update[:author] = get_bibtex_names('author') if .reload.size > 0 attributes_to_update[:editor] = get_bibtex_names('editor') if editors.reload.size > 0 c = cached_string('html') if bibtex_type == 'book' && !pages.blank? if pages.to_i.to_s == pages c = c + " #{pages} pp." else c = c + " #{pages}" end end n = [] n += [stated_year.to_s] if stated_year && year && stated_year != year n += ['in ' + Language.find(language_id).english_name.to_s] if language_id n += [note.to_s] if note c = c + " [#{n.join(', ')}]" unless n.empty? attributes_to_update.merge!( cached: c, cached_nomenclature_date: nomenclature_date, cached_author_string: (false) ) update_columns(attributes_to_update) end 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..count > 0 || self.editors.count > 0 || !self.serial.nil?) valid = true end if !valid errors.add( :base, 'Missing core data. A TaxonWorks source must have one of the following: author, editor, booktitle, title, url, journal, year, or stated year' ) end end #endregion hard validations # @return [Ignored] def sv_duplicate_title # only used in validating BibTeX output if Source.where(title: title).where.not(id: id).any? soft_validations.add(:title, 'Another source with this title exists, it may be duplicate') end end # @return [Ignored] def # only used in validating BibTeX output if !() soft_validations.add(:author, 'Valid BibTeX requires an author with this type of source.') end end # @return [Ignored] def sv_contains_a_writer # neither author nor editor if !has_writer? soft_validations.add(:base, 'There is neither author, nor editor associated with this source.') end end # @return [Ignored] def sv_has_title if title.blank? unless soft_validations..include?('There is no title associated with this source.') soft_validations.add(:title, 'There is no title associated with this source.') end end end # @return [Ignored] def sv_has_some_type_of_year if !has_some_year? soft_validations.add(:base, 'There is no year nor is there a stated year associated with this source.') end end # @return [Ignored] def sv_year_exists # only used in validating BibTeX output if year.blank? soft_validations.add(:year, 'Valid BibTeX requires a year with this type of source.') elsif year < 1700 soft_validations.add(:year, 'This year is prior to the 1700s.') end end # @return [Ignored] def sv_is_article_missing_journal if bibtex_type == 'article' if journal.nil? && serial_id.nil? soft_validations.add(:bibtex_type, 'This article is missing a journal name.') elsif serial_id.nil? soft_validations.add(:serial_id, 'This article is missing a serial.') end end end # @return [Ignored] def sv_has_a_publisher if publisher.blank? soft_validations.add(:publisher, 'Valid BibTeX requires a publisher to be associated with this source.') end end # @return [Ignored] def sv_has_booktitle if booktitle.blank? soft_validations.add(:booktitle, 'Valid BibTeX requires a book title to be associated with this source.') end end # @return [Ignored] def sv_is_contained_has_chapter_or_pages if chapter.blank? && pages.blank? soft_validations.add(:bibtex_type< |