blacklight-spotlight 3.0.0.alpha.8 → 3.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (206) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/app/assets/images/blacklight/arrow-alt-circle-left.svg +1 -0
  4. data/app/assets/images/blacklight/arrow-alt-circle-right.svg +1 -0
  5. data/app/assets/javascripts/spotlight/admin/{add_new_page_button.js → add_new_button.js} +7 -0
  6. data/app/assets/javascripts/spotlight/admin/block_mixins/autocompleteable.js +4 -4
  7. data/app/assets/javascripts/spotlight/admin/blocks/browse_block.js +55 -1
  8. data/app/assets/javascripts/spotlight/admin/blocks/browse_group_categories_block.js +88 -0
  9. data/app/assets/javascripts/spotlight/admin/blocks/pages_block.js +1 -1
  10. data/app/assets/javascripts/spotlight/admin/blocks/solr_documents_base_block.js +1 -1
  11. data/app/assets/javascripts/spotlight/admin/blocks/uploaded_items_block.js +7 -2
  12. data/app/assets/javascripts/spotlight/admin/crop.es6 +11 -0
  13. data/app/assets/javascripts/spotlight/admin/croppable.js +1 -1
  14. data/app/assets/javascripts/spotlight/admin/index.js +0 -2
  15. data/app/assets/javascripts/spotlight/admin/search_typeahead.js +2 -2
  16. data/app/assets/javascripts/spotlight/admin/sir-trevor/block_controls.js +21 -12
  17. data/app/assets/javascripts/spotlight/admin/sir-trevor/locales.js +11 -3
  18. data/app/assets/javascripts/spotlight/user/browse_group_categories.js +59 -0
  19. data/app/assets/javascripts/spotlight/user/index.js +1 -0
  20. data/app/assets/stylesheets/spotlight/_accessibility.scss +1 -1
  21. data/app/assets/stylesheets/spotlight/_breadcrumbs.scss +8 -0
  22. data/app/assets/stylesheets/spotlight/_browse.scss +16 -0
  23. data/app/assets/stylesheets/spotlight/_catalog.scss +6 -6
  24. data/app/assets/stylesheets/spotlight/_curation.scss +6 -0
  25. data/app/assets/stylesheets/spotlight/_featured_browse_categories_block.scss +214 -83
  26. data/app/assets/stylesheets/spotlight/_header.scss +1 -1
  27. data/app/assets/stylesheets/spotlight/_item_text_block.scss +6 -0
  28. data/app/assets/stylesheets/spotlight/_pages.scss +10 -5
  29. data/app/assets/stylesheets/spotlight/_report_a_problem.scss +5 -2
  30. data/app/assets/stylesheets/spotlight/_spotlight.scss +2 -0
  31. data/app/assets/stylesheets/spotlight/_translations.scss +7 -0
  32. data/app/assets/stylesheets/spotlight/browse_group_categories_block.scss +69 -0
  33. data/app/builders/spotlight/bootstrap_breadcrumbs_builder.rb +1 -2
  34. data/app/controllers/concerns/spotlight/search_helper.rb +2 -8
  35. data/app/controllers/spotlight/appearances_controller.rb +0 -12
  36. data/app/controllers/spotlight/browse_controller.rb +7 -3
  37. data/app/controllers/spotlight/catalog_controller.rb +1 -1
  38. data/app/controllers/spotlight/concerns/application_controller.rb +13 -2
  39. data/app/controllers/spotlight/exhibits_controller.rb +2 -3
  40. data/app/controllers/spotlight/featured_images_controller.rb +1 -1
  41. data/app/controllers/spotlight/groups_controller.rb +80 -0
  42. data/app/controllers/spotlight/pages_controller.rb +6 -9
  43. data/app/controllers/spotlight/resources/csv_upload_controller.rb +1 -1
  44. data/app/controllers/spotlight/searches_controller.rb +7 -19
  45. data/app/controllers/spotlight/translations_controller.rb +46 -0
  46. data/app/helpers/spotlight/application_helper.rb +1 -1
  47. data/app/helpers/spotlight/crop_helper.rb +4 -1
  48. data/app/helpers/spotlight/crud_link_helpers.rb +1 -1
  49. data/app/helpers/spotlight/main_app_helpers.rb +1 -1
  50. data/app/jobs/spotlight/add_uploads_from_csv.rb +30 -5
  51. data/app/mailers/spotlight/indexing_complete_mailer.rb +3 -2
  52. data/app/models/concerns/spotlight/exhibit_defaults.rb +1 -1
  53. data/app/models/concerns/spotlight/translatables.rb +17 -1
  54. data/app/models/sir_trevor_rails/blocks/browse_group_categories_block.rb +25 -0
  55. data/app/models/spotlight/ability.rb +2 -0
  56. data/app/models/spotlight/about_page.rb +3 -1
  57. data/app/models/spotlight/blacklight_configuration.rb +2 -1
  58. data/app/models/spotlight/contact.rb +1 -1
  59. data/app/models/spotlight/custom_field.rb +3 -3
  60. data/app/models/spotlight/exhibit.rb +18 -4
  61. data/app/models/spotlight/feature_page.rb +3 -1
  62. data/app/models/spotlight/featured_image.rb +29 -12
  63. data/app/models/spotlight/group.rb +22 -0
  64. data/app/models/spotlight/group_member.rb +11 -0
  65. data/app/models/spotlight/home_page.rb +3 -1
  66. data/app/models/spotlight/main_navigation.rb +2 -2
  67. data/app/models/spotlight/masthead.rb +1 -1
  68. data/app/models/spotlight/page.rb +5 -1
  69. data/app/models/spotlight/page_configurations.rb +6 -0
  70. data/app/models/spotlight/page_content.rb +2 -0
  71. data/app/models/spotlight/resources/csv_upload.rb +2 -1
  72. data/app/models/spotlight/resources/iiif_manifest.rb +8 -6
  73. data/app/models/spotlight/resources/upload.rb +1 -1
  74. data/app/models/spotlight/search.rb +10 -1
  75. data/app/models/spotlight/solr_document_sidecar.rb +7 -5
  76. data/app/models/spotlight/temporary_image.rb +8 -0
  77. data/app/services/spotlight/exhibit_import_export_service.rb +482 -0
  78. data/app/services/spotlight/solr_document_builder.rb +1 -0
  79. data/app/values/custom_field_name.rb +1 -0
  80. data/app/views/catalog/_save_search.html.erb +1 -1
  81. data/app/views/spotlight/about_pages/_empty.html.erb +5 -5
  82. data/app/views/spotlight/browse/_search.html.erb +5 -3
  83. data/app/views/spotlight/browse/_search_title.html.erb +2 -1
  84. data/app/views/spotlight/browse/index.html.erb +13 -0
  85. data/app/views/spotlight/catalog/_document.html.erb +2 -4
  86. data/app/views/spotlight/catalog/index.iiif_json.jbuilder +22 -0
  87. data/app/views/spotlight/contacts/_form.html.erb +1 -1
  88. data/app/views/spotlight/feature_pages/_empty.html.erb +5 -5
  89. data/app/views/spotlight/featured_images/_form.html.erb +1 -1
  90. data/app/views/spotlight/featured_images/_upload_form.html.erb +1 -1
  91. data/app/views/spotlight/home_pages/_empty.html.erb +3 -3
  92. data/app/views/spotlight/indexing_complete_mailer/documents_indexed.html.erb +9 -0
  93. data/app/views/spotlight/pages/_form.html.erb +2 -2
  94. data/app/views/spotlight/searches/_form.html.erb +13 -0
  95. data/app/views/spotlight/searches/_group.html.erb +27 -0
  96. data/app/views/spotlight/searches/_search.html.erb +1 -0
  97. data/app/views/spotlight/searches/index.html.erb +58 -17
  98. data/app/views/spotlight/shared/_honeypot_field.html.erb +4 -0
  99. data/app/views/spotlight/shared/_report_a_problem.html.erb +7 -10
  100. data/app/views/spotlight/sir_trevor/blocks/_browse_block.html.erb +1 -0
  101. data/app/views/spotlight/sir_trevor/blocks/_browse_group_categories_block.html.erb +44 -0
  102. data/app/views/spotlight/sir_trevor/blocks/_uploaded_items_block.html.erb +7 -1
  103. data/app/views/spotlight/translations/_browse_categories.html.erb +29 -3
  104. data/app/views/spotlight/translations/_general.html.erb +7 -7
  105. data/app/views/spotlight/translations/_groups.html.erb +34 -0
  106. data/app/views/spotlight/translations/_import.html.erb +24 -0
  107. data/app/views/spotlight/translations/_metadata.html.erb +1 -1
  108. data/app/views/spotlight/translations/_page.html.erb +5 -5
  109. data/app/views/spotlight/translations/_pages.html.erb +4 -4
  110. data/app/views/spotlight/translations/_pages_table.html.erb +5 -5
  111. data/app/views/spotlight/translations/_search_fields.html.erb +3 -3
  112. data/app/views/spotlight/translations/edit.html.erb +14 -6
  113. data/app/views/spotlight/translations/show.yaml.yamlbuilder +81 -0
  114. data/config/i18n-tasks.yml +7 -0
  115. data/config/locales/spotlight.ar.yml +47 -24
  116. data/config/locales/spotlight.en.yml +182 -127
  117. data/config/routes.rb +16 -1
  118. data/db/migrate/20200403161512_add_subtitle_to_searches.rb +7 -0
  119. data/db/migrate/20210113092223_create_spotlight_groups.rb +23 -0
  120. data/lib/generators/spotlight/install_generator.rb +23 -2
  121. data/lib/generators/spotlight/templates/config/initializers/sir_trevor_rails.rb +10 -0
  122. data/lib/generators/spotlight/templates/config/initializers/spotlight_initializer.rb +3 -1
  123. data/lib/spotlight/engine.rb +27 -3
  124. data/lib/spotlight/upload_field_config.rb +1 -0
  125. data/lib/spotlight/version.rb +1 -1
  126. data/spec/controllers/spotlight/about_pages_controller_spec.rb +3 -3
  127. data/spec/controllers/spotlight/browse_controller_spec.rb +24 -1
  128. data/spec/controllers/spotlight/catalog_controller_spec.rb +1 -1
  129. data/spec/controllers/spotlight/contacts_controller_spec.rb +2 -2
  130. data/spec/controllers/spotlight/feature_pages_controller_spec.rb +11 -0
  131. data/spec/controllers/spotlight/groups_controller_spec.rb +103 -0
  132. data/spec/controllers/spotlight/home_pages_controller_spec.rb +2 -2
  133. data/spec/controllers/spotlight/resources/csv_upload_controller_spec.rb +4 -4
  134. data/spec/controllers/spotlight/resources/upload_controller_spec.rb +2 -2
  135. data/spec/controllers/spotlight/searches_controller_spec.rb +10 -3
  136. data/spec/controllers/spotlight/translations_controller_spec.rb +53 -2
  137. data/spec/controllers/spotlight/view_configurations_controller_spec.rb +1 -1
  138. data/spec/examples.txt +1437 -1389
  139. data/spec/factories/featured_images.rb +4 -0
  140. data/spec/factories/group.rb +17 -0
  141. data/spec/factories/searches.rb +11 -1
  142. data/spec/features/add_contacts_spec.rb +1 -1
  143. data/spec/features/browse_category_admin_spec.rb +39 -7
  144. data/spec/features/browse_category_navigation_spec.rb +44 -0
  145. data/spec/features/browse_category_spec.rb +2 -2
  146. data/spec/features/catalog_spec.rb +2 -2
  147. data/spec/features/create_exhibit_spec.rb +5 -4
  148. data/spec/features/dashboard_spec.rb +7 -7
  149. data/spec/features/edit_search_fields_spec.rb +2 -2
  150. data/spec/features/exhibits/administration_spec.rb +3 -3
  151. data/spec/features/exhibits/edit_metadata_fields_spec.rb +1 -1
  152. data/spec/features/exhibits/language_create_edit_spec.rb +3 -3
  153. data/spec/features/exhibits/translation_editing_spec.rb +57 -8
  154. data/spec/features/home_page_spec.rb +13 -4
  155. data/spec/features/item_admin_spec.rb +4 -4
  156. data/spec/features/javascript/about_page_admin_spec.rb +1 -1
  157. data/spec/features/javascript/block_controls_spec.rb +3 -1
  158. data/spec/features/javascript/blocks/browse_group_categories_block_spec.rb +64 -0
  159. data/spec/features/javascript/blocks/uploaded_items_block_spec.rb +4 -1
  160. data/spec/features/javascript/browse_group_admin_spec.rb +45 -0
  161. data/spec/features/javascript/edit_in_place_spec.rb +3 -3
  162. data/spec/features/javascript/feature_page_admin_spec.rb +1 -1
  163. data/spec/features/javascript/search_config_admin_spec.rb +1 -1
  164. data/spec/features/report_a_problem_spec.rb +6 -5
  165. data/spec/helpers/spotlight/crud_link_helpers_spec.rb +3 -3
  166. data/spec/helpers/spotlight/pages_helper_spec.rb +2 -2
  167. data/spec/i18n_spec.rb +0 -2
  168. data/spec/jobs/spotlight/add_uploads_from_csv_spec.rb +13 -1
  169. data/spec/mailers/spotlight/indexing_complete_mailer_spec.rb +11 -1
  170. data/spec/models/sir_trevor_rails/blocks/browse_group_categories_block_spec.rb +41 -0
  171. data/spec/models/solr_document_spec.rb +2 -3
  172. data/spec/models/spotlight/access_controls_enforcement_search_builder_spec.rb +1 -0
  173. data/spec/models/spotlight/exhibit_spec.rb +18 -2
  174. data/spec/models/spotlight/featured_image_spec.rb +27 -0
  175. data/spec/models/spotlight/group_spec.rb +19 -0
  176. data/spec/models/spotlight/main_navigation_spec.rb +1 -1
  177. data/spec/models/spotlight/page_spec.rb +6 -1
  178. data/spec/models/spotlight/resources/upload_spec.rb +43 -79
  179. data/spec/models/spotlight/role_spec.rb +3 -3
  180. data/spec/models/spotlight/search_spec.rb +30 -3
  181. data/spec/{serializers/spotlight/exhibit_export_serializer_spec.rb → services/spotlight/exhibit_import_export_service_spec.rb} +168 -23
  182. data/spec/services/spotlight/iiif_resource_resolver_spec.rb +1 -1
  183. data/spec/services/spotlight/solr_document_builder_spec.rb +1 -1
  184. data/spec/spec_helper.rb +1 -1
  185. data/spec/support/features/test_features_helpers.rb +15 -0
  186. data/spec/test_app_templates/Gemfile.extra +1 -3
  187. data/spec/test_app_templates/catalog_controller.rb +6 -3
  188. data/spec/test_app_templates/lib/generators/test_app_generator.rb +1 -1
  189. data/spec/views/shared/_exhibit_navbar.html.erb_spec.rb +1 -1
  190. data/spec/views/spotlight/browse/index.html.erb_spec.rb +2 -0
  191. data/spec/views/spotlight/dashboards/_analytics.html.erb_spec.rb +1 -1
  192. data/spec/views/spotlight/dashboards/_reindexing_activity.html.erb_spec.rb +6 -6
  193. data/spec/views/spotlight/metadata_configurations/_metadata_field.html.erb_spec.rb +3 -3
  194. data/spec/views/spotlight/pages/show.html.erb_spec.rb +1 -0
  195. data/spec/views/spotlight/search_configurations/_facets.html.erb_spec.rb +1 -1
  196. data/spec/views/spotlight/search_configurations/_sort.html.erb_spec.rb +7 -8
  197. data/spec/views/spotlight/translations/_import.html.erb_spec.rb +24 -0
  198. data/vendor/assets/javascripts/leaflet-iiif.js +46 -21
  199. data/vendor/assets/javascripts/tiny-slider.js +3218 -0
  200. data/vendor/assets/stylesheets/tiny-slider.css +1 -0
  201. metadata +399 -284
  202. data/app/serializers/spotlight/exhibit_export_serializer.rb +0 -205
  203. data/app/serializers/spotlight/featured_image_representer.rb +0 -29
  204. data/app/serializers/spotlight/main_navigation_representer.rb +0 -13
  205. data/app/serializers/spotlight/page_representer.rb +0 -33
  206. data/vendor/assets/javascripts/handlebars-v1.3.0.js +0 -2746
@@ -5,7 +5,9 @@ module Spotlight
5
5
  # Feature pages
6
6
  class FeaturePage < Spotlight::Page
7
7
  extend FriendlyId
8
- friendly_id :title, use: %i[slugged scoped finders history], scope: %i[exhibit locale]
8
+ friendly_id :title, use: %i[slugged scoped finders history], scope: %i[exhibit locale] do |config|
9
+ config.reserved_words&.concat(%w[update_all])
10
+ end
9
11
 
10
12
  has_many :child_pages, class_name: 'Spotlight::FeaturePage', inverse_of: :parent_page, foreign_key: 'parent_page_id'
11
13
  belongs_to :parent_page, class_name: 'Spotlight::FeaturePage', optional: true
@@ -6,6 +6,24 @@ module Spotlight
6
6
  class FeaturedImage < ActiveRecord::Base
7
7
  mount_uploader :image, Spotlight::FeaturedImageUploader
8
8
 
9
+ before_validation do
10
+ next unless upload_id.present? && source == 'remote'
11
+
12
+ # copy the image from the temp upload
13
+ temp_image = Spotlight::TemporaryImage.find(upload_id)
14
+ self.image = CarrierWave::SanitizedFile.new tempfile: StringIO.new(temp_image.image.read),
15
+ filename: temp_image.image.filename || temp_image.image.identifier,
16
+ content_type: temp_image.image.content_type
17
+
18
+ # Unset the incoming iiif_tilesource, which points at the temp image
19
+ self.iiif_tilesource = nil
20
+ end
21
+
22
+ after_commit do
23
+ # Clean up the temporary image
24
+ Spotlight::TemporaryImage.find(upload_id).delete if upload_id.present?
25
+ end
26
+
9
27
  after_save do
10
28
  if image.present?
11
29
  image.cache! unless image.cached?
@@ -13,7 +31,7 @@ module Spotlight
13
31
  end
14
32
  end
15
33
 
16
- after_create :set_tilesource_from_uploaded_resource
34
+ attr_accessor :upload_id
17
35
 
18
36
  def iiif_url
19
37
  return unless iiif_service_base.present?
@@ -45,24 +63,23 @@ module Spotlight
45
63
  image.file.present?
46
64
  end
47
65
 
48
- private
49
-
50
- def set_tilesource_from_uploaded_resource
51
- return if iiif_tilesource
52
-
53
- riiif = Riiif::Engine.routes.url_helpers
54
- self.iiif_tilesource = riiif.info_path(id)
55
- save
66
+ def iiif_tilesource
67
+ if self[:iiif_tilesource]
68
+ self[:iiif_tilesource]
69
+ elsif file_present?
70
+ riiif = Riiif::Engine.routes.url_helpers
71
+ riiif.info_path(id)
72
+ end
56
73
  end
57
74
 
75
+ private
76
+
58
77
  def image_size
59
78
  Spotlight::Engine.config.featured_image_thumb_size
60
79
  end
61
80
 
62
81
  def iiif_service_base
63
- return unless iiif_tilesource
64
-
65
- iiif_tilesource.sub('/info.json', '')
82
+ iiif_tilesource&.sub('/info.json', '')
66
83
  end
67
84
  end
68
85
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ ##
5
+ # Exhibit saved searches
6
+ class Group < ActiveRecord::Base
7
+ include Spotlight::Translatables
8
+
9
+ extend FriendlyId
10
+ friendly_id :title, use: %i[slugged scoped finders history], scope: [:exhibit]
11
+ translates :title
12
+
13
+ self.table_name = 'spotlight_groups'
14
+ belongs_to :exhibit
15
+ has_many :group_members
16
+ has_many :searches, through: :group_members, source: :member, source_type: 'Spotlight::Search'
17
+ default_scope { order('weight ASC') }
18
+ scope :published, -> { where(published: true) }
19
+ accepts_nested_attributes_for :group_members
20
+ accepts_nested_attributes_for :searches
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ ##
5
+ # Exhibit saved searches
6
+ class GroupMember < ActiveRecord::Base
7
+ self.table_name = 'spotlight_groups_members'
8
+ belongs_to :group
9
+ belongs_to :member, polymorphic: true
10
+ end
11
+ end
@@ -5,7 +5,9 @@ module Spotlight
5
5
  # Exhibit home page
6
6
  class HomePage < Spotlight::Page
7
7
  extend FriendlyId
8
- friendly_id :title, use: %i[slugged scoped finders], scope: %i[exhibit locale]
8
+ friendly_id :title, use: %i[slugged scoped finders history], scope: %i[exhibit locale] do |config|
9
+ config.reserved_words&.concat(%w[update_all])
10
+ end
9
11
 
10
12
  before_save :publish
11
13
  before_create :default_content
@@ -26,8 +26,8 @@ module Spotlight
26
26
  end
27
27
  end
28
28
 
29
- def default_label
30
- I18n.t(:"spotlight.main_navigation.#{nav_type}")
29
+ def default_label(**options)
30
+ I18n.t(:"spotlight.main_navigation.#{nav_type}", **options)
31
31
  end
32
32
 
33
33
  private
@@ -11,7 +11,7 @@ module Spotlight
11
11
  private
12
12
 
13
13
  def image_size
14
- [1800, 180]
14
+ Spotlight::Engine.config.featured_image_masthead_size
15
15
  end
16
16
  end
17
17
  end
@@ -7,7 +7,11 @@ module Spotlight
7
7
  MAX_PAGES = Spotlight::Engine.config.max_pages
8
8
 
9
9
  extend FriendlyId
10
- friendly_id :title, use: %i[slugged scoped finders history], scope: %i[exhibit locale]
10
+ # Note: This configuration also needs to be duplicated on the
11
+ # STI models ({Spotlight::AboutPage}, {Spotlight::FeaturePage}, {Spotlight::HomePage})
12
+ friendly_id :title, use: %i[slugged scoped finders history], scope: %i[exhibit locale], treat_reserved_as_conflict: true do |config|
13
+ config.reserved_words&.concat(%w[update_all contacts])
14
+ end
11
15
 
12
16
  belongs_to :exhibit, touch: true
13
17
  belongs_to :created_by, class_name: Spotlight::Engine.config.user_class, optional: true
@@ -27,6 +27,7 @@ module Spotlight
27
27
  to: :context
28
28
 
29
29
  attr_reader :context, :page
30
+
30
31
  def initialize(context:, page:)
31
32
  @context = context
32
33
  @page = page
@@ -39,6 +40,7 @@ module Spotlight
39
40
  'attachment-endpoint': attachment_endpoint,
40
41
  'autocomplete-exhibit-catalog-path': exhibit_autocomplete_endpoint,
41
42
  'autocomplete-exhibit-pages-path': page_autocomplete_endpoint,
43
+ 'autocomplete-exhibit-browse-groups-path': browse_groups_autocomplete_endpoint,
42
44
  'autocomplete-exhibit-searches-path': search_autocomplete_endpoint,
43
45
  'preview-url': page_preview_url
44
46
  }.merge(downstream_parameters)
@@ -61,6 +63,10 @@ module Spotlight
61
63
  spotlight.exhibit_attachments_path(current_exhibit)
62
64
  end
63
65
 
66
+ def browse_groups_autocomplete_endpoint
67
+ spotlight.exhibit_groups_path(current_exhibit)
68
+ end
69
+
64
70
  def exhibit_autocomplete_endpoint
65
71
  spotlight.autocomplete_exhibit_catalog_path(current_exhibit, q: '%QUERY', format: 'json')
66
72
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'spotlight/page_content/sir_trevor'
4
+
3
5
  module Spotlight
4
6
  # Factory for picking the right page content renderer
5
7
  module PageContent
@@ -3,9 +3,10 @@
3
3
  module Spotlight
4
4
  module Resources
5
5
  ##
6
- # Shim object for CSV Uploads. see {Spotlight::AddUploadsFromCSV}
6
+ # Shim object for CSV Uploads. see {Spotlight::AddUploadsFromCsv}
7
7
  class CsvUpload
8
8
  attr_reader :url
9
+
9
10
  include ActiveModel::Model
10
11
  extend ActiveModel::Translation
11
12
  end
@@ -6,6 +6,7 @@ module Spotlight
6
6
  # A PORO to construct a solr hash for a given IiifManifest
7
7
  class IiifManifest
8
8
  attr_reader :collection
9
+
9
10
  def initialize(attrs = {})
10
11
  @url = attrs[:url]
11
12
  @manifest = attrs[:manifest]
@@ -36,6 +37,7 @@ module Spotlight
36
37
  private
37
38
 
38
39
  attr_reader :url, :manifest, :exhibit, :solr_hash
40
+
39
41
  delegate :blacklight_config, to: :exhibit
40
42
 
41
43
  def add_document_id
@@ -67,7 +69,7 @@ module Spotlight
67
69
  end
68
70
 
69
71
  def add_label
70
- return unless title_fields.present? && manifest.try(:label)
72
+ return unless title_fields.present? && manifest&.label
71
73
 
72
74
  Array.wrap(title_fields).each do |field|
73
75
  solr_hash[field] = metadata_class.new(manifest).label
@@ -142,7 +144,7 @@ module Spotlight
142
144
  end
143
145
 
144
146
  def thumbnail_field
145
- blacklight_config.index.try(:thumbnail_field)
147
+ blacklight_config.index.thumbnail_field
146
148
  end
147
149
 
148
150
  def full_image_field
@@ -150,11 +152,11 @@ module Spotlight
150
152
  end
151
153
 
152
154
  def tile_source_field
153
- blacklight_config.show.try(:tile_source_field)
155
+ blacklight_config.show.tile_source_field
154
156
  end
155
157
 
156
158
  def title_fields
157
- Spotlight::Engine.config.iiif_title_fields || blacklight_config.index.try(:title_field)
159
+ Spotlight::Engine.config.iiif_title_fields || blacklight_config.index&.title_field
158
160
  end
159
161
 
160
162
  def sidecar
@@ -185,7 +187,7 @@ module Spotlight
185
187
  end
186
188
 
187
189
  def label
188
- return unless manifest.try(:label)
190
+ return unless manifest&.label
189
191
 
190
192
  Array(json_ld_value(manifest.label)).map { |v| html_sanitize(v) }.first
191
193
  end
@@ -195,7 +197,7 @@ module Spotlight
195
197
  attr_reader :manifest
196
198
 
197
199
  def metadata
198
- manifest.try(:metadata) || []
200
+ manifest&.metadata || []
199
201
  end
200
202
 
201
203
  def metadata_hash
@@ -48,7 +48,7 @@ module Spotlight
48
48
  end
49
49
 
50
50
  def custom_fields_data
51
- data.slice(*exhibit.custom_fields.map(&:field).map(&:to_s)).select { |_k, v| v.present? }
51
+ data.slice(*exhibit.custom_fields.map(&:slug).map(&:to_s)).select { |_k, v| v.present? }
52
52
  end
53
53
 
54
54
  def configured_fields_data
@@ -12,12 +12,17 @@ module Spotlight
12
12
 
13
13
  self.table_name = 'spotlight_searches'
14
14
  belongs_to :exhibit
15
+ has_many :group_memberships, class_name: 'Spotlight::GroupMember', as: :member, dependent: :delete_all
16
+ has_many :groups, through: :group_memberships
17
+ accepts_nested_attributes_for :group_memberships
18
+ accepts_nested_attributes_for :groups
15
19
  serialize :query_params, Hash
16
20
  default_scope { order('weight ASC') }
17
21
  scope :published, -> { where(published: true) }
22
+ scope :unpublished, -> { where(published: [nil, false]) }
18
23
  validates :title, presence: true
19
24
 
20
- translates :title, :long_description
25
+ translates :title, :subtitle, :long_description
21
26
 
22
27
  has_paper_trail
23
28
 
@@ -26,6 +31,10 @@ module Spotlight
26
31
  accepts_nested_attributes_for :thumbnail, update_only: true, reject_if: proc { |attr| attr['iiif_tilesource'].blank? }
27
32
  accepts_nested_attributes_for :masthead, update_only: true, reject_if: proc { |attr| attr['iiif_tilesource'].blank? }
28
33
 
34
+ def full_title
35
+ [title, subtitle.presence].compact.join(' · ')
36
+ end
37
+
29
38
  def thumbnail_image_url
30
39
  return unless thumbnail&.iiif_url
31
40
 
@@ -33,11 +33,7 @@ module Spotlight
33
33
 
34
34
  # Roll our own polymorphism because our documents are not AREL-able
35
35
  def document
36
- document_type.new document_type.unique_key => document_id
37
- end
38
-
39
- def document_type
40
- (super.constantize if defined?(super)) || default_document_type
36
+ document_type_class.new document_type_class.unique_key => document_id
41
37
  end
42
38
 
43
39
  def default_document_type
@@ -46,6 +42,12 @@ module Spotlight
46
42
 
47
43
  private
48
44
 
45
+ def document_type_class
46
+ return default_document_type unless document_type.is_a?(String)
47
+
48
+ document_type.constantize
49
+ end
50
+
49
51
  def visibility_field
50
52
  blacklight_config.document_model.visibility_field(exhibit)
51
53
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ ##
5
+ # Featured images for browse categories, feature pages, and exhibits
6
+ class TemporaryImage < Spotlight::FeaturedImage
7
+ end
8
+ end
@@ -0,0 +1,482 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'roar/decorator'
4
+ require 'roar/json'
5
+ require 'base64'
6
+ require 'tempfile'
7
+
8
+ module Spotlight
9
+ # Utility service for importing and exporting exhibit data
10
+ class ExhibitImportExportService
11
+ class_attribute :serialization_pipeline, default: %i[
12
+ raw_json
13
+ add_feature_page_hierarchy
14
+ add_browse_group_hierarchy
15
+ add_page_content
16
+ attach_featured_images
17
+ attach_attachments
18
+ ]
19
+
20
+ attr_reader :exhibit, :include
21
+
22
+ def initialize(exhibit, include: Spotlight::Engine.config.exports)
23
+ @exhibit = exhibit
24
+ @include = include
25
+ end
26
+
27
+ def from_hash!(hash)
28
+ hash = hash.deep_symbolize_keys.reverse_merge(
29
+ main_navigations: {},
30
+ contact_emails: {},
31
+ searches: {},
32
+ about_pages: {},
33
+ feature_pages: {},
34
+ contacts: {},
35
+ custom_fields: {},
36
+ solr_document_sidecars: {},
37
+ resources: {},
38
+ attachments: {},
39
+ languages: {},
40
+ translations: {},
41
+ owned_taggings: {},
42
+ groups: {}
43
+ )
44
+
45
+ exhibit_attributes = hash.reject { |_k, v| v.is_a?(Array) || v.is_a?(Hash) }
46
+ exhibit.update(exhibit_attributes.except(:theme))
47
+ exhibit.theme = exhibit_attributes[:theme] if exhibit.themes.include? exhibit_attributes[:theme]
48
+
49
+ deserialize_featured_image(exhibit, :masthead, hash[:masthead]) if hash[:masthead]
50
+ deserialize_featured_image(exhibit, :thumbnail, hash[:thumbnail]) if hash[:thumbnail]
51
+
52
+ exhibit.blacklight_configuration.update hash[:blacklight_configuration].deep_stringify_keys if hash[:blacklight_configuration]
53
+
54
+ hash[:main_navigations].each do |attr|
55
+ ar = exhibit.main_navigations.find_or_initialize_by(nav_type: attr[:nav_type])
56
+ ar.update(attr)
57
+ end
58
+
59
+ hash[:contact_emails].each do |attr|
60
+ ar = exhibit.contact_emails.find_or_initialize_by(email: attr[:email])
61
+ ar.update(attr)
62
+ end
63
+
64
+ hash[:groups].each do |attr|
65
+ gr = exhibit.groups.find_or_initialize_by(slug: attr[:slug])
66
+ gr.update(attr)
67
+ end
68
+
69
+ hash[:searches].each do |attr|
70
+ group_slugs = attr.delete(:group_slugs) || []
71
+ masthead = attr.delete(:masthead)
72
+ thumbnail = attr.delete(:thumbnail)
73
+
74
+ ar = exhibit.searches.find_or_initialize_by(slug: attr[:slug])
75
+ ar.update(attr)
76
+
77
+ ar.update(groups: exhibit.groups.select { |x| group_slugs.include? x.slug })
78
+
79
+ deserialize_featured_image(ar, :masthead, masthead) if masthead
80
+ deserialize_featured_image(ar, :thumbnail, thumbnail) if thumbnail
81
+ end
82
+
83
+ hash[:about_pages].each do |attr|
84
+ masthead = attr.delete(:masthead)
85
+ thumbnail = attr.delete(:thumbnail)
86
+ translated_pages = attr.delete(:translated_pages) || []
87
+
88
+ ar = exhibit.about_pages.find_or_initialize_by(slug: attr[:slug])
89
+ ar.update(attr)
90
+
91
+ deserialize_featured_image(ar, :masthead, masthead) if masthead
92
+ deserialize_featured_image(ar, :thumbnail, thumbnail) if thumbnail
93
+
94
+ translated_pages.each do |tattr|
95
+ masthead = tattr.delete(:masthead)
96
+ thumbnail = tattr.delete(:thumbnail)
97
+
98
+ tar = ar.translated_page_for(tattr[:locale]) || ar.clone_for_locale(tattr[:locale])
99
+ tar.update(tattr)
100
+
101
+ deserialize_featured_image(ar, :masthead, masthead) if masthead
102
+ deserialize_featured_image(ar, :thumbnail, thumbnail) if thumbnail
103
+ end
104
+ end
105
+
106
+ hash[:feature_pages].each do |attr|
107
+ masthead = attr.delete(:masthead)
108
+ thumbnail = attr.delete(:thumbnail)
109
+
110
+ ar = exhibit.feature_pages.find_or_initialize_by(slug: attr[:slug])
111
+ ar.update(attr.except(:parent_page_slug, :translated_pages))
112
+
113
+ deserialize_featured_image(ar, :masthead, masthead) if masthead
114
+ deserialize_featured_image(ar, :thumbnail, thumbnail) if thumbnail
115
+ end
116
+
117
+ feature_pages = exhibit.feature_pages.index_by(&:slug)
118
+ hash[:feature_pages].each do |attr|
119
+ next unless attr[:parent_page_slug]
120
+
121
+ feature_pages[attr[:slug]].parent_page_id = feature_pages[attr[:parent_page_slug]].id
122
+ end
123
+
124
+ hash[:feature_pages].each do |attr|
125
+ ar = exhibit.feature_pages.find_or_initialize_by(slug: attr[:slug])
126
+
127
+ (attr[:translated_pages] || []).each do |tattr|
128
+ masthead = tattr.delete(:masthead)
129
+ thumbnail = tattr.delete(:thumbnail)
130
+
131
+ tar = ar.translated_page_for(tattr[:locale]) || ar.clone_for_locale(tattr[:locale])
132
+ tar.update(tattr)
133
+
134
+ deserialize_featured_image(ar, :masthead, masthead) if masthead
135
+ deserialize_featured_image(ar, :thumbnail, thumbnail) if thumbnail
136
+ end
137
+ end
138
+
139
+ if hash[:home_page]
140
+ translated_pages = hash[:home_page].delete(:translated_pages) || []
141
+ exhibit.home_page.update(hash[:home_page].except(:thumbnail))
142
+ deserialize_featured_image(exhibit.home_page, :thumbnail, hash[:home_page][:thumbnail]) if hash[:home_page][:thumbnail]
143
+
144
+ translated_pages.each do |tattr|
145
+ masthead = tattr.delete(:masthead)
146
+ thumbnail = tattr.delete(:thumbnail)
147
+
148
+ tar = exhibit.home_page.translated_page_for(tattr[:locale]) || exhibit.home_page.clone_for_locale(tattr[:locale])
149
+ tar.update(tattr)
150
+
151
+ deserialize_featured_image(ar, :masthead, masthead) if masthead
152
+ deserialize_featured_image(ar, :thumbnail, thumbnail) if thumbnail
153
+ end
154
+ end
155
+
156
+ hash[:contacts].each do |attr|
157
+ avatar = attr.delete(:avatar)
158
+
159
+ ar = exhibit.contacts.find_or_initialize_by(slug: attr[:slug])
160
+ ar.update(attr)
161
+
162
+ deserialize_featured_image(ar, :avatar, avatar) if avatar
163
+ end
164
+
165
+ hash[:custom_fields].each do |attr|
166
+ ar = exhibit.custom_fields.find_or_initialize_by(slug: attr[:slug])
167
+ ar.update(attr)
168
+ end
169
+
170
+ hash[:solr_document_sidecars].each do |attr|
171
+ ar = exhibit.solr_document_sidecars.find_or_initialize_by(document_id: attr[:document_id])
172
+ ar.update(attr)
173
+ end
174
+
175
+ hash[:resources].each do |attr|
176
+ upload = attr.delete(:upload)
177
+
178
+ ar = exhibit.resources.find_or_initialize_by(type: attr[:type], url: attr[:url])
179
+ ar.update(attr)
180
+
181
+ deserialize_featured_image(ar, :upload, upload) if upload
182
+ end
183
+
184
+ hash[:attachments].each do |attr|
185
+ file = attr.delete(:file)
186
+
187
+ # dedupe by something??
188
+ ar = exhibit.attachments.build(attr)
189
+ ar.file = CarrierWave::SanitizedFile.new tempfile: StringIO.new(Base64.decode64(file[:content])),
190
+ filename: file[:filename],
191
+ content_type: file[:content_type]
192
+ end
193
+
194
+ hash[:languages].each do |attr|
195
+ ar = exhibit.languages.find_or_initialize_by(locale: attr[:locale])
196
+ ar.update(attr)
197
+ end
198
+
199
+ hash[:translations].each do |attr|
200
+ ar = exhibit.translations.find_or_initialize_by(locale: attr[:locale], key: attr[:key])
201
+ ar.update(attr)
202
+ end
203
+
204
+ hash[:owned_taggings].each do |attr|
205
+ tag = ActsAsTaggableOn::Tag.find_or_create_by(name: attr[:tag][:name])
206
+ exhibit.owned_taggings.build(attr.except(:tag).merge(tag_id: tag.id))
207
+ end
208
+ end
209
+
210
+ def deserialize_featured_image(obj, method, data)
211
+ file = data.delete(:image)
212
+ image = obj.public_send("build_#{method}")
213
+ image.update(data)
214
+ if file
215
+ image.image = CarrierWave::SanitizedFile.new tempfile: StringIO.new(Base64.decode64(file[:content])),
216
+ filename: file[:filename],
217
+ content_type: file[:content_type]
218
+ # Unset the iiif_tilesource field as the new image should be different, because
219
+ # the source has been reloaded
220
+ image.iiif_tilesource = nil
221
+ end
222
+ image.save!
223
+ obj.update(method => image)
224
+ end
225
+
226
+ def as_json(*_args)
227
+ self.class.serialization_pipeline.inject({}) do |memo, step|
228
+ method(step).call(memo)
229
+ end
230
+ end
231
+
232
+ private
233
+
234
+ def attach_featured_images(json)
235
+ json[:masthead] = serialize_featured_image(json[:masthead_id]) if json[:masthead_id]
236
+ json.delete(:masthead_id)
237
+ json[:thumbnail] = serialize_featured_image(json[:thumbnail_id]) if json[:thumbnail_id]
238
+ json.delete(:thumbnail_id)
239
+
240
+ (json[:searches] || []).each do |search|
241
+ search[:masthead] = serialize_featured_image(search[:masthead_id]) if search[:masthead_id]
242
+ search.delete(:masthead_id)
243
+ search[:thumbnail] = serialize_featured_image(search[:thumbnail_id]) if search[:thumbnail_id]
244
+ search.delete(:thumbnail_id)
245
+ end
246
+
247
+ (json[:about_pages] || []).each do |page|
248
+ page[:masthead] = serialize_featured_image(page[:masthead_id]) if page[:masthead_id]
249
+ page.delete(:masthead_id)
250
+ page[:thumbnail] = serialize_featured_image(page[:thumbnail_id]) if page[:thumbnail_id]
251
+ page.delete(:thumbnail_id)
252
+
253
+ (page[:translated_pages] || []).each do |translated_page|
254
+ translated_page[:masthead] = serialize_featured_image(translated_page[:masthead_id]) if translated_page[:masthead_id]
255
+ translated_page.delete(:masthead_id)
256
+ translated_page[:thumbnail] = serialize_featured_image(translated_page[:thumbnail_id]) if translated_page[:thumbnail_id]
257
+ translated_page.delete(:thumbnail_id)
258
+ end
259
+ end
260
+
261
+ (json[:feature_pages] || []).each do |page|
262
+ page[:masthead] = serialize_featured_image(page[:masthead_id]) if page[:masthead_id]
263
+ page.delete(:masthead_id)
264
+ page[:thumbnail] = serialize_featured_image(page[:thumbnail_id]) if page[:thumbnail_id]
265
+ page.delete(:thumbnail_id)
266
+
267
+ (page[:translated_pages] || []).each do |translated_page|
268
+ translated_page[:masthead] = serialize_featured_image(translated_page[:masthead_id]) if translated_page[:masthead_id]
269
+ translated_page.delete(:masthead_id)
270
+ translated_page[:thumbnail] = serialize_featured_image(translated_page[:thumbnail_id]) if translated_page[:thumbnail_id]
271
+ translated_page.delete(:thumbnail_id)
272
+ end
273
+ end
274
+
275
+ if json[:home_page]
276
+ json[:home_page][:masthead] = serialize_featured_image(json[:home_page][:masthead_id]) if json[:home_page][:masthead_id]
277
+ json[:home_page].delete(:masthead_id)
278
+ json[:home_page][:thumbnail] = serialize_featured_image(json[:home_page][:thumbnail_id]) if json[:home_page][:thumbnail_id]
279
+ json[:home_page].delete(:thumbnail_id)
280
+
281
+ (json[:home_page][:translated_pages] || []).each do |translated_page|
282
+ translated_page[:masthead] = serialize_featured_image(translated_page[:masthead_id]) if translated_page[:masthead_id]
283
+ translated_page.delete(:masthead_id)
284
+ translated_page[:thumbnail] = serialize_featured_image(translated_page[:thumbnail_id]) if translated_page[:thumbnail_id]
285
+ translated_page.delete(:thumbnail_id)
286
+ end
287
+ end
288
+
289
+ (json[:contacts] || []).each do |page|
290
+ page[:avatar] = serialize_featured_image(page[:avatar_id]) if page[:avatar_id]
291
+ page.delete(:avatar_id)
292
+ end
293
+
294
+ (json[:resources] || []).each do |page|
295
+ page[:upload] = serialize_featured_image(page[:upload_id]) if page[:upload_id]
296
+ page.delete(:upload_id)
297
+ end
298
+
299
+ json
300
+ end
301
+
302
+ def serialize_featured_image(id)
303
+ image = Spotlight::FeaturedImage.find(id)
304
+ file = image.image.file
305
+ if file
306
+ img = {
307
+ image: {
308
+ filename: file.filename, content_type: file.content_type, content: Base64.encode64(file.read)
309
+ }
310
+ }
311
+ end
312
+
313
+ image.as_json(except: %i[id image]).merge(img || {}).deep_symbolize_keys
314
+ end
315
+
316
+ def attach_attachments(json)
317
+ return json unless json[:attachments]
318
+
319
+ json[:attachments].each do |attachment|
320
+ a = exhibit.attachments.find(attachment[:id])
321
+ file = a.file.file
322
+
323
+ attachment[:file] = {
324
+ filename: file.filename,
325
+ content_type: file.content_type,
326
+ content: Base64.encode64(file.read)
327
+ }
328
+
329
+ attachment.delete(:id)
330
+ end
331
+
332
+ json
333
+ end
334
+
335
+ def add_feature_page_hierarchy(json)
336
+ return json unless json[:feature_pages]
337
+
338
+ page_id_map = json[:feature_pages].map { |x| [x[:id], x[:slug]] }.to_h
339
+
340
+ json[:feature_pages].each do |page|
341
+ page.delete(:id)
342
+ next unless page[:parent_page_id]
343
+
344
+ page[:parent_page_slug] = page_id_map[page[:parent_page_id]]
345
+ page.delete(:parent_page_id)
346
+ end
347
+
348
+ json
349
+ end
350
+
351
+ def add_browse_group_hierarchy(json)
352
+ return json unless json[:groups] && json[:searches]
353
+
354
+ json[:searches].each do |attr|
355
+ search = exhibit.searches.find_by(slug: attr[:slug])
356
+
357
+ attr[:group_slugs] = search.groups.pluck(:slug)
358
+ end
359
+
360
+ json
361
+ end
362
+
363
+ def add_page_content(json)
364
+ (json[:feature_pages] || []).each do |page|
365
+ p = exhibit.feature_pages.find_by(slug: page[:slug])
366
+ page[:content] = p.read_attribute(:content)
367
+ (page[:translated_pages]).each do |translated_page|
368
+ translated_page[:content] = p.translated_page_for(translated_page[:locale]).read_attribute(:content)
369
+ end
370
+ end
371
+
372
+ (json[:about_pages] || []).each do |page|
373
+ p = exhibit.about_pages.find_by(slug: page[:slug])
374
+ page[:content] = p.read_attribute(:content)
375
+ (page[:translated_pages]).each do |translated_page|
376
+ translated_page[:content] = p.translated_page_for(translated_page[:locale]).read_attribute(:content)
377
+ end
378
+ end
379
+
380
+ json[:home_page][:content] = exhibit.home_page.read_attribute(:content) if json[:home_page]
381
+ (json.dig(:home_page, :translated_pages) || []).each do |translated_page|
382
+ translated_page[:content] = exhibit.home_page.translated_page_for(translated_page[:locale]).read_attribute(:content)
383
+ end
384
+ json
385
+ end
386
+
387
+ def raw_json(_input = nil)
388
+ exhibit.as_json(
389
+ {
390
+ except: %i[id slug site_id],
391
+ include: {}.merge(
392
+ if_include?(:config,
393
+ main_navigations: {
394
+ except: %i[id exhibit_id]
395
+ },
396
+ contact_emails: {
397
+ except: %i[id exhibit_id confirmation_token]
398
+ },
399
+ languages: {
400
+ except: %i[id exhibit_id]
401
+ },
402
+ translations: {
403
+ only: %i[locale key value interpolations is_proc]
404
+ })
405
+ ).merge(
406
+ if_include?(:pages,
407
+ searches: { # thumbnail
408
+ except: %i[id scope exhibit_id]
409
+ },
410
+ groups: {
411
+ except: %i[id exhibit_id]
412
+ },
413
+ about_pages: { # thumbnail
414
+ except: %i[id scope exhibit_id parent_page_id content],
415
+ include: {
416
+ translated_pages: {
417
+ except: %i[id scope exhibit_id parent_page_id default_locale_page_id content]
418
+ }
419
+ }
420
+ },
421
+ home_page: { # thumbnail
422
+ except: %i[id slug scope exhibit_id parent_page_id content],
423
+ include: {
424
+ translated_pages: {
425
+ except: %i[id scope exhibit_id parent_page_id default_locale_page_id content]
426
+ }
427
+ }
428
+ },
429
+ feature_pages: { # thumbnail
430
+ except: %i[scope exhibit_id content],
431
+ include: {
432
+ translated_pages: {
433
+ except: %i[id scope exhibit_id parent_page_id default_locale_page_id content]
434
+ }
435
+ }
436
+ },
437
+ contacts: {
438
+ except: %i[id exhibit_id]
439
+ })
440
+ ).merge(
441
+ if_include?(:blacklight_configuration,
442
+ blacklight_configuration: {
443
+ except: %i[id exhibit_id]
444
+ },
445
+ # blacklight_configuration
446
+ custom_fields: {
447
+ except: %i[id exhibit_id]
448
+ })
449
+ ).merge(
450
+ if_include?(:resources,
451
+ # resources
452
+ solr_document_sidecars: {
453
+ except: %i[id exhibit_id]
454
+ },
455
+ owned_taggings: {
456
+ only: %i[taggable_id taggable_type context],
457
+ include: {
458
+ tag: {
459
+ only: [:name]
460
+ }
461
+ }
462
+ },
463
+ resources: { # upload
464
+ except: %i[id exhibit_id],
465
+ methods: :type
466
+ })
467
+ ).merge(
468
+ if_include?(:attachments,
469
+ # attachments
470
+ attachments: { # file
471
+ except: %i[exhibit_id]
472
+ })
473
+ )
474
+ }.merge(include[:config] ? {} : { only: %i[does_not_exist] })
475
+ ).deep_symbolize_keys
476
+ end
477
+
478
+ def if_include?(config, res)
479
+ include[config] ? res : {}
480
+ end
481
+ end
482
+ end