pennmarc 1.4.11 → 1.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8afdbe2c48c9cf22c9d3242757f070996e6181f3d1ac570ece3fdbf04e8c2f5d
4
- data.tar.gz: 3ddc3f81b9a8152e53aa19430cc45180e30fc89a11f3cda5ebae41cb9c0a275c
3
+ metadata.gz: ed8428b56d9ed1e81912885aef968ba0f104f647491afd2498e4e3893748b9c4
4
+ data.tar.gz: fead988cff9b7cb56201510a7eba63152dd16f8e691ece753d6b306158afc53e
5
5
  SHA512:
6
- metadata.gz: 7fd53869a4b48a1d8446a969283c8e60f467952dfc17a7c251ca9ffae767bc4f9a9e04f7dba24a3c6a97aa18856a23b79bb181dadf8766832c6d85fbfa4a3928
7
- data.tar.gz: 35a12593cd5352b3ffa9de11afa86ef22b7b00d2ae0f48f1e78f961f71a68d50bd694e13baaa41bf85ca25e9877097360405e57619a976fa0e688ef6845856e9
6
+ metadata.gz: 21e44d07cd4bde553e717f84a944dd297055f94518a27a14f9d9fb3d64c3f1f9172746af0ba0d9e1549fb69198a7ef5f5c23b5323189bba500d04cc5ace40a32
7
+ data.tar.gz: caaf7925c9d91833c92ba25524dc70f4bad6f4b31c7640ff35823f20477d5d9c7b7449770bd8ffdcd66fde1a39460ddbad58666d71eeeb22ca3b225f5da72d9a
@@ -0,0 +1,17 @@
1
+ ---
2
+ collections:
3
+ - name: "https://gitlab.library.upenn.edu/api/v4/projects/devops%2Fansible%2Fcollection\
4
+ /packages/generic/collections/stack-v2.2.0/library-stack-2.2.0.tar.gz"
5
+ type: url
6
+ - name: "https://gitlab.library.upenn.edu/api/v4/projects/devops%2Fansible%2Fcollection\
7
+ /packages/generic/collections/proxmox-v1.6.0/library-proxmox-1.6.0.tar.gz"
8
+ type: url
9
+ - name: "https://gitlab.library.upenn.edu/api/v4/projects/devops%2Fansible%2Fcollection\
10
+ /packages/generic/collections/services-v1.11.0/library-services-1.11.0.tar.gz"
11
+ type: url
12
+ - name: "https://gitlab.library.upenn.edu/api/v4/projects/devops%2Fansible%2Fcollection\
13
+ /packages/generic/collections/infra-v4.7.0/library-infra-4.7.0.tar.gz"
14
+ type: url
15
+ - name: "https://gitlab.library.upenn.edu/api/v4/projects/devops%2Fansible%2Fcollection\
16
+ /packages/generic/collections/ci-v0.7.0/library-ci-0.7.0.tar.gz"
17
+ type: url
data/.gitlab-ci.yml CHANGED
@@ -1,34 +1,102 @@
1
1
  ---
2
+ variables:
3
+ FF_SCRIPT_SECTIONS: "true"
4
+
2
5
  include:
3
- - template: Jobs/Secret-Detection.gitlab-ci.yml
6
+ - component: gitlab.library.upenn.edu/devops/gitlab/components/sast/secret_scan@~latest
7
+ - component: gitlab.library.upenn.edu/devops/gitlab/components/general/install_ansible@~latest
8
+ - component: gitlab.library.upenn.edu/devops/gitlab/components/general/ci_requirements@~latest
9
+ - component: gitlab.library.upenn.edu/devops/gitlab/components/general/install_hashicorp_vault@~latest
10
+ - component: gitlab.library.upenn.edu/devops/gitlab/components/general/vault_jwt_auth@~latest
11
+ inputs:
12
+ ci_requirements_file: ".gitlab/ci-requirements.yml"
13
+ vault_addr: ${VAULT_ADDR}
14
+ vault_auth_path: ${VAULT_AUTH_PATH}
15
+ vault_auth_role: ${VAULT_AUTH_ROLE}
16
+ - component: gitlab.library.upenn.edu/devops/gitlab/components/tests/rubocop@~latest
17
+ inputs:
18
+ app_dir: "."
19
+ job_image_name: "ruby"
20
+ job_image_tag: "3.2.2"
21
+ needs:
22
+ - secret_detection
4
23
 
5
24
  stages:
6
25
  - secret_check
7
- - main_pipeline
26
+ - test
27
+ - deploy
28
+
29
+ rubocop:
30
+ before_script:
31
+ - bundle install -j $(nproc)
32
+ rules:
33
+ - exists:
34
+ - .rubocop.yml
8
35
 
9
- secret_detection:
10
- stage: secret_check
11
- allow_failure: false
36
+ rspec:
37
+ stage: test
38
+ image: ruby:3.2.2
39
+ needs:
40
+ - secret_detection
41
+ variables:
42
+ ANSIBLE_COLLECTIONS_CACHE_DIR: .cache/ansible/collections
43
+ GALAXY_REQUIREMENTS_FILE: ".gitlab/ci-requirements.yml"
44
+ cache:
45
+ - key:
46
+ files:
47
+ - .gitlab/ci-requirements.yml
48
+ prefix: ansible-collections
49
+ paths:
50
+ - $ANSIBLE_COLLECTIONS_CACHE_DIR
51
+ before_script:
52
+ - !reference [.install_ansible, before_script]
53
+ - !reference [.ci_requirements, before_script]
54
+ - bundle install -j $(nproc)
55
+ script:
56
+ - tail -F --pid=$$ rspec.log 2>/dev/null &
57
+ - ansible-playbook library.ci.rails_test -e rails_test_app_dir="."
12
58
  artifacts:
13
59
  paths:
14
- - .pipeline.yml
15
- expire_in: 1 day
60
+ - rspec.xml
61
+ expire_in: 1 week
62
+ reports:
63
+ junit: rspec.xml
64
+ rules:
65
+ - exists:
66
+ - spec/**.rb
67
+ - spec/**/**.rb
16
68
  tags:
17
69
  - build
18
- rules:
19
- - when: always
20
70
 
21
- main:
22
- stage: main_pipeline
23
- needs:
24
- - secret_detection
25
- allow_failure: false
26
- trigger:
27
- include:
28
- - artifact: .pipeline.yml
29
- job: secret_detection
30
- strategy: depend
71
+ # This step was inspired from Gitlab's example of gem publication:
72
+ # https://gitlab.com/gitlab-org/quality/pipeline-common/-/blob/master/ci/gem-release.yml
73
+ gem_publication:
74
+ stage: deploy
75
+ image: ruby:3.2.2
76
+ extends:
77
+ - .vault_jwt_auth
78
+ variables:
79
+ GEMSPEC_FILE: "${CI_PROJECT_NAME}.gemspec"
80
+ before_script:
81
+ - !reference [.install_ansible, before_script]
82
+ - !reference [.install_hashicorp_vault, before_script]
83
+ - !reference [.vault_jwt_auth, before_script]
84
+ script:
85
+ - export GEM_HOST_API_KEY="$(vault kv get -field=rubygems_api_key ${VAULT_KV_ENDPOINT}${ENVIRONMENT})"
86
+ - |
87
+ gem -v
88
+ rm -f ./*.gem
89
+ [ -f "${GEMSPEC_FILE}" ] || (echo "No ${GEMSPEC_FILE} file found!" && exit 1)
90
+ - '[ -n "${GEM_HOST_API_KEY}" ] || (echo "GEM_HOST_API_KEY is not set!" && exit 1)'
91
+ - GEM_FILE=$(echo "${CI_PROJECT_NAME}-${CI_COMMIT_TAG:1}.gem")
92
+ - |
93
+ gem build "${GEMSPEC_FILE}"
94
+ [ -f "${GEM_FILE}" ] || (echo "No ${GEM_FILE} file found!" && exit 1)
95
+ - '[ "${DISABLE_GEM_PUSH}" == "true" ] || gem push "${GEM_FILE}"'
31
96
  rules:
32
- - if: "$CI_MERGE_REQUEST_IID"
33
- - if: "$CI_COMMIT_TAG"
34
- - if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
97
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+(\.[a-zA-Z0-9]+)?$/
98
+ artifacts:
99
+ paths:
100
+ - "*.gem"
101
+ tags:
102
+ - deploy
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 10000`
3
- # on 2025-10-22 21:06:47 UTC using RuboCop version 1.79.2.
3
+ # on 2026-09-28 22:29:36 UTC using RuboCop version 1.84.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -33,7 +33,7 @@ Metrics/ClassLength:
33
33
  - 'lib/pennmarc/helpers/subject.rb'
34
34
  - 'lib/pennmarc/helpers/title.rb'
35
35
 
36
- # Offense count: 22
36
+ # Offense count: 23
37
37
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
38
38
  Metrics/CyclomaticComplexity:
39
39
  Exclude:
@@ -70,7 +70,7 @@ Metrics/ModuleLength:
70
70
  Exclude:
71
71
  - 'lib/pennmarc/util.rb'
72
72
 
73
- # Offense count: 17
73
+ # Offense count: 18
74
74
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
75
75
  Metrics/PerceivedComplexity:
76
76
  Exclude:
@@ -82,6 +82,7 @@ Metrics/PerceivedComplexity:
82
82
  - 'lib/pennmarc/helpers/note.rb'
83
83
  - 'lib/pennmarc/helpers/production.rb'
84
84
  - 'lib/pennmarc/helpers/series.rb'
85
+ - 'lib/pennmarc/helpers/subject.rb'
85
86
  - 'lib/pennmarc/helpers/title.rb'
86
87
  - 'lib/pennmarc/util.rb'
87
88
 
@@ -109,18 +110,16 @@ RSpec/ExampleLength:
109
110
  Exclude:
110
111
  - 'spec/lib/pennmarc/marc_util_spec.rb'
111
112
 
113
+ # Offense count: 1
114
+ # Configuration parameters: AllowSubject, Max.
115
+ RSpec/MultipleMemoizedHelpers:
116
+ Exclude:
117
+ - 'spec/lib/pennmarc/helpers/subject_spec.rb'
118
+
112
119
  # Offense count: 2
113
- # Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata.
114
- # Include: **/*_spec.rb
120
+ # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
121
+ # SupportedInflectors: default, active_support
115
122
  RSpec/SpecFilePathFormat:
116
123
  Exclude:
117
124
  - 'spec/lib/pennmarc/parser_spec.rb'
118
125
  - 'spec/lib/pennmarc/services/title_suggestion_weight_service_spec.rb'
119
-
120
- # Offense count: 1
121
- # This cop supports unsafe autocorrection (--autocorrect-all).
122
- # Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns.
123
- # SupportedStyles: predicate, comparison
124
- Style/NumericPredicate:
125
- Exclude:
126
- - 'lib/pennmarc/services/title_suggestion_weight_service.rb'
data/README.md CHANGED
@@ -93,7 +93,19 @@ rspec
93
93
  2. Create a new tag that matches the version set in step 1 (ex: v1.1.0).
94
94
  3. Add a release title that is the same as the tag name.
95
95
  4. Submit by clicking "Create Release".
96
- 4. Once the release is created a pipeline will run to publish the gem to RubyGems.
96
+ 4. Once the release is created a pipeline will run to publish the gem to RubyGems.
97
+
98
+ ### Publishing Manually
99
+
100
+ You can also publish the gem using `gem` commands. After updating the `VERSION` value on `main`, you can run the
101
+ following commands:
102
+
103
+ ```bash
104
+ gem build pennmarc.gemspec
105
+ gem push pennmarc-<VERSION>.gem
106
+ ```
107
+
108
+ You may need an OTP code for use in the push to bundler.
97
109
 
98
110
  ### Versioning Guidelines
99
111
 
@@ -5,28 +5,36 @@ module PennMARC
5
5
  class HeadingControl
6
6
  # These codes are expected to be found in sf2 of a subject/genre field when the indicator2 value is 7, indicating
7
7
  # "source specified". There are some sources whose headings we don't want to display.
8
- ALLOWED_SOURCE_CODES = %w[aat cct fast ftamc gmgpc gsafd homoit jlabsh lcgft lcsh lcstt lctgm
8
+ ALLOWED_SOURCE_CODES = %w[aat cct fast ftamc gmgpc gsafd homoit homoit/eng jlabsh lcgft lcsh lcstt lctgm local/upenn
9
9
  local/osu mesh ndlsh nli nlksh rbbin rbgenr rbmscv rbpap rbpri rbprov rbpub rbtyp].freeze
10
10
 
11
11
  REMOVE_TERM_REGEX = /#{Mappers.headings_to_remove&.join('|')}/i
12
12
  REPLACE_TERM_REGEX = /(#{Mappers.heading_overrides.keys.join('|')})/i
13
13
 
14
+ # Don't perform any term overriding on these tags
15
+ NO_OVERRIDE_TAGS = %w[600 610 611 630].freeze
16
+
14
17
  class << self
15
18
  # Replace or remove any terms in provided values pursuant to the configuration in remove and override mappers.
16
19
  # Used to remove or replace offensive or otherwise undesirable subject headings.
17
- # @param values [Array]
18
- # @return [Array] values with terms removed/replaced
19
- def term_override(values)
20
- values.filter_map do |value|
21
- # Remove values if they contain a remove term
22
- next nil if value.match?(REMOVE_TERM_REGEX)
20
+ # @param [String] value
21
+ # @return [String, nil]
22
+ def term_override(value)
23
+ # Remove values if they contain a remove term
24
+ return if value.match?(REMOVE_TERM_REGEX)
25
+
26
+ # return early if there are no terms to replace
27
+ return value if value.match(REPLACE_TERM_REGEX).nil?
23
28
 
24
- # return early if theres no terms to replace
25
- next value if value.match(REPLACE_TERM_REGEX).nil?
29
+ # lookup and perform replacement
30
+ value.sub(::Regexp.last_match.to_s, Mappers.heading_overrides[::Regexp.last_match.to_s.downcase])
31
+ end
26
32
 
27
- # lookup and perform replacement
28
- value.sub(::Regexp.last_match.to_s, Mappers.heading_overrides[::Regexp.last_match.to_s.downcase])
29
- end
33
+ # Is the given field overrideable?
34
+ # @param field [MARC::Field]
35
+ # @return [Boolean]
36
+ def overrideable_field?(field)
37
+ !field.tag.in?(NO_OVERRIDE_TAGS)
30
38
  end
31
39
  end
32
40
  end
@@ -9,10 +9,10 @@ module PennMARC
9
9
  TAGS = %w[100 110].freeze
10
10
 
11
11
  # For creator fields intended for display, these subfields are ignored
12
- DISPLAY_EXCLUDED_SUBFIELDS = %w[a 0 1 4 5 6 8 t].freeze
12
+ DISPLAY_EXCLUDED_SUBFIELDS = %w[a 0 1 4 5 6 7 8 t].freeze
13
13
 
14
14
  # For creator fields intended for searching, these subfields are ignored
15
- SEARCH_EXCLUDED_SUBFIELDS = %w[a 1 4 5 6 8 t].freeze
15
+ SEARCH_EXCLUDED_SUBFIELDS = %w[a 1 4 5 6 7 8 t].freeze
16
16
 
17
17
  # Aux tags for Author/Creator information, for use in search_aux method
18
18
  AUX_TAGS = %w[100 110 111 400 410 411 700 710 711 800 810 811].freeze
@@ -35,7 +35,7 @@ module PennMARC
35
35
  # Author/Creator search field, from tags
36
36
  # {https://www.oclc.org/bibformats/en/1xx/100.html 100 Main Entry--Personal Name} and
37
37
  # {https://www.oclc.org/bibformats/en/1xx/110.html 110 Main Entry--Corporate Name}. Includes subfield values
38
- # except for those in the {DISPLAY_EXCLUDED_SUBFIELDS} constant. Maps any relator codes found in ǂ4. To better
38
+ # except for those in the {SEARCH_EXCLUDED_SUBFIELDS} constant. Maps any relator codes found in ǂ4. To better
39
39
  # handle name searches, returns names as both "First Last" and "Last, First" if a comma is found in ǂa. Also
40
40
  # indexes any linked values in the 880.
41
41
  # @param record [MARC::Record]
@@ -463,7 +463,7 @@ module PennMARC
463
463
  # @param relator_map [Hash]
464
464
  # @return [String]
465
465
  def parse_show_value(field, relator_map: Mappers.relator)
466
- creator = join_subfields(field, &subfield_not_in?(%w[0 1 4 6 8 e w]))
466
+ creator = join_subfields(field, &subfield_not_in?(%w[0 1 4 6 7 8 e w]))
467
467
  append_relator(field: field, joined_subfields: creator, relator_term_sf: 'e', relator_map: relator_map)
468
468
  end
469
469
 
@@ -475,7 +475,7 @@ module PennMARC
475
475
  # @return [String]
476
476
  def parse_conference_detail_show_value(field, relator_map: Mappers.relator)
477
477
  conf = if subfield_undefined? field, 'i'
478
- join_subfields field, &subfield_not_in?(%w[0 4 5 6 8 e j w])
478
+ join_subfields field, &subfield_not_in?(%w[0 4 5 6 7 8 e j w])
479
479
  else
480
480
  ''
481
481
  end
@@ -493,11 +493,11 @@ module PennMARC
493
493
  end
494
494
 
495
495
  # Does the given field describe a contributor representing university provenance. A contributor tag with a '$5'
496
- # value starting with the university identifier represents university provenance.
496
+ # value equal to the university identifier represents university provenance.
497
497
  # @param field [MARC::Field]
498
498
  # @return [Boolean]
499
499
  def university_provenance?(field)
500
- field.tag.in?(CONTRIBUTOR_TAGS) && subfield_value?(field, '5', /^#{UNIVERSITY_IDENTIFIER}/)
500
+ field.tag.in?(CONTRIBUTOR_TAGS) && subfield_values(field, '5').include?(UNIVERSITY_IDENTIFIER)
501
501
  end
502
502
  end
503
503
  end
@@ -16,7 +16,7 @@ module PennMARC
16
16
  # @return [Array<String>] array of editions and their alternates
17
17
  def show(record, with_alternate: true)
18
18
  editions = record.fields('250').map do |field|
19
- join_subfields(field, &subfield_not_in?(%w[6 8]))
19
+ join_subfields(field, &subfield_not_in?(%w[6 7 8]))
20
20
  end
21
21
  editions += linked_alternate_not_6_or_8(record, '250') if with_alternate
22
22
 
@@ -30,7 +30,7 @@ module PennMARC
30
30
  edition = record.fields('250').first
31
31
  return if edition.blank?
32
32
 
33
- join_subfields(edition, &subfield_not_in?(%w[6 8]))
33
+ join_subfields(edition, &subfield_not_in?(%w[6 7 8]))
34
34
  end
35
35
 
36
36
  # Entry for another available edition of the target item (horizontal relationship). When a note is generated
@@ -38,7 +38,7 @@ module PennMARC
38
38
  def show(record)
39
39
  results = record.fields('300').map { |f| join_subfields(f, &subfield_not_in?(%w[3 6 8])) }
40
40
  results += record.fields(%w[254 255 310 342 352 362]).map do |f|
41
- join_subfields(f, &subfield_not_in?(%w[6 8]))
41
+ join_subfields(f, &subfield_not_in?(%w[6 7 8]))
42
42
  end
43
43
  results += record.fields('340').map { |f| join_subfields(f, &subfield_not_in?(%w[0 2 6 8])) }
44
44
  results += record.fields('880').map do |f|
@@ -16,7 +16,7 @@ module PennMARC
16
16
  # @return [Array<String>] language values and notes
17
17
  def show(record)
18
18
  values = record.fields('546').map do |field|
19
- join_subfields field, &subfield_not_in?(%w[6 8])
19
+ join_subfields field, &subfield_not_in?(%w[6 7 8])
20
20
  end
21
21
  language_values = values + linked_alternate(record, '546', &subfield_not_in?(%w[6 8]))
22
22
  language_values.uniq
@@ -7,25 +7,25 @@ module PennMARC
7
7
  # tightly to be preserved. As a result fo this, display methods and facet methods below are ported from their state
8
8
  # prior to Michael's 2/2021 subject parsing changes.
9
9
  class Subject < Helper
10
- class << self
11
- # Tags that serve as sources for Subject search values
12
- # @todo why are 541 and 561 included here? these fields include info about source of acquisition
13
- SEARCH_TAGS = %w[541 561 600 610 611 630 650 651 653].freeze
10
+ # Tags that serve as sources for Subject search values
11
+ # @todo why are 541 and 561 included here? these fields include info about source of acquisition
12
+ SEARCH_TAGS = %w[541 561 600 610 611 630 650 651 653].freeze
14
13
 
15
- # Valid indicator 2 values indicating the source thesaurus for subject terms. These are:
16
- # - 0: LCSH
17
- # - 1: LC Children's
18
- # - 2: MeSH
19
- # - 4: Source not specified (local?)
20
- # - 7: Source specified in ǂ2
21
- VALID_SOURCE_INDICATORS = %w[0 1 2 4 7].freeze
14
+ # Valid indicator 2 values indicating the source thesaurus for subject terms. These are:
15
+ # - 0: LCSH
16
+ # - 1: LC Children's
17
+ # - 2: MeSH
18
+ # - 4: Source not specified (local?)
19
+ # - 7: Source specified in ǂ2
20
+ VALID_SOURCE_INDICATORS = %w[0 1 2 4 7].freeze
22
21
 
23
- # Tags that serve as sources for Subject facet values
24
- DISPLAY_TAGS = %w[600 610 611 630 650 651].freeze
22
+ # Tags that serve as sources for Subject facet values
23
+ DISPLAY_TAGS = %w[600 610 611 630 650 651].freeze
25
24
 
26
- # Local subject heading tags
27
- LOCAL_TAGS = %w[690 691 697].freeze
25
+ # Local subject heading tags
26
+ LOCAL_TAGS = %w[690 691 697].freeze
28
27
 
28
+ class << self
29
29
  # All Subjects for searching. This includes most subfield content from any field contained in {SEARCH_TAGS} or
30
30
  # 69X, including any linked 880 fields. Fields must have an indicator2 value in {VALID_SOURCE_INDICATORS}.
31
31
  # @todo this includes subfields that may not be desired like 1 (uri) and 2 (source code) but this might be OK for
@@ -63,11 +63,11 @@ module PennMARC
63
63
  #
64
64
  # @note this is ported mostly form MG's new-style Subject parsing
65
65
  # @param record [MARC::Record]
66
- # @param override [Boolean] remove undesirable terms or not
66
+ # @param override [Boolean] whether to remove undesirable terms in eligible fields or not
67
67
  # @return [Array<String>] array of all subject values for faceting
68
68
  def facet(record, override: true)
69
- values = subject_fields(record, type: :facet).filter_map { |field|
70
- term_hash = build_subject_hash(field)
69
+ subject_fields(record, type: :facet).filter_map { |field|
70
+ term_hash = build_subject_hash(field, override)
71
71
  next if term_hash.blank? || term_hash[:count]&.zero?
72
72
 
73
73
  heading = format_term type: :facet, term: term_hash
@@ -75,8 +75,6 @@ module PennMARC
75
75
 
76
76
  [heading, main_term].compact_blank
77
77
  }.flatten.uniq
78
-
79
- override ? HeadingControl.term_override(values) : values
80
78
  end
81
79
 
82
80
  # All Subjects for display. This includes all {DISPLAY_TAGS} and {LOCAL_TAGS}. For tags that specify a source,
@@ -87,8 +85,8 @@ module PennMARC
87
85
  # @param override [Boolean] to remove undesirable terms or not
88
86
  # @return [Array] array of all subject values for display
89
87
  def show(record, override: true)
90
- values = subject_fields(record, type: :all).filter_map { |field|
91
- term_hash = build_subject_hash(field)
88
+ subject_fields(record, type: :all).filter_map { |field|
89
+ term_hash = build_subject_hash(field, override)
92
90
  next if term_hash.blank? || term_hash[:count]&.zero?
93
91
 
94
92
  heading = format_term type: :display, term: term_hash
@@ -96,7 +94,6 @@ module PennMARC
96
94
 
97
95
  [heading, main_term].compact_blank
98
96
  }.flatten.uniq
99
- override ? HeadingControl.term_override(values) : values
100
97
  end
101
98
 
102
99
  # Get Subjects from "Children" ontology
@@ -105,14 +102,13 @@ module PennMARC
105
102
  # @param override [Boolean] remove undesirable terms or not
106
103
  # @return [Array] array of children's subject values for display
107
104
  def childrens_show(record, override: true)
108
- values = subject_fields(record, type: :display, options: { tags: DISPLAY_TAGS, indicator2: '1' })
109
- .filter_map { |field|
110
- term_hash = build_subject_hash(field)
111
- next if term_hash.blank? || term_hash[:count]&.zero?
105
+ subject_fields(record, type: :display, options: { tags: DISPLAY_TAGS, indicator2: '1' })
106
+ .filter_map { |field|
107
+ term_hash = build_subject_hash(field, override)
108
+ next if term_hash.blank? || term_hash[:count]&.zero?
112
109
 
113
- format_term type: :display, term: term_hash
110
+ format_term type: :display, term: term_hash
114
111
  }.uniq
115
- override ? HeadingControl.term_override(values) : values
116
112
  end
117
113
 
118
114
  # Get Subjects from "MeSH" ontology
@@ -121,14 +117,13 @@ module PennMARC
121
117
  # @param override [Boolean] remove undesirable terms or not
122
118
  # @return [Array] array of MeSH subject values for display
123
119
  def medical_show(record, override: true)
124
- values = subject_fields(record, type: :display, options: { tags: DISPLAY_TAGS, indicator2: '2' })
125
- .filter_map { |field|
126
- term_hash = build_subject_hash(field)
127
- next if term_hash.blank? || term_hash[:count]&.zero?
120
+ subject_fields(record, type: :display, options: { tags: DISPLAY_TAGS, indicator2: '2' })
121
+ .filter_map { |field|
122
+ term_hash = build_subject_hash(field, override)
123
+ next if term_hash.blank? || term_hash[:count]&.zero?
128
124
 
129
- format_term type: :display, term: term_hash
125
+ format_term type: :display, term: term_hash
130
126
  }.uniq
131
- override ? HeadingControl.term_override(values) : values
132
127
  end
133
128
 
134
129
  # Get Subject values from {DISPLAY_TAGS} where indicator2 is 4 and {LOCAL_TAGS}. Do not include any values where
@@ -140,15 +135,14 @@ module PennMARC
140
135
  def local_show(record, override: true)
141
136
  local_fields = subject_fields(record, type: :display, options: { tags: DISPLAY_TAGS, indicator2: '4' }) +
142
137
  subject_fields(record, type: :local)
143
- values = local_fields.filter_map { |field|
138
+ local_fields.filter_map { |field|
144
139
  next if subfield_value?(field, '2', /penncoi/)
145
140
 
146
- term_hash = build_subject_hash(field)
141
+ term_hash = build_subject_hash(field, override)
147
142
  next if term_hash.blank? || term_hash[:count]&.zero?
148
143
 
149
144
  format_term type: :display, term: term_hash
150
145
  }.uniq
151
- override ? HeadingControl.term_override(values) : values
152
146
  end
153
147
 
154
148
  private
@@ -177,25 +171,26 @@ module PennMARC
177
171
  end
178
172
  end
179
173
 
180
- # Format a term hash as a string for display
174
+ # Format a term hash as a string for display, performing overrides if needed
181
175
  #
182
- # @todo support search field formatting?
183
176
  # @param type [Symbol]
184
177
  # @param term [Hash] components and information as a hash
185
- # @return [String]
178
+ # @return [String, nil]
186
179
  def format_term(type:, term:)
187
180
  return unless type.in? %i[facet display]
188
181
 
189
182
  # attempt to handle poorly coded record
190
183
  normalize_single_subfield(term[:parts].first) if term[:count] == 1 && term[:parts].first.present?
191
184
 
192
- case type
193
- when :facet
194
- trim_trailing(:period, term[:parts].join('--').strip)
195
- when :display
196
- display_value = "#{term[:parts].join('--')} #{term[:append].join(' ')}".strip
197
- display_value.ends_with?('.') ? display_value : "#{display_value}."
198
- end
185
+ value = case type
186
+ when :facet
187
+ trim_trailing(:period, term[:parts].join('--').strip)
188
+ when :display
189
+ display_value = "#{term[:parts].join('--')} #{term[:append].join(' ')}".strip
190
+ display_value.ends_with?('.') ? display_value : "#{display_value}."
191
+ end
192
+
193
+ term[:override] ? HeadingControl.term_override(value) : value
199
194
  end
200
195
 
201
196
  # Is a field intended for display in a general subject field? To be included, the field tag is in either
@@ -251,13 +246,14 @@ module PennMARC
251
246
  # heading. - MG
252
247
  # @todo do i need all this?
253
248
  # @param field [MARC::DataField]
249
+ # @param override [Boolean]
254
250
  # @return [Hash{Symbol => Integer, Array, Boolean, String}, Nil]
255
- def build_subject_hash(field)
251
+ def build_subject_hash(field, override)
256
252
  term_info = { count: 0, parts: [], append: [], uri: nil,
257
253
  local: field.indicator2 == '4' || field.tag.starts_with?('69'), # local subject heading
258
254
  vernacular: field.tag == '880',
259
- subfield_a: nil,
260
- lcsh: lcsh?(field) }
255
+ override: override && HeadingControl.overrideable_field?(field),
256
+ subfield_a: nil, lcsh: lcsh?(field) }
261
257
  field.each do |subfield|
262
258
  case subfield.code
263
259
  when '0', '6', '8', '5', '7'
@@ -68,7 +68,7 @@ module PennMARC
68
68
  record.fields(%w[245 880]).filter_map { |field|
69
69
  next if field.tag == '880' && no_subfield_value_matches?(field, '6', /^245/)
70
70
 
71
- join_subfields(field, &subfield_not_in?(%w[c 6 8 h]))
71
+ join_subfields(field, &subfield_not_in?(%w[c 6 7 8 h]))
72
72
  }.uniq
73
73
  end
74
74
 
@@ -77,7 +77,7 @@ module PennMARC
77
77
  # @param record [MARC::Record]
78
78
  # @return [Array<String>] array of auxiliary title values for search
79
79
  def search_aux(record)
80
- values = search_aux_values(record: record, title_type: :main, &subfield_not_in?(%w[c 6 8])) +
80
+ values = search_aux_values(record: record, title_type: :main, &subfield_not_in?(%w[c 6 7 8])) +
81
81
  search_aux_values(record: record, title_type: :related, &subfield_in?(%w[s t])) +
82
82
  search_aux_values(record: record, title_type: :entity, &subfield_in?(%w[t])) +
83
83
  search_aux_values(record: record, title_type: :note, &subfield_in?(%w[t]))
@@ -95,7 +95,7 @@ module PennMARC
95
95
  record.fields(%w[245 880]).filter_map { |field|
96
96
  next if field.tag == '880' && no_subfield_value_matches?(field, '6', /^245/)
97
97
 
98
- join_subfields(field, &subfield_not_in?(%w[c 6 8 h]))
98
+ join_subfields(field, &subfield_not_in?(%w[c 6 7 8 h]))
99
99
  }.uniq
100
100
  end
101
101
 
@@ -105,7 +105,7 @@ module PennMARC
105
105
  # @param record [MARC::Record]
106
106
  # @return [Array<String>] auxiliary journal title information for search
107
107
  def journal_search_aux(record)
108
- values = search_aux_values(record: record, title_type: :main, journal: true, &subfield_not_in?(%w[c 6 8])) +
108
+ values = search_aux_values(record: record, title_type: :main, journal: true, &subfield_not_in?(%w[c 6 7 8])) +
109
109
  search_aux_values(record: record, title_type: :related, journal: true, &subfield_in?(%w[s t])) +
110
110
  search_aux_values(record: record, title_type: :entity, journal: true, &subfield_in?(%w[t])) +
111
111
  search_aux_values(record: record, title_type: :note, journal: true, &subfield_in?(%w[t]))
@@ -120,20 +120,20 @@ module PennMARC
120
120
  # @return [String] single title for display
121
121
  def show(record)
122
122
  field = record.fields('245')&.first
123
- return Array.wrap(NO_TITLE_PROVIDED) unless field.present?
123
+ return NO_TITLE_PROVIDED unless field.present?
124
124
 
125
125
  values = title_values(field)
126
126
  [format_title(values[:title_or_form]), values[:punctuation], values[:other_info]].compact_blank.join(' ')
127
127
  end
128
128
 
129
- # Same as show, but with all subfields included as found - except for subfield c.
129
+ # Same as show, but with most subfields included as found.
130
130
  # @param record [MARC::Record]
131
131
  # @return [String] detailed title for display
132
132
  def detailed_show(record)
133
133
  field = record.fields('245')&.first
134
134
  return unless field
135
135
 
136
- join_subfields(field, &subfield_not_in?(%w[6 8]))
136
+ join_subfields(field, &subfield_not_in?(%w[6 7 8]))
137
137
  end
138
138
 
139
139
  # Same structure as show, but linked alternate title.
@@ -198,7 +198,7 @@ module PennMARC
198
198
  # @return [Array<String>] Array of standardized titles as strings
199
199
  def standardized_show(record)
200
200
  standardized_titles = record.fields(%w[130 240]).map do |field|
201
- join_subfields(field, &subfield_not_in?(%w[0 6 8 e w]))
201
+ join_subfields(field, &subfield_not_in?(%w[0 6 7 8 e w]))
202
202
  end
203
203
  standardized_titles += record.fields('730').filter_map do |field|
204
204
  # skip unless one of the indicators is blank
@@ -207,13 +207,13 @@ module PennMARC
207
207
  # skip if a subfield i is present
208
208
  next if subfield_defined?(field, 'i')
209
209
 
210
- join_subfields(field, &subfield_not_in?(%w[5 6 8 e w]))
210
+ join_subfields(field, &subfield_not_in?(%w[5 6 7 8 e w]))
211
211
  end
212
212
  titles = standardized_titles + record.fields('880').filter_map do |field|
213
213
  next unless subfield_undefined?(field, 'i') &&
214
214
  subfield_value?(field, '6', /^(130|240|730)/)
215
215
 
216
- join_subfields field, &subfield_not_in?(%w[5 6 8 e w])
216
+ join_subfields field, &subfield_not_in?(%w[5 6 7 8 e w])
217
217
  end
218
218
  titles.uniq
219
219
  end
@@ -227,18 +227,18 @@ module PennMARC
227
227
  # @return [Array<String>] Array of other titles as strings
228
228
  def other_show(record)
229
229
  other_titles = record.fields('246').map do |field|
230
- join_subfields(field, &subfield_not_in?(%w[6 8]))
230
+ join_subfields(field, &subfield_not_in?(%w[6 7 8]))
231
231
  end
232
232
  other_titles += record.fields('740')
233
233
  .filter_map do |field|
234
234
  next unless field.indicator2.in? ['', ' ', '0', '1', '3']
235
235
 
236
- join_subfields(field, &subfield_not_in?(%w[5 6 8]))
236
+ join_subfields(field, &subfield_not_in?(%w[5 6 7 8]))
237
237
  end
238
238
  titles = other_titles + record.fields('880').filter_map do |field|
239
239
  next unless subfield_value? field, '6', /^(246|740)/
240
240
 
241
- join_subfields(field, &subfield_not_in?(%w[5 6 8]))
241
+ join_subfields(field, &subfield_not_in?(%w[5 6 7 8]))
242
242
  end
243
243
  titles.uniq
244
244
  end
@@ -257,7 +257,7 @@ module PennMARC
257
257
  next unless field.tag == '247' || (field.tag == '880' && subfield_value?(field, '6', /^247/))
258
258
 
259
259
  # 6 and 8 are not meaningful for display
260
- former_title = join_subfields field, &subfield_not_in?(%w[6 8 e w])
260
+ former_title = join_subfields field, &subfield_not_in?(%w[6 7 8 e w])
261
261
  former_title_append = join_subfields field, &subfield_in?(%w[e w])
262
262
  "#{former_title} #{former_title_append}".strip
263
263
  }.uniq
@@ -3,10 +3,21 @@
3
3
  # read "Illegal Noncitizens".
4
4
  # Also, use lower case only in the key to support efficient case-insensitive matching.
5
5
  "illegal alien children": Undocumented immigrant children
6
- "alien detention centers": Immigrant detention centers
6
+ "alien detention centers": Noncitizen detention centers
7
7
  "alien criminals": Noncitizen criminals
8
- "alien property": Foreign-owned property
8
+ "alien property": Foreign property
9
9
  "illegal aliens": Undocumented immigrants
10
10
  "alien labor": Foreign workers
11
11
  gypsies: Romanies
12
12
  aliens: Noncitizens
13
+ "east indian americans": Indian Americans
14
+ "east indian american": Indian American
15
+ "east indians": Indians (India)
16
+ "east indian": Indian (India)
17
+ "indians of north america": Indigenous peoples of North America
18
+ "indians of south america": Indigenous peoples of South America
19
+ "indians of central america": Indigenous peoples of Central America
20
+ "indians of mexico": Indigenous peoples of Mexico
21
+ "indians of the west indies": Indigenous peoples of the West Indies
22
+ "memphis race riot, memphis, tenn., 1866": Memphis Race Massacre, Memphis, Tenn., 1866
23
+ "east saint louis race riot, east saint louis, ill., 1917": East Saint Louis Race Massacre, East Saint Louis, Ill., 1917
@@ -856,6 +856,10 @@ muselock:
856
856
  - Penn Museum Library
857
857
  display: Penn Museum Library - Locked Cases (ask staff)
858
858
  aeon: true
859
+ musemap:
860
+ specific_location: Penn Museum Library - Maps
861
+ library: Penn Museum Library
862
+ display: Penn Museum Library - Maps
859
863
  musemedia:
860
864
  specific_location: Penn Museum Library - Media
861
865
  library: Penn Museum Library
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PennMARC
4
- VERSION = '1.4.11'
4
+ VERSION = '1.4.12'
5
5
  end
@@ -8,45 +8,35 @@ describe 'PennMARC::HeadingControl' do
8
8
  describe '.process' do
9
9
  context 'with a term for removal' do
10
10
  it 'removes the term if found in isolation' do
11
- values = [remove_term]
12
- expect(PennMARC::HeadingControl.term_override(values)).to eq []
11
+ expect(PennMARC::HeadingControl.term_override(remove_term)).to be_nil
13
12
  end
14
13
 
15
14
  it 'removes the term regardless of case' do
16
- values = [remove_term.downcase]
17
- expect(PennMARC::HeadingControl.term_override(values)).to eq []
15
+ expect(PennMARC::HeadingControl.term_override(remove_term.downcase)).to be_nil
18
16
  end
19
17
 
20
18
  it 'removes the term if it is included as a substring' do
21
- values = ["#{remove_term}--History"]
22
- expect(PennMARC::HeadingControl.term_override(values)).to eq []
19
+ value = "#{remove_term}--History"
20
+ expect(PennMARC::HeadingControl.term_override(value)).to be_nil
23
21
  end
24
22
  end
25
23
 
26
24
  PennMARC::Mappers.heading_overrides.each do |target, replacement|
27
25
  context "with the \"#{target}\" term" do
28
26
  it 'replaces the term in isolation' do
29
- values = [target]
30
- expect(PennMARC::HeadingControl.term_override(values)).to eq [replacement]
27
+ expect(PennMARC::HeadingControl.term_override(target)).to eq replacement
31
28
  end
32
29
 
33
30
  it 'replaces the term when used with other headings' do
34
- values = ["#{target}--History"]
35
- expect(PennMARC::HeadingControl.term_override(values)).to eq ["#{replacement}--History"]
31
+ value = "#{target}--History"
32
+ expect(PennMARC::HeadingControl.term_override(value)).to eq "#{replacement}--History"
36
33
  end
37
34
 
38
35
  it 'replaces the term regardless of case' do
39
- values = ["#{target.titleize}--History"]
40
- expect(PennMARC::HeadingControl.term_override(values)).to eq ["#{replacement}--History"]
36
+ value = "#{target.titleize}--History"
37
+ expect(PennMARC::HeadingControl.term_override(value)).to eq "#{replacement}--History"
41
38
  end
42
39
  end
43
40
  end
44
-
45
- context 'with a variety of terms' do
46
- it 'removes and replaces terms as needed' do
47
- values = [remove_term, replace_term, 'History']
48
- expect(PennMARC::HeadingControl.term_override(values)).to contain_exactly 'History', replaced_term
49
- end
50
- end
51
41
  end
52
42
  end
@@ -199,6 +199,20 @@ describe 'PennMARC::Subject' do
199
199
  'Franklin, Benjamin', 'Philadelphia (Pa.)')
200
200
  end
201
201
  end
202
+
203
+ context 'with a record with fields not subject to overriding' do
204
+ let(:replace_term) { PennMARC::Mappers.heading_overrides.keys[2] }
205
+ let(:fields) do
206
+ [marc_field(tag: '650', indicator2: '0', subfields: { a: replace_term }),
207
+ marc_field(tag: PennMARC::HeadingControl::NO_OVERRIDE_TAGS.sample, indicator2: '0',
208
+ subfields: { a: replace_term })]
209
+ end
210
+
211
+ it 'only overrides in the allowed field' do
212
+ replaced_term = PennMARC::HeadingControl.term_override(replace_term)
213
+ expect(values).to contain_exactly(replace_term, replaced_term)
214
+ end
215
+ end
202
216
  end
203
217
 
204
218
  describe '.show' do
@@ -142,7 +142,7 @@ describe 'PennMARC::Title' do
142
142
  end
143
143
 
144
144
  it 'returns default title' do
145
- expect(helper.show(record)).to eq [PennMARC::Title::NO_TITLE_PROVIDED]
145
+ expect(helper.show(record)).to eq PennMARC::Title::NO_TITLE_PROVIDED
146
146
  end
147
147
  end
148
148
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pennmarc
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.11
4
+ version: 1.4.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Kanning
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2026-10-08 00:00:00.000000000 Z
15
+ date: 2026-11-11 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: activesupport
@@ -94,8 +94,8 @@ extra_rdoc_files: []
94
94
  files:
95
95
  - ".gitignore"
96
96
  - ".gitlab-ci.yml"
97
+ - ".gitlab/ci-requirements.yml"
97
98
  - ".gitleaks.toml"
98
- - ".pipeline.yml"
99
99
  - ".rspec"
100
100
  - ".rubocop.yml"
101
101
  - ".rubocop_todo.yml"
data/.pipeline.yml DELETED
@@ -1,73 +0,0 @@
1
- variables:
2
- FF_SCRIPT_SECTIONS: "true"
3
- GIT_CLONE_PATH: "${CI_BUILDS_DIR}/${CI_PROJECT_NAME}/${CI_JOB_ID}"
4
-
5
- include:
6
- - component: gitlab.library.upenn.edu/devops/gitlab/components/general/install_hashicorp_vault@~latest
7
- - component: gitlab.library.upenn.edu/devops/gitlab/components/general/vault_jwt_auth@~latest
8
- inputs:
9
- vault_addr: ${VAULT_ADDR}
10
- vault_auth_path: ${VAULT_AUTH_PATH}
11
- vault_auth_role: ${VAULT_AUTH_ROLE}
12
-
13
- - project: "devops/gitlab/ci-templates/ruby"
14
- ref: "sans-dind"
15
- file:
16
- - ".rspec.yml"
17
- - ".rubocop.yml"
18
-
19
- stages:
20
- - test
21
- - deploy
22
-
23
- rspec_app_test:
24
- stage: test
25
- image: ruby:3.2.2
26
- before_script:
27
- - bundle install -j $(nproc)
28
- extends:
29
- - .rspec
30
- tags:
31
- - build
32
-
33
- rubocop_app_test:
34
- stage: test
35
- image: ruby:3.2.2
36
- before_script:
37
- - bundle install -j $(nproc)
38
- extends:
39
- - .rubocop
40
- tags:
41
- - build
42
-
43
- # This step was inspired from Gitlab's example of gem publication:
44
- # https://gitlab.com/gitlab-org/quality/pipeline-common/-/blob/master/ci/gem-release.yml
45
- gem_publication:
46
- stage: deploy
47
- image: ruby:3.2.2
48
- extends:
49
- - .vault_jwt_auth
50
- variables:
51
- GEMSPEC_FILE: "${CI_PROJECT_NAME}.gemspec"
52
- before_script:
53
- - !reference [.install_hashicorp_vault, before_script]
54
- - !reference [.vault_jwt_auth, before_script]
55
- script:
56
- - export GEM_HOST_API_KEY="$(vault kv get -field=rubygems_api_key ${VAULT_KV_ENDPOINT}${ENVIRONMENT})"
57
- - |
58
- gem -v
59
- rm -f ./*.gem
60
- [ -f "${GEMSPEC_FILE}" ] || (echo "No ${GEMSPEC_FILE} file found!" && exit 1)
61
- - '[ -n "${GEM_HOST_API_KEY}" ] || (echo "GEM_HOST_API_KEY is not set!" && exit 1)'
62
- - GEM_FILE=$(echo "${CI_PROJECT_NAME}-${CI_COMMIT_TAG:1}.gem")
63
- - |
64
- gem build "${GEMSPEC_FILE}"
65
- [ -f "${GEM_FILE}" ] || (echo "No ${GEM_FILE} file found!" && exit 1)
66
- - '[ "${DISABLE_GEM_PUSH}" == "true" ] || gem push "${GEM_FILE}"'
67
- rules:
68
- - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+(\.[a-zA-Z0-9]+)?$/
69
- artifacts:
70
- paths:
71
- - "*.gem"
72
- tags:
73
- - deploy