bulkrax 9.3.4 → 9.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.
- checksums.yaml +4 -4
- data/README.md +11 -1
- data/app/assets/javascripts/bulkrax/application.js +2 -1
- data/app/assets/javascripts/bulkrax/bulkrax.js +13 -4
- data/app/assets/javascripts/bulkrax/bulkrax_utils.js +96 -0
- data/app/assets/javascripts/bulkrax/datatables.js +1 -0
- data/app/assets/javascripts/bulkrax/entries.js +17 -10
- data/app/assets/javascripts/bulkrax/importers.js.erb +9 -2
- data/app/assets/javascripts/bulkrax/importers_stepper.js +2420 -0
- data/app/assets/stylesheets/bulkrax/application.css +1 -1
- data/app/assets/stylesheets/bulkrax/import_export.scss +9 -2
- data/app/assets/stylesheets/bulkrax/stepper/_header.scss +83 -0
- data/app/assets/stylesheets/bulkrax/stepper/_mixins.scss +26 -0
- data/app/assets/stylesheets/bulkrax/stepper/_navigation.scss +103 -0
- data/app/assets/stylesheets/bulkrax/stepper/_responsive.scss +46 -0
- data/app/assets/stylesheets/bulkrax/stepper/_review.scss +92 -0
- data/app/assets/stylesheets/bulkrax/stepper/_settings.scss +106 -0
- data/app/assets/stylesheets/bulkrax/stepper/_success.scss +26 -0
- data/app/assets/stylesheets/bulkrax/stepper/_summary.scss +171 -0
- data/app/assets/stylesheets/bulkrax/stepper/_upload.scss +339 -0
- data/app/assets/stylesheets/bulkrax/stepper/_validation.scss +237 -0
- data/app/assets/stylesheets/bulkrax/stepper/_variables.scss +46 -0
- data/app/assets/stylesheets/bulkrax/stepper.scss +32 -0
- data/app/controllers/bulkrax/guided_imports_controller.rb +175 -0
- data/app/controllers/bulkrax/importers_controller.rb +34 -28
- data/app/controllers/concerns/bulkrax/guided_import_demo_scenarios.rb +201 -0
- data/app/controllers/concerns/bulkrax/importer_file_handler.rb +217 -0
- data/app/factories/bulkrax/object_factory.rb +3 -2
- data/app/factories/bulkrax/valkyrie_object_factory.rb +61 -17
- data/app/jobs/bulkrax/export_work_job.rb +1 -3
- data/app/jobs/bulkrax/importer_job.rb +11 -4
- data/app/models/bulkrax/csv_entry.rb +27 -7
- data/app/models/bulkrax/entry.rb +4 -0
- data/app/models/bulkrax/importer.rb +31 -1
- data/app/models/concerns/bulkrax/has_matchers.rb +2 -2
- data/app/models/concerns/bulkrax/importer_exporter_behavior.rb +6 -5
- data/app/parsers/bulkrax/application_parser.rb +31 -5
- data/app/parsers/bulkrax/csv_parser.rb +42 -10
- data/app/parsers/concerns/bulkrax/csv_parser/csv_template_generation.rb +73 -0
- data/app/parsers/concerns/bulkrax/csv_parser/csv_validation.rb +133 -0
- data/app/parsers/concerns/bulkrax/csv_parser/csv_validation_helpers.rb +282 -0
- data/app/parsers/concerns/bulkrax/csv_parser/csv_validation_hierarchy.rb +96 -0
- data/app/services/bulkrax/csv_template/column_builder.rb +60 -0
- data/app/services/bulkrax/csv_template/column_descriptor.rb +58 -0
- data/app/services/bulkrax/csv_template/csv_builder.rb +83 -0
- data/app/services/bulkrax/csv_template/explanation_builder.rb +57 -0
- data/app/services/bulkrax/csv_template/field_analyzer.rb +56 -0
- data/app/services/bulkrax/csv_template/file_path_generator.rb +47 -0
- data/app/services/bulkrax/csv_template/file_validator.rb +68 -0
- data/app/services/bulkrax/csv_template/mapping_manager.rb +55 -0
- data/app/services/bulkrax/csv_template/model_loader.rb +50 -0
- data/app/services/bulkrax/csv_template/row_builder.rb +35 -0
- data/app/services/bulkrax/csv_template/schema_analyzer.rb +70 -0
- data/app/services/bulkrax/csv_template/split_formatter.rb +44 -0
- data/app/services/bulkrax/csv_template/value_determiner.rb +68 -0
- data/app/services/bulkrax/stepper_response_formatter.rb +347 -0
- data/app/services/bulkrax/validation_error_csv_builder.rb +99 -0
- data/app/validators/bulkrax/csv_row/child_reference.rb +56 -0
- data/app/validators/bulkrax/csv_row/circular_reference.rb +71 -0
- data/app/validators/bulkrax/csv_row/controlled_vocabulary.rb +74 -0
- data/app/validators/bulkrax/csv_row/duplicate_identifier.rb +63 -0
- data/app/validators/bulkrax/csv_row/missing_source_identifier.rb +31 -0
- data/app/validators/bulkrax/csv_row/parent_reference.rb +59 -0
- data/app/validators/bulkrax/csv_row/required_values.rb +64 -0
- data/app/views/bulkrax/entries/_parsed_metadata.html.erb +1 -1
- data/app/views/bulkrax/entries/_raw_metadata.html.erb +1 -1
- data/app/views/bulkrax/entries/show.html.erb +6 -6
- data/app/views/bulkrax/exporters/_form.html.erb +19 -43
- data/app/views/bulkrax/exporters/edit.html.erb +2 -2
- data/app/views/bulkrax/exporters/index.html.erb +5 -5
- data/app/views/bulkrax/exporters/new.html.erb +3 -5
- data/app/views/bulkrax/exporters/show.html.erb +3 -3
- data/app/views/bulkrax/guided_imports/new.html.erb +567 -0
- data/app/views/bulkrax/importers/_bagit_fields.html.erb +9 -9
- data/app/views/bulkrax/importers/_browse_everything.html.erb +1 -1
- data/app/views/bulkrax/importers/_csv_fields.html.erb +11 -11
- data/app/views/bulkrax/importers/_edit_form_buttons.html.erb +23 -23
- data/app/views/bulkrax/importers/_edit_item_buttons.html.erb +2 -2
- data/app/views/bulkrax/importers/_file_uploader.html.erb +3 -3
- data/app/views/bulkrax/importers/_form.html.erb +4 -5
- data/app/views/bulkrax/importers/_oai_fields.html.erb +8 -18
- data/app/views/bulkrax/importers/_xml_fields.html.erb +13 -13
- data/app/views/bulkrax/importers/edit.html.erb +2 -2
- data/app/views/bulkrax/importers/index.html.erb +19 -14
- data/app/views/bulkrax/importers/new.html.erb +10 -9
- data/app/views/bulkrax/importers/show.html.erb +23 -7
- data/app/views/bulkrax/importers/upload_corrected_entries.html.erb +6 -6
- data/app/views/bulkrax/shared/_bulkrax_errors.html.erb +11 -11
- data/app/views/bulkrax/shared/_bulkrax_field_mapping.html.erb +3 -3
- data/config/i18n-tasks.yml +195 -0
- data/config/locales/bulkrax.de.yml +504 -0
- data/config/locales/bulkrax.en.yml +487 -28
- data/config/locales/bulkrax.es.yml +504 -0
- data/config/locales/bulkrax.fr.yml +504 -0
- data/config/locales/bulkrax.it.yml +504 -0
- data/config/locales/bulkrax.pt-BR.yml +504 -0
- data/config/locales/bulkrax.zh.yml +503 -0
- data/config/routes.rb +10 -0
- data/lib/bulkrax/data/demo_scenarios.json +2235 -0
- data/lib/bulkrax/version.rb +1 -1
- data/lib/bulkrax.rb +31 -3
- data/lib/tasks/bulkrax_tasks.rake +0 -102
- metadata +55 -3
- /data/{app/services → lib}/wings/custom_queries/find_by_source_identifier.rb +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
module CsvRow
|
|
5
|
+
##
|
|
6
|
+
# Detects circular parent-child relationships in the CSV.
|
|
7
|
+
# A circular reference occurs when following the parent chain from any record
|
|
8
|
+
# eventually leads back to itself (e.g. A→B→C→A).
|
|
9
|
+
#
|
|
10
|
+
# The validator builds a directed graph (child → parents) from all records on
|
|
11
|
+
# first invocation and caches the set of all record ids involved in any cycle.
|
|
12
|
+
# Subsequent per-row calls simply check membership in that set.
|
|
13
|
+
#
|
|
14
|
+
# Requires context key:
|
|
15
|
+
# :relationship_graph – Hash { source_identifier => [parent_ids] } built by
|
|
16
|
+
# run_row_validators before iterating rows.
|
|
17
|
+
module CircularReference
|
|
18
|
+
def self.call(record, row_index, context)
|
|
19
|
+
cycle_ids = context[:circular_reference_ids] ||= detect_cycle_ids(context[:relationship_graph] || {})
|
|
20
|
+
return unless cycle_ids.include?(record[:source_identifier])
|
|
21
|
+
|
|
22
|
+
context[:errors] << {
|
|
23
|
+
row: row_index,
|
|
24
|
+
source_identifier: record[:source_identifier],
|
|
25
|
+
severity: 'error',
|
|
26
|
+
category: 'circular_reference',
|
|
27
|
+
column: 'parents',
|
|
28
|
+
value: record[:source_identifier],
|
|
29
|
+
message: I18n.t('bulkrax.importer.guided_import.validation.circular_reference_validator.errors.message',
|
|
30
|
+
value: record[:source_identifier]),
|
|
31
|
+
suggestion: I18n.t('bulkrax.importer.guided_import.validation.circular_reference_validator.errors.suggestion')
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns the set of all source identifiers that participate in at least one cycle.
|
|
36
|
+
# Uses recursive DFS with a per-branch ancestry set to detect back-edges.
|
|
37
|
+
def self.detect_cycle_ids(graph)
|
|
38
|
+
all_nodes = graph.keys.to_set | graph.values.flatten.to_set
|
|
39
|
+
visited = Set.new
|
|
40
|
+
cycle_ids = Set.new
|
|
41
|
+
|
|
42
|
+
all_nodes.each do |node|
|
|
43
|
+
next if visited.include?(node)
|
|
44
|
+
dfs(node, graph, visited, [], cycle_ids)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
cycle_ids
|
|
48
|
+
end
|
|
49
|
+
private_class_method :detect_cycle_ids
|
|
50
|
+
|
|
51
|
+
def self.dfs(node, graph, visited, ancestors, cycle_ids) # rubocop:disable Metrics/MethodLength
|
|
52
|
+
visited.add(node)
|
|
53
|
+
ancestors.push(node)
|
|
54
|
+
|
|
55
|
+
(graph[node] || []).each do |neighbor|
|
|
56
|
+
if ancestors.include?(neighbor)
|
|
57
|
+
# Back-edge found: mark every node in the cycle path
|
|
58
|
+
cycle_start = ancestors.index(neighbor)
|
|
59
|
+
ancestors[cycle_start..].each { |n| cycle_ids.add(n) }
|
|
60
|
+
cycle_ids.add(neighbor)
|
|
61
|
+
elsif !visited.include?(neighbor)
|
|
62
|
+
dfs(neighbor, graph, visited, ancestors, cycle_ids)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
ancestors.pop
|
|
67
|
+
end
|
|
68
|
+
private_class_method :dfs
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
module CsvRow
|
|
5
|
+
##
|
|
6
|
+
# Validates that controlled vocabulary values in each row are valid according
|
|
7
|
+
# to the QA authority for that field.
|
|
8
|
+
module ControlledVocabulary
|
|
9
|
+
def self.call(record, row_index, context) # rubocop:disable Metrics/MethodLength
|
|
10
|
+
field_metadata = context[:field_metadata]
|
|
11
|
+
return if field_metadata.blank?
|
|
12
|
+
|
|
13
|
+
model = record[:model]
|
|
14
|
+
metadata = field_metadata[model]
|
|
15
|
+
return if metadata.blank?
|
|
16
|
+
|
|
17
|
+
controlled_terms = metadata[:controlled_vocab_terms] || []
|
|
18
|
+
return if controlled_terms.blank?
|
|
19
|
+
|
|
20
|
+
controlled_terms.each do |field|
|
|
21
|
+
value = record[:raw_row][field]
|
|
22
|
+
next if value.blank?
|
|
23
|
+
|
|
24
|
+
authority = load_authority(field)
|
|
25
|
+
next if authority.nil?
|
|
26
|
+
|
|
27
|
+
term = authority.find(value)
|
|
28
|
+
next unless term.blank? || term.dig('active') == false
|
|
29
|
+
|
|
30
|
+
context[:errors] << {
|
|
31
|
+
row: row_index,
|
|
32
|
+
source_identifier: record[:source_identifier],
|
|
33
|
+
severity: 'error',
|
|
34
|
+
category: 'invalid_controlled_value',
|
|
35
|
+
column: field,
|
|
36
|
+
value: value,
|
|
37
|
+
message: I18n.t('bulkrax.importer.guided_import.validation.controlled_vocabulary_validator.errors.message',
|
|
38
|
+
value: value, field: field),
|
|
39
|
+
suggestion: suggestion(value, authority)
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.load_authority(field)
|
|
45
|
+
Qa::Authorities::Local.subauthority_for(field.pluralize)
|
|
46
|
+
rescue Qa::InvalidSubAuthority
|
|
47
|
+
begin
|
|
48
|
+
Qa::Authorities::Local.subauthority_for(field)
|
|
49
|
+
rescue Qa::InvalidSubAuthority
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
private_class_method :load_authority
|
|
54
|
+
|
|
55
|
+
def self.suggestion(value, authority)
|
|
56
|
+
suggestion = DidYouMean::SpellChecker.new(dictionary: dictionary_for(authority)).correct(value).first
|
|
57
|
+
return fallback_suggestion if suggestion.nil?
|
|
58
|
+
|
|
59
|
+
I18n.t('bulkrax.importer.guided_import.validation.did_you_mean', suggestion: suggestion)
|
|
60
|
+
end
|
|
61
|
+
private_class_method :suggestion
|
|
62
|
+
|
|
63
|
+
def self.fallback_suggestion
|
|
64
|
+
I18n.t('bulkrax.importer.guided_import.validation.controlled_vocabulary_validator.errors.suggestion')
|
|
65
|
+
end
|
|
66
|
+
private_class_method :fallback_suggestion
|
|
67
|
+
|
|
68
|
+
def self.dictionary_for(authority)
|
|
69
|
+
authority.all.filter_map { |term| term['label'] if term['active'] == true }.uniq
|
|
70
|
+
end
|
|
71
|
+
private_class_method :dictionary_for
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
module CsvRow
|
|
5
|
+
##
|
|
6
|
+
# Validates that each row has a unique source_identifier.
|
|
7
|
+
# Uses context[:seen_ids] (Hash: id => first_seen_row_number) to detect duplicates.
|
|
8
|
+
module DuplicateIdentifier
|
|
9
|
+
def self.call(record, row_index, context)
|
|
10
|
+
source_id = record[:source_identifier]
|
|
11
|
+
return if source_id.blank? && Bulkrax.fill_in_blank_source_identifiers.present?
|
|
12
|
+
|
|
13
|
+
source_id_label = context[:source_identifier] || 'source_identifier'
|
|
14
|
+
first_row = context[:seen_ids][source_id]
|
|
15
|
+
|
|
16
|
+
if first_row
|
|
17
|
+
add_duplicate_error(context, row_index, source_id, source_id_label, first_row)
|
|
18
|
+
else
|
|
19
|
+
context[:seen_ids][source_id] = row_index
|
|
20
|
+
add_existing_warning(context, row_index, source_id, source_id_label)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.add_duplicate_error(context, row_index, source_id, source_id_label, first_row)
|
|
25
|
+
context[:errors] << {
|
|
26
|
+
row: row_index,
|
|
27
|
+
source_identifier: source_id,
|
|
28
|
+
severity: 'error',
|
|
29
|
+
category: 'duplicate_source_identifier',
|
|
30
|
+
column: source_id_label,
|
|
31
|
+
value: source_id,
|
|
32
|
+
message: I18n.t('bulkrax.importer.guided_import.validation.duplicate_identifier_validator.errors.message',
|
|
33
|
+
value: source_id,
|
|
34
|
+
field: source_id_label,
|
|
35
|
+
original_row: first_row),
|
|
36
|
+
suggestion: I18n.t('bulkrax.importer.guided_import.validation.duplicate_identifier_validator.errors.suggestion',
|
|
37
|
+
field: source_id_label)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
private_class_method :add_duplicate_error
|
|
41
|
+
|
|
42
|
+
def self.add_existing_warning(context, row_index, source_id, source_id_label)
|
|
43
|
+
find_record = context[:find_record_by_source_identifier]
|
|
44
|
+
return unless find_record&.call(source_id)
|
|
45
|
+
|
|
46
|
+
context[:errors] << {
|
|
47
|
+
row: row_index,
|
|
48
|
+
source_identifier: source_id,
|
|
49
|
+
severity: 'warning',
|
|
50
|
+
category: 'existing_source_identifier',
|
|
51
|
+
column: source_id_label,
|
|
52
|
+
value: source_id,
|
|
53
|
+
message: I18n.t('bulkrax.importer.guided_import.validation.existing_source_identifier_validator.warnings.message',
|
|
54
|
+
value: source_id,
|
|
55
|
+
field: source_id_label),
|
|
56
|
+
suggestion: I18n.t('bulkrax.importer.guided_import.validation.existing_source_identifier_validator.warnings.suggestion',
|
|
57
|
+
field: source_id_label)
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
private_class_method :add_existing_warning
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
module CsvRow
|
|
5
|
+
##
|
|
6
|
+
# Validates that each row has a value for source_identifier unless
|
|
7
|
+
# fill_in_blank_source_identifiers is configured (in which case Bulkrax
|
|
8
|
+
# will generate one automatically).
|
|
9
|
+
module MissingSourceIdentifier
|
|
10
|
+
def self.call(record, row_index, context)
|
|
11
|
+
return if Bulkrax.fill_in_blank_source_identifiers.present?
|
|
12
|
+
return if record[:source_identifier].present?
|
|
13
|
+
|
|
14
|
+
source_id_label = context[:source_identifier] || 'source_identifier'
|
|
15
|
+
|
|
16
|
+
context[:errors] << {
|
|
17
|
+
row: row_index,
|
|
18
|
+
source_identifier: nil,
|
|
19
|
+
severity: 'error',
|
|
20
|
+
category: 'missing_source_identifier',
|
|
21
|
+
column: source_id_label,
|
|
22
|
+
value: nil,
|
|
23
|
+
message: I18n.t('bulkrax.importer.guided_import.validation.missing_source_identifier_validator.errors.message',
|
|
24
|
+
field: source_id_label),
|
|
25
|
+
suggestion: I18n.t('bulkrax.importer.guided_import.validation.missing_source_identifier_validator.errors.suggestion',
|
|
26
|
+
field: source_id_label)
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
module CsvRow
|
|
5
|
+
##
|
|
6
|
+
# Validates that any parent references in a row point to source identifiers
|
|
7
|
+
# that exist either elsewhere in the same CSV or as existing repository records.
|
|
8
|
+
# Uses context[:all_ids] (Set of all source identifiers) to validate references
|
|
9
|
+
# within the CSV, and context[:find_record_by_source_identifier] (callable) to
|
|
10
|
+
# look up existing records in the same way the importer does at runtime.
|
|
11
|
+
# Uses context[:parent_split_pattern] (String/Regexp, may be nil) for multi-value splitting.
|
|
12
|
+
module ParentReference
|
|
13
|
+
def self.call(record, row_index, context)
|
|
14
|
+
all_ids = context[:all_ids]
|
|
15
|
+
find_record = context[:find_record_by_source_identifier]
|
|
16
|
+
|
|
17
|
+
collect_parent_ids(record, context).each do |parent_id|
|
|
18
|
+
next if all_ids.include?(parent_id)
|
|
19
|
+
next if find_record&.call(parent_id)
|
|
20
|
+
|
|
21
|
+
context[:errors] << {
|
|
22
|
+
row: row_index,
|
|
23
|
+
source_identifier: record[:source_identifier],
|
|
24
|
+
severity: 'error',
|
|
25
|
+
category: 'invalid_parent_reference',
|
|
26
|
+
column: 'parent',
|
|
27
|
+
value: parent_id,
|
|
28
|
+
message: I18n.t('bulkrax.importer.guided_import.validation.parent_reference_validator.errors.message',
|
|
29
|
+
value: parent_id,
|
|
30
|
+
field: 'source_identifier'),
|
|
31
|
+
suggestion: I18n.t('bulkrax.importer.guided_import.validation.parent_reference_validator.errors.suggestion')
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.collect_parent_ids(record, context)
|
|
37
|
+
split_pattern = context[:parent_split_pattern]
|
|
38
|
+
parent_column = context[:parent_column] || 'parents'
|
|
39
|
+
|
|
40
|
+
base_ids = if split_pattern
|
|
41
|
+
record[:parent].to_s.split(split_pattern).map(&:strip).reject(&:blank?)
|
|
42
|
+
elsif record[:parent].present?
|
|
43
|
+
[record[:parent].to_s.strip]
|
|
44
|
+
else
|
|
45
|
+
[]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
suffix_pattern = /\A#{Regexp.escape(parent_column)}_\d+\z/
|
|
49
|
+
suffix_ids = record[:raw_row]
|
|
50
|
+
.select { |k, _| k.to_s.match?(suffix_pattern) }
|
|
51
|
+
.values
|
|
52
|
+
.map(&:to_s).map(&:strip).reject(&:blank?)
|
|
53
|
+
|
|
54
|
+
(base_ids + suffix_ids).uniq
|
|
55
|
+
end
|
|
56
|
+
private_class_method :collect_parent_ids
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
module CsvRow
|
|
5
|
+
##
|
|
6
|
+
# Validates that each row provides a value for every required field of its model.
|
|
7
|
+
# Numeric suffixes on column names are normalised before checking
|
|
8
|
+
# (e.g. 'title_1' satisfies the 'title' requirement).
|
|
9
|
+
module RequiredValues
|
|
10
|
+
def self.call(record, row_index, context)
|
|
11
|
+
field_metadata = context[:field_metadata]
|
|
12
|
+
return if field_metadata.blank?
|
|
13
|
+
|
|
14
|
+
using_default = record[:model].blank?
|
|
15
|
+
model = record[:model].presence || Bulkrax.default_work_type
|
|
16
|
+
metadata = field_metadata[model]
|
|
17
|
+
return if metadata.blank?
|
|
18
|
+
|
|
19
|
+
add_default_work_type_warning(context, record, row_index, model) if using_default
|
|
20
|
+
add_missing_required_value_errors(context, record, row_index, metadata)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.add_default_work_type_warning(context, record, row_index, model)
|
|
24
|
+
# Suppress per-row warning when a file-level notice already covers all rows.
|
|
25
|
+
return if context[:notices]&.any? { |n| n[:field] == 'model' }
|
|
26
|
+
|
|
27
|
+
context[:errors] << {
|
|
28
|
+
row: row_index,
|
|
29
|
+
source_identifier: record[:source_identifier],
|
|
30
|
+
severity: 'warning',
|
|
31
|
+
category: 'default_work_type_used',
|
|
32
|
+
column: 'model',
|
|
33
|
+
value: nil,
|
|
34
|
+
message: I18n.t('bulkrax.importer.guided_import.validation.default_work_type_validator.warnings.message',
|
|
35
|
+
default_work_type: model),
|
|
36
|
+
suggestion: I18n.t('bulkrax.importer.guided_import.validation.default_work_type_validator.warnings.suggestion')
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
private_class_method :add_default_work_type_warning
|
|
40
|
+
|
|
41
|
+
def self.add_missing_required_value_errors(context, record, row_index, metadata)
|
|
42
|
+
(metadata[:required_terms] || []).each do |field|
|
|
43
|
+
next if record[:raw_row].any? { |key, value| normalize_header(key.to_s) == field && value.present? }
|
|
44
|
+
|
|
45
|
+
context[:errors] << {
|
|
46
|
+
row: row_index,
|
|
47
|
+
source_identifier: record[:source_identifier],
|
|
48
|
+
severity: 'error',
|
|
49
|
+
category: 'missing_required_value',
|
|
50
|
+
column: field,
|
|
51
|
+
value: nil,
|
|
52
|
+
message: I18n.t('bulkrax.importer.guided_import.validation.required_field_validator.errors.message', field: field),
|
|
53
|
+
suggestion: I18n.t('bulkrax.importer.guided_import.validation.required_field_validator.errors.suggestion', field: field)
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
private_class_method :add_missing_required_value_errors
|
|
58
|
+
|
|
59
|
+
def self.normalize_header(header)
|
|
60
|
+
header.sub(/_\d+\z/, '')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div class="accordion-container">
|
|
3
3
|
<div class="accordion-heading" role="tab" id="parsed-metadata-heading">
|
|
4
4
|
<a class="accordion-title" role="button" data-toggle="collapse" data-target="#parsed-metadata-show" aria-expanded="true" aria-controls="parsed-metadata-show">
|
|
5
|
-
|
|
5
|
+
<%= t('bulkrax.entry.labels.parsed_metadata') %>:
|
|
6
6
|
</a>
|
|
7
7
|
<a role="button" data-toggle="collapse" data-target="#parsed-metadata-show" aria-expanded="true" aria-controls="parsed-metadata-show">
|
|
8
8
|
<div class="accordion-icon fa fa-times-circle" aria-hidden="true"></div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div class="accordion-container">
|
|
3
3
|
<div class="accordion-heading" role="tab" id="raw-metadata-heading">
|
|
4
4
|
<a class="accordion-title" role="button" data-toggle="collapse" data-target="#raw-metadata-show" aria-expanded="true" aria-controls="raw-metadata-show">
|
|
5
|
-
|
|
5
|
+
<%= t('bulkrax.entry.labels.raw_metadata') %>:
|
|
6
6
|
</a>
|
|
7
7
|
<a role="button" data-toggle="collapse" data-target="#raw-metadata-show" aria-expanded="true" aria-controls="raw-metadata-show">
|
|
8
8
|
<div class="accordion-icon fa fa-times-circle" aria-hidden="true"></div>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
<p class='bulkrax-p-align'>
|
|
15
15
|
<strong><%= t('bulkrax.importer.labels.type') %>:</strong>
|
|
16
|
-
<%= @entry.factory_class || '
|
|
16
|
+
<%= @entry.factory_class || t('bulkrax.entry.labels.unknown') %>
|
|
17
17
|
</p>
|
|
18
18
|
<%= render partial: 'raw_metadata'%>
|
|
19
19
|
|
|
@@ -39,29 +39,29 @@
|
|
|
39
39
|
<% if factory_record.present? %>
|
|
40
40
|
<% factory_record_class = factory_record.class %>
|
|
41
41
|
<% factory_record_class_human = factory_record_class.model_name.human %>
|
|
42
|
-
<strong><%= factory_record_class_human
|
|
42
|
+
<strong><%= t('bulkrax.entry.labels.record_link', record_type: factory_record_class_human) %>:</strong>
|
|
43
43
|
<% if defined?(Hyrax) && factory_record_class_human == 'Collection' %>
|
|
44
44
|
<%= link_to factory_record_class_human, hyrax.polymorphic_path(factory_record) %>
|
|
45
45
|
<% else %>
|
|
46
46
|
<%= link_to factory_record_class_human, main_app.polymorphic_path(factory_record) %>
|
|
47
47
|
<% end %>
|
|
48
48
|
<% else %>
|
|
49
|
-
<strong
|
|
49
|
+
<strong><%= t('bulkrax.entry.labels.item_link') %>:</strong> <%= t('bulkrax.entry.labels.item_not_imported') %>
|
|
50
50
|
<% end %>
|
|
51
51
|
<% rescue => e %>
|
|
52
|
-
<strong
|
|
52
|
+
<strong><%= t('bulkrax.entry.labels.item_link') %>:</strong> <%= t('bulkrax.entry.labels.item_link_error', message: e.message) %>
|
|
53
53
|
<% end %>
|
|
54
54
|
<% else %>
|
|
55
55
|
<% record = @entry&.hyrax_record %>
|
|
56
56
|
<% if record.present? && @entry.factory_class %>
|
|
57
|
-
<strong><%= record.model_name.human
|
|
57
|
+
<strong><%= t('bulkrax.entry.labels.record_link', record_type: record.model_name.human) %>:</strong>
|
|
58
58
|
<% if defined?(Hyrax) && record.model_name.human == "Collection" %>
|
|
59
59
|
<%= link_to record.model_name.human, hyrax.polymorphic_path(record) %>
|
|
60
60
|
<% else %>
|
|
61
61
|
<%= link_to record.model_name.human, main_app.polymorphic_path(record) %>
|
|
62
62
|
<% end %>
|
|
63
63
|
<% else %>
|
|
64
|
-
<strong
|
|
64
|
+
<strong><%= t('bulkrax.entry.labels.item_link') %>:</strong> <%= t('bulkrax.entry.labels.item_no_association') %>
|
|
65
65
|
<% end %>
|
|
66
66
|
<% end %>
|
|
67
67
|
</p>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div class="panel-body card-body">
|
|
2
2
|
<% if exporter.errors.any? %>
|
|
3
3
|
<div id="error_explanation">
|
|
4
|
-
<h2><%=
|
|
4
|
+
<h2><%= t('bulkrax.exporter.validations.errors_prohibited', count: exporter.errors.count) %></h2>
|
|
5
5
|
|
|
6
6
|
<ul>
|
|
7
7
|
<% exporter.errors.full_messages.each do |message| %>
|
|
@@ -11,35 +11,31 @@
|
|
|
11
11
|
</div>
|
|
12
12
|
<% end %>
|
|
13
13
|
|
|
14
|
-
<%= form.input :name,
|
|
14
|
+
<%= form.input :name, input_html: { class: 'form-control' } %>
|
|
15
15
|
|
|
16
16
|
<%= form.hidden_field :user_id, value: current_user.id %>
|
|
17
17
|
|
|
18
18
|
<%= form.input :export_type,
|
|
19
|
-
collection:
|
|
20
|
-
label: t('bulkrax.exporter.labels.export_type'),
|
|
19
|
+
collection: form.object.export_type_list,
|
|
21
20
|
required: true,
|
|
22
|
-
prompt: '
|
|
21
|
+
prompt: t('bulkrax.exporter.prompts.export_type'),
|
|
23
22
|
input_html: { class: 'form-control' } %>
|
|
24
23
|
|
|
25
24
|
<%= form.input :export_from,
|
|
26
25
|
collection: form.object.export_from_list,
|
|
27
|
-
label: t('bulkrax.exporter.labels.export_from'),
|
|
28
26
|
required: true,
|
|
29
|
-
prompt: '
|
|
27
|
+
prompt: t('bulkrax.exporter.prompts.export_from'),
|
|
30
28
|
input_html: { class: 'form-control' } %>
|
|
31
29
|
|
|
32
30
|
<%= form.input :export_source_importer,
|
|
33
|
-
label: t('bulkrax.exporter.labels.importer'),
|
|
34
31
|
required: true,
|
|
35
|
-
prompt: '
|
|
32
|
+
prompt: t('bulkrax.exporter.prompts.select_from_list'),
|
|
36
33
|
label_html: { class: 'importer export-source-option d-none hidden' },
|
|
37
34
|
input_html: { class: 'importer export-source-option d-none hidden form-control' },
|
|
38
|
-
collection:
|
|
35
|
+
collection: form.object.importers_list.sort %>
|
|
39
36
|
|
|
40
37
|
<%= form.input :export_source_collection,
|
|
41
|
-
prompt: '
|
|
42
|
-
label: t('bulkrax.exporter.labels.collection'),
|
|
38
|
+
prompt: t('bulkrax.exporter.prompts.start_typing'),
|
|
43
39
|
required: true,
|
|
44
40
|
placeholder: @collection&.title&.first,
|
|
45
41
|
label_html: { class: 'collection export-source-option d-none hidden' },
|
|
@@ -53,61 +49,41 @@
|
|
|
53
49
|
%>
|
|
54
50
|
|
|
55
51
|
<%= form.input :export_source_worktype,
|
|
56
|
-
label: t('bulkrax.exporter.labels.worktype'),
|
|
57
52
|
required: true,
|
|
58
|
-
prompt: '
|
|
53
|
+
prompt: t('bulkrax.exporter.prompts.select_from_list'),
|
|
59
54
|
label_html: { class: 'worktype export-source-option d-none hidden' },
|
|
60
55
|
input_html: { class: 'worktype export-source-option d-none hidden form-control' },
|
|
61
56
|
collection: Bulkrax.curation_concerns.map { |cc| [cc.to_s, cc.to_s] } %>
|
|
62
57
|
|
|
63
58
|
<%= form.input :limit,
|
|
64
59
|
as: :integer,
|
|
65
|
-
hint: 'leave blank or 0 for all records',
|
|
66
|
-
label: t('bulkrax.exporter.labels.limit'),
|
|
67
60
|
input_html: { class: 'form-control' } %>
|
|
68
61
|
|
|
69
|
-
<%= form.input :generated_metadata?,
|
|
70
|
-
as: :boolean,
|
|
71
|
-
label: t('bulkrax.exporter.labels.generated_metadata'),
|
|
72
|
-
hint: t('bulkrax.exporter.hints.generated_metadata') %>
|
|
62
|
+
<%= form.input :generated_metadata?, as: :boolean %>
|
|
73
63
|
|
|
74
|
-
<%= form.input :include_thumbnails?,
|
|
75
|
-
as: :boolean,
|
|
76
|
-
label: t('bulkrax.exporter.labels.include_thumbnails'),
|
|
77
|
-
hint: t('bulkrax.exporter.hints.include_thumbnails') %>
|
|
64
|
+
<%= form.input :include_thumbnails?, as: :boolean %>
|
|
78
65
|
|
|
79
|
-
<%= form.input :date_filter,
|
|
80
|
-
as: :boolean,
|
|
81
|
-
label: t('bulkrax.exporter.labels.filter_by_date') %>
|
|
66
|
+
<%= form.input :date_filter, as: :boolean %>
|
|
82
67
|
|
|
83
68
|
<div id="date_filter_picker" class="d-none hidden">
|
|
84
|
-
<%= form.input :start_date,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
input_html: { class: 'form-control' } %>
|
|
88
|
-
|
|
89
|
-
<%= form.input :finish_date,
|
|
90
|
-
as: :date,
|
|
91
|
-
label: t('bulkrax.exporter.labels.finish_date'),
|
|
92
|
-
input_html: { class: 'form-control' } %>
|
|
69
|
+
<%= form.input :start_date, as: :date, input_html: { class: 'form-control' } %>
|
|
70
|
+
|
|
71
|
+
<%= form.input :finish_date, as: :date, input_html: { class: 'form-control' } %>
|
|
93
72
|
</div>
|
|
94
73
|
<% if defined?(::Hyrax) %>
|
|
95
74
|
<%= form.input :work_visibility,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
input_html: { class: 'form-control' } %>
|
|
75
|
+
collection: form.object.work_visibility_list,
|
|
76
|
+
input_html: { class: 'form-control' } %>
|
|
99
77
|
<% end %>
|
|
100
78
|
|
|
101
79
|
<% if defined?(::Hyrax) %>
|
|
102
80
|
<%= form.input :workflow_status,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
input_html: { class: 'form-control' } %>
|
|
81
|
+
collection: form.object.workflow_status_list,
|
|
82
|
+
input_html: { class: 'form-control' } %>
|
|
106
83
|
<% end %>
|
|
107
84
|
|
|
108
85
|
<%= form.input :parser_klass,
|
|
109
86
|
collection: Bulkrax.parsers.map {|p| [p[:name], p[:class_name], {'data-partial' => p[:partial]}] if p[:class_name].constantize.export_supported? }.compact,
|
|
110
|
-
label: t('bulkrax.exporter.labels.export_format'),
|
|
111
87
|
input_html: { class: 'form-control' } %>
|
|
112
88
|
</div>
|
|
113
89
|
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
<%= render 'form', exporter: @exporter, form: form %>
|
|
10
10
|
<div class="panel-footer card-footer bulkrax-card-footer">
|
|
11
11
|
<div class='pull-right'>
|
|
12
|
-
<%= form.button :submit, value: '
|
|
12
|
+
<%= form.button :submit, value: t('helpers.action.exporter.update'), class: 'btn btn-primary' %>
|
|
13
13
|
|
|
|
14
|
-
<%= form.button :submit, value: '
|
|
14
|
+
<%= form.button :submit, value: t('helpers.action.exporter.update_and_re_export'), class: 'btn btn-primary' %>
|
|
15
15
|
|
|
|
16
16
|
<% cancel_path = form.object.persisted? ? exporter_path(form.object) : exporters_path %>
|
|
17
17
|
<%= link_to t('bulkrax.cancel'), cancel_path, class: 'btn btn-default ' %>
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
<table id='exporters-table' class="table table-striped">
|
|
18
18
|
<thead>
|
|
19
19
|
<tr>
|
|
20
|
-
<th scope="col"
|
|
21
|
-
<th scope="col"
|
|
22
|
-
<th scope="col"
|
|
23
|
-
<th scope="col"
|
|
24
|
-
<th scope="col"
|
|
20
|
+
<th scope="col"><%= t('bulkrax.table_header.labels.name') %></th>
|
|
21
|
+
<th scope="col"><%= t('bulkrax.table_header.labels.status') %></th>
|
|
22
|
+
<th scope="col"><%= t('bulkrax.table_header.labels.date_exported') %></th>
|
|
23
|
+
<th scope="col"><%= t('bulkrax.table_header.labels.downloadable_files') %></th>
|
|
24
|
+
<th scope="col"><%= t('bulkrax.table_header.labels.actions') %></th>
|
|
25
25
|
</tr>
|
|
26
26
|
</thead>
|
|
27
27
|
</table>
|
|
@@ -9,12 +9,10 @@
|
|
|
9
9
|
<%= render 'form', exporter: @exporter, form: form %>
|
|
10
10
|
<div class="panel-footer card-footer bulkrax-card-footer">
|
|
11
11
|
<div class='pull-right'>
|
|
12
|
-
<%=
|
|
13
|
-
|
|
14
|
-
<%= form.button :submit, value: 'Create', class: 'btn btn-primary' %>
|
|
15
|
-
|
|
|
12
|
+
<%= submit_tag t('helpers.action.exporter.create_and_export'), class: 'btn btn-primary me-2' %>
|
|
13
|
+
<%= submit_tag t('helpers.action.exporter.create'), class: 'btn btn-default me-2' %>
|
|
16
14
|
<% cancel_path = form.object.persisted? ? exporter_path(form.object) : exporters_path %>
|
|
17
|
-
<%= link_to t('bulkrax.cancel'), cancel_path, class: 'btn btn-default
|
|
15
|
+
<%= link_to t('bulkrax.cancel'), cancel_path, class: 'btn btn-default' %>
|
|
18
16
|
</div>
|
|
19
17
|
</div>
|
|
20
18
|
<% end %>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<%= simple_form_for @exporter, method: :get, url: exporter_download_path(@exporter), html: { class: 'form-inline bulkrax-p-align' } do |form| %>
|
|
12
12
|
<strong>Download:</strong>
|
|
13
13
|
<%= render 'downloads', exporter: @exporter, form: form %>
|
|
14
|
-
<%= form.button :submit, value: '
|
|
14
|
+
<%= form.button :submit, value: t('helpers.action.exporter.download'), data: { disable_with: false } %>
|
|
15
15
|
<% end %>
|
|
16
16
|
<% end %>
|
|
17
17
|
|
|
@@ -101,9 +101,9 @@
|
|
|
101
101
|
<%= render partial: 'bulkrax/shared/entries_tab', locals: { item: @exporter } %>
|
|
102
102
|
</div>
|
|
103
103
|
<br>
|
|
104
|
-
<%= link_to '
|
|
104
|
+
<%= link_to t('helpers.action.exporter.edit'), edit_exporter_path(@exporter) %>
|
|
105
105
|
|
|
|
106
|
-
<%= link_to '
|
|
106
|
+
<%= link_to t('helpers.action.exporter.back'), exporters_path %>
|
|
107
107
|
</div>
|
|
108
108
|
</div>
|
|
109
109
|
</div>
|