arclight 0.0.1 → 1.4.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.
Files changed (202) 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 +44 -0
  6. data/.gitignore +7 -0
  7. data/.rspec +0 -1
  8. data/.rubocop.yml +362 -0
  9. data/.rubocop_todo.yml +122 -0
  10. data/.solr_wrapper +5 -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 +57 -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 +40 -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 +32 -0
  72. data/app/components/arclight/document_components_hierarchy_component.rb +28 -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/group_component.html.erb +34 -0
  78. data/app/components/arclight/group_component.rb +23 -0
  79. data/app/components/arclight/header_component.html.erb +5 -0
  80. data/app/components/arclight/header_component.rb +10 -0
  81. data/app/components/arclight/index_metadata_field_component.html.erb +16 -0
  82. data/app/components/arclight/index_metadata_field_component.rb +20 -0
  83. data/app/components/arclight/masthead_component.html.erb +15 -0
  84. data/app/components/arclight/masthead_component.rb +10 -0
  85. data/app/components/arclight/metadata_section_component.html.erb +13 -0
  86. data/app/components/arclight/metadata_section_component.rb +22 -0
  87. data/app/components/arclight/oembed_viewer_component.html.erb +5 -0
  88. data/app/components/arclight/oembed_viewer_component.rb +16 -0
  89. data/app/components/arclight/online_content_filter_component.html.erb +15 -0
  90. data/app/components/arclight/online_content_filter_component.rb +19 -0
  91. data/app/components/arclight/online_status_indicator_component.rb +19 -0
  92. data/app/components/arclight/repository_breadcrumb_component.html.erb +10 -0
  93. data/app/components/arclight/repository_breadcrumb_component.rb +17 -0
  94. data/app/components/arclight/repository_location_component.html.erb +20 -0
  95. data/app/components/arclight/repository_location_component.rb +10 -0
  96. data/app/components/arclight/search_bar_component.html.erb +27 -0
  97. data/app/components/arclight/search_bar_component.rb +32 -0
  98. data/app/components/arclight/search_result_breadcrumbs_component.html.erb +6 -0
  99. data/app/components/arclight/search_result_breadcrumbs_component.rb +37 -0
  100. data/app/components/arclight/search_result_component.html.erb +20 -0
  101. data/app/components/arclight/search_result_component.rb +18 -0
  102. data/app/components/arclight/search_result_title_component.html.erb +15 -0
  103. data/app/components/arclight/search_result_title_component.rb +15 -0
  104. data/app/components/arclight/sidebar_component.html.erb +9 -0
  105. data/app/components/arclight/sidebar_component.rb +19 -0
  106. data/app/components/arclight/upper_metadata_layout_component.rb +10 -0
  107. data/app/components/blacklight/icons/bookmark_component.rb +12 -0
  108. data/app/components/blacklight/icons/collection_component.rb +14 -0
  109. data/app/components/blacklight/icons/compact_component.rb +12 -0
  110. data/app/components/blacklight/icons/container_component.rb +14 -0
  111. data/app/components/blacklight/icons/ead_component.rb +12 -0
  112. data/app/components/blacklight/icons/file_component.rb +14 -0
  113. data/app/components/blacklight/icons/folder_component.rb +12 -0
  114. data/app/components/blacklight/icons/online_component.rb +15 -0
  115. data/app/components/blacklight/icons/pdf_component.rb +12 -0
  116. data/app/components/blacklight/icons/repository_component.rb +12 -0
  117. data/app/controllers/arclight/repositories_controller.rb +40 -0
  118. data/app/helpers/arclight/ead_format_helpers.rb +309 -0
  119. data/app/helpers/arclight/field_config_helpers.rb +26 -0
  120. data/app/helpers/arclight_helper.rb +108 -0
  121. data/app/models/arclight/document_downloads.rb +122 -0
  122. data/app/models/arclight/parent.rb +24 -0
  123. data/app/models/arclight/parents.rb +39 -0
  124. data/app/models/arclight/requests/aeon_external_request.rb +42 -0
  125. data/app/models/arclight/requests/aeon_web_ead.rb +52 -0
  126. data/app/models/arclight/requests/google_form.rb +49 -0
  127. data/app/models/concerns/arclight/catalog.rb +44 -0
  128. data/app/models/concerns/arclight/search_behavior.rb +47 -0
  129. data/app/models/concerns/arclight/solr_document.rb +159 -0
  130. data/app/presenters/arclight/index_presenter.rb +10 -0
  131. data/app/presenters/arclight/show_presenter.rb +54 -0
  132. data/app/views/arclight/.keep +0 -0
  133. data/app/views/arclight/_requests.html.erb +7 -0
  134. data/app/views/arclight/repositories/_repository.html.erb +43 -0
  135. data/app/views/arclight/repositories/index.html.erb +9 -0
  136. data/app/views/arclight/repositories/show.html.erb +37 -0
  137. data/app/views/arclight/requests/_aeon_external_request_endpoint.html.erb +8 -0
  138. data/app/views/arclight/requests/_aeon_web_ead.html.erb +6 -0
  139. data/app/views/arclight/requests/_google_form.html.erb +10 -0
  140. data/app/views/catalog/_document_list.html.erb +8 -0
  141. data/app/views/catalog/_group.html.erb +4 -0
  142. data/app/views/catalog/_group_toggle.html.erb +10 -0
  143. data/app/views/catalog/_search_results_header.html.erb +3 -0
  144. data/app/views/catalog/hierarchy.html.erb +19 -0
  145. data/app/views/catalog/index.html.erb +17 -0
  146. data/app/views/shared/_breadcrumbs.html.erb +3 -0
  147. data/app/views/shared/_main_menu_links.html.erb +6 -0
  148. data/arclight.gemspec +29 -11
  149. data/bin/rails +13 -0
  150. data/config/breadcrumbs.rb +24 -0
  151. data/config/i18n-tasks.yml +134 -0
  152. data/config/importmap.rb +3 -0
  153. data/config/locales/arclight.en.yml +100 -0
  154. data/config/repositories.yml +0 -0
  155. data/config/routes.rb +6 -0
  156. data/docker-compose.yml +16 -0
  157. data/lib/arclight/digital_object.rb +27 -0
  158. data/lib/arclight/engine.rb +47 -0
  159. data/lib/arclight/exceptions.rb +19 -0
  160. data/lib/arclight/hash_absolute_xpath.rb +62 -0
  161. data/lib/arclight/level_label.rb +46 -0
  162. data/lib/arclight/missing_id_strategy.rb +21 -0
  163. data/lib/arclight/normalized_date.rb +49 -0
  164. data/lib/arclight/normalized_id.rb +30 -0
  165. data/lib/arclight/normalized_title.rb +33 -0
  166. data/lib/arclight/repository.rb +116 -0
  167. data/lib/arclight/routes/hierarchy.rb +19 -0
  168. data/lib/arclight/routes.rb +8 -0
  169. data/lib/arclight/traject/ead2_component_config.rb +334 -0
  170. data/lib/arclight/traject/ead2_config.rb +315 -0
  171. data/lib/arclight/traject/nokogiri_namespaceless_reader.rb +22 -0
  172. data/lib/arclight/version.rb +3 -1
  173. data/lib/arclight/year_range.rb +111 -0
  174. data/lib/arclight.rb +11 -1
  175. data/lib/generators/arclight/install_generator.rb +126 -0
  176. data/lib/generators/arclight/templates/arclight.scss +5 -0
  177. data/lib/generators/arclight/templates/catalog_controller.rb +422 -0
  178. data/lib/generators/arclight/templates/config/downloads.yml +25 -0
  179. data/lib/generators/arclight/templates/config/locales/arclight.en.yml +61 -0
  180. data/lib/generators/arclight/templates/config/repositories.yml +53 -0
  181. data/lib/generators/arclight/update_generator.rb +22 -0
  182. data/lib/tasks/index.rake +84 -0
  183. data/package.json +30 -0
  184. data/solr/conf/_rest_managed.json +3 -0
  185. data/solr/conf/admin-extra.html +31 -0
  186. data/solr/conf/elevate.xml +36 -0
  187. data/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
  188. data/solr/conf/protwords.txt +21 -0
  189. data/solr/conf/schema.xml +400 -0
  190. data/solr/conf/solrconfig.xml +328 -0
  191. data/solr/conf/spellings.txt +2 -0
  192. data/solr/conf/stopwords.txt +58 -0
  193. data/solr/conf/stopwords_en.txt +58 -0
  194. data/solr/conf/synonyms.txt +31 -0
  195. data/solr/conf/xslt/example.xsl +132 -0
  196. data/solr/conf/xslt/example_atom.xsl +67 -0
  197. data/solr/conf/xslt/example_rss.xsl +66 -0
  198. data/solr/conf/xslt/luke.xsl +337 -0
  199. data/tasks/arclight.rake +72 -0
  200. data/template.rb +10 -0
  201. metadata +432 -20
  202. data/.travis.yml +0 -5
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arclight
4
+ ##
5
+ # Plain ruby class to model serializing/deserializing digital object data
6
+ class DigitalObject
7
+ attr_reader :label, :href
8
+
9
+ def initialize(label:, href:)
10
+ @label = label.presence || href
11
+ @href = href
12
+ end
13
+
14
+ def to_json(*)
15
+ { label: label, href: href }.to_json
16
+ end
17
+
18
+ def self.from_json(json)
19
+ object_data = JSON.parse(json)
20
+ new(label: object_data['label'], href: object_data['href'])
21
+ end
22
+
23
+ def ==(other)
24
+ href == other.href && label == other.label
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'blacklight'
4
+ require 'traject'
5
+ require 'active_model'
6
+ require 'arclight/exceptions'
7
+ require 'arclight/normalized_date'
8
+ require 'arclight/normalized_id'
9
+ require 'arclight/normalized_title'
10
+ require 'arclight/digital_object'
11
+ require 'gretel'
12
+
13
+ module Arclight
14
+ ##
15
+ # This is the defining class for the Arclight Rails Engine
16
+ class Engine < ::Rails::Engine
17
+ config.oembed_resource_exclude_patterns = [/\.pdf$/, /\.ppt$/]
18
+
19
+ Arclight::Engine.config.catalog_controller_group_query_params = {
20
+ group: true,
21
+ 'group.field': '_root_',
22
+ 'group.ngroups': true,
23
+ 'group.limit': 3,
24
+ fl: '*,collection:[subquery]',
25
+ 'collection.q': '{!terms f=id v=$row._root_}',
26
+ 'collection.defType': 'lucene',
27
+ 'collection.fl': '*',
28
+ 'collection.rows': 1
29
+ }
30
+
31
+ initializer 'arclight.helpers' do
32
+ config.after_initialize do
33
+ ActiveSupport.on_load(:action_view) { include ArclightHelper }
34
+ end
35
+ end
36
+
37
+ initializer 'arclight.assets', before: 'assets' do |app|
38
+ app.config.assets.precompile << 'arclight/arclight.js'
39
+ app.config.assets.precompile << 'arclight/oembed_controller.js'
40
+ app.config.assets.precompile << 'arclight/truncate_controller.js'
41
+ end
42
+
43
+ initializer 'arclight.importmap', before: 'importmap' do |app|
44
+ app.config.importmap.paths << Engine.root.join('config/importmap.rb') if app.config.respond_to?(:importmap)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arclight
4
+ module Exceptions
5
+ # Id's must be present on all documents and components
6
+ class IDNotFound < StandardError
7
+ def message
8
+ 'id must be present for all documents and components'
9
+ end
10
+ end
11
+
12
+ # Unittitle or unitdate must be present on all documents and components
13
+ class TitleNotFound < StandardError
14
+ def message
15
+ '<unittitle/> or <unitdate/> must be present for all documents and components'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Arclight
6
+ ##
7
+ # Take a Nokogiri node and get its absolute path (inserting our own indexes for component levels)
8
+ # and hash that outout. This is intended as a potential strategy for handling missing IDs in EADs.
9
+ class HashAbsoluteXpath
10
+ class << self
11
+ attr_writer :hash_algorithm
12
+
13
+ def hash_algorithm
14
+ return Digest::SHA1 unless defined? @hash_algorithm
15
+
16
+ @hash_algorithm
17
+ end
18
+ end
19
+
20
+ COMPONENT_NODE_NAME_REGEX = /^c\d{,2}$/
21
+ attr_reader :node
22
+
23
+ def initialize(node)
24
+ @node = node
25
+ end
26
+
27
+ def to_hexdigest
28
+ self.class.hash_algorithm.hexdigest(absolute_xpath).prepend('al_')
29
+ end
30
+
31
+ def absolute_xpath
32
+ ancestor_tree = node.ancestors.map do |ancestor|
33
+ ancestor_name_and_index(ancestor)
34
+ end
35
+
36
+ "#{[ancestor_tree.reverse, node.name].flatten.join('/')}#{current_index}"
37
+ end
38
+
39
+ private
40
+
41
+ def current_index
42
+ siblings.index(node)
43
+ end
44
+
45
+ def component_siblings_for_node(xml_node)
46
+ xml_node.parent.children.select { |n| n.name =~ COMPONENT_NODE_NAME_REGEX }
47
+ end
48
+
49
+ def siblings
50
+ @siblings ||= component_siblings_for_node(node)
51
+ end
52
+
53
+ def ancestor_name_and_index(ancestor)
54
+ if ancestor.name =~ COMPONENT_NODE_NAME_REGEX
55
+ index = component_siblings_for_node(ancestor).index(ancestor)
56
+ "#{ancestor.name}#{index}"
57
+ else
58
+ ancestor.name
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arclight
4
+ ##
5
+ # A utility class to return a human-readable label for an EAD @level code.
6
+ # Can use the value from @otherlevel if provided.
7
+ # Examples from @level: recordgrp = "Record Group"
8
+ # collection = "Collection"
9
+ # subseries = "Subseries"
10
+ # otherlevel = (text provided in @otherlevel)
11
+ class LevelLabel
12
+ # @param [String] `level` from the collection or component @level
13
+ # @param [String] `other_level` from the collection or component @otherlevel
14
+ def initialize(level, other_level = nil)
15
+ @level = level
16
+ @other_level = other_level if other_level.present?
17
+ end
18
+
19
+ # @return [String] the human-readable label
20
+ def to_s
21
+ human_readable_level
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :level, :other_level
27
+
28
+ CUSTOM_LEVEL_LABELS = {
29
+ recordgrp: 'Record Group',
30
+ subgrp: 'Subgroup'
31
+ }.freeze
32
+
33
+ def human_readable_level
34
+ if level == 'otherlevel'
35
+ alternative_level
36
+ elsif level.present?
37
+ CUSTOM_LEVEL_LABELS.fetch(level.to_sym, level.capitalize).to_s
38
+ end
39
+ end
40
+
41
+ def alternative_level
42
+ alternative_level = other_level if other_level
43
+ alternative_level.present? ? alternative_level.capitalize : 'Other'
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'arclight/hash_absolute_xpath'
4
+
5
+ module Arclight
6
+ ##
7
+ # A class to configure a selected MissingIdStrategy.
8
+ # Defaults to Arclight::HashAbsoluteXpath
9
+ # This can be updated in an initializer to be any other class
10
+ class MissingIdStrategy
11
+ class << self
12
+ attr_writer :selected
13
+
14
+ def selected
15
+ return Arclight::HashAbsoluteXpath unless defined? @selected
16
+
17
+ @selected
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arclight
4
+ ##
5
+ # A utility class to normalize dates, typically by joining inclusive and bulk dates
6
+ # e.g., "1990-2000, bulk 1990-1999"
7
+ # @see http://www2.archivists.org/standards/DACS/part_I/chapter_2/4_date
8
+ class NormalizedDate
9
+ # @param [String | Array<String>] `inclusive` from the `unitdate`
10
+ # @param [Array<String>] `bulk` from the `unitdate`
11
+ # @param [Array<String>] `other` from the `unitdate` when type is not specified
12
+ def initialize(inclusive, bulk = [], other = [])
13
+ @inclusive = (inclusive || []).map do |inclusive_text|
14
+ if inclusive_text.is_a? Array # of YYYY-YYYY for ranges
15
+ # NOTE: This code is not routable AFAICT in actual indexing.
16
+ # We pass arrays of strings (or xml nodes) here, and never a multidimensional array
17
+ year_range(inclusive_text)
18
+ elsif inclusive_text.present?
19
+ inclusive_text.strip
20
+ end
21
+ end&.join(', ')
22
+
23
+ @bulk = Array.wrap(bulk).compact.map(&:strip).join(', ')
24
+ @other = Array.wrap(other).compact.map(&:strip).join(', ')
25
+ end
26
+
27
+ # @return [String] the normalized title/date
28
+ def to_s
29
+ normalize
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :inclusive, :bulk, :other
35
+
36
+ def year_range(date_array)
37
+ YearRange.new(date_array.include?('/') ? date_array : date_array.map { |v| v.tr('-', '/') }).to_s
38
+ end
39
+
40
+ # @see http://www2.archivists.org/standards/DACS/part_I/chapter_2/4_date for rules
41
+ def normalize
42
+ result = []
43
+ result << inclusive if inclusive.present?
44
+ result << other if other.present?
45
+ result << "bulk #{bulk}" if bulk.present?
46
+ result.compact.map(&:strip).join(', ').presence
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'arclight/exceptions'
4
+
5
+ module Arclight
6
+ ##
7
+ # A simple utility class to normalize identifiers
8
+ class NormalizedId
9
+ # Accepts unused kwargs from the ead2_config.rb id to_field directive
10
+ # (:title and :repository) so that applications can provide a custom
11
+ # id_normalizer class to traject to form the collection id from these attributes.
12
+ def initialize(id, **_kwargs)
13
+ @id = id
14
+ end
15
+
16
+ def to_s
17
+ normalize
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :id
23
+
24
+ def normalize
25
+ raise Arclight::Exceptions::IDNotFound if id.blank?
26
+
27
+ id.strip.tr('.', '-')
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'arclight/exceptions'
4
+
5
+ module Arclight
6
+ ##
7
+ # A utility class to normalize titles, typically by joining
8
+ # the title and date, e.g., "My Title, 1990-2000"
9
+ class NormalizedTitle
10
+ # @param [String] `title` from the `unittitle`
11
+ # @param [String] `date` from the `unitdate`
12
+ def initialize(title, date = nil)
13
+ @title = title.gsub(/\s*,\s*$/, '').strip if title.present?
14
+ @date = date.strip if date.present?
15
+ end
16
+
17
+ # @return [String] the normalized title/date
18
+ def to_s
19
+ normalize
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :title, :date, :default
25
+
26
+ def normalize
27
+ result = [title, date].compact.join(', ')
28
+ raise Arclight::Exceptions::TitleNotFound if result.blank?
29
+
30
+ result
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require 'active_support'
5
+ require 'active_support/core_ext/hash/indifferent_access'
6
+
7
+ module Arclight
8
+ # Static information about a given repository identified by a unique `slug`.
9
+ # These data are loaded from config/repositories.yml
10
+ class Repository
11
+ include ActiveModel::Model
12
+ DEFAULTS = {
13
+ request_types: {},
14
+ contact_html: '',
15
+ location_html: '',
16
+ visit_note: nil
17
+ }.freeze
18
+
19
+ def initialize(attributes = {})
20
+ @attributes = DEFAULTS.merge(attributes).with_indifferent_access
21
+ end
22
+
23
+ attr_reader :attributes
24
+ attr_accessor :collection_count
25
+
26
+ def method_missing(field, *args, &block)
27
+ return attributes[field] if attributes.include?(field)
28
+
29
+ super
30
+ end
31
+
32
+ def respond_to_missing?(field, *args)
33
+ attributes.include?(field) || super
34
+ end
35
+
36
+ # rubocop:disable Rails/OutputSafety
37
+ def contact
38
+ contact_html.html_safe
39
+ end
40
+
41
+ def location
42
+ location_html.html_safe
43
+ end
44
+ # rubocop:enable Rails/OutputSafety
45
+
46
+ def request_config_present?
47
+ request_configs = request_types.values || []
48
+ request_configs.dig(0, 'request_url').present? &&
49
+ request_configs.dig(0, 'request_mappings').present?
50
+ end
51
+
52
+ def request_config_present_for_type?(type)
53
+ config = request_config_for_type(type)
54
+
55
+ config['request_url'].present? &&
56
+ config['request_mappings'].present?
57
+ end
58
+
59
+ def request_config_for_type(type)
60
+ request_types.fetch(type, {})
61
+ end
62
+
63
+ def available_request_types
64
+ return [] unless request_types.present?
65
+
66
+ request_types.keys
67
+ end
68
+
69
+ # Load repository information from a YAML file
70
+ #
71
+ # @param [String] `filename`
72
+ # @return [Hash<Slug,Repository>]
73
+ def self.from_yaml(file)
74
+ repos = {}
75
+ data = YAML.safe_load(File.read(file))
76
+ data.each_key do |slug|
77
+ repos[slug] = new(data[slug].merge(slug: slug))
78
+ end
79
+ repos
80
+ end
81
+
82
+ # Mimics ActiveRecord's `all` behavior
83
+ #
84
+ # @return [Array<Repository>]
85
+ def self.all(yaml_file = nil)
86
+ yaml_file = ENV['REPOSITORY_FILE'] || 'config/repositories.yml' if yaml_file.nil?
87
+ from_yaml(yaml_file).values
88
+ end
89
+
90
+ # Mimics ActiveRecord dynamic `find_by` behavior for the slug or name
91
+ #
92
+ # @param [String] `slug` or `name`
93
+ # @return [Repository]
94
+ def self.find_by(slug: nil, name: nil, yaml_file: nil)
95
+ if slug
96
+ all(yaml_file).find { |repo| repo.slug == slug }
97
+ elsif name
98
+ all(yaml_file).find { |repo| repo.name == name }
99
+ else
100
+ raise ArgumentError, 'Requires either slug or name parameters to find_by'
101
+ end
102
+ end
103
+
104
+ # Mimics ActiveRecord dynamic `find_by!` behavior for the slug or name
105
+ #
106
+ # @param [String] `slug` or `name` -- same as `find_by`
107
+ # @return [Repository]
108
+ # @raise [ActiveRecord::RecordNotFound] if cannot find repository
109
+ def self.find_by!(**kwargs)
110
+ repository = find_by(**kwargs)
111
+ raise ActiveRecord::RecordNotFound if repository.blank?
112
+
113
+ repository
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arclight
4
+ module Routes
5
+ # Inject a hierarchy route for displaying the
6
+ # components in the collection context
7
+ class Hierarchy
8
+ def initialize(defaults = {})
9
+ @defaults = defaults
10
+ end
11
+
12
+ def call(mapper, _options = {})
13
+ mapper.member do
14
+ mapper.get 'hierarchy'
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arclight
4
+ # Custom routes for Arclight
5
+ module Routes
6
+ require 'arclight/routes/hierarchy'
7
+ end
8
+ end