bulkrax 5.2.1 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -4
  3. data/app/assets/javascripts/bulkrax/navtabs.js.erb +9 -0
  4. data/app/controllers/bulkrax/exporters_controller.rb +2 -1
  5. data/app/controllers/bulkrax/importers_controller.rb +2 -1
  6. data/app/factories/bulkrax/object_factory.rb +5 -3
  7. data/app/jobs/bulkrax/create_relationships_job.rb +59 -16
  8. data/app/matchers/bulkrax/application_matcher.rb +4 -3
  9. data/app/models/bulkrax/csv_entry.rb +18 -13
  10. data/app/models/bulkrax/importer.rb +2 -3
  11. data/app/parsers/bulkrax/csv_parser.rb +2 -2
  12. data/app/parsers/bulkrax/parser_export_record_set.rb +72 -55
  13. data/app/views/bulkrax/entries/show.html.erb +5 -5
  14. data/app/views/bulkrax/importers/_bagit_fields.html.erb +6 -5
  15. data/app/views/bulkrax/importers/_csv_fields.html.erb +2 -1
  16. data/app/views/bulkrax/importers/_oai_fields.html.erb +5 -4
  17. data/app/views/bulkrax/importers/_xml_fields.html.erb +11 -10
  18. data/app/views/bulkrax/importers/edit.html.erb +4 -2
  19. data/app/views/bulkrax/importers/new.html.erb +4 -2
  20. data/app/views/hyrax/dashboard/sidebar/_bulkrax_sidebar_additions.html.erb +4 -2
  21. data/app/views/hyrax/dashboard/sidebar/_repository_content.html.erb +4 -2
  22. data/config/locales/bulkrax.en.yml +11 -6
  23. data/db/migrate/20210806044408_remove_unused_last_error.rb +3 -3
  24. data/db/migrate/20230608153601_add_indices_to_bulkrax.rb +14 -0
  25. data/lib/bulkrax/engine.rb +10 -9
  26. data/lib/bulkrax/version.rb +1 -1
  27. data/lib/bulkrax.rb +13 -1
  28. data/lib/generators/bulkrax/templates/config/initializers/bulkrax.rb +2 -2
  29. data/lib/tasks/bulkrax_tasks.rake +102 -0
  30. metadata +47 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a811bef32bb83948c7ea8fe9cbc3aead7e9fa3e09f88fe6d3ec45e4cdef7461
4
- data.tar.gz: 253314c9d1a35505b50ad70e6fb0f9dec0f5331a55cdee6555cb137fff697e08
3
+ metadata.gz: 56ac96f944d880492d7f7aa50870557ba81cf2ed23875751509b3ec23411644b
4
+ data.tar.gz: f18c9fa1d5a968d9214587476f2f949e04015e1402f6cb2ac516616669567c0e
5
5
  SHA512:
6
- metadata.gz: 3c8281a0c12778d8db9a6e3e8dc39a9c591e2d81e9d668b9b52da400b65e48012f40b51b86aa75182fc26b7bf8d20e761af7a00b6c1c798df34dba35da7890fc
7
- data.tar.gz: d71d16a09cfa1d9b0c3954bf2be9f2778bacdaa98e0a99d301c0b0762d8be04b0213f99463e05e6aab33321b64a9284551b32a1435e97569097e32c23e74f95d
6
+ metadata.gz: 0556a64e91eb41a27597b717fbf9e1de0682d666f116397e64a1b58a64fffaaadfd2dac645fbd256e3bd14ffb9065ce5f0ceaac920296f6623629199a6e130b5
7
+ data.tar.gz: dada612e0ee38d0b74ceacedc5c034cb39e26111edc90c39afd0d04338f77c1e0c0857a4d983bd7bd24fe8d7e601e2b8a6c32242e3142b2faad3a3369cbf82c1
data/README.md CHANGED
@@ -17,6 +17,7 @@ And then execute:
17
17
  ```bash
18
18
  $ bundle install
19
19
  $ rails generate bulkrax:install
20
+ $ rails db:migrate
20
21
  ```
21
22
 
22
23
  If using Sidekiq, set up queues for `import` and `export`.
@@ -32,6 +33,7 @@ gem 'bulkrax'
32
33
  And then execute:
33
34
  ```bash
34
35
  $ bundle install
36
+ $ rails db:migrate
35
37
  ```
36
38
 
37
39
  Mount the engine in your routes file
@@ -61,7 +63,7 @@ If using Sidekiq, set up queues for `import` and `export`.
61
63
  *= require 'bulkrax/application'
62
64
  ```
63
65
 
64
- You'll want to add an intializer to configure the importer to your needs:
66
+ You'll want to add an initializer to configure the importer to your needs:
65
67
 
66
68
  ```ruby
67
69
  # config/initializers/bulkrax.rb
@@ -112,13 +114,13 @@ An Import needs to know what Work Type to create. The importer looks for:
112
114
 
113
115
  If it does not find either of these, or the data they contain is not a valid Work Type in the repository, the `default_work_type` will be used.
114
116
 
115
- The install generator sets `default_work_type` to the first Work Type returned by `Hyrax.config.curation_concerns` but this can be overriden by setting `default_work_type` in `config/initializer/bulkrax.rb` as shown above.
117
+ The install generator sets `default_work_type` to the first Work Type returned by `Hyrax.config.curation_concerns` (stringified), but this can be overwritten by setting `default_work_type` in `config/initializer/bulkrax.rb` as shown above.
116
118
 
117
119
  ## Configuring Field Mapping
118
120
 
119
121
  It's unlikely that the incoming import data has fields that exactly match those in your repository. Field mappings allow you to tell bulkrax how to map field in the incoming data to a field in your application.
120
122
 
121
- By default, a mapping for the OAI parser has been added to map standard oai_dc fields to Hyrax basic_metadata. The other parsers have no default mapping, and will map any incoming fields to Hyrax properties with the same name. Configurations can be added in `config/intializers/bulkrax.rb`
123
+ By default, a mapping for the OAI parser has been added to map standard oai_dc fields to Hyrax basic_metadata. The other parsers have no default mapping, and will map any incoming fields to Hyrax properties with the same name. Configurations can be added in `config/initializers/bulkrax.rb`
122
124
 
123
125
  Configuring field mappings is documented in the [Bulkrax Configuration Guide](https://github.com/samvera-labs/bulkrax/wiki/Configuring-Bulkrax).
124
126
 
@@ -176,7 +178,7 @@ To edit an importer or exporter, select the edit icon (pencil) and complete the
176
178
  To delete an importer or exporter, select the delete (x) icon.
177
179
 
178
180
  ### Downloading an export
179
- Once your the exporter has run, a download icon will apear on the exporters menu page.
181
+ Once your the exporter has run, a download icon will appear on the exporters menu page.
180
182
 
181
183
  ## Contributing
182
184
  If you're working on a PR for this project, create a feature branch off of `main`.
@@ -0,0 +1,9 @@
1
+ <% unless defined?(::Hyku) %>
2
+ // enables the tabs in the importers/exporters pages.
3
+ $(document).ready(function() {
4
+ $('.nav-tabs a').click(function (e) {
5
+ e.preventDefault();
6
+ $(this).tab('show');
7
+ });
8
+ });
9
+ <% end %>
@@ -13,7 +13,8 @@ module Bulkrax
13
13
 
14
14
  # GET /exporters
15
15
  def index
16
- @exporters = Exporter.all
16
+ # NOTE: We're paginating this in the browser.
17
+ @exporters = Exporter.order(created_at: :desc).all
17
18
 
18
19
  add_exporter_breadcrumbs if defined?(::Hyrax)
19
20
  end
@@ -20,7 +20,8 @@ module Bulkrax
20
20
 
21
21
  # GET /importers
22
22
  def index
23
- @importers = Importer.all
23
+ # NOTE: We're paginating this in the browser.
24
+ @importers = Importer.order(created_at: :desc).all
24
25
  if api_request?
25
26
  json_response('index')
26
27
  elsif defined?(::Hyrax)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bulkrax
4
- class ObjectFactory
4
+ class ObjectFactory # rubocop:disable Metrics/ClassLength
5
5
  extend ActiveModel::Callbacks
6
6
  include Bulkrax::FileFactory
7
7
  include DynamicRecordLookup
@@ -87,7 +87,8 @@ module Bulkrax
87
87
  end
88
88
 
89
89
  def find
90
- return find_by_id if attributes[:id].present?
90
+ found = find_by_id if attributes[:id].present?
91
+ return found if found.present?
91
92
  return search_by_identifier if attributes[work_identifier].present?
92
93
  end
93
94
 
@@ -102,7 +103,8 @@ module Bulkrax
102
103
  end
103
104
 
104
105
  def search_by_identifier
105
- query = { work_identifier =>
106
+ work_index = ::ActiveFedora.index_field_mapper.solr_name(work_identifier, :facetable)
107
+ query = { work_index =>
106
108
  source_identifier_value }
107
109
  # Query can return partial matches (something6 matches both something6 and something68)
108
110
  # so we need to weed out any that are not the correct full match. But other items might be
@@ -61,38 +61,82 @@ module Bulkrax
61
61
  number_of_successes = 0
62
62
  number_of_failures = 0
63
63
  errors = []
64
-
65
- ActiveRecord::Base.uncached do
66
- Bulkrax::PendingRelationship.where(parent_id: parent_identifier, importer_run_id: importer_run_id)
67
- .ordered.find_each do |rel|
68
- process(relationship: rel, importer_run_id: importer_run_id, parent_record: parent_record, ability: ability)
69
- number_of_successes += 1
70
- rescue => e
71
- number_of_failures += 1
72
- errors << e
64
+ @parent_record_members_added = false
65
+ @child_members_added = []
66
+
67
+ if parent_record
68
+ conditionally_acquire_lock_for(parent_record.id) do
69
+ ActiveRecord::Base.uncached do
70
+ Bulkrax::PendingRelationship.where(parent_id: parent_identifier, importer_run_id: importer_run_id)
71
+ .ordered.find_each do |rel|
72
+ process(relationship: rel, importer_run_id: importer_run_id, parent_record: parent_record, ability: ability)
73
+ number_of_successes += 1
74
+ rescue => e
75
+ number_of_failures += 1
76
+ errors << e
77
+ end
78
+ end
79
+
80
+ # save record if members were added
81
+ if @parent_record_members_added
82
+ parent_record.save!
83
+ # Ensure that the new relationship gets indexed onto the children
84
+ @child_members_added.each(&:update_index)
85
+ end
73
86
  end
87
+ else
88
+ # In moving the check of the parent record "up" we've exposed a hidden reporting foible.
89
+ # Namely we were reporting one error per child record when the parent record was itself
90
+ # unavailable.
91
+ #
92
+ # We have chosen not to duplicate that "number of errors" as it does not seem like the
93
+ # correct pattern for reporting a singular error (the previous pattern being one error per
94
+ # child who's parent is not yet created).
95
+ number_of_failures = 1
96
+ errors = ["Parent record not yet available for creating relationships with children records."]
74
97
  end
75
98
 
76
- # save record if members were added
77
- parent_record.save! if @parent_record_members_added
78
-
79
- # rubocop:disable Rails/SkipsModelValidations
80
99
  if errors.present?
100
+ # rubocop:disable Rails/SkipsModelValidations
81
101
  importer_run.increment!(:failed_relationships, number_of_failures)
102
+ # rubocop:enable Rails/SkipsModelValidations
103
+
82
104
  parent_entry&.set_status_info(errors.last, importer_run)
83
105
 
84
106
  # TODO: This can create an infinite job cycle, consider a time to live tracker.
85
107
  reschedule({ parent_identifier: parent_identifier, importer_run_id: importer_run_id })
86
108
  return false # stop current job from continuing to run after rescheduling
87
109
  else
110
+ # rubocop:disable Rails/SkipsModelValidations
88
111
  Bulkrax::ImporterRun.find(importer_run_id).increment!(:processed_relationships, number_of_successes)
112
+ # rubocop:enable Rails/SkipsModelValidations
89
113
  end
90
- # rubocop:enable Rails/SkipsModelValidations
91
114
  end
92
115
  # rubocop:enable Metrics/MethodLength
93
116
 
94
117
  private
95
118
 
119
+ ##
120
+ # We can use Hyrax's lock manager when we have one available.
121
+ if defined?(::Hyrax)
122
+ include Hyrax::Lockable
123
+
124
+ def conditionally_acquire_lock_for(*args, &block)
125
+ if Bulkrax.use_locking?
126
+ acquire_lock_for(*args, &block)
127
+ else
128
+ yield
129
+ end
130
+ end
131
+ else
132
+ # Otherwise, we're providing no meaningful lock manager at this time.
133
+ def acquire_lock_for(*)
134
+ yield
135
+ end
136
+
137
+ alias conditionally_acquire_lock_for acquire_lock_for
138
+ end
139
+
96
140
  def process(relationship:, importer_run_id:, parent_record:, ability:)
97
141
  raise "#{relationship} needs a child to create relationship" if relationship.child_id.nil?
98
142
  raise "#{relationship} needs a parent to create relationship" if relationship.parent_id.nil?
@@ -124,8 +168,7 @@ module Bulkrax
124
168
 
125
169
  parent_record.ordered_members << child_record
126
170
  @parent_record_members_added = true
127
- # TODO: Do we need to save the child record?
128
- child_record.save!
171
+ @child_members_added << child_record
129
172
  end
130
173
 
131
174
  def reschedule(parent_identifier:, importer_run_id:)
@@ -26,7 +26,8 @@ module Bulkrax
26
26
 
27
27
  # @result will evaluate to an empty string for nil content values
28
28
  @result = content.to_s.gsub(/\s/, ' ').strip # remove any line feeds and tabs
29
- process_split if @result.present?
29
+ # blank needs to be based to split, only skip nil
30
+ process_split unless @result.nil?
30
31
  @result = @result[0] if @result.is_a?(Array) && @result.size == 1
31
32
  process_parse
32
33
  return @result
@@ -36,8 +37,8 @@ module Bulkrax
36
37
  if self.split.is_a?(TrueClass)
37
38
  @result = @result.split(Bulkrax.multi_value_element_split_on)
38
39
  elsif self.split
39
- result = @result.split(Regexp.new(self.split))
40
- @result = result.map(&:strip)
40
+ @result = @result.split(Regexp.new(self.split))
41
+ @result = @result.map(&:strip).select(&:present?)
41
42
  end
42
43
  end
43
44
 
@@ -20,7 +20,7 @@ module Bulkrax
20
20
  raise StandardError, 'CSV path empty' if path.blank?
21
21
  options = {
22
22
  headers: true,
23
- header_converters: ->(h) { h.to_sym },
23
+ header_converters: ->(h) { h.to_s.strip.to_sym },
24
24
  encoding: 'utf-8'
25
25
  }.merge(csv_read_data_options)
26
26
 
@@ -243,20 +243,17 @@ module Bulkrax
243
243
  object_metadata(Array.wrap(data))
244
244
  end
245
245
 
246
- def build_value(key, value)
247
- return unless hyrax_record.respond_to?(key.to_s)
246
+ def build_value(property_name, mapping_config)
247
+ return unless hyrax_record.respond_to?(property_name.to_s)
248
248
 
249
- data = hyrax_record.send(key.to_s)
250
- if data.is_a?(ActiveTriples::Relation)
251
- if value['join']
252
- self.parsed_metadata[key_for_export(key)] = data.map { |d| prepare_export_data(d) }.join(Bulkrax.multi_value_element_join_on).to_s
253
- else
254
- data.each_with_index do |d, i|
255
- self.parsed_metadata["#{key_for_export(key)}_#{i + 1}"] = prepare_export_data(d)
256
- end
257
- end
249
+ data = hyrax_record.send(property_name.to_s)
250
+
251
+ if mapping_config['join'] || !data.is_a?(Enumerable)
252
+ self.parsed_metadata[key_for_export(property_name)] = prepare_export_data_with_join(data)
258
253
  else
259
- self.parsed_metadata[key_for_export(key)] = prepare_export_data(data)
254
+ data.each_with_index do |d, i|
255
+ self.parsed_metadata["#{key_for_export(property_name)}_#{i + 1}"] = prepare_export_data(d)
256
+ end
260
257
  end
261
258
  end
262
259
 
@@ -269,6 +266,14 @@ module Bulkrax
269
266
  "#{unnumbered_key}#{key.sub(clean_key, '')}"
270
267
  end
271
268
 
269
+ def prepare_export_data_with_join(data)
270
+ # Yes...it's possible we're asking to coerce a multi-value but only have a single value.
271
+ return data.to_s unless data.is_a?(Enumerable)
272
+ return "" if data.empty?
273
+
274
+ data.map { |d| prepare_export_data(d) }.join(Bulkrax.multi_value_element_join_on).to_s
275
+ end
276
+
272
277
  def prepare_export_data(datum)
273
278
  if datum.is_a?(ActiveTriples::Resource)
274
279
  datum.to_uri.to_s
@@ -24,13 +24,12 @@ module Bulkrax
24
24
  attr_writer :current_run
25
25
 
26
26
  def self.safe_uri_filename(uri)
27
- uri = URI.parse(uri) unless uri.is_a?(URI)
28
27
  r = Faraday.head(uri.to_s)
29
28
  return CGI.parse(r.headers['content-disposition'])["filename"][0].delete("\"")
30
29
  rescue
31
- filename = File.basename(uri.path)
30
+ filename = File.basename(uri.to_s)
32
31
  filename.delete!('/')
33
- filename.presence || file_set.id
32
+ filename.presence || SecureRandom.uuid
34
33
  end
35
34
 
36
35
  def status
@@ -33,9 +33,9 @@ module Bulkrax
33
33
  model_field_mappings.map(&:to_sym).each do |model_mapping|
34
34
  next unless r.key?(model_mapping)
35
35
 
36
- if r[model_mapping].casecmp('collection').zero?
36
+ if r[model_mapping].strip.casecmp('collection').zero?
37
37
  @collections << r
38
- elsif r[model_mapping].casecmp('fileset').zero?
38
+ elsif r[model_mapping].strip.casecmp('fileset').zero?
39
39
  @file_sets << r
40
40
  else
41
41
  @works << r
@@ -21,6 +21,30 @@ module Bulkrax
21
21
  "Bulkrax::ParserExportRecordSet::#{export_from.classify}".constantize.new(parser: parser)
22
22
  end
23
23
 
24
+ SOLR_QUERY_PAGE_SIZE = 512
25
+
26
+ ##
27
+ # A helper method for handling querying large batches of IDs. By default SOLR has a max of 1024
28
+ # `OR` clauses per query. This method helps chunk large sets of IDs into batches.
29
+ #
30
+ # @param array [Array<Object>]
31
+ # @param page_size [Integer]
32
+ # @yieldparam [Array<Object>] slice of the original arrays which are yielded. The results of
33
+ # the yield are merged into the return value.
34
+ #
35
+ # @return [Array<Object>]
36
+ #
37
+ # @see https://github.com/samvera-labs/bulkrax/issues/776
38
+ def self.in_batches(array, page_size: SOLR_QUERY_PAGE_SIZE)
39
+ array = Array.wrap(array)
40
+ return [] if array.empty?
41
+ results = []
42
+ array.each_slice(page_size) do |slice|
43
+ results += Array.wrap(yield(slice))
44
+ end
45
+ results
46
+ end
47
+
24
48
  # @abstract
25
49
  #
26
50
  # @note This has {#each} and {#count} but is not an Enumerable. But because it has these two
@@ -36,6 +60,7 @@ module Bulkrax
36
60
  delegate :limit_reached?, :work_entry_class, :collection_entry_class, :file_set_entry_class, :importerexporter, to: :parser
37
61
  private :limit_reached?, :work_entry_class, :collection_entry_class, :file_set_entry_class, :importerexporter
38
62
 
63
+ ##
39
64
  # @return [Integer]
40
65
  def count
41
66
  sum = works.count + collections.count + file_sets.count
@@ -44,6 +69,7 @@ module Bulkrax
44
69
  return sum
45
70
  end
46
71
 
72
+ ##
47
73
  # Yield first the works, then collections, then file sets. Once we've yielded as many times
48
74
  # as the parser's limit, we break the iteration and return.
49
75
  #
@@ -134,8 +160,6 @@ module Bulkrax
134
160
  end
135
161
  end
136
162
 
137
- SOLR_QUERY_PAGE_SIZE = 512
138
-
139
163
  # @note In most cases, when we don't have any candidate file sets, there is no need to query SOLR.
140
164
  #
141
165
  # @see Bulkrax::ParserExportRecordSet::Importer#file_sets
@@ -148,20 +172,14 @@ module Bulkrax
148
172
  # @see https://github.com/scientist-softserv/britishlibrary/issues/289
149
173
  # @see https://github.com/samvera/hyrax/blob/64c0bbf0dc0d3e1b49f040b50ea70d177cc9d8f6/app/indexers/hyrax/work_indexer.rb#L15-L18
150
174
  def file_sets
151
- @file_sets ||= if candidate_file_set_ids.empty?
152
- []
153
- else
154
- results = []
155
- candidate_file_set_ids.each_slice(SOLR_QUERY_PAGE_SIZE) do |ids|
156
- fsq = "has_model_ssim:#{Bulkrax.file_model_class} AND id:(\"" + ids.join('" OR "') + "\")"
157
- fsq += extra_filters if extra_filters.present?
158
- results += ActiveFedora::SolrService.query(
159
- fsq,
160
- { fl: "id", method: :post, rows: ids.size }
161
- )
162
- end
163
- results
164
- end
175
+ @file_sets ||= ParserExportRecordSet.in_batches(candidate_file_set_ids) do |batch_of_ids|
176
+ fsq = "has_model_ssim:#{Bulkrax.file_model_class} AND id:(\"" + batch_of_ids.join('" OR "') + "\")"
177
+ fsq += extra_filters if extra_filters.present?
178
+ ActiveFedora::SolrService.query(
179
+ fsq,
180
+ { fl: "id", method: :post, rows: batch_of_ids.size }
181
+ )
182
+ end
165
183
  end
166
184
 
167
185
  def solr_name(base_name)
@@ -227,32 +245,34 @@ module Bulkrax
227
245
  end
228
246
  end
229
247
 
230
- def works_query_kwargs
231
- query_kwargs.merge(
232
- fq: [
233
- %(#{solr_name(work_identifier)}:("#{complete_entry_identifiers.join('" OR "')}")),
234
- "has_model_ssim:(#{Bulkrax.curation_concerns.join(' OR ')})"
235
- ],
236
- fl: 'id'
237
- )
238
- end
239
-
240
- def works_query
241
- extra_filters.to_s
242
- end
243
-
244
- def collections_query_kwargs
245
- query_kwargs.merge(
246
- fq: [
247
- %(#{solr_name(work_identifier)}:("#{complete_entry_identifiers.join('" OR "')}")),
248
- "has_model_ssim:Collection"
249
- ],
250
- fl: 'id'
251
- )
248
+ def works
249
+ @works ||= ParserExportRecordSet.in_batches(complete_entry_identifiers) do |ids|
250
+ ActiveFedora::SolrService.query(
251
+ extra_filters.to_s,
252
+ **query_kwargs.merge(
253
+ fq: [
254
+ %(#{solr_name(work_identifier)}:("#{ids.join('" OR "')}")),
255
+ "has_model_ssim:(#{Bulkrax.curation_concerns.join(' OR ')})"
256
+ ],
257
+ fl: 'id'
258
+ )
259
+ )
260
+ end
252
261
  end
253
262
 
254
- def collections_query
255
- "has_model_ssim:Collection #{extra_filters}"
263
+ def collections
264
+ @collections ||= ParserExportRecordSet.in_batches(complete_entry_identifiers) do |ids|
265
+ ActiveFedora::SolrService.query(
266
+ "has_model_ssim:Collection #{extra_filters}",
267
+ **query_kwargs.merge(
268
+ fq: [
269
+ %(#{solr_name(work_identifier)}:("#{ids.join('" OR "')}")),
270
+ "has_model_ssim:Collection"
271
+ ],
272
+ fl: "id"
273
+ )
274
+ )
275
+ end
256
276
  end
257
277
 
258
278
  # This is an exception; we don't know how many candidate file sets there might be. So we will instead
@@ -260,21 +280,18 @@ module Bulkrax
260
280
  #
261
281
  # @see Bulkrax::ParserExportRecordSet::Base#file_sets
262
282
  def file_sets
263
- @file_sets ||= ActiveFedora::SolrService.query(file_sets_query, **file_sets_query_kwargs)
264
- end
265
-
266
- def file_sets_query_kwargs
267
- query_kwargs.merge(
268
- fq: [
269
- %(#{solr_name(work_identifier)}:("#{complete_entry_identifiers.join('" OR "')}")),
270
- "has_model_ssim:#{Bulkrax.file_model_class}"
271
- ],
272
- fl: 'id'
273
- )
274
- end
275
-
276
- def file_sets_query
277
- extra_filters
283
+ @file_sets ||= ParserExportRecordSet.in_batches(complete_entry_identifiers) do |ids|
284
+ ActiveFedora::SolrService.query(
285
+ extra_filters,
286
+ query_kwargs.merge(
287
+ fq: [
288
+ %(#{solr_name(work_identifier)}:("#{ids.join('" OR "')}")),
289
+ "has_model_ssim:#{Bulkrax.file_model_class}"
290
+ ],
291
+ fl: 'id'
292
+ )
293
+ )
294
+ end
278
295
  end
279
296
  end
280
297
  end
@@ -2,17 +2,17 @@
2
2
  <div class="panel panel-default">
3
3
  <div class="panel-body">
4
4
  <p class='bulkrax-p-align'>
5
- <strong>Identifier:</strong>
5
+ <strong><%= t('bulkrax.importer.labels.identifier') %>:</strong>
6
6
  <%= @entry.identifier %>
7
7
  </p>
8
8
 
9
9
  <p class='bulkrax-p-align'>
10
- <strong>Entry ID:</strong>
10
+ <strong><%= t('bulkrax.importer.labels.entry_id') %>:</strong>
11
11
  <%= @entry.id %>
12
12
  </p>
13
13
 
14
14
  <p class='bulkrax-p-align'>
15
- <strong>Type:</strong>
15
+ <strong><%= t('bulkrax.importer.labels.type') %>:</strong>
16
16
  <%= @entry.factory_class || 'Unknown' %>
17
17
  </p>
18
18
  <%= render partial: 'raw_metadata'%>
@@ -23,10 +23,10 @@
23
23
 
24
24
  <p class="bulkrax-p-align">
25
25
  <% if @importer.present? %>
26
- <strong>Importer:</strong>
26
+ <strong><%= t('bulkrax.importer.labels.importer') %>:</strong>
27
27
  <%= link_to @importer.name, importer_path(@importer) %>
28
28
  <% elsif @exporter.present? %>
29
- <strong>Exporter:</strong>
29
+ <strong><%= t('bulkrax.importer.labels.exporter') %>:</strong>
30
30
  <%= link_to @exporter.name, exporter_path(@exporter) %>
31
31
  <% end %>
32
32
  </p>
@@ -1,15 +1,15 @@
1
1
  <div class='bagit_fields'>
2
2
 
3
- <%#= fi.input :metadata_type,
4
- collection: importer.import_metadata_type,
3
+ <%#= fi.input :metadata_type,
4
+ collection: importer.import_metadata_type,
5
5
  selected: importer.parser_fields['metadata_type'],
6
6
  include_blank: true,
7
7
  input_html: { class: 'form-control' }
8
8
  %>
9
9
  <%= fi.input :metadata_file_name, as: :string, input_html: { value: importer.parser_fields['metadata_file_name'] } %>
10
10
 
11
- <%= fi.input :metadata_format,
12
- collection: importer.import_metadata_format,
11
+ <%= fi.input :metadata_format,
12
+ collection: importer.import_metadata_format,
13
13
  selected: importer.parser_fields['metadata_format'],
14
14
  include_blank: true,
15
15
  input_html: { class: 'form-control' }
@@ -18,7 +18,8 @@
18
18
  <%= fi.input :visibility,
19
19
  collection: [
20
20
  ['Public', 'open'],
21
- ['Private', 'restricted']
21
+ ['Private', 'restricted'],
22
+ ['Institution', 'authenticated']
22
23
  ],
23
24
  selected: importer.parser_fields['visibility'] || 'open',
24
25
  input_html: { class: 'form-control' }
@@ -3,7 +3,8 @@
3
3
  <%= fi.input :visibility,
4
4
  collection: [
5
5
  ['Public', 'open'],
6
- ['Private', 'restricted']
6
+ ['Private', 'restricted'],
7
+ ['Institution', 'authenticated']
7
8
  ],
8
9
  selected: importer.parser_fields['visibility'] || 'open',
9
10
  input_html: { class: 'form-control' }
@@ -1,16 +1,17 @@
1
1
  <div class='oai_fields'>
2
2
  <%= fi.input :base_url, as: :string, input_html: { value: importer.parser_fields['base_url'] } %>
3
-
3
+
4
4
  <%= fi.input :metadata_prefix, as: :string, hint: 'Such as oai_dc, dcterms or oai_qdc', input_html: { value: importer.parser_fields['metadata_prefix'] } %>
5
-
5
+
6
6
  <%= fi.input :set, collection: [importer.parser_fields['set']], label: 'Set (source)', selected: importer.parser_fields['set'] %>
7
7
  <button type="button" class="btn btn-default refresh-set-source">Refresh Sets</button>
8
8
 
9
9
  <%= fi.input :visibility,
10
10
  collection: [
11
11
  ['Public', 'open'],
12
- ['Private', 'restricted']
13
- ],
12
+ ['Private', 'restricted'],
13
+ ['Institution', 'authenticated']
14
+ ],x
14
15
  selected: importer.parser_fields['visibility'] || 'open',
15
16
  input_html: { class: 'form-control' }
16
17
  %>
@@ -1,31 +1,32 @@
1
1
  <div class='xml_fields'>
2
2
 
3
- <%# @todo improve on this implementation.
4
- As it stands, it's a hostage to namespaces,
5
- eg. dc:title
3
+ <%# @todo improve on this implementation.
4
+ As it stands, it's a hostage to namespaces,
5
+ eg. dc:title
6
6
  if namespaces aren't in the xml, we would have to specify dc:title
7
7
  but if the namespaces ARE present, we remove them so we would need title
8
8
  %>
9
- <%= fi.input :record_element,
10
- hint: 'Provide the xml element name to use to identify the record, or records, eg. ROW - each record in the attached XML is wrapped in a <ROW> tag.',
9
+ <%= fi.input :record_element,
10
+ hint: 'Provide the xml element name to use to identify the record, or records, eg. ROW - each record in the attached XML is wrapped in a <ROW> tag.',
11
11
  input_html: { value: importer.parser_fields['record_element'] }
12
12
  %>
13
13
 
14
- <%= fi.input :import_type,
14
+ <%= fi.input :import_type,
15
15
  collection: [
16
16
  ['Single Work per Metadata File', 'single'],
17
- ['Multiple Works per Metadata File', 'multiple']
18
- ],
17
+ ['Multiple Works per Metadata File', 'multiple']
18
+ ],
19
19
  selected: importer.parser_fields['import_type'],
20
20
  input_html: { class: 'form-control' }
21
21
  %>
22
-
22
+
23
23
  <h4>Visiblity</h4>
24
24
 
25
25
  <%= fi.input :visibility,
26
26
  collection: [
27
27
  ['Public', 'open'],
28
- ['Private', 'restricted']
28
+ ['Private', 'restricted'],
29
+ ['Institution', 'authenticated']
29
30
  ],
30
31
  selected: importer.parser_fields['visibility'] || 'open',
31
32
  input_html: { class: 'form-control' }
@@ -10,7 +10,9 @@
10
10
  <%= render 'form', importer: @importer, form: form %>
11
11
  <div class="panel-footer">
12
12
  <div class='pull-right'>
13
- <%= link_to 'Update Importer', '#bulkraxModal', class: "btn btn-primary", data: { toggle: 'modal' } %>
13
+ <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#bulkraxModal">
14
+ Update Importer
15
+ </button>
14
16
  <%= render 'edit_form_buttons', form: form %>
15
17
  <% cancel_path = form.object.persisted? ? importer_path(form.object) : importers_path %>
16
18
  | <%= link_to t('.cancel'), cancel_path, class: 'btn btn-default ' %>
@@ -19,4 +21,4 @@
19
21
  <% end %>
20
22
  </div>
21
23
  </div>
22
- </div>
24
+ </div>
@@ -9,8 +9,10 @@
9
9
  <%= render 'form', importer: @importer, form: form %>
10
10
  <div class="panel-footer">
11
11
  <div class='pull-right'>
12
- <%= form.button :submit, value: 'Create and Validate', class: 'btn btn-primary' %>
13
- |
12
+ <% if ENV['SHOW_CREATE_AND_VALIDATE'] == 'true' %>
13
+ <%= form.button :submit, value: 'Create and Validate', class: 'btn btn-primary' %>
14
+ |
15
+ <% end %>
14
16
  <%= form.button :submit, value: 'Create and Import', class: 'btn btn-primary' %>
15
17
  |
16
18
  <%= form.button :submit, value: 'Create', class: 'btn btn-primary' %>
@@ -1,10 +1,12 @@
1
1
  <% if current_ability.can_import_works? %>
2
- <%= menu.nav_link(bulkrax.importers_path) do %>
2
+ <%= menu.nav_link(bulkrax.importers_path,
3
+ title: t('bulkrax.admin.sidebar.importers')) do %>
3
4
  <span class="fa fa-cloud-upload" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('bulkrax.admin.sidebar.importers') %></span>
4
5
  <% end %>
5
6
  <% end %>
6
7
  <% if current_ability.can_export_works? %>
7
- <%= menu.nav_link(bulkrax.exporters_path) do %>
8
+ <%= menu.nav_link(bulkrax.exporters_path,
9
+ title: t('bulkrax.admin.sidebar.exporters')) do %>
8
10
  <span class="fa fa-cloud-download" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('bulkrax.admin.sidebar.exporters') %></span>
9
11
  <% end %>
10
12
  <% end %>
@@ -3,14 +3,16 @@
3
3
  <%= menu.nav_link(hyrax.my_collections_path,
4
4
  class: "nav-link",
5
5
  onclick: "dontChangeAccordion(event);",
6
- also_active_for: hyrax.dashboard_collections_path) do %>
6
+ also_active_for: hyrax.dashboard_collections_path,
7
+ title: t('hyrax.admin.sidebar.collections')) do %>
7
8
  <span class="fa fa-folder-open" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.collections') %></span>
8
9
  <% end %>
9
10
 
10
11
  <%= menu.nav_link(hyrax.my_works_path,
11
12
  class: "nav-link",
12
13
  onclick: "dontChangeAccordion(event);",
13
- also_active_for: hyrax.dashboard_works_path) do %>
14
+ also_active_for: hyrax.dashboard_works_path,
15
+ title: t('hyrax.admin.sidebar.works')) do %>
14
16
  <span class="fa fa-file" aria-hidden="true"></span> <span class="sidebar-action-text"><%= t('hyrax.admin.sidebar.works') %></span>
15
17
  <% end %>
16
18
 
@@ -46,18 +46,23 @@ en:
46
46
  generated_metadata: "These exported fields currently cannot be imported."
47
47
  importer:
48
48
  labels:
49
- name: Name
50
- user: User
51
49
  admin_set: Admin set
50
+ collection_entries: Collection Entries
51
+ entry_id: Entry ID
52
+ exporter: Exporter
53
+ file_set_entries: File Set Entries
52
54
  frequency: Frequency
53
- parser_klass: Parser klass
55
+ identifier: Identifier
56
+ importer: Importer
54
57
  limit: Limit
55
- total_work_entries: Total Works
58
+ name: Name
59
+ parser_klass: Parser klass
56
60
  total_collections: Total Collections
57
61
  total_file_sets: Total File Sets
62
+ total_work_entries: Total Works
63
+ type: Type
64
+ user: User
58
65
  work_entries: Work Entries
59
- collection_entries: Collection Entries
60
- file_set_entries: File Set Entries
61
66
  table_header:
62
67
  labels:
63
68
  identifier: Identifier
@@ -1,7 +1,7 @@
1
1
  class RemoveUnusedLastError < ActiveRecord::Migration[5.1]
2
2
  def change
3
- remove_column :bulkrax_entries, :last_error
4
- remove_column :bulkrax_exporters, :last_error
5
- remove_column :bulkrax_importers, :last_error
3
+ remove_column :bulkrax_entries, :last_error if column_exists?(:bulkrax_entries, :last_error)
4
+ remove_column :bulkrax_exporters, :last_error if column_exists?(:bulkrax_exporters, :last_error)
5
+ remove_column :bulkrax_importers, :last_error if column_exists?(:bulkrax_importers, :last_error)
6
6
  end
7
7
  end
@@ -0,0 +1,14 @@
1
+ class AddIndicesToBulkrax < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_index :bulkrax_entries, :identifier unless index_exists?(:bulkrax_entries, :identifier)
4
+ add_index :bulkrax_entries, :type unless index_exists?(:bulkrax_entries, :type)
5
+ add_index :bulkrax_entries, [:importerexporter_id, :importerexporter_type], name: 'bulkrax_entries_importerexporter_idx' unless index_exists?(:bulkrax_entries, [:importerexporter_id, :importerexporter_type], name: 'bulkrax_entries_importerexporter_idx')
6
+
7
+ add_index :bulkrax_pending_relationships, :parent_id unless index_exists?(:bulkrax_pending_relationships, :parent_id)
8
+ add_index :bulkrax_pending_relationships, :child_id unless index_exists?(:bulkrax_pending_relationships, :child_id)
9
+
10
+ add_index :bulkrax_statuses, [:statusable_id, :statusable_type], name: 'bulkrax_statuses_statusable_idx' unless index_exists?(:bulkrax_statuses, [:statusable_id, :statusable_type], name: 'bulkrax_statuses_statusable_idx')
11
+ add_index :bulkrax_statuses, [:runnable_id, :runnable_type], name: 'bulkrax_statuses_runnable_idx' unless index_exists?(:bulkrax_statuses, [:runnable_id, :runnable_type], name: 'bulkrax_statuses_runnable_idx')
12
+ add_index :bulkrax_statuses, :error_class unless index_exists?(:bulkrax_statuses, :error_class)
13
+ end
14
+ end
@@ -23,15 +23,16 @@ module Bulkrax
23
23
  end
24
24
 
25
25
  config.after_initialize do
26
- my_engine_root = Bulkrax::Engine.root.to_s
27
- paths = ActionController::Base.view_paths.collect(&:to_s)
28
- hyrax_path = paths.detect { |path| path.match(/\/hyrax-[\d\.]+.*/) }
29
- paths = if hyrax_path
30
- paths.insert(paths.index(hyrax_path), my_engine_root + '/app/views')
31
- else
32
- paths.insert(0, my_engine_root + '/app/views')
33
- end
34
- ActionController::Base.view_paths = paths
26
+ # We want to ensure that Bulkrax is earlier in the lookup for view_paths than Hyrax. That is
27
+ # we favor view in Bulkrax over those in Hyrax.
28
+ if defined?(Hyrax)
29
+ my_engine_root = Bulkrax::Engine.root.to_s
30
+ hyrax_engine_root = Hyrax::Engine.root.to_s
31
+ paths = ActionController::Base.view_paths.collect(&:to_s)
32
+ hyrax_view_path = paths.detect { |path| path.match(%r{^#{hyrax_engine_root}}) }
33
+ paths.insert(paths.index(hyrax_view_path), File.join(my_engine_root, 'app', 'views')) if hyrax_view_path
34
+ ActionController::Base.view_paths = paths.uniq
35
+ end
35
36
  end
36
37
  end
37
38
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bulkrax
4
- VERSION = '5.2.1'
4
+ VERSION = '5.4.0'
5
5
  end
data/lib/bulkrax.rb CHANGED
@@ -34,6 +34,15 @@ module Bulkrax
34
34
  :required_elements,
35
35
  :reserved_properties,
36
36
  :server_name
37
+
38
+ attr_writer :use_locking
39
+
40
+ def use_locking
41
+ return @use_locking if defined?(@use_locking)
42
+
43
+ ENV.key?("REDIS_HOST")
44
+ end
45
+ alias use_locking? use_locking
37
46
  end
38
47
 
39
48
  def config
@@ -87,7 +96,10 @@ module Bulkrax
87
96
  :reserved_properties,
88
97
  :reserved_properties=,
89
98
  :server_name,
90
- :server_name=
99
+ :server_name=,
100
+ :use_locking,
101
+ :use_locking=,
102
+ :use_locking?
91
103
 
92
104
  config do |conf|
93
105
  conf.parsers = [
@@ -7,8 +7,8 @@ Bulkrax.setup do |config|
7
7
  # ]
8
8
 
9
9
  # WorkType to use as the default if none is specified in the import
10
- # Default is the first returned by Hyrax.config.curation_concerns
11
- # config.default_work_type = MyWork
10
+ # Default is the first returned by Hyrax.config.curation_concerns, stringified
11
+ # config.default_work_type = "MyWork"
12
12
 
13
13
  # Factory Class to use when generating and saving objects
14
14
  config.object_factory = Bulkrax::ObjectFactory
@@ -1,6 +1,108 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :bulkrax do
4
+ # Usage example: rails bulkrax:generate_test_csvs['5','100','GenericWork']
5
+ desc 'Generate CSVs with fake data for testing purposes'
6
+ task :generate_test_csvs, [:num_of_csvs, :csv_rows, :record_type] => :environment do |_t, args|
7
+ # NOTE: If this line throws an error, run `gem install faker` inside your Docker container
8
+ require 'faker'
9
+ require 'csv'
10
+
11
+ FileUtils.mkdir_p(Rails.root.join('tmp', 'imports'))
12
+
13
+ IGNORED_PROPERTIES = %w[
14
+ admin_set_id
15
+ alternate_ids
16
+ arkivo_checksum
17
+ created_at
18
+ date_modified
19
+ date_uploaded
20
+ depositor
21
+ embargo
22
+ has_model
23
+ head
24
+ internal_resource
25
+ label
26
+ lease
27
+ member_ids
28
+ member_of_collection_ids
29
+ modified_date
30
+ new_record
31
+ on_behalf_of
32
+ owner
33
+ proxy_depositor
34
+ rendering_ids
35
+ representative_id
36
+ state
37
+ tail
38
+ thumbnail_id
39
+ updated_at
40
+ ].freeze
41
+
42
+ BULKRAX_PROPERTIES = %w[
43
+ source_identifier
44
+ model
45
+ ].freeze
46
+
47
+ num_of_csvs = args.num_of_csvs.presence&.to_i || 5
48
+ csv_rows = args.csv_rows.presence&.to_i || 100
49
+ record_type = args.record_type.presence&.constantize || GenericWork
50
+
51
+ csv_header = if Hyrax.config.try(:use_valkyrie?)
52
+ record_type.schema.map { |k| k.name.to_s }
53
+ else
54
+ record_type.properties.keys
55
+ end
56
+
57
+ csv_header -= IGNORED_PROPERTIES
58
+ csv_header.unshift(*BULKRAX_PROPERTIES)
59
+
60
+ num_of_csvs.times do |i|
61
+ CSV.open(Rails.root.join('tmp', 'imports', "importer_#{i}.csv"), 'wb') do |csv|
62
+ csv << csv_header
63
+ csv_rows.times do |_index|
64
+ row = []
65
+ csv_header.each do |prop_name|
66
+ row << case prop_name
67
+ when 'id', 'source_identifier'
68
+ Faker::Number.number(digits: 4)
69
+ when 'model'
70
+ record_type.to_s
71
+ when 'rights_statement'
72
+ 'http://rightsstatements.org/vocab/CNE/1.0/'
73
+ when 'license'
74
+ 'https://creativecommons.org/licenses/by-nc/4.0/'
75
+ when 'based_near'
76
+ # FIXME: Set a proper :based_near value
77
+ nil
78
+ else
79
+ Faker::Lorem.sentence
80
+ end
81
+ end
82
+ csv << row
83
+ end
84
+ end
85
+ end
86
+
87
+ num_of_csvs.times do |i|
88
+ Bulkrax::Importer.create(
89
+ name: "Generated CSV #{i}",
90
+ admin_set_id: 'admin_set/default',
91
+ user_id: User.find_by(email: 'admin@example.com').id,
92
+ frequency: 'PT0S',
93
+ parser_klass: 'Bulkrax::CsvParser',
94
+ parser_fields: {
95
+ 'visibility' => 'open',
96
+ 'rights_statement' => '',
97
+ 'override_rights_statement' => '0',
98
+ 'file_style' => 'Specify a Path on the Server',
99
+ 'import_file_path' => "tmp/imports/importer_#{i}.csv",
100
+ 'update_files' => false
101
+ }
102
+ )
103
+ end
104
+ end
105
+
4
106
  desc "Remove old exported zips and create new ones with the new file structure"
5
107
  task rerun_all_exporters: :environment do
6
108
  # delete the existing folders and zip files
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bulkrax
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.2.1
4
+ version: 5.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Kaufman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-24 00:00:00.000000000 Z
11
+ date: 2023-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dry-monads
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.4.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.4.0
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: iso8601
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -238,6 +252,34 @@ dependencies:
238
252
  - - ">="
239
253
  - !ruby/object:Gem::Version
240
254
  version: '0'
255
+ - !ruby/object:Gem::Dependency
256
+ name: redis
257
+ requirement: !ruby/object:Gem::Requirement
258
+ requirements:
259
+ - - "~>"
260
+ - !ruby/object:Gem::Version
261
+ version: '4.2'
262
+ type: :development
263
+ prerelease: false
264
+ version_requirements: !ruby/object:Gem::Requirement
265
+ requirements:
266
+ - - "~>"
267
+ - !ruby/object:Gem::Version
268
+ version: '4.2'
269
+ - !ruby/object:Gem::Dependency
270
+ name: psych
271
+ requirement: !ruby/object:Gem::Requirement
272
+ requirements:
273
+ - - "~>"
274
+ - !ruby/object:Gem::Version
275
+ version: '3.3'
276
+ type: :development
277
+ prerelease: false
278
+ version_requirements: !ruby/object:Gem::Requirement
279
+ requirements:
280
+ - - "~>"
281
+ - !ruby/object:Gem::Version
282
+ version: '3.3'
241
283
  description: Bulkrax is a batteries included importer for Samvera applications. It
242
284
  currently includes support for OAI-PMH (DC and Qualified DC) and CSV out of the
243
285
  box. It is also designed to be extensible, allowing you to easily add new importers
@@ -258,6 +300,7 @@ files:
258
300
  - app/assets/javascripts/bulkrax/entries.js
259
301
  - app/assets/javascripts/bulkrax/exporters.js
260
302
  - app/assets/javascripts/bulkrax/importers.js.erb
303
+ - app/assets/javascripts/bulkrax/navtabs.js.erb
261
304
  - app/assets/stylesheets/bulkrax/accordion.scss
262
305
  - app/assets/stylesheets/bulkrax/application.css
263
306
  - app/assets/stylesheets/bulkrax/coderay.scss
@@ -395,6 +438,7 @@ files:
395
438
  - db/migrate/20220412233954_add_include_thumbnails_to_bulkrax_exporters.rb
396
439
  - db/migrate/20220413180915_add_generated_metadata_to_bulkrax_exporters.rb
397
440
  - db/migrate/20220609001128_rename_bulkrax_importer_run_to_importer_run.rb
441
+ - db/migrate/20230608153601_add_indices_to_bulkrax.rb
398
442
  - lib/bulkrax.rb
399
443
  - lib/bulkrax/engine.rb
400
444
  - lib/bulkrax/entry_spec_helper.rb
@@ -427,7 +471,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
427
471
  - !ruby/object:Gem::Version
428
472
  version: '0'
429
473
  requirements: []
430
- rubygems_version: 3.0.3
474
+ rubygems_version: 3.1.6
431
475
  signing_key:
432
476
  specification_version: 4
433
477
  summary: Import and export tool for Hyrax and Hyku