hyrax 5.2.0 → 5.3.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 (527) hide show
  1. checksums.yaml +4 -4
  2. data/.dassie/.env +5 -1
  3. data/.dassie/Gemfile +4 -2
  4. data/.dassie/app/assets/images/bulkrax/removed.png +0 -0
  5. data/.dassie/app/assets/javascripts/application.js +2 -0
  6. data/.dassie/app/assets/stylesheets/application.css +1 -0
  7. data/.dassie/app/controllers/catalog_controller.rb +1 -0
  8. data/.dassie/app/forms/admin_set_resource_form.rb +6 -0
  9. data/.dassie/app/forms/collection_resource_form.rb +5 -2
  10. data/.dassie/app/forms/generic_work_resource_form.rb +5 -2
  11. data/.dassie/app/forms/monograph_form.rb +5 -3
  12. data/.dassie/app/indexers/admin_set_resource_indexer.rb +5 -0
  13. data/.dassie/app/indexers/collection_resource_indexer.rb +3 -3
  14. data/.dassie/app/indexers/generic_work_resource_indexer.rb +6 -3
  15. data/.dassie/app/indexers/monograph_indexer.rb +6 -2
  16. data/.dassie/app/models/ability.rb +8 -0
  17. data/.dassie/app/models/collection_resource.rb +4 -2
  18. data/.dassie/app/models/concerns/bulkrax/has_local_processing.rb +8 -0
  19. data/.dassie/app/models/generic_work_resource.rb +5 -2
  20. data/.dassie/app/models/monograph.rb +5 -2
  21. data/.dassie/app/views/blacklight_dynamic_sitemap/sitemap/show.xml.builder +22 -0
  22. data/.dassie/bin/importer +142 -0
  23. data/.dassie/bin/web +4 -0
  24. data/.dassie/config/browse_everything_providers.yml +1 -1
  25. data/.dassie/config/bulkrax_api.yml +86 -0
  26. data/.dassie/config/initializers/bulkrax.rb +92 -0
  27. data/.dassie/config/initializers/clamav.rb +2 -0
  28. data/.dassie/config/initializers/hyrax.rb +7 -1
  29. data/.dassie/config/initializers/riiif.rb +4 -80
  30. data/.dassie/config/initializers/wings.rb +1 -1
  31. data/.dassie/config/metadata_profiles/m3_profile.yaml +929 -0
  32. data/.dassie/config/puma.rb +4 -2
  33. data/.dassie/config/routes.rb +7 -0
  34. data/.dassie/config/sidekiq.yml +17 -0
  35. data/.dassie/config/uv/uv.html +63 -81
  36. data/.dassie/db/migrate/20240606205215_create_hyrax_flexible_schemas.rb +9 -0
  37. data/.dassie/db/migrate/20240606205216_add_contexts_to_hyrax_flexible_schemas.rb +5 -0
  38. data/.dassie/db/migrate/20250826142438_create_bulkrax_importers.bulkrax.rb +19 -0
  39. data/.dassie/db/migrate/20250826142439_create_bulkrax_importer_runs.bulkrax.rb +17 -0
  40. data/.dassie/db/migrate/20250826142440_create_bulkrax_entries.bulkrax.rb +17 -0
  41. data/.dassie/db/migrate/20250826142441_add_status_to_entry.bulkrax.rb +10 -0
  42. data/.dassie/db/migrate/20250826142442_add_collections_to_importer_runs.bulkrax.rb +7 -0
  43. data/.dassie/db/migrate/20250826142443_change_collection_ids_on_entries.bulkrax.rb +6 -0
  44. data/.dassie/db/migrate/20250826142444_create_bulkrax_exporters.bulkrax.rb +20 -0
  45. data/.dassie/db/migrate/20250826142445_create_bulkrax_exporter_runs.bulkrax.rb +15 -0
  46. data/.dassie/db/migrate/20250826142446_change_importer_and_exporter_to_polymorphic.bulkrax.rb +19 -0
  47. data/.dassie/db/migrate/20250826142447_add_total_collection_records_to_importer_runs.bulkrax.rb +6 -0
  48. data/.dassie/db/migrate/20250826142448_add_children_to_importer_runs.bulkrax.rb +7 -0
  49. data/.dassie/db/migrate/20250826142449_change_total_records_to_total_work_entries.bulkrax.rb +7 -0
  50. data/.dassie/db/migrate/20250826142450_change_entry_last_error.bulkrax.rb +20 -0
  51. data/.dassie/db/migrate/20250826142451_add_validate_only_to_bulkrax_importers.bulkrax.rb +6 -0
  52. data/.dassie/db/migrate/20250826142452_add_status_to_importers.bulkrax.rb +10 -0
  53. data/.dassie/db/migrate/20250826142453_remove_foreign_key_from_bulkrax_entries.bulkrax.rb +6 -0
  54. data/.dassie/db/migrate/20250826142454_add_status_to_exporters.bulkrax.rb +8 -0
  55. data/.dassie/db/migrate/20250826142455_add_invalid_record_to_importer_run.bulkrax.rb +6 -0
  56. data/.dassie/db/migrate/20250826142456_create_bulkrax_statuses.bulkrax.rb +19 -0
  57. data/.dassie/db/migrate/20250826142457_move_to_statuses.bulkrax.rb +31 -0
  58. data/.dassie/db/migrate/20250826142458_add_date_filter_and_status_to_bulkrax_exporters.bulkrax.rb +8 -0
  59. data/.dassie/db/migrate/20250826142459_add_workflow_status_to_bulkrax_exporter.bulkrax.rb +6 -0
  60. data/.dassie/db/migrate/20250826142460_remove_unused_last_error.bulkrax.rb +8 -0
  61. data/.dassie/db/migrate/20250826142461_increase_text_sizes.bulkrax.rb +13 -0
  62. data/.dassie/db/migrate/20250826142462_change_bulkrax_statuses_error_message_column_type_to_text.bulkrax.rb +6 -0
  63. data/.dassie/db/migrate/20250826142463_rename_children_counters_to_relationships.bulkrax.rb +7 -0
  64. data/.dassie/db/migrate/20250826142464_add_file_set_counters_to_importer_runs.bulkrax.rb +8 -0
  65. data/.dassie/db/migrate/20250826142465_add_import_attempts_to_entries.bulkrax.rb +6 -0
  66. data/.dassie/db/migrate/20250826142466_add_work_counters_to_importer_runs.bulkrax.rb +7 -0
  67. data/.dassie/db/migrate/20250826142467_create_bulkrax_pending_relationships.bulkrax.rb +14 -0
  68. data/.dassie/db/migrate/20250826142468_add_order_to_bulkrax_pending_relationships.bulkrax.rb +6 -0
  69. data/.dassie/db/migrate/20250826142469_add_include_thumbnails_to_bulkrax_exporters.bulkrax.rb +6 -0
  70. data/.dassie/db/migrate/20250826142470_add_generated_metadata_to_bulkrax_exporters.bulkrax.rb +6 -0
  71. data/.dassie/db/migrate/20250826142471_rename_bulkrax_importer_run_to_importer_run.bulkrax.rb +18 -0
  72. data/.dassie/db/migrate/20250826142472_add_indices_to_bulkrax.bulkrax.rb +26 -0
  73. data/.dassie/db/migrate/20250826142473_denormalize_status_message.bulkrax.rb +8 -0
  74. data/.dassie/db/migrate/20250826142474_update_identifier_index.bulkrax.rb +7 -0
  75. data/.dassie/db/migrate/20250826142475_add_index_to_metadata_bulkrax_identifier.bulkrax.rb +19 -0
  76. data/.dassie/db/migrate/20250826142476_add_file_name_to_uploaded_files.bulkrax.rb +6 -0
  77. data/.dassie/db/migrate/20250826142477_add_error_tracking_to_pending_relationships.bulkrax.rb +6 -0
  78. data/.dassie/db/migrate/20250826142478_add_last_imported_at_to_bulkrax_importers.bulkrax.rb +6 -0
  79. data/.dassie/db/migrate/20250826142479_add_next_import_at_to_bulkrax_importers.bulkrax.rb +6 -0
  80. data/.dassie/db/migrate/20250826142480_entry_error_denormalization.bulkrax.rb +8 -0
  81. data/.dassie/db/migrate/20250826142481_faster_first_entry.bulkrax.rb +8 -0
  82. data/.dassie/db/migrate/20260430000001_create_hyrax_redirect_paths.hyrax.rb +13 -0
  83. data/.dassie/db/migrate/20260521003627_add_additional_redirect_info.hyrax.rb +32 -0
  84. data/.dassie/db/schema.rb +154 -1
  85. data/.dassie/package.json +1 -1
  86. data/.dassie/rspec_split_configuration.json +835 -0
  87. data/.dassie/solr/conf/solrconfig.xml +21 -5
  88. data/.github/{ISSUE_TEMPLATE.md → ISSUE_TEMPLATE/issue_template.md} +5 -0
  89. data/.github/workflows/lint-build-test.yml +5 -1
  90. data/.github/workflows/release-charts.yml +48 -0
  91. data/.gitignore +6 -0
  92. data/.koppie/.env +5 -1
  93. data/.koppie/Gemfile +3 -1
  94. data/.koppie/app/assets/images/bulkrax/removed.png +0 -0
  95. data/.koppie/app/assets/javascripts/application.js +2 -0
  96. data/.koppie/app/assets/stylesheets/application.css +1 -0
  97. data/.koppie/app/controllers/catalog_controller.rb +1 -1
  98. data/.koppie/app/forms/collection_resource_form.rb +5 -2
  99. data/.koppie/app/forms/file_set_form.rb +5 -0
  100. data/.koppie/app/forms/generic_work_form.rb +5 -3
  101. data/.koppie/app/forms/monograph_form.rb +5 -3
  102. data/.koppie/app/indexers/collection_resource_indexer.rb +5 -2
  103. data/.koppie/app/indexers/file_set_indexer.rb +7 -0
  104. data/.koppie/app/indexers/generic_work_indexer.rb +6 -3
  105. data/.koppie/app/indexers/monograph_indexer.rb +6 -2
  106. data/.koppie/app/models/ability.rb +8 -0
  107. data/.koppie/app/models/collection_resource.rb +4 -2
  108. data/.koppie/app/models/concerns/bulkrax/has_local_processing.rb +8 -0
  109. data/.koppie/app/models/generic_work.rb +5 -2
  110. data/.koppie/app/models/monograph.rb +5 -2
  111. data/.koppie/app/views/blacklight_dynamic_sitemap/sitemap/show.xml.builder +22 -0
  112. data/.koppie/bin/importer +142 -0
  113. data/.koppie/bin/web +4 -0
  114. data/.koppie/config/browse_everything_providers.yml +2 -0
  115. data/.koppie/config/bulkrax_api.yml +86 -0
  116. data/.koppie/config/initializers/bulkrax.rb +92 -0
  117. data/.koppie/config/initializers/clamav.rb +2 -0
  118. data/.koppie/config/initializers/hyrax.rb +11 -3
  119. data/.koppie/config/initializers/riiif.rb +4 -82
  120. data/.koppie/config/metadata_profiles/m3_profile.yaml +930 -0
  121. data/.koppie/config/routes.rb +7 -0
  122. data/.koppie/config/sidekiq.yml +2 -0
  123. data/.koppie/config/uv/uv.html +63 -81
  124. data/.koppie/db/migrate/20240606205215_create_hyrax_flexible_schemas.rb +9 -0
  125. data/.koppie/db/migrate/20240606205216_add_contexts_to_hyrax_flexible_schemas.rb +5 -0
  126. data/.koppie/db/migrate/20250826142438_create_bulkrax_importers.bulkrax.rb +19 -0
  127. data/.koppie/db/migrate/20250826142439_create_bulkrax_importer_runs.bulkrax.rb +17 -0
  128. data/.koppie/db/migrate/20250826142440_create_bulkrax_entries.bulkrax.rb +17 -0
  129. data/.koppie/db/migrate/20250826142441_add_status_to_entry.bulkrax.rb +10 -0
  130. data/.koppie/db/migrate/20250826142442_add_collections_to_importer_runs.bulkrax.rb +7 -0
  131. data/.koppie/db/migrate/20250826142443_change_collection_ids_on_entries.bulkrax.rb +6 -0
  132. data/.koppie/db/migrate/20250826142444_create_bulkrax_exporters.bulkrax.rb +20 -0
  133. data/.koppie/db/migrate/20250826142445_create_bulkrax_exporter_runs.bulkrax.rb +15 -0
  134. data/.koppie/db/migrate/20250826142446_change_importer_and_exporter_to_polymorphic.bulkrax.rb +19 -0
  135. data/.koppie/db/migrate/20250826142447_add_total_collection_records_to_importer_runs.bulkrax.rb +6 -0
  136. data/.koppie/db/migrate/20250826142448_add_children_to_importer_runs.bulkrax.rb +7 -0
  137. data/.koppie/db/migrate/20250826142449_change_total_records_to_total_work_entries.bulkrax.rb +7 -0
  138. data/.koppie/db/migrate/20250826142450_change_entry_last_error.bulkrax.rb +20 -0
  139. data/.koppie/db/migrate/20250826142451_add_validate_only_to_bulkrax_importers.bulkrax.rb +6 -0
  140. data/.koppie/db/migrate/20250826142452_add_status_to_importers.bulkrax.rb +10 -0
  141. data/.koppie/db/migrate/20250826142453_remove_foreign_key_from_bulkrax_entries.bulkrax.rb +6 -0
  142. data/.koppie/db/migrate/20250826142454_add_status_to_exporters.bulkrax.rb +8 -0
  143. data/.koppie/db/migrate/20250826142455_add_invalid_record_to_importer_run.bulkrax.rb +6 -0
  144. data/.koppie/db/migrate/20250826142456_create_bulkrax_statuses.bulkrax.rb +19 -0
  145. data/.koppie/db/migrate/20250826142457_move_to_statuses.bulkrax.rb +31 -0
  146. data/.koppie/db/migrate/20250826142458_add_date_filter_and_status_to_bulkrax_exporters.bulkrax.rb +8 -0
  147. data/.koppie/db/migrate/20250826142459_add_workflow_status_to_bulkrax_exporter.bulkrax.rb +6 -0
  148. data/.koppie/db/migrate/20250826142460_remove_unused_last_error.bulkrax.rb +8 -0
  149. data/.koppie/db/migrate/20250826142461_increase_text_sizes.bulkrax.rb +13 -0
  150. data/.koppie/db/migrate/20250826142462_change_bulkrax_statuses_error_message_column_type_to_text.bulkrax.rb +6 -0
  151. data/.koppie/db/migrate/20250826142463_rename_children_counters_to_relationships.bulkrax.rb +7 -0
  152. data/.koppie/db/migrate/20250826142464_add_file_set_counters_to_importer_runs.bulkrax.rb +8 -0
  153. data/.koppie/db/migrate/20250826142465_add_import_attempts_to_entries.bulkrax.rb +6 -0
  154. data/.koppie/db/migrate/20250826142466_add_work_counters_to_importer_runs.bulkrax.rb +7 -0
  155. data/.koppie/db/migrate/20250826142467_create_bulkrax_pending_relationships.bulkrax.rb +14 -0
  156. data/.koppie/db/migrate/20250826142468_add_order_to_bulkrax_pending_relationships.bulkrax.rb +6 -0
  157. data/.koppie/db/migrate/20250826142469_add_include_thumbnails_to_bulkrax_exporters.bulkrax.rb +6 -0
  158. data/.koppie/db/migrate/20250826142470_add_generated_metadata_to_bulkrax_exporters.bulkrax.rb +6 -0
  159. data/.koppie/db/migrate/20250826142471_rename_bulkrax_importer_run_to_importer_run.bulkrax.rb +18 -0
  160. data/.koppie/db/migrate/20250826142472_add_indices_to_bulkrax.bulkrax.rb +26 -0
  161. data/.koppie/db/migrate/20250826142473_denormalize_status_message.bulkrax.rb +8 -0
  162. data/.koppie/db/migrate/20250826142474_update_identifier_index.bulkrax.rb +7 -0
  163. data/.koppie/db/migrate/20250826142475_add_index_to_metadata_bulkrax_identifier.bulkrax.rb +19 -0
  164. data/.koppie/db/migrate/20250826142476_add_file_name_to_uploaded_files.bulkrax.rb +6 -0
  165. data/.koppie/db/migrate/20250826142477_add_error_tracking_to_pending_relationships.bulkrax.rb +6 -0
  166. data/.koppie/db/migrate/20250826142478_add_last_imported_at_to_bulkrax_importers.bulkrax.rb +6 -0
  167. data/.koppie/db/migrate/20250826142479_add_next_import_at_to_bulkrax_importers.bulkrax.rb +6 -0
  168. data/.koppie/db/migrate/20250826142480_entry_error_denormalization.bulkrax.rb +8 -0
  169. data/.koppie/db/migrate/20250826142481_faster_first_entry.bulkrax.rb +8 -0
  170. data/.koppie/db/migrate/20260430000001_create_hyrax_redirect_paths.hyrax.rb +13 -0
  171. data/.koppie/db/migrate/20260521003627_add_additional_redirect_info.hyrax.rb +32 -0
  172. data/.koppie/db/schema.rb +154 -1
  173. data/.koppie/package.json +1 -1
  174. data/.koppie/rspec_split_configuration.json +835 -0
  175. data/.koppie/solr/conf/solrconfig.xml +0 -5
  176. data/.koppie/yarn.lock +5587 -752
  177. data/.rubocop.yml +1 -0
  178. data/CONTAINERS.md +2 -0
  179. data/Dockerfile +9 -22
  180. data/Gemfile +0 -1
  181. data/Gemfile.allinson +2 -0
  182. data/README.md +1 -2
  183. data/app/actors/hyrax/actors/file_actor.rb +2 -0
  184. data/app/actors/hyrax/actors/file_set_actor.rb +1 -1
  185. data/app/assets/javascripts/hyrax/app.js.erb +143 -10
  186. data/app/assets/javascripts/hyrax/collection_select.js +68 -0
  187. data/app/assets/javascripts/hyrax/copy_permalink_button.js +28 -0
  188. data/app/assets/javascripts/hyrax/redirects.js +45 -0
  189. data/app/assets/javascripts/hyrax/relationships/control.es6 +21 -1
  190. data/app/assets/javascripts/hyrax/select_work_type.es6 +7 -2
  191. data/app/assets/javascripts/hyrax.js +3 -1
  192. data/app/assets/stylesheets/hyrax/_file-show.scss +1 -0
  193. data/app/assets/stylesheets/hyrax/_viewer.scss +8 -0
  194. data/app/assets/stylesheets/hyrax/dashboard.scss +22 -1
  195. data/app/assets/stylesheets/hyrax/sidebar.scss +98 -3
  196. data/app/authorities/qa/authorities/collections.rb +9 -9
  197. data/app/controllers/concerns/hyrax/analytics_error_handling.rb +97 -0
  198. data/app/controllers/concerns/hyrax/collections_controller_behavior.rb +3 -1
  199. data/app/controllers/concerns/hyrax/ensure_migrated_behavior.rb +50 -0
  200. data/app/controllers/concerns/hyrax/flexible_catalog_behavior.rb +206 -0
  201. data/app/controllers/concerns/hyrax/flexible_schema_behavior.rb +17 -0
  202. data/app/controllers/concerns/hyrax/redirect_to_display_url.rb +54 -0
  203. data/app/controllers/concerns/hyrax/works_controller_behavior.rb +52 -10
  204. data/app/controllers/hyrax/admin/admin_sets_controller.rb +21 -13
  205. data/app/controllers/hyrax/admin/analytics/collection_reports_controller.rb +2 -0
  206. data/app/controllers/hyrax/admin/analytics/work_reports_controller.rb +1 -0
  207. data/app/controllers/hyrax/admin/metadata_profiles_controller.rb +66 -0
  208. data/app/controllers/hyrax/batch_uploads_controller.rb +2 -0
  209. data/app/controllers/hyrax/dashboard/collections_controller.rb +27 -6
  210. data/app/controllers/hyrax/dashboard/nest_collections_controller.rb +2 -1
  211. data/app/controllers/hyrax/dashboard_controller.rb +2 -1
  212. data/app/controllers/hyrax/file_sets_controller.rb +19 -1
  213. data/app/controllers/hyrax/homepage_controller.rb +7 -0
  214. data/app/controllers/hyrax/my/works_controller.rb +2 -21
  215. data/app/controllers/hyrax/permissions_controller.rb +4 -2
  216. data/app/controllers/hyrax/redirects_controller.rb +56 -0
  217. data/app/controllers/hyrax/single_use_links_viewer_controller.rb +2 -1
  218. data/app/controllers/hyrax/transcripts_controller.rb +31 -0
  219. data/app/controllers/hyrax/uploads_controller.rb +18 -9
  220. data/app/forms/concerns/hyrax/based_near_field_behavior.rb +77 -0
  221. data/app/forms/concerns/hyrax/flexible_form_behavior.rb +37 -0
  222. data/app/forms/concerns/hyrax/redirects_field_behavior.rb +95 -0
  223. data/app/forms/concerns/hyrax/transcripts_behavior.rb +40 -0
  224. data/app/forms/hyrax/forms/admin_set_form.rb +1 -1
  225. data/app/forms/hyrax/forms/administrative_set_form.rb +3 -0
  226. data/app/forms/hyrax/forms/batch_edit_form.rb +1 -3
  227. data/app/forms/hyrax/forms/batch_upload_form.rb +14 -0
  228. data/app/forms/hyrax/forms/file_set_edit_form.rb +1 -1
  229. data/app/forms/hyrax/forms/file_set_form.rb +6 -7
  230. data/app/forms/hyrax/forms/pcdm_collection_form.rb +17 -5
  231. data/app/forms/hyrax/forms/pcdm_object_form.rb +11 -3
  232. data/app/forms/hyrax/forms/resource_batch_edit_form.rb +8 -22
  233. data/app/forms/hyrax/forms/resource_form.rb +110 -10
  234. data/app/forms/hyrax/forms.rb +1 -0
  235. data/app/helpers/hyrax/attributes_helper.rb +109 -0
  236. data/app/helpers/hyrax/blacklight_override.rb +26 -0
  237. data/app/helpers/hyrax/collections_helper.rb +2 -0
  238. data/app/helpers/hyrax/file_set_form_helper.rb +24 -0
  239. data/app/helpers/hyrax/hyrax_helper_behavior.rb +51 -2
  240. data/app/helpers/hyrax/iiif_helper.rb +11 -1
  241. data/app/helpers/hyrax/permalink_helper.rb +37 -0
  242. data/app/helpers/hyrax/redirects_tab_helper.rb +29 -0
  243. data/app/helpers/hyrax/work_form_helper.rb +9 -5
  244. data/app/helpers/hyrax/works_helper.rb +14 -0
  245. data/app/indexers/concerns/hyrax/workflow_indexer.rb +30 -0
  246. data/app/indexers/hyrax/file_set_indexer.rb +1 -0
  247. data/app/indexers/hyrax/indexers/administrative_set_indexer.rb +2 -1
  248. data/app/indexers/hyrax/indexers/file_set_indexer.rb +6 -2
  249. data/app/indexers/hyrax/indexers/pcdm_collection_indexer.rb +11 -1
  250. data/app/indexers/hyrax/indexers/pcdm_object_indexer.rb +25 -4
  251. data/app/indexers/hyrax/indexers/redirects_indexer.rb +29 -0
  252. data/app/indexers/hyrax/indexers/resource_indexer.rb +4 -0
  253. data/app/indexers/hyrax/indexers.rb +2 -0
  254. data/app/inputs/controlled_vocabulary_input.rb +7 -1
  255. data/app/jobs/content_deposit_error_event_job.rb +14 -0
  256. data/app/jobs/create_work_job.rb +2 -0
  257. data/app/jobs/migrate_files_to_valkyrie_job.rb +7 -1
  258. data/app/models/batch_upload_item.rb +4 -0
  259. data/app/models/concerns/hyrax/ability/flexible_metadata_ability.rb +10 -0
  260. data/app/models/concerns/hyrax/ability.rb +3 -1
  261. data/app/models/concerns/hyrax/collection_behavior.rb +4 -0
  262. data/app/models/concerns/hyrax/file_set/transcripts.rb +16 -0
  263. data/app/models/concerns/hyrax/file_set_behavior.rb +9 -0
  264. data/app/models/concerns/hyrax/flexibility.rb +107 -0
  265. data/app/models/concerns/hyrax/redirects_normalization.rb +38 -0
  266. data/app/models/concerns/hyrax/solr_document/characterization.rb +1 -1
  267. data/app/models/concerns/hyrax/solr_document/metadata.rb +9 -0
  268. data/app/models/concerns/hyrax/solr_document/ordered_members.rb +4 -0
  269. data/app/models/concerns/hyrax/solr_document_behavior.rb +28 -6
  270. data/app/models/concerns/hyrax/suppressible.rb +1 -1
  271. data/app/models/concerns/hyrax/valkyrie_lazy_migration.rb +1 -1
  272. data/app/models/concerns/hyrax/with_embargoes_and_leases.rb +30 -0
  273. data/app/models/concerns/hyrax/work_behavior.rb +8 -0
  274. data/app/models/hyrax/administrative_set.rb +1 -1
  275. data/app/models/hyrax/change_set.rb +3 -1
  276. data/app/models/hyrax/embargo.rb +3 -1
  277. data/app/models/hyrax/file_set.rb +4 -2
  278. data/app/models/hyrax/flexible_schema.rb +219 -0
  279. data/app/models/hyrax/lease.rb +3 -1
  280. data/app/models/hyrax/pcdm_collection.rb +4 -1
  281. data/app/models/hyrax/redirect.rb +64 -0
  282. data/app/models/hyrax/redirect_path.rb +14 -0
  283. data/app/models/hyrax/resource.rb +22 -25
  284. data/app/models/hyrax/riiif/file.rb +43 -0
  285. data/app/models/hyrax/riiif/file_resolver.rb +65 -0
  286. data/app/models/hyrax/uploaded_file.rb +9 -0
  287. data/app/models/hyrax/virus_scanner.rb +16 -3
  288. data/app/models/hyrax/work.rb +4 -1
  289. data/app/models/job_io_wrapper.rb +12 -3
  290. data/app/models/user_mailbox.rb +3 -1
  291. data/app/presenters/hyrax/admin_set_presenter.rb +11 -0
  292. data/app/presenters/hyrax/annotates_content.rb +35 -0
  293. data/app/presenters/hyrax/collection_presenter.rb +5 -1
  294. data/app/presenters/hyrax/displays_content.rb +133 -0
  295. data/app/presenters/hyrax/displays_image.rb +11 -4
  296. data/app/presenters/hyrax/displays_transcripts.rb +59 -0
  297. data/app/presenters/hyrax/file_set_presenter.rb +8 -18
  298. data/app/presenters/hyrax/google_scholar_presenter.rb +39 -0
  299. data/app/presenters/hyrax/iiif_manifest_presenter.rb +22 -4
  300. data/app/presenters/hyrax/missing_method_behavior.rb +15 -0
  301. data/app/presenters/hyrax/pcdm_member_presenter_factory.rb +2 -4
  302. data/app/presenters/hyrax/work_show_presenter.rb +56 -14
  303. data/app/renderers/hyrax/renderers/attribute_renderer.rb +6 -2
  304. data/app/renderers/hyrax/renderers/license_attribute_renderer.rb +10 -10
  305. data/app/renderers/hyrax/renderers/redirects_label_attribute_renderer.rb +25 -0
  306. data/app/renderers/hyrax/renderers/rights_statement_attribute_renderer.rb +10 -11
  307. data/app/search_builders/hyrax/dashboard/nested_collections_search_builder.rb +1 -5
  308. data/app/search_builders/hyrax/filter_by_type.rb +1 -1
  309. data/app/services/hyrax/action/create_valkyrie_work.rb +3 -2
  310. data/app/services/hyrax/authority_rendering_helper.rb +28 -0
  311. data/app/services/hyrax/authority_service.rb +107 -0
  312. data/app/services/hyrax/callbacks.rb +2 -2
  313. data/app/services/hyrax/collections/nested_collection_query_service.rb +9 -3
  314. data/app/services/hyrax/collections_service.rb +17 -0
  315. data/app/services/hyrax/custom_queries/find_by_property_value.rb +89 -0
  316. data/app/services/hyrax/edit_permissions_service.rb +4 -4
  317. data/app/services/hyrax/file_set_type_service.rb +12 -4
  318. data/app/services/hyrax/flexible_schema_validator_service.rb +153 -0
  319. data/app/services/hyrax/flexible_schema_validators/class_validator.rb +123 -0
  320. data/app/services/hyrax/flexible_schema_validators/core_metadata_validator.rb +211 -0
  321. data/app/services/hyrax/flexible_schema_validators/existing_records_validator.rb +83 -0
  322. data/app/services/hyrax/flexible_schema_validators/redirects_validator.rb +116 -0
  323. data/app/services/hyrax/flexible_schema_validators/schema_validator.rb +34 -0
  324. data/app/services/hyrax/flexible_schema_validators/sort_properties_validator.rb +54 -0
  325. data/app/services/hyrax/license_service.rb +1 -1
  326. data/app/services/hyrax/listeners/metadata_index_listener.rb +11 -0
  327. data/app/services/hyrax/m3_profile_editor.rb +32 -0
  328. data/app/services/hyrax/m3_schema_loader.rb +119 -0
  329. data/app/services/hyrax/manifest_builder_service.rb +18 -24
  330. data/app/services/hyrax/permalink_path.rb +25 -0
  331. data/app/services/hyrax/qa_select_service.rb +9 -5
  332. data/app/services/hyrax/redirect_path_normalizer.rb +58 -0
  333. data/app/services/hyrax/redirects_lookup.rb +59 -0
  334. data/app/services/hyrax/resource_permissions_visibility_propagator.rb +54 -0
  335. data/app/services/hyrax/resource_types_service.rb +3 -20
  336. data/app/services/hyrax/rights_statement_service.rb +1 -1
  337. data/app/services/hyrax/schema_loader.rb +205 -0
  338. data/app/services/hyrax/search_service.rb +1 -1
  339. data/app/services/hyrax/simple_schema_loader.rb +28 -127
  340. data/app/services/hyrax/solr_service.rb +39 -2
  341. data/app/services/hyrax/tolerant_select_service.rb +5 -3
  342. data/app/services/hyrax/valkyrie_upload.rb +2 -2
  343. data/app/services/hyrax/visibility_propagator.rb +4 -1
  344. data/app/services/hyrax/work_uploads_handler.rb +17 -9
  345. data/app/services/hyrax/workflow/workflow_factory.rb +5 -4
  346. data/app/services/migrate_resource_service.rb +7 -1
  347. data/app/uploaders/hyrax/uploaded_file_uploader.rb +2 -2
  348. data/app/utils/hyrax/data_destroyers/collection_branding_destroyer.rb +1 -1
  349. data/app/utils/hyrax/required_data_seeder.rb +1 -0
  350. data/app/utils/hyrax/required_data_seeders/collection_type_seeder.rb +1 -1
  351. data/app/utils/hyrax/required_data_seeders/flexible_profile_seeder.rb +28 -0
  352. data/app/validators/hyrax/controlled_vocabulary_validator.rb +64 -0
  353. data/app/validators/hyrax/redirect_validator.rb +128 -0
  354. data/app/views/catalog/_thumbnail_list_default.html.erb +2 -2
  355. data/app/views/collections/edit_fields/_schema_version.html.erb +1 -0
  356. data/app/views/hyrax/admin/admin_sets/_form_metadata.html.erb +24 -2
  357. data/app/views/hyrax/admin/admin_sets/_show_document_list_row.html.erb +1 -1
  358. data/app/views/hyrax/admin/admin_sets/show.html.erb +1 -1
  359. data/app/views/hyrax/admin/analytics/_analytics_error.html.erb +42 -0
  360. data/app/views/hyrax/admin/analytics/collection_reports/index.html.erb +11 -9
  361. data/app/views/hyrax/admin/analytics/work_reports/index.html.erb +60 -57
  362. data/app/views/hyrax/admin/features/index.html.erb +1 -1
  363. data/app/views/hyrax/admin/metadata_profiles/_import_modal.html.erb +20 -0
  364. data/app/views/hyrax/admin/metadata_profiles/index.html.erb +54 -0
  365. data/app/views/hyrax/base/_attribute_rows.html.erb +4 -21
  366. data/app/views/hyrax/base/_citations.html.erb +1 -0
  367. data/app/views/hyrax/base/_currently_shared.html.erb +2 -1
  368. data/app/views/hyrax/base/_file_manager_thumbnail.html.erb +1 -1
  369. data/app/views/hyrax/base/_form_member_of_collections.html.erb +6 -6
  370. data/app/views/hyrax/base/_form_redirects.html.erb +125 -0
  371. data/app/views/hyrax/base/_form_share.html.erb +7 -2
  372. data/app/views/hyrax/base/_member.html.erb +1 -1
  373. data/app/views/hyrax/base/_social_media.html.erb +1 -1
  374. data/app/views/hyrax/base/iiif_viewers/_universal_viewer.html.erb +1 -1
  375. data/app/views/hyrax/base/show.html.erb +3 -0
  376. data/app/views/hyrax/collections/_default_group.html.erb +3 -0
  377. data/app/views/hyrax/collections/_list_collections.html.erb +3 -0
  378. data/app/views/hyrax/collections/_list_works.html.erb +1 -1
  379. data/app/views/hyrax/collections/_show_document_list_row.html.erb +1 -1
  380. data/app/views/hyrax/collections/show.html.erb +4 -0
  381. data/app/views/hyrax/dashboard/_sidebar.html.erb +4 -2
  382. data/app/views/hyrax/dashboard/collections/_button_for_update_collection.html.erb +2 -1
  383. data/app/views/hyrax/dashboard/collections/_default_group.html.erb +3 -0
  384. data/app/views/hyrax/dashboard/collections/_form.html.erb +17 -0
  385. data/app/views/hyrax/dashboard/collections/_form_for_select_collection.html.erb +7 -10
  386. data/app/views/hyrax/dashboard/collections/_list_collections.html.erb +3 -0
  387. data/app/views/hyrax/dashboard/collections/_list_works.html.erb +1 -1
  388. data/app/views/hyrax/dashboard/collections/_show_document_list_row.html.erb +1 -1
  389. data/app/views/hyrax/dashboard/profiles/_edit_primary.html.erb +1 -8
  390. data/app/views/hyrax/dashboard/profiles/_trophy_edit.html.erb +1 -1
  391. data/app/views/hyrax/dashboard/profiles/show.html.erb +5 -5
  392. data/app/views/hyrax/dashboard/sidebar/_metadata.html.erb +11 -0
  393. data/app/views/hyrax/dashboard/sidebar/_repository_content.html.erb +1 -0
  394. data/app/views/hyrax/dashboard/works/_default_group.html.erb +3 -0
  395. data/app/views/hyrax/dashboard/works/_list_works.html.erb +4 -2
  396. data/app/views/hyrax/file_sets/_form.html.erb +16 -3
  397. data/app/views/hyrax/file_sets/_metadata.html.erb +35 -0
  398. data/app/views/hyrax/file_sets/_valkyrie_form.html.erb +37 -0
  399. data/app/views/hyrax/file_sets/edit.html.erb +14 -8
  400. data/app/views/hyrax/file_sets/media_display/_audio.html.erb +4 -0
  401. data/app/views/hyrax/file_sets/media_display/_image.html.erb +2 -2
  402. data/app/views/hyrax/file_sets/media_display/_video.html.erb +4 -0
  403. data/app/views/hyrax/homepage/_explore_collections.html.erb +1 -1
  404. data/app/views/hyrax/homepage/_featured_fields.html.erb +1 -1
  405. data/app/views/hyrax/homepage/_recent_document.html.erb +1 -1
  406. data/app/views/hyrax/homepage/robots.text.erb +17 -0
  407. data/app/views/hyrax/my/collections/_default_group.html.erb +3 -0
  408. data/app/views/hyrax/my/collections/_list_collections.html.erb +3 -0
  409. data/app/views/hyrax/my/collections/_modal_add_subcollection.html.erb +2 -2
  410. data/app/views/hyrax/my/collections/_modal_add_to_collection.html.erb +2 -2
  411. data/app/views/hyrax/my/works/_default_group.html.erb +3 -0
  412. data/app/views/hyrax/my/works/_list_works.html.erb +5 -2
  413. data/app/views/hyrax/shared/_copy_permalink.html.erb +10 -0
  414. data/app/views/hyrax/users/_user_info.html.erb +1 -6
  415. data/app/views/layouts/_head_tag_content.html.erb +2 -0
  416. data/app/views/layouts/hyrax/dashboard.html.erb +2 -1
  417. data/app/views/records/edit_fields/_contexts.html.erb +1 -0
  418. data/app/views/records/edit_fields/_license.html.erb +17 -5
  419. data/app/views/records/edit_fields/_resource_type.html.erb +16 -2
  420. data/app/views/records/edit_fields/_rights_statement.html.erb +17 -5
  421. data/app/views/records/edit_fields/_schema_version.html.erb +1 -0
  422. data/app/views/records/edit_fields/_transcript_ids.html.erb +18 -0
  423. data/app/views/records/show_fields/_rights.html.erb +1 -1
  424. data/app/views/shared/_citations.html.erb +46 -31
  425. data/app/views/shared/_schema_version.html.erb +38 -0
  426. data/bin/dev-entrypoint.sh +14 -4
  427. data/chart/hyrax/Chart.yaml +4 -4
  428. data/chart/hyrax/templates/ingress.yaml +3 -0
  429. data/chart/hyrax/values.yaml +10 -8
  430. data/config/features.rb +27 -0
  431. data/config/initializers/blacklight_dynamic_sitemap.rb +43 -0
  432. data/config/initializers/deprecation_patch.rb +27 -0
  433. data/config/initializers/hydra_works.rb +3 -0
  434. data/config/initializers/i18n_extensions.rb +197 -0
  435. data/config/initializers/storage_adapter_initializer.rb +15 -9
  436. data/config/locales/hyrax.de.yml +172 -54
  437. data/config/locales/hyrax.en.yml +183 -66
  438. data/config/locales/hyrax.es.yml +174 -49
  439. data/config/locales/hyrax.fr.yml +169 -45
  440. data/config/locales/hyrax.it.yml +172 -48
  441. data/config/locales/hyrax.pt-BR.yml +172 -54
  442. data/config/locales/hyrax.zh.yml +145 -21
  443. data/config/metadata/basic_metadata.yaml +66 -0
  444. data/config/metadata/batch_edit_metadata.yaml +119 -0
  445. data/config/metadata/file_set_metadata.yaml +19 -0
  446. data/config/metadata/redirects.yaml +15 -0
  447. data/config/metadata_profiles/m3_json_schema.json +487 -0
  448. data/config/metadata_profiles/m3_profile.yaml +954 -0
  449. data/config/routes.rb +9 -0
  450. data/docker-compose-allinson.yml +176 -0
  451. data/docker-compose-dassie.yml +14 -6
  452. data/docker-compose-koppie.yml +18 -10
  453. data/docker-compose-sirenia.yml +20 -10
  454. data/documentation/copy_permalink.md +29 -0
  455. data/documentation/developing-your-hyrax-based-app.md +2 -2
  456. data/documentation/file-ingest-characterization.md +763 -0
  457. data/documentation/flexible_metadata.md +113 -0
  458. data/documentation/forms/field_behaviors.md +297 -0
  459. data/documentation/redirects.md +428 -0
  460. data/hyrax.gemspec +7 -2
  461. data/lib/freyja/metadata_adapter.rb +1 -1
  462. data/lib/freyja/persister.rb +25 -1
  463. data/lib/generators/hyrax/collection_resource/collection_resource_generator.rb +2 -2
  464. data/lib/generators/hyrax/collection_resource/templates/collection.rb.erb +5 -1
  465. data/lib/generators/hyrax/collection_resource/templates/collection_form.rb.erb +5 -1
  466. data/lib/generators/hyrax/collection_resource/templates/collection_indexer.rb.erb +6 -1
  467. data/lib/generators/hyrax/iiif_viewer/USAGE +22 -0
  468. data/lib/generators/hyrax/iiif_viewer/iiif_viewer_generator.rb +49 -0
  469. data/lib/generators/hyrax/iiif_viewer/templates/_clover.html.erb +7 -0
  470. data/lib/generators/hyrax/iiif_viewer/templates/clover/clover.css +3328 -0
  471. data/lib/generators/hyrax/iiif_viewer/templates/clover/clover.html +16 -0
  472. data/lib/generators/hyrax/iiif_viewer/templates/clover/clover.js +237 -0
  473. data/lib/generators/hyrax/install_generator.rb +25 -1
  474. data/lib/generators/hyrax/riiif_generator.rb +1 -1
  475. data/lib/generators/hyrax/templates/app/views/blacklight_dynamic_sitemap/sitemap/show.xml.builder +22 -0
  476. data/lib/generators/hyrax/templates/config/clamav.rb +2 -0
  477. data/lib/generators/hyrax/templates/config/initializers/riiif.rb +4 -80
  478. data/lib/generators/hyrax/templates/db/migrate/20240606205215_create_hyrax_flexible_schemas.rb.erb +9 -0
  479. data/lib/generators/hyrax/templates/db/migrate/20240606205216_add_contexts_to_hyrax_flexible_schemas.rb.erb +5 -0
  480. data/lib/generators/hyrax/templates/db/migrate/20260430000001_create_hyrax_redirect_paths.rb.erb +22 -0
  481. data/lib/generators/hyrax/templates/db/migrate/20260521003627_add_additional_redirect_info.rb.erb +39 -0
  482. data/lib/generators/hyrax/templates/package.json +1 -1
  483. data/lib/generators/hyrax/templates/uv.html +60 -82
  484. data/lib/generators/hyrax/work/templates/indexer.rb.erb +1 -1
  485. data/lib/generators/hyrax/work/templates/model.rb.erb +5 -1
  486. data/lib/generators/hyrax/work_resource/templates/form.rb.erb +5 -2
  487. data/lib/generators/hyrax/work_resource/templates/indexer.rb.erb +6 -2
  488. data/lib/generators/hyrax/work_resource/templates/work.rb.erb +5 -2
  489. data/lib/generators/hyrax/work_resource/work_resource_generator.rb +1 -0
  490. data/lib/hyrax/active_fedora_dummy_model.rb +6 -0
  491. data/lib/hyrax/configuration.rb +197 -3
  492. data/lib/hyrax/deprecation.rb +31 -0
  493. data/lib/hyrax/engine.rb +2 -0
  494. data/lib/hyrax/form_fields.rb +17 -5
  495. data/lib/hyrax/indexer.rb +31 -2
  496. data/lib/hyrax/resource_sync/resource_list_writer.rb +3 -0
  497. data/lib/hyrax/schema.rb +15 -4
  498. data/lib/hyrax/specs/shared_specs/factories/administrative_sets.rb +12 -0
  499. data/lib/hyrax/specs/shared_specs/factories/hyrax_embargo.rb +1 -1
  500. data/lib/hyrax/specs/shared_specs/factories/hyrax_file_set.rb +13 -0
  501. data/lib/hyrax/specs/shared_specs/factories/hyrax_lease.rb +5 -1
  502. data/lib/hyrax/specs/shared_specs/factories/hyrax_work.rb +14 -0
  503. data/lib/hyrax/specs/shared_specs/simple_work.rb +41 -0
  504. data/lib/hyrax/specs/shared_specs.rb +1 -0
  505. data/lib/hyrax/transactions/collection_create.rb +2 -1
  506. data/lib/hyrax/transactions/collection_destroy.rb +1 -0
  507. data/lib/hyrax/transactions/collection_update.rb +2 -1
  508. data/lib/hyrax/transactions/container.rb +28 -0
  509. data/lib/hyrax/transactions/steps/apply_permission_template_on_update.rb +65 -0
  510. data/lib/hyrax/transactions/steps/apply_workflow_on_admin_set_change.rb +97 -0
  511. data/lib/hyrax/transactions/steps/ensure_admin_set.rb +1 -1
  512. data/lib/hyrax/transactions/steps/remove_redirect_paths.rb +35 -0
  513. data/lib/hyrax/transactions/steps/save.rb +10 -0
  514. data/lib/hyrax/transactions/steps/set_default_admin_set.rb +1 -1
  515. data/lib/hyrax/transactions/steps/sync_redirect_paths.rb +141 -0
  516. data/lib/hyrax/transactions/work_create.rb +2 -1
  517. data/lib/hyrax/transactions/work_destroy.rb +1 -0
  518. data/lib/hyrax/transactions/work_update.rb +4 -1
  519. data/lib/hyrax/version.rb +1 -1
  520. data/lib/hyrax.rb +61 -0
  521. data/lib/tasks/m3_profile_cleanup.rake +35 -0
  522. data/template.rb +4 -1
  523. metadata +313 -19
  524. data/.dassie/public/robots.txt +0 -1
  525. data/.koppie/public/robots.txt +0 -1
  526. data/app/forms/concerns/hyrax/basic_metadata_form_fields_behavior.rb +0 -39
  527. data/app/views/catalog/_thumbnail_list_collection.html.erb +0 -4
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hyrax
4
+ module FlexibleSchemaValidators
5
+ # Handles class-related validations for flexible metadata profiles
6
+ class ClassValidator
7
+ # @param profile [Hash] M3 profile data
8
+ # @param required_classes [Array<String>] Foundational classes that must be present
9
+ # @param errors [Array<String>] Array to append validation errors to
10
+ def initialize(profile, required_classes, errors)
11
+ @profile = profile
12
+ @required_classes = required_classes
13
+ @errors = errors
14
+ end
15
+
16
+ # Validates that referenced classes are registered Hyrax curation concern
17
+ # types and that Valkyrie models use the correct `...Resource` naming
18
+ # convention when applicable.
19
+ #
20
+ # @return [void]
21
+ def validate_availability!
22
+ classes_to_validate = all_profile_classes - @required_classes
23
+ invalid_classes = []
24
+ mismatched_valkyrie_classes = []
25
+
26
+ classes_to_validate.each do |klass|
27
+ validate_class(klass, invalid_classes, mismatched_valkyrie_classes)
28
+ end
29
+
30
+ report_mismatched_classes(mismatched_valkyrie_classes)
31
+ report_invalid_classes(invalid_classes)
32
+ end
33
+
34
+ # Validates that all classes referenced within property `available_on`
35
+ # definitions are themselves defined in the top-level `classes` section of
36
+ # the profile.
37
+ #
38
+ # @return [void]
39
+ def validate_references!
40
+ properties = @profile['properties'] || {}
41
+
42
+ referenced_classes = properties.values.flat_map do |prop|
43
+ prop.dig('available_on', 'class')
44
+ end.compact.uniq
45
+
46
+ undefined_classes = referenced_classes - @profile['classes'].keys
47
+
48
+ return if undefined_classes.empty?
49
+
50
+ @errors << "Classes referenced in `available_on` but not defined in `classes`: #{undefined_classes.join(', ')}."
51
+ end
52
+
53
+ private
54
+
55
+ # Gathers all unique class names from both the top-level `classes`
56
+ # definition and all `available_on` property references.
57
+ #
58
+ # @return [Array<String>]
59
+ def all_profile_classes
60
+ profile_classes = @profile.fetch('classes', {}).keys
61
+ properties = @profile['properties'] || {}
62
+ available_on_classes = properties.values.flat_map do |prop|
63
+ prop.dig('available_on', 'class')
64
+ end.compact
65
+
66
+ clean_class_names(profile_classes + available_on_classes).uniq
67
+ end
68
+
69
+ def clean_class_names(names)
70
+ names.map { |name| name.to_s.strip.gsub(/^::/, '') }
71
+ end
72
+
73
+ # Validates a single class, checking for registration as a curation concern
74
+ # and for Valkyrie naming mismatches using the Valkyrie resolver.
75
+ #
76
+ # @param klass [String] the class name to validate
77
+ # @param invalid_classes [Array<String>] an array to append invalid class errors to
78
+ # @param mismatched_valkyrie_classes [Array<Hash>] an array to append Valkyrie mismatch errors to
79
+ # @return [void]
80
+ def validate_class(klass, invalid_classes, mismatched_valkyrie_classes)
81
+ base_class_name = klass.gsub(/(?<=.)Resource$/, '')
82
+ unless Hyrax.config.registered_curation_concern_types.include?(base_class_name)
83
+ invalid_classes << klass
84
+ return
85
+ end
86
+
87
+ begin
88
+ expected_valkyrie_class = Valkyrie.config.resource_class_resolver.call(base_class_name)
89
+ expected_class_name = expected_valkyrie_class.to_s
90
+
91
+ mismatched_valkyrie_classes << { non_resource: klass, resource: expected_class_name } if klass != expected_class_name
92
+ rescue NameError
93
+ # This occurs if a registered concern doesn't have a loadable backing class,
94
+ # which is an invalid state.
95
+ invalid_classes << klass
96
+ end
97
+ end
98
+
99
+ # Appends a formatted error message for any Valkyrie naming mismatches.
100
+ #
101
+ # @param mismatched_classes [Array<Hash>]
102
+ # @return [void]
103
+ def report_mismatched_classes(mismatched_classes)
104
+ return if mismatched_classes.empty?
105
+
106
+ message = mismatched_classes.map do |mismatch|
107
+ "'#{mismatch[:non_resource]}' should be '#{mismatch[:resource]}'"
108
+ end.join(', ')
109
+ @errors << "Mismatched Valkyrie classes found: #{message}."
110
+ end
111
+
112
+ # Appends a formatted error message for any invalid classes.
113
+ #
114
+ # @param invalid_classes [Array<String>]
115
+ # @return [void]
116
+ def report_invalid_classes(invalid_classes)
117
+ return if invalid_classes.empty?
118
+
119
+ @errors << "Invalid classes: #{invalid_classes.join(', ')}."
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hyrax
4
+ module FlexibleSchemaValidators
5
+ ##
6
+ # @api private
7
+ #
8
+ # Validates the core metadata properties of a flexible metadata profile.
9
+ class CoreMetadataValidator
10
+ ##
11
+ # @param profile [Hash] the flexible metadata profile
12
+ # @param errors [Array<String>] an array to append errors to
13
+ def initialize(profile:, errors:)
14
+ @profile = profile
15
+ @errors = errors
16
+ end
17
+
18
+ # Validate the profile against the core metadata requirements and append
19
+ # any human-readable error messages to {#errors}.
20
+ #
21
+ # @return [void]
22
+ def validate!
23
+ core_metadata['attributes'].each do |property, config|
24
+ next unless validate_property_exists(property)
25
+
26
+ validate_property_multi_value(property, config)
27
+ validate_property_indexing(property, config)
28
+ validate_property_predicate(property, config)
29
+ validate_property_available_on(property)
30
+ validate_property_cardinality(property, config)
31
+ end
32
+ validate_keyword_property
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :profile, :errors
38
+
39
+ # Load and memoize the core metadata definition from
40
+ # `config/metadata/core_metadata.yaml`.
41
+ #
42
+ # @return [Hash] the core metadata configuration with indifferent access
43
+ def core_metadata
44
+ return @core_metadata if @core_metadata
45
+
46
+ @core_metadata = YAML.safe_load(
47
+ File.read(Hyrax::Engine.root.join('config', 'metadata', 'core_metadata.yaml'))
48
+ ).with_indifferent_access
49
+
50
+ # Ensure the `creator` attribute is always treated as core metadata even
51
+ # when it is not present in the YAML definition shipped with Hyrax.
52
+ @core_metadata['attributes'] ||= {}
53
+
54
+ @core_metadata['attributes']['creator'] ||= {
55
+ 'type' => 'string',
56
+ 'multiple' => true,
57
+ 'index_keys' => ['creator_sim', 'creator_tesim'],
58
+ 'predicate' => 'http://purl.org/dc/elements/1.1/creator'
59
+ }
60
+
61
+ @core_metadata
62
+ end
63
+
64
+ # Memoized convenience accessor that returns the class keys defined in
65
+ # the profile.
66
+ #
67
+ # @return [Array<String>]
68
+ def defined_classes
69
+ @defined_classes ||= profile['classes'].keys
70
+ end
71
+
72
+ # Ensures the given property is present in the profile's `properties`
73
+ # section.
74
+ #
75
+ # @param property [String, Symbol] the property name
76
+ # @return [Boolean] true if the property exists, otherwise false
77
+ def validate_property_exists(property)
78
+ return true if profile['properties'][property].present?
79
+
80
+ errors << "Missing required property: #{property}."
81
+ false
82
+ end
83
+
84
+ # Validate that the property's `data_type` matches the expected type
85
+ # based on the core metadata's `multiple` setting.
86
+ #
87
+ # @param property [String, Symbol] the property name
88
+ # @param config [Hash] the core metadata configuration for the property
89
+ # @return [void]
90
+ def validate_property_multi_value(property, config)
91
+ return unless config.key?("multiple")
92
+
93
+ property_config = profile.dig('properties', property) || {}
94
+
95
+ required_data_type = config['multiple'] ? 'array' : 'string'
96
+
97
+ actual_data_type = determine_data_type_from_config(property_config)
98
+
99
+ return if actual_data_type == required_data_type
100
+
101
+ errors << "Property '#{property}' must have data_type set to '#{required_data_type}'."
102
+ end
103
+
104
+ # Determine the property's effective data type from its configuration.
105
+ #
106
+ # @param property_config [Hash] the property's configuration from the profile
107
+ # @return [String] either 'array' or 'string'
108
+ def determine_data_type_from_config(property_config)
109
+ if property_config['data_type']
110
+ property_config['data_type']
111
+ elsif property_config['multi_value']
112
+ 'array'
113
+ else
114
+ 'string'
115
+ end
116
+ end
117
+
118
+ # Validate that the profile includes all indexing keys required by the
119
+ # core metadata configuration.
120
+ #
121
+ # @param property [String, Symbol]
122
+ # @param config [Hash]
123
+ # @return [void]
124
+ def validate_property_indexing(property, config)
125
+ return unless config.key?('index_keys')
126
+
127
+ profile_indexing = profile.dig('properties', property, 'indexing') || []
128
+ missing_keys = config['index_keys'] - profile_indexing
129
+
130
+ return if missing_keys.empty?
131
+
132
+ errors << "Property '#{property}' is missing required indexing: #{missing_keys.join(', ')}."
133
+ end
134
+
135
+ # Ensures that the property's predicate matches the core metadata definition.
136
+ #
137
+ # @param property [String, Symbol]
138
+ # @param config [Hash]
139
+ # @return [void]
140
+ def validate_property_predicate(property, config)
141
+ return unless config.key?('predicate')
142
+ return if profile.dig('properties', property, 'property_uri') == config['predicate']
143
+
144
+ errors << "Property '#{property}' must have property_uri set to #{config['predicate']}."
145
+ end
146
+
147
+ # Validates that if the `keyword` property is present, it is correctly
148
+ # configured as a multi-valued field.
149
+ #
150
+ # @return [void]
151
+ def validate_keyword_property
152
+ keyword_prop = profile.dig('properties', 'keyword')
153
+ return unless keyword_prop
154
+
155
+ return if keyword_prop['data_type'] == 'array'
156
+
157
+ errors << "Property 'keyword' must have data_type set to 'array'."
158
+ end
159
+
160
+ # Checks that the property is available on all classes defined in the profile.
161
+ # A property entry with `name: <property>` is treated as an alias and counts
162
+ # toward coverage for the resolved name.
163
+ #
164
+ # @param property [String, Symbol]
165
+ # @return [void]
166
+ def validate_property_available_on(property)
167
+ available_on_classes = classes_covered_by_resolved_name(property.to_s)
168
+ missing_classes = defined_classes - available_on_classes
169
+
170
+ return if missing_classes.empty?
171
+ errors << "Property '#{property}' must be available on all classes, but is missing from: #{missing_classes.join(', ')}."
172
+ end
173
+
174
+ # Returns all classes covered by any profile property whose resolved name
175
+ # matches +property_name+ (either directly or via the `name` override key).
176
+ #
177
+ # @param property_name [String]
178
+ # @return [Array<String>]
179
+ def classes_covered_by_resolved_name(property_name)
180
+ profile['properties'].each_with_object([]) do |(key, config), covered|
181
+ resolved = (config['name'] || key).to_s
182
+ next unless resolved == property_name
183
+
184
+ covered.concat(Array(config.dig('available_on', 'class')))
185
+ end
186
+ end
187
+
188
+ # Validates the property's cardinality, ensuring that `title` is required
189
+ # (i.e., has a minimum cardinality of 1).
190
+ #
191
+ # @param property [String, Symbol]
192
+ # @param _config [Hash] the core metadata configuration for the property (unused)
193
+ # @return [void]
194
+ def validate_property_cardinality(property, _config)
195
+ # Ensure that the `title` property is always required by enforcing
196
+ # a cardinality minimum of at least 1. According to the M3 profile
197
+ # specification, `cardinality.minimum` > 0 is interpreted as
198
+ # "required".
199
+ return unless property.to_s == 'title'
200
+
201
+ minimum = profile.dig('properties', property, 'cardinality', 'minimum')
202
+
203
+ # Treat missing `cardinality` or `minimum` as 0 (i.e., not required).
204
+ required = minimum.to_i.positive?
205
+ return if required
206
+
207
+ errors << "Property 'title' must have a cardinality minimum of at least 1."
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hyrax
4
+ module FlexibleSchemaValidators
5
+ # Validates that classes with existing records in the repository are not removed from the profile.
6
+ class ExistingRecordsValidator
7
+ # @param profile [Hash] M3 profile data
8
+ # @param required_classes [Array<String>] Foundational classes that must be present
9
+ # @param errors [Array<String>] Array to append validation errors to
10
+ def initialize(profile, required_classes, errors)
11
+ @profile = profile
12
+ @required_classes = required_classes
13
+ @errors = errors
14
+ end
15
+
16
+ # Validates that no classes with existing records in the repository have been
17
+ # removed from the profile.
18
+ #
19
+ # @return [void]
20
+ def validate!
21
+ profile_classes_set = Set.new(@profile.fetch('classes', {}).keys)
22
+ classes_with_records = []
23
+ potential_existing_classes.each do |model_class|
24
+ model_identifier = model_class.to_s
25
+ counterpart_identifier = counterpart_for(model_identifier)
26
+
27
+ # If this model or its counterpart is in the profile, we don't need to check it for removal.
28
+ next if profile_classes_set.include?(model_identifier) || profile_classes_set.include?(counterpart_identifier)
29
+
30
+ # If it's not in the profile, check if it has records.
31
+ classes_with_records << model_identifier if model_has_records?(model_class, model_identifier)
32
+ end
33
+
34
+ return if classes_with_records.empty?
35
+
36
+ @errors << "Classes with existing records cannot be removed from the profile: #{classes_with_records.uniq.join(', ')}."
37
+ end
38
+
39
+ private
40
+
41
+ # Determines the counterpart model name (e.g., Image -> ImageResource).
42
+ def counterpart_for(model_identifier)
43
+ return unless defined?(Wings)
44
+
45
+ klass = model_identifier.safe_constantize
46
+ return if klass.blank?
47
+
48
+ Wings::ModelRegistry.lookup(klass).to_s
49
+ rescue NameError
50
+ # This can happen if a class is not loadable,
51
+ # or if a counterpart model does not exist.
52
+ nil
53
+ end
54
+
55
+ # Queries the repository to see if a given model has any records.
56
+ def model_has_records?(model_class, class_name)
57
+ Hyrax.query_service.count_all_of_model(model: model_class).positive?
58
+ rescue StandardError => e
59
+ Rails.logger.error "Error checking records for #{class_name}: #{e.message}"
60
+ false
61
+ end
62
+
63
+ # Gathers all unique, canonical model classes that could potentially have records.
64
+ # @return [Array<Class>]
65
+ def potential_existing_classes
66
+ return @models if @models.present?
67
+ @models = @required_classes.clone.map(&:safe_constantize)
68
+ Hyrax.config.registered_curation_concern_types.each do |concern_type|
69
+ resource = concern_type.match?(/Resource$/) ? concern_type : "#{concern_type}Resource"
70
+ resource.safe_constantize
71
+ @models << Valkyrie.config.resource_class_resolver.call(concern_type)
72
+ rescue NameError, LoadError
73
+ # This can happen if a concern is registered but its class is not loadable.
74
+ # We can safely ignore it, as it couldn't have records anyway.
75
+ Rails.logger.warn "Could not resolve model class for registered concern: #{concern_type}"
76
+ end
77
+
78
+ @models.uniq!
79
+ @models
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hyrax
4
+ module FlexibleSchemaValidators
5
+ ##
6
+ # @api private
7
+ #
8
+ # Validates the `redirects` property in an m3 profile against the
9
+ # two-layer feature gating (config + Flipflop):
10
+ #
11
+ # | config | flipflop | property | result |
12
+ # |--------|----------|----------|-------------------------------------|
13
+ # | off | n/a | present | warn (property will be ignored) |
14
+ # | off | n/a | absent | silent |
15
+ # | on | off | present | warn (property is loaded but unused)|
16
+ # | on | off | absent | silent |
17
+ # | on | on | absent | error (property is required) |
18
+ # | on | on | present | check available_on.class lists at |
19
+ # | | | | least one work or collection class |
20
+ # | | | | declared in this profile's classes |
21
+ class RedirectsValidator
22
+ ##
23
+ # @param profile [Hash] the flexible metadata profile
24
+ # @param errors [Array<String>] an array to append errors to
25
+ # @param warnings [Array<String>] an array to append warnings to
26
+ def initialize(profile:, errors:, warnings: [])
27
+ @profile = profile
28
+ @errors = errors
29
+ @warnings = warnings
30
+ end
31
+
32
+ # Validate the profile against the redirects requirements and append
33
+ # any human-readable error messages to {#errors} (or warnings to
34
+ # {#warnings} for the dead-property cases).
35
+ #
36
+ # @return [void]
37
+ def validate!
38
+ return validate_when_config_off unless Hyrax.config.redirects_enabled?
39
+ return validate_when_flipflop_off unless Flipflop.redirects?
40
+
41
+ validate_when_enabled
42
+ end
43
+
44
+ private
45
+
46
+ def redirects_property
47
+ @redirects_property ||= @profile&.dig('properties', 'redirects')
48
+ end
49
+
50
+ def validate_when_config_off
51
+ return if redirects_property.blank?
52
+ @warnings << I18n.t('hyrax.flexible_schema_validators.redirects_validator.warnings.config_disabled')
53
+ end
54
+
55
+ def validate_when_flipflop_off
56
+ return if redirects_property.blank?
57
+ @warnings << I18n.t('hyrax.flexible_schema_validators.redirects_validator.warnings.flipflop_disabled')
58
+ end
59
+
60
+ def validate_when_enabled
61
+ if redirects_property.blank?
62
+ @errors << I18n.t('hyrax.flexible_schema_validators.redirects_validator.errors.property_required')
63
+ return
64
+ end
65
+
66
+ @errors << I18n.t('hyrax.flexible_schema_validators.redirects_validator.errors.invalid_type', actual_type: redirects_property['type'].inspect) unless redirects_property['type'].to_s == 'hash'
67
+
68
+ available_on = clean(Array(redirects_property.dig('available_on', 'class')))
69
+ return if (available_on & profile_work_or_collection_classes).any?
70
+
71
+ @errors << I18n.t('hyrax.flexible_schema_validators.redirects_validator.errors.invalid_available_on')
72
+ end
73
+
74
+ # Class names declared in this m3 profile's top-level `classes:` block,
75
+ # filtered to keep only those that represent works or collections.
76
+ # FileSets, AdminSets, and any non-work/non-collection class are
77
+ # excluded — redirects only apply to works and collections.
78
+ def profile_work_or_collection_classes
79
+ @profile_work_or_collection_classes ||= begin
80
+ declared = clean(Array(@profile&.dig('classes')&.keys))
81
+ declared.select { |name| work_or_collection?(name) }
82
+ end
83
+ end
84
+
85
+ def work_or_collection?(class_name)
86
+ registered_collection_names.include?(class_name) ||
87
+ registered_work_names.include?(class_name)
88
+ end
89
+
90
+ def registered_collection_names
91
+ @registered_collection_names ||= clean(Hyrax::ModelRegistry.collection_class_names)
92
+ end
93
+
94
+ # Work class names from the registry, with collection / file_set /
95
+ # admin_set names explicitly excluded. The registry can include any
96
+ # class an adopter has registered as a curation concern, so the
97
+ # exclusion guards against e.g. a FileSet being registered alongside
98
+ # works and slipping through as a "work" type for redirects.
99
+ # Each registered name is also paired with its `Resource`-suffixed
100
+ # Valkyrie equivalent — `class_validator` accepts both forms.
101
+ def registered_work_names
102
+ @registered_work_names ||= begin
103
+ works = clean(Hyrax::ModelRegistry.work_class_names)
104
+ works -= clean(Hyrax::ModelRegistry.collection_class_names)
105
+ works -= clean(Hyrax::ModelRegistry.file_set_class_names)
106
+ works -= clean(Hyrax::ModelRegistry.admin_set_class_names)
107
+ works.flat_map { |name| [name, "#{name}Resource"] }
108
+ end
109
+ end
110
+
111
+ def clean(names)
112
+ names.map { |name| name.to_s.delete_prefix('::') }
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hyrax
4
+ module FlexibleSchemaValidators
5
+ # Handles JSON schema validation for flexible metadata profiles
6
+ class SchemaValidator
7
+ def initialize(schemer, profile, errors)
8
+ @schemer = schemer
9
+ @profile = profile
10
+ @errors = errors
11
+ end
12
+
13
+ def validate!
14
+ @schemer.validate(@profile).to_a&.each do |error|
15
+ pointer = error['data_pointer']
16
+ type = error['type']
17
+
18
+ if pointer.end_with?('/available_on') && error['data'].nil? && type == 'object'
19
+ @errors << "Schema error at `#{pointer}`: `available_on` cannot be empty and must have a `class` or `context` sub-property."
20
+ elsif type == 'required'
21
+ missing_keys = error.dig('details', 'missing_keys')&.join("', '")
22
+ @errors << "Schema error at `#{pointer}`: Missing required properties: '#{missing_keys}'."
23
+ else
24
+ @errors << "Schema error at `#{pointer}`: Invalid value `#{error['data'].inspect}` for type `#{type}`."
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :schemer, :profile, :errors
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hyrax
4
+ module FlexibleSchemaValidators
5
+ class SortPropertiesValidator
6
+ attr_reader :profile, :sort_properties
7
+
8
+ def initialize(profile, warnings)
9
+ @profile = profile
10
+ @warnings = warnings
11
+ @sort_properties = find_sort_properties
12
+ end
13
+
14
+ def validate!
15
+ sort_properties.each do |property|
16
+ properties_without_sort_properties = work_types_from_profile - (profile.dig('properties', property, 'available_on', 'class') || [])
17
+ next if properties_without_sort_properties.empty?
18
+
19
+ msg = I18n.t(
20
+ 'hyrax.flexible_schema_validators.sort_properties_validator.warnings.message',
21
+ property: property,
22
+ classes: properties_without_sort_properties.join(', ')
23
+ )
24
+ @warnings << msg
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def find_sort_properties
31
+ CatalogController.blacklight_config.sort_fields.keys.filter_map do |sort_key|
32
+ field = sort_key.split.first.sub(/_[^_]*$/, '')
33
+ field unless system_properties.include?(field)
34
+ end.uniq
35
+ end
36
+
37
+ def system_properties
38
+ %w[score system_modified system_create]
39
+ end
40
+
41
+ def work_types_from_profile
42
+ work_types = available_works.map do |work_type|
43
+ Valkyrie.config.resource_class_resolver.call(work_type).to_s
44
+ end
45
+
46
+ profile['classes'].keys.filter_map { |klass_name| klass_name if work_types.include?(klass_name) }
47
+ end
48
+
49
+ def available_works
50
+ Hyrax.config.registered_curation_concern_types
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Hyrax
3
3
  # Provide select options for the license (dcterms:rights) field
4
- class LicenseService < QaSelectService
4
+ class LicenseService < TolerantSelectService
5
5
  def initialize(_authority_name = nil)
6
6
  super('licenses')
7
7
  end
@@ -52,6 +52,8 @@ module Hyrax
52
52
 
53
53
  ##
54
54
  # Re-index the resource.
55
+ # If the updated object is a file set used as the thumbnail of a parent work,
56
+ # also reindex that parent so thumbnail_alt_text_tesim stays fresh.
55
57
  #
56
58
  # Called when 'object.metadata.updated' event is published
57
59
  # @param [Dry::Events::Event] event
@@ -59,6 +61,7 @@ module Hyrax
59
61
  def on_object_metadata_updated(event)
60
62
  return unless resource? event[:object]
61
63
  Hyrax.index_adapter.save(resource: event[:object])
64
+ reindex_parent_for_thumbnail(event[:object]) if event[:object].file_set?
62
65
  end
63
66
 
64
67
  ##
@@ -96,6 +99,14 @@ module Hyrax
96
99
 
97
100
  private
98
101
 
102
+ def reindex_parent_for_thumbnail(file_set)
103
+ parent = Hyrax.custom_queries.find_parent_work(resource: file_set)
104
+ return unless parent&.thumbnail_id == file_set.id
105
+ Hyrax.index_adapter.save(resource: parent)
106
+ rescue Valkyrie::Persistence::ObjectNotFoundError, Hyrax::ObjectNotFoundError
107
+ nil
108
+ end
109
+
99
110
  def resource?(resource)
100
111
  return true if resource.is_a? Valkyrie::Resource
101
112
  log_non_resource(resource)