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,428 @@
1
+ # URL Redirects
2
+
3
+ Hyrax can register arbitrary URL paths as redirects to a work or collection's permanent URL. This is the migration safety net for institutions moving from DSpace, CONTENTdm, Islandora, bepress, or any other repository system whose URLs are cited in published scholarship, embedded in LibGuides, indexed by search engines, and bookmarked by researchers — those URLs need to keep resolving after the move.
4
+
5
+ This feature is **disabled by default** and must be explicitly enabled in two layers.
6
+
7
+ ## Two-layer feature gating
8
+
9
+ The redirects feature is gated by **two** independent switches:
10
+
11
+ 1. **`Hyrax.config.redirects_enabled?`** — application-level config. Controls whether the schema and properties exist in this Hyrax application at all. Set in `config/initializers/hyrax.rb` or via the `HYRAX_REDIRECTS_ENABLED` environment variable. Default: `false`.
12
+
13
+ 2. **`Flipflop.redirects?`** — runtime feature flag. Controls whether the redirects feature is active. Only registered when the config is on. Default when registered: `false`. Toggleable via the Flipflop admin UI. Multi-tenant host apps (e.g. Hyku) can resolve this flag per tenant via Flipflop's strategy chain.
14
+
15
+ | Config | Flipflop | What's true |
16
+ |---|---|---|
17
+ | off | n/a (unregistered) | The schema is not loaded. The Flipflop feature is not registered. No `redirects` attribute on any resource. No indexer. No route. No controller. m3 profile does not require a `redirects` property. The feature is wholly absent. |
18
+ | on | registered, off (default) | The schema is loaded. The `redirects` attribute exists on `Hyrax::Work` and `Hyrax::PcdmCollection`. The indexer is included on resource indexers but writes no Solr field. Routes/controllers/UI gates check Flipflop and stay silent. m3 profile may declare `redirects` (loaded but unused — a warning is logged on profile validation). |
19
+ | on | on | All of the above, plus: the indexer writes `redirects_path_tesim` to the Solr document for show-page rendering. The route/controller/UI engage. m3 profile validation **requires** the `redirects` property to be declared with `type: hash` and available on at least one work or collection class declared in the profile. |
20
+
21
+ The two-layer split is deliberate: the application-level config controls *availability* (the schema is structural — toggling it after data is written would orphan persisted entries), and the Flipflop controls *use* at request time.
22
+
23
+ ## Enabling the config
24
+
25
+ ### Via environment variable
26
+
27
+ ```sh
28
+ export HYRAX_REDIRECTS_ENABLED=true
29
+ ```
30
+
31
+ This is the easiest path for koppie/dassie/sirenia/allinson and for CI environments.
32
+
33
+ ### Via initializer
34
+
35
+ ```ruby
36
+ # config/initializers/hyrax.rb
37
+ Hyrax.config do |config|
38
+ config.redirects_enabled = true
39
+ end
40
+ ```
41
+
42
+ This is appropriate when an adopter wants the feature unconditionally without depending on the deploy environment.
43
+
44
+ ### What changes when the config is on
45
+
46
+ - `config/metadata/redirects.yaml` becomes part of the schema set (visible to `Hyrax::SimpleSchemaLoader#permissive_schema_for_valkrie_adapter` and `Hyrax::Schema(:redirects)`).
47
+ - `Hyrax::Work` includes `Hyrax::Schema(:redirects)` when `work_default_metadata` is true; `Hyrax::PcdmCollection` includes it when `collection_include_metadata?` is true. The form-side `redirects` property is installed per-resource at form initialization: `Hyrax::Forms::ResourceForm#initialize` checks whether the resource responds to `redirects` and, if so, installs `Hyrax::FormFields(:redirects)` on the form's singleton class. This single check handles all four combinations of base-class metadata gates and per-class flexibility (flex-false models that include the schema, flex-true models whose m3 profile declares `redirects`, and the two cases where neither applies). The check runs *after* any flexible-mode resource reconstruction so it sees the resource Reform will use, not a pre-reconstruction copy whose singleton class may still carry attributes from an older m3 schema version.
48
+ - The `:redirects` Flipflop feature is registered and appears in the admin Flipflop UI.
49
+ - `Hyrax::Indexers::PcdmObjectIndexer` and `Hyrax::Indexers::PcdmCollectionIndexer` include `Hyrax::Indexers::RedirectsIndexer` (the include itself is conditional on this config; when off, the mixin is not added at all).
50
+ - The m3 profile validator runs the redirects-specific check (no error or warning unless the Flipflop is also on, or the profile contains a stale `redirects` property).
51
+
52
+ ### What changes when the config is off
53
+
54
+ - None of the above happens. Hyrax behaves as if the redirects feature didn't exist.
55
+
56
+ ## Enabling the Flipflop
57
+
58
+ Once the config is on, the `:redirects` feature appears in the experimental_features group of the Hyrax Flipflop admin UI. Toggling it on:
59
+
60
+ - Causes `Hyrax::Indexers::RedirectsIndexer` to write the `redirects_path_tesim` field to the Solr document for resources with `redirects` entries (used by show-page rendering).
61
+ - Activates the catch-all redirect route and `Hyrax::RedirectsController`.
62
+ - Causes the m3 profile validator to **require** a `redirects` property in the flexible metadata profile (when `flexible: true` mode is also active).
63
+
64
+ If the Flipflop is on but the m3 profile is missing the `redirects` property, the profile fails validation with a clear error message. Adopters running flexible metadata must add a `redirects` property to their m3 profile before enabling the Flipflop.
65
+
66
+ ## m3 profile requirements (`flexible: true` mode)
67
+
68
+ Adopters running `HYRAX_FLEXIBLE=true` and choosing to use redirects must declare a `redirects` property in their m3 profile. The minimal declaration:
69
+
70
+ ```yaml
71
+ properties:
72
+ redirects:
73
+ available_on:
74
+ class:
75
+ - GenericWork
76
+ - CollectionResource
77
+ cardinality:
78
+ minimum: 0
79
+ display_label:
80
+ default: Redirects
81
+ type: hash
82
+ multiple: true
83
+ form:
84
+ display: false
85
+ indexing:
86
+ - editor_only
87
+ predicate: http://samvera.org/ns/hyku/redirects
88
+ range: http://www.w3.org/2001/XMLSchema#string
89
+ view:
90
+ render_term: redirects_path
91
+ render_as: redirects_label
92
+ html_dl: true
93
+ ```
94
+
95
+ The `form:` block is required in m3. Without it, the m3 form-definition loader skips the property (its filter drops attributes with empty `form_options`), and the Aliases tab errors when the partial reads `f.object.redirects`. The Aliases UI is rendered by `_form_redirects.html.erb`, not by the auto-generated form, so `display: false` is the right value here.
96
+
97
+ The `display_label` and `range` entries are required for profile validation. `display_label.default` controls the label that appears next to the redirects field on show pages.
98
+
99
+ The `indexing: [editor_only]` entry and the `view:` block together opt the redirects field into the show-page display described below in [Displaying redirect aliases on show pages](#displaying-redirect-aliases-on-show-pages). Both are required for that display to appear with editor-or-admin visibility. Note the placement: `editor_only` is an entry in the `indexing:` array (read by `Hyrax::SchemaLoader::AttributeDefinition#editor_only?`); `render_term`, `render_as`, and `html_dl` are inside the `view:` block. Non-flexible mode structures `editor_only` differently — see [Schema details](#schema-details-flexible-false-mode) below.
100
+
101
+ Each redirect entry is a plain hash with `path` and `is_display_url` keys, persisted as JSONB on the parent resource. The `type: hash` token resolves to `Dry::Types['hash']`, which round-trips entries through Postgres without the sub-field stripping a nested `Valkyrie::Resource` would suffer. `available_on.class` must include at least one work or collection class declared in this profile's top-level `classes:` block; substitute the class names your profile declares (`Image`, `Etd`, `Oer`, etc.) as appropriate.
102
+
103
+ Validation matrix on profile save (with the config on):
104
+
105
+ | Flipflop | Property | Result |
106
+ |---|---|---|
107
+ | off | absent | silent (the feature is not in active use) |
108
+ | off | present | warning (property is loaded but unused) |
109
+ | on | absent | error (property is required) |
110
+ | on | declared without `type: hash` | error |
111
+ | on | `available_on.class` lists no work or collection class declared in this profile | error |
112
+ | on | present, complete | silent (valid) |
113
+
114
+ When the config is **off**, an m3 profile that declares `redirects` produces a warning rather than an error. The property is dead — it won't be loaded — but Hyrax doesn't refuse to save the profile.
115
+
116
+ ## Schema details (`flexible: false` mode)
117
+
118
+ In default (non-flexible) mode the schema lives in `config/metadata/redirects.yaml`:
119
+
120
+ ```yaml
121
+ attributes:
122
+ redirects:
123
+ type: hash
124
+ multiple: true
125
+ form:
126
+ display: false
127
+ view:
128
+ editor_only: true
129
+ render_term: redirects_path
130
+ render_as: redirects_label
131
+ html_dl: true
132
+ predicate: http://samvera.org/ns/hyku/redirects
133
+ mappings:
134
+ simple_dc_pmh: ~
135
+ ```
136
+
137
+ The `view:` block opts the redirects field into the show-page display described below in [Displaying redirect aliases on show pages](#displaying-redirect-aliases-on-show-pages). In non-flexible mode, all view options live inside the `view:` block — that's where `Hyrax::SimpleSchemaLoader#view_definitions_for` reads them from. (Flex-true uses a different structure for `editor_only`; see the [m3 profile requirements](#m3-profile-requirements-flexible-true-mode) section.)
138
+
139
+ When the config is on, this schema is loaded and `Hyrax::Work` / `Hyrax::PcdmCollection` include it via `Hyrax::Schema(:redirects)`. The schema loader produces:
140
+
141
+ ```ruby
142
+ attribute :redirects, Valkyrie::Types::Array.of(Dry::Types['hash'])
143
+ ```
144
+
145
+ Each entry is a plain hash with `path` and `is_display_url` keys. Entries persist as JSONB on the parent resource, so sub-fields round-trip cleanly without an intermediate nested-resource schema mangling them. `Hyrax::Redirect` is retained as a thin Ruby presenter the form view consumes; non-form code (validator, indexer, sync step) reads the persisted hash directly.
146
+
147
+ When the config is off, the loader filters `redirects.yaml` out of the schema set entirely. The file is on disk but invisible to `permissive_schema_for_valkrie_adapter`, `Hyrax::Schema(:redirects)`, and any other consumer of the simple schema loader.
148
+
149
+ ## Path normalization
150
+
151
+ Every redirect path in Hyrax passes through `Hyrax::RedirectPathNormalizer.call`. This is the single source of truth for "what does this path look like on disk?". Normalization rules:
152
+
153
+ 1. If the input parses as a URL with a scheme and host (e.g. `https://old.example.edu/handle/12345/678`), keep only the path component.
154
+ 2. Strip query strings (`?utm_source=foo`) and fragments (`#section`).
155
+ 3. Ensure a leading slash (`handle/123` → `/handle/123`).
156
+ 4. Strip trailing slashes (`/handle/123/` → `/handle/123`), with the exception that the bare path `/` is preserved.
157
+
158
+ The normalizer is idempotent — `normalize(normalize(x)) == normalize(x)`. Normalization happens in two distinct contexts:
159
+
160
+ **On write (resource layer).** `Hyrax::RedirectsNormalization` is included on `Hyrax::Work` and `Hyrax::PcdmCollection` and overrides `set_value` so any assignment to the `redirects` attribute — form save, console write, importer, change-set apply — normalizes each entry's path before persistence. Read-side consumers (`Hyrax::Indexers::RedirectsIndexer`, `Hyrax::Transactions::Steps::SyncRedirectPaths`) trust the persisted shape and do not re-normalize.
161
+
162
+ **On input (boundary layer).** Three boundary points canonicalize input from outside the resource before consulting the persisted state:
163
+
164
+ - `Hyrax::RedirectsFieldBehavior#redirects_attributes_populator` normalizes form entries before the form-level validator runs, so a user pasting a full URL (`https://old.example.edu/handle/123?utm=email`) is forgivingly accepted and the validator sees the canonical form.
165
+ - `Hyrax::RedirectsLookup.find_row` (called by `Hyrax::RedirectsController#show` and by `Hyrax::RedirectToDisplayUrl#redirect_to_display_url_if_needed`) normalizes the incoming request path before the table lookup, so `/foo/` and `/foo` both resolve.
166
+ - `Hyrax::RedirectsLookup` normalizes its input on construction, so callers can pass any reasonable form.
167
+
168
+ ## Validation
169
+
170
+ `Hyrax::RedirectValidator` is wired into `Hyrax::Forms::ResourceForm` (and therefore both `Hyrax::Forms::PcdmObjectForm` for works and `Hyrax::Forms::PcdmCollectionForm` for collections) when `Hyrax.config.redirects_enabled?`. It runs only when the Flipflop is also on; otherwise it is a no-op.
171
+
172
+ By the time the validator sees an entry, the path has already been normalized by the form's `redirects=` setter, so the validator can assume canonical form and focus on rule violations.
173
+
174
+ The validator enforces six rules on the `redirects` attribute:
175
+
176
+ | Rule | Trigger | Error |
177
+ |---|---|---|
178
+ | Path is present | `entry.path` is blank | `redirect path can't be blank` |
179
+ | Path format | path doesn't start with `/`, or contains any character outside the RFC 3986 path set (alphanumerics, `-._~`, sub-delims `!$&'()*+,;=`, `:`, `@`, `/`, and `%XX` percent-encoding) | `is not a valid redirect path` |
180
+ | Reserved prefix | path equals or starts with one of `Hyrax.config.reserved_redirect_prefixes` (defaults to the routes Hyrax itself reserves) | the path is reserved by the application and may not be used as an alias |
181
+ | Intra-record uniqueness | the same path appears more than once on a single record | `is listed more than once on this record` |
182
+ | Global uniqueness | the path is already in use on a different record (excluding the current record's own id) | `is already in use by another record` |
183
+ | At most one display URL | more than one entry has `is_display_url: true` | `at most one redirect entry may be marked as the display URL` |
184
+
185
+ ### Reserved-prefix list
186
+
187
+ The reserved-prefix list lives in `Hyrax.config.reserved_redirect_prefixes`. The default covers the routes Hyrax itself reserves: `/admin`, `/api`, `/assets`, `/batch_edits`, `/batch_uploads`, `/capabilitylist`, `/catalog`, `/changelist`, `/collections`, `/concern`, `/content_blocks`, `/dashboard`, `/downloads`, `/embargoes`, `/featured_works`, `/files`, `/leases`, `/notifications`, `/pages`, `/proxies`, `/rails`, `/resourcelist`, `/uploads`, `/users`, and `/.well-known`.
188
+
189
+ Host applications with their own reserved routes (Hyku's `/single_signon`, for example) should extend the list in their initializer:
190
+
191
+ ```ruby
192
+ # config/initializers/hyrax.rb
193
+ Hyrax.config do |config|
194
+ config.reserved_redirect_prefixes += ['/single_signon']
195
+ end
196
+ ```
197
+
198
+ The validator rejects any redirect path that equals one of these prefixes, or starts with one followed by `/` (so `/admin` is reserved, and `/admin/anything` is reserved, but `/administrator` would *not* be reserved by the `/admin` entry).
199
+
200
+ ### Uniqueness lookup and the `hyrax_redirect_paths` table
201
+
202
+ Global uniqueness is enforced by a Postgres table, `hyrax_redirect_paths`, with columns:
203
+
204
+ | Column | Purpose |
205
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------- |
206
+ | `from_path` | The URL the visitor entered (an alias or the UUID URL itself). Unique B-tree indexed — the primary request-time lookup key. |
207
+ | `to_path` | The URL the address bar should display after the resolver routes the request. Equals the display alias's `from_path` on rows for resources with a display URL; equals `permalink_path` otherwise. |
208
+ | `permalink_path` | The resource's canonical UUID path (e.g. `/concern/generic_works/<uuid>` or `/collections/<uuid>`). Constant per resource. |
209
+ | `resource_id` | The UUID of the work or collection. Indexed (non-unique) for the per-resource sync described below. |
210
+ | `is_display_url` | Boolean. `true` on the row whose `from_path` is the record's chosen display URL; `false` everywhere else. |
211
+
212
+ When a resource has a display URL set, the sync step writes one row per alias plus an extra row whose `from_path` is the UUID URL, so visitors hitting the bare UUID URL are also routed to the display alias.
213
+
214
+ The unique index on `from_path` gives the hard guarantee that no two records can share an alias even under concurrent saves.
215
+
216
+ `Hyrax::RedirectsLookup` exposes two class methods against `hyrax_redirect_paths`:
217
+
218
+ - `.taken?(path, except_id: nil)` — the uniqueness check used by the validator at form-submit time. Returns a boolean. SQL: `SELECT 1 FROM hyrax_redirect_paths WHERE from_path = ? AND resource_id <> ? LIMIT 1;`.
219
+ - `.find_row(path)` — the request-time lookup used by the resolver and the `RedirectToDisplayUrl` before_action. Returns the full `Hyrax::RedirectPath` row, or nil. SQL: `SELECT * FROM hyrax_redirect_paths WHERE from_path = ? LIMIT 1;`. Normalizes its input via `Hyrax::RedirectPathNormalizer` before querying, so `/foo/` and `/foo` resolve to the same row.
220
+
221
+ The validator calls `.taken?(path, except_id: record.id)` to give the user friendly feedback at form-submit time. If two simultaneous requests both pass validation (because both checked the table before either committed), the unique index on `from_path` rejects the second one at insert time and the enclosing transaction returns `Failure`.
222
+
223
+ ### Sync between `redirects` attribute and the redirects table
224
+
225
+ The redirects table is kept in sync by two `dry-transaction` steps composed into the create/update/destroy transactions:
226
+
227
+ - `Hyrax::Transactions::Steps::SyncRedirectPaths` — runs after the resource is saved (in `WorkCreate`, `WorkUpdate`, `CollectionCreate`, `CollectionUpdate`). Deletes the resource's existing rows and reinserts the current redirect set in a single DB transaction. On `ActiveRecord::RecordNotUnique` (race lost), returns `Failure([:redirect_path_collision, ...])`, which short-circuits the enclosing transaction and surfaces back to the controller. On any other `ActiveRecord::StatementInvalid` (missing table, schema drift, connection drop), logs the error and returns `Failure([:redirect_path_sync_error, ...])` rather than raising — the resource is already persisted by the time this step runs, so a soft-fail is preferable to a 500 with partial state. No-op when either the config or the Flipflop is off.
228
+ - `Hyrax::Transactions::Steps::RemoveRedirectPaths` — runs before `delete_resource` in `WorkDestroy` and `CollectionDestroy`. Clears the resource's rows so deleted resources don't leave dangling claims on redirect paths. Same `StatementInvalid` treatment as the sync step: returns `Failure([:redirect_path_remove_error, ...])` instead of raising. Gated only on the config (not the Flipflop): cleanup must happen regardless of whether the feature is currently in active use, so that an admin toggling the Flipflop off mid-deployment doesn't leave orphaned rows that could later collide with new resources after a re-enable.
229
+
230
+ Neither step does anything when the config is off, so adopters who don't enable the redirects feature pay no cost for them.
231
+
232
+ ## Resolver behavior
233
+
234
+ ### Route placement
235
+
236
+ The redirect resolver is wired up as a **catch-all route in the host application's `config/routes.rb`**, not in the Hyrax engine. It must be the **last route in the host application** so every other route — engine mounts, host-specific routes, and `curation_concerns_basic_routes` — gets first crack at matching the request. The install generator appends the route at the end of `config/routes.rb`:
237
+
238
+ ```ruby
239
+ # config/routes.rb (host app, end of file)
240
+ get '*alias_path', to: 'hyrax/redirects#show',
241
+ constraints: ->(_req) { Hyrax.config.redirects_active? },
242
+ format: false
243
+ ```
244
+
245
+ The constraint lambda is evaluated on every request: when `redirects_active?` is false (config off, or Flipflop off, or both), the catch-all is transparent and Rails returns its default 404 for any path that didn't match an earlier route. When `redirects_active?` is true, the request reaches `Hyrax::RedirectsController#show`.
246
+
247
+ Adopters with existing installs (predating this feature) need to add the catch-all line manually. Adopters who run a custom catch-all (e.g. a 404 page handler) should put the redirect line *before* their handler, so registered redirects take precedence and unregistered paths fall through to the custom 404.
248
+
249
+ ### What happens at request time
250
+
251
+ When both gates are open, `Hyrax::RedirectsController#show` serves any path not claimed by an earlier route:
252
+
253
+ - The incoming path is normalized via `Hyrax::RedirectPathNormalizer` so the lookup matches the canonical form stored in the table (a request for `/foo/` resolves the same record as `/foo`).
254
+ - `Hyrax::RedirectsLookup.find_row(path)` does one indexed `find_by(from_path:)` against `hyrax_redirect_paths`. No Solr.
255
+ - If no row matches, the controller raises `ActionController::RoutingError` so Rails serves its standard 404.
256
+ - If the matched row has `is_display_url: true` (the visitor entered the resource's display URL), the controller looks up the curation-concern controller for the row's `permalink_path` via `Rails.application.routes.recognize_path`, sets `request.env['hyrax.redirects.dispatched'] = true` so the inner show controller's redirect-check before_action skips itself, and calls `controller_class.dispatch(info[:action], request, response)`. The inner controller writes into the shared `response`, and the outer action sets `self.response_body = response.body` so Rails skips its own template lookup for `RedirectsController#show`. The show page renders in place at the visited path.
257
+ - Otherwise (the visitor entered a non-display alias), the controller responds `301 Moved Permanently` with `Location:` set to the row's `to_path`. That target is either the display alias (when the resource has one) or the resource's UUID URL.
258
+
259
+ ### Show-action redirect on UUID URL visits
260
+
261
+ Visiting the bare UUID URL bypasses the catch-all (Rails routes natively to the curation-concern controller's `show`). The `Hyrax::RedirectToDisplayUrl` controller concern handles that case via a `before_action :redirect_to_display_url_if_needed` included on `WorksControllerBehavior` and `CollectionsControllerBehavior`:
262
+
263
+ - If `request.env['hyrax.redirects.dispatched']` is set (this request was dispatched by `RedirectsController`), skip — the visitor is already where they should be.
264
+ - Otherwise, `Hyrax::RedirectsLookup.find_row(request.path)` looks up a row by the visited path.
265
+ - If a row exists and its `to_path` differs from the visited path, 301 to the row's `to_path`.
266
+ - Otherwise, render normally.
267
+
268
+ When a resource has a display URL, the sync step writes a row keyed on the UUID URL whose `to_path` is the display alias — that's the row this before_action finds, and the 301 sends the visitor to the display alias. The browser then makes a fresh request to the display alias; that request matches the catch-all (display aliases are not native Rails routes — they exist only in `hyrax_redirect_paths`), reaches `RedirectsController#show`, and dispatches in place.
269
+
270
+ ### Locale preservation
271
+
272
+ `RedirectsController` inherits the host app's `ApplicationController`, which includes `Hyrax::Controller`. `Hyrax::Controller#set_locale` reads `params[:locale]` (or falls back to `I18n.default_locale`) on every request. When the resolver issues a 301 to a string path (the row's `to_path`), it appends `?locale=<value>` to that path whenever the incoming request carried a `locale` param, so the destination page renders in the visitor's chosen language. When the resolver dispatches in place at the display URL, it merges `locale: params[:locale]` into the inner request's `path_parameters` before calling `controller_class.dispatch`, so the inner controller's `set_locale` before_action picks up the same locale.
273
+
274
+ The same locale-preservation behavior is in `Hyrax::RedirectToDisplayUrl#redirect_to_display_url_if_needed` for the UUID-URL-to-display-alias 301.
275
+
276
+ Aliases are stored without a locale prefix. A visitor whose browser arrives at `/<locale>/handle/12345/678` (which happens in host apps that mount routes inside `scope "(:locale)"`, such as Hyku) does **not** match a row stored as `/handle/12345/678` — that path is treated as not registered. Host apps that need locale-prefixed aliases to resolve must store each prefixed form as its own row or override `Hyrax::RedirectsLookup`.
277
+
278
+ ### Performance
279
+
280
+ The resolver does one indexed DB query per request. `hyrax_redirect_paths` is a small derived table (one row per registered alias plus one per resource with a display URL); the `from_path` unique index makes lookups sub-millisecond on any realistic load. No request-time Solr query, no application-level cache. If profiling later shows a hot path, the lookup is the obvious cache point.
281
+
282
+ ## Reindexing after enabling
283
+
284
+ Toggling the config or the Flipflop changes what the indexer writes to the Solr document. Existing records need a reindex to have the new field populated (when both gates open) or removed (when either closes):
285
+
286
+ ```sh
287
+ bundle exec rails hyrax:solr:reindex_everything
288
+ ```
289
+
290
+ ## Displaying redirect aliases on show pages
291
+
292
+ When the redirects feature is enabled, the registered aliases for a work or collection appear on its show page as a list of clickable links. The display is gated by `editor_only: true` — visible to signed-in users with edit ability for the record (which includes site admins, who can edit everything). Public visitors and signed-in users without edit ability see no trace of the redirects on the show page.
293
+
294
+ The `editor_only` flag (and its sibling `admin_only`) are general flexible-metadata property visibility flags. For the full reference — including the combination rule when both flags are set and the catalog-exclusion behavior — see [Property visibility flags](flexible_metadata.md#property-visibility-flags) in the flexible metadata documentation.
295
+
296
+ The display is driven by two pieces on the redirects schema: an `editor_only` flag that gates visibility, and a `render_as: redirects_label` instruction that selects the renderer. **The two flex modes structure these differently** because their schema loaders read view options from different places.
297
+
298
+ **Non-flexible mode (`HYRAX_FLEXIBLE=false`)** — all view options live inside the `view:` block in `config/metadata/redirects.yaml`:
299
+
300
+ ```yaml
301
+ view:
302
+ editor_only: true
303
+ render_term: redirects_path
304
+ render_as: redirects_label
305
+ html_dl: true
306
+ ```
307
+
308
+ `Hyrax::SimpleSchemaLoader#view_definitions_for` reads `property.meta['view']` and passes the block through verbatim to the show-page rendering.
309
+
310
+ **Flexible mode (`HYRAX_FLEXIBLE=true`)** — `editor_only` is an entry in the property's `indexing:` array; the rest live inside the `view:` block:
311
+
312
+ ```yaml
313
+ indexing:
314
+ - editor_only
315
+ view:
316
+ render_term: redirects_path
317
+ render_as: redirects_label
318
+ html_dl: true
319
+ ```
320
+
321
+ `Hyrax::M3SchemaLoader#view_definitions_for` calls `definition.view_options`, which reads `editor_only?` (via `Hyrax::SchemaLoader::AttributeDefinition#editor_only?`, which honors both top-level `editor_only: true` and `indexing: [editor_only]`) and injects it into the view options hash. The `view:` block contents are passed through alongside.
322
+
323
+ Full snippets for each mode are shown in the [m3 profile requirements](#m3-profile-requirements-flexible-true-mode) and [Schema details](#schema-details-flexible-false-mode) sections above.
324
+
325
+ ### How it works
326
+
327
+ - The redirects indexer writes `redirects_path_tesim` to the Solr document for show-page rendering. (The resolver does not consult Solr; it queries the `hyrax_redirect_paths` table directly.)
328
+ - `Hyrax::SolrDocument::Metadata` declares `redirects_path` as a SolrDocument attribute bound to the `redirects_path_tesim` field. This is what makes `solr_document.redirects_path` available; the per-attribute declaration is required (Hyrax does not coerce arbitrary Solr fields into methods automatically).
329
+ - The presenter's `MissingMethodBehavior` delegates `presenter.redirects_path` to `solr_document.redirects_path`.
330
+ - The `render_term: redirects_path` view option tells the show-page partial to call `presenter.redirects_path` instead of `presenter.redirects`. The bare `redirects` attribute returns the persisted array of hashes and isn't useful for direct rendering.
331
+ - The `render_as: redirects_label` view option tells Hyrax to use the `Hyrax::Renderers::RedirectsLabelAttributeRenderer` class to render the field. Each path becomes a clickable link whose text is the full absolute URL (host + path) and whose `href` is the path alone (the browser resolves it against the current host).
332
+ - The `html_dl: true` view option matches the show page's description-list layout — the renderer outputs `<dt>/<dd>` rather than the table-row `<tr>/<td>` it would default to.
333
+ - The `editor_only: true` view option makes the existing attribute-rows partial skip rendering the field entirely when the current user cannot edit the record. No section heading, no empty list — just nothing.
334
+
335
+ ### Adopter customization
336
+
337
+ Adopters can override the renderer to change link styling or the displayed URL format. Subclass `Hyrax::Renderers::RedirectsLabelAttributeRenderer` and register the subclass under the same `render_as` key.
338
+
339
+ Adopters can also turn the display off without removing the redirects feature: remove the `view:` block from the schema (or the m3 profile property declaration). The redirects functionality continues to work — only the show-page display goes away.
340
+
341
+ ## Migration playbook (Bulkrax)
342
+
343
+ Institutions migrating from another repository typically have hundreds to thousands of legacy URLs to preserve. Bulkrax (v9.5+) supports redirects via its `nested_attributes: true` field-mapping flag.
344
+
345
+ ### CSV column format
346
+
347
+ Each redirect entry maps to two numbered columns: `redirect_path_<n>` and `redirect_is_display_url_<n>`. A row with two redirects, one marked as the display URL:
348
+
349
+ ```csv
350
+ source_identifier,title,redirect_path_1,redirect_is_display_url_1,redirect_path_2,redirect_is_display_url_2
351
+ work-001,My Work,/handle/12345/678,true,/old/path/678,false
352
+ ```
353
+
354
+ `is_display_url` accepts the literal string `true` or `false` (boolean strings, not `1`/`0`). At most one entry per record may be marked as the display URL.
355
+
356
+ ### Field-mapping configuration
357
+
358
+ Add to the host app's Bulkrax field-mapping configuration (usually `config/initializers/default_bulkrax_mappings.rb` or a per-tenant override):
359
+
360
+ ```ruby
361
+ 'path' => { from: ['redirect_path'], object: 'redirects', nested_attributes: true },
362
+ 'is_display_url' => { from: ['redirect_is_display_url'], object: 'redirects', nested_attributes: true }
363
+ ```
364
+
365
+ See [field_behaviors.md](forms/field_behaviors.md#wiring-up-bulkrax-imports) for the conventions around the `nested_attributes: true` flag.
366
+
367
+ ### Reserved-prefix list
368
+
369
+ The validator rejects redirect paths that match any prefix in `Hyrax.config.reserved_redirect_prefixes`. Hyrax ships a default list covering its own routes (`/admin`, `/dashboard`, `/catalog`, `/concern`, etc.); host applications extend it for their own routes (see "Validation" above). A row whose redirect path matches a reserved prefix fails validation per-row and is reported in the import errors; the rest of the import continues.
370
+
371
+ ### Reindex after import
372
+
373
+ `hyrax_redirect_paths` rows are populated by `SyncRedirectPaths` on every save (form-driven, console, or Bulkrax import — all save paths run the transaction step). New records created via Bulkrax import get their redirect rows written automatically; no separate reindex pass is required to make the resolver see them.
374
+
375
+ If the redirects feature was just enabled (config + Flipflop turned on) and existing records need their redirects to become resolvable, run the reindex command from the section above. (Reindex re-runs the save path, which re-runs the sync step.)
376
+
377
+ ### Common errors
378
+
379
+ - **"is reserved by the application and may not be used as an alias"** — the path matches a reserved prefix. Choose a different path or extend `Hyrax.config.reserved_redirect_prefixes` if the conflict is intentional.
380
+ - **"is already in use by another record"** — the path is registered as a redirect on a different work or collection. Paths are globally unique.
381
+ - **"at most one redirect entry may be marked as the display URL"** — multiple rows in the same CSV record have `redirect_is_display_url_<n>=true`. Only one entry per record may be marked as the display URL.
382
+ - **m3 profile validation: "redirects property is required"** (flexible mode) — the Flipflop is on but the m3 profile doesn't declare the `redirects` property. Add it per the section above.
383
+ - **m3 profile validation: "the property will be ignored"** (flexible mode, config off) — a stale `redirects` property is declared but the config is off. Either remove the property from the profile or re-enable the config.
384
+
385
+ ## Disabling
386
+
387
+ To fully disable: unset `HYRAX_REDIRECTS_ENABLED` (or set `Hyrax.config.redirects_enabled = false`) and reboot. The schema is no longer loaded, the attribute is no longer included on `Hyrax::Work` / `Hyrax::PcdmCollection`, the Flipflop feature is no longer registered, and the indexer mixin is no longer included. Persisted `redirects` entries on records remain in storage (Postgres / Fedora) but are inaccessible because the attribute no longer exists on the model. Re-enabling restores access.
388
+
389
+ To disable the feature at runtime without changing the config, toggle the `:redirects` Flipflop off in the admin Flipflop UI. Multi-tenant host apps can scope this per-tenant via their Flipflop strategy chain.
390
+
391
+ ### Caveats
392
+
393
+ - **`Hyrax::Redirect` (the presenter) is always defined.** The class file is loaded by Rails autoloading as soon as anything references the constant. It costs effectively nothing when unused. The "wholly absent" effect of disabling the config applies to the schema, the attribute on `Hyrax::Work` / `Hyrax::PcdmCollection`, the Flipflop, the indexer, and the m3 profile validator's enforcement — but not to the constant itself.
394
+ - **The `disabled_schemas` filter only affects `Hyrax::SimpleSchemaLoader`.** Adopters running `flexible: true` whose m3 profile contains a stale `redirects` property will still see the attribute defined on records loaded via `M3SchemaLoader` even if `Hyrax.config.redirects_enabled?` is false. The m3 profile validator logs a warning in that situation ("the property will be ignored"); follow the warning by removing the `redirects` property from the m3 profile or setting the config back on.
395
+
396
+ ## For contributors
397
+
398
+ ### Why a config *and* a Flipflop?
399
+
400
+ The schema include on `Hyrax::Work` and `Hyrax::PcdmCollection` runs at class-load time, often triggered by Bulkrax's initializer before the Flipflop facade is wired up. `Hyrax.config.redirects_enabled?` is queryable that early; `Flipflop.redirects?` is not. The config gates structural choices (does the attribute exist on the model? does the schema YAML get loaded?); the Flipflop gates runtime behavior at request time (do routes/controllers/indexer become active?). Multi-tenant host apps can resolve the Flipflop per tenant; Hyrax itself doesn't have that concept.
401
+
402
+ ### Calling `Flipflop.redirects?`
403
+
404
+ The `:redirects` Flipflop feature is only registered when `Hyrax.config.redirects_enabled?` is true. When the config is off, calling `Flipflop.redirects?` raises `NoMethodError`. Any new code that consults the feature flag must short-circuit on the config first:
405
+
406
+ ```ruby
407
+ Hyrax.config.redirects_enabled? && Flipflop.redirects?
408
+ ```
409
+
410
+ Alternatively, gate the inclusion of the calling code itself on `Hyrax.config.redirects_enabled?` (as the indexer mixin does in `Hyrax::Indexers::PcdmObjectIndexer` and `Hyrax::Indexers::PcdmCollectionIndexer`); then the body can check `Flipflop.redirects?` alone because it only runs when the feature is registered.
411
+
412
+ ## See also
413
+
414
+ - `documentation/flexible_metadata.md` — m3 profile fundamentals and how the redirects feature interacts with flexible metadata.
415
+ - `documentation/forms/field_behaviors.md` — the Field Behavior pattern used by `Hyrax::RedirectsFieldBehavior` to wire the form's nested-attribute property.
416
+ - `Hyrax::Redirect` (`app/models/hyrax/redirect.rb`) — thin Ruby presenter for a single redirect entry; used on the form's render path.
417
+ - `Hyrax::RedirectsFieldBehavior` (`app/forms/concerns/hyrax/redirects_field_behavior.rb`) — form-side wiring for the `redirects_attributes` virtual property and the `redirects_display_url_index` radio-group scalar. Owns the populator (which folds the radio-group index into per-row `is_display_url` flags) and the `deserialize!` strip for the nested-attributes payload. The form partial wraps each persisted hash in a `Hyrax::Redirect` presenter inline at render time.
418
+ - `Hyrax::Indexers::RedirectsIndexer` (`app/indexers/hyrax/indexers/redirects_indexer.rb`) — the indexer mixin. Writes `redirects_path_tesim` to the Solr document for show-page rendering.
419
+ - `Hyrax::RedirectToDisplayUrl` (`app/controllers/concerns/hyrax/redirect_to_display_url.rb`) — controller concern included on `WorksControllerBehavior` and `CollectionsControllerBehavior`. Adds a `before_action` on show that routes UUID-URL visitors to the display alias when one is set.
420
+ - `Hyrax::SolrDocument::Metadata` (`app/models/concerns/hyrax/solr_document/metadata.rb`) — declares the `redirects_path` attribute on `SolrDocument`, bound to the `redirects_path_tesim` Solr field. This is what makes `solr_document.redirects_path` (and therefore `presenter.redirects_path` via `MissingMethodBehavior`) available to the show-page renderer.
421
+ - `Hyrax::Renderers::RedirectsLabelAttributeRenderer` (`app/renderers/hyrax/renderers/redirects_label_attribute_renderer.rb`) — show-page renderer that turns each redirect path into a clickable link.
422
+ - `Hyrax::RedirectsController` (`app/controllers/hyrax/redirects_controller.rb`) — the redirect resolver.
423
+ - `Hyrax::FlexibleSchemaValidators::RedirectsValidator` (`app/services/hyrax/flexible_schema_validators/redirects_validator.rb`) — the m3 profile validator.
424
+ - `Hyrax::RedirectValidator` (`app/validators/hyrax/redirect_validator.rb`) — the form-level entry validator.
425
+ - `Hyrax::RedirectPathNormalizer` (`app/services/hyrax/redirect_path_normalizer.rb`) — canonical-form normalization for redirect paths.
426
+ - `Hyrax::RedirectsLookup` (`app/services/hyrax/redirects_lookup.rb`) — the uniqueness lookup against `hyrax_redirect_paths`.
427
+ - `Hyrax::RedirectPath` (`app/models/hyrax/redirect_path.rb`) — ActiveRecord model for the `hyrax_redirect_paths` table.
428
+ - `Hyrax::Transactions::Steps::SyncRedirectPaths` and `Hyrax::Transactions::Steps::RemoveRedirectPaths` (`lib/hyrax/transactions/steps/`) — the transaction steps that keep the redirects table in sync with each resource's `redirects` attribute.
data/hyrax.gemspec CHANGED
@@ -38,6 +38,7 @@ SUMMARY
38
38
  spec.add_dependency 'almond-rails', '~> 0.1'
39
39
  spec.add_dependency 'awesome_nested_set', '~> 3.1'
40
40
  spec.add_dependency 'blacklight', '~> 7.29'
41
+ spec.add_dependency 'blacklight_dynamic_sitemap', '~> 1.0'
41
42
  spec.add_dependency 'blacklight-gallery', '~> 4.6.4'
42
43
  spec.add_dependency 'breadcrumbs_on_rails', '~> 3.0'
43
44
  spec.add_dependency 'browse-everything', '>= 0.16', '< 2.0'
@@ -63,10 +64,12 @@ SUMMARY
63
64
  spec.add_dependency 'hydra-works', '>= 0.16'
64
65
  spec.add_dependency 'iiif_manifest', '>= 0.3', '< 2.0'
65
66
  spec.add_dependency 'json-schema' # for Arkivo
67
+ spec.add_dependency 'json_schemer' # Required for m3 schema validation
66
68
  spec.add_dependency 'legato', '~> 0.3'
67
69
  spec.add_dependency 'linkeddata' # Required for getting values from geonames
68
70
  spec.add_dependency 'listen', '~> 3.9'
69
71
  spec.add_dependency 'mailboxer', '~> 0.12'
72
+ spec.add_dependency 'multi_json', '1.18.0' # Locked below 1.19.1 because undefined method `except' for an instance of JSON::Ext::Generator::State
70
73
  spec.add_dependency 'nest', '~> 3.1'
71
74
  spec.add_dependency 'noid-rails', '~> 3.0'
72
75
  spec.add_dependency 'oauth'
@@ -89,6 +92,7 @@ SUMMARY
89
92
  spec.add_dependency 'sprockets', '3.7.2' # 3.7.3 fails feature specs
90
93
  spec.add_dependency 'sass-rails', '~> 6.0'
91
94
  spec.add_dependency 'select2-rails', '~> 3.5'
95
+ spec.add_dependency 'language_list'
92
96
 
93
97
  spec.add_development_dependency "capybara", '~> 3.29'
94
98
  spec.add_development_dependency 'capybara-screenshot', '~> 1.0'
@@ -108,7 +112,8 @@ SUMMARY
108
112
  spec.add_development_dependency 'rails-controller-testing', '~> 1'
109
113
  # the hyrax style guide is based on `bixby`. see `.rubocop.yml`
110
114
  spec.add_development_dependency 'bixby', '~> 5.0', '>= 5.0.2' # bixby 5 briefly dropped Ruby 2.5
111
- spec.add_development_dependency 'shoulda-callback-matchers', '~> 1.1.1'
112
- spec.add_development_dependency 'shoulda-matchers', '~> 3.1'
115
+ spec.add_development_dependency 'rubocop-ast', '>= 1.17.0', '< 1.30' # pin to range compatible with rubocop 1.28.2
116
+ spec.add_development_dependency 'shoulda-callback-matchers'
117
+ spec.add_development_dependency 'shoulda-matchers'
113
118
  spec.add_development_dependency 'webmock'
114
119
  end
@@ -11,7 +11,7 @@ module Freyja
11
11
  ##
12
12
  # @return [Freyja::Persister]
13
13
  def persister
14
- @persister ||= Freyja::Persister.new(adapter: self)
14
+ @persister ||= Freyja::Persister.new(adapter: self, wings_service: Wings::Valkyrie::Persister.new(adapter: self))
15
15
  end
16
16
 
17
17
  ##
@@ -3,6 +3,13 @@
3
3
  module Freyja
4
4
  # Persister for Postgres MetadataAdapter.
5
5
  class Persister < Valkyrie::Persistence::Postgres::Persister
6
+ attr_reader :wings_service
7
+
8
+ def initialize(adapter:, wings_service:)
9
+ @wings_service = wings_service
10
+ super(adapter: adapter)
11
+ end
12
+
6
13
  # Persists a resource within the database
7
14
  #
8
15
  # Modified from the upstream to skip previously persisted check
@@ -35,7 +42,8 @@ module Freyja
35
42
  new_resource = resource_factory.to_resource(object: orm_object)
36
43
  # if the resource was wings and is now a Valkyrie resource, we need to migrate sipity, files, and members
37
44
  if Hyrax.config.valkyrie_transition? && was_wings && !new_resource.wings?
38
- MigrateFilesToValkyrieJob.perform_later(new_resource) if new_resource.is_a?(Hyrax::FileSet) && new_resource.file_ids.size == 1 && new_resource.file_ids.first.id.to_s.match('/files/')
45
+ # Check if all file_ids match the '/files/' pattern
46
+ MigrateFilesToValkyrieJob.perform_later(new_resource) if new_resource.is_a?(Hyrax::FileSet) && new_resource.file_ids.all? { |id_holder| id_holder.to_s.match('/files/') }
39
47
  # migrate any members if the resource is a Hyrax work
40
48
  if new_resource.is_a?(Hyrax::Work)
41
49
  member_ids = new_resource.member_ids.map(&:to_s)
@@ -45,5 +53,21 @@ module Freyja
45
53
  end
46
54
  new_resource
47
55
  end
56
+
57
+ # Deletes a resource persisted within the database from both postgres and fedora
58
+ # @param [Valkyrie::Resource] resource
59
+ # @return [Valkyrie::Resource] the deleted resource
60
+ def delete(resource:)
61
+ # call super to delete from postgres
62
+ super(resource: resource)
63
+ # After deletion, we need to ensure that the resource is no longer found
64
+ # because it could still exist in fedora.
65
+ fedora_record = Hyrax.query_service.find_by(id: resource.id)
66
+ wings_service.delete(resource: fedora_record) if fedora_record
67
+ resource
68
+ rescue Valkyrie::Persistence::ObjectNotFoundError
69
+ # If the resource is not found, we return the original resource
70
+ resource
71
+ end
48
72
  end
49
73
  end
@@ -69,7 +69,7 @@ class Hyrax::CollectionResourceGenerator < Rails::Generators::NamedBase # ruboco
69
69
  template('collection_form.rb.erb', filepath)
70
70
  return unless include_basic_metadata?
71
71
  inject_into_file filepath, before: /include Hyrax::FormFields/ do
72
- "include Hyrax::FormFields(:basic_metadata)\n "
72
+ "include Hyrax::FormFields(:basic_metadata) if Hyrax.config.collection_include_metadata?\n "
73
73
  end
74
74
  end
75
75
 
@@ -84,7 +84,7 @@ class Hyrax::CollectionResourceGenerator < Rails::Generators::NamedBase # ruboco
84
84
  template('collection_indexer.rb.erb', filepath)
85
85
  return unless include_basic_metadata?
86
86
  inject_into_file filepath, before: /include Hyrax::Indexer/ do
87
- "include Hyrax::Indexer(:basic_metadata)\n "
87
+ "include Hyrax::Indexer(:basic_metadata) if Hyrax.config.collection_include_metadata?\n "
88
88
  end
89
89
  end
90
90
 
@@ -30,5 +30,9 @@ class <%= class_name %> < Hyrax::PcdmCollection
30
30
  # * add Valkyrie attributes to this class
31
31
  # * update form and indexer to process the attributes
32
32
  #
33
- include Hyrax::Schema(:<%= file_name %>)
33
+ if Hyrax.config.collection_include_metadata?
34
+ include Hyrax::Schema(:basic_metadata)
35
+ include Hyrax::Schema(:collection_resource)
36
+ include Hyrax::Schema(:<%= file_name %>)
37
+ end
34
38
  end
@@ -3,5 +3,9 @@
3
3
  # Generated via
4
4
  # `rails generate hyrax:collection_resource <%= class_name %>`
5
5
  class <%= class_name %>Form < Hyrax::Forms::PcdmCollectionForm
6
- include Hyrax::FormFields(:<%= file_name %>)
6
+ if Hyrax.config.collection_include_metadata?
7
+ include Hyrax::FormFields(:basic_metadata)
8
+ include Hyrax::FormFields(:<%= file_name %>)
9
+ end
10
+ check_if_flexible(<%= class_name %>)
7
11
  end
@@ -3,5 +3,10 @@
3
3
  # Generated via
4
4
  # `rails generate hyrax:collection_resource <%= class_name %>`
5
5
  class <%= class_name %>Indexer < Hyrax::Indexers::PcdmCollectionIndexer
6
- include Hyrax::Indexer(:<%= file_name %>)
6
+ if Hyrax.config.collection_include_metadata?
7
+ include Hyrax::Indexer(:basic_metadata)
8
+ include Hyrax::Indexer(:<%= file_name %>)
9
+ end
10
+
11
+ check_if_flexible(<%= class_name %>)
7
12
  end
@@ -0,0 +1,22 @@
1
+ Description:
2
+ This generator installs a IIIF viewer in your app and configures it
3
+ as the default viewer for audiovisual files.
4
+
5
+ Currently, the only option is Clover (https://samvera-labs.github.io/clover-iiif/),
6
+ but we may add more in future.
7
+
8
+ To remove the viewer:
9
+ rails destroy hyrax:iiif_viewer VIEWER
10
+
11
+ Example:
12
+
13
+ rails generate hyrax:iiif_viewer clover
14
+
15
+ will create the following files:
16
+ - public/clover/clover.css
17
+ - public/clover/clover.js
18
+ - public/clover/clover.html
19
+ - app/views/hyrax/base/iiif_viewers/_clover.html.erb
20
+
21
+ This will also:
22
+ - set the IIIF AV viewer to Clover in your config/initializers/hyrax.rb