bulkrax 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56a6005830d733023f9455ebb9e0565081dacbfbf1a36ebad290d594d74499f4
4
- data.tar.gz: dbfe501d7c62a47091e23fbc9b7b7e8aff3147f88c999eb9d55bc6e6bf27ae05
3
+ metadata.gz: a35c9f6307c21a9933950a3d383ab04e5a49e15b9980d76f6cfb52a5378f5dd0
4
+ data.tar.gz: d5a58eeb2b755c68e6c66103afb5dbc649205aed73b2d85167246ca912bb07bf
5
5
  SHA512:
6
- metadata.gz: 1054dbc43420ad3fba9f75e8c88cec4b795c55a1dc5416b5b37a7a75eff994f629aa7740dda54f635be406ec2b7c4f913b1de7426cbd1a378573cd42d526e5c7
7
- data.tar.gz: 6eb81924d7587caddcea1ec4b7d71b41324336a85ea2ae6662662028ccc3c5b92f93b5aa02fe7d0a9c19cb5e16db560ff95cfc74b85f475bae0c425ecb96824c
6
+ metadata.gz: d01b9ecd45171f2b20a5c33605fafe645b7c389a141ed70b41a04ecb7bcf1d1f1c4f430ea50fda7d7ef702fd12123cfd904868ba6468a306a41eca5643061e45
7
+ data.tar.gz: 943eb873c86e40adf96cd7b0badb1ccce719f088d01aada4e18df90ec5a4f4df5b5ebfda5bfe6f74943c6fa371632937a0918f6aed70a4e14a75bcef0b7eaa0b
@@ -37,6 +37,7 @@ module Bulkrax
37
37
 
38
38
  @work_entries = @importer.entries.where(type: @importer.parser.entry_class.to_s).page(params[:work_entries_page]).per(30)
39
39
  @collection_entries = @importer.entries.where(type: @importer.parser.collection_entry_class.to_s).page(params[:collections_entries_page]).per(30)
40
+ @file_set_entries = @importer.entries.where(type: @importer.parser.file_set_entry_class.to_s).page(params[:file_set_entries_page]).per(30)
40
41
  end
41
42
  end
42
43
 
@@ -4,11 +4,13 @@ module Bulkrax
4
4
  class ObjectFactory
5
5
  extend ActiveModel::Callbacks
6
6
  include Bulkrax::FileFactory
7
+ include DynamicRecordLookup
8
+
7
9
  define_model_callbacks :save, :create
8
- attr_reader :attributes, :object, :source_identifier_value, :klass, :replace_files, :update_files, :work_identifier, :collection_field_mapping
10
+ attr_reader :attributes, :object, :source_identifier_value, :klass, :replace_files, :update_files, :work_identifier, :collection_field_mapping, :related_parents_parsed_mapping
9
11
 
10
12
  # rubocop:disable Metrics/ParameterLists
11
- def initialize(attributes:, source_identifier_value:, work_identifier:, collection_field_mapping:, replace_files: false, user: nil, klass: nil, update_files: false)
13
+ def initialize(attributes:, source_identifier_value:, work_identifier:, collection_field_mapping:, related_parents_parsed_mapping: nil, replace_files: false, user: nil, klass: nil, update_files: false)
12
14
  ActiveSupport::Deprecation.warn(
13
15
  'Creating Collections using the collection_field_mapping will no longer be supported as of Bulkrax version 3.0.' \
14
16
  ' Please configure Bulkrax to use related_parents_field_mapping and related_children_field_mapping instead.'
@@ -19,6 +21,7 @@ module Bulkrax
19
21
  @user = user || User.batch_user
20
22
  @work_identifier = work_identifier
21
23
  @collection_field_mapping = collection_field_mapping
24
+ @related_parents_parsed_mapping = related_parents_parsed_mapping
22
25
  @source_identifier_value = source_identifier_value
23
26
  @klass = klass || Bulkrax.default_work_type.constantize
24
27
  end
@@ -33,7 +36,7 @@ module Bulkrax
33
36
  arg_hash = { id: attributes[:id], name: 'UPDATE', klass: klass }
34
37
  @object = find
35
38
  if object
36
- object.reindex_extent = Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX
39
+ object.reindex_extent = Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX if object.respond_to?(:reindex_extent)
37
40
  ActiveSupport::Notifications.instrument('import.importer', arg_hash) { update }
38
41
  else
39
42
  ActiveSupport::Notifications.instrument('import.importer', arg_hash.merge(name: 'CREATE')) { create }
@@ -51,10 +54,16 @@ module Bulkrax
51
54
 
52
55
  def update
53
56
  raise "Object doesn't exist" unless object
54
- destroy_existing_files if @replace_files && klass != Collection
57
+ destroy_existing_files if @replace_files && ![Collection, FileSet].include?(klass)
55
58
  attrs = attribute_update
56
59
  run_callbacks :save do
57
- klass == Collection ? update_collection(attrs) : work_actor.update(environment(attrs))
60
+ if klass == Collection
61
+ update_collection(attrs)
62
+ elsif klass == FileSet
63
+ update_file_set(attrs)
64
+ else
65
+ work_actor.update(environment(attrs))
66
+ end
58
67
  end
59
68
  log_updated(object)
60
69
  end
@@ -90,10 +99,16 @@ module Bulkrax
90
99
  def create
91
100
  attrs = create_attributes
92
101
  @object = klass.new
93
- object.reindex_extent = Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX
102
+ object.reindex_extent = Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX if object.respond_to?(:reindex_extent)
94
103
  run_callbacks :save do
95
104
  run_callbacks :create do
96
- klass == Collection ? create_collection(attrs) : work_actor.create(environment(attrs))
105
+ if klass == Collection
106
+ create_collection(attrs)
107
+ elsif klass == FileSet
108
+ create_file_set(attrs)
109
+ else
110
+ work_actor.create(environment(attrs))
111
+ end
97
112
  end
98
113
  end
99
114
  log_created(object)
@@ -150,6 +165,35 @@ module Bulkrax
150
165
  object.save!
151
166
  end
152
167
 
168
+ # This method is heavily inspired by Hyrax's AttachFilesToWorkJob
169
+ def create_file_set(attrs)
170
+ work = find_record(attributes[related_parents_parsed_mapping].first)
171
+ work_permissions = work.permissions.map(&:to_hash)
172
+ file_set_attrs = attrs.slice(*object.attributes.keys)
173
+ object.assign_attributes(file_set_attrs)
174
+
175
+ attrs['uploaded_files'].each do |uploaded_file_id|
176
+ uploaded_file = ::Hyrax::UploadedFile.find(uploaded_file_id)
177
+ next if uploaded_file.file_set_uri.present?
178
+
179
+ actor = ::Hyrax::Actors::FileSetActor.new(object, @user)
180
+ uploaded_file.update(file_set_uri: actor.file_set.uri)
181
+ actor.file_set.permissions_attributes = work_permissions
182
+ actor.create_metadata
183
+ actor.create_content(uploaded_file)
184
+ actor.attach_to_work(work)
185
+ end
186
+
187
+ object.save!
188
+ end
189
+
190
+ def update_file_set(attrs)
191
+ file_set_attrs = attrs.slice(*object.attributes.keys)
192
+ actor = ::Hyrax::Actors::FileSetActor.new(object, @user)
193
+
194
+ actor.update_metadata(file_set_attrs)
195
+ end
196
+
153
197
  # Add child to parent's #member_collections
154
198
  # Add parent to child's #member_of_collections
155
199
  def persist_collection_memberships(parent:, child:)
@@ -17,6 +17,8 @@ module Bulkrax
17
17
  # NOTE: In the context of this job, "identifier" is used to generically refer
18
18
  # to either a record's ID or an Bulkrax::Entry's source_identifier.
19
19
  class CreateRelationshipsJob < ApplicationJob
20
+ include DynamicRecordLookup
21
+
20
22
  queue_as :import
21
23
 
22
24
  attr_accessor :base_entry, :child_record, :parent_record, :importer_run
@@ -74,39 +76,6 @@ module Bulkrax
74
76
  end
75
77
  end
76
78
 
77
- # This method allows us to create relationships with preexisting records (by their ID) OR
78
- # with records that are concurrently being imported (by their Bulkrax::Entry source_identifier).
79
- #
80
- # @param identifier [String] Work/Collection ID or Bulkrax::Entry source_identifier
81
- # @return [Work, Collection, nil] Work or Collection if found, otherwise nil
82
- def find_record(identifier)
83
- record = Entry.find_by(identifier: identifier)
84
- record ||= ::Collection.where(id: identifier).first
85
- if record.blank?
86
- available_work_types.each do |work_type|
87
- record ||= work_type.where(id: identifier).first
88
- end
89
- end
90
- record = record.factory.find if record.is_a?(Entry)
91
-
92
- record
93
- end
94
-
95
- # Check if the record is a Work
96
- def curation_concern?(record)
97
- available_work_types.include?(record.class)
98
- end
99
-
100
- # @return [Array<Class>] list of work type classes
101
- def available_work_types
102
- # If running in a Hyku app, do not reference disabled work types
103
- @available_work_types ||= if defined?(::Hyku)
104
- ::Site.instance.available_works.map(&:constantize)
105
- else
106
- ::Hyrax.config.curation_concerns
107
- end
108
- end
109
-
110
79
  def user
111
80
  @user ||= importer_run.importer.user
112
81
  end
@@ -11,9 +11,11 @@ module Bulkrax
11
11
  entry.build
12
12
  entry.save
13
13
  add_user_to_permission_template!(entry)
14
+ ImporterRun.find(args[1]).increment!(:processed_records)
14
15
  ImporterRun.find(args[1]).increment!(:processed_collections)
15
16
  ImporterRun.find(args[1]).decrement!(:enqueued_records)
16
17
  rescue => e
18
+ ImporterRun.find(args[1]).increment!(:failed_records)
17
19
  ImporterRun.find(args[1]).increment!(:failed_collections)
18
20
  ImporterRun.find(args[1]).decrement!(:enqueued_records)
19
21
  raise e
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bulkrax
4
+ class MissingParentError < ::StandardError; end
5
+ class ImportFileSetJob < ApplicationJob
6
+ include DynamicRecordLookup
7
+
8
+ queue_as :import
9
+
10
+ def perform(entry_id, importer_run_id)
11
+ entry = Entry.find(entry_id)
12
+ parent_identifier = entry.raw_metadata[entry.related_parents_raw_mapping]&.strip
13
+
14
+ validate_parent!(parent_identifier)
15
+
16
+ entry.build
17
+ if entry.succeeded?
18
+ # rubocop:disable Rails/SkipsModelValidations
19
+ ImporterRun.find(importer_run_id).increment!(:processed_records)
20
+ ImporterRun.find(importer_run_id).increment!(:processed_file_sets)
21
+ else
22
+ ImporterRun.find(importer_run_id).increment!(:failed_records)
23
+ ImporterRun.find(importer_run_id).increment!(:failed_file_sets)
24
+ # rubocop:enable Rails/SkipsModelValidations
25
+ end
26
+ ImporterRun.find(importer_run_id).decrement!(:enqueued_records) # rubocop:disable Rails/SkipsModelValidations
27
+ entry.save!
28
+
29
+ rescue MissingParentError => e
30
+ # try waiting for the parent record to be created
31
+ entry.import_attempts += 1
32
+ entry.save!
33
+ if entry.import_attempts < 5
34
+ ImportFileSetJob
35
+ .set(wait: (entry.import_attempts + 1).minutes)
36
+ .perform_later(entry_id, importer_run_id)
37
+ else
38
+ ImporterRun.find(importer_run_id).decrement!(:enqueued_records) # rubocop:disable Rails/SkipsModelValidations
39
+ entry.status_info(e)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :parent_record
46
+
47
+ def validate_parent!(parent_identifier)
48
+ # if parent_identifier is missing, it will be caught by #validate_presence_of_parent!
49
+ return if parent_identifier.blank?
50
+
51
+ find_parent_record(parent_identifier)
52
+ check_parent_exists!(parent_identifier)
53
+ check_parent_is_a_work!(parent_identifier)
54
+ end
55
+
56
+ def check_parent_exists!(parent_identifier)
57
+ raise MissingParentError, %(Unable to find a record with the identifier "#{parent_identifier}") if parent_record.blank?
58
+ end
59
+
60
+ def check_parent_is_a_work!(parent_identifier)
61
+ error_msg = %(A record with the ID "#{parent_identifier}" was found, but it was a #{parent_record.class}, which is not an valid/available work type)
62
+ raise ::StandardError, error_msg unless curation_concern?(parent_record)
63
+ end
64
+
65
+ def find_parent_record(parent_identifier)
66
+ @parent_record ||= find_record(parent_identifier)
67
+ end
68
+ end
69
+ end
@@ -10,11 +10,13 @@ module Bulkrax
10
10
  entry.build
11
11
  if entry.status == "Complete"
12
12
  ImporterRun.find(args[1]).increment!(:processed_records)
13
+ ImporterRun.find(args[1]).increment!(:processed_works)
13
14
  ImporterRun.find(args[1]).decrement!(:enqueued_records) unless ImporterRun.find(args[1]).enqueued_records <= 0 # rubocop:disable Style/IdenticalConditionalBranches
14
15
  else
15
16
  # do not retry here because whatever parse error kept you from creating a work will likely
16
17
  # keep preventing you from doing so.
17
18
  ImporterRun.find(args[1]).increment!(:failed_records)
19
+ ImporterRun.find(args[1]).increment!(:failed_works)
18
20
  ImporterRun.find(args[1]).decrement!(:enqueued_records) unless ImporterRun.find(args[1]).enqueued_records <= 0 # rubocop:disable Style/IdenticalConditionalBranches
19
21
  end
20
22
  entry.save!
@@ -20,6 +20,7 @@ module Bulkrax
20
20
 
21
21
  importer.import_collections
22
22
  importer.import_works
23
+ importer.import_file_sets
23
24
  end
24
25
 
25
26
  def unzip_imported_file(parser)
@@ -31,6 +32,7 @@ module Bulkrax
31
32
  def update_current_run_counters(importer)
32
33
  importer.current_run.total_work_entries = importer.limit || importer.parser.works_total
33
34
  importer.current_run.total_collection_entries = importer.parser.collections_total
35
+ importer.current_run.total_file_set_entries = importer.parser.file_sets_total
34
36
  importer.current_run.save!
35
37
  end
36
38
 
@@ -9,7 +9,7 @@ module Bulkrax
9
9
  # Use identifier set by CsvParser#unique_collection_identifier, which falls back
10
10
  # on the Collection's first title if record[source_identifier] is not present
11
11
  def add_identifier
12
- self.parsed_metadata[work_identifier] = self.identifier
12
+ self.parsed_metadata[work_identifier] = [self.identifier].flatten
13
13
  end
14
14
 
15
15
  def add_collection_type_gid
@@ -40,9 +40,9 @@ module Bulkrax
40
40
 
41
41
  self.parsed_metadata = {}
42
42
  add_identifier
43
- add_metadata_for_model
44
43
  add_visibility
45
44
  add_ingested_metadata
45
+ add_metadata_for_model
46
46
  add_rights_statement
47
47
  add_collections
48
48
  add_local
@@ -57,6 +57,9 @@ module Bulkrax
57
57
  def add_metadata_for_model
58
58
  if factory_class == Collection
59
59
  add_collection_type_gid
60
+ elsif factory_class == FileSet
61
+ add_path_to_file
62
+ validate_presence_of_parent!
60
63
  else
61
64
  add_file unless importerexporter.metadata_only?
62
65
  add_admin_set_id
@@ -68,7 +71,9 @@ module Bulkrax
68
71
  'Creating Collections using the collection_field_mapping will no longer be supported as of Bulkrax version 3.0.' \
69
72
  ' Please configure Bulkrax to use related_parents_field_mapping and related_children_field_mapping instead.'
70
73
  )
71
- record.sort.each do |key, value|
74
+ # we do not want to sort the values in the record before adding the metadata.
75
+ # if we do, the factory_class will be set to the default_work_type for all values that come before "model" or "work type"
76
+ record.each do |key, value|
72
77
  next if self.parser.collection_field_mapping.to_s == key_without_numbers(key)
73
78
 
74
79
  index = key[/\d+/].to_i - 1 if key[/\d+/].to_i != 0
@@ -83,7 +88,11 @@ module Bulkrax
83
88
  elsif record['file'].is_a?(Array)
84
89
  self.parsed_metadata['file'] = record['file']
85
90
  end
86
- self.parsed_metadata['file'] = self.parsed_metadata['file'].map { |f| path_to_file(f.tr(' ', '_')) }
91
+ self.parsed_metadata['file'] = self.parsed_metadata['file'].map do |f|
92
+ next if f.blank?
93
+
94
+ path_to_file(f.tr(' ', '_'))
95
+ end.compact
87
96
  end
88
97
 
89
98
  def build_export_metadata
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bulkrax
4
+ class CsvFileSetEntry < CsvEntry
5
+ def factory_class
6
+ ::FileSet
7
+ end
8
+
9
+ def add_path_to_file
10
+ parsed_metadata['file'].each_with_index do |filename, i|
11
+ path_to_file = ::File.join(parser.path_to_files, filename)
12
+
13
+ parsed_metadata['file'][i] = path_to_file
14
+ end
15
+ raise ::StandardError, 'one or more file paths are invalid' unless parsed_metadata['file'].map { |file_path| ::File.file?(file_path) }.all?
16
+
17
+ parsed_metadata['file']
18
+ end
19
+
20
+ def validate_presence_of_parent!
21
+ return if parsed_metadata[related_parents_parsed_mapping]&.map(&:present?)&.any?
22
+
23
+ raise StandardError, 'File set must be related to at least one work'
24
+ end
25
+ end
26
+ end
@@ -99,7 +99,12 @@ module Bulkrax
99
99
  @current_run ||= if file? && zip?
100
100
  self.importer_runs.create!
101
101
  else
102
- self.importer_runs.create!(total_work_entries: self.limit || parser.works_total, total_collection_entries: parser.collections_total)
102
+ entry_counts = {
103
+ total_work_entries: self.limit || parser.works_total,
104
+ total_collection_entries: parser.collections_total,
105
+ total_file_set_entries: parser.file_sets_total
106
+ }
107
+ self.importer_runs.create!(entry_counts)
103
108
  end
104
109
  end
105
110
 
@@ -134,6 +139,13 @@ module Bulkrax
134
139
  status_info(e)
135
140
  end
136
141
 
142
+ def import_file_sets
143
+ self.save if self.new_record? # Object needs to be saved for statuses
144
+ parser.create_file_sets
145
+ rescue StandardError => e
146
+ status_info(e)
147
+ end
148
+
137
149
  # Prepend the base_url to ensure unique set identifiers
138
150
  # @todo - move to parser, as this is OAI specific
139
151
  def unique_collection_identifier(id)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bulkrax
4
+ module DynamicRecordLookup
5
+ # Search entries, collections, and every available work type for a record that
6
+ # has the provided identifier.
7
+ #
8
+ # @param identifier [String] Work/Collection ID or Bulkrax::Entry source_identifier
9
+ # @return [Work, Collection, nil] Work or Collection if found, otherwise nil
10
+ def find_record(identifier)
11
+ record = Entry.find_by(identifier: identifier)
12
+ record ||= ::Collection.where(id: identifier).first # rubocop:disable Rails/FindBy
13
+ if record.blank?
14
+ available_work_types.each do |work_type|
15
+ record ||= work_type.where(id: identifier).first # rubocop:disable Rails/FindBy
16
+ end
17
+ end
18
+
19
+ record.is_a?(Entry) ? record.factory.find : record
20
+ end
21
+
22
+ # Check if the record is a Work
23
+ def curation_concern?(record)
24
+ available_work_types.include?(record.class)
25
+ end
26
+
27
+ private
28
+
29
+ # @return [Array<Class>] list of work type classes
30
+ def available_work_types
31
+ # If running in a Hyku app, do not include disabled work types
32
+ @available_work_types ||= if defined?(::Hyku)
33
+ ::Site.instance.available_works.map(&:constantize)
34
+ else
35
+ ::Hyrax.config.curation_concerns
36
+ end
37
+ end
38
+ end
39
+ end
@@ -11,9 +11,9 @@ module Bulkrax
11
11
  unless self.importerexporter.validate_only
12
12
  raise CollectionsCreatedError unless collections_created?
13
13
  @item = factory.run!
14
+ parent_jobs if self.parsed_metadata[related_parents_parsed_mapping].present?
15
+ child_jobs if self.parsed_metadata[related_children_parsed_mapping].present?
14
16
  end
15
- parent_jobs if self.parsed_metadata[related_parents_parsed_mapping].present?
16
- child_jobs if self.parsed_metadata[related_children_parsed_mapping].present?
17
17
  rescue RSolr::Error::Http, CollectionsCreatedError => e
18
18
  raise e
19
19
  rescue StandardError => e
@@ -26,12 +26,16 @@ module Bulkrax
26
26
 
27
27
  def parent_jobs
28
28
  self.parsed_metadata[related_parents_parsed_mapping].each do |parent_identifier|
29
+ next if parent_identifier.blank?
30
+
29
31
  CreateRelationshipsJob.perform_later(entry_identifier: self.identifier, parent_identifier: parent_identifier, importer_run: self.last_run)
30
32
  end
31
33
  end
32
34
 
33
35
  def child_jobs
34
36
  self.parsed_metadata[related_children_parsed_mapping].each do |child_identifier|
37
+ next if child_identifier.blank?
38
+
35
39
  CreateRelationshipsJob.perform_later(entry_identifier: self.identifier, child_identifier: child_identifier, importer_run: self.last_run)
36
40
  end
37
41
  end
@@ -92,6 +96,7 @@ module Bulkrax
92
96
  source_identifier_value: identifier,
93
97
  work_identifier: parser.work_identifier,
94
98
  collection_field_mapping: parser.collection_field_mapping,
99
+ related_parents_parsed_mapping: related_parents_parsed_mapping,
95
100
  replace_files: replace_files,
96
101
  user: user,
97
102
  klass: factory_class,
@@ -20,15 +20,17 @@ module Bulkrax
20
20
  (last_imported_at || Time.current) + frequency.to_seconds if schedulable? && last_imported_at.present?
21
21
  end
22
22
 
23
- def increment_counters(index, collection = false)
23
+ def increment_counters(index, collection: false, file_set: false)
24
24
  # Only set the totals if they were not set on initialization
25
25
  if collection
26
26
  current_run.total_collection_entries = index + 1 unless parser.collections_total.positive?
27
+ elsif file_set
28
+ current_run.total_file_set_entries = index + 1 unless parser.file_sets_total.positive?
27
29
  else
28
30
  # TODO: differentiate between work and collection counts for exporters
29
31
  current_run.total_work_entries = index + 1 unless limit.to_i.positive? || parser.total.positive?
30
32
  end
31
- current_run.enqueued_records = index + 1
33
+ current_run.enqueued_records += 1
32
34
  current_run.save!
33
35
  end
34
36
 
@@ -114,6 +114,10 @@ module Bulkrax
114
114
  raise StandardError, 'must be defined' if importer?
115
115
  end
116
116
 
117
+ def create_file_sets
118
+ raise StandardError, 'must be defined' if importer?
119
+ end
120
+
117
121
  # Optional, define if using browse everything for file upload
118
122
  def retrieve_cloud_files(files); end
119
123
 
@@ -234,6 +238,10 @@ module Bulkrax
234
238
  0
235
239
  end
236
240
 
241
+ def file_sets_total
242
+ 0
243
+ end
244
+
237
245
  def write
238
246
  write_files
239
247
  zip
@@ -59,7 +59,7 @@ module Bulkrax
59
59
  }
60
60
  new_entry = find_or_create_entry(collection_entry_class, collection, 'Bulkrax::Importer', metadata)
61
61
  ImportCollectionJob.perform_now(new_entry.id, current_run.id)
62
- increment_counters(index, true)
62
+ increment_counters(index, collection: true)
63
63
  end
64
64
  end
65
65
 
@@ -25,7 +25,7 @@ module Bulkrax
25
25
  # retrieve a list of unique collections
26
26
  records.map do |r|
27
27
  collections = []
28
- r[collection_field_mapping].split(/\s*[;|]\s*/).each { |title| collections << { title: title } } if r[collection_field_mapping].present?
28
+ r[collection_field_mapping].split(/\s*[;|]\s*/).each { |title| collections << { title: title, from_collection_field_mapping: true } } if r[collection_field_mapping].present?
29
29
  model_field_mappings.each do |model_mapping|
30
30
  collections << r if r[model_mapping.to_sym]&.downcase == 'collection'
31
31
  end
@@ -38,13 +38,27 @@ module Bulkrax
38
38
  end
39
39
 
40
40
  def works
41
- records - collections
41
+ records - collections - file_sets
42
42
  end
43
43
 
44
44
  def works_total
45
45
  works.size
46
46
  end
47
47
 
48
+ def file_sets
49
+ records.map do |r|
50
+ file_sets = []
51
+ model_field_mappings.each do |model_mapping|
52
+ file_sets << r if r[model_mapping.to_sym]&.downcase == 'fileset'
53
+ end
54
+ file_sets
55
+ end.flatten.compact.uniq
56
+ end
57
+
58
+ def file_sets_total
59
+ file_sets.size
60
+ end
61
+
48
62
  # We could use CsvEntry#fields_from_data(data) but that would mean re-reading the data
49
63
  def import_fields
50
64
  @import_fields ||= records.inject(:merge).keys.compact.uniq
@@ -74,11 +88,31 @@ module Bulkrax
74
88
  collections.each_with_index do |collection, index|
75
89
  next if collection.blank?
76
90
  break if records.find_index(collection).present? && limit_reached?(limit, records.find_index(collection))
77
-
78
- new_entry = find_or_create_entry(collection_entry_class, unique_collection_identifier(collection), 'Bulkrax::Importer', collection.to_h)
91
+ ActiveSupport::Deprecation.warn(
92
+ 'Creating Collections using the collection_field_mapping will no longer be supported as of Bulkrax version 3.0.' \
93
+ ' Please configure Bulkrax to use related_parents_field_mapping and related_children_field_mapping instead.'
94
+ )
95
+
96
+ ## BEGIN
97
+ # Add required metadata to collections being imported using the collection_field_mapping, which only have a :title
98
+ # TODO: Remove once collection_field_mapping is removed
99
+ metadata = if collection.delete(:from_collection_field_mapping)
100
+ uci = unique_collection_identifier(collection)
101
+ {
102
+ title: collection[:title],
103
+ work_identifier => uci,
104
+ source_identifier => uci,
105
+ visibility: 'open',
106
+ collection_type_gid: ::Hyrax::CollectionType.find_or_create_default_collection_type.gid
107
+ }
108
+ end
109
+ collection_hash = metadata.presence || collection
110
+ ## END
111
+
112
+ new_entry = find_or_create_entry(collection_entry_class, collection_hash[source_identifier], 'Bulkrax::Importer', collection_hash)
79
113
  # TODO: add support for :delete option
80
114
  ImportCollectionJob.perform_now(new_entry.id, current_run.id)
81
- increment_counters(index, true)
115
+ increment_counters(index, collection: true)
82
116
  end
83
117
  importer.record_status
84
118
  rescue StandardError => e
@@ -104,6 +138,20 @@ module Bulkrax
104
138
  status_info(e)
105
139
  end
106
140
 
141
+ def create_file_sets
142
+ file_sets.each_with_index do |file_set, index|
143
+ next unless record_has_source_identifier(file_set, records.find_index(file_set))
144
+ break if limit_reached?(limit, records.find_index(file_set))
145
+
146
+ new_entry = find_or_create_entry(file_set_entry_class, file_set[source_identifier], 'Bulkrax::Importer', file_set.to_h)
147
+ ImportFileSetJob.perform_later(new_entry.id, current_run.id)
148
+ increment_counters(index, file_set: true)
149
+ end
150
+ importer.record_status
151
+ rescue StandardError => e
152
+ status_info(e)
153
+ end
154
+
107
155
  def write_partial_import_file(file)
108
156
  import_filename = import_file_path.split('/').last
109
157
  partial_import_filename = "#{File.basename(import_filename, '.csv')}_corrected_entries.csv"
@@ -179,6 +227,10 @@ module Bulkrax
179
227
  CsvCollectionEntry
180
228
  end
181
229
 
230
+ def file_set_entry_class
231
+ CsvFileSetEntry
232
+ end
233
+
182
234
  # See https://stackoverflow.com/questions/2650517/count-the-number-of-lines-in-a-file-without-reading-entire-file-into-memory
183
235
  # Changed to grep as wc -l counts blank lines, and ignores the final unescaped line (which may or may not contain data)
184
236
  def total
@@ -76,7 +76,7 @@ module Bulkrax
76
76
  new_entry = collection_entry_class.where(importerexporter: importerexporter, identifier: unique_collection_identifier, raw_metadata: metadata).first_or_create!
77
77
  # perform now to ensure this gets created before work imports start
78
78
  ImportCollectionJob.perform_now(new_entry.id, importerexporter.current_run.id)
79
- increment_counters(index, true)
79
+ increment_counters(index, collection: true)
80
80
  end
81
81
  end
82
82
 
@@ -24,6 +24,7 @@
24
24
  <th scope="col">Entries Deleted Upstream</th>
25
25
  <th scope="col">Total Collection Entries</th>
26
26
  <th scope="col">Total Work Entries</th>
27
+ <th scope="col">Total File Set Entries</th>
27
28
  <th scope="col"></th>
28
29
  <th scope="col"></th>
29
30
  <th scope="col"></th>
@@ -36,12 +37,13 @@
36
37
  <td><%= importer.status %></td>
37
38
  <td><%= importer.last_imported_at.strftime("%b %d, %Y") if importer.last_imported_at %></td>
38
39
  <td><%= importer.next_import_at.strftime("%b %d, %Y") if importer.next_import_at %></td>
39
- <td><%= importer.importer_runs.last&.enqueued_records %></td>
40
- <td><%= (importer.importer_runs.last&.processed_collections || 0) + (importer.importer_runs.last&.processed_records || 0) %></td>
41
- <td><%= (importer.importer_runs.last&.failed_collections || 0) + (importer.importer_runs.last&.failed_records || 0) %></td>
42
- <td><%= importer.importer_runs.last&.deleted_records %></td>
43
- <td><%= importer.importer_runs.last&.total_collection_entries %></td>
44
- <td><%= importer.importer_runs.last&.total_work_entries %></td>
40
+ <td><%= importer.last_run&.enqueued_records %></td>
41
+ <td><%= (importer.last_run&.processed_records || 0) %></td>
42
+ <td><%= (importer.last_run&.failed_records || 0) %></td>
43
+ <td><%= importer.last_run&.deleted_records %></td>
44
+ <td><%= importer.last_run&.total_collection_entries %></td>
45
+ <td><%= importer.last_run&.total_work_entries %></td>
46
+ <td><%= importer.last_run&.total_file_set_entries %></td>
45
47
  <td><%= link_to raw('<span class="glyphicon glyphicon-info-sign"></span>'), importer_path(importer) %></td>
46
48
  <td><%= link_to raw('<span class="glyphicon glyphicon-pencil"></span>'), edit_importer_path(importer) %></td>
47
49
  <td><%= link_to raw('<span class="glyphicon glyphicon-remove"></span>'), importer, method: :delete, data: { confirm: 'Are you sure?' } %></td>
@@ -56,14 +56,19 @@
56
56
 
57
57
  <%= render partial: 'bulkrax/shared/bulkrax_field_mapping', locals: {item: @importer} %>
58
58
 
59
- <p class="bulkrax-p-align">
59
+ <p class="bulkrax-p-align" title="<%= @importer.last_run&.processed_works %> processed, <%= @importer.last_run&.failed_works %> failed">
60
60
  <strong>Total Works:</strong>
61
- <%= @importer.importer_runs.last&.total_work_entries %>
61
+ <%= @importer.last_run&.total_work_entries %>
62
62
  </p>
63
63
 
64
- <p class="bulkrax-p-align">
64
+ <p class="bulkrax-p-align" title="<%= @importer.last_run&.processed_collections %> processed, <%= @importer.last_run&.failed_collections %> failed">
65
65
  <strong>Total Collections:</strong>
66
- <%= @importer.importer_runs.last&.total_collection_entries %>
66
+ <%= @importer.last_run&.total_collection_entries %>
67
+ </p>
68
+
69
+ <p class="bulkrax-p-align" title="<%= @importer.last_run&.processed_file_sets %> processed, <%= @importer.last_run&.failed_file_sets %> failed">
70
+ <strong>Total File Sets:</strong>
71
+ <%= @importer.last_run&.total_file_set_entries %>
67
72
  </p>
68
73
 
69
74
  <div class="bulkrax-nav-tab-bottom-margin">
@@ -71,6 +76,7 @@
71
76
  <ul class="bulkrax-nav-tab-top-margin tab-nav nav nav-tabs" role="tablist">
72
77
  <li role="presentation" class='active'><a href="#work-entries" aria-controls="work-entries" role="tab" data-toggle="tab">Work Entries</a></li>
73
78
  <li role="presentation"><a href="#collection-entries" aria-controls="collection-entries" role="tab" data-toggle="tab">Collection Entries</a></li>
79
+ <li role="presentation"><a href="#file-set-entries" aria-controls="file-set-entries" role="tab" data-toggle="tab">File Set Entries</a></li>
74
80
  </ul>
75
81
  <!-- Tab panes -->
76
82
  <div class="tab-content outline">
@@ -158,6 +164,42 @@
158
164
  <%= page_entries_info(@collection_entries) %><br />
159
165
  <%= paginate(@collection_entries, theme: 'blacklight', param_name: :collections_entries_page, params: {anchor: 'collection-entries'}) %>
160
166
  </div>
167
+ <div role="tabpanel" class="tab-pane bulkrax-nav-tab-table-left-align" id="file-set-entries">
168
+ <table class='table table-striped'>
169
+ <thead>
170
+ <tr>
171
+ <th>Identifier</th>
172
+ <th>Entry ID</th>
173
+ <th>Status</th>
174
+ <th>Errors</th>
175
+ <th>Status Set At</th>
176
+ <th>Actions</th>
177
+ </tr>
178
+ </thead>
179
+ <tbody>
180
+ <% @file_set_entries.each do |e| %>
181
+ <tr>
182
+ <td><%= link_to e.identifier, bulkrax.importer_entry_path(@importer.id, e.id) %></td>
183
+ <td><%= e.id %></td>
184
+ <% if e.status == "Complete" %>
185
+ <td><span class="glyphicon glyphicon-ok" style="color: green;"></span> <%= e.status %></td>
186
+ <% else %>
187
+ <td><span class="glyphicon glyphicon-remove" style="color: red;"></span> <%= e.status %></td>
188
+ <% end %>
189
+ <% if e.last_error.present? %>
190
+ <td><%= link_to e.last_error.dig("error_class"), bulkrax.importer_entry_path(@importer.id, e.id) %></td>
191
+ <% else %>
192
+ <td></td>
193
+ <% end %>
194
+ <td><%= e.status_at %></td>
195
+ <td><%= link_to raw('<span class="glyphicon glyphicon-info-sign"></span>'), bulkrax.importer_entry_path(@importer.id, e.id) %></td>
196
+ </tr>
197
+ <% end %>
198
+ </tbody>
199
+ </table>
200
+ <%= page_entries_info(@file_set_entries) %><br />
201
+ <%= paginate(@file_set_entries, theme: 'blacklight', param_name: :file_set_entries_page, params: {anchor: 'file-set-entries'}) %>
202
+ </div>
161
203
  </div>
162
204
  </div>
163
205
 
@@ -0,0 +1,7 @@
1
+ class AddFileSetCountersToImporterRuns < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column :bulkrax_importer_runs, :processed_file_sets, :integer, default: 0 unless column_exists?(:bulkrax_importer_runs, :processed_file_sets)
4
+ add_column :bulkrax_importer_runs, :failed_file_sets, :integer, default: 0 unless column_exists?(:bulkrax_importer_runs, :failed_file_sets)
5
+ add_column :bulkrax_importer_runs, :total_file_set_entries, :integer, default: 0 unless column_exists?(:bulkrax_importer_runs, :total_file_set_entries)
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ class AddImportAttemptsToEntries < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column :bulkrax_entries, :import_attempts, :integer, default: 0 unless column_exists?(:bulkrax_entries, :import_attempts)
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddWorkCountersToImporterRuns < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column :bulkrax_importer_runs, :processed_works, :integer, default: 0 unless column_exists?(:bulkrax_importer_runs, :processed_works)
4
+ add_column :bulkrax_importer_runs, :failed_works, :integer, default: 0 unless column_exists?(:bulkrax_importer_runs, :failed_works)
5
+ end
6
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bulkrax
4
- VERSION = '2.0.0'
4
+ VERSION = '2.1.1'
5
5
  end
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: 2.0.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Kaufman
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-14 00:00:00.000000000 Z
11
+ date: 2022-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -224,7 +224,11 @@ dependencies:
224
224
  - - ">="
225
225
  - !ruby/object:Gem::Version
226
226
  version: '0'
227
- description: Description of Bulkrax.
227
+ description: Bulkrax is a batteries included importer for Samvera applications. It
228
+ currently includes support for OAI-PMH (DC and Qualified DC) and CSV out of the
229
+ box. It is also designed to be extensible, allowing you to easily add new importers
230
+ in to your application or to include them with other gems. Bulkrax provides a full
231
+ admin interface including creating, editing, scheduling and reviewing imports.
228
232
  email:
229
233
  - rob@notch8.com
230
234
  executables: []
@@ -261,6 +265,7 @@ files:
261
265
  - app/jobs/bulkrax/export_work_job.rb
262
266
  - app/jobs/bulkrax/exporter_job.rb
263
267
  - app/jobs/bulkrax/import_collection_job.rb
268
+ - app/jobs/bulkrax/import_file_set_job.rb
264
269
  - app/jobs/bulkrax/import_work_job.rb
265
270
  - app/jobs/bulkrax/importer_job.rb
266
271
  - app/mailers/bulkrax/application_mailer.rb
@@ -271,6 +276,7 @@ files:
271
276
  - app/models/bulkrax/application_record.rb
272
277
  - app/models/bulkrax/csv_collection_entry.rb
273
278
  - app/models/bulkrax/csv_entry.rb
279
+ - app/models/bulkrax/csv_file_set_entry.rb
274
280
  - app/models/bulkrax/entry.rb
275
281
  - app/models/bulkrax/exporter.rb
276
282
  - app/models/bulkrax/exporter_run.rb
@@ -286,6 +292,7 @@ files:
286
292
  - app/models/bulkrax/status.rb
287
293
  - app/models/bulkrax/xml_entry.rb
288
294
  - app/models/concerns/bulkrax/download_behavior.rb
295
+ - app/models/concerns/bulkrax/dynamic_record_lookup.rb
289
296
  - app/models/concerns/bulkrax/errored_entries.rb
290
297
  - app/models/concerns/bulkrax/export_behavior.rb
291
298
  - app/models/concerns/bulkrax/file_factory.rb
@@ -353,6 +360,9 @@ files:
353
360
  - db/migrate/20210806065737_increase_text_sizes.rb
354
361
  - db/migrate/20211004170708_change_bulkrax_statuses_error_message_column_type_to_text.rb
355
362
  - db/migrate/20211203195233_rename_children_counters_to_relationships.rb
363
+ - db/migrate/20211220195027_add_file_set_counters_to_importer_runs.rb
364
+ - db/migrate/20220118001339_add_import_attempts_to_entries.rb
365
+ - db/migrate/20220119213325_add_work_counters_to_importer_runs.rb
356
366
  - lib/bulkrax.rb
357
367
  - lib/bulkrax/engine.rb
358
368
  - lib/bulkrax/version.rb
@@ -368,7 +378,7 @@ homepage: https://github.com/samvera-labs/bulkrax
368
378
  licenses:
369
379
  - Apache-2.0
370
380
  metadata: {}
371
- post_install_message:
381
+ post_install_message:
372
382
  rdoc_options: []
373
383
  require_paths:
374
384
  - lib
@@ -383,8 +393,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
383
393
  - !ruby/object:Gem::Version
384
394
  version: '0'
385
395
  requirements: []
386
- rubygems_version: 3.1.2
387
- signing_key:
396
+ rubygems_version: 3.1.4
397
+ signing_key:
388
398
  specification_version: 4
389
- summary: Summary of Bulkrax.
399
+ summary: Import and export tool for Hyrax and Hyku
390
400
  test_files: []