alchemy_cms 7.4.10 → 8.0.0.b

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.

Potentially problematic release.


This version of alchemy_cms might be problematic. Click here for more details.

Files changed (425) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -5
  3. data/app/assets/builds/alchemy/admin/page-select.css +1 -1
  4. data/app/assets/builds/alchemy/admin/print.css +1 -1
  5. data/app/assets/builds/alchemy/admin.css +2 -2
  6. data/app/assets/builds/alchemy/dark-theme.css +1 -0
  7. data/app/assets/builds/alchemy/light-theme.css +1 -0
  8. data/app/assets/builds/alchemy/theme.css +1 -0
  9. data/app/assets/builds/alchemy/welcome.css +1 -1
  10. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  11. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -0
  12. data/app/assets/builds/tinymce/skins/ui/alchemy/content.min.css +1 -0
  13. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  14. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/content.min.css +1 -0
  15. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -0
  16. data/app/assets/config/alchemy_manifest.js +0 -2
  17. data/app/assets/images/alchemy/element_icons/default.svg +1 -0
  18. data/app/assets/images/alchemy/icons-sprite.svg +1 -0
  19. data/app/components/alchemy/admin/element_select.rb +39 -0
  20. data/app/components/alchemy/admin/resource/applied_filter.rb +29 -0
  21. data/app/components/alchemy/admin/resource/checkbox_filter.rb +36 -0
  22. data/app/components/alchemy/admin/resource/datepicker_filter.rb +42 -0
  23. data/app/components/alchemy/admin/resource/select_filter.rb +43 -0
  24. data/app/components/alchemy/admin/toolbar_button.rb +5 -2
  25. data/app/components/alchemy/ingredients/number_view.rb +18 -0
  26. data/app/controllers/alchemy/admin/attachments_controller.rb +10 -15
  27. data/app/controllers/alchemy/admin/clipboard_controller.rb +2 -6
  28. data/app/controllers/alchemy/admin/elements_controller.rb +3 -1
  29. data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
  30. data/app/controllers/alchemy/admin/pages_controller.rb +15 -15
  31. data/app/controllers/alchemy/admin/pictures_controller.rb +27 -37
  32. data/app/controllers/alchemy/admin/resources_controller.rb +16 -106
  33. data/app/controllers/alchemy/attachments_controller.rb +43 -14
  34. data/app/controllers/alchemy/messages_controller.rb +1 -1
  35. data/app/controllers/alchemy/pages_controller.rb +26 -4
  36. data/app/controllers/concerns/alchemy/admin/resource_filter.rb +93 -0
  37. data/app/decorators/alchemy/element_editor.rb +5 -48
  38. data/app/decorators/alchemy/ingredient_editor.rb +3 -53
  39. data/app/helpers/alchemy/admin/attachments_helper.rb +5 -5
  40. data/app/helpers/alchemy/admin/base_helper.rb +14 -84
  41. data/app/helpers/alchemy/admin/pages_helper.rb +1 -1
  42. data/app/helpers/alchemy/base_helper.rb +0 -30
  43. data/app/helpers/alchemy/elements_block_helper.rb +0 -14
  44. data/app/helpers/alchemy/pages_helper.rb +1 -1
  45. data/{lib → app/helpers}/alchemy/resources_helper.rb +5 -45
  46. data/app/javascript/alchemy_admin/components/action.js +2 -0
  47. data/app/javascript/alchemy_admin/components/alchemy_html_element.js +3 -3
  48. data/app/javascript/alchemy_admin/components/auto_submit.js +20 -0
  49. data/app/javascript/alchemy_admin/components/datepicker.js +18 -7
  50. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +8 -7
  51. data/app/javascript/alchemy_admin/components/element_editor.js +25 -15
  52. data/app/javascript/alchemy_admin/components/element_select.js +43 -0
  53. data/app/javascript/alchemy_admin/components/index.js +3 -0
  54. data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
  55. data/app/javascript/alchemy_admin/components/remote_select.js +9 -2
  56. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +5 -1
  57. data/app/javascript/alchemy_admin/components/tinymce.js +93 -14
  58. data/app/javascript/alchemy_admin/components/update_check.js +42 -0
  59. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +15 -8
  60. data/app/javascript/alchemy_admin/components/uploader/progress.js +12 -6
  61. data/app/javascript/alchemy_admin/components/uploader.js +4 -2
  62. data/app/javascript/alchemy_admin/confirm_dialog.js +27 -57
  63. data/app/javascript/alchemy_admin/dialog.js +1 -1
  64. data/app/javascript/alchemy_admin/dirty.js +3 -2
  65. data/app/javascript/alchemy_admin/file_editors.js +1 -1
  66. data/app/javascript/alchemy_admin/i18n.js +15 -16
  67. data/app/javascript/alchemy_admin/image_loader.js +4 -2
  68. data/app/javascript/alchemy_admin/initializer.js +1 -49
  69. data/app/javascript/alchemy_admin/picture_editors.js +7 -4
  70. data/app/javascript/alchemy_admin/picture_selector.js +4 -4
  71. data/app/javascript/alchemy_admin/utils/ajax.js +51 -44
  72. data/app/javascript/alchemy_admin.js +3 -8
  73. data/app/jobs/alchemy/delete_picture_job.rb +12 -0
  74. data/app/models/alchemy/admin/filters/base.rb +38 -0
  75. data/app/models/alchemy/admin/filters/checkbox.rb +24 -0
  76. data/app/models/alchemy/admin/filters/datepicker.rb +53 -0
  77. data/app/models/alchemy/admin/filters/select.rb +70 -0
  78. data/app/models/alchemy/admin/resource_name.rb +27 -0
  79. data/app/models/alchemy/attachment.rb +49 -44
  80. data/app/models/alchemy/base_record.rb +2 -0
  81. data/app/models/alchemy/element/definitions.rb +1 -1
  82. data/app/models/alchemy/element/element_ingredients.rb +6 -6
  83. data/app/models/alchemy/element/presenters.rb +3 -12
  84. data/app/models/alchemy/element.rb +10 -27
  85. data/app/models/alchemy/element_definition.rb +190 -0
  86. data/app/models/alchemy/ingredient.rb +10 -43
  87. data/app/models/alchemy/ingredient_definition.rb +134 -0
  88. data/app/models/alchemy/ingredient_validator.rb +7 -3
  89. data/app/models/alchemy/ingredients/number.rb +19 -0
  90. data/app/models/alchemy/language.rb +2 -21
  91. data/app/models/alchemy/message.rb +3 -7
  92. data/app/models/alchemy/node.rb +1 -1
  93. data/app/models/alchemy/page/{page_layouts.rb → definitions.rb} +12 -19
  94. data/app/models/alchemy/page/fixed_attributes.rb +1 -1
  95. data/app/models/alchemy/page/page_elements.rb +13 -14
  96. data/app/models/alchemy/page/page_naming.rb +3 -11
  97. data/app/models/alchemy/page/page_natures.rb +20 -15
  98. data/app/models/alchemy/page/page_scopes.rb +1 -1
  99. data/app/models/alchemy/page.rb +12 -39
  100. data/app/models/alchemy/page_definition.rb +115 -0
  101. data/app/models/alchemy/picture.rb +71 -99
  102. data/app/models/alchemy/picture_variant.rb +115 -5
  103. data/{lib → app/models}/alchemy/resource.rb +4 -18
  104. data/{lib → app/models}/alchemy/searchable_resource.rb +15 -0
  105. data/app/models/alchemy/site/layout.rb +5 -5
  106. data/app/models/alchemy/site.rb +1 -21
  107. data/app/models/alchemy/storage_adapter/active_storage/attachment_url.rb +41 -0
  108. data/app/models/alchemy/storage_adapter/active_storage/picture_url.rb +55 -0
  109. data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +40 -0
  110. data/app/models/alchemy/storage_adapter/active_storage.rb +173 -0
  111. data/app/models/alchemy/{attachment/url.rb → storage_adapter/dragonfly/attachment_url.rb} +12 -12
  112. data/app/models/alchemy/storage_adapter/dragonfly/picture_url.rb +75 -0
  113. data/app/models/alchemy/{picture → storage_adapter/dragonfly}/preprocessor.rb +4 -4
  114. data/app/models/alchemy/storage_adapter/dragonfly.rb +205 -0
  115. data/app/models/alchemy/storage_adapter.rb +74 -0
  116. data/app/models/concerns/alchemy/picture_thumbnails.rb +19 -6
  117. data/app/models/concerns/alchemy/relatable_resource.rb +28 -0
  118. data/app/serializers/alchemy/element_serializer.rb +0 -1
  119. data/app/services/alchemy/dragonfly_to_image_processing.rb +100 -0
  120. data/app/stylesheets/alchemy/_custom-properties.scss +162 -0
  121. data/app/stylesheets/alchemy/_defaults.scss +3 -0
  122. data/app/stylesheets/alchemy/_extends.scss +69 -0
  123. data/app/{assets/stylesheets → stylesheets}/alchemy/_mixins.scss +40 -68
  124. data/app/stylesheets/alchemy/_themes.scss +540 -0
  125. data/app/stylesheets/alchemy/_variables.scss +5 -0
  126. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/archive.scss +45 -42
  127. data/app/stylesheets/alchemy/admin/attachments.scss +17 -0
  128. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/base.scss +20 -15
  129. data/app/stylesheets/alchemy/admin/buttons.scss +135 -0
  130. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/clipboard.scss +2 -2
  131. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/dashboard.scss +13 -16
  132. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/dialogs.scss +33 -16
  133. data/app/stylesheets/alchemy/admin/element-select.scss +11 -0
  134. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/elements.scss +239 -133
  135. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/errors.scss +5 -5
  136. data/app/stylesheets/alchemy/admin/filters.scss +57 -0
  137. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/flatpickr.scss +54 -76
  138. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/form_fields.scss +10 -11
  139. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/forms.scss +28 -21
  140. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/frame.scss +20 -18
  141. data/app/stylesheets/alchemy/admin/hints.scss +5 -0
  142. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/icons.scss +1 -1
  143. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/image_library.scss +21 -61
  144. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/images.scss +1 -1
  145. data/app/stylesheets/alchemy/admin/labels.scss +5 -0
  146. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/lists.scss +3 -3
  147. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/navigation.scss +55 -59
  148. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/node-select.scss +1 -10
  149. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/nodes.scss +21 -18
  150. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/notices.scss +20 -19
  151. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/page-select.scss +18 -2
  152. data/app/stylesheets/alchemy/admin/pagination.scss +137 -0
  153. data/app/stylesheets/alchemy/admin/preview_window.scss +46 -0
  154. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/print.scss +1 -1
  155. data/app/stylesheets/alchemy/admin/resource_info.scss +148 -0
  156. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/search.scss +10 -7
  157. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/selects.scss +85 -46
  158. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/shoelace.scss +37 -68
  159. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/sitemap.scss +39 -34
  160. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/tables.scss +9 -7
  161. data/app/stylesheets/alchemy/admin/tags.scss +143 -0
  162. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/toolbar.scss +6 -6
  163. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/typography.scss +3 -6
  164. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/upload.scss +7 -5
  165. data/app/stylesheets/alchemy/admin.scss +44 -0
  166. data/app/stylesheets/alchemy/dark-theme.scss +5 -0
  167. data/app/stylesheets/alchemy/light-theme.scss +6 -0
  168. data/app/stylesheets/alchemy/theme.scss +13 -0
  169. data/app/stylesheets/alchemy/welcome.scss +75 -0
  170. data/app/stylesheets/tinymce/skins/content/alchemy/content.scss +69 -0
  171. data/app/{assets/stylesheets/tinymce/skins/content/alchemy → stylesheets/tinymce/skins/content/alchemy-dark}/content.scss +12 -12
  172. data/app/stylesheets/tinymce/skins/ui/alchemy/content.scss +1 -0
  173. data/app/{assets/stylesheets → stylesheets}/tinymce/skins/ui/alchemy/skin.scss +158 -176
  174. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/content.scss +1 -0
  175. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +3784 -0
  176. data/app/views/alchemy/admin/attachments/_files_list.html.erb +22 -12
  177. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  178. data/app/views/alchemy/admin/attachments/assign.js.erb +4 -3
  179. data/app/views/alchemy/admin/attachments/show.html.erb +55 -43
  180. data/app/views/alchemy/admin/{elements/_clipboard_button.html.erb → clipboard/_button.html.erb} +3 -5
  181. data/app/views/alchemy/admin/clipboard/_update_nested_element_button.turbo_stream.erb +11 -0
  182. data/app/views/alchemy/admin/clipboard/clear.turbo_stream.erb +4 -0
  183. data/app/views/alchemy/admin/clipboard/index.html.erb +15 -13
  184. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +18 -0
  185. data/app/views/alchemy/admin/clipboard/remove.turbo_stream.erb +9 -0
  186. data/app/views/alchemy/admin/crop.html.erb +1 -1
  187. data/app/views/alchemy/admin/dashboard/info.html.erb +17 -31
  188. data/app/views/alchemy/admin/elements/_element.html.erb +4 -8
  189. data/app/views/alchemy/admin/elements/_form.html.erb +9 -9
  190. data/app/views/alchemy/admin/elements/_header.html.erb +5 -1
  191. data/app/views/alchemy/admin/elements/_toolbar.html.erb +4 -6
  192. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +2 -1
  193. data/app/views/alchemy/admin/elements/index.html.erb +2 -2
  194. data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +3 -16
  195. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +0 -9
  196. data/app/views/alchemy/admin/languages/_form.html.erb +1 -1
  197. data/app/views/alchemy/admin/languages/_table.html.erb +1 -1
  198. data/app/views/alchemy/admin/languages/index.html.erb +5 -2
  199. data/app/views/alchemy/admin/layoutpages/index.html.erb +1 -12
  200. data/app/views/alchemy/admin/pages/_form.html.erb +2 -2
  201. data/app/views/alchemy/admin/pages/_page.html.erb +2 -3
  202. data/app/views/alchemy/admin/pages/_toolbar.html.erb +1 -15
  203. data/app/views/alchemy/admin/pages/index.html.erb +1 -1
  204. data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +9 -12
  205. data/app/views/alchemy/admin/partials/_search_form.html.erb +4 -9
  206. data/app/views/alchemy/admin/pictures/_archive.html.erb +16 -29
  207. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +3 -7
  208. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +1 -1
  209. data/app/views/alchemy/admin/pictures/_form.html.erb +1 -1
  210. data/app/views/alchemy/admin/pictures/_infos.html.erb +21 -52
  211. data/app/views/alchemy/admin/pictures/_library_sidebar.html.erb +7 -0
  212. data/app/views/alchemy/admin/pictures/_picture.html.erb +14 -20
  213. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +20 -16
  214. data/app/views/alchemy/admin/pictures/_sorting_select.html.erb +13 -0
  215. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +1 -1
  216. data/app/views/alchemy/admin/pictures/edit_multiple.html.erb +1 -6
  217. data/app/views/alchemy/admin/pictures/index.html.erb +5 -19
  218. data/app/views/alchemy/admin/pictures/show.html.erb +10 -5
  219. data/app/views/alchemy/admin/resources/_applied_filters.html.erb +8 -0
  220. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +5 -25
  221. data/app/views/alchemy/admin/resources/_pagination.html.erb +6 -0
  222. data/app/views/alchemy/admin/resources/_per_page_select.html.erb +4 -2
  223. data/app/views/alchemy/admin/resources/_resource_table.html.erb +1 -1
  224. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +36 -0
  225. data/app/views/alchemy/admin/resources/_table_header.html.erb +1 -15
  226. data/app/views/alchemy/admin/sites/index.html.erb +5 -1
  227. data/app/views/alchemy/admin/styleguide/index.html.erb +118 -58
  228. data/app/views/alchemy/admin/tags/index.html.erb +1 -1
  229. data/app/views/alchemy/admin/tinymce/_setup.html.erb +7 -7
  230. data/app/{javascript/alchemy_admin/locales/en.js → views/alchemy/admin/translations/_en.js} +5 -2
  231. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -1
  232. data/app/views/alchemy/admin/uploader/_setup.html.erb +4 -4
  233. data/app/views/alchemy/base/error_notice.html.erb +1 -1
  234. data/app/views/alchemy/ingredients/_number_editor.html.erb +24 -0
  235. data/app/views/alchemy/no_index.html.erb +31 -0
  236. data/app/views/alchemy/welcome.html.erb +12 -10
  237. data/app/views/kaminari/alchemy/_first_page.html.erb +5 -3
  238. data/app/views/kaminari/alchemy/_last_page.html.erb +5 -3
  239. data/app/views/kaminari/alchemy/_next_page.html.erb +5 -3
  240. data/app/views/kaminari/alchemy/_paginator.html.erb +18 -13
  241. data/app/views/kaminari/alchemy/_prev_page.html.erb +5 -3
  242. data/app/views/layouts/alchemy/admin.html.erb +25 -24
  243. data/config/alchemy/config.yml +3 -2
  244. data/config/initializers/dragonfly.rb +0 -1
  245. data/config/initializers/mime_types.rb +1 -0
  246. data/config/locales/alchemy.en.yml +57 -21
  247. data/config/routes.rb +0 -2
  248. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +14 -0
  249. data/lib/alchemy/admin/preview_url.rb +4 -5
  250. data/lib/alchemy/cache_digests/template_tracker.rb +6 -9
  251. data/lib/alchemy/config_missing.rb +14 -0
  252. data/lib/alchemy/configuration/base_option.rb +24 -0
  253. data/lib/alchemy/configuration/boolean_option.rb +16 -0
  254. data/lib/alchemy/configuration/class_option.rb +15 -0
  255. data/lib/alchemy/configuration/class_set_option.rb +46 -0
  256. data/lib/alchemy/configuration/integer_list_option.rb +13 -0
  257. data/lib/alchemy/configuration/integer_option.rb +12 -0
  258. data/lib/alchemy/configuration/list_option.rb +22 -0
  259. data/lib/alchemy/configuration/regexp_option.rb +11 -0
  260. data/lib/alchemy/configuration/string_list_option.rb +13 -0
  261. data/lib/alchemy/configuration/string_option.rb +11 -0
  262. data/lib/alchemy/configuration.rb +115 -0
  263. data/lib/alchemy/configuration_methods.rb +3 -1
  264. data/lib/alchemy/configurations/default_language.rb +12 -0
  265. data/lib/alchemy/configurations/default_site.rb +10 -0
  266. data/lib/alchemy/configurations/format_matchers.rb +11 -0
  267. data/lib/alchemy/configurations/mailer.rb +16 -0
  268. data/lib/alchemy/configurations/main.rb +223 -0
  269. data/lib/alchemy/configurations/page_cache.rb +19 -0
  270. data/lib/alchemy/configurations/preview.rb +32 -0
  271. data/lib/alchemy/configurations/sitemap.rb +10 -0
  272. data/lib/alchemy/configurations/uploader.rb +34 -0
  273. data/lib/alchemy/engine.rb +81 -24
  274. data/lib/alchemy/hints.rb +3 -7
  275. data/lib/alchemy/install/tasks.rb +0 -12
  276. data/lib/alchemy/on_page_layout.rb +2 -2
  277. data/lib/alchemy/propshaft/tinymce_asset.rb +15 -0
  278. data/lib/alchemy/seeder.rb +2 -2
  279. data/lib/alchemy/tasks/tidy.rb +18 -0
  280. data/lib/alchemy/tasks/usage.rb +4 -4
  281. data/lib/alchemy/test_support/config_stubbing.rb +1 -7
  282. data/lib/alchemy/test_support/factories/attachment_factory.rb +13 -2
  283. data/lib/alchemy/test_support/factories/language_factory.rb +1 -1
  284. data/lib/alchemy/test_support/factories/page_factory.rb +2 -3
  285. data/lib/alchemy/test_support/factories/picture_factory.rb +31 -2
  286. data/lib/alchemy/test_support/factories/site_factory.rb +2 -2
  287. data/lib/alchemy/test_support/having_crop_action_examples.rb +2 -2
  288. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +80 -26
  289. data/lib/alchemy/test_support/relatable_resource_examples.rb +58 -0
  290. data/lib/alchemy/test_support/shared_ingredient_examples.rb +5 -5
  291. data/lib/alchemy/tinymce.rb +0 -1
  292. data/lib/alchemy/upgrader/eight_zero.rb +14 -0
  293. data/lib/alchemy/upgrader.rb +33 -20
  294. data/lib/alchemy/version.rb +1 -1
  295. data/lib/alchemy.rb +185 -172
  296. data/lib/alchemy_cms.rb +1 -7
  297. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +0 -3
  298. data/lib/generators/alchemy/install/files/_article.html.erb +6 -4
  299. data/lib/generators/alchemy/install/files/alchemy.en.yml +22 -3
  300. data/lib/generators/alchemy/install/files/application.html.erb +5 -0
  301. data/lib/generators/alchemy/install/install_generator.rb +25 -23
  302. data/lib/generators/alchemy/install/templates/alchemy.rb.tt +200 -0
  303. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -1
  304. data/lib/generators/alchemy/install/templates/elements.yml.tt +3 -1
  305. data/lib/generators/alchemy/install/templates/menus.yml.tt +1 -1
  306. data/lib/generators/alchemy/install/templates/page_layouts.yml.tt +2 -2
  307. data/lib/generators/alchemy/page_layouts/page_layouts_generator.rb +2 -2
  308. data/lib/tasks/alchemy/assets.rake +14 -0
  309. data/lib/tasks/alchemy/tidy.rake +6 -0
  310. data/lib/tasks/alchemy/upgrade.rake +12 -47
  311. data/vendor/assets/stylesheets/tinymce/skins/content/dark/content.min.css +1 -0
  312. data/vendor/assets/stylesheets/tinymce/skins/content/default/content.min.css +1 -0
  313. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide/skin.min.css +1 -0
  314. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/content.min.css +1 -0
  315. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/skin.min.css +1 -0
  316. data/vendor/javascript/clipboard.min.js +1 -1
  317. data/vendor/javascript/cropperjs.min.js +1 -1
  318. data/vendor/javascript/handlebars.min.js +3 -3
  319. data/vendor/javascript/jquery.min.js +1 -1
  320. data/vendor/javascript/select2.min.js +3 -3
  321. data/vendor/javascript/shoelace.min.js +92 -76
  322. data/vendor/javascript/sortable.min.js +2 -2
  323. data/vendor/javascript/tinymce.min.js +1 -1
  324. data/vendor/javascript/ungap-custom-elements.min.js +2 -2
  325. metadata +223 -208
  326. data/CHANGELOG.md +0 -2041
  327. data/CODE_OF_CONDUCT.md +0 -13
  328. data/CONTRIBUTING.md +0 -73
  329. data/Gemfile +0 -71
  330. data/Rakefile +0 -102
  331. data/SECURITY.md +0 -13
  332. data/alchemy_cms.gemspec +0 -88
  333. data/app/assets/builds/alchemy/admin/page-select.css.map +0 -1
  334. data/app/assets/builds/alchemy/admin/print.css.map +0 -1
  335. data/app/assets/builds/alchemy/admin.css.map +0 -1
  336. data/app/assets/builds/alchemy/custom-properties.css +0 -1
  337. data/app/assets/builds/alchemy/custom-properties.css.map +0 -1
  338. data/app/assets/builds/alchemy/welcome.css.map +0 -1
  339. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css.map +0 -1
  340. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css.map +0 -1
  341. data/app/assets/javascripts/alchemy/admin.js +0 -10
  342. data/app/assets/stylesheets/alchemy/_defaults.scss +0 -3
  343. data/app/assets/stylesheets/alchemy/_deprecated_variables.scss +0 -45
  344. data/app/assets/stylesheets/alchemy/_deprecation.scss +0 -17
  345. data/app/assets/stylesheets/alchemy/_extends.scss +0 -62
  346. data/app/assets/stylesheets/alchemy/_variables.scss +0 -201
  347. data/app/assets/stylesheets/alchemy/admin/attachments.scss +0 -40
  348. data/app/assets/stylesheets/alchemy/admin/buttons.scss +0 -123
  349. data/app/assets/stylesheets/alchemy/admin/hints.scss +0 -5
  350. data/app/assets/stylesheets/alchemy/admin/labels.scss +0 -3
  351. data/app/assets/stylesheets/alchemy/admin/pagination.scss +0 -92
  352. data/app/assets/stylesheets/alchemy/admin/preview_window.scss +0 -33
  353. data/app/assets/stylesheets/alchemy/admin/resource_info.scss +0 -42
  354. data/app/assets/stylesheets/alchemy/admin/tags.scss +0 -158
  355. data/app/assets/stylesheets/alchemy/admin.scss +0 -42
  356. data/app/assets/stylesheets/alchemy/custom-properties.css +0 -98
  357. data/app/assets/stylesheets/alchemy/welcome.scss +0 -57
  358. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.css +0 -711
  359. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.inline.css +0 -705
  360. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.inline.min.css +0 -7
  361. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.min.css +0 -7
  362. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.mobile.css +0 -29
  363. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.mobile.min.css +0 -7
  364. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.mobile.css +0 -677
  365. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.mobile.min.css +0 -7
  366. data/app/controllers/alchemy/elements_controller.rb +0 -32
  367. data/app/helpers/alchemy/admin/elements_helper.rb +0 -25
  368. data/app/models/alchemy/element/dom_id.rb +0 -31
  369. data/app/models/alchemy/picture/calculations.rb +0 -49
  370. data/app/models/alchemy/picture/transformations.rb +0 -115
  371. data/app/models/alchemy/picture/url.rb +0 -54
  372. data/app/views/alchemy/admin/attachments/destroy.js.erb +0 -1
  373. data/app/views/alchemy/admin/clipboard/clear.js.erb +0 -3
  374. data/app/views/alchemy/admin/clipboard/insert.js.erb +0 -29
  375. data/app/views/alchemy/admin/clipboard/remove.js.erb +0 -10
  376. data/app/views/alchemy/admin/resources/_filter.html.erb +0 -12
  377. data/app/views/alchemy/admin/resources/_resource.html.erb +0 -34
  378. data/app/views/alchemy/admin/resources/_table.html.erb +0 -29
  379. data/app/views/alchemy/elements/show.html.erb +0 -1
  380. data/app/views/alchemy/elements/show.js.erb +0 -1
  381. data/app/views/alchemy/ingredients/_audio_view.html.erb +0 -1
  382. data/app/views/alchemy/ingredients/_boolean_view.html.erb +0 -1
  383. data/app/views/alchemy/ingredients/_datetime_view.html.erb +0 -3
  384. data/app/views/alchemy/ingredients/_file_view.html.erb +0 -4
  385. data/app/views/alchemy/ingredients/_headline_view.html.erb +0 -4
  386. data/app/views/alchemy/ingredients/_html_view.html.erb +0 -1
  387. data/app/views/alchemy/ingredients/_link_view.html.erb +0 -4
  388. data/app/views/alchemy/ingredients/_node_view.html.erb +0 -1
  389. data/app/views/alchemy/ingredients/_page_view.html.erb +0 -1
  390. data/app/views/alchemy/ingredients/_picture_view.html.erb +0 -4
  391. data/app/views/alchemy/ingredients/_richtext_view.html.erb +0 -3
  392. data/app/views/alchemy/ingredients/_select_view.html.erb +0 -1
  393. data/app/views/alchemy/ingredients/_text_view.html.erb +0 -4
  394. data/app/views/alchemy/ingredients/_video_view.html.erb +0 -3
  395. data/babel.config.js +0 -12
  396. data/bin/importmap +0 -4
  397. data/bin/rails +0 -9
  398. data/bin/rspec +0 -3
  399. data/bin/setup +0 -30
  400. data/bin/start +0 -17
  401. data/bun.lockb +0 -0
  402. data/bundles/shoelace.js +0 -12
  403. data/bundles/tinymce.js +0 -22
  404. data/config/initializers/assets.rb +0 -4
  405. data/eslint.config.js +0 -17
  406. data/lib/alchemy/config.rb +0 -114
  407. data/lib/alchemy/element_definition.rb +0 -73
  408. data/lib/alchemy/page_layout.rb +0 -73
  409. data/lib/alchemy/resource_filter.rb +0 -40
  410. data/lib/alchemy/upgrader/seven_point_four.rb +0 -26
  411. data/lib/alchemy/upgrader/seven_point_three.rb +0 -52
  412. data/lib/alchemy/upgrader/tasks/.keep +0 -0
  413. data/lib/generators/alchemy/ingredient/templates/view.html.erb +0 -1
  414. data/lib/generators/alchemy/install/files/alchemy_admin.js +0 -1
  415. data/lib/generators/alchemy/install/files/all.js +0 -11
  416. data/lib/generators/alchemy/install/files/article.css +0 -25
  417. data/rollup.config.mjs +0 -108
  418. data/vendor/assets/images/remixicon.symbol.svg +0 -11
  419. /data/app/{assets/stylesheets → stylesheets}/alchemy/_fonts.scss +0 -0
  420. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/attachment-select.scss +0 -0
  421. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/flash.scss +0 -0
  422. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/list_filter.scss +0 -0
  423. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/spinner.scss +0 -0
  424. /data/app/{assets/stylesheets → stylesheets}/tinymce/skins/skintool.json +0 -0
  425. /data/app/{assets/stylesheets → stylesheets}/tinymce/skins/ui/alchemy/fonts/tinymce-mobile.woff +0 -0
@@ -28,33 +28,12 @@ module Alchemy
28
28
  large: "240x180"
29
29
  }.with_indifferent_access.freeze
30
30
 
31
- CONVERTIBLE_FILE_FORMATS = %w[gif jpg jpeg png webp].freeze
32
-
33
- TRANSFORMATION_OPTIONS = [
34
- :crop,
35
- :crop_from,
36
- :crop_size,
37
- :flatten,
38
- :format,
39
- :quality,
40
- :size,
41
- :upsample
42
- ]
43
-
44
31
  include Alchemy::Logger
45
32
  include Alchemy::NameConversions
46
33
  include Alchemy::Taggable
47
34
  include Alchemy::TouchElements
48
- include Calculations
49
-
50
- has_many :picture_ingredients,
51
- class_name: "Alchemy::Ingredients::Picture",
52
- foreign_key: "related_object_id",
53
- inverse_of: :related_object
35
+ include Alchemy::RelatableResource
54
36
 
55
- has_many :elements, through: :picture_ingredients
56
- has_many :pages, through: :elements
57
- has_many :thumbs, class_name: "Alchemy::PictureThumb", dependent: :destroy
58
37
  has_many :descriptions, class_name: "Alchemy::PictureDescription", dependent: :destroy
59
38
 
60
39
  accepts_nested_attributes_for :descriptions, allow_destroy: true, reject_if: ->(attr) { attr[:text].blank? }
@@ -72,7 +51,7 @@ module Alchemy
72
51
 
73
52
  # Image preprocessing class
74
53
  def self.preprocessor_class
75
- @_preprocessor_class ||= Preprocessor
54
+ @_preprocessor_class ||= Alchemy.storage_adapter.preprocessor_class
76
55
  end
77
56
 
78
57
  # Set a image preprocessing class
@@ -84,56 +63,40 @@ module Alchemy
84
63
  @_preprocessor_class = klass
85
64
  end
86
65
 
87
- # Enables Dragonfly image processing
88
- dragonfly_accessor :image_file, app: :alchemy_pictures do
89
- # Preprocess after uploading the picture
90
- after_assign do |image|
91
- if has_convertible_format?
92
- self.class.preprocessor_class.new(image).call
93
- end
94
- end
95
- end
96
-
97
- before_save :sanitize_image_file_name
98
- # Create important thumbnails upfront
99
- after_create -> { PictureThumb.generate_thumbs!(self) if has_convertible_format? }
66
+ include Alchemy.storage_adapter.picture_class_methods
100
67
 
101
68
  # We need to define this method here to have it available in the validations below.
102
69
  class << self
103
70
  def allowed_filetypes
104
- Config.get(:uploader).fetch("allowed_filetypes", {}).fetch("alchemy/pictures", [])
71
+ Alchemy.config.uploader.allowed_filetypes.alchemy_pictures
105
72
  end
106
73
  end
107
74
 
108
75
  validates_presence_of :image_file
109
- validates_size_of :image_file, maximum: Config.get(:uploader)["file_size_limit"].megabytes
110
- validates_property :format,
111
- of: :image_file,
112
- in: allowed_filetypes,
113
- case_sensitive: false,
114
- message: Alchemy.t("not a valid image")
76
+ validates_size_of :image_file, maximum: Alchemy.config.uploader.file_size_limit.megabytes
77
+ validate :image_file_type_allowed, if: -> { image_file.present? }
115
78
 
116
- stampable stamper_class_name: Alchemy.user_class.name
79
+ stampable stamper_class_name: Alchemy.user_class_name
117
80
 
118
81
  scope :named, ->(name) { where("#{table_name}.name LIKE ?", "%#{name}%") }
119
82
  scope :recent, -> { where("#{table_name}.created_at > ?", Time.current - 24.hours).order(:created_at) }
120
- scope :deletable, -> do
121
- where(
122
- "#{table_name}.id NOT IN (SELECT related_object_id FROM alchemy_ingredients WHERE related_object_id IS NOT NULL AND related_object_type = ?)",
123
- name
124
- )
125
- end
126
83
  scope :without_tag, -> { left_outer_joins(:taggings).where(gutentag_taggings: {id: nil}) }
127
- scope :by_file_format, ->(format) { where(image_file_format: format) }
84
+ scope :by_file_format, ->(file_format) do
85
+ Alchemy.storage_adapter.by_file_format_scope(file_format)
86
+ end
87
+
88
+ # Case insensitive Ransack searching and sorting for name attribute
89
+ ransacker :name, type: :string do
90
+ arel_table[:name].lower
91
+ end
128
92
 
129
93
  # Class methods
130
94
 
131
95
  class << self
132
96
  # The class used to generate URLs for pictures
133
97
  #
134
- # @see Alchemy::Picture::Url
135
98
  def url_class
136
- @_url_class ||= Alchemy::Picture::Url
99
+ @_url_class ||= Alchemy.storage_adapter.picture_url_class
137
100
  end
138
101
 
139
102
  # Set a different picture url class
@@ -143,22 +106,16 @@ module Alchemy
143
106
  @_url_class = klass
144
107
  end
145
108
 
146
- def alchemy_resource_filters
147
- @_file_formats ||= distinct.pluck(:image_file_format).compact.presence || []
148
- [
149
- {
150
- name: :by_file_format,
151
- values: @_file_formats
152
- },
153
- {
154
- name: :misc,
155
- values: %w[recent last_upload without_tag deletable]
156
- }
157
- ]
109
+ def searchable_alchemy_resource_attributes
110
+ Alchemy.storage_adapter.searchable_alchemy_resource_attributes(name)
158
111
  end
159
112
 
160
- def searchable_alchemy_resource_attributes
161
- %w[name image_file_name]
113
+ def ransackable_attributes(_auth_object = nil)
114
+ Alchemy.storage_adapter.ransackable_attributes(name)
115
+ end
116
+
117
+ def ransackable_associations(_auth_object = nil)
118
+ Alchemy.storage_adapter.ransackable_associations(name)
162
119
  end
163
120
 
164
121
  def last_upload
@@ -167,33 +124,30 @@ module Alchemy
167
124
 
168
125
  Picture.where(upload_hash: last_picture.upload_hash)
169
126
  end
127
+
128
+ def ransackable_scopes(_auth_object = nil)
129
+ [:by_file_format, :recent, :last_upload, :without_tag, :deletable]
130
+ end
131
+
132
+ def file_formats(scope = all)
133
+ Alchemy.storage_adapter.file_formats(name, scope:)
134
+ end
170
135
  end
171
136
 
172
137
  # Instance methods
173
138
 
174
139
  # Returns an url (or relative path) to a processed image for use inside an image_tag helper.
175
140
  #
176
- # Any additional options are passed to the url method, so you can add params to your url.
177
- #
178
141
  # Example:
179
142
  #
180
143
  # <%= image_tag picture.url(size: '320x200', format: 'png') %>
181
144
  #
182
- # @see Alchemy::PictureVariant#call for transformation options
183
- # @see Alchemy::Picture::Url#call for url options
184
145
  # @return [String|Nil]
185
146
  def url(options = {})
186
147
  return unless image_file
187
148
 
188
- variant = PictureVariant.new(self, options.slice(*TRANSFORMATION_OPTIONS))
189
- self.class.url_class.new(variant).call(
190
- options.except(*TRANSFORMATION_OPTIONS).merge(
191
- basename: name,
192
- ext: variant.render_format,
193
- name: name
194
- )
195
- )
196
- rescue ::Dragonfly::Job::Fetch::NotFound => e
149
+ self.class.url_class.new(self).call(options)
150
+ rescue Alchemy.storage_adapter.rescuable_errors => e
197
151
  log_warning(e.message)
198
152
  nil
199
153
  end
@@ -208,7 +162,7 @@ module Alchemy
208
162
 
209
163
  url(
210
164
  flatten: true,
211
- format: image_file_format || "jpg",
165
+ format: image_file_extension || "jpg",
212
166
  size: size
213
167
  )
214
168
  end
@@ -242,18 +196,12 @@ module Alchemy
242
196
  end
243
197
  end
244
198
 
245
- # Returns the suffix of the filename.
246
- #
247
- def suffix
248
- image_file.ext
249
- end
250
-
251
199
  # Returns a humanized, readable name from image filename.
252
200
  #
253
201
  def humanized_name
254
202
  return "" if image_file_name.blank?
255
203
 
256
- convert_to_humanized_name(image_file_name, suffix)
204
+ convert_to_humanized_name(image_file_name, image_file_extension)
257
205
  end
258
206
 
259
207
  # Returns the format the image should be rendered with
@@ -263,9 +211,9 @@ module Alchemy
263
211
  #
264
212
  def default_render_format
265
213
  if convertible?
266
- Config.get(:image_output_format)
214
+ Alchemy.config.image_output_format
267
215
  else
268
- image_file_format
216
+ image_file_extension
269
217
  end
270
218
  end
271
219
 
@@ -275,15 +223,15 @@ module Alchemy
275
223
  # image has not a convertible file format (i.e. SVG) this returns +false+
276
224
  #
277
225
  def convertible?
278
- Config.get(:image_output_format) &&
279
- Config.get(:image_output_format) != "original" &&
226
+ Alchemy.config.image_output_format &&
227
+ Alchemy.config.image_output_format != "original" &&
280
228
  has_convertible_format?
281
229
  end
282
230
 
283
231
  # Returns true if the image can be converted into other formats
284
232
  #
285
233
  def has_convertible_format?
286
- image_file_format.in?(CONVERTIBLE_FILE_FORMATS)
234
+ Alchemy.storage_adapter.has_convertible_format?(self)
287
235
  end
288
236
 
289
237
  # Checks if the picture is restricted.
@@ -298,12 +246,32 @@ module Alchemy
298
246
  pages.any? && pages.not_restricted.blank?
299
247
  end
300
248
 
301
- # Returns true if picture is not assigned to any Picture ingredient.
302
- #
303
- def deletable?
304
- picture_ingredients.empty?
249
+ def image_file_name
250
+ Alchemy.storage_adapter.image_file_name(self)
251
+ end
252
+
253
+ def image_file_format
254
+ Alchemy.storage_adapter.image_file_format(self)
255
+ end
256
+
257
+ def image_file_size
258
+ Alchemy.storage_adapter.image_file_size(self)
259
+ end
260
+
261
+ def image_file_width
262
+ Alchemy.storage_adapter.image_file_width(self)
263
+ end
264
+
265
+ def image_file_height
266
+ Alchemy.storage_adapter.image_file_height(self)
305
267
  end
306
268
 
269
+ def image_file_extension
270
+ Alchemy.storage_adapter.image_file_extension(self)
271
+ end
272
+ alias_method :suffix, :image_file_extension
273
+ deprecate suffix: :image_file_extension, deprecator: Alchemy::Deprecation
274
+
307
275
  # A size String from original image file values.
308
276
  #
309
277
  # == Example
@@ -314,8 +282,12 @@ module Alchemy
314
282
  "#{image_file_width}x#{image_file_height}"
315
283
  end
316
284
 
317
- def sanitize_image_file_name
318
- self.image_file_name = sanitized_filename(image_file_name)
285
+ private
286
+
287
+ def image_file_type_allowed
288
+ unless image_file_extension&.in?(self.class.allowed_filetypes)
289
+ errors.add(:image_file, Alchemy.t("not a valid image"))
290
+ end
319
291
  end
320
292
  end
321
293
  end
@@ -11,7 +11,6 @@ module Alchemy
11
11
  extend Forwardable
12
12
 
13
13
  include Alchemy::Logger
14
- include Alchemy::Picture::Transformations
15
14
 
16
15
  ANIMATED_IMAGE_FORMATS = %w[gif webp]
17
16
  TRANSPARENT_IMAGE_FORMATS = %w[gif webp png]
@@ -89,20 +88,20 @@ module Alchemy
89
88
  end
90
89
 
91
90
  options = {
92
- flatten: !render_format.in?(ANIMATED_IMAGE_FORMATS) && picture.image_file_format == "gif"
91
+ flatten: !render_format.in?(ANIMATED_IMAGE_FORMATS) && picture.image_file_extension == "gif"
93
92
  }.with_indifferent_access.merge(options)
94
93
 
95
94
  encoding_options = []
96
95
 
97
- convert_format = render_format.sub("jpeg", "jpg") != picture.image_file_format.sub("jpeg", "jpg")
96
+ convert_format = render_format.sub("jpeg", "jpg") != picture.image_file_extension.sub("jpeg", "jpg")
98
97
 
99
98
  if encodable_image? && (convert_format || options[:quality])
100
- quality = options[:quality] || Config.get(:output_image_quality)
99
+ quality = options[:quality] || Alchemy.config.output_image_quality
101
100
  encoding_options << "-quality #{quality}"
102
101
  end
103
102
 
104
103
  if options[:flatten]
105
- if render_format.in?(TRANSPARENT_IMAGE_FORMATS) && picture.image_file_format.in?(TRANSPARENT_IMAGE_FORMATS)
104
+ if render_format.in?(TRANSPARENT_IMAGE_FORMATS) && picture.image_file_extension.in?(TRANSPARENT_IMAGE_FORMATS)
106
105
  encoding_options << "-background transparent"
107
106
  end
108
107
  encoding_options << "-flatten"
@@ -120,5 +119,116 @@ module Alchemy
120
119
  def encodable_image?
121
120
  render_format.in?(ENCODABLE_IMAGE_FORMATS)
122
121
  end
122
+
123
+ # Returns the rendered cropped image. Tries to use the crop_from and crop_size
124
+ # parameters. When they can't be parsed, it just crops from the center.
125
+ #
126
+ def crop(size, crop_from = nil, crop_size = nil, upsample = false)
127
+ raise "No size given!" if size.empty?
128
+
129
+ render_to = inferred_sizes_from_string(size)
130
+ if crop_from && crop_size
131
+ top_left = point_from_string(crop_from)
132
+ crop_dimensions = inferred_sizes_from_string(crop_size)
133
+ xy_crop_resize(render_to, top_left, crop_dimensions, upsample)
134
+ else
135
+ center_crop(render_to, upsample)
136
+ end
137
+ end
138
+
139
+ # Returns the rendered resized image using imagemagick directly.
140
+ #
141
+ def resize(size, upsample = false)
142
+ image_file.thumbnail(upsample ? size : "#{size}>")
143
+ end
144
+
145
+ # Given a string with an x, this function return a Hash with key :x and :y
146
+ #
147
+ def point_from_string(string = "0x0")
148
+ string = "0x0" if string.empty?
149
+ raise ArgumentError if !string.match(/(\d*x)|(x\d*)/)
150
+
151
+ x, y = string.scan(/(\d*)x(\d*)/)[0].map(&:to_i)
152
+
153
+ x = 0 if x.nil?
154
+ y = 0 if y.nil?
155
+ {
156
+ x: x,
157
+ y: y
158
+ }
159
+ end
160
+
161
+ def inferred_sizes_from_string(string)
162
+ sizes = sizes_from_string(string)
163
+ ratio = image_file_width.to_f / image_file_height
164
+
165
+ if sizes[:width].zero?
166
+ sizes[:width] = (sizes[:height] * ratio).round.to_i
167
+ end
168
+ if sizes[:height].zero?
169
+ sizes[:height] = (sizes[:width] / ratio).round.to_i
170
+ end
171
+
172
+ sizes
173
+ end
174
+
175
+ # Given a string with an x, this function returns a Hash with point
176
+ # :width and :height.
177
+ #
178
+ def sizes_from_string(string)
179
+ width, height = string.to_s.split("x", 2).map(&:to_i)
180
+
181
+ {
182
+ width: width,
183
+ height: height
184
+ }
185
+ end
186
+
187
+ # Returns true if both dimensions of the base image are bigger than the dimensions hash.
188
+ #
189
+ def is_bigger_than?(dimensions)
190
+ image_file_width > dimensions[:width] && image_file_height > dimensions[:height]
191
+ end
192
+
193
+ # Returns true is one dimension of the base image is smaller than the dimensions hash.
194
+ #
195
+ def is_smaller_than?(dimensions)
196
+ !is_bigger_than?(dimensions)
197
+ end
198
+
199
+ # Converts a dimensions hash to a string of from "20x20"
200
+ #
201
+ def dimensions_to_string(dimensions)
202
+ "#{dimensions[:width]}x#{dimensions[:height]}"
203
+ end
204
+
205
+ # Uses imagemagick to make a centercropped thumbnail. Does not scale the image up.
206
+ #
207
+ def center_crop(dimensions, upsample)
208
+ if is_smaller_than?(dimensions) && upsample == false
209
+ dimensions = reduce_to_image(dimensions)
210
+ end
211
+ image_file.thumbnail("#{dimensions_to_string(dimensions)}#")
212
+ end
213
+
214
+ # Use imagemagick to custom crop an image. Uses -thumbnail for better performance when resizing.
215
+ #
216
+ def xy_crop_resize(dimensions, top_left, crop_dimensions, upsample)
217
+ crop_argument = dimensions_to_string(crop_dimensions)
218
+ crop_argument += "+#{top_left[:x]}+#{top_left[:y]}"
219
+
220
+ resize_argument = dimensions_to_string(dimensions)
221
+ resize_argument += ">" unless upsample
222
+ image_file.crop_resize(crop_argument, resize_argument)
223
+ end
224
+
225
+ # Used when centercropping.
226
+ #
227
+ def reduce_to_image(dimensions)
228
+ {
229
+ width: [dimensions[:width].to_i, image_file_width.to_i].min,
230
+ height: [dimensions[:height].to_i, image_file_height.to_i].min
231
+ }
232
+ end
123
233
  end
124
234
  end
@@ -98,8 +98,10 @@ module Alchemy
98
98
  # resource = Resource.new('/admin/tags', {"engine_name"=>"alchemy"}, Gutentag::Tag)
99
99
  #
100
100
  class Resource
101
+ include Alchemy::Admin::ResourceName
102
+
101
103
  attr_accessor :resource_relations, :model_associations
102
- attr_reader :model
104
+ attr_reader :model, :controller_path
103
105
 
104
106
  DEFAULT_SKIPPED_ATTRIBUTES = %w[id created_at creator_id]
105
107
  DEFAULT_SKIPPED_ASSOCIATIONS = %w[creator]
@@ -119,18 +121,6 @@ module Alchemy
119
121
  end
120
122
  end
121
123
 
122
- def resource_array
123
- @_resource_array ||= controller_path_array.reject { |el| el == "admin" }
124
- end
125
-
126
- def resources_name
127
- @_resources_name ||= resource_array.last
128
- end
129
-
130
- def resource_name
131
- @_resource_name ||= resources_name.singularize
132
- end
133
-
134
124
  def namespaced_resource_name
135
125
  @_namespaced_resource_name ||= begin
136
126
  namespaced_resources_name.to_s.singularize
@@ -296,11 +286,7 @@ module Alchemy
296
286
  end
297
287
 
298
288
  def guess_model_from_controller_path
299
- resource_array.join("/").classify.constantize
300
- end
301
-
302
- def controller_path_array
303
- @controller_path.split("/")
289
+ resource_model_name.classify.constantize
304
290
  end
305
291
 
306
292
  def namespace_diff
@@ -21,6 +21,21 @@ module Alchemy
21
21
  searchable_alchemy_resource_associations
22
22
  end
23
23
 
24
+ # Allow all scopes used in resource filters to be used with Ransack
25
+ def ransackable_scopes(_auth_object = nil)
26
+ if respond_to?(:alchemy_resource_filters)
27
+ alchemy_resource_filters.flat_map do |filter_set|
28
+ if respond_to?(filter_set[:name])
29
+ filter_set[:name].to_s
30
+ else
31
+ filter_set[:values].map(&:to_s)
32
+ end
33
+ end
34
+ else
35
+ []
36
+ end
37
+ end
38
+
24
39
  protected
25
40
 
26
41
  def searchable_alchemy_resource_attributes
@@ -42,8 +42,8 @@ module Alchemy
42
42
  #
43
43
  def page_layout_names(layoutpages: false)
44
44
  page_layout_definitions.select do |layout|
45
- !!layout["layoutpage"] && layoutpages || !layout["layoutpage"] && !layoutpages
46
- end.collect { |layout| layout["name"] }
45
+ !!layout.layoutpage && layoutpages || !layout.layoutpage && !layoutpages
46
+ end.tap { _1.collect!(&:name) }
47
47
  end
48
48
 
49
49
  # Returns sites page layout definitions
@@ -52,11 +52,11 @@ module Alchemy
52
52
  #
53
53
  def page_layout_definitions
54
54
  if definition["page_layouts"].presence
55
- Alchemy::PageLayout.all.select do |layout|
56
- layout["name"].in?(definition["page_layouts"])
55
+ Alchemy::PageDefinition.all.select do |layout|
56
+ layout.name.in?(definition["page_layouts"])
57
57
  end
58
58
  else
59
- Alchemy::PageLayout.all
59
+ Alchemy::PageDefinition.all
60
60
  end
61
61
  end
62
62
 
@@ -21,12 +21,7 @@ module Alchemy
21
21
  validates_uniqueness_of :host, case_sensitive: false
22
22
 
23
23
  # associations
24
- has_many :languages
25
-
26
- before_destroy if: -> { languages.any? } do
27
- errors.add(:languages, :still_present)
28
- throw(:abort)
29
- end
24
+ has_many :languages, dependent: :restrict_with_error
30
25
 
31
26
  scope :published, -> { where(public: true) }
32
27
 
@@ -57,21 +52,6 @@ module Alchemy
57
52
  end
58
53
 
59
54
  class << self
60
- # @deprecated Use {Alchemy::Current#site=} instead.
61
- def current=(site)
62
- Current.site = site
63
- end
64
- deprecate "current=": :"Alchemy::Current.site=", deprecator: Alchemy::Deprecation
65
-
66
- # @deprecated Use {Alchemy::Current#site} instead.
67
- def current
68
- Current.site
69
- end
70
- deprecate current: :"Alchemy::Current.site", deprecator: Alchemy::Deprecation
71
-
72
- alias_method :default, :first
73
- deprecate default: :first, deprecator: Alchemy::Deprecation
74
-
75
55
  def find_for_host(host)
76
56
  # These are split up into two separate queries in order to run the
77
57
  # fastest query first (selecting the domain by its primary host name).
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class StorageAdapter
5
+ # The class representing an URL to an ActiveStorage attachment
6
+ #
7
+ class ActiveStorage::AttachmentUrl
8
+ attr_reader :attachment
9
+
10
+ def initialize(attachment)
11
+ @attachment = attachment
12
+ end
13
+
14
+ # The attachment url
15
+ #
16
+ # @param [Hash] options
17
+ # @option options [Symbol] :download return a URL for downloading the attachment
18
+ # @option options [Symbol] :name The filename
19
+ # @option options [Symbol] :format The file extension
20
+ #
21
+ # @return [String]
22
+ #
23
+ def call(options = {})
24
+ if attachment.file.attached?
25
+ options[:format] ||= attachment.suffix
26
+ if options.delete(:download)
27
+ routes.download_attachment_path(attachment, options)
28
+ else
29
+ routes.show_attachment_path(attachment, options)
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def routes
37
+ Alchemy::Engine.routes.url_helpers
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class StorageAdapter
5
+ # Returns the URL to a variant of a picture using ActiveStorage
6
+ class ActiveStorage::PictureUrl
7
+ attr_reader :picture, :image_file
8
+
9
+ # @param [Alchemy::Picture]
10
+ #
11
+ def initialize(picture)
12
+ @picture = picture
13
+ @image_file = picture.image_file
14
+ end
15
+
16
+ # The URL to a variant of a picture
17
+ #
18
+ # @return [String]
19
+ #
20
+ def call(options = {})
21
+ variant_options = DragonflyToImageProcessing.call(options)
22
+ variant_options[:format] = options[:format] || default_output_format
23
+ variant = image_file&.variant(variant_options)
24
+ return unless variant
25
+
26
+ Rails.application.routes.url_helpers.rails_blob_path(
27
+ variant,
28
+ {
29
+ filename: filename(options),
30
+ format: variant_options[:format],
31
+ only_path: true
32
+ }
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def filename(options = {})
39
+ if picture.name.presence
40
+ picture.name.to_param
41
+ else
42
+ picture.image_file_name
43
+ end
44
+ end
45
+
46
+ def default_output_format
47
+ if Alchemy.config.image_output_format == "original"
48
+ picture.image_file_extension
49
+ else
50
+ Alchemy.config.image_output_format
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end