bulkrax 4.2.1 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
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">