blacklight-spotlight 3.0.0.rc3 → 3.0.0.rc4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/spotlight/admin/reindex_monitor.js +1 -0
  3. data/app/assets/stylesheets/spotlight/browse_group_categories_block.scss +23 -0
  4. data/app/controllers/spotlight/catalog_controller.rb +4 -1
  5. data/app/controllers/spotlight/dashboards_controller.rb +1 -1
  6. data/app/controllers/spotlight/exhibits_controller.rb +1 -1
  7. data/app/helpers/spotlight/application_helper.rb +19 -0
  8. data/app/helpers/spotlight/pages_helper.rb +1 -1
  9. data/app/jobs/concerns/spotlight/job_tracking.rb +47 -0
  10. data/app/jobs/concerns/spotlight/limit_concurrency.rb +33 -0
  11. data/app/jobs/spotlight/add_uploads_from_csv.rb +6 -3
  12. data/app/jobs/spotlight/application_job.rb +8 -0
  13. data/app/jobs/spotlight/cleanup_job_trackers_job.rb +13 -0
  14. data/app/jobs/spotlight/default_thumbnail_job.rb +1 -3
  15. data/app/jobs/spotlight/reindex_exhibit_job.rb +36 -0
  16. data/app/jobs/spotlight/reindex_job.rb +49 -41
  17. data/app/jobs/spotlight/rename_sidecar_field_job.rb +2 -2
  18. data/app/jobs/spotlight/update_job_trackers_job.rb +20 -0
  19. data/app/models/concerns/spotlight/user.rb +2 -1
  20. data/app/models/spotlight/event.rb +13 -0
  21. data/app/models/spotlight/exhibit.rb +4 -14
  22. data/app/models/spotlight/job_tracker.rb +105 -0
  23. data/app/models/spotlight/reindex_progress.rb +44 -27
  24. data/app/models/spotlight/resource.rb +24 -58
  25. data/app/models/spotlight/resources/iiif_harvester.rb +10 -1
  26. data/app/models/spotlight/resources/iiif_manifest.rb +3 -1
  27. data/app/models/spotlight/resources/iiif_service.rb +1 -1
  28. data/app/models/spotlight/resources/json_upload.rb +12 -0
  29. data/app/models/spotlight/resources/upload.rb +25 -2
  30. data/app/models/spotlight/solr_document_sidecar.rb +2 -1
  31. data/app/services/spotlight/etl.rb +7 -0
  32. data/app/services/spotlight/etl/context.rb +52 -0
  33. data/app/services/spotlight/etl/executor.rb +194 -0
  34. data/app/services/spotlight/etl/loaders.rb +12 -0
  35. data/app/services/spotlight/etl/pipeline.rb +81 -0
  36. data/app/services/spotlight/etl/solr_loader.rb +96 -0
  37. data/app/services/spotlight/etl/sources.rb +25 -0
  38. data/app/services/spotlight/etl/step.rb +82 -0
  39. data/app/services/spotlight/etl/transforms.rb +64 -0
  40. data/app/services/spotlight/validity_checker.rb +5 -5
  41. data/app/views/spotlight/dashboards/_reindexing_activity.html.erb +6 -6
  42. data/app/views/spotlight/shared/_locale_picker.html.erb +1 -1
  43. data/app/views/spotlight/sir_trevor/blocks/_browse_group_categories_block.html.erb +4 -3
  44. data/config/locales/spotlight.ar.yml +11 -1
  45. data/config/locales/spotlight.en.yml +3 -2
  46. data/db/migrate/20210122082032_create_job_trackers.rb +22 -0
  47. data/db/migrate/20210126123041_create_events.rb +15 -0
  48. data/lib/generators/spotlight/scaffold_resource_generator.rb +5 -13
  49. data/lib/spotlight/engine.rb +8 -1
  50. data/lib/spotlight/version.rb +1 -1
  51. data/spec/controllers/spotlight/catalog_controller_spec.rb +3 -1
  52. data/spec/examples.txt +1448 -1437
  53. data/spec/factories/job_trackers.rb +9 -0
  54. data/spec/features/add_items_spec.rb +9 -4
  55. data/spec/features/javascript/reindex_monitor_spec.rb +1 -1
  56. data/spec/features/site_users_management_spec.rb +4 -4
  57. data/spec/helpers/spotlight/pages_helper_spec.rb +8 -0
  58. data/spec/jobs/spotlight/reindex_exhibit_job_spec.rb +43 -0
  59. data/spec/jobs/spotlight/reindex_job_spec.rb +30 -59
  60. data/spec/models/spotlight/exhibit_spec.rb +3 -57
  61. data/spec/models/spotlight/reindex_progress_spec.rb +89 -87
  62. data/spec/models/spotlight/resource_spec.rb +69 -90
  63. data/spec/models/spotlight/resources/iiif_harvester_spec.rb +9 -10
  64. data/spec/models/spotlight/solr_document_sidecar_spec.rb +1 -0
  65. data/spec/services/spotlight/etl/context_spec.rb +66 -0
  66. data/spec/services/spotlight/etl/executor_spec.rb +149 -0
  67. data/spec/services/spotlight/etl/pipeline_spec.rb +22 -0
  68. data/spec/services/spotlight/etl/solr_loader_spec.rb +76 -0
  69. data/spec/services/spotlight/etl/step_spec.rb +70 -0
  70. data/spec/spec_helper.rb +2 -5
  71. data/spec/views/spotlight/dashboards/_reindexing_activity.html.erb_spec.rb +22 -19
  72. metadata +55 -15
  73. data/app/models/concerns/spotlight/resources/open_graph.rb +0 -36
  74. data/app/models/spotlight/reindexing_log_entry.rb +0 -42
  75. data/app/services/spotlight/resources/iiif_builder.rb +0 -19
  76. data/app/services/spotlight/solr_document_builder.rb +0 -77
  77. data/app/services/spotlight/upload_solr_document_builder.rb +0 -57
  78. data/spec/factories/reindexing_log_entries.rb +0 -54
  79. data/spec/models/spotlight/reindexing_log_entry_spec.rb +0 -129
  80. data/spec/models/spotlight/resources/open_graph_spec.rb +0 -65
  81. data/spec/services/spotlight/solr_document_builder_spec.rb +0 -66
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ module Etl
5
+ # Solr data loader with a built-in buffer to combine document updates into batches
6
+ class SolrLoader
7
+ attr_reader :queue, :batch_size
8
+
9
+ delegate :size, to: :queue
10
+
11
+ def initialize(batch_size: Spotlight::Engine.config.solr_batch_size, solr_connection: nil)
12
+ @queue = Queue.new
13
+ @batch_size = batch_size
14
+ @blacklight_solr = solr_connection
15
+ end
16
+
17
+ def call(data, pipeline = nil)
18
+ @queue << data
19
+
20
+ write_to_index(pipeline) if @queue.size >= @batch_size
21
+ end
22
+
23
+ def finalize(pipeline = nil)
24
+ write_to_index(pipeline)
25
+
26
+ commit! if pipeline.nil? || pipeline.context.additional_parameters[:commit]
27
+ end
28
+
29
+ private
30
+
31
+ def write_to_index(pipeline)
32
+ batch = drain_queue
33
+
34
+ return unless write? && batch.any?
35
+
36
+ send_batch(batch, pipeline)
37
+ end
38
+
39
+ def send_batch(documents, pipeline)
40
+ blacklight_solr.update params: { commitWithin: 500 },
41
+ data: documents.to_json,
42
+ headers: { 'Content-Type' => 'application/json' }
43
+ rescue StandardError => e
44
+ logger.warn "Error sending a batch of documents to solr: #{e}"
45
+
46
+ documents.each do |doc|
47
+ send_one(doc, pipeline)
48
+ end
49
+ end
50
+
51
+ def send_one(document, pipeline)
52
+ blacklight_solr.update params: { commitWithin: 500 },
53
+ data: [document].to_json,
54
+ headers: { 'Content-Type' => 'application/json' }
55
+ rescue StandardError => e
56
+ pipeline&.on_error(self, e, document.to_json)
57
+ end
58
+
59
+ def blacklight_solr
60
+ @blacklight_solr ||= RSolr.connect(connection_config.merge(adapter: connection_config[:http_adapter]))
61
+ end
62
+
63
+ def connection_config
64
+ Blacklight.connection_config
65
+ end
66
+
67
+ def drain_queue
68
+ arr = []
69
+
70
+ begin
71
+ arr << @queue.deq(true) while arr.length < @batch_size && !@queue.empty?
72
+ rescue ThreadError
73
+ # @queue throws a ThreadError if it is empty...
74
+ end
75
+
76
+ arr
77
+ end
78
+
79
+ def commit!
80
+ return unless write?
81
+
82
+ blacklight_solr.commit
83
+ rescue StandardError => e
84
+ logger.warn "Unable to commit to solr: #{e}"
85
+ end
86
+
87
+ def write?
88
+ Spotlight::Engine.config.writable_index
89
+ end
90
+
91
+ def logger
92
+ Rails.logger
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ module Etl
5
+ # Basic ETL source implementations
6
+ module Sources
7
+ # A simple source that just returns the original resource(s)
8
+ IdentitySource = lambda do |context|
9
+ Array.wrap(context.resource)
10
+ end
11
+
12
+ # A transform step that calls a method on the resource to generate a source
13
+ def self.SourceMethodSource(method) # rubocop:disable Naming/MethodName
14
+ lambda do |context|
15
+ context.resource.public_send(method)
16
+ end
17
+ end
18
+
19
+ # A simple source that retrieves the stored data from a Spotlight::Resource
20
+ StoredData = lambda do |context, **|
21
+ Array.wrap(context.resource.data)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ module Etl
5
+ # ETL pipeline step
6
+ class Step
7
+ attr_reader :definition, :context, :executor
8
+
9
+ # @param [Class, Proc] definition the step to run
10
+ # @param [String] label
11
+ # @param [Spotlight::Etl::Executor] executor the execution environment
12
+ def initialize(definition, label: nil, executor: nil)
13
+ @definition = definition
14
+ @executor = executor
15
+ @label = label
16
+ end
17
+
18
+ # rubocop:disable Metrics/MethodLength
19
+ def call(*args)
20
+ with_logger do |logger|
21
+ logger.debug { "Called with #{transform_data_for_debugging(args.first)}" }
22
+
23
+ catch :skip do
24
+ return action.call(*args).tap do |result|
25
+ logger.debug { " => Returning #{transform_data_for_debugging(result)}" } if $VERBOSE
26
+ end
27
+ end
28
+
29
+ logger.debug ' => Caught skip.'
30
+ throw :skip
31
+ end
32
+ rescue StandardError => e
33
+ with_logger do |logger|
34
+ logger.error("Caught exception #{e}")
35
+ end
36
+ raise(e)
37
+ end
38
+ # rubocop:enable Metrics/MethodLength
39
+
40
+ def finalize(*args)
41
+ action.finalize(*args) if action.respond_to? :finalize
42
+ end
43
+
44
+ private
45
+
46
+ # @return [#call]
47
+ def action
48
+ case definition
49
+ when Class
50
+ # memoize the class' instance for the lifetime of the step
51
+ @memoized_action ||= definition.new
52
+ else # Proc, etc
53
+ definition
54
+ end
55
+ end
56
+
57
+ # @return [#to_string]
58
+ def label
59
+ @label || definition
60
+ end
61
+
62
+ # NOTE: this is super weird to support Rails 5.2
63
+ # @return [Logger]
64
+ def with_logger
65
+ yield(Rails.logger) && return unless executor
66
+
67
+ executor.with_logger do |logger|
68
+ logger.tagged(label) do
69
+ yield logger
70
+ end
71
+ end
72
+ end
73
+
74
+ ##
75
+ # @param [Hash] data
76
+ # @return [String] a simplified + truncated version of the data hash for debugging
77
+ def transform_data_for_debugging(data)
78
+ executor&.transform_data_for_debugging(data) || data.inspect.truncate(100)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ module Etl
5
+ # Basic + default transform steps
6
+ module Transforms
7
+ # A transform step that "transforms" the source into the data element
8
+ IdentityTransform = lambda do |data, pipeline|
9
+ data.merge(pipeline.source)
10
+ end
11
+
12
+ # A transform step that calls a method on the source to generate a document
13
+ def self.SourceMethodTransform(method) # rubocop:disable Naming/MethodName
14
+ lambda do |data, pipeline|
15
+ data.merge(pipeline.source.public_send(method))
16
+ end
17
+ end
18
+
19
+ # A transform step that throws away blank data
20
+ RejectBlank = lambda do |data, _|
21
+ throw :skip if data.blank?
22
+ data
23
+ end
24
+
25
+ # A trasnform step that ensures data has a unique key attribute
26
+ RejectMissingUniqueId = lambda do |data, pipeline|
27
+ id = pipeline.context.unique_key(data)
28
+
29
+ throw :skip if id.blank?
30
+ data
31
+ end
32
+
33
+ # A transform that adds exhibit-specific metadata (like Spotlight sidecar data)
34
+ # to the document
35
+ ApplyExhibitMetadata = lambda do |data, pipeline|
36
+ resource = pipeline.context.resource
37
+ document_model = pipeline.context.document_model
38
+ id = pipeline.context.unique_key(data)
39
+
40
+ next data unless document_model && id.present?
41
+
42
+ exhibit_metadata = document_model.build_for_exhibit(id, resource.exhibit, resource: (resource if resource.persisted?))
43
+
44
+ data.reverse_merge(exhibit_metadata.to_solr)
45
+ end
46
+
47
+ # A transform that adds application-specific metadata (like what resource generated the solr document)
48
+ ApplyApplicationMetadata = lambda do |data, pipeline|
49
+ resource = pipeline.context.resource
50
+ document_model = pipeline.context.document_model
51
+
52
+ data.reverse_merge(
53
+ Spotlight::Engine.config.resource_global_id_field => (resource.to_global_id.to_s if resource.persisted?),
54
+ document_model.resource_type_field => resource.class.to_s.tableize
55
+ )
56
+ end
57
+
58
+ # A transform that adds externally-provided metadata to the document
59
+ ApplyPipelineMetadata = lambda do |data, pipeline|
60
+ data.reverse_merge(pipeline.context.additional_metadata)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -5,16 +5,16 @@ module Spotlight
5
5
  # check if a delayed job still needs to run
6
6
  class ValidityChecker
7
7
  # Return a validity token
8
- # @param [ActiveModel::Model]
8
+ # @param [ActiveJob::Base]
9
9
  # @return [Object] any serializable object
10
- def mint(_model); end
10
+ def mint(_job); end
11
11
 
12
12
  # Check if the token is still valid for the model
13
- # @param [ActiveModel::Model]
13
+ # @param [ActiveJob::Base]
14
14
  # @param [Object] the serializable token minted by #mint
15
15
  # @return [boolean]
16
- def check(_model, _token)
17
- true
16
+ def check(_job, validity_token: nil)
17
+ validity_token || true
18
18
  end
19
19
  end
20
20
  end
@@ -12,13 +12,13 @@
12
12
  </tr>
13
13
  </thead>
14
14
  <tbody>
15
- <% @recent_reindexing.each do |log_entry| %>
15
+ <% @recent_reindexing.each do |tracker| %>
16
16
  <tr class="d-flex">
17
- <td class="col-3"><%= l log_entry.start_time, format: :long if log_entry.start_time %></td>
18
- <td class="col-3"><%= log_entry.user.email if log_entry.user %></td>
19
- <td class="col-1"><%= log_entry.items_reindexed_count %></td>
20
- <td class="col-3"><%= distance_of_time_in_words log_entry.duration, 0, include_seconds: true if log_entry.duration %></td>
21
- <td class="col-2"><%= t "reindexing_log.status.#{log_entry.job_status}" %></td>
17
+ <td class="col-3"><%= l tracker.created_at, format: :long %></td>
18
+ <td class="col-3"><%= tracker.user.email if tracker.user %></td>
19
+ <td class="col-1"><%= tracker.progress %></td>
20
+ <td class="col-3"><%= distance_of_time_in_words tracker.created_at, tracker.updated_at, include_seconds: true if tracker.completed? || tracker.failed? %></td>
21
+ <td class="col-2"><%= t "reindexing_log.status.#{tracker.status}" %></td>
22
22
  </tr>
23
23
  <% end %>
24
24
  </tbody>
@@ -6,7 +6,7 @@
6
6
  <ul class="dropdown-menu">
7
7
  <% locale_selecter_dropown_options.each do |language| %>
8
8
  <li>
9
- <%= link_to language.to_native, (spotlight.url_for(locale: lanaguage.locale) rescue url_for(locale: language.locale)), class: 'dropdown-item', data: { turbolinks: false } %>
9
+ <%= link_to language.to_native, current_page_for_locale(language.locale), class: 'dropdown-item', data: { turbolinks: false } %>
10
10
  </li>
11
11
  <% end %>
12
12
  </ul>
@@ -5,15 +5,15 @@
5
5
  <div class="d-flex justify-content-between align-items-end">
6
6
  <h2 class="m-0"><%= group.title %></h2>
7
7
  <div class="pl-5">
8
- <%= link_to 'View all', exhibit_browse_groups_path(current_exhibit, group) %>
8
+ <%= link_to t(:'.view_all'), exhibit_browse_groups_path(current_exhibit, group) %>
9
9
  </div>
10
10
  </div>
11
11
  <% if group.searches.count > 1 %>
12
12
  <ul class="browse-group-categories-controls p-0 pt-2 pt-md-0 ml-md-auto mb-0 d-flex justify-content-center align-items-end">
13
- <li class="prev px-1">
13
+ <li class="prev px-1 overflow-hidden">
14
14
  <%= blacklight_icon 'arrow-alt-circle-left' %>
15
15
  </li>
16
- <li class="next px-1">
16
+ <li class="next px-1 overflow-hidden">
17
17
  <%= blacklight_icon 'arrow-alt-circle-right' %>
18
18
  </li>
19
19
  </ul>
@@ -34,6 +34,7 @@
34
34
  <span class="item-count"><%= t(:'.items', count: search.count) %></span>
35
35
  <% end %>
36
36
  </div>
37
+ <div class="hover-overlay"></div>
37
38
  </div>
38
39
  <% end %>
39
40
  </div>
@@ -34,6 +34,9 @@ ar:
34
34
  zero_results:
35
35
  expand_html: "تستطيع أن <a href=\"%{clear_search_url}\"> تمسح استعلام البحث </a> أو محاولة <a href=\"%{expand_search_url}\"> البحث في كل المواد المعروضة لاستعلام البحث ل\"%{browse_query}\"</a>"
36
36
  result_number: لم يتطابق بحثك مع أي عنصر في هذه الفئة.
37
+ exhibits:
38
+ groups:
39
+ all: الكل
37
40
  header_links:
38
41
  contact: ملاحظات واقتراحات
39
42
  login: تسجيل الدخول
@@ -52,7 +55,14 @@ ar:
52
55
  few: "%{count} عناصر"
53
56
  many: "%{count} عنصراً"
54
57
  other: "%{count} عنصراً"
55
-
58
+ browse_group_categories_block:
59
+ items:
60
+ one: "%{count} عنصر"
61
+ two: "%{count} عنصرين"
62
+ few: "%{count} عناصر"
63
+ many: "%{count} عنصراً"
64
+ other: "%{count} عنصراً"
65
+ view_all: مشاهدة الكل
56
66
  link_to_search_block:
57
67
  items:
58
68
  zero: "%{count} عنصر"
@@ -167,10 +167,10 @@ en:
167
167
  zh: Chinese
168
168
  reindexing_log:
169
169
  status:
170
+ completed: Successful
171
+ enqueued: Not yet started
170
172
  failed: Failed
171
173
  in_progress: In progress
172
- succeeded: Successful
173
- unstarted: Not yet started
174
174
  shared:
175
175
  site_sidebar:
176
176
  documentation: Curator documentation
@@ -765,6 +765,7 @@ en:
765
765
  items:
766
766
  one: "%{count} item"
767
767
  other: "%{count} items"
768
+ view_all: View all
768
769
  link_to_search_block:
769
770
  items:
770
771
  one: "%{count} item"
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateJobTrackers < ActiveRecord::Migration[5.2]
4
+ def change
5
+ create_table :spotlight_job_trackers do |t|
6
+ t.references :on, null: false, polymorphic: true, index: true
7
+ t.references :resource, null: false, polymorphic: true, index: true
8
+ t.string :job_id
9
+ t.string :job_class
10
+ t.string :parent_job_id
11
+ t.string :parent_job_class
12
+ t.string :status
13
+ t.references :user
14
+ t.text :log
15
+ t.text :data
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :spotlight_job_trackers, :job_id
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateEvents < ActiveRecord::Migration[5.2]
4
+ def change
5
+ create_table :spotlight_events do |t|
6
+ t.references :exhibit, null: false, polymorphic: true
7
+ t.references :resource, null: false, polymorphic: true, index: true
8
+ t.string :type
9
+ t.string :collation_key
10
+ t.text :data
11
+
12
+ t.timestamps
13
+ end
14
+ end
15
+ end
@@ -7,23 +7,15 @@ module Spotlight
7
7
  # spotlight:scaffold_resource generator
8
8
  class ScaffoldResource < Rails::Generators::NamedBase
9
9
  source_root File.expand_path('templates', __dir__)
10
- def create_document_builder
11
- create_file "app/services/#{file_name}_builder.rb", <<-FILE.strip_heredoc
12
- class #{class_name}Builder < Spotlight::SolrDocumentBuilder
13
- def to_solr
14
- return to_enum(:to_solr) unless block_given?
15
-
16
- # TODO: your implementation here
17
- # yield { id: resource.id }
18
- end
19
- end
20
- FILE
21
- end
22
10
 
23
11
  def create_model
24
12
  create_file "app/models/#{file_name}_resource.rb", <<-FILE.strip_heredoc
25
13
  class #{class_name}Resource < Spotlight::Resource
26
- self.document_builder_class = #{class_name}Builder
14
+ def self.indexing_pipeline
15
+ @indexing_pipeline ||= super.dup.tap do |pipeline|
16
+ # your pipeline here...
17
+ end
18
+ end
27
19
  end
28
20
  FILE
29
21
  end