alchemy_cms 8.3.11 → 8.4.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 (368) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +58 -10
  4. data/app/assets/builds/alchemy/admin/page-select.css +1 -1
  5. data/app/assets/builds/alchemy/admin/print.css +1 -1
  6. data/app/assets/builds/alchemy/admin/remote-select.css +1 -0
  7. data/app/assets/builds/alchemy/admin/tags-autocomplete.css +1 -0
  8. data/app/assets/builds/alchemy/admin.css +4 -4
  9. data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
  10. data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
  11. data/app/assets/builds/alchemy/dark-theme.css +1 -1
  12. data/app/assets/builds/alchemy/light-theme.css +1 -1
  13. data/app/assets/builds/alchemy/preview.min.js +1 -1
  14. data/app/assets/builds/alchemy/theme.css +1 -1
  15. data/app/assets/builds/alchemy/welcome.css +1 -1
  16. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  17. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
  18. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  19. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
  20. data/app/assets/images/alchemy/icons-sprite.svg +1 -1
  21. data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +1 -1
  22. data/app/components/alchemy/admin/dashboard/widgets/news_reader.html.erb +38 -0
  23. data/app/components/alchemy/admin/dashboard/widgets/news_reader.rb +54 -0
  24. data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +1 -1
  25. data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +1 -1
  26. data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +11 -3
  27. data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +6 -1
  28. data/app/components/alchemy/admin/element_editor.html.erb +1 -1
  29. data/app/components/alchemy/admin/element_select.rb +30 -20
  30. data/app/components/alchemy/admin/element_status_indicators.html.erb +16 -0
  31. data/app/components/alchemy/admin/element_status_indicators.rb +31 -0
  32. data/app/components/alchemy/admin/link_dialog/external_tab.rb +2 -1
  33. data/app/components/alchemy/admin/main_navigation/entry.rb +61 -0
  34. data/app/components/alchemy/admin/main_navigation/menu.rb +21 -0
  35. data/app/components/alchemy/admin/main_navigation/sub_navigation.rb +31 -0
  36. data/app/components/alchemy/admin/page_node.html.erb +1 -1
  37. data/app/components/alchemy/admin/page_permitted_roles_select.html.erb +11 -0
  38. data/app/components/alchemy/admin/page_permitted_roles_select.rb +35 -0
  39. data/app/components/alchemy/admin/page_select.rb +18 -8
  40. data/app/components/alchemy/admin/preview_time_select.rb +1 -1
  41. data/app/components/alchemy/admin/toolbar_button.rb +43 -20
  42. data/app/components/alchemy/admin/uploader_button.rb +66 -0
  43. data/app/components/alchemy/ingredients/base_editor.rb +10 -1
  44. data/app/components/alchemy/ingredients/boolean_editor.rb +1 -0
  45. data/app/components/alchemy/ingredients/color_editor.rb +6 -3
  46. data/app/components/alchemy/ingredients/datetime_view.rb +1 -1
  47. data/app/components/alchemy/ingredients/picture_view.rb +2 -4
  48. data/app/components/alchemy/menubar.html.erb +157 -0
  49. data/app/components/alchemy/menubar.rb +15 -0
  50. data/app/controllers/alchemy/admin/attachments_controller.rb +12 -9
  51. data/app/controllers/alchemy/admin/base_controller.rb +34 -21
  52. data/app/controllers/alchemy/admin/dashboard_controller.rb +4 -0
  53. data/app/controllers/alchemy/admin/elements_controller.rb +4 -23
  54. data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
  55. data/app/controllers/alchemy/admin/nodes_controller.rb +1 -1
  56. data/app/controllers/alchemy/admin/pages_controller.rb +1 -1
  57. data/app/controllers/alchemy/admin/pictures_controller.rb +6 -3
  58. data/app/controllers/alchemy/admin/resources_controller.rb +5 -5
  59. data/app/controllers/alchemy/admin/sites_controller.rb +1 -1
  60. data/app/controllers/alchemy/base_controller.rb +24 -16
  61. data/app/controllers/alchemy/pages_controller.rb +29 -8
  62. data/app/controllers/alchemy/sitemap_controller.rb +58 -0
  63. data/app/controllers/concerns/alchemy/admin/csp_protection.rb +43 -0
  64. data/app/controllers/concerns/alchemy/admin/current_language.rb +1 -1
  65. data/app/controllers/concerns/alchemy/admin/resource_filter.rb +3 -1
  66. data/{lib → app/controllers/concerns}/alchemy/controller_actions.rb +13 -0
  67. data/app/controllers/concerns/alchemy/legacy_page_redirects.rb +4 -4
  68. data/app/helpers/alchemy/admin/base_helper.rb +4 -1
  69. data/app/helpers/alchemy/admin/form_helper.rb +0 -2
  70. data/app/helpers/alchemy/admin/navigation_helper.rb +10 -181
  71. data/app/helpers/alchemy/elements_block_helper.rb +14 -0
  72. data/app/helpers/alchemy/resources_helper.rb +15 -1
  73. data/app/javascript/alchemy_admin/components/attachment_select.js +11 -20
  74. data/app/javascript/alchemy_admin/components/auto_submit.js +2 -4
  75. data/app/javascript/alchemy_admin/components/button.js +0 -6
  76. data/app/javascript/alchemy_admin/components/clear_schedule_button.js +21 -0
  77. data/app/javascript/alchemy_admin/components/color_select.js +50 -45
  78. data/app/javascript/alchemy_admin/components/conditional_field.js +50 -0
  79. data/app/javascript/alchemy_admin/components/dirty_guard.js +77 -0
  80. data/app/javascript/alchemy_admin/components/dom_id_select.js +17 -10
  81. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +7 -2
  82. data/app/javascript/alchemy_admin/components/element_editor.js +29 -56
  83. data/app/javascript/alchemy_admin/components/element_select.js +26 -66
  84. data/app/javascript/alchemy_admin/components/icon.js +9 -4
  85. data/app/javascript/alchemy_admin/{image_cropper.js → components/image_cropper.js} +39 -53
  86. data/app/javascript/alchemy_admin/components/image_overlay_link.js +14 -0
  87. data/app/javascript/alchemy_admin/components/index.js +12 -1
  88. data/app/javascript/alchemy_admin/components/main_navi_entry.js +75 -0
  89. data/app/javascript/alchemy_admin/components/menu_node.js +73 -0
  90. data/app/javascript/alchemy_admin/components/news_reader.js +109 -0
  91. data/app/javascript/alchemy_admin/components/node_folder.js +36 -0
  92. data/app/javascript/alchemy_admin/components/node_form.js +34 -0
  93. data/app/javascript/alchemy_admin/components/node_select.js +25 -30
  94. data/app/javascript/alchemy_admin/components/node_tree.js +56 -0
  95. data/app/javascript/alchemy_admin/components/page_node.js +2 -1
  96. data/app/javascript/alchemy_admin/components/page_select.js +16 -34
  97. data/app/javascript/alchemy_admin/components/picture_archive.js +92 -0
  98. data/app/javascript/alchemy_admin/components/picture_editor.js +2 -1
  99. data/app/javascript/alchemy_admin/components/preview_window.js +11 -3
  100. data/app/javascript/alchemy_admin/components/remote_select.js +461 -81
  101. data/app/javascript/alchemy_admin/components/select.js +26 -88
  102. data/app/javascript/alchemy_admin/components/sitemap.js +2 -1
  103. data/app/javascript/alchemy_admin/components/sortable_elements.js +2 -1
  104. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +63 -48
  105. data/app/javascript/alchemy_admin/components/tinymce.js +2 -1
  106. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +3 -2
  107. data/app/javascript/alchemy_admin/components/uploader.js +19 -3
  108. data/app/javascript/alchemy_admin/confirm_dialog.js +7 -1
  109. data/app/javascript/alchemy_admin/dialog.js +182 -110
  110. data/app/javascript/alchemy_admin/hotkeys.js +0 -5
  111. data/app/javascript/alchemy_admin/i18n.js +3 -9
  112. data/app/javascript/alchemy_admin/image_overlay.js +58 -42
  113. data/app/javascript/alchemy_admin/initializer.js +2 -2
  114. data/app/javascript/alchemy_admin/link_dialog.js +15 -2
  115. data/app/javascript/alchemy_admin/routes.js +18 -0
  116. data/app/javascript/alchemy_admin/turbo_stream_actions.js +88 -0
  117. data/app/javascript/alchemy_admin/utils/ajax.js +14 -3
  118. data/app/javascript/alchemy_admin/utils/events.js +5 -0
  119. data/app/javascript/alchemy_admin/utils/json_script.js +12 -0
  120. data/app/javascript/alchemy_admin/utils/tom_select.js +171 -0
  121. data/app/javascript/alchemy_admin.js +2 -19
  122. data/app/javascript/preview.js +131 -113
  123. data/app/jobs/alchemy/invalidate_elements_cache_job.rb +1 -1
  124. data/app/models/alchemy/admin/content_security_policy.rb +143 -0
  125. data/app/models/alchemy/admin/filters/base.rb +7 -0
  126. data/app/models/alchemy/admin/filters/datepicker.rb +1 -1
  127. data/app/models/alchemy/admin/filters/select.rb +20 -7
  128. data/app/models/alchemy/attachment.rb +14 -7
  129. data/app/models/alchemy/cache_control.rb +104 -0
  130. data/app/models/alchemy/current.rb +1 -1
  131. data/app/models/alchemy/eager_loading.rb +0 -2
  132. data/app/models/alchemy/element.rb +23 -8
  133. data/app/models/alchemy/element_definition.rb +1 -0
  134. data/app/models/alchemy/elements_repository.rb +11 -0
  135. data/app/models/alchemy/folded_page.rb +5 -1
  136. data/app/models/alchemy/ingredient.rb +26 -0
  137. data/app/models/alchemy/ingredients/datetime.rb +1 -1
  138. data/app/models/alchemy/ingredients/link.rb +2 -0
  139. data/app/models/alchemy/ingredients/picture.rb +3 -0
  140. data/app/models/alchemy/ingredients/text.rb +2 -0
  141. data/app/models/alchemy/language.rb +19 -7
  142. data/app/models/alchemy/legacy_page_url.rb +6 -1
  143. data/app/models/alchemy/node.rb +51 -0
  144. data/app/models/alchemy/page/page_natures.rb +41 -8
  145. data/app/models/alchemy/page/page_scopes.rb +23 -0
  146. data/app/models/alchemy/page/sitemap_cache_key.rb +56 -0
  147. data/app/models/alchemy/page.rb +88 -32
  148. data/app/models/alchemy/page_definition.rb +1 -1
  149. data/app/models/alchemy/page_mutex.rb +16 -0
  150. data/app/models/alchemy/page_version.rb +24 -1
  151. data/app/models/alchemy/permissions.rb +6 -5
  152. data/app/models/alchemy/picture.rb +15 -7
  153. data/app/models/alchemy/picture_description.rb +22 -0
  154. data/app/models/alchemy/picture_thumb.rb +18 -0
  155. data/app/models/alchemy/publishable/timestamp_resolver.rb +70 -0
  156. data/app/models/alchemy/resource.rb +1 -1
  157. data/app/models/alchemy/safe_url_validator.rb +34 -0
  158. data/app/models/alchemy/site.rb +8 -3
  159. data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +7 -3
  160. data/app/models/alchemy/storage_adapter/active_storage.rb +4 -1
  161. data/app/models/alchemy/tag.rb +10 -4
  162. data/{lib → app/models/concerns}/alchemy/filetypes.rb +5 -0
  163. data/app/models/concerns/alchemy/picture_thumbnails.rb +61 -6
  164. data/app/models/concerns/alchemy/publishable.rb +14 -27
  165. data/app/models/concerns/alchemy/taggable.rb +92 -0
  166. data/app/models/concerns/alchemy/user_methods.rb +95 -0
  167. data/app/services/alchemy/duplicate_element.rb +5 -2
  168. data/app/services/alchemy/element_preloader.rb +1 -2
  169. data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
  170. data/app/stylesheets/alchemy/_extends.scss +0 -8
  171. data/app/stylesheets/alchemy/_themes.scss +10 -9
  172. data/app/stylesheets/alchemy/admin/_empty-state.scss +29 -0
  173. data/app/stylesheets/alchemy/admin/_help.scss +10 -0
  174. data/app/stylesheets/alchemy/admin/_tom-select.scss +104 -15
  175. data/app/stylesheets/alchemy/admin/archive.scss +56 -11
  176. data/app/stylesheets/alchemy/admin/base.scss +4 -5
  177. data/app/stylesheets/alchemy/admin/buttons.scss +12 -1
  178. data/app/stylesheets/alchemy/admin/dashboard.scss +150 -20
  179. data/app/stylesheets/alchemy/admin/dialogs.scss +27 -36
  180. data/app/stylesheets/alchemy/admin/element-select.scss +1 -1
  181. data/app/stylesheets/alchemy/admin/elements.scss +8 -24
  182. data/app/stylesheets/alchemy/admin/flash.scss +2 -1
  183. data/app/stylesheets/alchemy/admin/forms.scss +60 -24
  184. data/app/stylesheets/alchemy/admin/frame.scss +40 -4
  185. data/app/stylesheets/alchemy/admin/image_library.scss +9 -12
  186. data/app/stylesheets/alchemy/admin/navigation.scss +17 -14
  187. data/app/stylesheets/alchemy/admin/node-select.scss +13 -37
  188. data/app/stylesheets/alchemy/admin/nodes.scss +1 -1
  189. data/app/stylesheets/alchemy/admin/page-select.scss +6 -97
  190. data/app/stylesheets/alchemy/admin/print.scss +0 -4
  191. data/app/stylesheets/alchemy/admin/remote-select.scss +170 -0
  192. data/app/stylesheets/alchemy/admin/search.scss +2 -3
  193. data/app/stylesheets/alchemy/admin/selects.scss +1 -294
  194. data/app/stylesheets/alchemy/admin/sitemap.scss +2 -1
  195. data/app/stylesheets/alchemy/admin/tables.scss +7 -1
  196. data/app/stylesheets/alchemy/admin/tags-autocomplete.scss +17 -0
  197. data/app/stylesheets/alchemy/admin/tags.scss +0 -13
  198. data/app/stylesheets/alchemy/admin/toolbar.scss +9 -6
  199. data/app/stylesheets/alchemy/admin/upload.scss +61 -1
  200. data/app/stylesheets/alchemy/admin.scss +4 -1
  201. data/app/types/alchemy/cache_control_type.rb +84 -0
  202. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  203. data/app/views/alchemy/_menubar.html.erb +2 -154
  204. data/app/views/alchemy/_preview_mode_code.html.erb +0 -3
  205. data/app/views/alchemy/admin/_left_menu.html.erb +18 -5
  206. data/app/views/alchemy/admin/_top_menu.html.erb +1 -1
  207. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +5 -4
  208. data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
  209. data/app/views/alchemy/admin/attachments/_files_list.html.erb +71 -83
  210. data/app/views/alchemy/admin/attachments/assign.turbo_stream.erb +8 -0
  211. data/app/views/alchemy/admin/attachments/index.html.erb +32 -7
  212. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  213. data/app/views/alchemy/admin/crop.html.erb +20 -20
  214. data/app/views/alchemy/admin/dashboard/_footer.html.erb +0 -3
  215. data/app/views/alchemy/admin/dashboard/_top.html.erb +1 -0
  216. data/app/views/alchemy/admin/dashboard/help.html.erb +84 -57
  217. data/app/views/alchemy/admin/dashboard/license.html.erb +6 -0
  218. data/app/views/alchemy/admin/elements/_footer.html.erb +0 -1
  219. data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
  220. data/app/views/alchemy/admin/elements/_header.html.erb +2 -19
  221. data/app/views/alchemy/admin/elements/_preview_text_quote.html.erb +3 -0
  222. data/app/views/alchemy/admin/elements/_schedule.html.erb +7 -7
  223. data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -1
  224. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +1 -1
  225. data/app/views/alchemy/admin/elements/index.html.erb +1 -1
  226. data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +2 -2
  227. data/app/views/alchemy/admin/elements/update.turbo_stream.erb +31 -0
  228. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +6 -3
  229. data/app/views/alchemy/admin/languages/_table.html.erb +0 -2
  230. data/app/views/alchemy/admin/languages/index.html.erb +25 -29
  231. data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +1 -3
  232. data/app/views/alchemy/admin/layoutpages/edit.html.erb +1 -1
  233. data/app/views/alchemy/admin/layoutpages/index.html.erb +26 -7
  234. data/app/views/alchemy/admin/nodes/_form.html.erb +3 -21
  235. data/app/views/alchemy/admin/nodes/_node.html.erb +93 -88
  236. data/app/views/alchemy/admin/nodes/index.html.erb +15 -23
  237. data/app/views/alchemy/admin/pages/_create_language_form.html.erb +2 -6
  238. data/app/views/alchemy/admin/pages/_form.html.erb +2 -1
  239. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +1 -1
  240. data/app/views/alchemy/admin/pages/_table.html.erb +2 -4
  241. data/app/views/alchemy/admin/pages/_toolbar.html.erb +2 -1
  242. data/app/views/alchemy/admin/pages/edit.html.erb +10 -21
  243. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +1 -1
  244. data/app/views/alchemy/admin/pages/index.html.erb +26 -31
  245. data/app/views/alchemy/admin/pages/info.html.erb +2 -2
  246. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +2 -2
  247. data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +1 -1
  248. data/app/views/alchemy/admin/partials/_routes.html.erb +24 -48
  249. data/app/views/alchemy/admin/pictures/_archive.html.erb +3 -17
  250. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
  251. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +5 -3
  252. data/app/views/alchemy/admin/pictures/_form.html.erb +0 -1
  253. data/app/views/alchemy/admin/pictures/_infos.html.erb +5 -1
  254. data/app/views/alchemy/admin/pictures/_picture.html.erb +1 -1
  255. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -4
  256. data/app/views/alchemy/admin/pictures/assign.turbo_stream.erb +7 -0
  257. data/app/views/alchemy/admin/pictures/index.html.erb +38 -30
  258. data/app/views/alchemy/admin/pictures/show.html.erb +41 -41
  259. data/app/views/alchemy/admin/resources/_empty_state.html.erb +27 -0
  260. data/app/views/alchemy/admin/resources/_pagination.html.erb +1 -1
  261. data/app/views/alchemy/admin/resources/_per_page_select.html.erb +1 -1
  262. data/app/views/alchemy/admin/resources/_resource_table.html.erb +0 -2
  263. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +3 -2
  264. data/app/views/alchemy/admin/resources/index.html.erb +32 -13
  265. data/app/views/alchemy/admin/sites/index.html.erb +24 -22
  266. data/app/views/alchemy/admin/styleguide/index.html.erb +35 -12
  267. data/app/views/alchemy/admin/tags/index.html.erb +27 -25
  268. data/app/views/alchemy/admin/tinymce/_setup.html.erb +6 -8
  269. data/app/views/alchemy/admin/translations/_en.js +3 -0
  270. data/app/views/alchemy/admin/uploader/_button.html.erb +11 -17
  271. data/app/views/alchemy/admin/uploader/_setup.html.erb +3 -3
  272. data/app/views/alchemy/pages/sitemap.xml.erb +2 -0
  273. data/app/views/kaminari/alchemy/_first_page.html.erb +1 -2
  274. data/app/views/kaminari/alchemy/_gap.html.erb +0 -1
  275. data/app/views/kaminari/alchemy/_last_page.html.erb +1 -2
  276. data/app/views/kaminari/alchemy/_next_page.html.erb +1 -2
  277. data/app/views/kaminari/alchemy/_page.html.erb +1 -2
  278. data/app/views/kaminari/alchemy/_paginator.html.erb +0 -1
  279. data/app/views/kaminari/alchemy/_prev_page.html.erb +1 -2
  280. data/app/views/layouts/alchemy/admin/dialog.html.erb +3 -0
  281. data/app/views/layouts/alchemy/admin.html.erb +5 -6
  282. data/config/brakeman.ignore +23 -1
  283. data/config/importmap.rb +10 -8
  284. data/config/initializers/dragonfly.rb +21 -17
  285. data/config/locales/alchemy.en.yml +52 -11
  286. data/config/routes.rb +3 -3
  287. data/db/migrate/20260727120000_add_permitted_roles_to_alchemy_pages.rb +23 -0
  288. data/db/migrate/20260820120000_add_cached_tag_list_to_taggables.rb +57 -0
  289. data/lib/alchemy/configuration/number_option.rb +13 -0
  290. data/lib/alchemy/configuration/proc_option.rb +13 -0
  291. data/lib/alchemy/configuration.rb +2 -0
  292. data/lib/alchemy/configurations/admin_components.rb +30 -0
  293. data/lib/alchemy/configurations/format_matchers.rb +7 -1
  294. data/lib/alchemy/configurations/main.rb +103 -4
  295. data/lib/alchemy/configurations/menu.rb +91 -0
  296. data/lib/alchemy/configurations/menu_item.rb +123 -0
  297. data/lib/alchemy/configurations/sitemap.rb +1 -0
  298. data/lib/alchemy/configurations/uploader.rb +15 -1
  299. data/lib/alchemy/engine.rb +0 -6
  300. data/lib/alchemy/i18n.rb +26 -0
  301. data/lib/alchemy/modules.rb +52 -50
  302. data/lib/alchemy/tasks/generate_thumbnails.rb +99 -0
  303. data/lib/alchemy/tasks/tidy.rb +1 -1
  304. data/lib/alchemy/test_support/capybara_helpers.rb +48 -37
  305. data/lib/alchemy/test_support/current_language_shared_examples.rb +1 -1
  306. data/lib/alchemy/test_support/factories/dummy_user_factory.rb +4 -1
  307. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +132 -0
  308. data/lib/alchemy/test_support/shared_ingredient_examples.rb +52 -0
  309. data/lib/alchemy/test_support/shared_publishable_examples.rb +29 -4
  310. data/lib/alchemy/upgrader/eight_four.rb +237 -0
  311. data/lib/alchemy/upgrader/register_module_rewriter.rb +177 -0
  312. data/lib/alchemy/upgrader.rb +2 -1
  313. data/lib/alchemy/version.rb +1 -1
  314. data/lib/alchemy.rb +0 -1
  315. data/lib/alchemy_cms.rb +0 -13
  316. data/lib/generators/alchemy/elements/templates/view.html.erb +1 -1
  317. data/lib/generators/alchemy/elements/templates/view.html.haml +1 -1
  318. data/lib/generators/alchemy/elements/templates/view.html.slim +1 -1
  319. data/lib/generators/alchemy/install/install_generator.rb +2 -13
  320. data/lib/generators/alchemy/install/templates/alchemy.rb.tt +24 -9
  321. data/lib/generators/alchemy/module/module_generator.rb +1 -0
  322. data/lib/generators/alchemy/module/templates/ability.rb.tt +1 -1
  323. data/lib/generators/alchemy/module/templates/module_config.rb.tt +16 -18
  324. data/lib/generators/alchemy/user_columns_migration/templates/migration.rb.tt +19 -0
  325. data/lib/generators/alchemy/user_columns_migration/user_columns_migration_generator.rb +30 -0
  326. data/lib/tasks/alchemy/thumbnails.rake +16 -29
  327. data/lib/tasks/alchemy/upgrade.rake +45 -3
  328. data/vendor/javascript/cropperjs.min.js +3 -3
  329. data/vendor/javascript/floating-ui.min.js +1 -1
  330. data/vendor/javascript/tinymce.min.js +5 -5
  331. data/vendor/javascript/tom-select.min.js +1 -1
  332. metadata +85 -65
  333. data/app/javascript/alchemy_admin/components/action.js +0 -50
  334. data/app/javascript/alchemy_admin/dirty.js +0 -55
  335. data/app/javascript/alchemy_admin/node_tree.js +0 -82
  336. data/app/javascript/alchemy_admin/picture_selector.js +0 -67
  337. data/app/javascript/alchemy_admin/templates/compiled.js +0 -1
  338. data/app/javascript/alchemy_admin/templates/node_folder.hbs +0 -3
  339. data/app/stylesheets/alchemy/admin/attachment-select.scss +0 -19
  340. data/app/views/alchemy/admin/_main_navi.html.erb +0 -6
  341. data/app/views/alchemy/admin/attachments/assign.js.erb +0 -15
  342. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +0 -30
  343. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +0 -20
  344. data/app/views/alchemy/admin/partials/_sub_navigation.html.erb +0 -11
  345. data/app/views/alchemy/admin/pictures/assign.js.erb +0 -13
  346. data/app/views/alchemy/base/error_notice.js.erb +0 -1
  347. data/app/views/alchemy/base/permission_denied.js.erb +0 -3
  348. data/app/views/alchemy/base/redirect.js.erb +0 -11
  349. data/config/alchemy/config.yml +0 -217
  350. data/config/alchemy/modules.yml +0 -82
  351. data/config/initializers/rails_live_reload.rb +0 -13
  352. data/lib/alchemy/dev_support/live_reload_watcher.rb +0 -5
  353. data/lib/alchemy/taggable.rb +0 -49
  354. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -34
  355. data/vendor/assets/stylesheets/alchemy_admin/select2.css +0 -1
  356. data/vendor/javascript/handlebars.min.js +0 -29
  357. data/vendor/javascript/jquery.min.js +0 -2
  358. data/vendor/javascript/rails-ujs.min.js +0 -1
  359. data/vendor/javascript/select2.min.js +0 -23
  360. /data/{lib → app/controllers/concerns}/alchemy/ability_helper.rb +0 -0
  361. /data/{lib → app/controllers/concerns}/alchemy/admin/locale.rb +0 -0
  362. /data/{lib → app/controllers/concerns}/alchemy/admin/preview_time.rb +0 -0
  363. /data/{lib → app/controllers/concerns}/alchemy/admin/timezone.rb +0 -0
  364. /data/{lib → app/controllers/concerns}/alchemy/configuration_methods.rb +0 -0
  365. /data/{lib → app/models}/alchemy/admin/preview_url.rb +0 -0
  366. /data/{lib → app/models/concerns}/alchemy/hints.rb +0 -0
  367. /data/{lib → app/models/concerns}/alchemy/name_conversions.rb +0 -0
  368. /data/{lib → app/services}/alchemy/elements_finder.rb +0 -0
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ # The Content Security Policy Alchemy applies to its own admin responses.
6
+ #
7
+ # Not configured by default. Opt in with:
8
+ #
9
+ # Alchemy.config.admin_content_security_policy = "Alchemy::Admin::ContentSecurityPolicy"
10
+ #
11
+ # 'self' follows the origin the admin is served from, so mounting it under
12
+ # its own subdomain through +Alchemy.admin_constraints+ needs no extra
13
+ # configuration. The page preview is same origin as well, because it falls
14
+ # back to an admin path, unless a preview host is configured, in which case
15
+ # that host is added to +frame-src+.
16
+ #
17
+ # +img-src+ is the exception: it allows any https host, because the host
18
+ # serving the pictures is not knowable here. Narrow it in a subclass, or
19
+ # keep pictures same origin with
20
+ # +config.active_storage.resolve_model_to_route = :rails_storage_proxy+.
21
+ #
22
+ # Assets your application adds through +admin_stylesheets+ or through
23
+ # +Alchemy.importmap+ are picked up automatically, so a module pinned to a
24
+ # CDN does not need any extra configuration. Subclass to allow anything
25
+ # else, and configure the subclass instead:
26
+ #
27
+ # class AdminContentSecurityPolicy < Alchemy::Admin::ContentSecurityPolicy
28
+ # def call
29
+ # super.tap do |policy|
30
+ # policy.connect_src(*policy.directives["connect-src"], "https://api.example.com")
31
+ # end
32
+ # end
33
+ # end
34
+ #
35
+ class ContentSecurityPolicy
36
+ class InvalidSourceError < StandardError; end
37
+
38
+ # Rails calls an asset host proc with the asset source, so we need one to
39
+ # evaluate it with. Any source resolves to the same origin unless the
40
+ # host shards, which the %d form covers separately.
41
+ ASSET_SOURCE = "/"
42
+
43
+ def initialize(request = nil)
44
+ @request = request
45
+ end
46
+
47
+ attr_reader :request
48
+ # Send the policy as Content-Security-Policy-Report-Only, which reports
49
+ # violations to the browser console and to +report_uri+ without blocking
50
+ # anything. Worth running first, to find what a policy would break.
51
+ def report_only? = false
52
+
53
+ # Authorizes the inline scripts Alchemy renders. It has to be
54
+ # unguessable, otherwise injected markup could carry a valid nonce.
55
+ def nonce_generator = ->(_request) { SecureRandom.base64(16) }
56
+
57
+ def call
58
+ ActionDispatch::ContentSecurityPolicy.new do |policy|
59
+ policy.default_src :self
60
+ # script-src and style-src have to be named explicitly, because Rails
61
+ # only appends the nonce to directives the policy actually declares.
62
+ policy.script_src :self, *asset_hosts, *importmap_origins
63
+ policy.style_src :self, *asset_hosts, *admin_stylesheet_origins
64
+ # Inline style attributes cannot carry a nonce. Turbo sets them for
65
+ # its progress bar, and so do several of our bundled dependencies.
66
+ policy.style_src_attr :unsafe_inline
67
+ # Pictures come from wherever the storage backend puts them, which
68
+ # cannot be read from here. ActiveStorage redirects to a signed
69
+ # service URL, and a redirect still has to match the host.
70
+ policy.img_src :self, :data, :blob, :https
71
+ policy.font_src :self, :data, *asset_hosts
72
+ policy.media_src :self, :blob, *asset_hosts
73
+ policy.connect_src :self, *asset_hosts
74
+ policy.frame_src :self, *preview_hosts
75
+ policy.object_src :none
76
+ policy.base_uri :self
77
+ policy.form_action :self
78
+ # An endpoint of your own that the browser POSTs a JSON report to
79
+ # whenever it blocks something:
80
+ #
81
+ # policy.report_uri "/csp-violation-reports"
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def asset_hosts
88
+ host = ActionController::Base.asset_host
89
+ return [] if host.blank?
90
+
91
+ hosts = if host.respond_to?(:call)
92
+ [call_asset_host(host)]
93
+ elsif host.include?("%d")
94
+ # Rails shards these over four hosts.
95
+ 4.times.map { host % _1 }
96
+ else
97
+ [host]
98
+ end
99
+ hosts.filter_map { origin(_1) }.uniq
100
+ end
101
+
102
+ def call_asset_host(host)
103
+ arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity
104
+ args = [ASSET_SOURCE]
105
+ args << request if request && (arity > 1 || arity < 0)
106
+ host.call(*args)
107
+ end
108
+
109
+ # Modules can be pinned to a CDN, in which case the pin holds an absolute
110
+ # URL instead of an asset name.
111
+ def importmap_origins
112
+ Alchemy.importmap.packages.values.filter_map { origin(_1.path) }.uniq
113
+ end
114
+
115
+ # Admin stylesheets are usually asset names, but an absolute URL is
116
+ # allowed here too.
117
+ def admin_stylesheet_origins
118
+ Alchemy.config.admin_stylesheets.filter_map { origin(_1) }.uniq
119
+ end
120
+
121
+ # A CSP source is an origin, so anything with a host contributes one and
122
+ # everything else (asset names, module specifiers) is same origin already.
123
+ def origin(url)
124
+ uri = URI.parse(url.to_s)
125
+ return nil unless uri.host
126
+
127
+ port = ":#{uri.port}" if uri.port && uri.port != uri.default_port
128
+ "#{uri.scheme ? "#{uri.scheme}://" : "//"}#{uri.host}#{port}"
129
+ rescue URI::InvalidURIError => error
130
+ raise InvalidSourceError, "Cannot build a Content Security Policy source from " \
131
+ "#{url.inspect} (#{error.message}). It came from your asset host, your " \
132
+ "admin stylesheets, an importmap pin or a preview host."
133
+ end
134
+
135
+ # The page editor renders the frontend of the application in an iframe,
136
+ # which is a different host whenever a preview host is configured.
137
+ def preview_hosts
138
+ preview = Alchemy.config.preview
139
+ ([preview.host] + preview.per_site_configs.map(&:host)).filter_map { origin(_1) }.uniq
140
+ end
141
+ end
142
+ end
143
+ end
@@ -11,6 +11,13 @@ module Alchemy
11
11
  @resource_name = resource_name
12
12
  end
13
13
 
14
+ # The strong parameters entries needed to permit this filter's value(s)
15
+ # under `q`. Override in subclasses whose input can submit a shape
16
+ # other than a single scalar value (e.g. an array from `<select multiple>`).
17
+ def permitted_search_params
18
+ [name]
19
+ end
20
+
14
21
  def applied_filter_component(search_filter_params:, resource_url_proxy:, query:)
15
22
  Alchemy::Admin::Resource::AppliedFilter.new(
16
23
  link: dismiss_filter_url(search_filter_params, resource_url_proxy),
@@ -45,7 +45,7 @@ module Alchemy
45
45
  when :time
46
46
  ::I18n.t(:time, scope: [:time, :formats, :alchemy])
47
47
  end
48
- ::I18n.l(date, format: format)
48
+ Alchemy.l(date, format: format)
49
49
  end
50
50
  end
51
51
  end
@@ -10,9 +10,13 @@ module Alchemy
10
10
  # @param name [String] The name of the filter.
11
11
  # @param resource_name [String] The name of the resource.
12
12
  # @param options [Proc, Array] A proc that returns the options for the select, or an array of options.
13
- def initialize(name:, resource_name:, options:)
13
+ # @param multiple [Boolean, Proc] Whether multiple values can be selected, or a proc called with the search filter params that returns a Boolean.
14
+ # @param include_blank [Boolean, String, Proc] Whether (or with what label) to include a blank option, or a proc called with the search filter params that returns a Boolean/String.
15
+ def initialize(name:, resource_name:, options:, multiple: false, include_blank: true)
14
16
  super(name:, resource_name:)
15
17
  @options = options_to_proc(options)
18
+ @multiple = bool_or_proc(multiple)
19
+ @include_blank = bool_or_proc(include_blank)
16
20
  end
17
21
 
18
22
  # Returns a select filter component.
@@ -31,18 +35,27 @@ module Alchemy
31
35
  )
32
36
  end
33
37
 
38
+ # `multiple` can be enabled dynamically via a Proc, so both the
39
+ # scalar and array shape of the submitted value need to be permitted.
40
+ def permitted_search_params
41
+ [name, {name => []}]
42
+ end
43
+
34
44
  private
35
45
 
36
46
  def multiple?(params)
37
- params[:only].presence&.many? || params[:except].present?
47
+ @multiple.call(params)
38
48
  end
39
49
 
40
50
  def include_blank(params)
41
- if params[:only].present? || params[:except].present?
42
- false
43
- else
44
- Alchemy.t(:all, scope: [:filters, resource_name, name])
45
- end
51
+ value = @include_blank.call(params)
52
+ return value unless value == true
53
+
54
+ Alchemy.t(:all, scope: [:filters, resource_name, name])
55
+ end
56
+
57
+ def bool_or_proc(value)
58
+ value.is_a?(Proc) ? value : ->(_params) { value }
46
59
  end
47
60
 
48
61
  def options_to_proc(options)
@@ -5,16 +5,23 @@
5
5
  # Table name: alchemy_attachments
6
6
  #
7
7
  # id :integer not null, primary key
8
- # name :string
9
- # file_name :string
8
+ # cached_tag_list :text
10
9
  # file_mime_type :string
10
+ # file_name :string
11
11
  # file_size :integer
12
- # creator_id :integer
13
- # updater_id :integer
12
+ # file_uid :string
13
+ # name :string
14
14
  # created_at :datetime not null
15
15
  # updated_at :datetime not null
16
- # cached_tag_list :text
17
- # file_uid :string
16
+ # creator_id :integer
17
+ # updater_id :integer
18
+ #
19
+ # Indexes
20
+ #
21
+ # index_alchemy_attachments_on_created_at (created_at)
22
+ # index_alchemy_attachments_on_creator_id (creator_id)
23
+ # index_alchemy_attachments_on_file_uid (file_uid)
24
+ # index_alchemy_attachments_on_updater_id (updater_id)
18
25
  #
19
26
 
20
27
  module Alchemy
@@ -199,7 +206,7 @@ module Alchemy
199
206
  private
200
207
 
201
208
  def file_type_allowed
202
- unless extension&.in?(self.class.allowed_filetypes)
209
+ unless extension&.downcase&.in?(self.class.allowed_filetypes.map(&:downcase))
203
210
  errors.add(:file, Alchemy.t("not a valid file"))
204
211
  end
205
212
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ # Normalized HTTP Cache-Control directives derived from a page layout `cache`
5
+ # or element `page_cache` config value. Combine two values with `#&` to get the
6
+ # most restrictive of the two — this is how the page layout and its elements
7
+ # are merged (elements can only tighten, never loosen).
8
+ class CacheControl
9
+ VISIBILITIES = [:public, :private].freeze
10
+
11
+ # @param value [nil, true, false, Integer, Hash, CacheControl]
12
+ # @return [CacheControl]
13
+ def self.parse(value)
14
+ case value
15
+ when CacheControl then value
16
+ when nil, true then new(default: true)
17
+ when false then new(no_store: true)
18
+ when Integer then new(max_age: value)
19
+ when Hash then from_hash(value)
20
+ else new(default: true)
21
+ end
22
+ end
23
+
24
+ def self.from_hash(hash)
25
+ options = hash.transform_keys(&:to_sym)
26
+ new(
27
+ no_store: !!options[:no_store],
28
+ no_cache: !!options[:no_cache],
29
+ max_age: options[:max_age],
30
+ visibility: (options[:visibility] || :public).to_sym
31
+ )
32
+ end
33
+ private_class_method :from_hash
34
+
35
+ attr_reader :max_age, :visibility
36
+
37
+ def initialize(no_store: false, no_cache: false, max_age: nil, visibility: :public, default: false)
38
+ @no_store = no_store
39
+ @no_cache = no_cache
40
+ @max_age = max_age
41
+ @visibility = visibility
42
+ @default = default
43
+ end
44
+
45
+ def no_store?
46
+ @no_store
47
+ end
48
+
49
+ def no_cache?
50
+ @no_cache
51
+ end
52
+
53
+ def default?
54
+ @default
55
+ end
56
+
57
+ # Whether the response is flagged `public` to HTTP caches. A no-cache
58
+ # response is never public: Rails only pairs no-cache with public, and
59
+ # "public" adds nothing to an always-revalidated response.
60
+ def public?
61
+ visibility == :public && !no_cache?
62
+ end
63
+
64
+ def private?
65
+ visibility == :private
66
+ end
67
+
68
+ # Most restrictive of self and other.
69
+ def &(other)
70
+ return self if other.nil?
71
+ return self.class.new(no_store: true) if no_store? || other.no_store?
72
+
73
+ vis = (private? || other.private?) ? :private : :public
74
+ return self.class.new(no_cache: true, visibility: vis) if no_cache? || other.no_cache?
75
+
76
+ self.class.new(visibility: vis, max_age: min_max_age(other))
77
+ end
78
+
79
+ def restrict_visibility
80
+ return self if no_store? || private?
81
+
82
+ self.class.new(no_cache: no_cache?, max_age: max_age, visibility: :private)
83
+ end
84
+
85
+ def ==(other)
86
+ other.is_a?(CacheControl) &&
87
+ no_store? == other.no_store? &&
88
+ no_cache? == other.no_cache? &&
89
+ max_age == other.max_age &&
90
+ visibility == other.visibility
91
+ end
92
+ alias_method :eql?, :==
93
+
94
+ def hash
95
+ [no_store?, no_cache?, max_age, visibility].hash
96
+ end
97
+
98
+ private
99
+
100
+ def min_max_age(other)
101
+ [max_age, other.max_age].compact.min
102
+ end
103
+ end
104
+ end
@@ -1,6 +1,6 @@
1
1
  module Alchemy
2
2
  class Current < ActiveSupport::CurrentAttributes
3
- attribute :preview_page, :preview_time, :page, :language, :site
3
+ attribute :preview_page, :preview_time, :page, :language, :site, :user
4
4
 
5
5
  def language
6
6
  super || Language.default
@@ -18,14 +18,12 @@ module Alchemy
18
18
  raise UnsupportedPageVersion unless version.in? PAGE_VERSIONS
19
19
 
20
20
  [
21
- :tags,
22
21
  {
23
22
  :language => :site,
24
23
  version => {
25
24
  elements: [
26
25
  :page,
27
26
  :touchable_pages,
28
- :tags,
29
27
  {
30
28
  ingredients: :related_object
31
29
  }
@@ -5,20 +5,34 @@
5
5
  # Table name: alchemy_elements
6
6
  #
7
7
  # id :integer not null, primary key
8
+ # cached_tag_list :text
9
+ # fixed :boolean default(FALSE), not null
10
+ # folded :boolean default(FALSE), not null
8
11
  # name :string
9
12
  # position :integer
10
- # page_version_id :integer not null
11
- # public_on :timestamp
12
- # public_until :timestamp
13
- # fixed :boolean default(FALSE)
14
- # folded :boolean default(FALSE)
15
- # unique :boolean default(FALSE)
13
+ # public_on :datetime
14
+ # public_until :datetime
15
+ # unique :boolean default(FALSE), not null
16
16
  # created_at :datetime not null
17
17
  # updated_at :datetime not null
18
18
  # creator_id :integer
19
- # updater_id :integer
20
- # cached_tag_list :text
19
+ # page_version_id :integer not null
21
20
  # parent_element_id :integer
21
+ # updater_id :integer
22
+ #
23
+ # Indexes
24
+ #
25
+ # idx_alchemy_elements_on_page_version_id_and_parent_element_id (page_version_id,parent_element_id)
26
+ # idx_alchemy_elements_on_page_version_id_and_position (page_version_id,position)
27
+ # index_alchemy_elements_on_creator_id (creator_id)
28
+ # index_alchemy_elements_on_fixed (fixed)
29
+ # index_alchemy_elements_on_page_version_id (page_version_id)
30
+ # index_alchemy_elements_on_public_on_and_public_until (public_on,public_until)
31
+ # index_alchemy_elements_on_updater_id (updater_id)
32
+ #
33
+ # Foreign Keys
34
+ #
35
+ # page_version_id (page_version_id => alchemy_page_versions.id) ON DELETE => cascade
22
36
  #
23
37
 
24
38
  require_dependency "alchemy/element/definitions"
@@ -49,6 +63,7 @@ module Alchemy
49
63
  "ingredients",
50
64
  "message",
51
65
  "nestable_elements",
66
+ "page_cache",
52
67
  "searchable",
53
68
  "taggable",
54
69
  "warning"
@@ -13,6 +13,7 @@ module Alchemy
13
13
  attribute :taggable, :boolean, default: false
14
14
  attribute :compact, :boolean, default: false
15
15
  attribute :fixed, :boolean, default: false
16
+ attribute :page_cache, Alchemy::CacheControlType.new, default: true
16
17
  attribute :ingredients, default: []
17
18
  attribute :nestable_elements, default: []
18
19
  attribute :autogenerate, default: []
@@ -125,6 +125,17 @@ module Alchemy
125
125
  elements.each(&)
126
126
  end
127
127
 
128
+ # Allows the repository to be passed to the ActionView collection renderer
129
+ # (+render repository+), which detects a collection by +to_ary+.
130
+ def to_ary
131
+ elements
132
+ end
133
+
134
+ def size
135
+ elements.size
136
+ end
137
+ alias_method :length, :size
138
+
128
139
  private
129
140
 
130
141
  attr_reader :elements
@@ -5,9 +5,13 @@
5
5
  # Table name: alchemy_folded_pages
6
6
  #
7
7
  # id :integer not null, primary key
8
+ # folded :boolean default(FALSE), not null
8
9
  # page_id :integer not null
9
10
  # user_id :integer not null
10
- # folded :boolean default(FALSE)
11
+ #
12
+ # Indexes
13
+ #
14
+ # index_alchemy_folded_pages_on_page_id_and_user_id (page_id,user_id) UNIQUE
11
15
  #
12
16
 
13
17
  module Alchemy
@@ -1,5 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # == Schema Information
4
+ #
5
+ # Table name: alchemy_ingredients
6
+ #
7
+ # id :integer not null, primary key
8
+ # data :json
9
+ # related_object_type :string
10
+ # role :string not null
11
+ # type :string not null
12
+ # value :text
13
+ # created_at :datetime not null
14
+ # updated_at :datetime not null
15
+ # element_id :integer not null
16
+ # related_object_id :integer
17
+ #
18
+ # Indexes
19
+ #
20
+ # idx_alchemy_ingredient_relation (related_object_id,related_object_type)
21
+ # index_alchemy_ingredients_on_element_id (element_id)
22
+ # index_alchemy_ingredients_on_element_id_and_role (element_id,role) UNIQUE
23
+ # index_alchemy_ingredients_on_type (type)
24
+ #
25
+ # Foreign Keys
26
+ #
27
+ # element_id (element_id => alchemy_elements.id) ON DELETE => cascade
28
+ #
3
29
  module Alchemy
4
30
  class Ingredient < BaseRecord
5
31
  class DefinitionError < StandardError; end
@@ -15,7 +15,7 @@ module Alchemy
15
15
  def preview_text(_maxlength = nil)
16
16
  return "" unless value
17
17
 
18
- ::I18n.l(value, format: :"alchemy.ingredient_date")
18
+ Alchemy.l(value, format: :"alchemy.ingredient_date")
19
19
  end
20
20
  end
21
21
  end
@@ -12,6 +12,8 @@ module Alchemy
12
12
 
13
13
  allow_settings %i[text]
14
14
 
15
+ validates_with Alchemy::SafeUrlValidator, attributes: [:value]
16
+
15
17
  alias_method :link, :value
16
18
  end
17
19
  end
@@ -31,6 +31,7 @@ module Alchemy
31
31
  crop
32
32
  css_classes
33
33
  fixed_ratio
34
+ format
34
35
  linkable
35
36
  size
36
37
  sizes
@@ -38,6 +39,8 @@ module Alchemy
38
39
  upsample
39
40
  ]
40
41
 
42
+ validates_with Alchemy::SafeUrlValidator, attributes: [:link]
43
+
41
44
  delegate :description_for, :name, to: :picture, allow_nil: true
42
45
 
43
46
  def alt_text(language: Alchemy::Current.language)
@@ -23,6 +23,8 @@ module Alchemy
23
23
  input_type
24
24
  linkable
25
25
  ]
26
+
27
+ validates_with Alchemy::SafeUrlValidator, attributes: [:link]
26
28
  end
27
29
  end
28
30
  end
@@ -5,19 +5,31 @@
5
5
  # Table name: alchemy_languages
6
6
  #
7
7
  # id :integer not null, primary key
8
- # name :string
9
- # language_code :string
8
+ # country_code :string default(""), not null
9
+ # default :boolean default(FALSE), not null
10
10
  # frontpage_name :string
11
+ # language_code :string
12
+ # locale :string
13
+ # name :string
11
14
  # page_layout :string default("intro")
12
- # public :boolean default(FALSE)
15
+ # public :boolean default(FALSE), not null
13
16
  # created_at :datetime not null
14
17
  # updated_at :datetime not null
15
18
  # creator_id :integer
16
- # updater_id :integer
17
- # default :boolean default(FALSE)
18
- # country_code :string default(""), not null
19
19
  # site_id :integer not null
20
- # locale :string
20
+ # updater_id :integer
21
+ #
22
+ # Indexes
23
+ #
24
+ # index_alchemy_languages_on_creator_id (creator_id)
25
+ # index_alchemy_languages_on_language_code (language_code)
26
+ # index_alchemy_languages_on_language_code_and_country_code (language_code,country_code)
27
+ # index_alchemy_languages_on_site_id (site_id)
28
+ # index_alchemy_languages_on_updater_id (updater_id)
29
+ #
30
+ # Foreign Keys
31
+ #
32
+ # site_id (site_id => alchemy_sites.id)
21
33
  #
22
34
 
23
35
  require_dependency "alchemy/site"
@@ -6,9 +6,14 @@
6
6
  #
7
7
  # id :integer not null, primary key
8
8
  # urlname :string not null
9
- # page_id :integer not null
10
9
  # created_at :datetime not null
11
10
  # updated_at :datetime not null
11
+ # page_id :integer not null
12
+ #
13
+ # Indexes
14
+ #
15
+ # index_alchemy_legacy_page_urls_on_page_id (page_id)
16
+ # index_alchemy_legacy_page_urls_on_urlname (urlname)
12
17
  #
13
18
 
14
19
  class Alchemy::LegacyPageUrl < ActiveRecord::Base