cocina-models 0.74.1 → 0.75.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/README.md +9 -0
- data/description_types.yml +1 -1
- data/lib/cocina/generator/generator.rb +0 -3
- data/lib/cocina/models/builders/dro_rights_description_builder.rb +69 -0
- data/lib/cocina/models/builders/name_title_group_builder.rb +134 -0
- data/lib/cocina/models/builders/rights_description_builder.rb +83 -0
- data/lib/cocina/models/builders/title_builder.rb +213 -0
- data/lib/cocina/models/validators/associated_name_validator.rb +77 -0
- data/lib/cocina/models/validators/validator.rb +1 -0
- data/lib/cocina/models/version.rb +1 -1
- metadata +7 -5
- data/lib/cocina/models/dro_rights_description_builder.rb +0 -67
- data/lib/cocina/models/rights_description_builder.rb +0 -81
- data/lib/cocina/models/title_builder.rb +0 -208
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ec67ece77b9025a117182d0213342e180e7b87dcd003a44e3d9dffeb03d08e1
|
4
|
+
data.tar.gz: 3ee4eee501728b92c356760c660514789082da70c41c990fc3822f5833a7f47d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4b0b94409167d640cfd84c7202468664ec32887405b17973e05ebdd77a55f608e53ad2c2956aabc85ac09dd9edecb6281eaafea7822d77bdea1075301e04aae6
|
7
|
+
data.tar.gz: 821ba48bca0b9c92031eb6ba2dbde666f2116c6c7f9076663730636f1c61f11e098551f2e81b975dea06289bd9ba09aa4f498ef67b36dbfa76d6581f29c515c4
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -42,6 +42,15 @@ The generator is tested via its output when run against `openapi.yml`, viz., the
|
|
42
42
|
|
43
43
|
Beyond what is necessary to test the generator, the Cocina model classes are not tested, i.e., they are assumed to be as specified in `openapi.yml`.
|
44
44
|
|
45
|
+
## Testing validation changes
|
46
|
+
|
47
|
+
If there is a possibility that a model or validation change will conflict with some existing objects then [validate-cocina](https://github.com/sul-dlss/dor-services-app/blob/main/bin/validate-cocina) should be used for testing. This must be run on sdr-deploy since it requires deploying a branch of cocina-models.
|
48
|
+
|
49
|
+
1. Create a cocina-models branch containing the proposed change and push to Github.
|
50
|
+
2. On sdr-deploy, check out `main`, update the `Gemfile` so that cocina-models references the branch, and `bundle install`.
|
51
|
+
3. Run `bin/validate-cocina`.
|
52
|
+
4. Check `validate-cocina.csv` for validation errors.
|
53
|
+
|
45
54
|
## Releasing
|
46
55
|
|
47
56
|
### Step 0: Share intent to change the models
|
data/description_types.yml
CHANGED
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Builders
|
6
|
+
# Rights description builder for items
|
7
|
+
class DroRightsDescriptionBuilder < RightsDescriptionBuilder
|
8
|
+
# @param [Cocina::Models::DRO] cocina_item
|
9
|
+
|
10
|
+
# This overrides the superclass
|
11
|
+
# @return [Cocina::Models::DROAccess]
|
12
|
+
def object_access
|
13
|
+
@object_access ||= cocina.access
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def object_level_access
|
19
|
+
super + access_level_from_files.uniq.map { |str| "#{str} (file)" }
|
20
|
+
end
|
21
|
+
|
22
|
+
def access_level_from_files
|
23
|
+
# dark access doesn't permit any file access
|
24
|
+
return [] if object_access.view == 'dark'
|
25
|
+
|
26
|
+
file_access_nodes.reject { |fa| same_as_object_access?(fa) }.flat_map do |fa|
|
27
|
+
file_access_from_file(fa)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# rubocop:disable Metrics/MethodLength
|
32
|
+
def file_access_from_file(file_access)
|
33
|
+
basic_access = if file_access[:view] == 'location-based'
|
34
|
+
"location: #{file_access[:location]}"
|
35
|
+
else
|
36
|
+
file_access[:view]
|
37
|
+
end
|
38
|
+
|
39
|
+
return [basic_access] if file_access[:view] == file_access[:download]
|
40
|
+
|
41
|
+
basic_access += ' (no-download)' if file_access[:view] != 'dark'
|
42
|
+
|
43
|
+
case file_access[:download]
|
44
|
+
when 'stanford'
|
45
|
+
[basic_access, 'stanford']
|
46
|
+
when 'location-based'
|
47
|
+
# Here we're using location to mean download location.
|
48
|
+
[basic_access, "location: #{file_access[:location]}"]
|
49
|
+
else
|
50
|
+
[basic_access]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
# rubocop:enable Metrics/MethodLength
|
54
|
+
|
55
|
+
def same_as_object_access?(file_access)
|
56
|
+
(file_access[:view] == object_access.view && file_access[:download] == object_access.download) ||
|
57
|
+
(object_access.view == 'citation-only' && file_access[:view] == 'dark')
|
58
|
+
end
|
59
|
+
|
60
|
+
def file_access_nodes
|
61
|
+
Array(cocina.structural.contains)
|
62
|
+
.flat_map { |fs| Array(fs.structural.contains) }
|
63
|
+
.map { |file| file.access.to_h }
|
64
|
+
.uniq
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Builders
|
6
|
+
# Helpers for MODS nameTitleGroups.
|
7
|
+
# MODS titles need to know if they match a contributor and thus need a nameTitleGroup
|
8
|
+
# MODS contributors need to know if they match a title and thus need a nameTitleGroup
|
9
|
+
# If there is a match, the nameTitleGroup number has to be consistent for the matching title(s)
|
10
|
+
# and the contributor(s)
|
11
|
+
class NameTitleGroupBuilder
|
12
|
+
# When to assign nameTitleGroup to MODS from cocina:
|
13
|
+
# for cocina title of type "uniform",
|
14
|
+
# look for cocina title properties :value or :structuredValue (recurse down through :parallelValue
|
15
|
+
# as needed), and look for associated :note with :type of "associated name" at the level of the
|
16
|
+
# non-empty title [value|structuredValue]
|
17
|
+
# The note of type "associated name" will have [value|structuredValue] which will match
|
18
|
+
# [value|structuredValue] for a contributor (possibly after recursing through :parallelValue).
|
19
|
+
# Thus, a title [value|structuredValue] and a contributor [value|structuredValue] are associated in
|
20
|
+
# cocina.
|
21
|
+
#
|
22
|
+
# If those criteria not met in Cocina, do not assign nameTitleGroup in MODS
|
23
|
+
#
|
24
|
+
# @params [Cocina::Models::Title] title
|
25
|
+
# @return [Hash<Hash, Hash>] key: hash of value or structuredValue property for title
|
26
|
+
# value: hash of value or structuredValue property for contributor
|
27
|
+
# e.g. {{:value=>"Portrait of the artist as a young man"}=>{:value=>"James Joyce"}}
|
28
|
+
# e.g. {{:value=>"Portrait of the artist as a young man"}=>{:structuredValue=>
|
29
|
+
# [{:value=>"Joyce, James", :type=>"name"},{:value=>"1882-1941", :type=>"life dates"}]}}
|
30
|
+
# e.g. {{:structuredValue=>[{:value=>"Demanding Food", :type=>"main"},
|
31
|
+
# {:value=>"A Cat's Life", :type=>"subtitle"}]}=>{:value=>"James Joyce"}}
|
32
|
+
# this complexity is needed for multilingual titles mapping to multilingual names.
|
33
|
+
def self.build_title_values_to_contributor_name_values(title)
|
34
|
+
result = {}
|
35
|
+
return result if title.blank?
|
36
|
+
|
37
|
+
# pair title value with contributor name value
|
38
|
+
title_value_note_slices(title).each do |value_note_slice|
|
39
|
+
title_val_slice = slice_of_value_or_structured_value(value_note_slice)
|
40
|
+
next if title_val_slice.blank?
|
41
|
+
|
42
|
+
associated_name_note = value_note_slice[:note]&.detect { |note| note[:type] == 'associated name' }
|
43
|
+
next if associated_name_note.blank?
|
44
|
+
|
45
|
+
# relevant note will be Array of either
|
46
|
+
# {
|
47
|
+
# value: 'string contributor name',
|
48
|
+
# type: 'associated name'
|
49
|
+
# }
|
50
|
+
# OR
|
51
|
+
# {
|
52
|
+
# structuredValue: [ structuredValue contributor name ],
|
53
|
+
# type: 'associated name'
|
54
|
+
# }
|
55
|
+
# and we want the hash without the :type attribute
|
56
|
+
result[title_val_slice] = slice_of_value_or_structured_value(associated_name_note)
|
57
|
+
end
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.contributor_for_contributor_name_value_slice(contributor_name_value_slice:, contributors:)
|
62
|
+
Array(contributors).find do |contributor|
|
63
|
+
contrib_name_value_slices = contributor_name_value_slices(contributor)
|
64
|
+
contrib_name_value_slices.include?(contributor_name_value_slice)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @params [Cocina::Models::Contributor] contributor
|
69
|
+
# @return [Hash] where we are only interested in
|
70
|
+
# hashes containing (either :value or :structureValue)
|
71
|
+
def self.contributor_name_value_slices(contributor)
|
72
|
+
return if contributor&.name.blank?
|
73
|
+
|
74
|
+
slices = []
|
75
|
+
Array(contributor.name).each do |contrib_name|
|
76
|
+
slices << value_slices(contrib_name)
|
77
|
+
end
|
78
|
+
slices.flatten
|
79
|
+
end
|
80
|
+
|
81
|
+
# @params [Cocina::Models::DescriptiveValue] desc_value
|
82
|
+
# @return [Array<Cocina::Models::DescriptiveValue>] where we are only interested in
|
83
|
+
# hashes containing (either :value or :structuredValue)
|
84
|
+
# rubocop:disable Metrics/AbcSize
|
85
|
+
def self.value_slices(desc_value)
|
86
|
+
slices = []
|
87
|
+
desc_value_slice = desc_value.to_h.slice(:value, :structuredValue, :parallelValue)
|
88
|
+
if desc_value_slice[:value].present? || desc_value_slice[:structuredValue].present?
|
89
|
+
slices << desc_value_slice.select { |_k, value| value.present? }
|
90
|
+
elsif desc_value_slice[:parallelValue].present?
|
91
|
+
desc_value_slice[:parallelValue].each { |parallel_val| slices << value_slices(parallel_val) }
|
92
|
+
end
|
93
|
+
# ignoring groupedValue
|
94
|
+
slices.flatten
|
95
|
+
end
|
96
|
+
# rubocop:enable Metrics/AbcSize
|
97
|
+
# private_class_method :value_slices
|
98
|
+
|
99
|
+
# for a given Hash (from a Cocina DescriptiveValue or Title or Name or ...)
|
100
|
+
# result will be either
|
101
|
+
# { value: 'string value' }
|
102
|
+
# OR
|
103
|
+
# { structuredValue: [ some structuredValue ] }
|
104
|
+
def self.slice_of_value_or_structured_value(hash)
|
105
|
+
if hash[:value].present?
|
106
|
+
hash.slice(:value).select { |_k, value| value.present? }
|
107
|
+
elsif hash[:structuredValue].present?
|
108
|
+
hash.slice(:structuredValue).select { |_k, value| value.present? }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# reduce parallelValues down to value or structuredValue for these slices
|
113
|
+
# @params [Cocina::Models::Title] title
|
114
|
+
# @return [Array<Cocina::Models::DescriptiveValue>] where we are only interested in
|
115
|
+
# hashes containing (either :value or :structureValue) and :note if present
|
116
|
+
# rubocop:disable Metrics/AbcSize
|
117
|
+
def self.title_value_note_slices(title)
|
118
|
+
slices = []
|
119
|
+
title_slice = title.to_h.slice(:value, :structuredValue, :parallelValue, :note)
|
120
|
+
if title_slice[:value].present? || title_slice[:structuredValue].present?
|
121
|
+
slices << title_slice.select { |_k, value| value.present? }
|
122
|
+
elsif title_slice[:parallelValue].present?
|
123
|
+
title_slice[:parallelValue].each do |parallel_val|
|
124
|
+
slices << title_value_note_slices(parallel_val)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
# ignoring groupedValue
|
128
|
+
slices.flatten
|
129
|
+
end
|
130
|
+
# rubocop:enable Metrics/AbcSize
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Builders
|
6
|
+
# RightsDescriptionBuilder
|
7
|
+
class RightsDescriptionBuilder
|
8
|
+
# @param [Cocina::Models::AdminPolicy, Cocina::Models::DRO] cocina_object
|
9
|
+
def self.build(cocina_object)
|
10
|
+
new(cocina_object).build
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(cocina_object)
|
14
|
+
@cocina = cocina_object
|
15
|
+
end
|
16
|
+
|
17
|
+
# This is set up to work for APOs, but this method is to be overridden on sub classes
|
18
|
+
# @return [Cocina::Models::AdminPolicyDefaultAccess]
|
19
|
+
def object_access
|
20
|
+
@object_access ||= cocina.administrative.accessTemplate
|
21
|
+
end
|
22
|
+
|
23
|
+
def build
|
24
|
+
return 'controlled digital lending' if object_access.controlledDigitalLending
|
25
|
+
|
26
|
+
return ['dark'] if object_access.view == 'dark'
|
27
|
+
|
28
|
+
object_level_access
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :cocina
|
34
|
+
|
35
|
+
# rubocop:disable Metrics/MethodLength
|
36
|
+
def object_level_access
|
37
|
+
case object_access.view
|
38
|
+
when 'citation-only'
|
39
|
+
['citation']
|
40
|
+
when 'world'
|
41
|
+
world_object_access
|
42
|
+
when 'location-based'
|
43
|
+
case object_access.download
|
44
|
+
when 'none'
|
45
|
+
["location: #{object_access.location} (no-download)"]
|
46
|
+
else
|
47
|
+
["location: #{object_access.location}"]
|
48
|
+
end
|
49
|
+
when 'stanford'
|
50
|
+
stanford_object_access
|
51
|
+
end
|
52
|
+
end
|
53
|
+
# rubocop:enable Metrics/MethodLength
|
54
|
+
|
55
|
+
def stanford_object_access
|
56
|
+
case object_access.download
|
57
|
+
when 'none'
|
58
|
+
['stanford (no-download)']
|
59
|
+
when 'location-based'
|
60
|
+
# this is an odd case we might want to move away from. See https://github.com/sul-dlss/cocina-models/issues/258
|
61
|
+
['stanford (no-download)', "location: #{object_access.location}"]
|
62
|
+
else
|
63
|
+
['stanford']
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def world_object_access
|
68
|
+
case object_access.download
|
69
|
+
when 'stanford'
|
70
|
+
['stanford', 'world (no-download)']
|
71
|
+
when 'none'
|
72
|
+
['world (no-download)']
|
73
|
+
when 'world'
|
74
|
+
['world']
|
75
|
+
when 'location-based'
|
76
|
+
# this is an odd case we might want to move away from. See https://github.com/sul-dlss/cocina-models/issues/258
|
77
|
+
['world (no-download)', "location: #{object_access.location}"]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deprecation'
|
4
|
+
|
5
|
+
module Cocina
|
6
|
+
module Models
|
7
|
+
module Builders
|
8
|
+
# TitleBuilder selects the prefered title from the cocina object for solr indexing
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
10
|
+
class TitleBuilder
|
11
|
+
extend Deprecation
|
12
|
+
# @param [[Array<Cocina::Models::Title,Cocina::Models::DescriptiveValue>] titles the titles to consider
|
13
|
+
# @param [Symbol] strategy ":first" is the strategy for selection when primary or display
|
14
|
+
# title are missing
|
15
|
+
# @param [Boolean] add_punctuation determines if the title should be formmated with punctuation
|
16
|
+
# @return [String] the title value for Solr
|
17
|
+
def self.build(titles, strategy: :first, add_punctuation: true)
|
18
|
+
if titles.respond_to?(:description)
|
19
|
+
Deprecation.warn(self,
|
20
|
+
"Calling TitleBuilder.build with a #{titles.class} is deprecated. " \
|
21
|
+
'It must be called with an array of titles')
|
22
|
+
titles = titles.description.title
|
23
|
+
end
|
24
|
+
new(strategy: strategy, add_punctuation: add_punctuation).build(titles)
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(strategy:, add_punctuation:)
|
28
|
+
@strategy = strategy
|
29
|
+
@add_punctuation = add_punctuation
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param [[Array<Cocina::Models::Title>] titles the titles to consider
|
33
|
+
# @return [String] the title value for Solr
|
34
|
+
def build(titles)
|
35
|
+
cocina_title = primary_title(titles) || untyped_title(titles)
|
36
|
+
cocina_title = other_title(titles) if cocina_title.blank?
|
37
|
+
|
38
|
+
if strategy == :first
|
39
|
+
extract_title(cocina_title)
|
40
|
+
else
|
41
|
+
cocina_title.map { |one| extract_title(one) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :strategy
|
48
|
+
|
49
|
+
def extract_title(cocina_title)
|
50
|
+
result = if cocina_title.value
|
51
|
+
cocina_title.value
|
52
|
+
elsif cocina_title.structuredValue.present?
|
53
|
+
title_from_structured_values(cocina_title)
|
54
|
+
elsif cocina_title.parallelValue.present?
|
55
|
+
return build(cocina_title.parallelValue)
|
56
|
+
end
|
57
|
+
remove_trailing_punctuation(result.strip) if result.present?
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_punctuation?
|
61
|
+
@add_punctuation
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Cocina::Models::Title, nil] title that has status=primary
|
65
|
+
def primary_title(titles)
|
66
|
+
primary_title = titles.find do |title|
|
67
|
+
title.status == 'primary'
|
68
|
+
end
|
69
|
+
return primary_title if primary_title.present?
|
70
|
+
|
71
|
+
# NOTE: structuredValues would only have status primary assigned as a sibling, not as an attribute
|
72
|
+
|
73
|
+
titles.find do |title|
|
74
|
+
title.parallelValue&.find do |parallel_title|
|
75
|
+
parallel_title.status == 'primary'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def untyped_title(titles)
|
81
|
+
method = strategy == :first ? :find : :select
|
82
|
+
untyped_title_for(titles.public_send(method))
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return [Array[Cocina::Models::Title]] first title that has no type attribute
|
86
|
+
def untyped_title_for(titles)
|
87
|
+
titles.each do |title|
|
88
|
+
if title.parallelValue.present?
|
89
|
+
untyped_title_for(title.parallelValue)
|
90
|
+
else
|
91
|
+
title.type.nil? || title.type == 'title'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# This handles 'main title', 'uniform' or 'translated'
|
97
|
+
def other_title(titles)
|
98
|
+
if strategy == :first
|
99
|
+
titles.first
|
100
|
+
else
|
101
|
+
titles
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# rubocop:disable Metrics/BlockLength
|
106
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
107
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
108
|
+
# rubocop:disable Metrics/MethodLength
|
109
|
+
# rubocop:disable Metrics/AbcSize
|
110
|
+
# @param [Cocina::Models::Title] title with structured values
|
111
|
+
# @return [String] the title value from combining the pieces of the structured_values by type and order
|
112
|
+
# with desired punctuation per specs
|
113
|
+
def title_from_structured_values(title)
|
114
|
+
structured_title = ''
|
115
|
+
part_name_number = ''
|
116
|
+
# combine pieces of the cocina structuredValue into a single title
|
117
|
+
title.structuredValue.each do |structured_value|
|
118
|
+
# There can be a structuredValue inside a structuredValue. For example,
|
119
|
+
# a uniform title where both the name and the title have internal StructuredValue
|
120
|
+
return title_from_structured_values(structured_value) if structured_value.structuredValue.present?
|
121
|
+
|
122
|
+
value = structured_value.value&.strip
|
123
|
+
next unless value
|
124
|
+
|
125
|
+
# additional types: name, uniform ...
|
126
|
+
case structured_value.type&.downcase
|
127
|
+
when 'nonsorting characters'
|
128
|
+
non_sort_value = "#{value}#{non_sorting_padding(title, value)}"
|
129
|
+
structured_title = if structured_title.present?
|
130
|
+
"#{structured_title}#{non_sort_value}"
|
131
|
+
else
|
132
|
+
non_sort_value
|
133
|
+
end
|
134
|
+
when 'part name', 'part number'
|
135
|
+
if part_name_number.blank?
|
136
|
+
part_name_number = part_name_number(title.structuredValue)
|
137
|
+
structured_title = if !add_punctuation?
|
138
|
+
[structured_title, part_name_number].join(' ')
|
139
|
+
elsif structured_title.present?
|
140
|
+
"#{structured_title.sub(/[ .,]*$/, '')}. #{part_name_number}. "
|
141
|
+
else
|
142
|
+
"#{part_name_number}. "
|
143
|
+
end
|
144
|
+
end
|
145
|
+
when 'main title', 'title'
|
146
|
+
structured_title = "#{structured_title}#{value}"
|
147
|
+
when 'subtitle'
|
148
|
+
# subtitle is preceded by space colon space, unless it is at the beginning of the title string
|
149
|
+
structured_title = if !add_punctuation?
|
150
|
+
[structured_title, value].join(' ')
|
151
|
+
elsif structured_title.present?
|
152
|
+
"#{structured_title.sub(/[. :]+$/, '')} : #{value.sub(/^:/, '').strip}"
|
153
|
+
else
|
154
|
+
structured_title = value.sub(/^:/, '').strip
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
structured_title
|
159
|
+
end
|
160
|
+
# rubocop:enable Metrics/AbcSize
|
161
|
+
# rubocop:enable Metrics/MethodLength
|
162
|
+
# rubocop:enable Metrics/BlockLength
|
163
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
164
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
165
|
+
|
166
|
+
def remove_trailing_punctuation(title)
|
167
|
+
title.sub(%r{[ .,;:/\\]+$}, '')
|
168
|
+
end
|
169
|
+
|
170
|
+
def non_sorting_padding(title, non_sorting_value)
|
171
|
+
non_sort_note = title.note&.find { |note| note.type&.downcase == 'nonsorting character count' }
|
172
|
+
if non_sort_note
|
173
|
+
padding_count = [non_sort_note.value.to_i - non_sorting_value.length, 0].max
|
174
|
+
' ' * padding_count
|
175
|
+
elsif ['\'', '-'].include?(non_sorting_value.last)
|
176
|
+
''
|
177
|
+
else
|
178
|
+
' '
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# combine part name and part number:
|
183
|
+
# respect order of occurrence
|
184
|
+
# separated from each other by comma space
|
185
|
+
def part_name_number(structured_values)
|
186
|
+
title_from_part = ''
|
187
|
+
structured_values.each do |structured_value|
|
188
|
+
case structured_value.type&.downcase
|
189
|
+
when 'part name', 'part number'
|
190
|
+
value = structured_value.value&.strip
|
191
|
+
next unless value
|
192
|
+
|
193
|
+
title_from_part = append_part_to_title(title_from_part, value)
|
194
|
+
|
195
|
+
end
|
196
|
+
end
|
197
|
+
title_from_part
|
198
|
+
end
|
199
|
+
|
200
|
+
def append_part_to_title(title_from_part, value)
|
201
|
+
if !add_punctuation?
|
202
|
+
[title_from_part, value].select(&:presence).join(' ')
|
203
|
+
elsif title_from_part.strip.present?
|
204
|
+
"#{title_from_part.sub(/[ .,]*$/, '')}, #{value}"
|
205
|
+
else
|
206
|
+
value
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
# rubocop:enable Metrics/ClassLength
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Validators
|
6
|
+
# Validates that when title.note with type "associated name" has a value, it must match a contributor name.
|
7
|
+
class AssociatedNameValidator
|
8
|
+
def self.validate(clazz, attributes)
|
9
|
+
new(clazz, attributes).validate
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(clazz, attributes)
|
13
|
+
@clazz = clazz
|
14
|
+
@attributes = attributes.deep_symbolize_keys
|
15
|
+
@error_paths = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate
|
19
|
+
return unless meets_preconditions?
|
20
|
+
|
21
|
+
return if resources.all? { |resource| valid?(resource) }
|
22
|
+
|
23
|
+
raise ValidationError,
|
24
|
+
'Missing data: Name associated with uniform title does not match any contributor.'
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :clazz, :attributes, :error_paths
|
30
|
+
|
31
|
+
def meets_preconditions?
|
32
|
+
resources.any? do |resource|
|
33
|
+
titles_with_associated_name_note_for(resource).present?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid?(resource)
|
38
|
+
titles_with_associated_name_note_for(resource).all? do |title|
|
39
|
+
contributor_name_value_slices = Builders::NameTitleGroupBuilder
|
40
|
+
.build_title_values_to_contributor_name_values(
|
41
|
+
Cocina::Models::Title.new(title)
|
42
|
+
).values
|
43
|
+
contributor_name_value_slices.all? do |contributor_name_value_slice|
|
44
|
+
contributors = Array(resource[:contributor]).map do |contributor|
|
45
|
+
Cocina::Models::Contributor.new(contributor)
|
46
|
+
end
|
47
|
+
Builders::NameTitleGroupBuilder.contributor_for_contributor_name_value_slice(
|
48
|
+
contributor_name_value_slice: contributor_name_value_slice, contributors: contributors
|
49
|
+
).present?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def resources
|
55
|
+
@resources ||= [description_attributes] + Array(description_attributes[:relatedResource])
|
56
|
+
end
|
57
|
+
|
58
|
+
def description_attributes
|
59
|
+
@description_attributes ||= if [Cocina::Models::Description,
|
60
|
+
Cocina::Models::RequestDescription].include?(clazz)
|
61
|
+
attributes
|
62
|
+
else
|
63
|
+
attributes[:description] || {}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def associated_name_note_for(title)
|
68
|
+
Array(title[:note]).detect { |note| note[:type] == 'associated name' }
|
69
|
+
end
|
70
|
+
|
71
|
+
def titles_with_associated_name_note_for(resource)
|
72
|
+
Array(resource[:title]).select { |title| associated_name_note_for(title).present? }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cocina-models
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.75.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Coyne
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-04-
|
11
|
+
date: 2022-04-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -313,6 +313,10 @@ files:
|
|
313
313
|
- lib/cocina/models/admin_policy_with_metadata.rb
|
314
314
|
- lib/cocina/models/administrative.rb
|
315
315
|
- lib/cocina/models/applies_to.rb
|
316
|
+
- lib/cocina/models/builders/dro_rights_description_builder.rb
|
317
|
+
- lib/cocina/models/builders/name_title_group_builder.rb
|
318
|
+
- lib/cocina/models/builders/rights_description_builder.rb
|
319
|
+
- lib/cocina/models/builders/title_builder.rb
|
316
320
|
- lib/cocina/models/business_barcode.rb
|
317
321
|
- lib/cocina/models/catalog_link.rb
|
318
322
|
- lib/cocina/models/catkey_barcode.rb
|
@@ -341,7 +345,6 @@ files:
|
|
341
345
|
- lib/cocina/models/doi.rb
|
342
346
|
- lib/cocina/models/dro.rb
|
343
347
|
- lib/cocina/models/dro_access.rb
|
344
|
-
- lib/cocina/models/dro_rights_description_builder.rb
|
345
348
|
- lib/cocina/models/dro_structural.rb
|
346
349
|
- lib/cocina/models/dro_with_metadata.rb
|
347
350
|
- lib/cocina/models/druid.rb
|
@@ -377,7 +380,6 @@ files:
|
|
377
380
|
- lib/cocina/models/request_file_set.rb
|
378
381
|
- lib/cocina/models/request_file_set_structural.rb
|
379
382
|
- lib/cocina/models/request_identification.rb
|
380
|
-
- lib/cocina/models/rights_description_builder.rb
|
381
383
|
- lib/cocina/models/sequence.rb
|
382
384
|
- lib/cocina/models/source.rb
|
383
385
|
- lib/cocina/models/source_id.rb
|
@@ -385,8 +387,8 @@ files:
|
|
385
387
|
- lib/cocina/models/standard_barcode.rb
|
386
388
|
- lib/cocina/models/stanford_access.rb
|
387
389
|
- lib/cocina/models/title.rb
|
388
|
-
- lib/cocina/models/title_builder.rb
|
389
390
|
- lib/cocina/models/validatable.rb
|
391
|
+
- lib/cocina/models/validators/associated_name_validator.rb
|
390
392
|
- lib/cocina/models/validators/catalog_links_validator.rb
|
391
393
|
- lib/cocina/models/validators/dark_validator.rb
|
392
394
|
- lib/cocina/models/validators/description_types_validator.rb
|
@@ -1,67 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Cocina
|
4
|
-
module Models
|
5
|
-
# Rights description builder for items
|
6
|
-
class DroRightsDescriptionBuilder < RightsDescriptionBuilder
|
7
|
-
# @param [Cocina::Models::DRO] cocina_item
|
8
|
-
|
9
|
-
# This overrides the superclass
|
10
|
-
# @return [Cocina::Models::DROAccess]
|
11
|
-
def object_access
|
12
|
-
@object_access ||= cocina.access
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
def object_level_access
|
18
|
-
super + access_level_from_files.uniq.map { |str| "#{str} (file)" }
|
19
|
-
end
|
20
|
-
|
21
|
-
def access_level_from_files
|
22
|
-
# dark access doesn't permit any file access
|
23
|
-
return [] if object_access.view == 'dark'
|
24
|
-
|
25
|
-
file_access_nodes.reject { |fa| same_as_object_access?(fa) }.flat_map do |fa|
|
26
|
-
file_access_from_file(fa)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
# rubocop:disable Metrics/MethodLength
|
31
|
-
def file_access_from_file(file_access)
|
32
|
-
basic_access = if file_access[:view] == 'location-based'
|
33
|
-
"location: #{file_access[:location]}"
|
34
|
-
else
|
35
|
-
file_access[:view]
|
36
|
-
end
|
37
|
-
|
38
|
-
return [basic_access] if file_access[:view] == file_access[:download]
|
39
|
-
|
40
|
-
basic_access += ' (no-download)' if file_access[:view] != 'dark'
|
41
|
-
|
42
|
-
case file_access[:download]
|
43
|
-
when 'stanford'
|
44
|
-
[basic_access, 'stanford']
|
45
|
-
when 'location-based'
|
46
|
-
# Here we're using location to mean download location.
|
47
|
-
[basic_access, "location: #{file_access[:location]}"]
|
48
|
-
else
|
49
|
-
[basic_access]
|
50
|
-
end
|
51
|
-
end
|
52
|
-
# rubocop:enable Metrics/MethodLength
|
53
|
-
|
54
|
-
def same_as_object_access?(file_access)
|
55
|
-
(file_access[:view] == object_access.view && file_access[:download] == object_access.download) ||
|
56
|
-
(object_access.view == 'citation-only' && file_access[:view] == 'dark')
|
57
|
-
end
|
58
|
-
|
59
|
-
def file_access_nodes
|
60
|
-
Array(cocina.structural.contains)
|
61
|
-
.flat_map { |fs| Array(fs.structural.contains) }
|
62
|
-
.map { |file| file.access.to_h }
|
63
|
-
.uniq
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
@@ -1,81 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Cocina
|
4
|
-
module Models
|
5
|
-
# RightsDescriptionBuilder
|
6
|
-
class RightsDescriptionBuilder
|
7
|
-
# @param [Cocina::Models::AdminPolicy, Cocina::Models::DRO] cocina_object
|
8
|
-
def self.build(cocina_object)
|
9
|
-
new(cocina_object).build
|
10
|
-
end
|
11
|
-
|
12
|
-
def initialize(cocina_object)
|
13
|
-
@cocina = cocina_object
|
14
|
-
end
|
15
|
-
|
16
|
-
# This is set up to work for APOs, but this method is to be overridden on sub classes
|
17
|
-
# @return [Cocina::Models::AdminPolicyDefaultAccess]
|
18
|
-
def object_access
|
19
|
-
@object_access ||= cocina.administrative.accessTemplate
|
20
|
-
end
|
21
|
-
|
22
|
-
def build
|
23
|
-
return 'controlled digital lending' if object_access.controlledDigitalLending
|
24
|
-
|
25
|
-
return ['dark'] if object_access.view == 'dark'
|
26
|
-
|
27
|
-
object_level_access
|
28
|
-
end
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
attr_reader :cocina
|
33
|
-
|
34
|
-
# rubocop:disable Metrics/MethodLength
|
35
|
-
def object_level_access
|
36
|
-
case object_access.view
|
37
|
-
when 'citation-only'
|
38
|
-
['citation']
|
39
|
-
when 'world'
|
40
|
-
world_object_access
|
41
|
-
when 'location-based'
|
42
|
-
case object_access.download
|
43
|
-
when 'none'
|
44
|
-
["location: #{object_access.location} (no-download)"]
|
45
|
-
else
|
46
|
-
["location: #{object_access.location}"]
|
47
|
-
end
|
48
|
-
when 'stanford'
|
49
|
-
stanford_object_access
|
50
|
-
end
|
51
|
-
end
|
52
|
-
# rubocop:enable Metrics/MethodLength
|
53
|
-
|
54
|
-
def stanford_object_access
|
55
|
-
case object_access.download
|
56
|
-
when 'none'
|
57
|
-
['stanford (no-download)']
|
58
|
-
when 'location-based'
|
59
|
-
# this is an odd case we might want to move away from. See https://github.com/sul-dlss/cocina-models/issues/258
|
60
|
-
['stanford (no-download)', "location: #{object_access.location}"]
|
61
|
-
else
|
62
|
-
['stanford']
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def world_object_access
|
67
|
-
case object_access.download
|
68
|
-
when 'stanford'
|
69
|
-
['stanford', 'world (no-download)']
|
70
|
-
when 'none'
|
71
|
-
['world (no-download)']
|
72
|
-
when 'world'
|
73
|
-
['world']
|
74
|
-
when 'location-based'
|
75
|
-
# this is an odd case we might want to move away from. See https://github.com/sul-dlss/cocina-models/issues/258
|
76
|
-
['world (no-download)', "location: #{object_access.location}"]
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
@@ -1,208 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'deprecation'
|
4
|
-
|
5
|
-
module Cocina
|
6
|
-
module Models
|
7
|
-
# TitleBuilder selects the prefered title from the cocina object for solr indexing
|
8
|
-
# rubocop:disable Metrics/ClassLength
|
9
|
-
class TitleBuilder
|
10
|
-
extend Deprecation
|
11
|
-
# @param [[Array<Cocina::Models::Title,Cocina::Models::DescriptiveValue>] titles the titles to consider
|
12
|
-
# @param [Symbol] strategy ":first" is the strategy for selection when primary or display title are missing
|
13
|
-
# @param [Boolean] add_punctuation determines if the title should be formmated with punctuation
|
14
|
-
# @return [String] the title value for Solr
|
15
|
-
def self.build(titles, strategy: :first, add_punctuation: true)
|
16
|
-
if titles.respond_to?(:description)
|
17
|
-
Deprecation.warn(self, "Calling TitleBuilder.build with a #{titles.class} is deprecated. It must be called with an array of titles")
|
18
|
-
titles = titles.description.title
|
19
|
-
end
|
20
|
-
new(strategy: strategy, add_punctuation: add_punctuation).build(titles)
|
21
|
-
end
|
22
|
-
|
23
|
-
def initialize(strategy:, add_punctuation:)
|
24
|
-
@strategy = strategy
|
25
|
-
@add_punctuation = add_punctuation
|
26
|
-
end
|
27
|
-
|
28
|
-
# @param [[Array<Cocina::Models::Title>] titles the titles to consider
|
29
|
-
# @return [String] the title value for Solr
|
30
|
-
def build(titles)
|
31
|
-
cocina_title = primary_title(titles) || untyped_title(titles)
|
32
|
-
cocina_title = other_title(titles) if cocina_title.blank?
|
33
|
-
|
34
|
-
if strategy == :first
|
35
|
-
extract_title(cocina_title)
|
36
|
-
else
|
37
|
-
cocina_title.map { |one| extract_title(one) }
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
attr_reader :strategy
|
44
|
-
|
45
|
-
def extract_title(cocina_title)
|
46
|
-
result = if cocina_title.value
|
47
|
-
cocina_title.value
|
48
|
-
elsif cocina_title.structuredValue.present?
|
49
|
-
title_from_structured_values(cocina_title)
|
50
|
-
elsif cocina_title.parallelValue.present?
|
51
|
-
return build(cocina_title.parallelValue)
|
52
|
-
end
|
53
|
-
remove_trailing_punctuation(result.strip) if result.present?
|
54
|
-
end
|
55
|
-
|
56
|
-
def add_punctuation?
|
57
|
-
@add_punctuation
|
58
|
-
end
|
59
|
-
|
60
|
-
# @return [Cocina::Models::Title, nil] title that has status=primary
|
61
|
-
def primary_title(titles)
|
62
|
-
primary_title = titles.find do |title|
|
63
|
-
title.status == 'primary'
|
64
|
-
end
|
65
|
-
return primary_title if primary_title.present?
|
66
|
-
|
67
|
-
# NOTE: structuredValues would only have status primary assigned as a sibling, not as an attribute
|
68
|
-
|
69
|
-
titles.find do |title|
|
70
|
-
title.parallelValue&.find do |parallel_title|
|
71
|
-
parallel_title.status == 'primary'
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def untyped_title(titles)
|
77
|
-
method = strategy == :first ? :find : :select
|
78
|
-
untyped_title_for(titles.public_send(method))
|
79
|
-
end
|
80
|
-
|
81
|
-
# @return [Array[Cocina::Models::Title]] first title that has no type attribute
|
82
|
-
def untyped_title_for(titles)
|
83
|
-
titles.each do |title|
|
84
|
-
if title.parallelValue.present?
|
85
|
-
untyped_title_for(title.parallelValue)
|
86
|
-
else
|
87
|
-
title.type.nil? || title.type == 'title'
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
# This handles 'main title', 'uniform' or 'translated'
|
93
|
-
def other_title(titles)
|
94
|
-
if strategy == :first
|
95
|
-
titles.first
|
96
|
-
else
|
97
|
-
titles
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
# rubocop:disable Metrics/BlockLength
|
102
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
103
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
104
|
-
# rubocop:disable Metrics/MethodLength
|
105
|
-
# rubocop:disable Metrics/AbcSize
|
106
|
-
# @param [Cocina::Models::Title] title with structured values
|
107
|
-
# @return [String] the title value from combining the pieces of the structured_values by type and order
|
108
|
-
# with desired punctuation per specs
|
109
|
-
def title_from_structured_values(title)
|
110
|
-
structured_title = ''
|
111
|
-
part_name_number = ''
|
112
|
-
# combine pieces of the cocina structuredValue into a single title
|
113
|
-
title.structuredValue.each do |structured_value|
|
114
|
-
# There can be a structuredValue inside a structuredValue. For example,
|
115
|
-
# a uniform title where both the name and the title have internal StructuredValue
|
116
|
-
return title_from_structured_values(structured_value) if structured_value.structuredValue.present?
|
117
|
-
|
118
|
-
value = structured_value.value&.strip
|
119
|
-
next unless value
|
120
|
-
|
121
|
-
# additional types: name, uniform ...
|
122
|
-
case structured_value.type&.downcase
|
123
|
-
when 'nonsorting characters'
|
124
|
-
non_sort_value = "#{value}#{non_sorting_padding(title, value)}"
|
125
|
-
structured_title = if structured_title.present?
|
126
|
-
"#{structured_title}#{non_sort_value}"
|
127
|
-
else
|
128
|
-
non_sort_value
|
129
|
-
end
|
130
|
-
when 'part name', 'part number'
|
131
|
-
if part_name_number.blank?
|
132
|
-
part_name_number = part_name_number(title.structuredValue)
|
133
|
-
structured_title = if !add_punctuation?
|
134
|
-
[structured_title, part_name_number].join(' ')
|
135
|
-
elsif structured_title.present?
|
136
|
-
"#{structured_title.sub(/[ .,]*$/, '')}. #{part_name_number}. "
|
137
|
-
else
|
138
|
-
"#{part_name_number}. "
|
139
|
-
end
|
140
|
-
end
|
141
|
-
when 'main title', 'title'
|
142
|
-
structured_title = "#{structured_title}#{value}"
|
143
|
-
when 'subtitle'
|
144
|
-
# subtitle is preceded by space colon space, unless it is at the beginning of the title string
|
145
|
-
structured_title = if !add_punctuation?
|
146
|
-
[structured_title, value].join(' ')
|
147
|
-
elsif structured_title.present?
|
148
|
-
"#{structured_title.sub(/[. :]+$/, '')} : #{value.sub(/^:/, '').strip}"
|
149
|
-
else
|
150
|
-
structured_title = value.sub(/^:/, '').strip
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
154
|
-
structured_title
|
155
|
-
end
|
156
|
-
# rubocop:enable Metrics/AbcSize
|
157
|
-
# rubocop:enable Metrics/MethodLength
|
158
|
-
# rubocop:enable Metrics/BlockLength
|
159
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
160
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
161
|
-
|
162
|
-
def remove_trailing_punctuation(title)
|
163
|
-
title.sub(%r{[ .,;:/\\]+$}, '')
|
164
|
-
end
|
165
|
-
|
166
|
-
def non_sorting_padding(title, non_sorting_value)
|
167
|
-
non_sort_note = title.note&.find { |note| note.type&.downcase == 'nonsorting character count' }
|
168
|
-
if non_sort_note
|
169
|
-
padding_count = [non_sort_note.value.to_i - non_sorting_value.length, 0].max
|
170
|
-
' ' * padding_count
|
171
|
-
elsif ['\'', '-'].include?(non_sorting_value.last)
|
172
|
-
''
|
173
|
-
else
|
174
|
-
' '
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
# combine part name and part number:
|
179
|
-
# respect order of occurrence
|
180
|
-
# separated from each other by comma space
|
181
|
-
def part_name_number(structured_values)
|
182
|
-
title_from_part = ''
|
183
|
-
structured_values.each do |structured_value|
|
184
|
-
case structured_value.type&.downcase
|
185
|
-
when 'part name', 'part number'
|
186
|
-
value = structured_value.value&.strip
|
187
|
-
next unless value
|
188
|
-
|
189
|
-
title_from_part = append_part_to_title(title_from_part, value)
|
190
|
-
|
191
|
-
end
|
192
|
-
end
|
193
|
-
title_from_part
|
194
|
-
end
|
195
|
-
|
196
|
-
def append_part_to_title(title_from_part, value)
|
197
|
-
if !add_punctuation?
|
198
|
-
[title_from_part, value].select(&:presence).join(' ')
|
199
|
-
elsif title_from_part.strip.present?
|
200
|
-
"#{title_from_part.sub(/[ .,]*$/, '')}, #{value}"
|
201
|
-
else
|
202
|
-
value
|
203
|
-
end
|
204
|
-
end
|
205
|
-
end
|
206
|
-
# rubocop:enable Metrics/ClassLength
|
207
|
-
end
|
208
|
-
end
|