zizia 1.0.1
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 +7 -0
- data/.gitignore +15 -0
- data/.rubocop.yml +65 -0
- data/.rubocop_todo.yml +21 -0
- data/.solr_wrapper +8 -0
- data/.travis.yml +11 -0
- data/Gemfile +12 -0
- data/README.md +77 -0
- data/Rakefile +34 -0
- data/docs/_config.yml +1 -0
- data/docs/index.md +98 -0
- data/lib/zizia/always_invalid_validator.rb +17 -0
- data/lib/zizia/hash_mapper.rb +44 -0
- data/lib/zizia/hyrax_basic_metadata_mapper.rb +149 -0
- data/lib/zizia/hyrax_record_importer.rb +261 -0
- data/lib/zizia/importer.rb +61 -0
- data/lib/zizia/input_record.rb +65 -0
- data/lib/zizia/log_stream.rb +43 -0
- data/lib/zizia/metadata_mapper.rb +83 -0
- data/lib/zizia/metadata_only_stack.rb +70 -0
- data/lib/zizia/parser.rb +132 -0
- data/lib/zizia/parsers/csv_parser.rb +45 -0
- data/lib/zizia/record_importer.rb +57 -0
- data/lib/zizia/spec/fakes/fake_parser.rb +22 -0
- data/lib/zizia/spec/shared_examples/a_mapper.rb +32 -0
- data/lib/zizia/spec/shared_examples/a_message_stream.rb +11 -0
- data/lib/zizia/spec/shared_examples/a_parser.rb +73 -0
- data/lib/zizia/spec/shared_examples/a_validator.rb +46 -0
- data/lib/zizia/spec.rb +15 -0
- data/lib/zizia/streams/formatted_message_stream.rb +70 -0
- data/lib/zizia/validator.rb +117 -0
- data/lib/zizia/validators/csv_format_validator.rb +26 -0
- data/lib/zizia/validators/title_validator.rb +30 -0
- data/lib/zizia/version.rb +5 -0
- data/lib/zizia.rb +73 -0
- data/log/.keep +0 -0
- data/spec/fixtures/bad_example.csv +2 -0
- data/spec/fixtures/example.csv +4 -0
- data/spec/fixtures/hyrax/example.csv +3 -0
- data/spec/fixtures/images/animals/cat.png +0 -0
- data/spec/fixtures/images/zizia.png +0 -0
- data/spec/fixtures/zizia.png +0 -0
- data/spec/integration/import_csv_spec.rb +28 -0
- data/spec/integration/import_hyrax_csv.rb +71 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/stdout_stream_spec.rb +9 -0
- data/spec/support/hyrax/basic_metadata.rb +30 -0
- data/spec/support/hyrax/core_metadata.rb +15 -0
- data/spec/support/shared_contexts/with_work_type.rb +101 -0
- data/spec/zizia/csv_format_validator_spec.rb +38 -0
- data/spec/zizia/csv_parser_spec.rb +73 -0
- data/spec/zizia/formatted_message_stream_spec.rb +35 -0
- data/spec/zizia/hash_mapper_spec.rb +8 -0
- data/spec/zizia/hyrax_basic_metadata_mapper_spec.rb +190 -0
- data/spec/zizia/hyrax_record_importer_spec.rb +178 -0
- data/spec/zizia/importer_spec.rb +46 -0
- data/spec/zizia/input_record_spec.rb +71 -0
- data/spec/zizia/parser_spec.rb +47 -0
- data/spec/zizia/record_importer_spec.rb +70 -0
- data/spec/zizia/title_validator_spec.rb +23 -0
- data/spec/zizia/validator_spec.rb +9 -0
- data/spec/zizia/version_spec.rb +7 -0
- data/spec/zizia_spec.rb +19 -0
- data/zizia.gemspec +34 -0
- metadata +246 -0
@@ -0,0 +1,261 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zizia
|
4
|
+
class HyraxRecordImporter < RecordImporter
|
5
|
+
# TODO: Get this from Hyrax config
|
6
|
+
DEFAULT_CREATOR_KEY = 'batchuser@example.com'
|
7
|
+
|
8
|
+
# @!attribute [rw] depositor
|
9
|
+
# @return [User]
|
10
|
+
attr_accessor :depositor
|
11
|
+
|
12
|
+
# @!attribute [rw] collection_id
|
13
|
+
# @return [String] The fedora ID for a Collection.
|
14
|
+
attr_accessor :collection_id
|
15
|
+
|
16
|
+
# @!attribute [rw] batch_id
|
17
|
+
# @return [String] an id number associated with the process that kicked off this import run
|
18
|
+
attr_accessor :batch_id
|
19
|
+
|
20
|
+
# @!attribute [rw] deduplication_field
|
21
|
+
# @return [String] if this is set, look for records with a match in this field
|
22
|
+
# and update the metadata instead of creating a new record. This will NOT re-import file attachments.
|
23
|
+
attr_accessor :deduplication_field
|
24
|
+
|
25
|
+
# @!attribute [rw] success_count
|
26
|
+
# @return [String] the number of records this importer has successfully created
|
27
|
+
attr_accessor :success_count
|
28
|
+
|
29
|
+
# @!attribute [rw] failure_count
|
30
|
+
# @return [String] the number of records this importer has failed to create
|
31
|
+
attr_accessor :failure_count
|
32
|
+
|
33
|
+
# @param attributes [Hash] Attributes that come
|
34
|
+
# from the UI or importer rather than from
|
35
|
+
# the CSV/mapper. These are useful for logging
|
36
|
+
# and tracking the output of an import job for
|
37
|
+
# a given collection, user, or batch.
|
38
|
+
# If a deduplication_field is provided, the system will
|
39
|
+
# look for existing works with that field and matching
|
40
|
+
# value and will update the record instead of creating a new record.
|
41
|
+
# @example
|
42
|
+
# attributes: { collection_id: '123',
|
43
|
+
# depositor_id: '456',
|
44
|
+
# batch_id: '789',
|
45
|
+
# deduplication_field: 'legacy_id'
|
46
|
+
# }
|
47
|
+
def initialize(error_stream: Zizia.config.default_error_stream,
|
48
|
+
info_stream: Zizia.config.default_info_stream,
|
49
|
+
attributes: {})
|
50
|
+
self.collection_id = attributes[:collection_id]
|
51
|
+
self.batch_id = attributes[:batch_id]
|
52
|
+
self.deduplication_field = attributes[:deduplication_field]
|
53
|
+
set_depositor(attributes[:depositor_id])
|
54
|
+
@success_count = 0
|
55
|
+
@failure_count = 0
|
56
|
+
super(error_stream: error_stream, info_stream: info_stream)
|
57
|
+
end
|
58
|
+
|
59
|
+
# "depositor" is a required field for Hyrax. If
|
60
|
+
# it hasn't been set, set it to the Hyrax default
|
61
|
+
# batch user.
|
62
|
+
def set_depositor(user_key)
|
63
|
+
user = ::User.find_by_user_key(user_key) if user_key
|
64
|
+
user ||= ::User.find(user_key) if user_key
|
65
|
+
user ||= ::User.find_or_create_system_user(DEFAULT_CREATOR_KEY)
|
66
|
+
self.depositor = user
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# @param record [ImportRecord]
|
71
|
+
# @return [ActiveFedora::Base]
|
72
|
+
# Search for any existing records that match on the deduplication_field
|
73
|
+
def find_existing_record(record)
|
74
|
+
return unless deduplication_field
|
75
|
+
return unless record.respond_to?(deduplication_field)
|
76
|
+
return if record.mapper.send(deduplication_field).nil?
|
77
|
+
return if record.mapper.send(deduplication_field).empty?
|
78
|
+
existing_records = import_type.where("#{deduplication_field}": record.mapper.send(deduplication_field).to_s)
|
79
|
+
raise "More than one record matches deduplication_field #{deduplication_field} with value #{record.mapper.send(deduplication_field)}" if existing_records.count > 1
|
80
|
+
existing_records&.first
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# @param record [ImportRecord]
|
85
|
+
#
|
86
|
+
# @return [void]
|
87
|
+
def import(record:)
|
88
|
+
existing_record = find_existing_record(record)
|
89
|
+
create_for(record: record) unless existing_record
|
90
|
+
update_for(existing_record: existing_record, update_record: record) if existing_record
|
91
|
+
rescue Faraday::ConnectionFailed, Ldp::HttpError => e
|
92
|
+
error_stream << e
|
93
|
+
rescue RuntimeError => e
|
94
|
+
error_stream << e
|
95
|
+
raise e
|
96
|
+
end
|
97
|
+
|
98
|
+
# TODO: You should be able to specify the import type in the import
|
99
|
+
def import_type
|
100
|
+
raise 'No curation_concern found for import' unless
|
101
|
+
defined?(Hyrax) && Hyrax&.config&.curation_concerns&.any?
|
102
|
+
|
103
|
+
Hyrax.config.curation_concerns.first
|
104
|
+
end
|
105
|
+
|
106
|
+
# The path on disk where file attachments can be found
|
107
|
+
def file_attachments_path
|
108
|
+
ENV['IMPORT_PATH'] || '/opt/data'
|
109
|
+
end
|
110
|
+
|
111
|
+
# Create a Hyrax::UploadedFile for each file attachment
|
112
|
+
# TODO: What if we can't find the file?
|
113
|
+
# TODO: How do we specify where the files can be found?
|
114
|
+
# @param [Zizia::InputRecord]
|
115
|
+
# @return [Array] an array of Hyrax::UploadedFile ids
|
116
|
+
def create_upload_files(record)
|
117
|
+
return unless record.mapper.respond_to?(:files)
|
118
|
+
files_to_attach = record.mapper.files
|
119
|
+
return [] if files_to_attach.nil? || files_to_attach.empty?
|
120
|
+
|
121
|
+
uploaded_file_ids = []
|
122
|
+
files_to_attach.each do |filename|
|
123
|
+
file = File.open(find_file_path(filename))
|
124
|
+
uploaded_file = Hyrax::UploadedFile.create(user: @depositor, file: file)
|
125
|
+
uploaded_file_ids << uploaded_file.id
|
126
|
+
file.close
|
127
|
+
end
|
128
|
+
uploaded_file_ids
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Within the directory specified by ENV['IMPORT_PATH'], find the first
|
133
|
+
# instance of a file matching the given filename.
|
134
|
+
# If there is no matching file, raise an exception.
|
135
|
+
# @param [String] filename
|
136
|
+
# @return [String] a full pathname to the found file
|
137
|
+
def find_file_path(filename)
|
138
|
+
filepath = Dir.glob("#{ENV['IMPORT_PATH']}/**/#{filename}").first
|
139
|
+
raise "Cannot find file #{filename}... Are you sure it has been uploaded and that the filename matches?" if filepath.nil?
|
140
|
+
filepath
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# When submitting location data (a.k.a. the "based near" attribute) via the UI,
|
145
|
+
# Hyrax expects to receive a `based_near_attributes` hash in a specific format.
|
146
|
+
# We need to take geonames urls as provided by the customer and transform them to
|
147
|
+
# mimic what the Hyrax UI would ordinarily produce. These will get turned into
|
148
|
+
# Hyrax::ControlledVocabularies::Location objects upon ingest.
|
149
|
+
# The expected hash looks like this:
|
150
|
+
# "based_near_attributes"=>
|
151
|
+
# {
|
152
|
+
# "0"=> {
|
153
|
+
# "id"=>"http://sws.geonames.org/5667009/", "_destroy"=>""
|
154
|
+
# },
|
155
|
+
# "1"=> {
|
156
|
+
# "id"=>"http://sws.geonames.org/6252001/", "_destroy"=>""
|
157
|
+
# },
|
158
|
+
# }
|
159
|
+
# @return [Hash] a "based_near_attributes" hash as
|
160
|
+
def based_near_attributes(based_near)
|
161
|
+
original_geonames_uris = based_near
|
162
|
+
return if original_geonames_uris.empty?
|
163
|
+
based_near_attributes = {}
|
164
|
+
original_geonames_uris.each_with_index do |uri, i|
|
165
|
+
based_near_attributes[i.to_s] = { 'id' => uri_to_sws(uri), "_destroy" => "" }
|
166
|
+
end
|
167
|
+
based_near_attributes
|
168
|
+
end
|
169
|
+
|
170
|
+
#
|
171
|
+
# Take a user-facing geonames URI and return an sws URI, of the form Hyrax expects
|
172
|
+
# (e.g., "http://sws.geonames.org/6252001/")
|
173
|
+
# @param [String] uri
|
174
|
+
# @return [String] an sws style geonames uri
|
175
|
+
def uri_to_sws(uri)
|
176
|
+
uri = URI(uri)
|
177
|
+
geonames_number = uri.path.split('/')[1]
|
178
|
+
"http://sws.geonames.org/#{geonames_number}/"
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
# Build a pared down actor stack that will not re-attach files,
|
184
|
+
# or set workflow, or do anything except update metadata.
|
185
|
+
# TODO: We should be able to set an environment variable that would allow updates to go through the regular
|
186
|
+
# actor stack instead of the stripped down one, in case we want to re-import files.
|
187
|
+
def metadata_only_middleware
|
188
|
+
terminator = Hyrax::Actors::Terminator.new
|
189
|
+
Zizia::MetadataOnlyStack.build_stack.build(terminator)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Update an existing object using the Hyrax actor stack
|
193
|
+
# We assume the object was created as expected if the actor stack returns true.
|
194
|
+
# Note that for now the update stack will only update metadata and update collection membership, it will not re-import files.
|
195
|
+
def update_for(existing_record:, update_record:)
|
196
|
+
info_stream << "event: record_update_started, batch_id: #{batch_id}, collection_id: #{collection_id}, #{deduplication_field}: #{update_record.respond_to?(deduplication_field) ? update_record.send(deduplication_field) : update_record}"
|
197
|
+
additional_attrs = {
|
198
|
+
depositor: @depositor.user_key
|
199
|
+
}
|
200
|
+
attrs = update_record.attributes.merge(additional_attrs)
|
201
|
+
attrs = attrs.merge(member_of_collections_attributes: { '0' => { id: collection_id } }) if collection_id
|
202
|
+
# Ensure nothing is passed in the files field,
|
203
|
+
# since this is reserved for Hyrax and is where uploaded_files will be attached
|
204
|
+
attrs.delete(:files)
|
205
|
+
|
206
|
+
# We aren't using the attach remote files actor, so make sure any remote files are removed from the params before we try to save the object.
|
207
|
+
attrs.delete(:remote_files)
|
208
|
+
|
209
|
+
based_near = attrs.delete(:based_near)
|
210
|
+
attrs = attrs.merge(based_near_attributes: based_near_attributes(based_near)) unless based_near.nil? || based_near.empty?
|
211
|
+
|
212
|
+
actor_env = Hyrax::Actors::Environment.new(existing_record, ::Ability.new(@depositor), attrs)
|
213
|
+
if metadata_only_middleware.update(actor_env)
|
214
|
+
info_stream << "event: record_updated, batch_id: #{batch_id}, record_id: #{existing_record.id}, collection_id: #{collection_id}, #{deduplication_field}: #{existing_record.respond_to?(deduplication_field) ? existing_record.send(deduplication_field) : existing_record}"
|
215
|
+
@success_count += 1
|
216
|
+
else
|
217
|
+
existing_record.errors.each do |attr, msg|
|
218
|
+
error_stream << "event: validation_failed, batch_id: #{batch_id}, collection_id: #{collection_id}, attribute: #{attr.capitalize}, message: #{msg}, record_title: record_title: #{attrs[:title] ? attrs[:title] : attrs}"
|
219
|
+
end
|
220
|
+
@failure_count += 1
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Create an object using the Hyrax actor stack
|
225
|
+
# We assume the object was created as expected if the actor stack returns true.
|
226
|
+
def create_for(record:)
|
227
|
+
info_stream << "event: record_import_started, batch_id: #{batch_id}, collection_id: #{collection_id}, record_title: #{record.respond_to?(:title) ? record.title : record}"
|
228
|
+
|
229
|
+
additional_attrs = {
|
230
|
+
uploaded_files: create_upload_files(record),
|
231
|
+
depositor: @depositor.user_key
|
232
|
+
}
|
233
|
+
|
234
|
+
created = import_type.new
|
235
|
+
|
236
|
+
attrs = record.attributes.merge(additional_attrs)
|
237
|
+
attrs = attrs.merge(member_of_collections_attributes: { '0' => { id: collection_id } }) if collection_id
|
238
|
+
|
239
|
+
# Ensure nothing is passed in the files field,
|
240
|
+
# since this is reserved for Hyrax and is where uploaded_files will be attached
|
241
|
+
attrs.delete(:files)
|
242
|
+
|
243
|
+
based_near = attrs.delete(:based_near)
|
244
|
+
attrs = attrs.merge(based_near_attributes: based_near_attributes(based_near)) unless based_near.nil? || based_near.empty?
|
245
|
+
|
246
|
+
actor_env = Hyrax::Actors::Environment.new(created,
|
247
|
+
::Ability.new(@depositor),
|
248
|
+
attrs)
|
249
|
+
|
250
|
+
if Hyrax::CurationConcern.actor.create(actor_env)
|
251
|
+
info_stream << "event: record_created, batch_id: #{batch_id}, record_id: #{created.id}, collection_id: #{collection_id}, record_title: #{attrs[:title]&.first}"
|
252
|
+
@success_count += 1
|
253
|
+
else
|
254
|
+
created.errors.each do |attr, msg|
|
255
|
+
error_stream << "event: validation_failed, batch_id: #{batch_id}, collection_id: #{collection_id}, attribute: #{attr.capitalize}, message: #{msg}, record_title: record_title: #{attrs[:title] ? attrs[:title] : attrs}"
|
256
|
+
end
|
257
|
+
@failure_count += 1
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zizia
|
4
|
+
##
|
5
|
+
# The chief entry point for bulk import of records. `Importer` accepts a
|
6
|
+
# {Parser} on initialization and iterates through its {Parser#records}, importing
|
7
|
+
# each using a given {RecordImporter}.
|
8
|
+
#
|
9
|
+
# @example Importing in bulk from a CSV file
|
10
|
+
# parser = Zizia::Parser.for(file: File.new('path/to/import.csv'))
|
11
|
+
#
|
12
|
+
# Zizia::Importer.new(parser: parser).import if parser.validate
|
13
|
+
#
|
14
|
+
class Importer
|
15
|
+
extend Forwardable
|
16
|
+
|
17
|
+
##
|
18
|
+
# @!attribute [rw] parser
|
19
|
+
# @return [Parser]
|
20
|
+
# @!attribute [rw] record_importer
|
21
|
+
# @return [RecordImporter]
|
22
|
+
attr_accessor :parser, :record_importer
|
23
|
+
|
24
|
+
##
|
25
|
+
# @!method records()
|
26
|
+
# @see Parser#records
|
27
|
+
def_delegator :parser, :records, :records
|
28
|
+
|
29
|
+
##
|
30
|
+
# @param parser [Parser] The parser to use as the source for import
|
31
|
+
# records.
|
32
|
+
# @param record_importer [RecordImporter] An object to handle import of
|
33
|
+
# each record
|
34
|
+
def initialize(parser:, record_importer: RecordImporter.new, info_stream: Zizia.config.default_info_stream, error_stream: Zizia.config.default_error_stream)
|
35
|
+
self.parser = parser
|
36
|
+
self.record_importer = record_importer
|
37
|
+
@info_stream = info_stream
|
38
|
+
@error_stream = error_stream
|
39
|
+
end
|
40
|
+
|
41
|
+
# Do not attempt to run an import if there are no records. Instead, just write to the log.
|
42
|
+
def no_records_message
|
43
|
+
@info_stream << "event: empty_import, batch_id: #{record_importer.batch_id}"
|
44
|
+
@error_stream << "event: empty_import, batch_id: #{record_importer.batch_id}"
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Import each record in {#records}.
|
49
|
+
#
|
50
|
+
# @return [void]
|
51
|
+
def import
|
52
|
+
no_records_message && return unless records.count.positive?
|
53
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
54
|
+
@info_stream << "event: start_import, batch_id: #{record_importer.batch_id}, expecting to import #{records.count} records."
|
55
|
+
records.each { |record| record_importer.import(record: record) }
|
56
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
57
|
+
elapsed_time = end_time - start_time
|
58
|
+
@info_stream << "event: finish_import, batch_id: #{record_importer.batch_id}, successful_record_count: #{record_importer.success_count}, failed_record_count: #{record_importer.failure_count}, elapsed_time: #{elapsed_time}, elapsed_time_per_record: #{elapsed_time / records.count}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zizia
|
4
|
+
##
|
5
|
+
# @example Building an importer with the factory
|
6
|
+
# record = InputRecord.from({some: :metadata}, mapper: MyMapper.new)
|
7
|
+
# record.some # => :metadata
|
8
|
+
#
|
9
|
+
class InputRecord
|
10
|
+
##
|
11
|
+
# @!attribute [rw] mapper
|
12
|
+
# @return [#map_fields]
|
13
|
+
attr_accessor :mapper
|
14
|
+
|
15
|
+
##
|
16
|
+
# @param mapper [#map_fields]
|
17
|
+
def initialize(mapper: HyraxBasicMetadataMapper.new)
|
18
|
+
self.mapper = mapper
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
##
|
23
|
+
# @param metadata [Object]
|
24
|
+
# @param mapper [#map_fields]
|
25
|
+
#
|
26
|
+
# @return [InputRecord] an input record mapping metadata with the given
|
27
|
+
# mapper
|
28
|
+
def from(metadata:, mapper: HyraxBasicMetadataMapper.new)
|
29
|
+
mapper.metadata = metadata
|
30
|
+
new(mapper: mapper)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# @return [Hash<Symbol, Object>]
|
36
|
+
def attributes
|
37
|
+
mapper.fields.each_with_object({}) do |field, attrs|
|
38
|
+
attrs[field] = public_send(field)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# @return [String, nil] an identifier for the representative file; nil if
|
44
|
+
# none is given.
|
45
|
+
def representative_file
|
46
|
+
return mapper.representative_file if
|
47
|
+
mapper.respond_to?(:representative_file)
|
48
|
+
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Respond to methods matching mapper fields
|
54
|
+
def method_missing(method_name, *args, &block)
|
55
|
+
return super unless mapper.field?(method_name)
|
56
|
+
mapper.public_send(method_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# @see #method_missing
|
61
|
+
def respond_to_missing?(method_name, include_private = false)
|
62
|
+
mapper.field?(method_name) || super
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zizia
|
4
|
+
class LogStream
|
5
|
+
##
|
6
|
+
# @!attribute [rw] logger
|
7
|
+
# @return [Logger]
|
8
|
+
# @!attribute [rw] severity
|
9
|
+
# @return [Logger::Serverity]
|
10
|
+
attr_accessor :logger, :severity
|
11
|
+
|
12
|
+
def initialize(logger: nil, severity: nil)
|
13
|
+
self.logger = logger || Logger.new(build_filename)
|
14
|
+
self.severity = severity || Logger::INFO
|
15
|
+
end
|
16
|
+
|
17
|
+
def <<(msg)
|
18
|
+
logger.add(severity, msg)
|
19
|
+
STDOUT << msg
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def build_filename
|
25
|
+
return ENV['IMPORT_LOG'] if ENV['IMPORT_LOG']
|
26
|
+
return rails_log_name if rails_log_name
|
27
|
+
'./log/zizia_import.log'
|
28
|
+
end
|
29
|
+
|
30
|
+
def rails_log_name
|
31
|
+
case Rails.env
|
32
|
+
when 'production'
|
33
|
+
Rails.root.join('log', "csv_import.log").to_s
|
34
|
+
when 'development'
|
35
|
+
Rails.root.join('log', "dev_csv_import.log").to_s
|
36
|
+
when 'test'
|
37
|
+
Rails.root.join('log', "test_csv_import.log").to_s
|
38
|
+
end
|
39
|
+
rescue ::NameError
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zizia
|
4
|
+
##
|
5
|
+
# A null/base mapper that maps no fields.
|
6
|
+
#
|
7
|
+
# Real mapper implementations need to provide `#fields`, enumerating over
|
8
|
+
# `Symbols` that represent the fields on the target object (e.g. an
|
9
|
+
# `ActiveFedora::Base`/`Hyrax::WorkBehavior`) that the mapper can handle.
|
10
|
+
# For each field in `#fields`, the mapper must respond to a matching method
|
11
|
+
# (i.e. `:title` => `#title`), and return the value(s) that should be set to
|
12
|
+
# the target's attributes upon mapping.
|
13
|
+
#
|
14
|
+
# To ease the implementation of field methods, this base class provides
|
15
|
+
# a `#method_missing` that forwards missing method names to a `#map_field`
|
16
|
+
# method. `#map_field` can be implemented to provide a generalized field
|
17
|
+
# mapping when a common pattern will be used for many methods. Callers should
|
18
|
+
# avoid relying on this protected method directly, since mappers may implement
|
19
|
+
# individual field methods in any other way (e.g. `def title; end`) to route
|
20
|
+
# around `#map_field`. Implementations are also free to override
|
21
|
+
# `#method_missing` if desired.
|
22
|
+
#
|
23
|
+
# Mappers generally operate over some input `#metadata`. Example metadata
|
24
|
+
# types that mappers could be implemented over include `Hash`, `CSV`, `XML`,
|
25
|
+
# `RDF::Graph`, etc...; mappers are free to interpret or ignore the contents
|
26
|
+
# of their underlying metadata data structures at their leisure. Values for
|
27
|
+
# fields are /usually/ derived from the `#metadata`, but can also be generated
|
28
|
+
# from complex logic or even hard-coded.
|
29
|
+
#
|
30
|
+
# @example Using a MetadataMapper
|
31
|
+
# mapper = MyMapper.new
|
32
|
+
# mapper.metadata = some_metadata_object
|
33
|
+
# mapper.fields # => [:title, :author, :description]
|
34
|
+
#
|
35
|
+
# mapper.title # => 'Some Title'
|
36
|
+
#
|
37
|
+
# mapper.fields.map { |field| mapper.send(field) }
|
38
|
+
#
|
39
|
+
# @see ImportRecord#attributes for the canonical usage of a `MetadataMapper`.
|
40
|
+
# @see HashMapper for an example implementation with dynamically generated
|
41
|
+
# fields
|
42
|
+
class MetadataMapper
|
43
|
+
##
|
44
|
+
# @!attribute [rw] metadata
|
45
|
+
# @return [Object]
|
46
|
+
attr_accessor :metadata
|
47
|
+
|
48
|
+
##
|
49
|
+
# @param name [Symbol]
|
50
|
+
#
|
51
|
+
# @return [Boolean]
|
52
|
+
def field?(name)
|
53
|
+
fields.include?(name)
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# @return [Enumerable<Symbol>] The fields the mapper can process
|
58
|
+
def fields
|
59
|
+
[]
|
60
|
+
end
|
61
|
+
|
62
|
+
def method_missing(method_name, *args, &block)
|
63
|
+
return map_field(method_name) if fields.include?(method_name)
|
64
|
+
super
|
65
|
+
end
|
66
|
+
|
67
|
+
def respond_to_missing?(method_name, include_private = false)
|
68
|
+
field?(method_name) || super
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
##
|
74
|
+
# @private
|
75
|
+
#
|
76
|
+
# @param name [Symbol]
|
77
|
+
#
|
78
|
+
# @return [Object]
|
79
|
+
def map_field(_name)
|
80
|
+
raise NotImplementedError
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zizia
|
4
|
+
class MetadataOnlyStack
|
5
|
+
def self.build_stack
|
6
|
+
ActionDispatch::MiddlewareStack.new.tap do |middleware|
|
7
|
+
# Wrap everything in a database transaction, if the save of the resource
|
8
|
+
# fails then roll back any database AdminSetChangeSet
|
9
|
+
middleware.use Hyrax::Actors::TransactionalRequest
|
10
|
+
|
11
|
+
# Ensure you are mutating the most recent version
|
12
|
+
# middleware.use Hyrax::Actors::OptimisticLockValidator
|
13
|
+
|
14
|
+
# Attach files from a URI (for BrowseEverything)
|
15
|
+
# middleware.use Hyrax::Actors::CreateWithRemoteFilesActor
|
16
|
+
|
17
|
+
# Attach files uploaded in the form to the UploadsController
|
18
|
+
# In Californica, for command line import,
|
19
|
+
# we are using the CreateWithFilesActor to attach
|
20
|
+
# local files with a file:// url, not via the UploadsController,
|
21
|
+
# so we never use the CreateWithFilesActor
|
22
|
+
# middleware.use Hyrax::Actors::CreateWithFilesActor
|
23
|
+
|
24
|
+
# Add/remove the resource to/from a collection
|
25
|
+
middleware.use Hyrax::Actors::CollectionsMembershipActor
|
26
|
+
|
27
|
+
# Add/remove to parent work
|
28
|
+
# middleware.use Hyrax::Actors::AddToWorkActor
|
29
|
+
|
30
|
+
# Add/remove children (works or file_sets)
|
31
|
+
# middleware.use Hyrax::Actors::AttachMembersActor
|
32
|
+
|
33
|
+
# Set the order of the children (works or file_sets)
|
34
|
+
# middleware.use Hyrax::Actors::ApplyOrderActor
|
35
|
+
|
36
|
+
# Sets the default admin set if they didn't supply one
|
37
|
+
# middleware.use Hyrax::Actors::DefaultAdminSetActor
|
38
|
+
|
39
|
+
# Decode the private/public/institution on the form into permisisons on
|
40
|
+
# the model
|
41
|
+
# We aren't using this in Californica, we're just setting the visibility
|
42
|
+
# at import time
|
43
|
+
# middleware.use Hyrax::Actors::InterpretVisibilityActor
|
44
|
+
|
45
|
+
# Handles transfering ownership of works from one user to another
|
46
|
+
# We aren't using this in Californica
|
47
|
+
# middleware.use Hyrax::Actors::TransferRequestActor
|
48
|
+
|
49
|
+
# Copies default permissions from the PermissionTemplate to the work
|
50
|
+
# middleware.use Hyrax::Actors::ApplyPermissionTemplateActor
|
51
|
+
|
52
|
+
# Remove attached FileSets when destroying a work
|
53
|
+
# middleware.use Hyrax::Actors::CleanupFileSetsActor
|
54
|
+
|
55
|
+
# Destroys the trophies in the database when the work is destroyed
|
56
|
+
# middleware.use Hyrax::Actors::CleanupTrophiesActor
|
57
|
+
|
58
|
+
# Destroys the feature tag in the database when the work is destroyed
|
59
|
+
# middleware.use Hyrax::Actors::FeaturedWorkActor
|
60
|
+
|
61
|
+
# Persist the metadata changes on the resource
|
62
|
+
middleware.use Hyrax::Actors::ModelActor
|
63
|
+
|
64
|
+
# Start the workflow for this work
|
65
|
+
# middleware.use Hyrax::Actors::InitializeWorkflowActor
|
66
|
+
end
|
67
|
+
end
|
68
|
+
# rubocop:enable Metrics/MethodLength
|
69
|
+
end
|
70
|
+
end
|