bulkrax 9.5.1 → 10.0.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +49 -0
  3. data/app/controllers/bulkrax/application_controller.rb +83 -0
  4. data/app/controllers/bulkrax/entries_controller.rb +16 -13
  5. data/app/controllers/bulkrax/exporters_controller.rb +21 -25
  6. data/app/controllers/bulkrax/guided_imports_controller.rb +4 -8
  7. data/app/controllers/bulkrax/importers_controller.rb +26 -29
  8. data/app/controllers/concerns/bulkrax/datatables_behavior.rb +18 -12
  9. data/app/controllers/concerns/bulkrax/importer_file_handler.rb +1 -1
  10. data/app/factories/bulkrax/object_factory.rb +2 -2
  11. data/app/factories/bulkrax/valkyrie_object_factory.rb +29 -17
  12. data/app/helpers/bulkrax/validation_helper.rb +1 -1
  13. data/app/jobs/bulkrax/create_relationships_job.rb +1 -1
  14. data/app/jobs/bulkrax/import_collection_job.rb +9 -2
  15. data/app/matchers/bulkrax/application_matcher.rb +5 -2
  16. data/app/models/bulkrax/csv_entry.rb +1 -1
  17. data/app/models/bulkrax/current.rb +7 -0
  18. data/app/models/bulkrax/export_scope.rb +21 -0
  19. data/app/models/bulkrax/exporter.rb +2 -2
  20. data/app/models/bulkrax/importer.rb +1 -1
  21. data/app/models/concerns/bulkrax/ability_behavior.rb +139 -0
  22. data/app/models/concerns/bulkrax/has_matchers.rb +1 -1
  23. data/app/models/concerns/bulkrax/import_behavior.rb +3 -3
  24. data/app/parsers/bulkrax/csv_parser.rb +13 -12
  25. data/app/parsers/bulkrax/oai_dc_parser.rb +1 -1
  26. data/app/parsers/bulkrax/parser_export_record_set.rb +26 -8
  27. data/app/parsers/concerns/bulkrax/csv_parser/csv_template_generation.rb +1 -1
  28. data/app/parsers/concerns/bulkrax/csv_parser/csv_validation.rb +1 -2
  29. data/app/parsers/concerns/bulkrax/csv_parser/csv_validation_helpers.rb +18 -1
  30. data/app/parsers/concerns/bulkrax/csv_parser/csv_validation_hierarchy.rb +2 -2
  31. data/app/search_builders/bulkrax/export_search_builder.rb +9 -0
  32. data/app/services/bulkrax/csv_template/column_builder.rb +2 -2
  33. data/app/services/bulkrax/csv_template/mapping_manager.rb +9 -7
  34. data/app/services/bulkrax/csv_template/schema_analyzer.rb +1 -5
  35. data/lib/bulkrax/engine.rb +1 -1
  36. data/lib/bulkrax/version.rb +1 -1
  37. data/lib/bulkrax.rb +76 -10
  38. data/lib/generators/bulkrax/install_generator.rb +22 -10
  39. data/lib/generators/bulkrax/templates/config/initializers/bulkrax.rb +1 -1
  40. metadata +33 -14
  41. data/app/factories/bulkrax/valkyrize-hyku.code-workspace +0 -19
  42. data/lib/wings/custom_queries/find_by_source_identifier.rb +0 -41
@@ -280,11 +280,14 @@ module Bulkrax
280
280
  # @param admin_set_id [String, nil] admin set used to resolve contexts
281
281
  # @return [Array<String>]
282
282
  def self.schema_properties(klass:, admin_set_id: nil)
283
- cached_schema_for(klass: klass, admin_set_id: admin_set_id).map { |k| k.name.to_s }
283
+ entry = schema_cache_entry(klass: klass, admin_set_id: admin_set_id)
284
+ entry[:properties] ||= entry[:schema].map { |k| k.name.to_s }.freeze
284
285
  end
285
286
 
286
287
  ##
287
- # Returns the schema for a model, memoized per (klass, admin_set_id) pair.
288
+ # Returns the schema for a model, memoized per (tenant, klass, admin_set_id)
289
+ # for the current request or job only, so a new metadata profile or a
290
+ # change to an admin set's contexts is picked up without a restart.
288
291
  # Delegates to +Hyrax.schema_for+ when available so that context-gated
289
292
  # properties are included without Bulkrax knowing about flexibility internals.
290
293
  #
@@ -292,16 +295,25 @@ module Bulkrax
292
295
  # @param admin_set_id [String, nil]
293
296
  # @return [Dry::Types::Hash]
294
297
  def self.cached_schema_for(klass:, admin_set_id: nil)
295
- @cached_schema_map ||= {}
296
- key = [klass.name, admin_set_id].compact.join('|')
297
- @cached_schema_map[key] ||=
298
- if admin_set_id.present? && defined?(Hyrax) && Hyrax.respond_to?(:schema_for)
299
- Hyrax.schema_for(klass: klass, admin_set_id: admin_set_id)
300
- else
301
- klass.new.singleton_class.schema || klass.schema
302
- end
298
+ schema_cache_entry(klass: klass, admin_set_id: admin_set_id)[:schema]
303
299
  end
304
300
 
301
+ def self.schema_cache_entry(klass:, admin_set_id:)
302
+ tenant = defined?(Apartment::Tenant) ? Apartment::Tenant.current : 'single'
303
+ Bulkrax::Current.schema_cache ||= {}
304
+ Bulkrax::Current.schema_cache[[tenant, klass, admin_set_id]] ||= { schema: resolve_schema(klass: klass, admin_set_id: admin_set_id) }
305
+ end
306
+ private_class_method :schema_cache_entry
307
+
308
+ def self.resolve_schema(klass:, admin_set_id:)
309
+ if admin_set_id.present? && defined?(Hyrax) && Hyrax.respond_to?(:schema_for)
310
+ Hyrax.schema_for(klass: klass, admin_set_id: admin_set_id)
311
+ else
312
+ klass.new.singleton_class.schema || klass.schema
313
+ end
314
+ end
315
+ private_class_method :resolve_schema
316
+
305
317
  def self.ordered_file_sets_for(object)
306
318
  return [] if object.blank?
307
319
 
@@ -434,7 +446,7 @@ module Bulkrax
434
446
 
435
447
  file_attrs = []
436
448
  uploads.each_with_index do |f, index|
437
- file_attrs << ({ uploaded_file_id: f["id"].to_s, filename: files[index]["file_name"] }).merge(additional_attributes[index])
449
+ file_attrs << { uploaded_file_id: f["id"].to_s, filename: files[index]["file_name"] }.merge(additional_attributes[index])
438
450
  end
439
451
  file_attrs.compact.uniq
440
452
  end
@@ -501,7 +513,7 @@ module Bulkrax
501
513
  @permitted_attributes ||= begin
502
514
  bare = base_permitted_attributes + if klass.respond_to?(:schema)
503
515
  admin_set_id = attributes[:admin_set_id] || attributes['admin_set_id']
504
- Bulkrax::ValkyrieObjectFactory.schema_properties(klass: klass, admin_set_id: admin_set_id)
516
+ Bulkrax::ValkyrieObjectFactory.schema_properties(klass: klass, admin_set_id: admin_set_id).map(&:to_sym)
505
517
  else
506
518
  klass.properties.keys.map(&:to_sym)
507
519
  end
@@ -590,18 +602,18 @@ module Bulkrax
590
602
 
591
603
  # @Override Destroy existing files with Hyrax::Transactions
592
604
  def destroy_existing_files(object: @object)
593
- existing_files = Hyrax.custom_queries.find_child_file_sets(resource: object)
594
- return if existing_files.empty?
595
-
596
- existing_files.each do |fs|
605
+ destroyed_ids = []
606
+ Hyrax.custom_queries.find_child_file_sets(resource: object).each do |fs|
597
607
  transactions["file_set.destroy"]
598
608
  .with_step_args("file_set.remove_from_work" => { user: @user },
599
609
  "file_set.delete" => { user: @user })
600
610
  .call(fs)
601
611
  .value!
612
+ destroyed_ids << fs.id
602
613
  end
614
+ return if destroyed_ids.empty?
603
615
 
604
- object.member_ids = object.member_ids.reject { |m| existing_files.detect { |f| f.id == m } }
616
+ object.member_ids -= destroyed_ids
605
617
  object.rendering_ids = []
606
618
  object.representative_id = nil
607
619
  object.thumbnail_id = nil
@@ -17,7 +17,7 @@ module Bulkrax
17
17
  end
18
18
 
19
19
  def check_admin_set
20
- return unless defined?(::Hyrax)
20
+ return unless Bulkrax.hyrax_loaded?
21
21
 
22
22
  if params[:importer][:admin_set_id].blank?
23
23
  params[:importer][:admin_set_id] = Bulkrax.object_factory.default_admin_set_id
@@ -114,7 +114,7 @@ module Bulkrax
114
114
  # We can use Hyrax's lock manager when we have one available.
115
115
  # However it's not certain that this is actually working, so to be
116
116
  # as safe as possible, we will reload resources before we update.
117
- if defined?(::Hyrax)
117
+ if Bulkrax.hyrax_loaded?
118
118
  include Hyrax::Lockable
119
119
 
120
120
  def conditionally_acquire_lock_for(*args, &block)
@@ -10,8 +10,15 @@ module Bulkrax
10
10
  begin
11
11
  entry.build
12
12
  entry.save!
13
- ImporterRun.increment_counter(:processed_records, args[1])
14
- ImporterRun.increment_counter(:processed_collections, args[1])
13
+ # An entry that fails inside #build records the error on itself rather
14
+ # than raising, so its status is what says whether this run succeeded.
15
+ if entry.succeeded?
16
+ ImporterRun.increment_counter(:processed_records, args[1])
17
+ ImporterRun.increment_counter(:processed_collections, args[1])
18
+ else
19
+ ImporterRun.increment_counter(:failed_records, args[1])
20
+ ImporterRun.increment_counter(:failed_collections, args[1])
21
+ end
15
22
  ImporterRun.decrement_counter(:enqueued_records, args[1]) unless ImporterRun.find(args[1]).enqueued_records <= 0 # rubocop:disable Style/IdenticalConditionalBranches
16
23
  rescue => e
17
24
  ImporterRun.increment_counter(:failed_records, args[1])
@@ -67,10 +67,13 @@ module Bulkrax
67
67
  end
68
68
 
69
69
  def parse_subject(src)
70
- string = src.strip.downcase
70
+ string = src.strip
71
71
  return if string.blank?
72
72
 
73
- string.slice(0, 1).capitalize + string.slice(1..-1)
73
+ return string.sub(%r{\Ahttps?}i, &:downcase) if string.match?(%r{\Ahttps?://}i)
74
+
75
+ downcased = string.downcase
76
+ downcased.slice(0, 1).capitalize + downcased.slice(1..-1)
74
77
  end
75
78
 
76
79
  def parse_types(src)
@@ -141,7 +141,7 @@ module Bulkrax
141
141
 
142
142
  def add_metadata_for_model
143
143
  if factory_class.present? && factory_class == Bulkrax.collection_model_class
144
- add_collection_type_gid if defined?(::Hyrax)
144
+ add_collection_type_gid if Bulkrax.hyrax_loaded?
145
145
  # add any additional collection metadata methods here
146
146
  elsif factory_class == Bulkrax.file_model_class
147
147
  validate_presence_of_filename!
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bulkrax
4
+ class Current < ActiveSupport::CurrentAttributes
5
+ attribute :schema_cache
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bulkrax
4
+ # A minimal scope adapter that satisfies the Blacklight::SearchBuilder and
5
+ # Hydra::AccessControlsEnforcement interface for export permission filtering.
6
+ #
7
+ # Blacklight::SearchBuilder expects the scope to respond to +blacklight_config+
8
+ # and +current_ability+. In the Hyrax catalog this is the controller; for
9
+ # Bulkrax exports a lightweight PORO is sufficient.
10
+ class ExportScope
11
+ attr_reader :current_ability
12
+
13
+ def initialize(ability)
14
+ @current_ability = ability
15
+ end
16
+
17
+ def blacklight_config
18
+ @blacklight_config ||= Blacklight::Configuration.new
19
+ end
20
+ end
21
+ end
@@ -69,7 +69,7 @@ module Bulkrax
69
69
  end
70
70
 
71
71
  def workflow_status_list
72
- Sipity::WorkflowState.all.map { |s| [s.name&.titleize, s.name] }.uniq if defined?(::Hyrax)
72
+ Sipity::WorkflowState.all.map { |s| [s.name&.titleize, s.name] }.uniq if Bulkrax.hyrax_loaded?
73
73
  end
74
74
 
75
75
  # If field_mapping is empty, setup a default based on the export_properties
@@ -84,7 +84,7 @@ module Bulkrax
84
84
  end
85
85
 
86
86
  def export_from_list
87
- if defined?(::Hyrax)
87
+ if Bulkrax.hyrax_loaded?
88
88
  [
89
89
  [I18n.t('bulkrax.exporter.labels.importer'), 'importer'],
90
90
  [I18n.t('bulkrax.exporter.labels.collection'), 'collection'],
@@ -18,7 +18,7 @@ module Bulkrax
18
18
  has_many :entries, as: :importerexporter, dependent: :destroy
19
19
 
20
20
  validates :name, presence: true
21
- validates :admin_set_id, presence: true if defined?(::Hyrax)
21
+ validates :admin_set_id, presence: true if Bulkrax.hyrax_loaded?
22
22
  validates :parser_klass, presence: true
23
23
 
24
24
  delegate :create_parent_child_relationships, :valid_import?, :write_errored_entries_file, :visibility, to: :parser
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bulkrax
4
+ # Concern providing Bulkrax-specific ability methods.
5
+ #
6
+ # Include this in your application's Ability class to gain default
7
+ # implementations of all Bulkrax authorization methods. Override any
8
+ # method to customize authorization behavior for your application.
9
+ #
10
+ # ## Wiring CanCan rules
11
+ #
12
+ # Call +bulkrax_default_abilities+ from your Ability's +initialize+ (or add
13
+ # it to +self.ability_logic+) to register CanCan +can+ rules for all
14
+ # Bulkrax resources:
15
+ #
16
+ # class Ability
17
+ # include Hydra::Ability
18
+ # include Bulkrax::AbilityBehavior
19
+ # self.ability_logic += [:bulkrax_default_abilities]
20
+ # end
21
+ #
22
+ # The method uses the four predicate hooks below as guards, so you can
23
+ # control who gets which rules simply by overriding those predicates.
24
+ #
25
+ # Example:
26
+ #
27
+ # class Ability
28
+ # include Hydra::Ability
29
+ # include Bulkrax::AbilityBehavior
30
+ # self.ability_logic += [:bulkrax_default_abilities]
31
+ #
32
+ # # Grant import/export access to users who can create works
33
+ # def can_import_works?
34
+ # can_create_any_work?
35
+ # end
36
+ #
37
+ # def can_export_works?
38
+ # can_create_any_work?
39
+ # end
40
+ #
41
+ # # Grant admin-level importer access to site admins
42
+ # def can_admin_importers?
43
+ # current_user.admin?
44
+ # end
45
+ # end
46
+ module AbilityBehavior
47
+ extend ActiveSupport::Concern
48
+
49
+ # Returns true if the current user may use importer functionality at all.
50
+ # Override in your Ability class to customize.
51
+ def can_import_works?
52
+ false
53
+ end
54
+
55
+ # Returns true if the current user may use exporter functionality at all.
56
+ # Override in your Ability class to customize.
57
+ def can_export_works?
58
+ false
59
+ end
60
+
61
+ # Returns true if the current user may administer ALL importers —
62
+ # i.e., view, edit, update, or destroy any importer regardless of ownership.
63
+ # Defaults to false; override to grant admin access.
64
+ def can_admin_importers?
65
+ false
66
+ end
67
+
68
+ # Returns true if the current user may administer ALL exporters —
69
+ # i.e., view, edit, update, or destroy any exporter regardless of ownership.
70
+ # Defaults to false; override to grant admin access.
71
+ def can_admin_exporters?
72
+ false
73
+ end
74
+
75
+ # Registers CanCan +can+ rules for all Bulkrax resources.
76
+ #
77
+ # Call this from your Ability's +initialize+ or add it to
78
+ # +self.ability_logic+. The predicate hooks above control which rules are
79
+ # granted; override them to tailor access.
80
+ #
81
+ # Rules for Importer and Exporter use hash-form conditions (SQL-generatable)
82
+ # so they work with +Model.accessible_by(current_ability)+. Rules for Entry
83
+ # use block-form conditions (checked in Ruby) because authorization must
84
+ # traverse the parent association — do not call +Entry.accessible_by()+;
85
+ # scope entries through the parent instead (e.g. +@importer.entries+).
86
+ def bulkrax_default_abilities
87
+ # Skip all rules when there is no authenticated user; avoids nil-id bugs
88
+ # and prevents block rules from matching NULL user_id records.
89
+ return unless current_user&.id
90
+
91
+ # Map Bulkrax-specific non-CRUD actions to their closest CanCan equivalents
92
+ # so that load_and_authorize_resource can authorize them without requiring
93
+ # a dedicated `can` declaration for each custom action name.
94
+ alias_action :entry_table, :importer_table, :exporter_table,
95
+ :original_file, :export_errors, :upload_corrected_entries, to: :read
96
+ alias_action :continue, :upload_corrected_entries_file, to: :update
97
+
98
+ grant_importer_abilities
99
+ grant_exporter_abilities
100
+ end
101
+
102
+ def grant_importer_abilities
103
+ if can_admin_importers?
104
+ # Prefer a single unconditional rule for admins. Older CanCanCan/
105
+ # ActiveRecord combinations used by the Hyrax 4 appraisal can fail when
106
+ # accessible_by has to merge unconditional and hash-conditioned rules.
107
+ can :manage, Bulkrax::Importer
108
+ can [:read, :update, :destroy], Bulkrax::Entry do |entry|
109
+ entry.importerexporter.is_a?(Bulkrax::Importer)
110
+ end
111
+ elsif can_import_works?
112
+ can :create, Bulkrax::Importer
113
+ can [:read, :update, :destroy], Bulkrax::Importer, user_id: current_user.id
114
+ can [:read, :update, :destroy], Bulkrax::Entry do |entry|
115
+ entry.importerexporter.is_a?(Bulkrax::Importer) &&
116
+ entry.importerexporter.user_id == current_user.id
117
+ end
118
+ end
119
+ end
120
+
121
+ def grant_exporter_abilities
122
+ if can_admin_exporters?
123
+ # Prefer a single unconditional rule for admins. See importer rule
124
+ # above for the Hyrax 4 accessible_by compatibility note.
125
+ can :manage, Bulkrax::Exporter
126
+ can [:read, :update, :destroy], Bulkrax::Entry do |entry|
127
+ entry.importerexporter.is_a?(Bulkrax::Exporter)
128
+ end
129
+ elsif can_export_works?
130
+ can :create, Bulkrax::Exporter
131
+ can [:read, :update, :destroy], Bulkrax::Exporter, user_id: current_user.id
132
+ can [:read, :update, :destroy], Bulkrax::Entry do |entry|
133
+ entry.importerexporter.is_a?(Bulkrax::Exporter) &&
134
+ entry.importerexporter.user_id == current_user.id
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -230,7 +230,7 @@ module Bulkrax
230
230
  # @return [Array] hyrax fields
231
231
  def field_to(field)
232
232
  fields = mapping&.map do |key, value|
233
- return unless value
233
+ next unless value
234
234
 
235
235
  if value['from'].instance_of?(Array)
236
236
  key if value['from'].include?(field) || key == field
@@ -34,7 +34,7 @@ module Bulkrax
34
34
  # TODO: This guard clause is not necessary as we can handle it in the
35
35
  # underlying factory. However, to do that requires adjusting about 7
36
36
  # failing specs. So for now this refactor appears acceptable
37
- return unless defined?(::Hyrax)
37
+ return unless Bulkrax.hyrax_loaded?
38
38
  return unless self.class.to_s.include?("Collection")
39
39
  factory.add_user_to_collection_permissions(collection: @item, user: user)
40
40
  end
@@ -87,7 +87,7 @@ module Bulkrax
87
87
  end
88
88
 
89
89
  def add_admin_set_id
90
- return unless defined?(::Hyrax)
90
+ return unless Bulkrax.hyrax_loaded?
91
91
 
92
92
  self.parsed_metadata['admin_set_id'] = importerexporter.admin_set_id if self.parsed_metadata['admin_set_id'].blank?
93
93
  end
@@ -161,7 +161,7 @@ module Bulkrax
161
161
  # @param field [String] name of the controlled property
162
162
  # @return [Boolean] provided value is a present, active authority ID for the provided field
163
163
  def active_id_for_authority?(value, field)
164
- return false unless defined?(::Hyrax)
164
+ return false unless Bulkrax.hyrax_loaded?
165
165
  field_service = ('Hyrax::' + "#{field}_service".camelcase).constantize
166
166
  active_authority_ids = field_service.new.active_elements.map { |ae| ae['id'] }
167
167
 
@@ -37,7 +37,6 @@ module Bulkrax
37
37
  @records
38
38
  end
39
39
 
40
- # rubocop:disable Metrics/AbcSize
41
40
  def build_records
42
41
  @collections = []
43
42
  @works = []
@@ -49,11 +48,9 @@ module Bulkrax
49
48
  next unless r.key?(model_mapping)
50
49
 
51
50
  model = r[model_mapping].nil? ? "" : r[model_mapping].strip
52
- # TODO: Eventually this should be refactored to us Hyrax.config.collection_model
53
- # We aren't right now because so many Bulkrax users are in between Fedora and Valkyrie
54
- if model.casecmp('collection').zero? || model.casecmp('collectionresource').zero?
51
+ if Bulkrax.collection_model_name?(model)
55
52
  @collections << r
56
- elsif model.casecmp('fileset').zero? || model.casecmp('hyrax::fileset').zero?
53
+ elsif Bulkrax.file_model_name?(model)
57
54
  @file_sets << r
58
55
  else
59
56
  @works << r
@@ -69,7 +66,6 @@ module Bulkrax
69
66
 
70
67
  true
71
68
  end
72
- # rubocop:enabled Metrics/AbcSize
73
69
 
74
70
  def collections
75
71
  build_records if @collections.nil?
@@ -372,12 +368,17 @@ module Bulkrax
372
368
  raise StandardError, "Record references local files but no files directory could be resolved from the import path" if files_dir.nil?
373
369
 
374
370
  r[file_mapping].split(split_pattern).map do |f|
375
- file = File.join(files_dir, f.strip.tr(' ', '_'))
376
- if File.exist?(file) # rubocop:disable Style/GuardClause
377
- file
378
- else
379
- raise "File #{file} does not exist"
380
- end
371
+ reference = f.strip
372
+ # Try the reference as written first, then the space->underscore
373
+ # variant produced by #remove_spaces_from_filenames. The rename only
374
+ # rewrites the top level of files/ (see unzip_spec), so nested
375
+ # attachments keep their spaces on disk and only match as written.
376
+ file = [reference, reference.tr(' ', '_')].uniq
377
+ .map { |candidate| File.join(files_dir, candidate) }
378
+ .find { |candidate| File.exist?(candidate) }
379
+ raise "File #{File.join(files_dir, reference)} does not exist" if file.nil?
380
+
381
+ file
381
382
  end
382
383
  end.flatten.compact.uniq
383
384
  end
@@ -71,7 +71,7 @@ module Bulkrax
71
71
  metadata = {
72
72
  visibility: 'open'
73
73
  }
74
- metadata[:collection_type_gid] = Hyrax::CollectionType.find_or_create_default_collection_type.to_global_id.to_s if defined?(::Hyrax)
74
+ metadata[:collection_type_gid] = Hyrax::CollectionType.find_or_create_default_collection_type.to_global_id.to_s if Bulkrax.hyrax_loaded?
75
75
 
76
76
  collections.each_with_index do |set, index|
77
77
  next unless collection_name == 'all' || collection_name == set.spec
@@ -120,7 +120,10 @@ module Bulkrax
120
120
  #
121
121
  # No sense attempting to query for more than the limit.
122
122
  def query_kwargs
123
- { fl: "id,#{Bulkrax.solr_key_for_member_file_ids}", method: :post, rows: row_limit }
123
+ base = { fl: "id,#{Bulkrax.solr_key_for_member_file_ids}", method: :post, rows: row_limit }
124
+ filters = permission_filters
125
+ return base if filters.empty?
126
+ base.merge(fq: filters)
124
127
  end
125
128
 
126
129
  # If we have a limit, we need not query beyond that limit
@@ -175,10 +178,25 @@ module Bulkrax
175
178
  @file_sets ||= ParserExportRecordSet.in_batches(candidate_file_set_ids) do |batch_of_ids|
176
179
  fsq = "has_model_ssim:\"#{Bulkrax.file_model_internal_resource.demodulize}\" AND id:(\"" + batch_of_ids.join('" OR "') + "\")"
177
180
  fsq += extra_filters if extra_filters.present?
178
- Bulkrax.object_factory.query(
179
- fsq,
180
- fl: "id", method: :post, rows: batch_of_ids.size
181
- )
181
+ fq = permission_filters
182
+ kwargs = { fl: "id", method: :post, rows: batch_of_ids.size }
183
+ kwargs[:fq] = fq unless fq.empty?
184
+ Bulkrax.object_factory.query(fsq, **kwargs)
185
+ end
186
+ end
187
+
188
+ def exporting_user
189
+ importerexporter.user || Bulkrax.fallback_user_for_importer_exporter_processing
190
+ end
191
+
192
+ def permission_filters
193
+ @permission_filters ||= begin
194
+ ability = Ability.new(exporting_user)
195
+ return [] if ability.can_admin_exporters?
196
+
197
+ scope = Bulkrax::ExportScope.new(ability)
198
+ builder = Bulkrax::ExportSearchBuilder.new(scope)
199
+ builder.to_h.fetch(:fq, [])
182
200
  end
183
201
  end
184
202
 
@@ -246,7 +264,7 @@ module Bulkrax
246
264
  Bulkrax.object_factory.query(
247
265
  extra_filters.to_s,
248
266
  **query_kwargs.merge(
249
- fq: [
267
+ fq: (query_kwargs[:fq] || []) + [
250
268
  %(#{solr_name(work_identifier)}:("#{ids.join('" OR "')}")),
251
269
  "has_model_ssim:(#{Bulkrax.curation_concern_internal_resources.join(' OR ')})"
252
270
  ],
@@ -261,7 +279,7 @@ module Bulkrax
261
279
  Bulkrax.object_factory.query(
262
280
  "has_model_ssim:#{Bulkrax.collection_model_internal_resource} #{extra_filters}",
263
281
  **query_kwargs.merge(
264
- fq: [
282
+ fq: (query_kwargs[:fq] || []) + [
265
283
  %(#{solr_name(work_identifier)}:("#{ids.join('" OR "')}")),
266
284
  "has_model_ssim:#{Bulkrax.collection_model_internal_resource}"
267
285
  ],
@@ -280,7 +298,7 @@ module Bulkrax
280
298
  Bulkrax.object_factory.query(
281
299
  extra_filters,
282
300
  **query_kwargs.merge(
283
- fq: [
301
+ fq: (query_kwargs[:fq] || []) + [
284
302
  %(#{solr_name(work_identifier)}:("#{ids.join('" OR "')}")),
285
303
  "has_model_ssim:#{Bulkrax.file_model_internal_resource}"
286
304
  ],
@@ -14,7 +14,7 @@ module Bulkrax
14
14
  # @param args [Hash] Additional arguments passed to output method (e.g., file_path)
15
15
  # @return [String] File path (for 'file' output) or CSV string (for 'csv_string' output)
16
16
  def generate_template(models: [], output: 'file', admin_set_id: nil, **args)
17
- raise NameError, "Hyrax is not defined" unless defined?(::Hyrax)
17
+ raise NameError, "Hyrax is not defined" unless Bulkrax.hyrax_loaded?
18
18
  TemplateContext.new(models: models, admin_set_id: admin_set_id).send("to_#{output}", **args)
19
19
  end
20
20
  end
@@ -71,8 +71,7 @@ module Bulkrax
71
71
  file_key = resolve_validation_key(mapping_manager, key: 'file', default: :file)
72
72
 
73
73
  csv_data = parse_validation_rows(raw_csv, source_id_key, parent_key, children_key, file_key)
74
- all_models = csv_data.map { |r| r[:model].to_s }.reject(&:blank?).uniq
75
- all_models |= [Bulkrax.default_work_type] if Bulkrax.default_work_type.present?
74
+ all_models = models_to_validate(csv_data, headers)
76
75
  field_analyzer = CsvTemplate::FieldAnalyzer.new(mappings, admin_set_id)
77
76
  field_metadata = build_validation_field_metadata(all_models, field_analyzer)
78
77
 
@@ -64,7 +64,7 @@ module Bulkrax
64
64
  Rails.logger.error("CsvParser.validate_csv: error building valid headers – #{e.message}")
65
65
  standard = %w[model source_identifier parents children file]
66
66
  model_fields = field_metadata.values.flat_map { |m| m[:properties] }
67
- .map { |prop| mapping_manager.key_to_mapped_column(prop) }
67
+ model_fields = model_fields.map { |prop| mapping_manager.key_to_mapped_column(prop) }
68
68
  (standard + model_fields).uniq
69
69
  end
70
70
 
@@ -129,6 +129,23 @@ module Bulkrax
129
129
  all_models.each { |model| missing_required << { model: model, field: source_id_key.to_s } }
130
130
  end
131
131
 
132
+ # Adding the default work type unconditionally makes every sheet answer
133
+ # for that model's required fields, so a collection-only sheet gets asked
134
+ # for properties belonging to a work type it never mentions.
135
+ def models_to_validate(csv_data, headers)
136
+ row_models = csv_data.map { |r| r[:model].to_s }.reject(&:blank?).uniq
137
+ return row_models if Bulkrax.default_work_type.blank?
138
+ return row_models unless default_work_type_needed?(csv_data, headers)
139
+
140
+ row_models | [Bulkrax.default_work_type]
141
+ end
142
+
143
+ def default_work_type_needed?(csv_data, headers)
144
+ return true unless headers.map(&:to_s).include?('model')
145
+
146
+ csv_data.empty? || csv_data.any? { |r| r[:model].blank? }
147
+ end
148
+
132
149
  # Adds a file-level notice when the model column is absent or every row has a blank
133
150
  # model value, indicating that the default work type will be used for all rows.
134
151
  # When this notice is present the per-row default_work_type_used warnings are
@@ -36,9 +36,9 @@ module Bulkrax
36
36
  model_str = item[:model].to_s
37
37
 
38
38
  opts = { type: nil, find_record: find_record, parent: parent_split_pattern, child: child_split_pattern }
39
- if model_str.casecmp('collection').zero? || model_str.casecmp('collectionresource').zero?
39
+ if Bulkrax.collection_model_name?(model_str)
40
40
  collections << build_item_hash(item, child_to_parents, all_ids, opts.merge(type: 'collection'))
41
- elsif model_str.casecmp('fileset').zero? || model_str.casecmp('hyrax::fileset').zero?
41
+ elsif Bulkrax.file_model_name?(model_str)
42
42
  file_sets << { id: item_id, title: item[:raw_row]['title'] || item_id, type: 'file_set' }
43
43
  else
44
44
  works << build_item_hash(item, child_to_parents, all_ids, opts.merge(type: 'work'))
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bulkrax
4
+ class ExportSearchBuilder < Blacklight::SearchBuilder
5
+ include Hydra::AccessControlsEnforcement
6
+
7
+ self.default_processor_chain = [:add_access_controls_to_solr_params]
8
+ end
9
+ end
@@ -43,8 +43,8 @@ module Bulkrax
43
43
 
44
44
  # When a property is the target of one or more `object:` field mappings,
45
45
  # emit each of those mappings' `from:` columns (e.g. redirect_path,
46
- # redirect_canonical, redirect_sequence) rather than the bare property
47
- # name (redirects). Otherwise fall back to the standard 1:1 mapping.
46
+ # redirect_display_url) rather than the bare property name (redirects).
47
+ # Otherwise fall back to the standard 1:1 mapping.
48
48
  def columns_for_property(property)
49
49
  nested = @service.mapping_manager.object_columns_for(property)
50
50
  return nested if nested.any?
@@ -35,16 +35,18 @@ module Bulkrax
35
35
 
36
36
  # Returns the column names that target a given object name via the
37
37
  # `object:` field-mapping pattern. The template generator uses this to
38
- # emit the per-child columns (e.g. redirect_path, redirect_canonical,
39
- # redirect_sequence) instead of the bare property name (redirects).
40
- # Numbering is intentionally omitted — the template shows the column
41
- # shape once; CSV rows can repeat the column with numeric suffixes
42
- # (e.g. redirect_path_1, redirect_path_2) at import time.
38
+ # emit the per-child columns (e.g. redirect_path, redirect_display_url)
39
+ # instead of the bare property name (redirects). Numbering is
40
+ # intentionally omitted — the template shows the column shape once;
41
+ # CSV rows can repeat the column with numeric suffixes (e.g.
42
+ # redirect_path_1, redirect_path_2) at import time.
43
+ # An import accepts every `from:` alias, but offering them all here would
44
+ # put two columns for the same field on the template.
43
45
  def object_columns_for(object_name)
44
46
  @mappings
45
47
  .select { |_k, v| v.is_a?(Hash) && v["object"] == object_name }
46
- .values
47
- .flat_map { |v| Array(v["from"]) }
48
+ .keys
49
+ .map { |key| key_to_mapped_column(key) }
48
50
  .uniq
49
51
  end
50
52