cocina_display 0.4.0 → 0.6.0
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 +4 -4
- data/README.md +21 -16
- data/lib/cocina_display/cocina_record.rb +33 -72
- data/lib/cocina_display/concerns/access.rb +71 -0
- data/lib/cocina_display/concerns/contributors.rb +60 -41
- data/lib/cocina_display/concerns/events.rb +51 -25
- data/lib/cocina_display/concerns/forms.rb +134 -0
- data/lib/cocina_display/concerns/identifiers.rb +11 -3
- data/lib/cocina_display/concerns/subjects.rb +91 -0
- data/lib/cocina_display/contributor.rb +66 -11
- data/lib/cocina_display/dates/date.rb +9 -8
- data/lib/cocina_display/dates/date_range.rb +8 -0
- data/lib/cocina_display/events/event.rb +78 -0
- data/lib/cocina_display/events/imprint.rb +101 -0
- data/lib/cocina_display/events/location.rb +56 -0
- data/lib/cocina_display/marc_relator_codes.rb +314 -0
- data/lib/cocina_display/subject.rb +127 -0
- data/lib/cocina_display/title_builder.rb +2 -1
- data/lib/cocina_display/utils.rb +45 -6
- data/lib/cocina_display/version.rb +1 -1
- data/script/find_records.rb +85 -0
- metadata +27 -11
- data/lib/cocina_display/imprint.rb +0 -123
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e86f2100a204e574eaf48f86dccf3b9bdc4985285e72e05292f9dd722d9d87e9
|
4
|
+
data.tar.gz: 5730dd6764fa87f39dc6d00dc66c8ea72f10685c7e3aef0b92864c16b1186cb6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fcfa03dd80c3673045803c11c1d7fa3f9c5284f75806c7c76b5ccb52cb765631670696fe3777a2a5cfbcd90b209ba5e898a618a92d9ecf0186670fdddb09cb6e
|
7
|
+
data.tar.gz: acbcb5d312ac4755cfff3a74119b68e316d929da0d0d91f397afdd51480f3a90332166f2ccc052e53c2d675d749f62a12f814513b58413611475e36d732c5041
|
data/README.md
CHANGED
@@ -24,11 +24,11 @@ gem install cocina_display
|
|
24
24
|
|
25
25
|
### Obtaining Cocina
|
26
26
|
|
27
|
-
To start, you need some Cocina in JSON form.
|
27
|
+
To start, you need some Cocina in JSON form. Consumers of this gem are likely to be applications that are already harvesting this data (for indexing) or have it stored in an index or database (for display). In testing, you may have neither of these.
|
28
28
|
|
29
29
|
You can download some directly from PURL by visiting an object's PURL URL and appending `.json` to the end, like `https://purl.stanford.edu/bb112zx3193.json`. Some examples are available in the `spec/fixtures` directory.
|
30
30
|
|
31
|
-
There is also a helper method to fetch the Cocina JSON for a given DRUID and immediately parse it into a `CocinaRecord` object:
|
31
|
+
There is also a helper method to fetch the Cocina JSON for a given DRUID over HTTP and immediately parse it into a `CocinaRecord` object:
|
32
32
|
|
33
33
|
```ruby
|
34
34
|
> record = CocinaDisplay::CocinaRecord.fetch('bb112zx3193')
|
@@ -51,7 +51,7 @@ The `CocinaRecord` class provides some methods to access common fields, as well
|
|
51
51
|
=> "Hearst Magazines, Inc."
|
52
52
|
```
|
53
53
|
|
54
|
-
See the [API Documentation](https://sul-dlss.github.io/cocina_display/CocinaDisplay/CocinaRecord.html) for more details on the methods available in the `CocinaRecord` class.
|
54
|
+
See the [API Documentation](https://sul-dlss.github.io/cocina_display/CocinaDisplay/CocinaRecord.html) for more details on the methods available in the `CocinaRecord` class. The gem provides a large number of methods, organized into concerns, to access different parts of the data.
|
55
55
|
|
56
56
|
### Fetching nested data
|
57
57
|
|
@@ -61,47 +61,52 @@ The previous example used `Hash#dig` to access the first contributor's first nam
|
|
61
61
|
|
62
62
|
```ruby
|
63
63
|
# name values for all contributors in description
|
64
|
-
> record.path('$.description.contributor
|
64
|
+
> record.path('$.description.contributor.*.name.*.value').search
|
65
65
|
=> ["Hearst Magazines, Inc.", "Chesebrough, Jerry"]
|
66
66
|
# only contributors with a role with value "photographer"
|
67
|
-
> record.path("$.description.contributor[?@.role[?@.value == 'photographer']].name
|
67
|
+
> record.path("$.description.contributor[?@.role[?@.value == 'photographer']].name.*.value").search
|
68
68
|
=> ["Chesebrough, Jerry"]
|
69
69
|
```
|
70
70
|
|
71
71
|
The JsonPath implementation used is [janeway](https://www.rubydoc.info/gems/janeway-jsonpath/0.6.0/file/README.md), which supports the full syntax from the [finalized 2024 version of the specification](https://www.rfc-editor.org/rfc/rfc9535.html). Results returned from `#path` are Enumerators.
|
72
72
|
|
73
|
-
In the following example, we start an expression with `"$.."` to search for contributor nodes at _any_ level (e.g. `event.contributors`) and discover that there is a third contributor, but it has no `name` value. Using the `['code', 'value']` syntax, we can retrieve both `code` and `value` and show
|
73
|
+
In the following example, we start an expression with `"$.."` to search for contributor nodes at _any_ level (e.g. `event.contributors`) and discover that there is a third contributor, but it has no `name` value. Using the `['code', 'value']` syntax, we can retrieve both `code` and `value` and show the path they came from:
|
74
74
|
|
75
75
|
```ruby
|
76
|
-
> record.path("$..contributor
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
=> ["Hearst Magazines, Inc.", "Chesebrough, Jerry", "CSt"]
|
76
|
+
> record.path("$..contributor.*.name[*]['code', 'value']").map { |value, _node, key, path| [key, value, path] }
|
77
|
+
[["value", "Hearst Magazines, Inc.", "$['description']['contributor'][0]['name'][0]['value']"],
|
78
|
+
["value", "Chesebrough, Jerry", "$['description']['contributor'][1]['name'][0]['value']"],
|
79
|
+
["code", "CSt", "$['description']['adminMetadata']['contributor'][0]['name'][0]['code']"]]
|
81
80
|
```
|
82
81
|
|
83
82
|
There is also a command line utility for quickly querying a JSON file using JsonPath. Online syntax checkers may give different results, so it helps to test locally. You can run it with:
|
84
83
|
|
85
84
|
```bash
|
86
|
-
cat spec/fixtures/bb112zx3193.json | janeway "$.description.contributor[?@.role[?@.value == 'photographer']].name
|
85
|
+
cat spec/fixtures/bb112zx3193.json | janeway "$.description.contributor[?@.role[?@.value == 'photographer']].name.*.value"
|
87
86
|
[
|
88
87
|
"Chesebrough, Jerry"
|
89
88
|
]
|
90
89
|
```
|
91
90
|
|
91
|
+
### Searching for records
|
92
|
+
|
93
|
+
Sometimes you need to determine if records exist "in the wild" that exhibit particular characteristics in the Cocina metadata, like the presence or absence of a field, or a specific value in a field. There is a template script in the `scripts/` directory that can be used to crawl all DRUIDs released to a particular target, like Searchworks, and examine each record.
|
94
|
+
|
92
95
|
## Development
|
93
96
|
|
94
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
97
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. This is a useful place to try out JSONPath expressions with `CocinaRecord#path`.
|
98
|
+
|
99
|
+
Tests are written using [rspec](https://rspec.info), with coverage automatically measured via [simplecov](https://github.com/simplecov-ruby/simplecov). CI will fail if coverage drops below 100%. For convenience, if you invoke a single spec file locally, coverage will not be reported.
|
95
100
|
|
96
|
-
|
101
|
+
Documentation is generated using [yard](https://yardoc.org). You can generate it locally by running `yardoc`, or `yard server --reload` to start a local server and watch for changes as you edit. There is a GitHub action that automatically generates and publishes the documentation to GitHub Pages on every push/merge to `main`.
|
97
102
|
|
98
|
-
|
103
|
+
To release a new version, update the version number in `version.rb`, run `bundle` to update `Gemfile.lock`, commit your changes, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
99
104
|
|
100
105
|
## Background
|
101
106
|
|
102
107
|
Historically, applications at SUL used a combination of several gems to render objects represented by MODS XML. With the transition to the Cocina data model, infrastructure applications adopted the [cocina-models gem](https://github.com/sul-dlss/cocina-models), which provides accessor objects and validators over Cocina JSON. Internal applications can fetch such objects over HTTP using [dor-services-client](https://github.com/sul-dlss/dor-services-client).
|
103
108
|
|
104
|
-
On the access side, Cocina JSON (the "public Cocina") is available statically via [PURL](https://purl.stanford.edu), but is only updated when an object is published ("shelved") from SDR. This frequently results in data that is technically invalid with respect to `cocina-models` but is still valid in the context of a patron-facing application.
|
109
|
+
On the access side, Cocina JSON (the "public Cocina") is available statically via [PURL](https://purl.stanford.edu), but is only updated when an object is published ("shelved") from SDR. This frequently results in data that is technically invalid with respect to `cocina-models` (i.e. it does not match the latest spec) but is still valid in the context of a patron-facing application (because it can still be rendered into useful information).
|
105
110
|
|
106
111
|
Cocina data can also be complex, representing the same underlying information in different ways. A "complete" implementation can involve checking multiple deeply nested paths to ensure no information is missed. Rather than tightly coupling access applications to `cocina-models`, this gem provides a set of helpers designed to safely parse Cocina JSON and render it in a consistent way across applications.
|
107
112
|
|
@@ -11,6 +11,10 @@ require_relative "concerns/events"
|
|
11
11
|
require_relative "concerns/contributors"
|
12
12
|
require_relative "concerns/identifiers"
|
13
13
|
require_relative "concerns/titles"
|
14
|
+
require_relative "concerns/access"
|
15
|
+
require_relative "concerns/subjects"
|
16
|
+
require_relative "concerns/forms"
|
17
|
+
require_relative "utils"
|
14
18
|
|
15
19
|
module CocinaDisplay
|
16
20
|
# Public Cocina metadata for an SDR object, as fetched from PURL.
|
@@ -19,23 +23,38 @@ module CocinaDisplay
|
|
19
23
|
include CocinaDisplay::Concerns::Contributors
|
20
24
|
include CocinaDisplay::Concerns::Identifiers
|
21
25
|
include CocinaDisplay::Concerns::Titles
|
26
|
+
include CocinaDisplay::Concerns::Access
|
27
|
+
include CocinaDisplay::Concerns::Subjects
|
28
|
+
include CocinaDisplay::Concerns::Forms
|
22
29
|
|
23
30
|
# Fetch a public Cocina document from PURL and create a CocinaRecord.
|
24
31
|
# @note This is intended to be used in development or testing only.
|
25
32
|
# @param druid [String] The bare DRUID of the object to fetch.
|
33
|
+
# @param deep_compact [Boolean] If true, compact the JSON to remove blank values.
|
26
34
|
# @return [CocinaDisplay::CocinaRecord]
|
27
35
|
# :nocov:
|
28
|
-
def self.fetch(druid)
|
29
|
-
|
36
|
+
def self.fetch(druid, deep_compact: false)
|
37
|
+
from_json(Net::HTTP.get(URI("https://purl.stanford.edu/#{druid}.json")), deep_compact: deep_compact)
|
30
38
|
end
|
31
39
|
# :nocov:
|
32
40
|
|
41
|
+
# Create a CocinaRecord from a JSON string.
|
42
|
+
# @param cocina_json [String]
|
43
|
+
# @param deep_compact [Boolean] If true, compact the JSON to remove blank values.
|
44
|
+
# @return [CocinaDisplay::CocinaRecord]
|
45
|
+
def self.from_json(cocina_json, deep_compact: false)
|
46
|
+
cocina_doc = JSON.parse(cocina_json)
|
47
|
+
deep_compact ? new(Utils.deep_compact_blank(cocina_doc)) : new(cocina_doc)
|
48
|
+
end
|
49
|
+
|
33
50
|
# The parsed Cocina document.
|
34
51
|
# @return [Hash]
|
35
52
|
attr_reader :cocina_doc
|
36
53
|
|
37
|
-
|
38
|
-
|
54
|
+
# Initialize a CocinaRecord with a Cocina document hash.
|
55
|
+
# @param cocina_doc [Hash]
|
56
|
+
def initialize(cocina_doc)
|
57
|
+
@cocina_doc = cocina_doc
|
39
58
|
end
|
40
59
|
|
41
60
|
# Evaluate a JSONPath expression against the Cocina document.
|
@@ -43,9 +62,9 @@ module CocinaDisplay
|
|
43
62
|
# @param path_expression [String] The JSONPath expression to evaluate.
|
44
63
|
# @see https://www.rubydoc.info/gems/janeway-jsonpath/0.6.0/file/README.md
|
45
64
|
# @example Name values for contributors
|
46
|
-
# record.path("$.description.contributor
|
65
|
+
# record.path("$.description.contributor.*.name.*.value").search #=> ["Smith, John", "ACME Corp."]
|
47
66
|
# @example Filtering nodes using a condition
|
48
|
-
# record.path("$.description.contributor[?(@.type == 'person')].name
|
67
|
+
# record.path("$.description.contributor[?(@.type == 'person')].name.*.value").search #=> ["Smith, John"]
|
49
68
|
def path(path_expression)
|
50
69
|
Janeway.enum_for(path_expression, cocina_doc)
|
51
70
|
end
|
@@ -73,6 +92,13 @@ module CocinaDisplay
|
|
73
92
|
cocina_doc["type"].split("/").last
|
74
93
|
end
|
75
94
|
|
95
|
+
# Primary processing label for the object.
|
96
|
+
# @note This may or may not be the same as the title.
|
97
|
+
# @return [String, nil]
|
98
|
+
def label
|
99
|
+
cocina_doc["label"]
|
100
|
+
end
|
101
|
+
|
76
102
|
# True if the object is a collection.
|
77
103
|
# @return [Boolean]
|
78
104
|
def collection?
|
@@ -88,72 +114,7 @@ module CocinaDisplay
|
|
88
114
|
# puts file["size"] #=> 123456
|
89
115
|
# end
|
90
116
|
def files
|
91
|
-
path("$.structural.contains
|
92
|
-
end
|
93
|
-
|
94
|
-
# The PURL URL for this object.
|
95
|
-
# @return [String]
|
96
|
-
# @example
|
97
|
-
# record.purl_url #=> "https://purl.stanford.edu/bx658jh7339"
|
98
|
-
def purl_url
|
99
|
-
cocina_doc.dig("description", "purl") || "https://purl.stanford.edu/#{bare_druid}"
|
100
|
-
end
|
101
|
-
|
102
|
-
# The URL to the PURL environment this object is from.
|
103
|
-
# @note Objects accessed via UAT will still have a production PURL base URL.
|
104
|
-
# @return [String]
|
105
|
-
# @example
|
106
|
-
# record.purl_base_url #=> "https://purl.stanford.edu"
|
107
|
-
def purl_base_url
|
108
|
-
URI(purl_url).origin
|
109
|
-
end
|
110
|
-
|
111
|
-
# The URL to the stacks environment this object is shelved in.
|
112
|
-
# Corresponds to the PURL environment.
|
113
|
-
# @see purl_base_url
|
114
|
-
# @return [String]
|
115
|
-
# @example
|
116
|
-
# record.stacks_base_url #=> "https://stacks.stanford.edu"
|
117
|
-
def stacks_base_url
|
118
|
-
if purl_base_url == "https://sul-purl-stage.stanford.edu"
|
119
|
-
"https://sul-stacks-stage.stanford.edu"
|
120
|
-
else
|
121
|
-
"https://stacks.stanford.edu"
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
# The oEmbed URL for the object, optionally with additional parameters.
|
126
|
-
# Corresponds to the PURL environment.
|
127
|
-
# @param params [Hash] Additional parameters to include in the oEmbed URL.
|
128
|
-
# @return [String]
|
129
|
-
# @return [nil] if the object is a collection.
|
130
|
-
# @example Generate an oEmbed URL for the viewer and hide the title
|
131
|
-
# record.oembed_url(hide_title: true) #=> "https://purl.stanford.edu/bx658jh7339/embed.json?hide_title=true"
|
132
|
-
def oembed_url(params: {})
|
133
|
-
return if collection?
|
134
|
-
|
135
|
-
params[:url] ||= purl_url
|
136
|
-
"#{purl_base_url}/embed.json?#{params.to_query}"
|
137
|
-
end
|
138
|
-
|
139
|
-
# The download URL to get the entire object as a .zip file.
|
140
|
-
# Stacks generates the .zip for the object on request.
|
141
|
-
# @return [String]
|
142
|
-
# @example
|
143
|
-
# record.download_url #=> "https://stacks.stanford.edu/object/bx658jh7339"
|
144
|
-
def download_url
|
145
|
-
"#{stacks_base_url}/object/#{bare_druid}"
|
146
|
-
end
|
147
|
-
|
148
|
-
# The IIIF manifest URL for the object.
|
149
|
-
# PURL generates the IIIF manifest.
|
150
|
-
# @param version [Integer] The IIIF presentation spec version to use (3 or 2).
|
151
|
-
# @return [String]
|
152
|
-
# @example
|
153
|
-
# record.iiif_manifest_url #=> "https://purl.stanford.edu/bx658jh7339/iiif3/manifest"
|
154
|
-
def iiif_manifest_url(version: 3)
|
155
|
-
iiif_path = (version == 3) ? "iiif3" : "iiif"
|
156
|
-
"#{purl_url}/#{iiif_path}/manifest"
|
117
|
+
path("$.structural.contains.*.structural.contains[*]")
|
157
118
|
end
|
158
119
|
end
|
159
120
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module CocinaDisplay
|
2
|
+
module Concerns
|
3
|
+
# Methods that generate URLs to access an object.
|
4
|
+
module Access
|
5
|
+
# The PURL URL for this object.
|
6
|
+
# @return [String]
|
7
|
+
# @example
|
8
|
+
# record.purl_url #=> "https://purl.stanford.edu/bx658jh7339"
|
9
|
+
def purl_url
|
10
|
+
cocina_doc.dig("description", "purl") || "https://purl.stanford.edu/#{bare_druid}"
|
11
|
+
end
|
12
|
+
|
13
|
+
# The URL to the PURL environment this object is from.
|
14
|
+
# @note Objects accessed via UAT will still have a production PURL base URL.
|
15
|
+
# @return [String]
|
16
|
+
# @example
|
17
|
+
# record.purl_base_url #=> "https://purl.stanford.edu"
|
18
|
+
def purl_base_url
|
19
|
+
URI(purl_url).origin
|
20
|
+
end
|
21
|
+
|
22
|
+
# The URL to the stacks environment this object is shelved in.
|
23
|
+
# Corresponds to the PURL environment.
|
24
|
+
# @see purl_base_url
|
25
|
+
# @return [String]
|
26
|
+
# @example
|
27
|
+
# record.stacks_base_url #=> "https://stacks.stanford.edu"
|
28
|
+
def stacks_base_url
|
29
|
+
if purl_base_url == "https://sul-purl-stage.stanford.edu"
|
30
|
+
"https://sul-stacks-stage.stanford.edu"
|
31
|
+
else
|
32
|
+
"https://stacks.stanford.edu"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# The oEmbed URL for the object, optionally with additional parameters.
|
37
|
+
# Corresponds to the PURL environment.
|
38
|
+
# @param params [Hash] Additional parameters to include in the oEmbed URL.
|
39
|
+
# @return [String]
|
40
|
+
# @return [nil] if the object is a collection.
|
41
|
+
# @example Generate an oEmbed URL for the viewer and hide the title
|
42
|
+
# record.oembed_url(hide_title: true) #=> "https://purl.stanford.edu/bx658jh7339/embed.json?hide_title=true"
|
43
|
+
def oembed_url(params: {})
|
44
|
+
return if collection?
|
45
|
+
|
46
|
+
params[:url] ||= purl_url
|
47
|
+
"#{purl_base_url}/embed.json?#{params.to_query}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# The download URL to get the entire object as a .zip file.
|
51
|
+
# Stacks generates the .zip for the object on request.
|
52
|
+
# @return [String]
|
53
|
+
# @example
|
54
|
+
# record.download_url #=> "https://stacks.stanford.edu/object/bx658jh7339"
|
55
|
+
def download_url
|
56
|
+
"#{stacks_base_url}/object/#{bare_druid}"
|
57
|
+
end
|
58
|
+
|
59
|
+
# The IIIF manifest URL for the object.
|
60
|
+
# PURL generates the IIIF manifest.
|
61
|
+
# @param version [Integer] The IIIF presentation spec version to use (3 or 2).
|
62
|
+
# @return [String]
|
63
|
+
# @example
|
64
|
+
# record.iiif_manifest_url #=> "https://purl.stanford.edu/bx658jh7339/iiif3/manifest"
|
65
|
+
def iiif_manifest_url(version: 3)
|
66
|
+
iiif_path = (version == 3) ? "iiif3" : "iiif"
|
67
|
+
"#{purl_url}/#{iiif_path}/manifest"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -4,90 +4,109 @@ module CocinaDisplay
|
|
4
4
|
module Concerns
|
5
5
|
# Methods for finding and formatting names for contributors
|
6
6
|
module Contributors
|
7
|
-
# The main
|
7
|
+
# The main contributor's name, formatted for display.
|
8
8
|
# @param with_date [Boolean] Include life dates, if present
|
9
9
|
# @return [String]
|
10
|
-
# @return [nil] if no main
|
10
|
+
# @return [nil] if no main contributor is found
|
11
11
|
# @example
|
12
|
-
# record.
|
12
|
+
# record.main_contributor_name #=> "Smith, John"
|
13
13
|
# @example with date
|
14
|
-
# record.
|
15
|
-
def
|
16
|
-
|
14
|
+
# record.main_contributor_name(with_date: true) #=> "Smith, John, 1970-2020"
|
15
|
+
def main_contributor_name(with_date: false)
|
16
|
+
main_contributor&.display_name(with_date: with_date)
|
17
17
|
end
|
18
18
|
|
19
|
-
# All
|
19
|
+
# All contributor names except the main one, formatted for display.
|
20
20
|
# @param with_date [Boolean] Include life dates, if present
|
21
21
|
# @return [Array<String>]
|
22
|
-
def
|
23
|
-
|
22
|
+
def additional_contributor_names(with_date: false)
|
23
|
+
additional_contributors.map { |c| c.display_name(with_date: with_date) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# All names of publishers, formatted for display.
|
27
|
+
# @return [Array<String>]
|
28
|
+
def publisher_names
|
29
|
+
publisher_contributors.map(&:display_name)
|
24
30
|
end
|
25
31
|
|
26
32
|
# All names of authors who are people, formatted for display.
|
27
33
|
# @param with_date [Boolean] Include life dates, if present
|
28
34
|
# @return [Array<String>]
|
29
|
-
def
|
30
|
-
|
35
|
+
def person_contributor_names(with_date: false)
|
36
|
+
contributors.filter(&:person?).map { |c| c.display_name(with_date: with_date) }
|
31
37
|
end
|
32
38
|
|
33
|
-
# All names of non-person
|
39
|
+
# All names of non-person contributors, formatted for display.
|
34
40
|
# This includes organizations, conferences, families, etc.
|
35
41
|
# @return [Array<String>]
|
36
42
|
# @see https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md#contributor-types
|
37
|
-
def
|
38
|
-
|
43
|
+
def impersonal_contributor_names
|
44
|
+
contributors.reject(&:person?).map(&:display_name)
|
39
45
|
end
|
40
46
|
|
41
|
-
# All names of
|
47
|
+
# All names of contributors that are organizations, formatted for display.
|
42
48
|
# @return [Array<String>]
|
43
|
-
def
|
44
|
-
|
49
|
+
def organization_contributor_names
|
50
|
+
contributors.filter(&:organization?).map(&:display_name)
|
45
51
|
end
|
46
52
|
|
47
|
-
# All names of
|
53
|
+
# All names of contributors that are conferences, formatted for display.
|
48
54
|
# @return [Array<String>]
|
49
|
-
def
|
50
|
-
|
55
|
+
def conference_contributor_names
|
56
|
+
contributors.filter(&:conference?).map(&:display_name)
|
51
57
|
end
|
52
58
|
|
53
|
-
# A
|
59
|
+
# A hash mapping role names to the names of contributors with that role.
|
60
|
+
# @param with_date [Boolean] Include life dates, if present
|
61
|
+
# @return [Hash<String, Array<String>>]
|
62
|
+
def contributor_names_by_role(with_date: false)
|
63
|
+
contributors.each_with_object({}) do |contributor, hash|
|
64
|
+
contributor.roles.each do |role|
|
65
|
+
hash[role.display_str] ||= []
|
66
|
+
hash[role.display_str] << contributor.display_name(with_date: with_date)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# A string value for sorting by contributor that sorts missing values last.
|
72
|
+
# Appends the sort title to break ties between contributor names.
|
54
73
|
# Ignores punctuation and leading/trailing spaces.
|
55
74
|
# @return [String]
|
56
|
-
def
|
57
|
-
|
75
|
+
def sort_contributor_name
|
76
|
+
sort_name = main_contributor&.display_name || "\u{10FFFF}"
|
77
|
+
sort_name_title = [sort_name, sort_title].join(" ")
|
78
|
+
sort_name_title.gsub(/[[:punct:]]*/, "").strip
|
58
79
|
end
|
59
80
|
|
60
|
-
private
|
61
|
-
|
62
81
|
# All contributors for the object, including authors, editors, etc.
|
63
82
|
# @return [Array<Contributor>]
|
64
83
|
def contributors
|
65
|
-
@contributors ||= path("$.description.contributor
|
84
|
+
@contributors ||= path("$.description.contributor.*").map { |c| Contributor.new(c) }
|
66
85
|
end
|
67
86
|
|
68
|
-
# All contributors with a "
|
87
|
+
# All contributors with a "publisher" role.
|
69
88
|
# @return [Array<Contributor>]
|
70
|
-
# @see Contributor#
|
71
|
-
def
|
72
|
-
contributors.filter(&:
|
89
|
+
# @see Contributor#publisher?
|
90
|
+
def publisher_contributors
|
91
|
+
contributors.filter(&:publisher?)
|
73
92
|
end
|
74
93
|
|
75
|
-
#
|
94
|
+
# Object representing the main contributor.
|
76
95
|
# Selected according to the following rules:
|
77
|
-
# 1. If there
|
78
|
-
# 2. If there are no primary
|
79
|
-
# 3. If there are
|
96
|
+
# 1. If there are contributors marked as primary, use the first one.
|
97
|
+
# 2. If there are no primary contributors, use the first contributor with no role.
|
98
|
+
# 3. If there are no contributors without a role, use the first contributor.
|
80
99
|
# @return [Contributor]
|
81
|
-
# @return [nil] if no
|
82
|
-
def
|
83
|
-
|
100
|
+
# @return [nil] if there are no contributors at all
|
101
|
+
def main_contributor
|
102
|
+
contributors.find(&:primary?).presence || contributors.find { |c| !c.role? }.presence || contributors.first
|
84
103
|
end
|
85
104
|
|
86
|
-
# All
|
105
|
+
# All contributors except the main one.
|
87
106
|
# @return [Array<Contributor>]
|
88
|
-
def
|
89
|
-
return [] if
|
90
|
-
|
107
|
+
def additional_contributors
|
108
|
+
return [] if contributors.empty? || contributors.one?
|
109
|
+
contributors - [main_contributor]
|
91
110
|
end
|
92
111
|
end
|
93
112
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative "../dates/date"
|
2
2
|
require_relative "../dates/date_range"
|
3
|
-
require_relative "../
|
3
|
+
require_relative "../events/event"
|
4
|
+
require_relative "../events/imprint"
|
4
5
|
|
5
6
|
module CocinaDisplay
|
6
7
|
module Concerns
|
@@ -41,6 +42,20 @@ module CocinaDisplay
|
|
41
42
|
pub_date_edtf(ignore_qualified: ignore_qualified)&.year
|
42
43
|
end
|
43
44
|
|
45
|
+
# The range of preferred publication years as an array of integers.
|
46
|
+
# Considers publication, creation, and capture dates in that order.
|
47
|
+
# Prefers dates marked as primary and those with a declared encoding.
|
48
|
+
# @param ignore_qualified [Boolean] Reject qualified dates (e.g. approximate)
|
49
|
+
# @return [Array<Integer>, nil]
|
50
|
+
# @note 6 BCE will appear as -5; 4 CE will appear as 4.
|
51
|
+
def pub_year_int_range(ignore_qualified: false)
|
52
|
+
date = pub_date(ignore_qualified: ignore_qualified)
|
53
|
+
return unless date
|
54
|
+
|
55
|
+
date = date.as_interval if date.is_a? CocinaDisplay::Dates::DateRange
|
56
|
+
date.to_a.map(&:year).compact.uniq.sort
|
57
|
+
end
|
58
|
+
|
44
59
|
# String for displaying the earliest preferred publication year or range.
|
45
60
|
# Considers publication, creation, and capture dates in that order.
|
46
61
|
# Prefers dates marked as primary and those with a declared encoding.
|
@@ -61,24 +76,28 @@ module CocinaDisplay
|
|
61
76
|
# @example
|
62
77
|
# CocinaRecord.fetch('bt553vr2845').imprint_display_str #=> "New York : Meridian Book, 1993, c1967"
|
63
78
|
def imprint_display_str
|
64
|
-
|
79
|
+
imprint_events.map(&:display_str).compact_blank.join("; ")
|
65
80
|
end
|
66
81
|
|
67
|
-
|
82
|
+
# List of places of publication as strings.
|
83
|
+
# Considers locations for all publication, creation, and capture events.
|
84
|
+
# @return [Array<String>]
|
85
|
+
def publication_places
|
86
|
+
publication_events.flat_map { |event| event.locations.map(&:display_str) }
|
87
|
+
end
|
68
88
|
|
69
|
-
#
|
70
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
filter_expr = type.present? ? "?match(@.type, \"#{type}\")" : "*"
|
89
|
+
# All events associated with the object.
|
90
|
+
# @return [Array<CocinaDisplay::Events::Event>]
|
91
|
+
def events
|
92
|
+
@events ||= path("$.description.event.*").map { |event| CocinaDisplay::Events::Event.new(event) }
|
93
|
+
end
|
75
94
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
95
|
+
# All events that could be used to select a publication date.
|
96
|
+
# Includes publication, creation, and capture events.
|
97
|
+
# Considers event types as well as date types if the event is untyped.
|
98
|
+
# @return [Array<CocinaDisplay::Events::Event>]
|
99
|
+
def publication_events
|
100
|
+
events.filter { |event| event.has_any_type?("publication", "creation", "capture") }
|
82
101
|
end
|
83
102
|
|
84
103
|
# Array of CocinaDisplay::Imprint objects for all relevant Cocina events.
|
@@ -86,19 +105,22 @@ module CocinaDisplay
|
|
86
105
|
# Considers event types as well as date types if the event is untyped.
|
87
106
|
# Prefers events where the date was not encoded, if any.
|
88
107
|
# @return [Array<CocinaDisplay::Imprint>] The list of Imprint objects
|
89
|
-
def
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
path("$.description.event[?@.date[?match(@.type, #{filter_expr})]]")
|
95
|
-
).uniq.map do |event|
|
96
|
-
CocinaDisplay::Imprint.new(event)
|
108
|
+
def imprint_events
|
109
|
+
imprints = events.filter do |event|
|
110
|
+
event.has_any_type?("publication", "creation", "capture", "copyright")
|
111
|
+
end.map do |event|
|
112
|
+
CocinaDisplay::Events::Imprint.new(event.cocina)
|
97
113
|
end
|
98
114
|
|
99
115
|
imprints.reject(&:date_encoding?).presence || imprints
|
100
116
|
end
|
101
117
|
|
118
|
+
# All dates associated with the object via an event.
|
119
|
+
# @return [Array<CocinaDisplay::Dates::Date>]
|
120
|
+
def event_dates
|
121
|
+
@event_dates ||= events.flat_map(&:dates)
|
122
|
+
end
|
123
|
+
|
102
124
|
# The earliest preferred publication date as a CocinaDisplay::Dates::Date object.
|
103
125
|
# Considers publication, creation, and capture dates in that order.
|
104
126
|
# Prefers dates marked as primary and those with a declared encoding.
|
@@ -106,8 +128,12 @@ module CocinaDisplay
|
|
106
128
|
# @return [CocinaDisplay::Dates::Date] The earliest preferred date
|
107
129
|
# @return [nil] if no dates are left after filtering
|
108
130
|
def pub_date(ignore_qualified: false)
|
109
|
-
|
110
|
-
|
131
|
+
pub_event_dates = event_dates.filter { |date| date.type == "publication" }
|
132
|
+
creation_event_dates = event_dates.filter { |date| date.type == "creation" }
|
133
|
+
capture_event_dates = event_dates.filter { |date| date.type == "capture" }
|
134
|
+
|
135
|
+
[pub_event_dates, creation_event_dates, capture_event_dates].flat_map do |dates|
|
136
|
+
earliest_preferred_date(dates, ignore_qualified: ignore_qualified)
|
111
137
|
end.compact.first
|
112
138
|
end
|
113
139
|
|