arclight 0.0.1 → 1.6.2

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.
Files changed (204) hide show
  1. checksums.yaml +5 -5
  2. data/.all-contributorsrc +450 -0
  3. data/.codeclimate.yml +5 -0
  4. data/.eslintrc +22 -0
  5. data/.github/workflows/ruby.yml +51 -0
  6. data/.gitignore +7 -0
  7. data/.rspec +0 -1
  8. data/.rubocop.yml +521 -0
  9. data/.rubocop_todo.yml +122 -0
  10. data/.solr_wrapper +6 -0
  11. data/CONTRIBUTING.md +43 -0
  12. data/CONTRIBUTORS.md +79 -0
  13. data/Gemfile +41 -0
  14. data/LICENSE.txt +1 -0
  15. data/README.md +100 -12
  16. data/Rakefile +13 -3
  17. data/app/assets/images/arclight/logo.png +0 -0
  18. data/app/assets/images/blacklight/bookmark.svg +1 -0
  19. data/app/assets/images/blacklight/collection.svg +5 -0
  20. data/app/assets/images/blacklight/compact.svg +1 -0
  21. data/app/assets/images/blacklight/container.svg +5 -0
  22. data/app/assets/images/blacklight/ead.svg +1 -0
  23. data/app/assets/images/blacklight/file.svg +5 -0
  24. data/app/assets/images/blacklight/folder.svg +1 -0
  25. data/app/assets/images/blacklight/list.svg +1 -0
  26. data/app/assets/images/blacklight/minus.svg +1 -0
  27. data/app/assets/images/blacklight/online.svg +5 -0
  28. data/app/assets/images/blacklight/pdf.svg +1 -0
  29. data/app/assets/images/blacklight/plus.svg +1 -0
  30. data/app/assets/images/blacklight/repository.svg +1 -0
  31. data/app/assets/javascripts/arclight/arclight.js +4 -0
  32. data/app/assets/javascripts/arclight/oembed_controller.js +58 -0
  33. data/app/assets/javascripts/arclight/truncate_controller.js +27 -0
  34. data/app/assets/stylesheets/arclight/application.scss +14 -0
  35. data/app/assets/stylesheets/arclight/build.scss +4 -0
  36. data/app/assets/stylesheets/arclight/modules/collection_search.scss +3 -0
  37. data/app/assets/stylesheets/arclight/modules/hierarchy_and_online_contents.scss +155 -0
  38. data/app/assets/stylesheets/arclight/modules/highlights.scss +11 -0
  39. data/app/assets/stylesheets/arclight/modules/icons.scss +20 -0
  40. data/app/assets/stylesheets/arclight/modules/layout.scss +220 -0
  41. data/app/assets/stylesheets/arclight/modules/mastheads.scss +42 -0
  42. data/app/assets/stylesheets/arclight/modules/repositories.scss +25 -0
  43. data/app/assets/stylesheets/arclight/modules/repository_card.scss +9 -0
  44. data/app/assets/stylesheets/arclight/modules/search_form.scss +9 -0
  45. data/app/assets/stylesheets/arclight/modules/search_results.scss +250 -0
  46. data/app/assets/stylesheets/arclight/modules/show_collection.scss +38 -0
  47. data/app/assets/stylesheets/arclight/modules/truncator.scss +58 -0
  48. data/app/assets/stylesheets/arclight/responsive.scss +13 -0
  49. data/app/assets/stylesheets/arclight/variables.scss +25 -0
  50. data/app/components/arclight/access_component.html.erb +14 -0
  51. data/app/components/arclight/access_component.rb +25 -0
  52. data/app/components/arclight/bookmark_component.html.erb +25 -0
  53. data/app/components/arclight/bookmark_component.rb +9 -0
  54. data/app/components/arclight/breadcrumb_component.rb +50 -0
  55. data/app/components/arclight/breadcrumbs_hierarchy_component.html.erb +34 -0
  56. data/app/components/arclight/breadcrumbs_hierarchy_component.rb +25 -0
  57. data/app/components/arclight/collection_context_component.html.erb +12 -0
  58. data/app/components/arclight/collection_context_component.rb +27 -0
  59. data/app/components/arclight/collection_info_component.html.erb +28 -0
  60. data/app/components/arclight/collection_info_component.rb +26 -0
  61. data/app/components/arclight/collection_sidebar_component.html.erb +15 -0
  62. data/app/components/arclight/collection_sidebar_component.rb +39 -0
  63. data/app/components/arclight/constraints_component.html.erb +7 -0
  64. data/app/components/arclight/constraints_component.rb +17 -0
  65. data/app/components/arclight/document_collection_context_component.html.erb +29 -0
  66. data/app/components/arclight/document_collection_context_component.rb +28 -0
  67. data/app/components/arclight/document_collection_hierarchy_component.html.erb +50 -0
  68. data/app/components/arclight/document_collection_hierarchy_component.rb +62 -0
  69. data/app/components/arclight/document_component.html.erb +39 -0
  70. data/app/components/arclight/document_component.rb +49 -0
  71. data/app/components/arclight/document_components_hierarchy_component.html.erb +20 -0
  72. data/app/components/arclight/document_components_hierarchy_component.rb +34 -0
  73. data/app/components/arclight/document_download_component.html.erb +24 -0
  74. data/app/components/arclight/document_download_component.rb +71 -0
  75. data/app/components/arclight/embed_component.html.erb +10 -0
  76. data/app/components/arclight/embed_component.rb +43 -0
  77. data/app/components/arclight/expand_hierarchy_button_component.html.erb +5 -0
  78. data/app/components/arclight/expand_hierarchy_button_component.rb +16 -0
  79. data/app/components/arclight/group_component.html.erb +34 -0
  80. data/app/components/arclight/group_component.rb +23 -0
  81. data/app/components/arclight/header_component.html.erb +5 -0
  82. data/app/components/arclight/header_component.rb +10 -0
  83. data/app/components/arclight/index_metadata_field_component.html.erb +16 -0
  84. data/app/components/arclight/index_metadata_field_component.rb +20 -0
  85. data/app/components/arclight/masthead_component.html.erb +17 -0
  86. data/app/components/arclight/masthead_component.rb +10 -0
  87. data/app/components/arclight/metadata_section_component.html.erb +13 -0
  88. data/app/components/arclight/metadata_section_component.rb +22 -0
  89. data/app/components/arclight/oembed_viewer_component.html.erb +5 -0
  90. data/app/components/arclight/oembed_viewer_component.rb +16 -0
  91. data/app/components/arclight/online_content_filter_component.html.erb +15 -0
  92. data/app/components/arclight/online_content_filter_component.rb +17 -0
  93. data/app/components/arclight/online_status_indicator_component.rb +19 -0
  94. data/app/components/arclight/repository_breadcrumb_component.html.erb +10 -0
  95. data/app/components/arclight/repository_breadcrumb_component.rb +17 -0
  96. data/app/components/arclight/repository_location_component.html.erb +20 -0
  97. data/app/components/arclight/repository_location_component.rb +10 -0
  98. data/app/components/arclight/search_bar_component.html.erb +27 -0
  99. data/app/components/arclight/search_bar_component.rb +32 -0
  100. data/app/components/arclight/search_result_breadcrumbs_component.html.erb +6 -0
  101. data/app/components/arclight/search_result_breadcrumbs_component.rb +37 -0
  102. data/app/components/arclight/search_result_component.html.erb +20 -0
  103. data/app/components/arclight/search_result_component.rb +18 -0
  104. data/app/components/arclight/search_result_title_component.html.erb +15 -0
  105. data/app/components/arclight/search_result_title_component.rb +15 -0
  106. data/app/components/arclight/sidebar_component.html.erb +9 -0
  107. data/app/components/arclight/sidebar_component.rb +19 -0
  108. data/app/components/arclight/upper_metadata_layout_component.rb +10 -0
  109. data/app/components/blacklight/icons/bookmark_component.rb +12 -0
  110. data/app/components/blacklight/icons/collection_component.rb +14 -0
  111. data/app/components/blacklight/icons/compact_component.rb +12 -0
  112. data/app/components/blacklight/icons/container_component.rb +14 -0
  113. data/app/components/blacklight/icons/ead_component.rb +12 -0
  114. data/app/components/blacklight/icons/file_component.rb +14 -0
  115. data/app/components/blacklight/icons/folder_component.rb +12 -0
  116. data/app/components/blacklight/icons/online_component.rb +15 -0
  117. data/app/components/blacklight/icons/pdf_component.rb +12 -0
  118. data/app/components/blacklight/icons/repository_component.rb +12 -0
  119. data/app/controllers/arclight/repositories_controller.rb +40 -0
  120. data/app/helpers/arclight/ead_format_helpers.rb +309 -0
  121. data/app/helpers/arclight/field_config_helpers.rb +26 -0
  122. data/app/helpers/arclight_helper.rb +108 -0
  123. data/app/models/arclight/document_downloads.rb +122 -0
  124. data/app/models/arclight/parent.rb +24 -0
  125. data/app/models/arclight/parents.rb +39 -0
  126. data/app/models/arclight/requests/aeon_external_request.rb +42 -0
  127. data/app/models/arclight/requests/aeon_web_ead.rb +52 -0
  128. data/app/models/arclight/requests/google_form.rb +49 -0
  129. data/app/models/concerns/arclight/catalog.rb +44 -0
  130. data/app/models/concerns/arclight/search_behavior.rb +47 -0
  131. data/app/models/concerns/arclight/solr_document.rb +160 -0
  132. data/app/presenters/arclight/index_presenter.rb +10 -0
  133. data/app/presenters/arclight/show_presenter.rb +54 -0
  134. data/app/views/arclight/.keep +0 -0
  135. data/app/views/arclight/_requests.html.erb +7 -0
  136. data/app/views/arclight/repositories/_repository.html.erb +43 -0
  137. data/app/views/arclight/repositories/index.html.erb +9 -0
  138. data/app/views/arclight/repositories/show.html.erb +37 -0
  139. data/app/views/arclight/requests/_aeon_external_request_endpoint.html.erb +8 -0
  140. data/app/views/arclight/requests/_aeon_web_ead.html.erb +6 -0
  141. data/app/views/arclight/requests/_google_form.html.erb +10 -0
  142. data/app/views/catalog/_document_list.html.erb +8 -0
  143. data/app/views/catalog/_group.html.erb +4 -0
  144. data/app/views/catalog/_group_toggle.html.erb +10 -0
  145. data/app/views/catalog/_search_results_header.html.erb +3 -0
  146. data/app/views/catalog/hierarchy.html.erb +19 -0
  147. data/app/views/catalog/index.html.erb +17 -0
  148. data/app/views/shared/_breadcrumbs.html.erb +3 -0
  149. data/app/views/shared/_main_menu_links.html.erb +6 -0
  150. data/arclight.gemspec +29 -11
  151. data/bin/rails +13 -0
  152. data/config/breadcrumbs.rb +24 -0
  153. data/config/i18n-tasks.yml +134 -0
  154. data/config/importmap.rb +3 -0
  155. data/config/locales/arclight.en.yml +100 -0
  156. data/config/repositories.yml +0 -0
  157. data/config/routes.rb +6 -0
  158. data/docker-compose.yml +16 -0
  159. data/lib/arclight/digital_object.rb +27 -0
  160. data/lib/arclight/engine.rb +47 -0
  161. data/lib/arclight/exceptions.rb +19 -0
  162. data/lib/arclight/hash_absolute_xpath.rb +62 -0
  163. data/lib/arclight/level_label.rb +46 -0
  164. data/lib/arclight/missing_id_strategy.rb +21 -0
  165. data/lib/arclight/normalized_date.rb +49 -0
  166. data/lib/arclight/normalized_id.rb +30 -0
  167. data/lib/arclight/normalized_title.rb +33 -0
  168. data/lib/arclight/repository.rb +116 -0
  169. data/lib/arclight/routes/hierarchy.rb +19 -0
  170. data/lib/arclight/routes.rb +8 -0
  171. data/lib/arclight/traject/ead2_component_config.rb +335 -0
  172. data/lib/arclight/traject/ead2_config.rb +316 -0
  173. data/lib/arclight/traject/nokogiri_namespaceless_reader.rb +22 -0
  174. data/lib/arclight/version.rb +3 -1
  175. data/lib/arclight/year_range.rb +111 -0
  176. data/lib/arclight.rb +11 -1
  177. data/lib/generators/arclight/install_generator.rb +163 -0
  178. data/lib/generators/arclight/templates/arclight.scss +6 -0
  179. data/lib/generators/arclight/templates/catalog_controller.rb +423 -0
  180. data/lib/generators/arclight/templates/config/downloads.yml +25 -0
  181. data/lib/generators/arclight/templates/config/locales/arclight.en.yml +61 -0
  182. data/lib/generators/arclight/templates/config/repositories.yml +53 -0
  183. data/lib/generators/arclight/update_generator.rb +22 -0
  184. data/lib/tasks/index.rake +84 -0
  185. data/package.json +30 -0
  186. data/solr/conf/_rest_managed.json +3 -0
  187. data/solr/conf/admin-extra.html +31 -0
  188. data/solr/conf/elevate.xml +36 -0
  189. data/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
  190. data/solr/conf/protwords.txt +21 -0
  191. data/solr/conf/schema.xml +400 -0
  192. data/solr/conf/solrconfig.xml +328 -0
  193. data/solr/conf/spellings.txt +2 -0
  194. data/solr/conf/stopwords.txt +58 -0
  195. data/solr/conf/stopwords_en.txt +58 -0
  196. data/solr/conf/synonyms.txt +31 -0
  197. data/solr/conf/xslt/example.xsl +132 -0
  198. data/solr/conf/xslt/example_atom.xsl +67 -0
  199. data/solr/conf/xslt/example_rss.xsl +66 -0
  200. data/solr/conf/xslt/luke.xsl +337 -0
  201. data/tasks/arclight.rake +79 -0
  202. data/template.rb +10 -0
  203. metadata +440 -20
  204. data/.travis.yml +0 -5
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'traject'
5
+ require 'traject/nokogiri_reader'
6
+ require 'traject_plus'
7
+ require 'traject_plus/macros'
8
+ require 'arclight/level_label'
9
+ require 'arclight/normalized_id'
10
+ require 'arclight/normalized_date'
11
+ require 'arclight/normalized_title'
12
+ require 'active_model/conversion' ## Needed for Arclight::Repository
13
+ require 'active_support/core_ext/array/wrap'
14
+ require 'arclight/digital_object'
15
+ require 'arclight/year_range'
16
+ require 'arclight/repository'
17
+ require 'arclight/traject/nokogiri_namespaceless_reader'
18
+
19
+ # rubocop:disable Style/MixinUsage
20
+ extend TrajectPlus::Macros
21
+ # rubocop:enable Style/MixinUsage
22
+
23
+ NAME_ELEMENTS = %w[corpname famname name persname].freeze
24
+
25
+ SEARCHABLE_NOTES_FIELDS = %w[
26
+ accessrestrict
27
+ accruals
28
+ altformavail
29
+ appraisal
30
+ arrangement
31
+ bibliography
32
+ bioghist
33
+ custodhist
34
+ fileplan
35
+ note
36
+ odd
37
+ originalsloc
38
+ otherfindaid
39
+ phystech
40
+ prefercite
41
+ processinfo
42
+ relatedmaterial
43
+ scopecontent
44
+ separatedmaterial
45
+ userestrict
46
+ ].freeze
47
+
48
+ DID_SEARCHABLE_NOTES_FIELDS = %w[
49
+ abstract
50
+ materialspec
51
+ physloc
52
+ note
53
+ ].freeze
54
+
55
+ settings do
56
+ provide 'component_traject_config', File.join(__dir__, 'ead2_component_config.rb')
57
+ provide 'id_normalizer', 'Arclight::NormalizedId'
58
+ provide 'date_normalizer', 'Arclight::NormalizedDate'
59
+ provide 'title_normalizer', 'Arclight::NormalizedTitle'
60
+ provide 'reader_class_name', 'Arclight::Traject::NokogiriNamespacelessReader'
61
+ provide 'solr_writer.commit_on_close', 'true'
62
+ provide 'repository', ENV.fetch('REPOSITORY_ID', nil)
63
+ provide 'logger', Logger.new($stderr)
64
+ end
65
+
66
+ each_record do |_record, context|
67
+ next unless settings['repository']
68
+
69
+ context.clipboard[:repository] = Arclight::Repository.find_by(
70
+ slug: settings['repository']
71
+ ).name
72
+ end
73
+
74
+ # ==================
75
+ # Top level document
76
+ #
77
+ # NOTE: All fields should be stored in Solr
78
+ # ==================
79
+
80
+ to_field 'id' do |record, accumulator|
81
+ id = record.at_xpath('/ead/eadheader/eadid')&.text
82
+ unitid = record.at_xpath('/ead/archdesc/did/unitid')&.text
83
+ title = record.at_xpath('/ead/archdesc/did/unittitle')&.text
84
+ repository = settings['repository']
85
+ accumulator << settings['id_normalizer'].constantize.new(id, unitid: unitid, title: title, repository: repository).to_s
86
+ end
87
+
88
+ to_field 'title_filing_ssi', extract_xpath('/ead/eadheader/filedesc/titlestmt/titleproper[@type="filing"]')
89
+ to_field 'title_ssm', extract_xpath('/ead/archdesc/did/unittitle')
90
+ to_field 'title_tesim', extract_xpath('/ead/archdesc/did/unittitle')
91
+ to_field 'ead_ssi', extract_xpath('/ead/eadheader/eadid')
92
+
93
+ to_field 'unitdate_ssm', extract_xpath('/ead/archdesc/did/unitdate')
94
+ to_field 'unitdate_bulk_ssim', extract_xpath('/ead/archdesc/did/unitdate[@type="bulk"]')
95
+ to_field 'unitdate_inclusive_ssm', extract_xpath('/ead/archdesc/did/unitdate[@type="inclusive"]')
96
+ to_field 'unitdate_other_ssim', extract_xpath('/ead/archdesc/did/unitdate[not(@type)]')
97
+
98
+ # All top-level docs treated as 'collection' for routing / display purposes
99
+ to_field 'level_ssm' do |_record, accumulator|
100
+ accumulator << 'collection'
101
+ end
102
+
103
+ # Keep the original top-level archdesc/@level for Level facet in addition to 'Collection'
104
+ to_field 'level_ssim' do |record, accumulator|
105
+ level = record.at_xpath('/ead/archdesc').attribute('level')&.value
106
+ other_level = record.at_xpath('/ead/archdesc').attribute('otherlevel')&.value
107
+
108
+ accumulator << Arclight::LevelLabel.new(level, other_level).to_s
109
+ accumulator << 'Collection' unless level == 'collection'
110
+ end
111
+
112
+ to_field 'unitid_ssm', extract_xpath('/ead/archdesc/did/unitid')
113
+ to_field 'unitid_tesim', extract_xpath('/ead/archdesc/did/unitid')
114
+
115
+ to_field 'normalized_date_ssm' do |_record, accumulator, context|
116
+ accumulator << settings['date_normalizer'].constantize.new(
117
+ context.output_hash['unitdate_inclusive_ssm'],
118
+ context.output_hash['unitdate_bulk_ssim'],
119
+ context.output_hash['unitdate_other_ssim']
120
+ ).to_s
121
+ end
122
+
123
+ to_field 'normalized_title_ssm' do |_record, accumulator, context|
124
+ title = context.output_hash['title_ssm']&.first
125
+ date = context.output_hash['normalized_date_ssm']&.first
126
+ accumulator << settings['title_normalizer'].constantize.new(title, date).to_s
127
+ end
128
+
129
+ to_field 'collection_title_tesim' do |_record, accumulator, context|
130
+ accumulator.concat context.output_hash.fetch('normalized_title_ssm', [])
131
+ end
132
+
133
+ to_field 'collection_ssim' do |_record, accumulator, context|
134
+ accumulator.concat context.output_hash.fetch('normalized_title_ssm', [])
135
+ end
136
+
137
+ to_field 'repository_ssm' do |_record, accumulator, context|
138
+ accumulator << context.clipboard[:repository]
139
+ end
140
+
141
+ to_field 'repository_ssim' do |_record, accumulator, context|
142
+ accumulator << context.clipboard[:repository]
143
+ end
144
+
145
+ to_field 'geogname_ssm', extract_xpath('/ead/archdesc/controlaccess/geogname')
146
+ to_field 'geogname_ssim', extract_xpath('/ead/archdesc/controlaccess/geogname')
147
+
148
+ to_field 'creator_ssm', extract_xpath('/ead/archdesc/did/origination')
149
+ to_field 'creator_ssim', extract_xpath('/ead/archdesc/did/origination')
150
+ to_field 'creator_sort' do |record, accumulator|
151
+ accumulator << record.xpath('/ead/archdesc/did/origination').map { |c| c.text.strip }.join(', ')
152
+ end
153
+
154
+ to_field 'creator_persname_ssim', extract_xpath('/ead/archdesc/did/origination/persname')
155
+ to_field 'creator_corpname_ssim', extract_xpath('/ead/archdesc/did/origination/corpname')
156
+ to_field 'creator_famname_ssim', extract_xpath('/ead/archdesc/did/origination/famname')
157
+
158
+ to_field 'creators_ssim' do |_record, accumulator, context|
159
+ accumulator.concat context.output_hash['creator_persname_ssim'] if context.output_hash['creator_persname_ssim']
160
+ accumulator.concat context.output_hash['creator_corpname_ssim'] if context.output_hash['creator_corpname_ssim']
161
+ accumulator.concat context.output_hash['creator_famname_ssim'] if context.output_hash['creator_famname_ssim']
162
+ end
163
+
164
+ to_field 'places_ssim', extract_xpath('/ead/archdesc/controlaccess/geogname')
165
+
166
+ to_field 'access_terms_ssm', extract_xpath('/ead/archdesc/userestrict/*[local-name()!="head"]')
167
+
168
+ to_field 'acqinfo_ssim', extract_xpath('/ead/archdesc/acqinfo/*[local-name()!="head"]')
169
+ to_field 'acqinfo_ssim', extract_xpath('/ead/archdesc/descgrp/acqinfo/*[local-name()!="head"]')
170
+
171
+ to_field 'access_subjects_ssim', extract_xpath('/ead/archdesc/controlaccess', to_text: false) do |_record, accumulator|
172
+ accumulator.map! do |element|
173
+ %w[subject function occupation genreform].map do |selector|
174
+ element.xpath(".//#{selector}").map(&:text)
175
+ end
176
+ end.flatten!
177
+ end
178
+
179
+ to_field 'access_subjects_ssm' do |_record, accumulator, context|
180
+ accumulator.concat Array.wrap(context.output_hash['access_subjects_ssim'])
181
+ end
182
+
183
+ to_field 'has_online_content_ssim', extract_xpath('.//dao') do |_record, accumulator|
184
+ accumulator.replace([accumulator.any?])
185
+ end
186
+
187
+ to_field 'digital_objects_ssm', extract_xpath('/ead/archdesc/did/dao|/ead/archdesc/dao', to_text: false) do |_record, accumulator|
188
+ accumulator.map! do |dao|
189
+ label = dao.attributes['title']&.value ||
190
+ dao.xpath('daodesc/p')&.text
191
+ href = (dao.attributes['href'] || dao.attributes['xlink:href'])&.value
192
+ Arclight::DigitalObject.new(label: label, href: href).to_json
193
+ end
194
+ end
195
+
196
+ # This accumulates direct text from a physdesc, ignoring child elements handled elsewhere
197
+ to_field 'physdesc_tesim', extract_xpath('/ead/archdesc/did/physdesc', to_text: false) do |_record, accumulator|
198
+ accumulator.map! do |element|
199
+ physdesc = []
200
+ element.children.map do |child|
201
+ next if child.instance_of?(Nokogiri::XML::Element)
202
+
203
+ physdesc << child.text&.strip unless child.text&.strip&.empty?
204
+ end.flatten
205
+ physdesc.join(' ') unless physdesc.empty?
206
+ end
207
+ end
208
+
209
+ to_field 'extent_ssm' do |record, accumulator|
210
+ physdescs = record.xpath('/ead/archdesc/did/physdesc')
211
+ extents_per_physdesc = physdescs.map do |physdesc|
212
+ extents = physdesc.xpath('./extent').map { |e| e.text.strip }
213
+ # Join extents within the same physdesc with an empty string
214
+ extents.join(' ') unless extents.empty?
215
+ end
216
+
217
+ # Add each physdesc separately to the accumulator
218
+ accumulator.concat(extents_per_physdesc)
219
+ end
220
+
221
+ to_field 'extent_tesim' do |_record, accumulator, context|
222
+ accumulator.concat context.output_hash['extent_ssm'] || []
223
+ end
224
+
225
+ to_field 'physfacet_tesim', extract_xpath('/ead/archdesc/did/physdesc/physfacet')
226
+ to_field 'dimensions_tesim', extract_xpath('/ead/archdesc/did/physdesc/dimensions')
227
+
228
+ to_field 'genreform_ssim', extract_xpath('/ead/archdesc/controlaccess/genreform')
229
+
230
+ to_field 'date_range_isim', extract_xpath('/ead/archdesc/did/unitdate/@normal', to_text: false) do |_record, accumulator|
231
+ range = Arclight::YearRange.new
232
+ next range.years if accumulator.blank?
233
+
234
+ ranges = accumulator.map(&:to_s)
235
+ range << range.parse_ranges(ranges)
236
+ accumulator.replace range.years
237
+ end
238
+
239
+ to_field 'indexes_html_tesm', extract_xpath('/ead/archdesc/index', to_text: false)
240
+ to_field 'indexes_tesim', extract_xpath('/ead/archdesc/index')
241
+
242
+ SEARCHABLE_NOTES_FIELDS.map do |selector|
243
+ to_field "#{selector}_html_tesm", extract_xpath("/ead/archdesc/#{selector}/*[local-name()!='head']", to_text: false) do |_record, accumulator|
244
+ accumulator.map!(&:to_html)
245
+ end
246
+ to_field "#{selector}_heading_ssm", extract_xpath("/ead/archdesc/#{selector}/head") unless selector == 'prefercite'
247
+ to_field "#{selector}_tesim", extract_xpath("/ead/archdesc/#{selector}/*[local-name()!='head']")
248
+ end
249
+
250
+ DID_SEARCHABLE_NOTES_FIELDS.map do |selector|
251
+ to_field "#{selector}_html_tesm", extract_xpath("/ead/archdesc/did/#{selector}", to_text: false)
252
+ to_field "#{selector}_tesim", extract_xpath("/ead/archdesc/did/#{selector}")
253
+ end
254
+
255
+ NAME_ELEMENTS.map do |selector|
256
+ to_field 'names_coll_ssim', extract_xpath("/ead/archdesc/controlaccess/#{selector}")
257
+ to_field 'names_ssim', extract_xpath("//#{selector}"), unique
258
+ to_field "#{selector}_ssim", extract_xpath("//#{selector}"), unique
259
+ end
260
+
261
+ to_field 'language_ssim', extract_xpath('/ead/archdesc/did/langmaterial')
262
+
263
+ to_field 'descrules_ssm', extract_xpath('/ead/eadheader/profiledesc/descrules')
264
+
265
+ # count all descendant components from the top-level
266
+ to_field 'total_component_count_is', first_only do |record, accumulator|
267
+ accumulator << record.xpath('//c|//c01|//c02|//c03|//c04|//c05|//c06|//c07|//c08|//c09|//c10|//c11|//c12').count
268
+ end
269
+
270
+ # count all digital objects from the top-level
271
+ to_field 'online_item_count_is', first_only do |record, accumulator|
272
+ accumulator << record.xpath('//dao').count
273
+ end
274
+
275
+ to_field 'component_level_isim' do |_record, accumulator, _context|
276
+ accumulator << 0
277
+ end
278
+
279
+ to_field 'sort_isi' do |_record, accumulator, _context|
280
+ accumulator << 0
281
+ end
282
+
283
+ # =============================
284
+ # Each component child document
285
+ # <c> <c01> <c12>
286
+ # =============================
287
+
288
+ to_field 'components' do |record, accumulator, context|
289
+ child_components = record.xpath("/ead/archdesc/dsc/c|#{('/ead/archdesc/dsc/c01'..'/ead/archdesc/dsc/c12').to_a.join('|')}")
290
+ next unless child_components.any?
291
+
292
+ counter = Class.new do
293
+ def increment
294
+ @counter ||= 0
295
+ @counter += 1
296
+ end
297
+ end.new
298
+
299
+ component_indexer = Traject::Indexer::NokogiriIndexer.new.tap do |i|
300
+ i.settings do
301
+ provide :parent, context
302
+ provide :root, context
303
+ provide :counter, counter
304
+ provide :depth, 1
305
+ provide :logger, context.settings[:logger]
306
+ provide :component_traject_config, context.settings[:component_traject_config]
307
+ end
308
+
309
+ i.load_config_file(context.settings[:component_traject_config])
310
+ end
311
+
312
+ child_components.each do |child_component|
313
+ output = component_indexer.map_record(child_component)
314
+ accumulator << output if output.keys.any?
315
+ end
316
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arclight
4
+ module Traject
5
+ # Provides a Traject Reader for XML Documents which removes the namespaces
6
+ class NokogiriNamespacelessReader < ::Traject::NokogiriReader
7
+ # Overrides the #each method (which is used for iterating through each Document)
8
+ # @param args
9
+ # @see ::Traject::NokogiriReader#each
10
+ # @see Enumerable#each
11
+ def each(*args)
12
+ return to_enum(:each, *args) unless block_given?
13
+
14
+ super do |doc|
15
+ new_doc = doc.dup
16
+ new_doc.remove_namespaces!
17
+ yield new_doc
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Arclight
2
- VERSION = '0.0.1'
4
+ VERSION = '1.6.2'
3
5
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arclight
4
+ # A range of years that handles gaps, such as [1999, 2000, 2002].
5
+ # Primary usage is:
6
+ # ```
7
+ # range = YearRange.new('1999/2004')
8
+ # range.years => [1999, 2000, 2001, 2002, 2003, 2004]
9
+ # range.to_s => '1999-2004'
10
+ # range << range.parse_ranges(['2010/2010'])
11
+ # range.years => [1999, 2000, 2001, 2002, 2003, 2004, 2010]
12
+ # range.to_s => '1999-2004, 2010'
13
+ # ```
14
+ class YearRange
15
+ attr_accessor :years
16
+
17
+ # @param [Array<String>] `dates` in the form YYYY/YYYY
18
+ def initialize(dates = nil)
19
+ @years = []
20
+ self << parse_ranges(dates) if dates.present?
21
+ end
22
+
23
+ # @return [String] a concise, human-readable version of the year range, including gaps
24
+ def to_s
25
+ return if years.empty?
26
+ return to_s_for_streak(years) unless gaps?
27
+
28
+ to_s_with_gaps
29
+ end
30
+
31
+ # @param [Array<Integer>] `other` the set of years to add
32
+ def <<(other)
33
+ return self if other.blank?
34
+
35
+ @years |= other # will remove duplicates
36
+ @years.sort!
37
+ self
38
+ end
39
+
40
+ # @param [String] `dates` in the form YYYY/YYYY
41
+ # @return [Array<Integer>] the set of years in the given range
42
+ def parse_range(dates)
43
+ return if dates.blank?
44
+
45
+ start_year, end_year = dates.split('/').map { |date| to_year_from_iso8601(date) }
46
+ return [start_year] if end_year.blank?
47
+ raise ArgumentError, "Range is too large: #{dates}" if (end_year - start_year) > 1000
48
+ raise ArgumentError, "Range is inverted: #{dates}" unless start_year <= end_year
49
+
50
+ (start_year..end_year).to_a
51
+ end
52
+
53
+ # @param [Array<String>] `dates` in the form YYYY/YYYY
54
+ # @return [Array<Integer>] the set of years in the given range
55
+ def parse_ranges(dates)
56
+ dates.map { |date| parse_range(date) }.flatten.sort.uniq
57
+ end
58
+
59
+ # @param [String] `date` a date in one of these formats:
60
+ # YYYY, YYYY-MM, YYYY-MM-DD, and YYYYMMDD
61
+ def to_year_from_iso8601(date)
62
+ return if date.blank?
63
+
64
+ date.split('-').first[0..3].to_i # Time.parse doesn't work here
65
+ end
66
+
67
+ # @return [Boolean] are there gaps between the years, such as 1999, 2000, 2002?
68
+ def gaps?
69
+ return false if years.blank?
70
+
71
+ (years.min..years.max).to_a != years
72
+ end
73
+
74
+ private
75
+
76
+ # Deals with making a human-readable range for years with 1 or more gaps.
77
+ # It involves detection of streaks between the gaps.
78
+ # @return [String] 1999-2000, 2002 for 1999, 2000, 2002
79
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
80
+ def to_s_with_gaps
81
+ raise ArgumentError if years.blank? || years.length < 2
82
+
83
+ results = []
84
+ streak = [years[0]]
85
+ i = streak.first
86
+ years[1..].each do |j|
87
+ i += 1
88
+ if i == j
89
+ streak << j
90
+ else # we have a gap
91
+ results << if streak.length == 1
92
+ streak.first.to_s
93
+ else
94
+ to_s_for_streak(streak)
95
+ end
96
+ streak = [j]
97
+ i = j
98
+ end
99
+ end
100
+ results << to_s_for_streak(streak)
101
+ results.join(', ')
102
+ end
103
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
104
+
105
+ def to_s_for_streak(streak)
106
+ return streak.min.to_s if streak.min == streak.max
107
+
108
+ streak.minmax.map(&:to_s).join('-')
109
+ end
110
+ end
111
+ end
data/lib/arclight.rb CHANGED
@@ -1,5 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'arclight/version'
4
+ require 'arclight/engine'
5
+ require 'arclight/repository'
6
+ require 'arclight/year_range'
2
7
 
8
+ # :nodoc:
3
9
  module Arclight
4
- # Your code goes here...
10
+ autoload :Routes, 'arclight/routes'
11
+
12
+ def self.deprecation
13
+ @deprecation ||= ActiveSupport::Deprecation.new('2.0', 'Arclight')
14
+ end
5
15
  end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module Arclight
6
+ ##
7
+ # Arclight install generator
8
+ class Install < Rails::Generators::Base # rubocop:disable Metrics/ClassLength
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ class_option :test, type: :boolean, default: false, aliases: '-t', desc: 'Indicates that app will be installed in a test environment'
12
+
13
+ def create_blacklight_catalog
14
+ remove_file 'app/controllers/catalog_controller.rb'
15
+ copy_file 'catalog_controller.rb', 'app/controllers/catalog_controller.rb'
16
+ end
17
+
18
+ def include_arclight_solrdocument
19
+ inject_into_file 'app/models/solr_document.rb', after: 'include Blacklight::Solr::Document' do
20
+ "\n include Arclight::SolrDocument"
21
+ end
22
+ end
23
+
24
+ def install_blacklight_locale_picker
25
+ gem 'blacklight-locale_picker'
26
+
27
+ Bundler.with_unbundled_env do
28
+ run 'bundle install'
29
+ end
30
+
31
+ generate 'blacklight:locale_picker:install'
32
+
33
+ inject_into_file 'app/helpers/application_helper.rb', after: 'include Blacklight::LocalePicker::LocaleHelper' do
34
+ "\n\n def additional_locale_routing_scopes\n [blacklight, arclight_engine]\n end"
35
+ end
36
+ end
37
+
38
+ def add_custom_routes
39
+ inject_into_file 'config/routes.rb', after: "mount Blacklight::Engine => '/'" do
40
+ "\n mount Arclight::Engine => '/'\n"
41
+ end
42
+
43
+ gsub_file 'config/routes.rb', 'root to: "catalog#index"', 'root to: "arclight/repositories#index"'
44
+ end
45
+
46
+ def add_frontend
47
+ if ENV['CI']
48
+ run "yarn add file:#{Arclight::Engine.root}"
49
+ elsif options[:test]
50
+ run 'yarn link arclight'
51
+
52
+ # If a branch was specified (e.g. you are running a template.rb build
53
+ # against a test branch), use the latest version available on npm
54
+ elsif ENV['BRANCH']
55
+ run 'yarn add arclight@latest'
56
+
57
+ # Otherwise, pick the version from npm that matches the Arclight
58
+ # gem version
59
+ else
60
+ run "yarn add arclight@#{arclight_yarn_version}"
61
+ end
62
+ end
63
+
64
+ def add_stylesheets
65
+ copy_file 'arclight.scss', 'app/assets/stylesheets/arclight.scss'
66
+ append_to_file 'app/assets/stylesheets/application.bootstrap.scss' do
67
+ <<~CONTENT
68
+ @import "arclight";
69
+ CONTENT
70
+ end
71
+ end
72
+
73
+ def add_arclight_search_behavior
74
+ inject_into_file 'app/models/search_builder.rb', after: 'include Blacklight::Solr::SearchBuilderBehavior' do
75
+ "\n include Arclight::SearchBehavior"
76
+ end
77
+ end
78
+
79
+ def solr_config
80
+ directory '../../../../solr', 'solr', force: true
81
+ end
82
+
83
+ def add_repository_config
84
+ copy_file 'config/repositories.yml' unless File.exist?('config/repositories.yml')
85
+ end
86
+
87
+ def add_download_config
88
+ copy_file 'config/downloads.yml' unless File.exist?('config/downloads.yml')
89
+ end
90
+
91
+ def add_i18n_config
92
+ copy_file 'config/locales/arclight.en.yml'
93
+ end
94
+
95
+ def modify_blacklight_yml
96
+ gsub_file 'config/locales/blacklight.en.yml', "application_name: 'Blacklight'", "application_name: 'Arclight'"
97
+ end
98
+
99
+ def assets
100
+ if using_importmap?
101
+ pin_javascript_dependencies
102
+ import_arclight_javascript
103
+ else
104
+ install_javascript_dependencies
105
+ end
106
+ end
107
+
108
+ def inject_arclight_routes
109
+ inject_into_file 'config/routes.rb',
110
+ " concern :hierarchy, Arclight::Routes::Hierarchy.new\n",
111
+ after: /concern :exportable.*\n/
112
+ inject_into_file 'config/routes.rb',
113
+ " concerns :hierarchy\n",
114
+ after: /resources :solr_documents.*\n/
115
+ end
116
+
117
+ private
118
+
119
+ def root
120
+ @root ||= Pathname(destination_root)
121
+ end
122
+
123
+ def using_importmap?
124
+ @using_importmap ||= root.join('config/importmap.rb').exist?
125
+ end
126
+
127
+ # This is the last step because any failure here wouldn't prevent the other steps from running
128
+ def install_javascript_dependencies
129
+ inject_into_file 'app/assets/javascripts/application.js', after: '//= require blacklight/blacklight' do
130
+ "\n// Required by Arclight" \
131
+ "\n//= require arclight/arclight"
132
+ end
133
+ end
134
+
135
+ def pin_javascript_dependencies
136
+ say 'Arclight Importmap asset generation'
137
+
138
+ append_to_file 'config/importmap.rb', <<~RUBY
139
+ pin "arclight", to: "arclight/arclight.js"
140
+ # TODO: We may be able to move these to a single importmap for arclight.
141
+ pin "arclight/oembed_controller", to: "arclight/oembed_controller.js"
142
+ pin "arclight/truncate_controller", to: "arclight/truncate_controller.js"
143
+ RUBY
144
+ end
145
+
146
+ def import_arclight_javascript
147
+ append_to_file 'app/javascript/application.js', "\nimport \"arclight\""
148
+ end
149
+
150
+ def package_yarn_version(package_name, requested_version)
151
+ versions = JSON.parse(`yarn info #{package_name} versions --json`)['data']
152
+ exact_match = versions.find { |v| v == requested_version }
153
+ return exact_match if exact_match
154
+
155
+ major_version = Gem::Version.new(requested_version).segments.first
156
+ "^#{major_version}"
157
+ end
158
+
159
+ def arclight_yarn_version
160
+ package_yarn_version('arclight', Arclight::VERSION)
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,6 @@
1
+ :root {
2
+ --bs-breadcrumb-divider: "»";
3
+ }
4
+
5
+ // Arclight, imported from arclight frontend package
6
+ @import 'arclight/app/assets/stylesheets/arclight/application';