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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/bulkrax/import_export.scss +9 -2
  3. data/app/controllers/bulkrax/importers_controller.rb +9 -0
  4. data/app/jobs/bulkrax/export_work_job.rb +1 -3
  5. data/app/services/bulkrax/sample_csv_service/column_builder.rb +58 -0
  6. data/app/services/bulkrax/sample_csv_service/column_descriptor.rb +56 -0
  7. data/app/services/bulkrax/sample_csv_service/csv_builder.rb +82 -0
  8. data/app/services/bulkrax/sample_csv_service/explanation_builder.rb +51 -0
  9. data/app/services/bulkrax/sample_csv_service/field_analyzer.rb +54 -0
  10. data/app/services/bulkrax/sample_csv_service/file_path_generator.rb +16 -0
  11. data/app/services/bulkrax/sample_csv_service/mapping_manager.rb +36 -0
  12. data/app/services/bulkrax/sample_csv_service/model_loader.rb +40 -0
  13. data/app/services/bulkrax/sample_csv_service/row_builder.rb +33 -0
  14. data/app/services/bulkrax/sample_csv_service/schema_analyzer.rb +69 -0
  15. data/app/services/bulkrax/sample_csv_service/split_formatter.rb +42 -0
  16. data/app/services/bulkrax/sample_csv_service/value_determiner.rb +67 -0
  17. data/app/services/bulkrax/sample_csv_service.rb +78 -0
  18. data/app/views/bulkrax/entries/_parsed_metadata.html.erb +1 -1
  19. data/app/views/bulkrax/entries/_raw_metadata.html.erb +1 -1
  20. data/app/views/bulkrax/entries/show.html.erb +6 -6
  21. data/app/views/bulkrax/exporters/_form.html.erb +19 -43
  22. data/app/views/bulkrax/exporters/edit.html.erb +2 -2
  23. data/app/views/bulkrax/exporters/index.html.erb +5 -5
  24. data/app/views/bulkrax/exporters/new.html.erb +3 -5
  25. data/app/views/bulkrax/exporters/show.html.erb +3 -3
  26. data/app/views/bulkrax/importers/_bagit_fields.html.erb +9 -9
  27. data/app/views/bulkrax/importers/_browse_everything.html.erb +1 -1
  28. data/app/views/bulkrax/importers/_csv_fields.html.erb +11 -11
  29. data/app/views/bulkrax/importers/_edit_form_buttons.html.erb +23 -23
  30. data/app/views/bulkrax/importers/_edit_item_buttons.html.erb +2 -2
  31. data/app/views/bulkrax/importers/_file_uploader.html.erb +3 -3
  32. data/app/views/bulkrax/importers/_form.html.erb +4 -5
  33. data/app/views/bulkrax/importers/_oai_fields.html.erb +8 -18
  34. data/app/views/bulkrax/importers/_xml_fields.html.erb +13 -13
  35. data/app/views/bulkrax/importers/edit.html.erb +2 -2
  36. data/app/views/bulkrax/importers/index.html.erb +13 -13
  37. data/app/views/bulkrax/importers/new.html.erb +10 -9
  38. data/app/views/bulkrax/importers/show.html.erb +7 -7
  39. data/app/views/bulkrax/importers/upload_corrected_entries.html.erb +6 -6
  40. data/app/views/bulkrax/shared/_bulkrax_errors.html.erb +11 -11
  41. data/app/views/bulkrax/shared/_bulkrax_field_mapping.html.erb +3 -3
  42. data/config/locales/bulkrax.en.yml +235 -2
  43. data/config/routes.rb +1 -0
  44. data/lib/bulkrax/version.rb +1 -1
  45. data/lib/bulkrax.rb +0 -3
  46. data/lib/tasks/bulkrax_tasks.rake +0 -102
  47. metadata +15 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8fc173a9153f3d6bf93ca309fda4a194cf6148eb26dc6b1bd9d4ec8e36e92cb0
4
- data.tar.gz: d180567668e87809880fd7b86a3494ea2676c7afe7749d34d9da52b0728eed3b
3
+ metadata.gz: d7205ae1ec1e6287a3afe95e94df9e3f7afcdb72ad0d6e934cca44004b28e8a4
4
+ data.tar.gz: bae60650dc0ad54df42c9aa7da87829039462f6159c25b296a029c28f99006e8
5
5
  SHA512:
6
- metadata.gz: 0f50fc0ac8d65266be8781a01f3bffeebf06177dc9253255ff0db130c3612b3e0d6e1cb6437cbb2445416a4e49dc31e8867c73ab423e7d34ae9a4a34b8b55c1b
7
- data.tar.gz: 5b5162665af58619b3017297155ef07263bc1ccd5caf172e778f9f66eeb47545ea9cfdf84746786ef911433f3b29cc879997a67f64dd23df2f9e9a3b44658025
6
+ metadata.gz: 7b81763a20e21c294bf0fa56479cafc506fdc589f31c11d3bbd0383b28fda247734f3abe4d0797e5601abd64815aa457ea8d908bfbf427a03fa5e500bbfe60d2
7
+ data.tar.gz: e34566bafe813523345aaa41b2ad9fc2921e78be118250dfa911453fba185ea25cea6f74440fa6d5ddbb1dbcc699aca1f4ca5d3eb139c3dae65d27953de5f45d
@@ -1,6 +1,5 @@
1
1
  .bulkrax-card-footer {
2
- min-height: 60px;
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