bulkrax 9.3.4 → 9.3.5
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/app/assets/stylesheets/bulkrax/import_export.scss +9 -2
- data/app/controllers/bulkrax/importers_controller.rb +9 -0
- data/app/jobs/bulkrax/export_work_job.rb +1 -3
- data/app/services/bulkrax/sample_csv_service/column_builder.rb +58 -0
- data/app/services/bulkrax/sample_csv_service/column_descriptor.rb +56 -0
- data/app/services/bulkrax/sample_csv_service/csv_builder.rb +82 -0
- data/app/services/bulkrax/sample_csv_service/explanation_builder.rb +51 -0
- data/app/services/bulkrax/sample_csv_service/field_analyzer.rb +54 -0
- data/app/services/bulkrax/sample_csv_service/file_path_generator.rb +16 -0
- data/app/services/bulkrax/sample_csv_service/mapping_manager.rb +36 -0
- data/app/services/bulkrax/sample_csv_service/model_loader.rb +40 -0
- data/app/services/bulkrax/sample_csv_service/row_builder.rb +33 -0
- data/app/services/bulkrax/sample_csv_service/schema_analyzer.rb +69 -0
- data/app/services/bulkrax/sample_csv_service/split_formatter.rb +42 -0
- data/app/services/bulkrax/sample_csv_service/value_determiner.rb +67 -0
- data/app/services/bulkrax/sample_csv_service.rb +78 -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/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 +13 -13
- data/app/views/bulkrax/importers/new.html.erb +10 -9
- data/app/views/bulkrax/importers/show.html.erb +7 -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/locales/bulkrax.en.yml +235 -2
- data/config/routes.rb +1 -0
- data/lib/bulkrax/version.rb +1 -1
- data/lib/bulkrax.rb +0 -3
- data/lib/tasks/bulkrax_tasks.rake +0 -102
- metadata +15 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7205ae1ec1e6287a3afe95e94df9e3f7afcdb72ad0d6e934cca44004b28e8a4
|
|
4
|
+
data.tar.gz: bae60650dc0ad54df42c9aa7da87829039462f6159c25b296a029c28f99006e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b81763a20e21c294bf0fa56479cafc506fdc589f31c11d3bbd0383b28fda247734f3abe4d0797e5601abd64815aa457ea8d908bfbf427a03fa5e500bbfe60d2
|
|
7
|
+
data.tar.gz: e34566bafe813523345aaa41b2ad9fc2921e78be118250dfa911453fba185ea25cea6f74440fa6d5ddbb1dbcc699aca1f4ca5d3eb139c3dae65d27953de5f45d
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
.bulkrax-card-footer {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
min-height: 60px;
|
|
4
3
|
}
|
|
5
4
|
|
|
6
5
|
div.importer_parser_fields_file_style span.radio {
|
|
@@ -45,3 +44,11 @@ div#s2id_exporter_export_source_collection {
|
|
|
45
44
|
margin-left: 10px;
|
|
46
45
|
margin-right: 10px;
|
|
47
46
|
}
|
|
47
|
+
|
|
48
|
+
// Make form input labels bold
|
|
49
|
+
.simple_form label.control-label,
|
|
50
|
+
.simple_form label.form-label,
|
|
51
|
+
.simple_form .form-group > label,
|
|
52
|
+
.simple_form .input > label {
|
|
53
|
+
font-weight: bold;
|
|
54
|
+
}
|
|
@@ -65,6 +65,15 @@ module Bulkrax
|
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# POST /importers/sample_csv_file
|
|
69
|
+
def sample_csv_file
|
|
70
|
+
sample = Bulkrax::SampleCsvService.call(model_name: 'all', output: 'file')
|
|
71
|
+
send_file sample, filename: File.basename(sample), type: 'text/csv'
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
flash[:error] = "Unable to generate sample CSV file: #{e.message}"
|
|
74
|
+
redirect_back fallback_location: bulkrax.importers_path
|
|
75
|
+
end
|
|
76
|
+
|
|
68
77
|
# GET /importers/1/edit
|
|
69
78
|
def edit
|
|
70
79
|
if api_request?
|
|
@@ -18,12 +18,10 @@ module Bulkrax
|
|
|
18
18
|
else
|
|
19
19
|
if entry.failed?
|
|
20
20
|
ExporterRun.increment_counter(:failed_records, args[1])
|
|
21
|
-
ExporterRun.decrement_counter(:enqueued_records, args[1]) unless exporter_run.reload.enqueued_records <= 0
|
|
22
|
-
raise entry.reload.current_status.error_class.constantize
|
|
23
21
|
else
|
|
24
22
|
ExporterRun.increment_counter(:processed_records, args[1])
|
|
25
|
-
ExporterRun.decrement_counter(:enqueued_records, args[1]) unless exporter_run.reload.enqueued_records <= 0
|
|
26
23
|
end
|
|
24
|
+
ExporterRun.decrement_counter(:enqueued_records, args[1]) unless exporter_run.reload.enqueued_records <= 0
|
|
27
25
|
# rubocop:enable Rails/SkipsModelValidations
|
|
28
26
|
end
|
|
29
27
|
return entry if exporter_run.reload.enqueued_records.positive?
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Builds column headers for CSV
|
|
5
|
+
class SampleCsvService::ColumnBuilder
|
|
6
|
+
def initialize(service)
|
|
7
|
+
@service = service
|
|
8
|
+
@descriptor = SampleCsvService::ColumnDescriptor.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def all_columns
|
|
12
|
+
required_columns + property_columns
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def required_columns
|
|
16
|
+
mapped_core_columns +
|
|
17
|
+
relationship_columns +
|
|
18
|
+
file_columns
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def mapped_core_columns
|
|
24
|
+
@descriptor.core_columns.map do |column|
|
|
25
|
+
@service.mapping_manager.key_to_mapped_column(column)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def property_columns
|
|
30
|
+
field_lists = @service.all_models.map do |m|
|
|
31
|
+
@service.field_analyzer.find_or_create_field_list_for(model_name: m)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
properties = field_lists
|
|
35
|
+
.flat_map { |item| item.values.flat_map { |config| config["properties"] || [] } }
|
|
36
|
+
.uniq
|
|
37
|
+
.map { |property| @service.mapping_manager.key_to_mapped_column(property) }
|
|
38
|
+
.uniq
|
|
39
|
+
|
|
40
|
+
(properties - required_columns).sort
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def relationship_columns
|
|
44
|
+
[
|
|
45
|
+
@service.mapping_manager.find_by_flag("related_children_field_mapping", 'children'),
|
|
46
|
+
@service.mapping_manager.find_by_flag("related_parents_field_mapping", 'parents')
|
|
47
|
+
]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def file_columns
|
|
51
|
+
SampleCsvService::ColumnDescriptor::COLUMN_DESCRIPTIONS[:files].flat_map do |property_hash|
|
|
52
|
+
property_hash.keys.map do |key|
|
|
53
|
+
@service.mapping_manager.key_to_mapped_column(key)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Manages column descriptions and metadata
|
|
5
|
+
class SampleCsvService::ColumnDescriptor
|
|
6
|
+
COLUMN_DESCRIPTIONS = {
|
|
7
|
+
include_first: [
|
|
8
|
+
{ "model" => "The work types configured in your repository are listed below.\nIf left blank, your default work type, #{Bulkrax.default_work_type}, is used." },
|
|
9
|
+
{ "source_identifier" => "This must be a unique identifier.\nIt can be alphanumeric with some special characters (e.g. hyphens, colons), and URLs are also supported." },
|
|
10
|
+
{ "id" => "This column would optionally be included only if it is a re-import, i.e. for updating or deleting records.\nThis is a key identifier used by the system, which you wouldn't have for new imports." },
|
|
11
|
+
{ "rights_statement" => "Rights statement URI for the work.\nIf not included, uses the value specified on the bulk import configuration screen." }
|
|
12
|
+
],
|
|
13
|
+
visibility: [
|
|
14
|
+
{ "visibility" => "Uses the value specified on the bulk import configuration screen if not added here.\nValid options: open, authenticated, restricted, embargo, lease" },
|
|
15
|
+
{ "embargo_release_date" => "Required for embargo (yyyy-mm-dd)" },
|
|
16
|
+
{ "visibility_during_embargo" => "Required for embargo" },
|
|
17
|
+
{ "visibility_after_embargo" => "Required for embargo" },
|
|
18
|
+
{ "lease_expiration_date" => "Required for lease (yyyy-mm-dd)" },
|
|
19
|
+
{ "visibility_during_lease" => "Required for lease" },
|
|
20
|
+
{ "visibility_after_lease" => "Required for lease" }
|
|
21
|
+
],
|
|
22
|
+
files: [
|
|
23
|
+
{ "file" => "Use filenames exactly matching those in your files folder.\nZip your CSV and files folder together and attach this to your importer." },
|
|
24
|
+
{ "remote_files" => "Use the URLs to remote files to be attached to the work." }
|
|
25
|
+
],
|
|
26
|
+
relationships: [
|
|
27
|
+
{ "parents" => "The source_identifier or id of work or collection to be attached as parent." },
|
|
28
|
+
{ "children" => "The source_identifier or id of work or file to be attached as child." }
|
|
29
|
+
],
|
|
30
|
+
other: [
|
|
31
|
+
{ "hide_from_catalog_search" => "Set to 1 to hide the collection from catalog search results." },
|
|
32
|
+
{ "show_pdf_download_button" => "Set to 1 to show a PDF download link on the work's page." },
|
|
33
|
+
{ "show_pdf_viewer" => "Set to 1 to show a PDF viewer on the work's page." },
|
|
34
|
+
{ "video_embed" => "A valid URL to a hosted video that can appear in an iframe, beginning with 'http://' or 'https://'." }
|
|
35
|
+
]
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
def core_columns
|
|
39
|
+
extract_column_names(:include_first) + extract_column_names(:visibility)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_description_for(column)
|
|
43
|
+
COLUMN_DESCRIPTIONS.each_value do |group|
|
|
44
|
+
prop = group.find { |hash| hash.key?(column) }
|
|
45
|
+
return prop[column] if prop
|
|
46
|
+
end
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def extract_column_names(group)
|
|
53
|
+
COLUMN_DESCRIPTIONS[group].map { |hash| hash.keys.first }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Builds CSV content
|
|
5
|
+
class SampleCsvService::CsvBuilder
|
|
6
|
+
IGNORED_PROPERTIES = %w[
|
|
7
|
+
admin_set_id alternate_ids
|
|
8
|
+
bulkrax_identifier
|
|
9
|
+
collection_type_gid contexts created_at
|
|
10
|
+
date date_modified date_uploaded depositor
|
|
11
|
+
embargo embargo_id
|
|
12
|
+
file_ids
|
|
13
|
+
has_model head
|
|
14
|
+
internal_resource is_child
|
|
15
|
+
lease lease_id
|
|
16
|
+
member_ids member_of_collection_ids modified_date
|
|
17
|
+
new_record
|
|
18
|
+
on_behalf_of owner proxy_depositor
|
|
19
|
+
rendering_ids representative_id
|
|
20
|
+
schema_version split_from_pdf_id state tail
|
|
21
|
+
thumbnail_id
|
|
22
|
+
updated_at
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def initialize(service)
|
|
26
|
+
@service = service
|
|
27
|
+
@column_builder = SampleCsvService::ColumnBuilder.new(service)
|
|
28
|
+
@row_builder = SampleCsvService::RowBuilder.new(service)
|
|
29
|
+
@header_row = nil
|
|
30
|
+
@required_headings = []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def write_to_file(file_path)
|
|
34
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
35
|
+
CSV.open(file_path, "w") { |csv| write_rows(csv) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def generate_string
|
|
39
|
+
CSV.generate { |csv| write_rows(csv) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def write_rows(csv)
|
|
45
|
+
csv_rows.each { |row| csv << row }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def csv_rows
|
|
49
|
+
@header_row = fill_header_row
|
|
50
|
+
rows = [
|
|
51
|
+
@header_row,
|
|
52
|
+
@row_builder.build_explanation_row(@header_row),
|
|
53
|
+
*@row_builder.build_model_rows(@header_row)
|
|
54
|
+
]
|
|
55
|
+
remove_empty_columns(rows)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def fill_header_row
|
|
59
|
+
@required_headings = @column_builder.required_columns
|
|
60
|
+
all_columns = @column_builder.all_columns
|
|
61
|
+
filtered = all_columns - IGNORED_PROPERTIES
|
|
62
|
+
@required_headings = @column_builder.required_columns & filtered
|
|
63
|
+
filtered
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def remove_empty_columns(rows)
|
|
67
|
+
return rows if rows.empty?
|
|
68
|
+
|
|
69
|
+
columns = rows.transpose
|
|
70
|
+
non_empty_columns = columns.select { |col| keep_column?(col) }
|
|
71
|
+
non_empty_columns.transpose
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def keep_column?(column)
|
|
75
|
+
heading = column[0]
|
|
76
|
+
return true if @required_headings.include?(heading)
|
|
77
|
+
|
|
78
|
+
# Check if any data row has content
|
|
79
|
+
column[2..-1].any? { |value| !value.nil? && value != "" && value != "---" }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Builds explanations for CSV columns
|
|
5
|
+
class SampleCsvService::ExplanationBuilder
|
|
6
|
+
def initialize(service)
|
|
7
|
+
@service = service
|
|
8
|
+
@descriptor = SampleCsvService::ColumnDescriptor.new
|
|
9
|
+
@split_formatter = SampleCsvService::SplitFormatter.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build_explanations(header_row)
|
|
13
|
+
header_row.map do |column|
|
|
14
|
+
{ column => build_explanation(column) }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def build_explanation(column)
|
|
21
|
+
mapping_key = @service.mapping_manager.mapped_to_key(column)
|
|
22
|
+
|
|
23
|
+
column_description = @descriptor.find_description_for(column)
|
|
24
|
+
controlled_vocab_info = controlled_vocab_text(mapping_key)
|
|
25
|
+
split_info = split_text(mapping_key, controlled_vocab_info)
|
|
26
|
+
|
|
27
|
+
components = [
|
|
28
|
+
column_description,
|
|
29
|
+
controlled_vocab_info,
|
|
30
|
+
split_info
|
|
31
|
+
].compact
|
|
32
|
+
|
|
33
|
+
components.join("\n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def controlled_vocab_text(field_name)
|
|
37
|
+
vocab_terms = @service.field_analyzer.controlled_vocab_terms
|
|
38
|
+
# based_near 'location' is handled specially because its controlled vocabulary is implemented differently
|
|
39
|
+
return unless vocab_terms.include?(field_name) || field_name == 'based_near'
|
|
40
|
+
'This property uses a controlled vocabulary.'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def split_text(mapping_key, controlled_vocab_info)
|
|
44
|
+
# regardless of schema, most controlled vocab fields only accept single values due to form limitations
|
|
45
|
+
return nil if controlled_vocab_info.present? && !mapping_key.in?(%w[location resource_type])
|
|
46
|
+
split_value = @service.mapping_manager.split_value_for(mapping_key)
|
|
47
|
+
return nil unless split_value
|
|
48
|
+
@split_formatter.format(split_value)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Analyzes model fields and schemas
|
|
5
|
+
class SampleCsvService::FieldAnalyzer
|
|
6
|
+
attr_reader :field_list
|
|
7
|
+
|
|
8
|
+
def initialize(mappings)
|
|
9
|
+
@mappings = mappings
|
|
10
|
+
@field_list = []
|
|
11
|
+
@schema = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def find_or_create_field_list_for(model_name:)
|
|
15
|
+
existing = @field_list.find { |entry| entry.key?(model_name) }
|
|
16
|
+
return existing if existing.present?
|
|
17
|
+
|
|
18
|
+
klass = SampleCsvService::ModelLoader.determine_klass_for(model_name)
|
|
19
|
+
return {} if klass.nil?
|
|
20
|
+
|
|
21
|
+
model_entry = build_field_list_entry(model_name, klass)
|
|
22
|
+
@field_list << model_entry
|
|
23
|
+
model_entry
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def controlled_vocab_terms
|
|
27
|
+
@field_list.flat_map do |hash|
|
|
28
|
+
hash.values.flat_map { |data| data["controlled_vocab_terms"] || [] }
|
|
29
|
+
end.uniq
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def build_field_list_entry(model_name, klass)
|
|
35
|
+
schema_analyzer = SampleCsvService::SchemaAnalyzer.new(klass)
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
model_name => {
|
|
39
|
+
'properties' => extract_properties(klass),
|
|
40
|
+
'required_terms' => schema_analyzer.required_terms,
|
|
41
|
+
'controlled_vocab_terms' => schema_analyzer.controlled_vocab_terms
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def extract_properties(klass)
|
|
47
|
+
if klass.respond_to?(:schema)
|
|
48
|
+
Bulkrax::ValkyrieObjectFactory.schema_properties(klass).map(&:to_s)
|
|
49
|
+
else
|
|
50
|
+
klass.properties.keys.map(&:to_s)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Utility classes
|
|
5
|
+
class SampleCsvService::FilePathGenerator
|
|
6
|
+
def self.default_path
|
|
7
|
+
path = Rails.root.join('tmp', 'imports', "bulkrax_template_#{timestamp}.csv")
|
|
8
|
+
FileUtils.mkdir_p(path.dirname.to_s)
|
|
9
|
+
path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.timestamp
|
|
13
|
+
Time.current.utc.strftime('%Y%m%d_%H%M%S')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Handles loading and filtering of Bulkrax field mappings
|
|
5
|
+
class SampleCsvService::MappingManager
|
|
6
|
+
attr_reader :mappings
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@mappings = load_mappings
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def mapped_to_key(column_str)
|
|
13
|
+
@mappings.find { |_k, v| v["from"].include?(column_str) }&.first || column_str
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def key_to_mapped_column(key)
|
|
17
|
+
@mappings.dig(key, "from")&.first || key
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def find_by_flag(field_name, default)
|
|
21
|
+
@mappings.find { |_k, v| v[field_name] == true }&.first || default
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def split_value_for(mapping_key)
|
|
25
|
+
@mappings.dig(mapping_key, "split")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def load_mappings
|
|
31
|
+
Bulkrax.field_mappings["Bulkrax::CsvParser"].reject do |_key, value|
|
|
32
|
+
value["generated"] == true
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Handles model loading based on configuration
|
|
5
|
+
class SampleCsvService::ModelLoader
|
|
6
|
+
attr_reader :models
|
|
7
|
+
|
|
8
|
+
def initialize(model_name)
|
|
9
|
+
@models = load_models(model_name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.determine_klass_for(model_name)
|
|
13
|
+
if Bulkrax.config.object_factory == Bulkrax::ValkyrieObjectFactory
|
|
14
|
+
Valkyrie.config.resource_class_resolver.call(model_name)
|
|
15
|
+
else
|
|
16
|
+
model_name.constantize
|
|
17
|
+
end
|
|
18
|
+
rescue StandardError
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def load_models(model_name)
|
|
25
|
+
case model_name
|
|
26
|
+
when nil then []
|
|
27
|
+
when 'all' then all_available_models
|
|
28
|
+
else
|
|
29
|
+
model_name.constantize ? [model_name] : []
|
|
30
|
+
end
|
|
31
|
+
rescue StandardError
|
|
32
|
+
[]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def all_available_models
|
|
36
|
+
Hyrax.config.curation_concerns.map(&:name) +
|
|
37
|
+
[Bulkrax.collection_model_class&.name, Bulkrax.file_model_class&.name].compact
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Builds CSV rows (explanations and model data)
|
|
5
|
+
class SampleCsvService::RowBuilder
|
|
6
|
+
def initialize(service)
|
|
7
|
+
@service = service
|
|
8
|
+
@explanation_builder = SampleCsvService::ExplanationBuilder.new(service)
|
|
9
|
+
@value_determiner = SampleCsvService::ValueDeterminer.new(service)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build_explanation_row(header_row)
|
|
13
|
+
@explanation_builder.build_explanations(header_row).map { |prop| prop.values.join(" ") }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_model_rows(header_row)
|
|
17
|
+
@service.all_models.map { |m| model_breakdown(m, header_row) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def model_breakdown(model_name, header_row)
|
|
23
|
+
klass = SampleCsvService::ModelLoader.determine_klass_for(model_name)
|
|
24
|
+
return [] if klass.nil?
|
|
25
|
+
|
|
26
|
+
field_list = @service.field_analyzer.find_or_create_field_list_for(model_name: model_name)
|
|
27
|
+
|
|
28
|
+
header_row.map do |column|
|
|
29
|
+
@value_determiner.determine_value(column, model_name, field_list)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Analyzes model schemas for required and controlled vocabulary fields
|
|
5
|
+
class SampleCsvService::SchemaAnalyzer
|
|
6
|
+
def initialize(klass)
|
|
7
|
+
@klass = klass
|
|
8
|
+
@schema = load_schema
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def required_terms
|
|
12
|
+
return [] if @schema.blank?
|
|
13
|
+
|
|
14
|
+
@schema.select do |field|
|
|
15
|
+
field.respond_to?(:meta) &&
|
|
16
|
+
field.meta["form"].is_a?(Hash) &&
|
|
17
|
+
field.meta["form"]["required"] == true
|
|
18
|
+
end.map(&:name).map(&:to_s)
|
|
19
|
+
rescue StandardError
|
|
20
|
+
[]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def controlled_vocab_terms
|
|
24
|
+
return [] unless @schema
|
|
25
|
+
|
|
26
|
+
controlled_properties = extract_controlled_properties
|
|
27
|
+
controlled_properties.empty? ? registered_controlled_vocab_fields : controlled_properties
|
|
28
|
+
rescue StandardError
|
|
29
|
+
[]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def load_schema
|
|
35
|
+
return nil unless @klass.respond_to?(:schema)
|
|
36
|
+
# Yes, this looks strange. The fallback is intentional.
|
|
37
|
+
# At the point in time when this service is being created, Hyrax behaves
|
|
38
|
+
# differently between flexible metadata setting on & off. This may be modified
|
|
39
|
+
# in the future, and this code can be revisited then.
|
|
40
|
+
# flexible=true: @klass.new.singleton_class.schema would return the full schema,
|
|
41
|
+
# but @klass.schema doesn't get the flexible metadata terms
|
|
42
|
+
# flexible=false: @klass.new.singleton_class.schema returns nil so it will fallback
|
|
43
|
+
@klass.new.singleton_class.schema || @klass.schema
|
|
44
|
+
rescue StandardError
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def extract_controlled_properties
|
|
49
|
+
return [] unless @schema
|
|
50
|
+
|
|
51
|
+
@schema.filter_map do |property|
|
|
52
|
+
next unless property.respond_to?(:meta)
|
|
53
|
+
sources = property.meta&.dig('controlled_values', 'sources')
|
|
54
|
+
next if sources.nil? || sources == ['null'] || sources == 'null'
|
|
55
|
+
property.name.to_s
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def registered_controlled_vocab_fields
|
|
60
|
+
qa_registry.filter_map do |k, v|
|
|
61
|
+
k.singularize if v.klass == Qa::Authorities::Local::FileBasedAuthority
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def qa_registry
|
|
66
|
+
@qa_registry ||= Qa::Authorities::Local.registry.instance_variable_get('@hash')
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Formats split pattern descriptions
|
|
5
|
+
class SampleCsvService::SplitFormatter
|
|
6
|
+
def format(split_value)
|
|
7
|
+
return "Property does not split." if split_value.nil?
|
|
8
|
+
|
|
9
|
+
if split_value == true
|
|
10
|
+
parse_pattern(Bulkrax.multi_value_element_split_on.source)
|
|
11
|
+
elsif split_value.is_a?(String)
|
|
12
|
+
parse_pattern(split_value)
|
|
13
|
+
else
|
|
14
|
+
split_value
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def parse_pattern(pattern)
|
|
21
|
+
chars = extract_characters(pattern)
|
|
22
|
+
format_message(chars)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def extract_characters(pattern)
|
|
26
|
+
if (match = pattern.match(/\[([^\]]+)\]/))
|
|
27
|
+
match[1]
|
|
28
|
+
elsif (single = pattern.match(/\\(.)/))
|
|
29
|
+
single[1]
|
|
30
|
+
else
|
|
31
|
+
pattern
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def format_message(chars)
|
|
36
|
+
formatted = chars.chars.then do |c|
|
|
37
|
+
c.length > 1 ? "#{c[0..-2].join(' ')}, or #{c.last}" : c.first
|
|
38
|
+
end
|
|
39
|
+
"Split multiple values with #{formatted}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# Determines values for CSV cells
|
|
5
|
+
class SampleCsvService::ValueDeterminer
|
|
6
|
+
def initialize(service)
|
|
7
|
+
@service = service
|
|
8
|
+
@column_builder = SampleCsvService::ColumnBuilder.new(service)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def determine_value(column, model_name, field_list)
|
|
12
|
+
key = @service.mapping_manager.mapped_to_key(column)
|
|
13
|
+
required_terms = field_list.dig(model_name, 'required_terms')
|
|
14
|
+
|
|
15
|
+
if field_list.dig(model_name, "properties")&.include?(key)
|
|
16
|
+
mark_required_or_optional(key, required_terms)
|
|
17
|
+
elsif special_column?(column, key)
|
|
18
|
+
special_value(column, key, model_name, required_terms)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def special_column?(column, key)
|
|
25
|
+
descriptor = SampleCsvService::ColumnDescriptor.new
|
|
26
|
+
visibility_cols = descriptor.send(:extract_column_names, :visibility)
|
|
27
|
+
|
|
28
|
+
key.in?(['model', 'work_type']) ||
|
|
29
|
+
column.in?(visibility_cols) ||
|
|
30
|
+
column == 'source_identifier' ||
|
|
31
|
+
column == 'rights_statement' ||
|
|
32
|
+
relationship_column?(column) ||
|
|
33
|
+
file_column?(column)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def special_value(column, key, model_name, required_terms)
|
|
37
|
+
return SampleCsvService::ModelLoader.determine_klass_for(model_name).to_s if key.in?(['model', 'work_type'])
|
|
38
|
+
return 'Required' if column == 'source_identifier'
|
|
39
|
+
return mark_required_or_optional(key, required_terms) if column == 'rights_statement'
|
|
40
|
+
# collections do not have files
|
|
41
|
+
return nil if file_column?(column) && model_name.in?([Bulkrax.collection_model_class].compact.map(&:to_s))
|
|
42
|
+
'Optional'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def mark_required_or_optional(field, required_terms)
|
|
46
|
+
return 'Unknown' unless required_terms
|
|
47
|
+
required_terms.include?(field) ? 'Required' : 'Optional'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def relationship_column?(column)
|
|
51
|
+
relationships = [
|
|
52
|
+
@service.mapping_manager.find_by_flag("related_children_field_mapping", 'children'),
|
|
53
|
+
@service.mapping_manager.find_by_flag("related_parents_field_mapping", 'parents')
|
|
54
|
+
]
|
|
55
|
+
column.in?(relationships)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def file_column?(column)
|
|
59
|
+
file_cols = SampleCsvService::ColumnDescriptor::COLUMN_DESCRIPTIONS[:files].flat_map do |property_hash|
|
|
60
|
+
property_hash.keys.filter_map do |key|
|
|
61
|
+
@service.mappings.dig(key, "from")&.first
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
column.in?(file_cols)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|