bulkrax 6.0.1 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +7 -7
  3. data/app/assets/javascripts/bulkrax/bulkrax.js +11 -0
  4. data/app/assets/javascripts/bulkrax/datatables.js +139 -0
  5. data/app/assets/javascripts/bulkrax/exporters.js +4 -4
  6. data/app/assets/javascripts/bulkrax/importers.js.erb +15 -1
  7. data/app/assets/stylesheets/bulkrax/import_export.scss +6 -1
  8. data/app/controllers/bulkrax/entries_controller.rb +52 -3
  9. data/app/controllers/bulkrax/exporters_controller.rb +20 -8
  10. data/app/controllers/bulkrax/importers_controller.rb +31 -12
  11. data/app/controllers/concerns/bulkrax/datatables_behavior.rb +201 -0
  12. data/app/factories/bulkrax/object_factory.rb +135 -163
  13. data/app/factories/bulkrax/object_factory_interface.rb +491 -0
  14. data/app/factories/bulkrax/valkyrie_object_factory.rb +402 -0
  15. data/app/helpers/bulkrax/application_helper.rb +7 -3
  16. data/app/helpers/bulkrax/importers_helper.rb +1 -1
  17. data/app/helpers/bulkrax/validation_helper.rb +4 -4
  18. data/app/jobs/bulkrax/create_relationships_job.rb +28 -17
  19. data/app/jobs/bulkrax/delete_and_import_collection_job.rb +8 -0
  20. data/app/jobs/bulkrax/delete_and_import_file_set_job.rb +8 -0
  21. data/app/jobs/bulkrax/delete_and_import_job.rb +20 -0
  22. data/app/jobs/bulkrax/delete_and_import_work_job.rb +8 -0
  23. data/app/jobs/bulkrax/delete_job.rb +8 -3
  24. data/app/jobs/bulkrax/download_cloud_file_job.rb +17 -4
  25. data/app/jobs/bulkrax/import_collection_job.rb +1 -1
  26. data/app/jobs/bulkrax/import_file_set_job.rb +6 -3
  27. data/app/jobs/bulkrax/import_job.rb +7 -0
  28. data/app/jobs/bulkrax/import_work_job.rb +1 -1
  29. data/app/jobs/bulkrax/importer_job.rb +19 -3
  30. data/app/matchers/bulkrax/application_matcher.rb +0 -2
  31. data/app/models/bulkrax/csv_collection_entry.rb +1 -3
  32. data/app/models/bulkrax/csv_entry.rb +9 -7
  33. data/app/models/bulkrax/entry.rb +9 -11
  34. data/app/models/bulkrax/exporter.rb +11 -4
  35. data/app/models/bulkrax/importer.rb +49 -10
  36. data/app/models/bulkrax/oai_entry.rb +0 -3
  37. data/app/models/bulkrax/oai_set_entry.rb +1 -3
  38. data/app/models/bulkrax/rdf_collection_entry.rb +1 -4
  39. data/app/models/bulkrax/rdf_entry.rb +70 -69
  40. data/app/models/bulkrax/status.rb +10 -1
  41. data/app/models/bulkrax/xml_entry.rb +0 -1
  42. data/app/models/concerns/bulkrax/dynamic_record_lookup.rb +2 -19
  43. data/app/models/concerns/bulkrax/export_behavior.rb +2 -2
  44. data/app/models/concerns/bulkrax/file_factory.rb +174 -118
  45. data/app/models/concerns/bulkrax/file_set_entry_behavior.rb +5 -3
  46. data/app/models/concerns/bulkrax/has_matchers.rb +28 -25
  47. data/app/models/concerns/bulkrax/import_behavior.rb +14 -33
  48. data/app/models/concerns/bulkrax/importer_exporter_behavior.rb +3 -2
  49. data/app/models/concerns/bulkrax/status_info.rb +8 -0
  50. data/app/parsers/bulkrax/application_parser.rb +116 -21
  51. data/app/parsers/bulkrax/bagit_parser.rb +173 -195
  52. data/app/parsers/bulkrax/csv_parser.rb +15 -57
  53. data/app/parsers/bulkrax/oai_dc_parser.rb +44 -16
  54. data/app/parsers/bulkrax/parser_export_record_set.rb +20 -24
  55. data/app/parsers/bulkrax/xml_parser.rb +18 -23
  56. data/app/services/bulkrax/factory_class_finder.rb +92 -0
  57. data/app/services/bulkrax/remove_relationships_for_importer.rb +3 -1
  58. data/app/services/hyrax/custom_queries/find_by_source_identifier.rb +50 -0
  59. data/app/services/wings/custom_queries/find_by_source_identifier.rb +32 -0
  60. data/app/views/bulkrax/entries/_parsed_metadata.html.erb +2 -2
  61. data/app/views/bulkrax/entries/_raw_metadata.html.erb +2 -2
  62. data/app/views/bulkrax/entries/show.html.erb +9 -8
  63. data/app/views/bulkrax/exporters/_form.html.erb +10 -10
  64. data/app/views/bulkrax/exporters/edit.html.erb +1 -1
  65. data/app/views/bulkrax/exporters/index.html.erb +13 -57
  66. data/app/views/bulkrax/exporters/new.html.erb +1 -1
  67. data/app/views/bulkrax/exporters/show.html.erb +6 -12
  68. data/app/views/bulkrax/importers/_browse_everything.html.erb +2 -2
  69. data/app/views/bulkrax/importers/_csv_fields.html.erb +8 -2
  70. data/app/views/bulkrax/importers/_edit_form_buttons.html.erb +8 -1
  71. data/app/views/bulkrax/importers/_edit_item_buttons.html.erb +18 -0
  72. data/app/views/bulkrax/importers/edit.html.erb +1 -1
  73. data/app/views/bulkrax/importers/index.html.erb +20 -64
  74. data/app/views/bulkrax/importers/new.html.erb +1 -1
  75. data/app/views/bulkrax/importers/show.html.erb +8 -14
  76. data/app/views/bulkrax/importers/upload_corrected_entries.html.erb +2 -2
  77. data/app/views/bulkrax/shared/_bulkrax_errors.html.erb +1 -1
  78. data/app/views/bulkrax/shared/_bulkrax_field_mapping.html.erb +1 -1
  79. data/app/views/bulkrax/shared/_entries_tab.html.erb +16 -0
  80. data/config/locales/bulkrax.en.yml +7 -0
  81. data/config/routes.rb +8 -2
  82. data/db/migrate/20230608153601_add_indices_to_bulkrax.rb +20 -9
  83. data/db/migrate/20240208005801_denormalize_status_message.rb +7 -0
  84. data/db/migrate/20240209070952_update_identifier_index.rb +6 -0
  85. data/db/migrate/20240307053156_add_index_to_metadata_bulkrax_identifier.rb +18 -0
  86. data/lib/bulkrax/engine.rb +23 -0
  87. data/lib/bulkrax/version.rb +1 -1
  88. data/lib/bulkrax.rb +107 -19
  89. data/lib/generators/bulkrax/templates/config/initializers/bulkrax.rb +2 -0
  90. data/lib/tasks/bulkrax_tasks.rake +13 -0
  91. data/lib/tasks/reset.rake +4 -4
  92. metadata +64 -8
  93. data/app/views/bulkrax/shared/_collection_entries_tab.html.erb +0 -39
  94. data/app/views/bulkrax/shared/_file_set_entries_tab.html.erb +0 -39
  95. data/app/views/bulkrax/shared/_work_entries_tab.html.erb +0 -39
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bulkrax
4
+ # rubocop:disable Metrics/ModuleLength
5
+ module DatatablesBehavior
6
+ extend ActiveSupport::Concern
7
+
8
+ def table_per_page
9
+ per_page = params[:length].to_i
10
+ per_page < 1 ? 30 : per_page
11
+ end
12
+
13
+ def order_value(column)
14
+ params['columns']&.[](column)&.[]('data')
15
+ end
16
+
17
+ def table_order
18
+ "#{order_value(params&.[]('order')&.[]('0')&.[]('column'))} #{params&.[]('order')&.[]('0')&.[]('dir')}" if params&.[]('order')&.[]('0')&.[]('column').present?
19
+ end
20
+
21
+ # convert offset to page number
22
+ def table_page
23
+ params[:start].blank? ? 1 : (params[:start].to_i / params[:length].to_i) + 1
24
+ end
25
+
26
+ def entry_table_search
27
+ return @entry_table_search if @entry_table_search
28
+ return @entry_table_search = false if params['search']&.[]('value').blank?
29
+
30
+ table_search_value = params['search']&.[]('value')&.downcase
31
+
32
+ ['identifier', 'id', 'status_message', 'type', 'updated_at'].map do |col|
33
+ column = Bulkrax::Entry.arel_table[col]
34
+ column = Arel::Nodes::NamedFunction.new('CAST', [column.as('text')])
35
+ column = Arel::Nodes::NamedFunction.new('LOWER', [column])
36
+ @entry_table_search = if @entry_table_search
37
+ @entry_table_search.or(column.matches("%#{table_search_value}%"))
38
+ else
39
+ column.matches("%#{table_search_value}%")
40
+ end
41
+ end
42
+
43
+ @entry_table_search
44
+ end
45
+
46
+ def importer_table_search
47
+ return @importer_table_search if @importer_table_search
48
+ return @importer_table_search = false if params['search']&.[]('value').blank?
49
+
50
+ table_search_value = params['search']&.[]('value')&.downcase
51
+
52
+ ['name', 'id', 'status_message', 'last_error_at', 'last_succeeded_at', 'updated_at'].map do |col|
53
+ column = Bulkrax::Importer.arel_table[col]
54
+ column = Arel::Nodes::NamedFunction.new('CAST', [column.as('text')])
55
+ column = Arel::Nodes::NamedFunction.new('LOWER', [column])
56
+ @importer_table_search = if @importer_table_search
57
+ @importer_table_search.or(column.matches("%#{table_search_value}%"))
58
+ else
59
+ column.matches("%#{table_search_value}%")
60
+ end
61
+ end
62
+
63
+ @importer_table_search
64
+ end
65
+
66
+ def exporter_table_search
67
+ return @exporter_table_search if @exporter_table_search
68
+ return @exporter_table_search = false if params['search']&.[]('value').blank?
69
+
70
+ table_search_value = params['search']&.[]('value')&.downcase
71
+
72
+ ['name', 'status_message', 'created_at'].map do |col|
73
+ column = Bulkrax::Exporter.arel_table[col]
74
+ column = Arel::Nodes::NamedFunction.new('CAST', [column.as('text')])
75
+ column = Arel::Nodes::NamedFunction.new('LOWER', [column])
76
+ @exporter_table_search = if @exporter_table_search
77
+ @exporter_table_search.or(column.matches("%#{table_search_value}%"))
78
+ else
79
+ column.matches("%#{table_search_value}%")
80
+ end
81
+ end
82
+
83
+ @exporter_table_search
84
+ end
85
+
86
+ def format_importers(importers)
87
+ result = importers.map do |i|
88
+ {
89
+ name: view_context.link_to(i.name, view_context.importer_path(i)),
90
+ status_message: status_message_for(i),
91
+ last_imported_at: i.last_imported_at&.strftime("%b %d, %Y"),
92
+ next_import_at: i.next_import_at&.strftime("%b %d, %Y"),
93
+ enqueued_records: i.last_run&.enqueued_records,
94
+ processed_records: i.last_run&.processed_records || 0,
95
+ failed_records: i.last_run&.failed_records || 0,
96
+ deleted_records: i.last_run&.deleted_records,
97
+ total_collection_entries: i.last_run&.total_collection_entries,
98
+ total_work_entries: i.last_run&.total_work_entries,
99
+ total_file_set_entries: i.last_run&.total_file_set_entries,
100
+ actions: importer_util_links(i)
101
+ }
102
+ end
103
+ {
104
+ data: result,
105
+ recordsTotal: Bulkrax::Importer.count,
106
+ recordsFiltered: Bulkrax::Importer.count
107
+ }
108
+ end
109
+
110
+ def format_exporters(exporters)
111
+ result = exporters.map do |e|
112
+ {
113
+ name: view_context.link_to(e.name, view_context.exporter_path(e)),
114
+ status_message: status_message_for(e),
115
+ created_at: e.created_at,
116
+ download: download_zip(e),
117
+ actions: exporter_util_links(e)
118
+ }
119
+ end
120
+ {
121
+ data: result,
122
+ recordsTotal: Bulkrax::Exporter.count,
123
+ recordsFiltered: Bulkrax::Exporter.count
124
+ }
125
+ end
126
+
127
+ def format_entries(entries, item)
128
+ result = entries.map do |e|
129
+ {
130
+ identifier: view_context.link_to(e.identifier, view_context.item_entry_path(item, e)),
131
+ id: e.id,
132
+ status_message: status_message_for(e),
133
+ type: e.type,
134
+ updated_at: e.updated_at,
135
+ errors: e.latest_status&.error_class&.present? ? view_context.link_to(e.latest_status.error_class, view_context.item_entry_path(item, e), title: e.latest_status.error_message) : "",
136
+ actions: entry_util_links(e, item)
137
+ }
138
+ end
139
+ {
140
+ data: result,
141
+ recordsTotal: item.entries.size,
142
+ recordsFiltered: item.entries.size
143
+ }
144
+ end
145
+
146
+ def entry_util_links(e, item)
147
+ links = []
148
+ links << view_context.link_to(view_context.raw('<span class="fa fa-info-circle"></span>'), view_context.item_entry_path(item, e))
149
+ links << "<a class='fa fa-repeat' data-toggle='modal' data-target='#bulkraxItemModal' data-entry-id='#{e.id}'></a>" if view_context.an_importer?(item)
150
+ links << view_context.link_to(view_context.raw('<span class="fa fa-trash"></span>'), view_context.item_entry_path(item, e), method: :delete, data: { confirm: 'This will delete the entry and any work associated with it. Are you sure?' })
151
+ links.join(" ")
152
+ end
153
+
154
+ def status_message_for(e)
155
+ if e.status_message == "Complete"
156
+ "<td><span class='fa fa-check' style='color: green;'></span> #{e.status_message}</td>"
157
+ elsif e.status_message == "Pending"
158
+ "<td><span class='fa fa-ellipsis-h' style='color: blue;'></span> #{e.status_message}</td>"
159
+ elsif e.status_message == "Skipped"
160
+ "<td><span class='fa fa-step-forward' style='color: yellow;'></span> #{e.status_message}</td>"
161
+ else
162
+ "<td><span class='fa fa-remove' style='color: #{e.status == 'Deleted' ? 'green' : 'red'};'></span> #{e.status_message}</td>"
163
+ end
164
+ end
165
+
166
+ def importer_util_links(i)
167
+ links = []
168
+ links << view_context.link_to(view_context.raw('<span class="fa fa-info-circle"></span>'), importer_path(i))
169
+ links << view_context.link_to(view_context.raw('<span class="fa fa-pencil"></span>'), edit_importer_path(i))
170
+ links << view_context.link_to(view_context.raw('<span class="fa fa-remove"></span>'), i, method: :delete, data: { confirm: 'Are you sure?' })
171
+ links.join(" ")
172
+ end
173
+
174
+ def exporter_util_links(i)
175
+ links = []
176
+ links << view_context.link_to(view_context.raw('<span class="fa fa-info-circle"></span>'), exporter_path(i))
177
+ links << view_context.link_to(view_context.raw('<span class="fa fa-pencil"></span>'), edit_exporter_path(i), data: { turbolinks: false })
178
+ links << view_context.link_to(view_context.raw('<span class="fa fa-remove"></span>'), i, method: :delete, data: { confirm: 'Are you sure?' })
179
+ links.join(" ")
180
+ end
181
+
182
+ def download_zip(e)
183
+ return unless File.exist?(e.exporter_export_zip_path)
184
+
185
+ options_html = e.exporter_export_zip_files.flatten.map do |file_name|
186
+ "<option value='#{CGI.escapeHTML(file_name)}'>#{CGI.escapeHTML(file_name)}</option>"
187
+ end.join
188
+
189
+ form_html = "<form class='simple_form edit_exporter' id='edit_exporter_#{e.id}' action='#{view_context.exporter_download_path(e)}' accept-charset='UTF-8' method='get'>"
190
+ form_html += "<input name='utf8' type='hidden' value='✓'>"
191
+ form_html += "<select class='btn btn-default form-control' style='width: 200px' name='exporter[exporter_export_zip_files]' id='exporter_#{e.id}_exporter_export_zip_files'>"
192
+ form_html += options_html
193
+ form_html += "</select>\n" # add newline here to add a space between the dropdown and the download button
194
+ form_html += "<input type='submit' name='commit' value='Download' class='btn btn-default'>"
195
+ form_html += "</form>"
196
+
197
+ form_html
198
+ end
199
+ end
200
+ # rubocop:enable Metrics/ModuleLength
201
+ end
@@ -1,153 +1,171 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bulkrax
4
- class ObjectFactory # rubocop:disable Metrics/ClassLength
5
- extend ActiveModel::Callbacks
4
+ # rubocop:disable Metrics/ClassLength
5
+ class ObjectFactory < ObjectFactoryInterface
6
6
  include Bulkrax::FileFactory
7
- include DynamicRecordLookup
8
7
 
9
- # @api private
10
- #
11
- # These are the attributes that we assume all "work type" classes (e.g. the given :klass) will
12
- # have in addition to their specific attributes.
13
- #
14
- # @return [Array<Symbol>]
15
- # @see #permitted_attributes
16
- class_attribute :base_permitted_attributes,
17
- default: %i[id edit_users edit_groups read_groups visibility work_members_attributes admin_set_id]
8
+ ##
9
+ # @!group Class Method Interface
18
10
 
19
- # @return [Boolean]
20
- #
21
- # @example
22
- # Bulkrax::ObjectFactory.transformation_removes_blank_hash_values = true
23
- #
24
- # @see #transform_attributes
25
- # @see https://github.com/samvera-labs/bulkrax/pull/708 For discussion concerning this feature
26
- # @see https://github.com/samvera-labs/bulkrax/wiki/Interacting-with-Metadata For documentation
27
- # concerning default behavior.
28
- class_attribute :transformation_removes_blank_hash_values, default: false
11
+ ##
12
+ # @note This does not save either object. We need to do that in another
13
+ # loop. Why? Because we might be adding many items to the parent.
14
+ def self.add_child_to_parent_work(parent:, child:)
15
+ return true if parent.ordered_members.to_a.include?(child_record)
29
16
 
30
- define_model_callbacks :save, :create
31
- attr_reader :attributes, :object, :source_identifier_value, :klass, :replace_files, :update_files, :work_identifier, :work_identifier_search_field, :related_parents_parsed_mapping, :importer_run_id
17
+ parent.ordered_members << child
18
+ end
32
19
 
33
- # rubocop:disable Metrics/ParameterLists
34
- def initialize(attributes:, source_identifier_value:, work_identifier:, work_identifier_search_field:, related_parents_parsed_mapping: nil, replace_files: false, user: nil, klass: nil, importer_run_id: nil, update_files: false)
35
- @attributes = ActiveSupport::HashWithIndifferentAccess.new(attributes)
36
- @replace_files = replace_files
37
- @update_files = update_files
38
- @user = user || User.batch_user
39
- @work_identifier = work_identifier
40
- @work_identifier_search_field = work_identifier_search_field
41
- @related_parents_parsed_mapping = related_parents_parsed_mapping
42
- @source_identifier_value = source_identifier_value
43
- @klass = klass || Bulkrax.default_work_type.constantize
44
- @importer_run_id = importer_run_id
20
+ def self.add_resource_to_collection(collection:, resource:, user:)
21
+ collection.try(:reindex_extent=, Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX) if
22
+ defined?(Hyrax::Adapters::NestingIndexAdapter)
23
+ resource.member_of_collections << collection
24
+ save!(resource: resource, user: user)
45
25
  end
46
- # rubocop:enable Metrics/ParameterLists
47
26
 
48
- # update files is set, replace files is set or this is a create
49
- def with_files
50
- update_files || replace_files || !object
27
+ def self.update_index_for_file_sets_of(resource:)
28
+ resource.file_sets.each(&:update_index) if resource.respond_to?(:file_sets)
51
29
  end
52
30
 
53
- def run
54
- arg_hash = { id: attributes[:id], name: 'UPDATE', klass: klass }
55
- @object = find
56
- if object
57
- object.reindex_extent = Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX if object.respond_to?(:reindex_extent)
58
- ActiveSupport::Notifications.instrument('import.importer', arg_hash) { update }
59
- else
60
- ActiveSupport::Notifications.instrument('import.importer', arg_hash.merge(name: 'CREATE')) { create }
61
- end
62
- yield(object) if block_given?
63
- object
31
+ ##
32
+ # @see Bulkrax::ObjectFactoryInterface
33
+ def self.export_properties
34
+ # TODO: Consider how this may or may not work for Valkyrie
35
+ properties = Bulkrax.curation_concerns.map { |work| work.properties.keys }.flatten.uniq.sort
36
+ properties.reject { |prop| Bulkrax.reserved_properties.include?(prop) }
64
37
  end
65
38
 
66
- def run!
67
- self.run
68
- # Create the error exception if the object is not validly saved for some reason
69
- raise ActiveFedora::RecordInvalid, object if !object.persisted? || object.changed?
70
- object
39
+ def self.field_multi_value?(field:, model:)
40
+ return false unless field_supported?(field: field, model: model)
41
+ return false unless model.singleton_methods.include?(:properties)
42
+
43
+ model&.properties&.[](field)&.[]("multiple")
71
44
  end
72
45
 
73
- def update
74
- raise "Object doesn't exist" unless object
75
- destroy_existing_files if @replace_files && ![Collection, FileSet].include?(klass)
76
- attrs = transform_attributes(update: true)
77
- run_callbacks :save do
78
- if klass == Collection
79
- update_collection(attrs)
80
- elsif klass == FileSet
81
- update_file_set(attrs)
82
- else
83
- update_work(attrs)
84
- end
85
- end
86
- object.apply_depositor_metadata(@user) && object.save! if object.depositor.nil?
87
- log_updated(object)
46
+ def self.field_supported?(field:, model:)
47
+ model.method_defined?(field) && model.properties[field].present?
48
+ end
49
+
50
+ def self.file_sets_for(resource:)
51
+ return [] if resource.blank?
52
+ return [resource] if resource.is_a?(Bulkrax.file_model_class)
53
+
54
+ resource.file_sets
88
55
  end
89
56
 
90
- def find
91
- found = find_by_id if attributes[:id].present?
92
- return found if found.present?
93
- return search_by_identifier if attributes[work_identifier].present?
57
+ ##
58
+ #
59
+ # @see Bulkrax::ObjectFactoryInterface
60
+ def self.find(id)
61
+ ActiveFedora::Base.find(id)
62
+ rescue ActiveFedora::ObjectNotFoundError => e
63
+ raise ObjectFactoryInterface::ObjectNotFoundError, e.message
94
64
  end
95
65
 
96
- def find_by_id
97
- klass.find(attributes[:id]) if klass.exists?(attributes[:id])
66
+ def self.find_or_create_default_admin_set
67
+ # NOTE: Hyrax 5+ removed this method
68
+ AdminSet.find_or_create_default_admin_set_id
98
69
  end
99
70
 
100
- def find_or_create
101
- o = find
102
- return o if o
103
- run(&:save!)
71
+ def self.publish(**)
72
+ return true
104
73
  end
105
74
 
106
- def search_by_identifier
107
- query = { work_identifier_search_field =>
108
- source_identifier_value }
109
- # Query can return partial matches (something6 matches both something6 and something68)
110
- # so we need to weed out any that are not the correct full match. But other items might be
111
- # in the multivalued field, so we have to go through them one at a time.
112
- match = klass.where(query).detect { |m| m.send(work_identifier).include?(source_identifier_value) }
75
+ ##
76
+ # @param value [String]
77
+ # @param klass [Class, #where]
78
+ # @param field [String, Symbol] A convenience parameter where we pass the
79
+ # same value to search_field and name_field.
80
+ # @param search_field [String, Symbol] the Solr field name
81
+ # (e.g. "title_tesim")
82
+ # @param name_field [String] the ActiveFedora::Base property name
83
+ # (e.g. "title")
84
+ # @param verify_property [TrueClass] when true, verify that the given :klass
85
+ #
86
+ # @return [NilClass] when no object is found.
87
+ # @return [ActiveFedora::Base] when a match is found, an instance of given
88
+ # :klass
89
+ # rubocop:disable Metrics/ParameterLists
90
+ #
91
+ # @note HEY WE'RE USING THIS FOR A WINGS CUSTOM QUERY. BE CAREFUL WITH
92
+ # REMOVING IT.
93
+ #
94
+ # @see # {Wings::CustomQueries::FindBySourceIdentifier#find_by_model_and_property_value}
95
+ def self.search_by_property(value:, klass:, field: nil, search_field: nil, name_field: nil, verify_property: false)
96
+ return nil unless klass.respond_to?(:where)
97
+ # We're not going to try to match nil nor "".
98
+ return if value.blank?
99
+ return if verify_property && !klass.properties.keys.include?(search_field)
100
+
101
+ search_field ||= field
102
+ name_field ||= field
103
+ raise "You must provide either (search_field AND name_field) OR field parameters" if search_field.nil? || name_field.nil?
104
+ # NOTE: Query can return partial matches (something6 matches both
105
+ # something6 and something68) so we need to weed out any that are not the
106
+ # correct full match. But other items might be in the multivalued field,
107
+ # so we have to go through them one at a time.
108
+ #
109
+ # A ssi field is string, so we're looking at exact matches.
110
+ # A tesi field is text, so partial matches work.
111
+ #
112
+ # We need to wrap the result in an Array, else we might have a scalar that
113
+ # will result again in partial matches.
114
+ match = klass.where(search_field => value).detect do |m|
115
+ # Don't use Array.wrap as we likely have an ActiveTriples::Relation
116
+ # which defiantly claims to be an Array yet does not behave consistently
117
+ # with an Array. Hopefully the name_field is not a Date or Time object,
118
+ # Because that too will be a mess.
119
+ Array(m.send(name_field)).include?(value)
120
+ end
113
121
  return match if match
114
122
  end
123
+ # rubocop:enable Metrics/ParameterLists
124
+
125
+ def self.query(q, **kwargs)
126
+ ActiveFedora::SolrService.query(q, **kwargs)
127
+ end
115
128
 
116
- # An ActiveFedora bug when there are many habtm <-> has_many associations means they won't all get saved.
117
- # https://github.com/projecthydra/active_fedora/issues/874
118
- # 2+ years later, still open!
119
- def create
120
- attrs = transform_attributes
121
- @object = klass.new
122
- object.reindex_extent = Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX if defined?(Hyrax::Adapters::NestingIndexAdapter) && object.respond_to?(:reindex_extent)
123
- run_callbacks :save do
124
- run_callbacks :create do
125
- if klass == Collection
126
- create_collection(attrs)
127
- elsif klass == FileSet
128
- create_file_set(attrs)
129
- else
130
- create_work(attrs)
131
- end
132
- end
129
+ def self.clean!
130
+ super do
131
+ ActiveFedora::Cleaner.clean!
133
132
  end
134
- object.apply_depositor_metadata(@user) && object.save! if object.depositor.nil?
135
- log_created(object)
136
133
  end
137
134
 
138
- def log_created(obj)
139
- msg = "Created #{klass.model_name.human} #{obj.id}"
140
- Rails.logger.info("#{msg} (#{Array(attributes[work_identifier]).first})")
135
+ def self.solr_name(field_name)
136
+ if defined?(Hyrax)
137
+ Hyrax.index_field_mapper.solr_name(field_name)
138
+ else
139
+ ActiveFedora.index_field_mapper.solr_name(field_name)
140
+ end
141
+ end
142
+
143
+ def self.ordered_file_sets_for(object)
144
+ object&.ordered_members.to_a.select(&:file_set?)
145
+ end
146
+
147
+ def self.save!(resource:, **)
148
+ resource.save!
141
149
  end
142
150
 
143
- def log_updated(obj)
144
- msg = "Updated #{klass.model_name.human} #{obj.id}"
145
- Rails.logger.info("#{msg} (#{Array(attributes[work_identifier]).first})")
151
+ def self.update_index(resources: [])
152
+ Array(resources).each(&:update_index)
146
153
  end
154
+ # @!endgroup Class Method Interface
155
+ ##
147
156
 
148
- def log_deleted_fs(obj)
149
- msg = "Deleted All Files from #{obj.id}"
150
- Rails.logger.info("#{msg} (#{Array(attributes[work_identifier]).first})")
157
+ def find_by_id
158
+ return false if attributes[:id].blank?
159
+ # Rails / Ruby upgrade, we moved from :exists? to :exist? However we want to continue (for a
160
+ # bit) to support older versions.
161
+ method_name = klass.respond_to?(:exist?) ? :exist? : :exists?
162
+ klass.find(attributes[:id]) if klass.send(method_name, attributes[:id])
163
+ rescue Valkyrie::Persistence::ObjectNotFoundError
164
+ false
165
+ end
166
+
167
+ def delete(_user)
168
+ find&.delete
151
169
  end
152
170
 
153
171
  private
@@ -238,52 +256,6 @@ module Bulkrax
238
256
  update == true ? actor.update_content(tmp_file) : actor.create_content(tmp_file, from_url: true)
239
257
  tmp_file.close
240
258
  end
241
-
242
- def clean_attrs(attrs)
243
- # avoid the "ArgumentError: Identifier must be a string of size > 0 in order to be treeified" error
244
- # when setting object.attributes
245
- attrs.delete('id') if attrs['id'].blank?
246
- attrs
247
- end
248
-
249
- def collection_type(attrs)
250
- return attrs if attrs['collection_type_gid'].present?
251
-
252
- attrs['collection_type_gid'] = Hyrax::CollectionType.find_or_create_default_collection_type.to_global_id.to_s
253
- attrs
254
- end
255
-
256
- # Override if we need to map the attributes from the parser in
257
- # a way that is compatible with how the factory needs them.
258
- def transform_attributes(update: false)
259
- @transform_attributes = attributes.slice(*permitted_attributes)
260
- @transform_attributes.merge!(file_attributes(update_files)) if with_files
261
- @transform_attributes = remove_blank_hash_values(@transform_attributes) if transformation_removes_blank_hash_values?
262
- update ? @transform_attributes.except(:id) : @transform_attributes
263
- end
264
-
265
- # Regardless of what the Parser gives us, these are the properties we are prepared to accept.
266
- def permitted_attributes
267
- klass.properties.keys.map(&:to_sym) + base_permitted_attributes
268
- end
269
-
270
- # Return a copy of the given attributes, such that all values that are empty or an array of all
271
- # empty values are fully emptied. (See implementation details)
272
- #
273
- # @param attributes [Hash]
274
- # @return [Hash]
275
- #
276
- # @see https://github.com/emory-libraries/dlp-curate/issues/1973
277
- def remove_blank_hash_values(attributes)
278
- dupe = attributes.dup
279
- dupe.each do |key, values|
280
- if values.is_a?(Array) && values.all? { |value| value.is_a?(String) && value.empty? }
281
- dupe[key] = []
282
- elsif values.is_a?(String) && values.empty?
283
- dupe[key] = nil
284
- end
285
- end
286
- dupe
287
- end
288
259
  end
260
+ # rubocop:enable Metrics/ClassLength
289
261
  end