bulkrax 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/bulkrax/exporters.js +12 -0
  3. data/app/assets/javascripts/bulkrax/importers.js.erb +27 -1
  4. data/app/controllers/bulkrax/exporters_controller.rb +3 -1
  5. data/app/controllers/bulkrax/importers_controller.rb +1 -1
  6. data/app/factories/bulkrax/object_factory.rb +35 -8
  7. data/app/jobs/bulkrax/create_relationships_job.rb +1 -1
  8. data/app/jobs/bulkrax/import_work_job.rb +12 -10
  9. data/app/matchers/bulkrax/application_matcher.rb +1 -1
  10. data/app/models/bulkrax/csv_entry.rb +14 -10
  11. data/app/models/bulkrax/importer.rb +20 -15
  12. data/app/models/bulkrax/oai_entry.rb +1 -2
  13. data/app/models/concerns/bulkrax/file_set_entry_behavior.rb +8 -1
  14. data/app/models/concerns/bulkrax/import_behavior.rb +10 -9
  15. data/app/parsers/bulkrax/application_parser.rb +87 -14
  16. data/app/parsers/bulkrax/bagit_parser.rb +2 -1
  17. data/app/parsers/bulkrax/csv_parser.rb +11 -10
  18. data/app/parsers/bulkrax/oai_dc_parser.rb +2 -2
  19. data/app/services/bulkrax/remove_relationships_for_importer.rb +107 -0
  20. data/app/views/bulkrax/exporters/_form.html.erb +3 -3
  21. data/app/views/bulkrax/exporters/show.html.erb +17 -41
  22. data/app/views/bulkrax/importers/edit.html.erb +1 -1
  23. data/app/views/bulkrax/importers/new.html.erb +1 -1
  24. data/app/views/bulkrax/importers/show.html.erb +3 -114
  25. data/app/views/bulkrax/shared/_collection_entries_tab.html.erb +39 -0
  26. data/app/views/bulkrax/shared/_file_set_entries_tab.html.erb +39 -0
  27. data/app/views/bulkrax/shared/_work_entries_tab.html.erb +39 -0
  28. data/app/views/hyrax/dashboard/sidebar/_bulkrax_sidebar_additions.html.erb +7 -5
  29. data/app/views/hyrax/dashboard/sidebar/_repository_content.html.erb +23 -15
  30. data/db/migrate/20211203195233_rename_children_counters_to_relationships.rb +1 -1
  31. data/db/migrate/20211220195027_add_file_set_counters_to_importer_runs.rb +1 -1
  32. data/db/migrate/20220118001339_add_import_attempts_to_entries.rb +1 -1
  33. data/db/migrate/20220119213325_add_work_counters_to_importer_runs.rb +1 -1
  34. data/db/migrate/20220301001839_create_bulkrax_pending_relationships.rb +1 -1
  35. data/db/migrate/20220303212810_add_order_to_bulkrax_pending_relationships.rb +1 -1
  36. data/db/migrate/20220412233954_add_include_thumbnails_to_bulkrax_exporters.rb +1 -1
  37. data/db/migrate/20220413180915_add_generated_metadata_to_bulkrax_exporters.rb +1 -1
  38. data/db/migrate/20220609001128_rename_bulkrax_importer_run_to_importer_run.rb +1 -1
  39. data/lib/bulkrax/version.rb +1 -1
  40. data/lib/bulkrax.rb +38 -11
  41. data/lib/generators/bulkrax/templates/config/initializers/bulkrax.rb +10 -0
  42. data/lib/tasks/bulkrax_tasks.rake +10 -11
  43. data/lib/tasks/reset.rake +65 -0
  44. metadata +7 -2
@@ -2,6 +2,9 @@
2
2
  require 'zip'
3
3
 
4
4
  module Bulkrax
5
+ # An abstract class that establishes the API for Bulkrax's import and export parsing.
6
+ #
7
+ # @abstract Subclass the Bulkrax::ApplicationParser to create a parser that handles a specific format (e.g. CSV, Bagit, XML, etc).
5
8
  class ApplicationParser # rubocop:disable Metrics/ClassLength
6
9
  attr_accessor :importerexporter, :headers
7
10
  alias importer importerexporter
@@ -12,14 +15,21 @@ module Bulkrax
12
15
  :exporter_export_path, :exporter_export_zip_path, :importer_unzip_path, :validate_only,
13
16
  to: :importerexporter
14
17
 
18
+ # @todo Convert to `class_attribute :parser_fiels, default: {}`
15
19
  def self.parser_fields
16
20
  {}
17
21
  end
18
22
 
23
+ # @return [TrueClass,FalseClass] this parser does or does not support exports.
24
+ #
25
+ # @todo Convert to `class_attribute :export_supported, default: false, instance_predicate: true` and `self << class; alias export_supported? export_supported; end`
19
26
  def self.export_supported?
20
27
  false
21
28
  end
22
29
 
30
+ # @return [TrueClass,FalseClass] this parser does or does not support imports.
31
+ #
32
+ # @todo Convert to `class_attribute :import_supported, default: false, instance_predicate: true` and `self << class; alias import_supported? import_supported; end`
23
33
  def self.import_supported?
24
34
  true
25
35
  end
@@ -29,49 +39,70 @@ module Bulkrax
29
39
  @headers = []
30
40
  end
31
41
 
32
- # @api
42
+ # @api public
43
+ # @abstract Subclass and override {#entry_class} to implement behavior for the parser.
33
44
  def entry_class
34
- raise StandardError, 'must be defined'
45
+ raise NotImplementedError, 'must be defined'
35
46
  end
36
47
 
37
- # @api
48
+ # @api public
49
+ # @abstract Subclass and override {#collection_entry_class} to implement behavior for the parser.
38
50
  def collection_entry_class
39
- raise StandardError, 'must be defined'
51
+ raise NotImplementedError, 'must be defined'
40
52
  end
41
53
 
42
- # @api
54
+ # @api public
55
+ # @abstract Subclass and override {#records} to implement behavior for the parser.
43
56
  def records(_opts = {})
44
- raise StandardError, 'must be defined'
57
+ raise NotImplementedError, 'must be defined'
45
58
  end
46
59
 
60
+ # @return [Symbol] the name of the identifying property in the source system from which we're
61
+ # importing (e.g. is *not* this application that mounts *this* Bulkrax engine).
62
+ #
63
+ # @see #work_identifier
64
+ # @see https://github.com/samvera-labs/bulkrax/wiki/CSV-Importer#source-identifier Bulkrax Wiki regarding source identifier
47
65
  def source_identifier
48
66
  @source_identifier ||= get_field_mapping_hash_for('source_identifier')&.values&.first&.[]('from')&.first&.to_sym || :source_identifier
49
67
  end
50
68
 
69
+ # @return [Symbol] the name of the identifying property for the system which we're importing
70
+ # into (e.g. the application that mounts *this* Bulkrax engine)
71
+ # @see #source_identifier
51
72
  def work_identifier
52
73
  @work_identifier ||= get_field_mapping_hash_for('source_identifier')&.keys&.first&.to_sym || :source
53
74
  end
54
75
 
76
+ # @return [String]
55
77
  def generated_metadata_mapping
56
78
  @generated_metadata_mapping ||= 'generated'
57
79
  end
58
80
 
81
+ # @return [String, NilClass]
82
+ # @see #related_parents_raw_mapping
59
83
  def related_parents_raw_mapping
60
84
  @related_parents_raw_mapping ||= get_field_mapping_hash_for('related_parents_field_mapping')&.values&.first&.[]('from')&.first
61
85
  end
62
86
 
87
+ # @return [String]
88
+ # @see #related_parents_field_mapping
63
89
  def related_parents_parsed_mapping
64
90
  @related_parents_parsed_mapping ||= (get_field_mapping_hash_for('related_parents_field_mapping')&.keys&.first || 'parents')
65
91
  end
66
92
 
93
+ # @return [String, NilClass]
94
+ # @see #related_children_parsed_mapping
67
95
  def related_children_raw_mapping
68
96
  @related_children_raw_mapping ||= get_field_mapping_hash_for('related_children_field_mapping')&.values&.first&.[]('from')&.first
69
97
  end
70
98
 
99
+ # @return [String]
100
+ # @see #related_children_raw_mapping
71
101
  def related_children_parsed_mapping
72
102
  @related_children_parsed_mapping ||= (get_field_mapping_hash_for('related_children_field_mapping')&.keys&.first || 'children')
73
103
  end
74
104
 
105
+ # @api private
75
106
  def get_field_mapping_hash_for(key)
76
107
  return instance_variable_get("@#{key}_hash") if instance_variable_get("@#{key}_hash").present?
77
108
 
@@ -85,6 +116,7 @@ module Bulkrax
85
116
  instance_variable_get("@#{key}_hash")
86
117
  end
87
118
 
119
+ # @return [Array<String>]
88
120
  def model_field_mappings
89
121
  model_mappings = Bulkrax.field_mappings[self.class.to_s]&.dig('model', :from) || []
90
122
  model_mappings |= ['model']
@@ -92,6 +124,7 @@ module Bulkrax
92
124
  model_mappings
93
125
  end
94
126
 
127
+ # @return [String]
95
128
  def perform_method
96
129
  if self.validate_only
97
130
  'perform_now'
@@ -100,29 +133,55 @@ module Bulkrax
100
133
  end
101
134
  end
102
135
 
136
+ # The visibility of the record. Acceptable values are: "open", "embaro", "lease", "authenticated", "restricted". The default is "open"
137
+ #
138
+ # @return [String]
139
+ # @see https://github.com/samvera/hydra-head/blob/main/hydra-access-controls/app/models/concerns/hydra/access_controls/access_right.rb Hydra::AccessControls::AccessRight for details on the range of values.
140
+ # @see https://github.com/samvera/hyrax/blob/bd2bcffc33e183904be2c175367648815f25bc2b/app/services/hyrax/visibility_intention.rb Hyrax::VisibilityIntention for how we process the visibility.
103
141
  def visibility
104
142
  @visibility ||= self.parser_fields['visibility'] || 'open'
105
143
  end
106
144
 
145
+ # @api public
146
+ #
147
+ # @param types [Array<Symbol>] the types of objects that we'll create.
148
+ #
149
+ # @see Bulkrax::Importer::DEFAULT_OBJECT_TYPES
150
+ # @see #create_collections
151
+ # @see #create_works
152
+ # @see #create_file_sets
153
+ # @see #create_relationships
154
+ def create_objects(types = [])
155
+ types.each do |object_type|
156
+ parser.send("create_#{object_type.pluralize}")
157
+ end
158
+ end
159
+
160
+ # @abstract Subclass and override {#create_collections} to implement behavior for the parser.
107
161
  def create_collections
108
- raise StandardError, 'must be defined' if importer?
162
+ raise NotImplementedError, 'must be defined' if importer?
109
163
  end
110
164
 
165
+ # @abstract Subclass and override {#create_works} to implement behavior for the parser.
111
166
  def create_works
112
- raise StandardError, 'must be defined' if importer?
167
+ raise NotImplementedError, 'must be defined' if importer?
113
168
  end
114
169
 
170
+ # @abstract Subclass and override {#create_file_sets} to implement behavior for the parser.
115
171
  def create_file_sets
116
- raise StandardError, 'must be defined' if importer?
172
+ raise NotImplementedError, 'must be defined' if importer?
117
173
  end
118
174
 
175
+ # @abstract Subclass and override {#create_relationships} to implement behavior for the parser.
119
176
  def create_relationships
120
- raise StandardError, 'must be defined' if importer?
177
+ raise NotImplementedError, 'must be defined' if importer?
121
178
  end
122
179
 
123
180
  # Optional, define if using browse everything for file upload
124
181
  def retrieve_cloud_files(files); end
125
182
 
183
+ # @param file [#path, #original_filename] the file object that with the relevant data for the
184
+ # import.
126
185
  def write_import_file(file)
127
186
  path = File.join(path_for_import, file.original_filename)
128
187
  FileUtils.mv(
@@ -133,47 +192,58 @@ module Bulkrax
133
192
  end
134
193
 
135
194
  # Base path for imported and exported files
195
+ # @param [String]
196
+ # @return [String] the base path for files that this parser will "parse"
136
197
  def base_path(type = 'import')
137
- ENV['HYKU_MULTITENANT'] ? File.join(Bulkrax.send("#{type}_path"), Site.instance.account.name) : Bulkrax.send("#{type}_path")
198
+ # account for multiple versions of hyku
199
+ is_multitenant = ENV['HYKU_MULTITENANT'] == 'true' || ENV['SETTINGS__MULTITENANCY__ENABLED'] == 'true'
200
+ is_multitenant ? File.join(Bulkrax.send("#{type}_path"), ::Site.instance.account.name) : Bulkrax.send("#{type}_path")
138
201
  end
139
202
 
140
203
  # Path where we'll store the import metadata and files
141
204
  # this is used for uploaded and cloud files
205
+ # @return [String]
142
206
  def path_for_import
143
207
  @path_for_import = File.join(base_path, importerexporter.path_string)
144
208
  FileUtils.mkdir_p(@path_for_import) unless File.exist?(@path_for_import)
145
209
  @path_for_import
146
210
  end
147
211
 
212
+ # @abstract Subclass and override {#setup_export_file} to implement behavior for the parser.
148
213
  def setup_export_file
149
- raise StandardError, 'must be defined' if exporter?
214
+ raise NotImplementedError, 'must be defined' if exporter?
150
215
  end
151
216
 
217
+ # @abstract Subclass and override {#write_files} to implement behavior for the parser.
152
218
  def write_files
153
- raise StandardError, 'must be defined' if exporter?
219
+ raise NotImplementedError, 'must be defined' if exporter?
154
220
  end
155
221
 
222
+ # @return [TrueClass,FalseClass]
156
223
  def importer?
157
224
  importerexporter.is_a?(Bulkrax::Importer)
158
225
  end
159
226
 
227
+ # @return [TrueClass,FalseClass]
160
228
  def exporter?
161
229
  importerexporter.is_a?(Bulkrax::Exporter)
162
230
  end
163
231
 
164
232
  # @param limit [Integer] limit set on the importerexporter
165
233
  # @param index [Integer] index of current iteration
166
- # @return [boolean]
234
+ # @return [TrueClass,FalseClass]
167
235
  def limit_reached?(limit, index)
168
236
  return false if limit.nil? || limit.zero? # no limit
169
237
  index >= limit
170
238
  end
171
239
 
172
240
  # Override to add specific validations
241
+ # @return [TrueClass,FalseClass]
173
242
  def valid_import?
174
243
  true
175
244
  end
176
245
 
246
+ # @return [TrueClass,FalseClass]
177
247
  def record_has_source_identifier(record, index)
178
248
  if record[source_identifier].blank?
179
249
  if Bulkrax.fill_in_blank_source_identifiers.present?
@@ -197,6 +267,7 @@ module Bulkrax
197
267
  end
198
268
  # rubocop:enable Rails/SkipsModelValidations
199
269
 
270
+ # @return [Array<String>]
200
271
  def required_elements
201
272
  if Bulkrax.fill_in_blank_source_identifiers
202
273
  ['title']
@@ -285,12 +356,14 @@ module Bulkrax
285
356
  end
286
357
 
287
358
  # Path for the import
359
+ # @return [String]
288
360
  def import_file_path
289
361
  @import_file_path ||= real_import_file_path
290
362
  end
291
363
 
292
364
  private
293
365
 
366
+ # @return [String]
294
367
  def real_import_file_path
295
368
  return importer_unzip_path if file? && zip?
296
369
  parser_fields['import_file_path']
@@ -134,7 +134,8 @@ module Bulkrax
134
134
 
135
135
  record.file_sets.each do |fs|
136
136
  file_name = filename(fs)
137
- next if file_name.blank?
137
+ next if file_name.blank? || fs.original_file.blank?
138
+
138
139
  io = open(fs.original_file.uri)
139
140
  file = Tempfile.new([file_name, File.extname(file_name)], binmode: true)
140
141
  file.write(io.read)
@@ -180,7 +180,7 @@ module Bulkrax
180
180
  end
181
181
 
182
182
  def current_work_ids
183
- ActiveSupport::Deprication.warn('Bulkrax::CsvParser#current_work_ids will be replaced with #current_record_ids in version 3.0')
183
+ ActiveSupport::Deprecation.warn('Bulkrax::CsvParser#current_work_ids will be replaced with #current_record_ids in version 3.0')
184
184
  current_record_ids
185
185
  end
186
186
 
@@ -200,14 +200,14 @@ module Bulkrax
200
200
  # get the parent collection and child collections
201
201
  @collection_ids = ActiveFedora::SolrService.query("id:#{importerexporter.export_source} #{extra_filters}", method: :post, rows: 2_147_483_647).map(&:id)
202
202
  @collection_ids += ActiveFedora::SolrService.query("has_model_ssim:Collection AND member_of_collection_ids_ssim:#{importerexporter.export_source}", method: :post, rows: 2_147_483_647).map(&:id)
203
+ find_child_file_sets(@work_ids)
203
204
  when 'worktype'
204
205
  @work_ids = ActiveFedora::SolrService.query("has_model_ssim:#{importerexporter.export_source + extra_filters}", method: :post, rows: 2_000_000_000).map(&:id)
206
+ find_child_file_sets(@work_ids)
205
207
  when 'importer'
206
208
  set_ids_for_exporting_from_importer
207
209
  end
208
210
 
209
- find_child_file_sets(@work_ids) if importerexporter.export_from == 'collection'
210
-
211
211
  @work_ids + @collection_ids + @file_set_ids
212
212
  end
213
213
  # rubocop:enable Metrics/AbcSize
@@ -355,9 +355,9 @@ module Bulkrax
355
355
  path = File.join(exporter_export_path, folder_count, 'files')
356
356
  FileUtils.mkdir_p(path) unless File.exist? path
357
357
  file = filename(fs)
358
- io = open(fs.original_file.uri)
359
- next if file.blank?
358
+ next if file.blank? || fs.original_file.blank?
360
359
 
360
+ io = open(fs.original_file.uri)
361
361
  File.open(File.join(path, file), 'wb') do |f|
362
362
  f.write(io.read)
363
363
  f.close
@@ -440,7 +440,7 @@ module Bulkrax
440
440
  file_mapping = Bulkrax.field_mappings.dig(self.class.to_s, 'file', :from)&.first&.to_sym || :file
441
441
  next if r[file_mapping].blank?
442
442
 
443
- r[file_mapping].split(/\s*[:;|]\s*/).map do |f|
443
+ r[file_mapping].split(Bulkrax.multi_value_element_split_on).map do |f|
444
444
  file = File.join(path_to_files, f.tr(' ', '_'))
445
445
  if File.exist?(file) # rubocop:disable Style/GuardClause
446
446
  file
@@ -455,9 +455,10 @@ module Bulkrax
455
455
  def path_to_files(**args)
456
456
  filename = args.fetch(:filename, '')
457
457
 
458
- @path_to_files ||= File.join(
459
- zip? ? importer_unzip_path : File.dirname(import_file_path), 'files', filename
460
- )
458
+ return @path_to_files if @path_to_files.present? && filename.blank?
459
+ @path_to_files = File.join(
460
+ zip? ? importer_unzip_path : File.dirname(import_file_path), 'files', filename
461
+ )
461
462
  end
462
463
 
463
464
  private
@@ -467,7 +468,7 @@ module Bulkrax
467
468
  entry_uid ||= if Bulkrax.fill_in_blank_source_identifiers.present?
468
469
  Bulkrax.fill_in_blank_source_identifiers.call(self, records.find_index(collection_hash))
469
470
  else
470
- collection_hash[:title].split(/\s*[;|]\s*/).first
471
+ collection_hash[:title].split(Bulkrax.multi_value_element_split_on).first
471
472
  end
472
473
 
473
474
  entry_uid
@@ -13,8 +13,7 @@ module Bulkrax
13
13
  def client
14
14
  @client ||= OAI::Client.new(importerexporter.parser_fields['base_url'],
15
15
  headers: headers,
16
- parser: 'libxml',
17
- metadata_prefix: importerexporter.parser_fields['metadata_prefix'])
16
+ parser: 'libxml')
18
17
  rescue StandardError
19
18
  raise OAIError
20
19
  end
@@ -32,6 +31,7 @@ module Bulkrax
32
31
  end
33
32
 
34
33
  def records(opts = {})
34
+ opts[:metadata_prefix] ||= importerexporter.parser_fields['metadata_prefix']
35
35
  opts[:set] = collection_name unless collection_name == 'all'
36
36
 
37
37
  opts[:from] = importerexporter&.last_imported_at&.strftime("%Y-%m-%d") if importerexporter.last_imported_at && only_updates
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+ module Bulkrax
3
+ # This module is rather destructive; it will break relationships between the works, file sets, and
4
+ # collections that were imported via an importer. You probably don't want to run this on your
5
+ # data, except in the case where you have been testing a Bulkrax::Importer, the parsers and
6
+ # mappings. Then, you might have relationships that you want to remove.
7
+ #
8
+ # tl;dr - Caution this will break things!
9
+ class RemoveRelationshipsForImporter
10
+ # @api public
11
+ #
12
+ # Remove the relationships of the works and collections for all of the Bulkrax::Entry records
13
+ # associated with the given Bulkrax::Importer.
14
+ #
15
+ # @param importer [Bulkrax::Importer]
16
+ # @param with_progress_bar [Boolean]
17
+ def self.break_relationships_for!(importer:, with_progress_bar: false)
18
+ entries = importer.entries.select(&:succeeded?)
19
+ progress_bar = build_progress_bar_for(with_progress_bar: with_progress_bar, entries: entries)
20
+ new(progress_bar: progress_bar, entries: entries).break_relationships!
21
+ end
22
+
23
+ # @api private
24
+ #
25
+ # A null object that conforms to this class's use of a progress bar.
26
+ module NullProgressBar
27
+ def self.increment; end
28
+ end
29
+
30
+ # @api private
31
+ #
32
+ # @return [#increment]
33
+ def self.build_progress_bar_for(with_progress_bar:, entries:)
34
+ return NullProgressBar unless with_progress_bar
35
+
36
+ begin
37
+ require 'ruby-progressbar'
38
+ ProgessBar.create(total: entries.count)
39
+ rescue LoadError
40
+ Rails.logger.info("Using NullProgressBar because ProgressBar is not available due to a LoadError.")
41
+ end
42
+ end
43
+
44
+ # @param entries [#each]
45
+ # @param progress_bar [#increment]
46
+ def initialize(entries:, progress_bar:)
47
+ @progress_bar = progress_bar
48
+ @entries = entries
49
+ end
50
+
51
+ attr_reader :entries, :progress_bar
52
+
53
+ def break_relationships!
54
+ entries.each do |entry|
55
+ progress_bar.increment
56
+
57
+ obj = entry.factory.find
58
+ next if obj.is_a?(FileSet) # FileSets must be attached to a Work
59
+
60
+ if obj.is_a?(Collection)
61
+ remove_relationships_from_collection(obj)
62
+ else
63
+ remove_relationships_from_work(obj)
64
+ end
65
+
66
+ obj.try(:reindex_extent=, Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX)
67
+ obj.save!
68
+ end
69
+ end
70
+
71
+ def remove_relationships_from_collection(collection)
72
+ # Remove child work relationships
73
+ collection.member_works.each do |work|
74
+ change = work.member_of_collections.delete(collection)
75
+ work.save! if change.present?
76
+ end
77
+
78
+ # Remove parent collection relationships
79
+ collection.member_of_collections.each do |parent_col|
80
+ Hyrax::Collections::NestedCollectionPersistenceService
81
+ .remove_nested_relationship_for(parent: parent_col, child: collection)
82
+ end
83
+
84
+ # Remove child collection relationships
85
+ collection.member_collections.each do |child_col|
86
+ Hyrax::Collections::NestedCollectionPersistenceService
87
+ .remove_nested_relationship_for(parent: collection, child: child_col)
88
+ end
89
+ end
90
+
91
+ def remove_relationships_from_work(work)
92
+ # Remove parent collection relationships
93
+ work.member_of_collections = []
94
+
95
+ # Remove parent work relationships
96
+ work.member_of_works.each do |parent_work|
97
+ parent_work.members.delete(work)
98
+ parent_work.save!
99
+ end
100
+
101
+ # Remove child work relationships
102
+ work.member_works.each do |child_work|
103
+ work.member_works.delete(child_work)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -29,7 +29,7 @@
29
29
 
30
30
  <%= form.input :export_source_importer,
31
31
  label: t('bulkrax.exporter.labels.importer'),
32
- # required: true,
32
+ required: true,
33
33
  prompt: 'Select from the list',
34
34
  label_html: { class: 'importer export-source-option hidden' },
35
35
  input_html: { class: 'importer export-source-option hidden' },
@@ -38,7 +38,7 @@
38
38
  <%= form.input :export_source_collection,
39
39
  prompt: 'Start typing ...',
40
40
  label: t('bulkrax.exporter.labels.collection'),
41
- # required: true,
41
+ required: true,
42
42
  placeholder: @collection&.title&.first,
43
43
  label_html: { class: 'collection export-source-option hidden' },
44
44
  input_html: {
@@ -52,7 +52,7 @@
52
52
 
53
53
  <%= form.input :export_source_worktype,
54
54
  label: t('bulkrax.exporter.labels.worktype'),
55
- # required: true,
55
+ required: true,
56
56
  prompt: 'Select from the list',
57
57
  label_html: { class: 'worktype export-source-option hidden' },
58
58
  input_html: { class: 'worktype export-source-option hidden' },
@@ -87,49 +87,25 @@
87
87
 
88
88
  <p class='bulkrax-p-align'><strong><%= t('bulkrax.exporter.labels.field_mapping') %>:</strong></p>
89
89
 
90
- <p class='bulkrax-p-align'>
91
- <strong><%= t('bulkrax.exporter.labels.total_work_entries') %>:</strong>
92
- <%= @exporter.exporter_runs.last&.total_work_entries %>
90
+ <p class="bulkrax-p-align" title="<%= @exporter.last_run&.total_work_entries %> processed, <%= @exporter.last_run&.failed_records %> failed">
91
+ <strong>Total Entries:</strong>
92
+ <%= @exporter.last_run&.total_work_entries %>
93
93
  </p>
94
94
  <br>
95
- <div class='bulkrax-nav-tab-table-left-align'>
96
- <h2>Entries</h2>
97
- <table class='table table-striped'>
98
- <thead>
99
- <tr>
100
- <th>Identifier</th>
101
- <th>Entry ID</th>
102
- <th>Status</th>
103
- <th>Errors</th>
104
- <th>Status Set At</th>
105
- <th>Actions</th>
106
- </tr>
107
- </thead>
108
- <tbody>
109
- <% @work_entries.each do |e| %>
110
- <tr>
111
- <td><%= link_to e.identifier, bulkrax.exporter_entry_path(@exporter.id, e.id) %></td>
112
- <td><%= e.id %></td>
113
- <% if e.status == 'Complete' %>
114
- <td><span class='glyphicon glyphicon-ok' style='color: green;'></span> <%= e.status %></td>
115
- <% elsif e.status == 'Pending' %>
116
- <td><span class='glyphicon glyphicon-option-horizontal' style='color: blue;'></span> <%= e.status %></td>
117
- <% else %>
118
- <td><span class='glyphicon glyphicon-remove' style='color: red;'></span> <%= e.status %></td>
119
- <% end %>
120
- <% if e.last_error.present? %>
121
- <td><%= link_to e.last_error.dig('error_class'), bulkrax.exporter_entry_path(@exporter.id, e.id) %></td>
122
- <% else %>
123
- <td></td>
124
- <% end %>
125
- <td><%= e.status_at %></td>
126
- <td><%= link_to raw("<span class='glyphicon glyphicon-info-sign'></span>"), bulkrax.exporter_entry_path(@exporter.id, e.id) %></td>
127
- </tr>
128
- <% end %>
129
- </tbody>
130
- </table>
131
- <%= page_entries_info(@work_entries) %><br>
132
- <%= paginate(@work_entries, param_name: :work_entries_page) %>
95
+
96
+ <div class="bulkrax-nav-tab-bottom-margin">
97
+ <!-- Nav tabs -->
98
+ <ul class="bulkrax-nav-tab-top-margin tab-nav nav nav-tabs" role="tablist">
99
+ <li role="presentation" class='active'><a href="#work-entries" aria-controls="work-entries" role="tab" data-toggle="tab">Work Entries</a></li>
100
+ <li role="presentation"><a href="#collection-entries" aria-controls="collection-entries" role="tab" data-toggle="tab">Collection Entries</a></li>
101
+ <li role="presentation"><a href="#file-set-entries" aria-controls="file-set-entries" role="tab" data-toggle="tab">File Set Entries</a></li>
102
+ </ul>
103
+ <!-- Tab panes -->
104
+ <div class="tab-content outline">
105
+ <%= render partial: 'bulkrax/shared/work_entries_tab', locals: { item: @exporter, entries: @work_entries } %>
106
+ <%= render partial: 'bulkrax/shared/collection_entries_tab', locals: { item: @exporter, entries: @collection_entries } %>
107
+ <%= render partial: 'bulkrax/shared/file_set_entries_tab', locals: { item: @exporter, entries: @file_set_entries } %>
108
+ </div>
133
109
  <br>
134
110
  <%= link_to 'Edit', edit_exporter_path(@exporter) %>
135
111
  |
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div class="row">
6
6
  <div class="col-md-12">
7
- <div class="panel panel-default tabs">
7
+ <div class="panel panel-default tabs importer-form">
8
8
 
9
9
  <%= simple_form_for @importer, html: { multipart: true } do |form| %>
10
10
  <%= render 'form', importer: @importer, form: form %>
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div class="row">
6
6
  <div class="col-md-12">
7
- <div class="panel panel-default tabs">
7
+ <div class="panel panel-default tabs importer-form">
8
8
  <%= simple_form_for @importer, html: { multipart: true } do |form| %>
9
9
  <%= render 'form', importer: @importer, form: form %>
10
10
  <div class="panel-footer">