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,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
class GuidedImportsController < ::Bulkrax::ApplicationController
|
|
5
|
+
include Hyrax::ThemedLayoutController if defined?(::Hyrax)
|
|
6
|
+
include Bulkrax::GuidedImportDemoScenarios if Bulkrax.config.guided_import_demo_scenarios_enabled
|
|
7
|
+
include Bulkrax::ImporterFileHandler
|
|
8
|
+
helper Bulkrax::ImportersHelper
|
|
9
|
+
|
|
10
|
+
before_action :authenticate_user!
|
|
11
|
+
before_action :check_permissions
|
|
12
|
+
with_themed_layout 'dashboard' if defined?(::Hyrax)
|
|
13
|
+
|
|
14
|
+
# trigger form to allow upload
|
|
15
|
+
def new
|
|
16
|
+
@importer = Importer.new
|
|
17
|
+
return unless defined?(::Hyrax)
|
|
18
|
+
add_importer_breadcrumbs
|
|
19
|
+
add_breadcrumb I18n.t('bulkrax.importer.guided_import.breadcrumb')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# AJAX endpoint to validate uploaded files
|
|
23
|
+
def validate
|
|
24
|
+
set_locale_from_params
|
|
25
|
+
|
|
26
|
+
files, error = resolve_validation_files
|
|
27
|
+
return render json: error, status: :ok if error
|
|
28
|
+
return render json: StepperResponseFormatter.error(message: I18n.t('bulkrax.importer.guided_import.validation.no_files_uploaded')), status: :ok unless files.any?
|
|
29
|
+
|
|
30
|
+
csv_file, zip_file = select_csv_and_zip(files)
|
|
31
|
+
|
|
32
|
+
unless csv_file
|
|
33
|
+
return render json: StepperResponseFormatter.error(message: I18n.t('bulkrax.importer.guided_import.validation.no_csv_uploaded')), status: :ok unless zip_file
|
|
34
|
+
|
|
35
|
+
csv_file, error = extract_csv_from_zip(zip_file)
|
|
36
|
+
return render json: error, status: :ok if error
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
admin_set_id = params[:importer]&.[](:admin_set_id)
|
|
40
|
+
validation_result = run_validation(csv_file, zip_file, admin_set_id: admin_set_id)
|
|
41
|
+
raw_csv_data = validation_result.delete(:raw_csv_data)
|
|
42
|
+
cache_key = cache_validation_errors(validation_result, raw_csv_data, csv_file)
|
|
43
|
+
formatted = StepperResponseFormatter.format(validation_result)
|
|
44
|
+
formatted[:validationErrorsCacheKey] = cache_key
|
|
45
|
+
render json: formatted, status: :ok
|
|
46
|
+
ensure
|
|
47
|
+
close_file_handles(files)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def download_validation_errors
|
|
51
|
+
cache_key = params[:key].to_s
|
|
52
|
+
expected_prefix = "guided_import_errors:#{session.id}:"
|
|
53
|
+
return head :not_found unless cache_key.start_with?(expected_prefix)
|
|
54
|
+
|
|
55
|
+
cached = Rails.cache.read(cache_key)
|
|
56
|
+
return head :not_found unless cached
|
|
57
|
+
|
|
58
|
+
csv = ValidationErrorCsvBuilder.build(
|
|
59
|
+
headers: cached[:headers],
|
|
60
|
+
csv_data: cached[:csv_data],
|
|
61
|
+
row_errors: cached[:row_errors],
|
|
62
|
+
file_errors: cached[:file_errors]
|
|
63
|
+
)
|
|
64
|
+
send_data csv, filename: error_csv_filename(cached[:original_filename]), type: 'text/csv', disposition: 'attachment'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create
|
|
68
|
+
files = nil
|
|
69
|
+
files = resolve_create_files
|
|
70
|
+
return render_invalid_uploaded_files_response if params[:uploaded_files].present? && files.empty?
|
|
71
|
+
|
|
72
|
+
@importer = Importer.new(importer_params)
|
|
73
|
+
@importer.parser_klass = 'Bulkrax::CsvParser'
|
|
74
|
+
@importer.user = current_user if respond_to?(:current_user) && current_user.present?
|
|
75
|
+
apply_field_mapping
|
|
76
|
+
|
|
77
|
+
if @importer.save
|
|
78
|
+
write_files(files)
|
|
79
|
+
Bulkrax::ImporterJob.perform_later(@importer.id)
|
|
80
|
+
|
|
81
|
+
respond_to do |format|
|
|
82
|
+
format.html { redirect_to bulkrax.importers_path, notice: I18n.t('bulkrax.importer.guided_import.flash.import_started') }
|
|
83
|
+
format.json { render json: { success: true, importer_id: @importer.id }, status: :created }
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
respond_to do |format|
|
|
87
|
+
format.html { render :new, status: :unprocessable_entity }
|
|
88
|
+
format.json { render json: { errors: @importer.errors.full_messages }, status: :unprocessable_entity }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
ensure
|
|
92
|
+
close_file_handles(files)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def render_invalid_uploaded_files_response
|
|
98
|
+
respond_to do |format|
|
|
99
|
+
format.html { render :new, status: :unprocessable_entity }
|
|
100
|
+
format.json { render json: { errors: ['No valid uploaded files found'] }, status: :unprocessable_entity }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Runs validation via the real service.
|
|
105
|
+
# @param csv_file [File, StringIO] the CSV to validate
|
|
106
|
+
# @param zip_file [File, nil] an optional ZIP containing file attachments
|
|
107
|
+
# @param admin_set_id [String, nil] optional admin set ID for validation context
|
|
108
|
+
# @return [Hash] validation result data
|
|
109
|
+
def cache_validation_errors(validation_result, raw_csv_data, csv_file)
|
|
110
|
+
has_errors = validation_result[:rowErrors]&.any? ||
|
|
111
|
+
validation_result[:missingRequired]&.any? ||
|
|
112
|
+
validation_result[:unrecognized]&.any? ||
|
|
113
|
+
validation_result[:emptyColumns]&.any? ||
|
|
114
|
+
validation_result[:missingFiles]&.any?
|
|
115
|
+
return nil unless has_errors
|
|
116
|
+
|
|
117
|
+
key = "guided_import_errors:#{session.id}:#{Time.now.to_i}"
|
|
118
|
+
Rails.cache.write(
|
|
119
|
+
key,
|
|
120
|
+
{
|
|
121
|
+
headers: validation_result[:headers],
|
|
122
|
+
csv_data: raw_csv_data,
|
|
123
|
+
row_errors: validation_result[:rowErrors] || [],
|
|
124
|
+
file_errors: {
|
|
125
|
+
missing_required: validation_result[:missingRequired] || [],
|
|
126
|
+
unrecognized: validation_result[:unrecognized] || {},
|
|
127
|
+
empty_columns: validation_result[:emptyColumns] || [],
|
|
128
|
+
missing_files: validation_result[:missingFiles] || []
|
|
129
|
+
},
|
|
130
|
+
original_filename: filename_for(csv_file)
|
|
131
|
+
},
|
|
132
|
+
expires_in: 1.hour
|
|
133
|
+
)
|
|
134
|
+
key
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def run_validation(csv_file, zip_file, admin_set_id: nil)
|
|
138
|
+
CsvParser.validate_csv(csv_file: csv_file, zip_file: zip_file, admin_set_id: admin_set_id)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def importer_params
|
|
142
|
+
params.require(:importer).permit(
|
|
143
|
+
:name,
|
|
144
|
+
:admin_set_id,
|
|
145
|
+
:limit,
|
|
146
|
+
parser_fields: [:visibility, :rights_statement, :override_rights_statement, :import_file_path, :file_style]
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def apply_field_mapping
|
|
151
|
+
@importer.field_mapping = Bulkrax.field_mappings['Bulkrax::CsvParser']
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def error_csv_filename(original_filename)
|
|
155
|
+
return 'import_errors.csv' if original_filename.blank?
|
|
156
|
+
|
|
157
|
+
base = File.basename(original_filename, '.*')
|
|
158
|
+
"#{base}_errors.csv"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def set_locale_from_params
|
|
162
|
+
I18n.locale = params[:locale] if params[:locale].present? && I18n.available_locales.include?(params[:locale].to_sym)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def add_importer_breadcrumbs
|
|
166
|
+
add_breadcrumb t(:'hyrax.controls.home'), main_app.root_path
|
|
167
|
+
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
|
|
168
|
+
add_breadcrumb 'Importers', bulkrax.importers_path
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def check_permissions
|
|
172
|
+
raise CanCan::AccessDenied unless current_ability.can_import_works?
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -8,6 +8,7 @@ module Bulkrax
|
|
|
8
8
|
include Bulkrax::API
|
|
9
9
|
include Bulkrax::DatatablesBehavior
|
|
10
10
|
include Bulkrax::ValidationHelper
|
|
11
|
+
include Bulkrax::ImporterFileHandler
|
|
11
12
|
|
|
12
13
|
protect_from_forgery unless: -> { api_request? }
|
|
13
14
|
before_action :token_authenticate!, if: -> { api_request? }, only: [:create, :update, :delete]
|
|
@@ -28,7 +29,8 @@ module Bulkrax
|
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
def importer_table
|
|
31
|
-
|
|
32
|
+
order = table_order.presence || Arel.sql('last_imported_at DESC NULLS LAST')
|
|
33
|
+
@importers = Importer.order(order).page(table_page).per(table_per_page)
|
|
32
34
|
@importers = @importers.where(importer_table_search) if importer_table_search.present?
|
|
33
35
|
respond_to do |format|
|
|
34
36
|
format.json { render json: format_importers(@importers) }
|
|
@@ -65,6 +67,16 @@ module Bulkrax
|
|
|
65
67
|
end
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
# GET /importers/sample_csv_file
|
|
71
|
+
def sample_csv_file
|
|
72
|
+
admin_set_id = params[:admin_set_id].presence
|
|
73
|
+
sample = Bulkrax::CsvParser.generate_template(models: 'all', output: 'file', admin_set_id: admin_set_id)
|
|
74
|
+
send_file sample, filename: File.basename(sample), type: 'text/csv', disposition: 'attachment'
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
flash[:error] = "Unable to generate sample CSV file: #{e.message}"
|
|
77
|
+
redirect_back fallback_location: bulkrax.importers_path
|
|
78
|
+
end
|
|
79
|
+
|
|
68
80
|
# GET /importers/1/edit
|
|
69
81
|
def edit
|
|
70
82
|
if api_request?
|
|
@@ -84,7 +96,7 @@ module Bulkrax
|
|
|
84
96
|
if api_request?
|
|
85
97
|
return return_json_response unless valid_create_params?
|
|
86
98
|
end
|
|
87
|
-
uploads =
|
|
99
|
+
uploads = uploaded_files_scope
|
|
88
100
|
file = file_param
|
|
89
101
|
cloud_files = cloud_params
|
|
90
102
|
|
|
@@ -123,7 +135,7 @@ module Bulkrax
|
|
|
123
135
|
if api_request?
|
|
124
136
|
return return_json_response unless valid_update_params?
|
|
125
137
|
end
|
|
126
|
-
uploads =
|
|
138
|
+
uploads = uploaded_files_scope
|
|
127
139
|
file = file_param
|
|
128
140
|
cloud_files = cloud_params
|
|
129
141
|
|
|
@@ -205,10 +217,26 @@ module Bulkrax
|
|
|
205
217
|
end
|
|
206
218
|
|
|
207
219
|
def original_file
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
220
|
+
file_type = params[:file_type]&.to_sym
|
|
221
|
+
|
|
222
|
+
files = @importer.original_files
|
|
223
|
+
if files.empty?
|
|
211
224
|
redirect_to @importer, alert: 'Importer does not support file re-download or the imported file is not found on the server.'
|
|
225
|
+
return
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# If file_type is specified, find that specific file
|
|
229
|
+
if file_type
|
|
230
|
+
file = files.find { |f| f[:type] == file_type }
|
|
231
|
+
if file
|
|
232
|
+
send_file file[:path], filename: file[:name], disposition: 'attachment'
|
|
233
|
+
else
|
|
234
|
+
redirect_to @importer, alert: "File type '#{file_type}' not found."
|
|
235
|
+
end
|
|
236
|
+
else
|
|
237
|
+
# Default behavior: send the first file (CSV) for backward compatibility
|
|
238
|
+
file = files.first
|
|
239
|
+
send_file file[:path], filename: file[:name], disposition: 'attachment'
|
|
212
240
|
end
|
|
213
241
|
end
|
|
214
242
|
|
|
@@ -221,28 +249,6 @@ module Bulkrax
|
|
|
221
249
|
|
|
222
250
|
private
|
|
223
251
|
|
|
224
|
-
def files_for_import(file, cloud_files, uploads)
|
|
225
|
-
return if file.blank? && cloud_files.blank? && uploads.blank?
|
|
226
|
-
|
|
227
|
-
@importer[:parser_fields]['import_file_path'] = @importer.parser.write_import_file(file) if file.present?
|
|
228
|
-
if cloud_files.present?
|
|
229
|
-
@importer[:parser_fields]['cloud_file_paths'] = cloud_files
|
|
230
|
-
# For BagIt, there will only be one bag, so we get the file_path back and set import_file_path
|
|
231
|
-
# For CSV, we expect only file uploads, so we won't get the file_path back
|
|
232
|
-
# and we expect the import_file_path to be set already
|
|
233
|
-
target = @importer.parser.retrieve_cloud_files(cloud_files, @importer)
|
|
234
|
-
@importer[:parser_fields]['import_file_path'] = target if target.present?
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
if uploads.present?
|
|
238
|
-
uploads.each do |upload|
|
|
239
|
-
@importer[:parser_fields]['import_file_path'] = @importer.parser.write_import_file(upload.file.file)
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
@importer.save
|
|
244
|
-
end
|
|
245
|
-
|
|
246
252
|
# Use callbacks to share common setup or constraints between actions.
|
|
247
253
|
def set_importer
|
|
248
254
|
@importer = Importer.find(params[:id] || params[:importer_id])
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bulkrax
|
|
4
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
|
+
module GuidedImportDemoScenarios
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
# Serve demo scenario fixtures for frontend testing
|
|
9
|
+
def demo_scenarios
|
|
10
|
+
file_path = Bulkrax::Engine.root.join('lib', 'bulkrax', 'data', 'demo_scenarios.json')
|
|
11
|
+
if File.exist?(file_path)
|
|
12
|
+
render json: File.read(file_path), status: :ok
|
|
13
|
+
else
|
|
14
|
+
render json: { error: I18n.t('bulkrax.importer.guided_import.flash.demo_not_available') }, status: :not_found
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def run_validation(csv_file, zip_file, admin_set_id: nil)
|
|
21
|
+
if ENV['DEMO_MODE'] == 'true'
|
|
22
|
+
generate_validation_response(csv_file, zip_file)
|
|
23
|
+
else
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# rubocop:disable Metrics/MethodLength
|
|
29
|
+
# Hardcoded mock response generator for demo mode
|
|
30
|
+
def generate_validation_response(_csv_file, zip_file)
|
|
31
|
+
# Generate mock collections
|
|
32
|
+
collections = [
|
|
33
|
+
{ id: 'col-1', title: 'Historical Photographs Collection', type: 'collection', parentIds: [], childrenIds: ['work-shared-1'] },
|
|
34
|
+
{ id: 'col-2', title: 'Manuscripts & Letters', type: 'collection', parentIds: [], childrenIds: [] },
|
|
35
|
+
{ id: 'col-3', title: 'Audio Recordings', type: 'collection', parentIds: [], childrenIds: ['work-shared-2'] }
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Generate mock works
|
|
39
|
+
works = []
|
|
40
|
+
189.times do |i|
|
|
41
|
+
parent_ids = if i < 75
|
|
42
|
+
['col-1']
|
|
43
|
+
elsif i < 140
|
|
44
|
+
['col-2']
|
|
45
|
+
elsif i < 189
|
|
46
|
+
['col-3']
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
works << {
|
|
50
|
+
id: "work-#{i + 1}",
|
|
51
|
+
title: "Work #{i + 1}",
|
|
52
|
+
type: 'work',
|
|
53
|
+
parentIds: parent_ids
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Multi-parent examples
|
|
58
|
+
works << { id: 'work-shared-1', title: 'Cross-Collection Photograph', type: 'work', parentIds: ['col-1', 'col-2'] }
|
|
59
|
+
works << { id: 'work-shared-2', title: 'Interdisciplinary Recording', type: 'work', parentIds: ['col-2', 'col-3'] }
|
|
60
|
+
|
|
61
|
+
# Generate mock file sets
|
|
62
|
+
file_sets = []
|
|
63
|
+
55.times do |i|
|
|
64
|
+
file_sets << {
|
|
65
|
+
id: "fs-#{i + 1}",
|
|
66
|
+
title: "FileSet #{i + 1}",
|
|
67
|
+
type: 'file_set'
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Mock headers with one unrecognized field
|
|
72
|
+
headers = ['source_identifier', 'title', 'creator', 'model', 'parents', 'children', 'file', 'description', 'date_created', 'legacy_id', 'subject']
|
|
73
|
+
unrecognized = ['legacy_id']
|
|
74
|
+
missing_required = []
|
|
75
|
+
missing_files = ['photo_087.tiff', 'letter_scan_12.pdf', 'recording_03.wav']
|
|
76
|
+
zip_included = zip_file.present?
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
headers: headers,
|
|
80
|
+
missingRequired: missing_required,
|
|
81
|
+
unrecognized: unrecognized,
|
|
82
|
+
rowCount: 247,
|
|
83
|
+
isValid: true,
|
|
84
|
+
hasWarnings: true,
|
|
85
|
+
collections: collections,
|
|
86
|
+
works: works,
|
|
87
|
+
fileSets: file_sets,
|
|
88
|
+
totalItems: collections.length + works.length + file_sets.length,
|
|
89
|
+
fileReferences: 55,
|
|
90
|
+
missingFiles: missing_files,
|
|
91
|
+
foundFiles: 52,
|
|
92
|
+
zipIncluded: zip_included,
|
|
93
|
+
messages: build_validation_messages(
|
|
94
|
+
headers: headers, unrecognized: unrecognized, missing_required: missing_required,
|
|
95
|
+
missing_files: missing_files, zip_included: zip_included, row_count: 247,
|
|
96
|
+
is_valid: true, has_warnings: true, file_references: 55
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
# rubocop:enable Metrics/MethodLength
|
|
101
|
+
|
|
102
|
+
# Builds the structured messages hash from validation results.
|
|
103
|
+
# @param results [Hash] with keys: headers, unrecognized, missing_required,
|
|
104
|
+
# missing_files, zip_included, row_count, is_valid, has_warnings, file_references
|
|
105
|
+
def build_validation_messages(results)
|
|
106
|
+
issues = []
|
|
107
|
+
issues << missing_required_issue(results[:missing_required]) if results[:missing_required]&.any?
|
|
108
|
+
issues << unrecognized_fields_issue(results[:unrecognized]) if results[:unrecognized]&.any?
|
|
109
|
+
issues << file_references_issue(results) if results[:file_references]&.positive?
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
validationStatus: validation_status(results),
|
|
113
|
+
issues: issues.compact
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def validation_status(results)
|
|
118
|
+
severity, icon, title = validation_status_level(results[:is_valid], results[:has_warnings])
|
|
119
|
+
recognized = results[:headers] - (results[:unrecognized] || [])
|
|
120
|
+
|
|
121
|
+
{
|
|
122
|
+
severity: severity,
|
|
123
|
+
icon: icon,
|
|
124
|
+
title: title,
|
|
125
|
+
summary: I18n.t('bulkrax.importer.guided_import.validation.columns_detected', columns: results[:headers].length, records: results[:row_count]),
|
|
126
|
+
details: results[:is_valid] ? I18n.t('bulkrax.importer.guided_import.validation.recognized_fields', fields: recognized.join(', ')) : I18n.t('bulkrax.importer.guided_import.validation.critical_errors'),
|
|
127
|
+
defaultOpen: true
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def validation_status_level(is_valid, has_warnings)
|
|
132
|
+
if !is_valid
|
|
133
|
+
['error', 'fa-times-circle', I18n.t('bulkrax.importer.guided_import.validation.failed')]
|
|
134
|
+
elsif has_warnings
|
|
135
|
+
['warning', 'fa-exclamation-triangle', I18n.t('bulkrax.importer.guided_import.validation.passed_warnings')]
|
|
136
|
+
else
|
|
137
|
+
['success', 'fa-check-circle', I18n.t('bulkrax.importer.guided_import.validation.passed')]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def missing_required_issue(missing_required)
|
|
142
|
+
{
|
|
143
|
+
type: 'missing_required_fields',
|
|
144
|
+
severity: 'error',
|
|
145
|
+
icon: 'fa-times-circle',
|
|
146
|
+
title: I18n.t('bulkrax.importer.guided_import.validation.missing_required_title'),
|
|
147
|
+
count: missing_required.length,
|
|
148
|
+
description: I18n.t('bulkrax.importer.guided_import.validation.missing_required_desc'),
|
|
149
|
+
items: missing_required.map { |field| { field: field, message: I18n.t('bulkrax.importer.guided_import.validation.missing_required_hint') } },
|
|
150
|
+
defaultOpen: false
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def unrecognized_fields_issue(unrecognized)
|
|
155
|
+
{
|
|
156
|
+
type: 'unrecognized_fields',
|
|
157
|
+
severity: 'warning',
|
|
158
|
+
icon: 'fa-exclamation-triangle',
|
|
159
|
+
title: I18n.t('bulkrax.importer.guided_import.validation.unrecognized_title'),
|
|
160
|
+
count: unrecognized.length,
|
|
161
|
+
description: I18n.t('bulkrax.importer.guided_import.validation.unrecognized_desc'),
|
|
162
|
+
items: unrecognized.map { |field| { field: field, message: nil } },
|
|
163
|
+
defaultOpen: false
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# rubocop:disable Metrics/MethodLength
|
|
168
|
+
def file_references_issue(results)
|
|
169
|
+
file_references = results[:file_references]
|
|
170
|
+
missing_files = results[:missing_files] || []
|
|
171
|
+
found_files = file_references - missing_files.length
|
|
172
|
+
|
|
173
|
+
if missing_files.any? && results[:zip_included]
|
|
174
|
+
{
|
|
175
|
+
type: 'file_references',
|
|
176
|
+
severity: 'warning',
|
|
177
|
+
icon: 'fa-info-circle',
|
|
178
|
+
title: I18n.t('bulkrax.importer.guided_import.validation.file_references_title'),
|
|
179
|
+
count: file_references,
|
|
180
|
+
summary: I18n.t('bulkrax.importer.guided_import.validation.files_found_in_zip', found: found_files, total: file_references),
|
|
181
|
+
description: I18n.t('bulkrax.importer.guided_import.validation.files_missing_from_zip', count: missing_files.length, files_word: 'file'.pluralize(missing_files.length)),
|
|
182
|
+
items: missing_files.map { |file| { field: file, message: I18n.t('bulkrax.importer.guided_import.validation.missing_from_zip') } },
|
|
183
|
+
defaultOpen: false
|
|
184
|
+
}
|
|
185
|
+
elsif !results[:zip_included]
|
|
186
|
+
{
|
|
187
|
+
type: 'file_references',
|
|
188
|
+
severity: 'warning',
|
|
189
|
+
icon: 'fa-exclamation-triangle',
|
|
190
|
+
title: I18n.t('bulkrax.importer.guided_import.validation.file_references_title'),
|
|
191
|
+
count: file_references,
|
|
192
|
+
summary: I18n.t('bulkrax.importer.guided_import.validation.files_referenced', count: file_references),
|
|
193
|
+
description: I18n.t('bulkrax.importer.guided_import.validation.no_zip_desc'),
|
|
194
|
+
items: [],
|
|
195
|
+
defaultOpen: false
|
|
196
|
+
}
|
|
197
|
+
end
|
|
198
|
+
end # rubocop:enable Metrics/MethodLength
|
|
199
|
+
end
|
|
200
|
+
# rubocop:enable Metrics/ModuleLength
|
|
201
|
+
end
|