alchemy_cms 7.4.6 → 8.0.0.a

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 (349) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -0
  3. data/Gemfile +13 -6
  4. data/README.md +13 -5
  5. data/alchemy_cms.gemspec +14 -5
  6. data/app/assets/builds/alchemy/admin/page-select.css +1 -1
  7. data/app/assets/builds/alchemy/admin/print.css +1 -1
  8. data/app/assets/builds/alchemy/admin.css +2 -2
  9. data/app/assets/builds/alchemy/custom-properties.css +1 -1
  10. data/app/assets/builds/alchemy/welcome.css +1 -1
  11. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  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/config/alchemy_manifest.js +0 -2
  15. data/app/assets/images/alchemy/icons-sprite.svg +1 -0
  16. data/app/components/alchemy/admin/resource/applied_filter.rb +29 -0
  17. data/app/components/alchemy/admin/resource/checkbox_filter.rb +36 -0
  18. data/app/components/alchemy/admin/resource/datepicker_filter.rb +42 -0
  19. data/app/components/alchemy/admin/resource/select_filter.rb +43 -0
  20. data/app/components/alchemy/admin/toolbar_button.rb +5 -2
  21. data/app/components/alchemy/ingredients/number_view.rb +18 -0
  22. data/app/controllers/alchemy/admin/attachments_controller.rb +8 -15
  23. data/app/controllers/alchemy/admin/clipboard_controller.rb +2 -6
  24. data/app/controllers/alchemy/admin/elements_controller.rb +1 -1
  25. data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
  26. data/app/controllers/alchemy/admin/pages_controller.rb +15 -15
  27. data/app/controllers/alchemy/admin/pictures_controller.rb +9 -5
  28. data/app/controllers/alchemy/admin/resources_controller.rb +16 -106
  29. data/app/controllers/alchemy/attachments_controller.rb +43 -14
  30. data/app/controllers/alchemy/messages_controller.rb +1 -1
  31. data/app/controllers/alchemy/pages_controller.rb +7 -2
  32. data/app/controllers/concerns/alchemy/admin/resource_filter.rb +92 -0
  33. data/app/decorators/alchemy/element_editor.rb +5 -48
  34. data/app/decorators/alchemy/ingredient_editor.rb +3 -53
  35. data/app/helpers/alchemy/admin/base_helper.rb +14 -84
  36. data/app/helpers/alchemy/admin/elements_helper.rb +4 -4
  37. data/app/helpers/alchemy/admin/pages_helper.rb +1 -1
  38. data/app/helpers/alchemy/base_helper.rb +0 -30
  39. data/app/helpers/alchemy/elements_block_helper.rb +0 -14
  40. data/app/helpers/alchemy/pages_helper.rb +1 -1
  41. data/{lib → app/helpers}/alchemy/resources_helper.rb +5 -45
  42. data/app/javascript/alchemy_admin/components/action.js +2 -0
  43. data/app/javascript/alchemy_admin/components/alchemy_html_element.js +3 -3
  44. data/app/javascript/alchemy_admin/components/datepicker.js +10 -2
  45. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +7 -7
  46. data/app/javascript/alchemy_admin/components/element_editor.js +1 -1
  47. data/app/javascript/alchemy_admin/components/index.js +1 -0
  48. data/app/javascript/alchemy_admin/components/remote_select.js +4 -1
  49. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +5 -1
  50. data/app/javascript/alchemy_admin/components/tinymce.js +4 -2
  51. data/app/javascript/alchemy_admin/components/update_check.js +42 -0
  52. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +15 -8
  53. data/app/javascript/alchemy_admin/components/uploader/progress.js +12 -6
  54. data/app/javascript/alchemy_admin/components/uploader.js +4 -2
  55. data/app/javascript/alchemy_admin/confirm_dialog.js +27 -57
  56. data/app/javascript/alchemy_admin/dirty.js +3 -2
  57. data/app/javascript/alchemy_admin/i18n.js +15 -16
  58. data/app/javascript/alchemy_admin/initializer.js +1 -49
  59. data/app/javascript/alchemy_admin/utils/ajax.js +51 -44
  60. data/app/javascript/alchemy_admin.js +3 -8
  61. data/app/models/alchemy/admin/filters/base.rb +38 -0
  62. data/app/models/alchemy/admin/filters/checkbox.rb +24 -0
  63. data/app/models/alchemy/admin/filters/datepicker.rb +53 -0
  64. data/app/models/alchemy/admin/filters/select.rb +70 -0
  65. data/app/models/alchemy/admin/resource_name.rb +27 -0
  66. data/app/models/alchemy/attachment.rb +51 -34
  67. data/app/models/alchemy/base_record.rb +2 -0
  68. data/app/models/alchemy/element/definitions.rb +1 -1
  69. data/app/models/alchemy/element/element_ingredients.rb +6 -6
  70. data/app/models/alchemy/element/presenters.rb +3 -12
  71. data/app/models/alchemy/element.rb +9 -27
  72. data/app/models/alchemy/element_definition.rb +160 -0
  73. data/app/models/alchemy/ingredient.rb +10 -43
  74. data/app/models/alchemy/ingredient_definition.rb +134 -0
  75. data/app/models/alchemy/ingredient_validator.rb +7 -3
  76. data/app/models/alchemy/ingredients/number.rb +19 -0
  77. data/app/models/alchemy/language.rb +0 -14
  78. data/app/models/alchemy/message.rb +3 -7
  79. data/app/models/alchemy/node.rb +1 -1
  80. data/app/models/alchemy/page/{page_layouts.rb → definitions.rb} +12 -19
  81. data/app/models/alchemy/page/fixed_attributes.rb +1 -1
  82. data/app/models/alchemy/page/page_elements.rb +13 -14
  83. data/app/models/alchemy/page/page_natures.rb +7 -7
  84. data/app/models/alchemy/page/page_scopes.rb +1 -1
  85. data/app/models/alchemy/page.rb +11 -33
  86. data/app/models/alchemy/page_definition.rb +115 -0
  87. data/app/models/alchemy/picture.rb +69 -79
  88. data/app/models/alchemy/picture_variant.rb +115 -5
  89. data/{lib → app/models}/alchemy/resource.rb +4 -18
  90. data/{lib → app/models}/alchemy/searchable_resource.rb +15 -0
  91. data/app/models/alchemy/site/layout.rb +5 -5
  92. data/app/models/alchemy/site.rb +0 -15
  93. data/app/models/alchemy/storage_adapter/active_storage/attachment_url.rb +41 -0
  94. data/app/models/alchemy/storage_adapter/active_storage/picture_url.rb +55 -0
  95. data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +40 -0
  96. data/app/models/alchemy/storage_adapter/active_storage.rb +173 -0
  97. data/app/models/alchemy/{attachment/url.rb → storage_adapter/dragonfly/attachment_url.rb} +12 -12
  98. data/app/models/alchemy/{picture/url.rb → storage_adapter/dragonfly/picture_url.rb} +28 -12
  99. data/app/models/alchemy/{picture → storage_adapter/dragonfly}/preprocessor.rb +4 -4
  100. data/app/models/alchemy/storage_adapter/dragonfly.rb +183 -0
  101. data/app/models/alchemy/storage_adapter.rb +74 -0
  102. data/app/models/concerns/alchemy/picture_thumbnails.rb +19 -6
  103. data/app/serializers/alchemy/element_serializer.rb +0 -1
  104. data/app/services/alchemy/dragonfly_to_image_processing.rb +100 -0
  105. data/app/stylesheets/alchemy/_defaults.scss +3 -0
  106. data/app/stylesheets/alchemy/_extends.scss +69 -0
  107. data/app/{assets/stylesheets → stylesheets}/alchemy/_mixins.scss +36 -49
  108. data/app/stylesheets/alchemy/_variables.scss +5 -0
  109. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/archive.scss +20 -37
  110. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/base.scss +16 -14
  111. data/app/stylesheets/alchemy/admin/buttons.scss +160 -0
  112. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/clipboard.scss +2 -2
  113. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/dashboard.scss +13 -16
  114. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/dialogs.scss +23 -16
  115. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/elements.scss +150 -105
  116. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/errors.scss +5 -5
  117. data/app/stylesheets/alchemy/admin/filters.scss +58 -0
  118. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/flatpickr.scss +53 -60
  119. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/form_fields.scss +21 -7
  120. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/forms.scss +31 -19
  121. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/frame.scss +20 -16
  122. data/app/stylesheets/alchemy/admin/hints.scss +5 -0
  123. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/icons.scss +10 -1
  124. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/image_library.scss +10 -8
  125. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/images.scss +1 -1
  126. data/app/stylesheets/alchemy/admin/labels.scss +5 -0
  127. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/lists.scss +3 -3
  128. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/navigation.scss +61 -55
  129. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/nodes.scss +21 -18
  130. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/notices.scss +18 -18
  131. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/page-select.scss +2 -2
  132. data/app/stylesheets/alchemy/admin/pagination.scss +144 -0
  133. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/preview_window.scss +8 -6
  134. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/print.scss +1 -1
  135. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/resource_info.scss +8 -5
  136. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/search.scss +9 -6
  137. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/selects.scss +49 -37
  138. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/shoelace.scss +5 -6
  139. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/sitemap.scss +38 -33
  140. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/tables.scss +6 -4
  141. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/tags.scss +6 -4
  142. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/toolbar.scss +12 -6
  143. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/typography.scss +2 -2
  144. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/upload.scss +7 -5
  145. data/app/stylesheets/alchemy/admin.scss +44 -0
  146. data/app/stylesheets/alchemy/custom-properties.css +244 -0
  147. data/app/stylesheets/alchemy/welcome.scss +75 -0
  148. data/app/{assets/stylesheets → stylesheets}/tinymce/skins/content/alchemy/content.scss +8 -9
  149. data/app/stylesheets/tinymce/skins/ui/alchemy/content.scss +1 -0
  150. data/app/{assets/stylesheets → stylesheets}/tinymce/skins/ui/alchemy/skin.scss +133 -136
  151. data/app/views/alchemy/admin/attachments/_files_list.html.erb +2 -2
  152. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  153. data/app/views/alchemy/admin/{elements/_clipboard_button.html.erb → clipboard/_button.html.erb} +3 -5
  154. data/app/views/alchemy/admin/clipboard/_update_nested_element_button.turbo_stream.erb +11 -0
  155. data/app/views/alchemy/admin/clipboard/clear.turbo_stream.erb +4 -0
  156. data/app/views/alchemy/admin/clipboard/index.html.erb +15 -13
  157. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +18 -0
  158. data/app/views/alchemy/admin/clipboard/remove.turbo_stream.erb +9 -0
  159. data/app/views/alchemy/admin/dashboard/info.html.erb +17 -31
  160. data/app/views/alchemy/admin/elements/_element.html.erb +4 -8
  161. data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
  162. data/app/views/alchemy/admin/elements/_header.html.erb +1 -0
  163. data/app/views/alchemy/admin/elements/_toolbar.html.erb +4 -6
  164. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +2 -1
  165. data/app/views/alchemy/admin/elements/index.html.erb +2 -2
  166. data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +3 -16
  167. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +0 -9
  168. data/app/views/alchemy/admin/languages/_form.html.erb +1 -1
  169. data/app/views/alchemy/admin/languages/_table.html.erb +1 -1
  170. data/app/views/alchemy/admin/languages/index.html.erb +5 -2
  171. data/app/views/alchemy/admin/layoutpages/index.html.erb +1 -12
  172. data/app/views/alchemy/admin/pages/_form.html.erb +2 -2
  173. data/app/views/alchemy/admin/pages/_page.html.erb +2 -3
  174. data/app/views/alchemy/admin/pages/_toolbar.html.erb +1 -15
  175. data/app/views/alchemy/admin/pages/index.html.erb +1 -1
  176. data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +9 -12
  177. data/app/views/alchemy/admin/partials/_search_form.html.erb +4 -9
  178. data/app/views/alchemy/admin/pictures/_archive.html.erb +4 -7
  179. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +2 -1
  180. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +1 -1
  181. data/app/views/alchemy/admin/pictures/index.html.erb +2 -7
  182. data/app/views/alchemy/admin/resources/_applied_filters.html.erb +8 -0
  183. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +11 -21
  184. data/app/views/alchemy/admin/resources/_pagination.html.erb +6 -0
  185. data/app/views/alchemy/admin/resources/_per_page_select.html.erb +4 -2
  186. data/app/views/alchemy/admin/resources/_resource_table.html.erb +1 -1
  187. data/app/views/alchemy/admin/resources/_table_header.html.erb +1 -15
  188. data/app/views/alchemy/admin/sites/index.html.erb +5 -1
  189. data/app/views/alchemy/admin/styleguide/index.html.erb +8 -0
  190. data/app/views/alchemy/admin/tags/index.html.erb +1 -1
  191. data/app/views/alchemy/admin/tinymce/_setup.html.erb +7 -7
  192. data/app/{javascript/alchemy_admin/locales/en.js → views/alchemy/admin/translations/_en.js} +5 -2
  193. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -1
  194. data/app/views/alchemy/admin/uploader/_setup.html.erb +4 -4
  195. data/app/views/alchemy/base/redirect.js.erb +1 -1
  196. data/app/views/alchemy/ingredients/_number_editor.html.erb +24 -0
  197. data/app/views/alchemy/no_index.html.erb +31 -0
  198. data/app/views/alchemy/welcome.html.erb +12 -10
  199. data/app/views/kaminari/alchemy/_first_page.html.erb +5 -3
  200. data/app/views/kaminari/alchemy/_last_page.html.erb +5 -3
  201. data/app/views/kaminari/alchemy/_next_page.html.erb +5 -3
  202. data/app/views/kaminari/alchemy/_paginator.html.erb +18 -13
  203. data/app/views/kaminari/alchemy/_prev_page.html.erb +5 -3
  204. data/app/views/layouts/alchemy/admin.html.erb +5 -9
  205. data/bun.lockb +0 -0
  206. data/bundles/remixicon.mjs +153 -0
  207. data/config/alchemy/config.yml +3 -2
  208. data/config/initializers/dragonfly.rb +0 -1
  209. data/config/initializers/mime_types.rb +1 -0
  210. data/config/locales/alchemy.en.yml +32 -14
  211. data/config/routes.rb +0 -2
  212. data/eslint.config.js +2 -1
  213. data/lib/alchemy/admin/preview_url.rb +4 -5
  214. data/lib/alchemy/cache_digests/template_tracker.rb +6 -9
  215. data/lib/alchemy/config_missing.rb +14 -0
  216. data/lib/alchemy/configuration/base_option.rb +24 -0
  217. data/lib/alchemy/configuration/boolean_option.rb +16 -0
  218. data/lib/alchemy/configuration/class_option.rb +15 -0
  219. data/lib/alchemy/configuration/class_set_option.rb +46 -0
  220. data/lib/alchemy/configuration/integer_list_option.rb +13 -0
  221. data/lib/alchemy/configuration/integer_option.rb +12 -0
  222. data/lib/alchemy/configuration/list_option.rb +22 -0
  223. data/lib/alchemy/configuration/regexp_option.rb +11 -0
  224. data/lib/alchemy/configuration/string_list_option.rb +13 -0
  225. data/lib/alchemy/configuration/string_option.rb +11 -0
  226. data/lib/alchemy/configuration.rb +115 -0
  227. data/lib/alchemy/configuration_methods.rb +3 -1
  228. data/lib/alchemy/configurations/default_language.rb +12 -0
  229. data/lib/alchemy/configurations/default_site.rb +10 -0
  230. data/lib/alchemy/configurations/format_matchers.rb +11 -0
  231. data/lib/alchemy/configurations/mailer.rb +16 -0
  232. data/lib/alchemy/configurations/main.rb +216 -0
  233. data/lib/alchemy/configurations/preview.rb +32 -0
  234. data/lib/alchemy/configurations/sitemap.rb +10 -0
  235. data/lib/alchemy/configurations/uploader.rb +34 -0
  236. data/lib/alchemy/engine.rb +65 -17
  237. data/lib/alchemy/hints.rb +3 -7
  238. data/lib/alchemy/on_page_layout.rb +2 -2
  239. data/lib/alchemy/propshaft/tinymce_asset.rb +15 -0
  240. data/lib/alchemy/seeder.rb +2 -2
  241. data/lib/alchemy/tasks/usage.rb +4 -4
  242. data/lib/alchemy/test_support/config_stubbing.rb +1 -7
  243. data/lib/alchemy/test_support/factories/attachment_factory.rb +13 -2
  244. data/lib/alchemy/test_support/factories/language_factory.rb +1 -1
  245. data/lib/alchemy/test_support/factories/page_factory.rb +2 -3
  246. data/lib/alchemy/test_support/factories/picture_factory.rb +30 -2
  247. data/lib/alchemy/test_support/factories/site_factory.rb +2 -2
  248. data/lib/alchemy/test_support/having_crop_action_examples.rb +2 -2
  249. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +80 -26
  250. data/lib/alchemy/test_support/shared_ingredient_examples.rb +5 -5
  251. data/lib/alchemy/upgrader/.keep +0 -0
  252. data/lib/alchemy/upgrader/eight_zero.rb +14 -0
  253. data/lib/alchemy/upgrader.rb +33 -20
  254. data/lib/alchemy/version.rb +1 -1
  255. data/lib/alchemy.rb +192 -170
  256. data/lib/alchemy_cms.rb +1 -7
  257. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +0 -3
  258. data/lib/generators/alchemy/install/files/_article.html.erb +6 -4
  259. data/lib/generators/alchemy/install/files/alchemy.en.yml +22 -3
  260. data/lib/generators/alchemy/install/files/application.html.erb +5 -0
  261. data/lib/generators/alchemy/install/install_generator.rb +5 -14
  262. data/lib/generators/alchemy/install/templates/alchemy.rb.tt +196 -0
  263. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -1
  264. data/lib/generators/alchemy/install/templates/elements.yml.tt +3 -1
  265. data/lib/generators/alchemy/install/templates/menus.yml.tt +1 -1
  266. data/lib/generators/alchemy/install/templates/page_layouts.yml.tt +2 -2
  267. data/lib/generators/alchemy/page_layouts/page_layouts_generator.rb +2 -2
  268. data/lib/tasks/alchemy/assets.rake +14 -0
  269. data/lib/tasks/alchemy/upgrade.rake +12 -47
  270. data/vendor/javascript/tinymce.min.js +1 -1
  271. data/vitest.config.js +21 -0
  272. metadata +181 -180
  273. data/app/assets/builds/alchemy/admin/page-select.css.map +0 -1
  274. data/app/assets/builds/alchemy/admin/print.css.map +0 -1
  275. data/app/assets/builds/alchemy/admin.css.map +0 -1
  276. data/app/assets/builds/alchemy/custom-properties.css.map +0 -1
  277. data/app/assets/builds/alchemy/welcome.css.map +0 -1
  278. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css.map +0 -1
  279. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css.map +0 -1
  280. data/app/assets/javascripts/alchemy/admin.js +0 -10
  281. data/app/assets/stylesheets/alchemy/_defaults.scss +0 -3
  282. data/app/assets/stylesheets/alchemy/_deprecated_variables.scss +0 -45
  283. data/app/assets/stylesheets/alchemy/_deprecation.scss +0 -17
  284. data/app/assets/stylesheets/alchemy/_extends.scss +0 -62
  285. data/app/assets/stylesheets/alchemy/_variables.scss +0 -201
  286. data/app/assets/stylesheets/alchemy/admin/buttons.scss +0 -124
  287. data/app/assets/stylesheets/alchemy/admin/hints.scss +0 -5
  288. data/app/assets/stylesheets/alchemy/admin/labels.scss +0 -3
  289. data/app/assets/stylesheets/alchemy/admin/pagination.scss +0 -92
  290. data/app/assets/stylesheets/alchemy/admin.scss +0 -42
  291. data/app/assets/stylesheets/alchemy/custom-properties.css +0 -98
  292. data/app/assets/stylesheets/alchemy/welcome.scss +0 -57
  293. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.css +0 -711
  294. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.inline.css +0 -705
  295. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.inline.min.css +0 -7
  296. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.min.css +0 -7
  297. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.mobile.css +0 -29
  298. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.mobile.min.css +0 -7
  299. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.mobile.css +0 -677
  300. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.mobile.min.css +0 -7
  301. data/app/controllers/alchemy/elements_controller.rb +0 -32
  302. data/app/models/alchemy/element/dom_id.rb +0 -31
  303. data/app/models/alchemy/picture/calculations.rb +0 -49
  304. data/app/models/alchemy/picture/transformations.rb +0 -115
  305. data/app/views/alchemy/admin/attachments/destroy.js.erb +0 -1
  306. data/app/views/alchemy/admin/clipboard/clear.js.erb +0 -3
  307. data/app/views/alchemy/admin/clipboard/insert.js.erb +0 -29
  308. data/app/views/alchemy/admin/clipboard/remove.js.erb +0 -10
  309. data/app/views/alchemy/admin/resources/_filter.html.erb +0 -12
  310. data/app/views/alchemy/admin/resources/_resource.html.erb +0 -34
  311. data/app/views/alchemy/admin/resources/_table.html.erb +0 -29
  312. data/app/views/alchemy/elements/show.html.erb +0 -1
  313. data/app/views/alchemy/elements/show.js.erb +0 -1
  314. data/app/views/alchemy/ingredients/_audio_view.html.erb +0 -1
  315. data/app/views/alchemy/ingredients/_boolean_view.html.erb +0 -1
  316. data/app/views/alchemy/ingredients/_datetime_view.html.erb +0 -3
  317. data/app/views/alchemy/ingredients/_file_view.html.erb +0 -4
  318. data/app/views/alchemy/ingredients/_headline_view.html.erb +0 -4
  319. data/app/views/alchemy/ingredients/_html_view.html.erb +0 -1
  320. data/app/views/alchemy/ingredients/_link_view.html.erb +0 -4
  321. data/app/views/alchemy/ingredients/_node_view.html.erb +0 -1
  322. data/app/views/alchemy/ingredients/_page_view.html.erb +0 -1
  323. data/app/views/alchemy/ingredients/_picture_view.html.erb +0 -4
  324. data/app/views/alchemy/ingredients/_richtext_view.html.erb +0 -3
  325. data/app/views/alchemy/ingredients/_select_view.html.erb +0 -1
  326. data/app/views/alchemy/ingredients/_text_view.html.erb +0 -4
  327. data/app/views/alchemy/ingredients/_video_view.html.erb +0 -3
  328. data/babel.config.js +0 -12
  329. data/config/initializers/assets.rb +0 -4
  330. data/lib/alchemy/config.rb +0 -114
  331. data/lib/alchemy/element_definition.rb +0 -73
  332. data/lib/alchemy/page_layout.rb +0 -73
  333. data/lib/alchemy/resource_filter.rb +0 -40
  334. data/lib/alchemy/upgrader/seven_point_four.rb +0 -26
  335. data/lib/alchemy/upgrader/seven_point_three.rb +0 -52
  336. data/lib/generators/alchemy/ingredient/templates/view.html.erb +0 -1
  337. data/lib/generators/alchemy/install/files/alchemy_admin.js +0 -1
  338. data/lib/generators/alchemy/install/files/all.js +0 -11
  339. data/lib/generators/alchemy/install/files/article.css +0 -25
  340. data/vendor/assets/images/remixicon.symbol.svg +0 -11
  341. /data/app/{assets/stylesheets → stylesheets}/alchemy/_fonts.scss +0 -0
  342. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/attachment-select.scss +0 -0
  343. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/attachments.scss +0 -0
  344. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/flash.scss +0 -0
  345. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/list_filter.scss +0 -0
  346. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/node-select.scss +0 -0
  347. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/spinner.scss +0 -0
  348. /data/app/{assets/stylesheets → stylesheets}/tinymce/skins/skintool.json +0 -0
  349. /data/app/{assets/stylesheets → stylesheets}/tinymce/skins/ui/alchemy/fonts/tinymce-mobile.woff +0 -0
@@ -36,7 +36,7 @@
36
36
  #
37
37
 
38
38
  require_dependency "alchemy/page/fixed_attributes"
39
- require_dependency "alchemy/page/page_layouts"
39
+ require_dependency "alchemy/page/definitions"
40
40
  require_dependency "alchemy/page/page_scopes"
41
41
  require_dependency "alchemy/page/page_natures"
42
42
  require_dependency "alchemy/page/page_naming"
@@ -44,7 +44,6 @@ require_dependency "alchemy/page/page_elements"
44
44
 
45
45
  module Alchemy
46
46
  class Page < BaseRecord
47
- include Alchemy::Hints
48
47
  include Alchemy::Logger
49
48
  include Alchemy::Taggable
50
49
 
@@ -90,7 +89,7 @@ module Alchemy
90
89
 
91
90
  acts_as_nested_set(dependent: :destroy, scope: [:layoutpage, :language_id])
92
91
 
93
- stampable stamper_class_name: Alchemy.user_class.name
92
+ stampable stamper_class_name: Alchemy.user_class_name
94
93
 
95
94
  belongs_to :language
96
95
 
@@ -156,7 +155,7 @@ module Alchemy
156
155
  after_update :touch_nodes
157
156
 
158
157
  # Concerns
159
- include PageLayouts
158
+ include Definitions
160
159
  include PageScopes
161
160
  include PageNatures
162
161
  include PageNaming
@@ -164,6 +163,7 @@ module Alchemy
164
163
 
165
164
  # site_name accessor
166
165
  delegate :name, to: :site, prefix: true, allow_nil: true
166
+ delegate :has_hint?, :hint, to: :definition
167
167
 
168
168
  # Class methods
169
169
  #
@@ -183,37 +183,10 @@ module Alchemy
183
183
  @_url_path_class = klass
184
184
  end
185
185
 
186
- def alchemy_resource_filters
187
- [
188
- {
189
- name: :by_page_layout,
190
- values: PageLayout.all.map { |p| [Alchemy.t(p["name"], scope: "page_layout_names"), p["name"]] }
191
- },
192
- {
193
- name: :status,
194
- values: %w[published not_public restricted]
195
- }
196
- ]
197
- end
198
-
199
186
  def searchable_alchemy_resource_attributes
200
187
  %w[name urlname title]
201
188
  end
202
189
 
203
- # Used to store the current page previewed in the edit page template.
204
- # @deprecated Use {Alchemy::Current#preview_page=} instead.
205
- def current_preview=(page)
206
- Current.preview_page = page
207
- end
208
- deprecate "current_preview=": :"Alchemy::Current.preview_page=", deprecator: Alchemy::Deprecation
209
-
210
- # Returns the current page previewed in the edit page template.
211
- # @deprecated Use {Alchemy::Current#preview_page} instead.
212
- def current_preview
213
- Current.preview_page
214
- end
215
- deprecate current_preview: :"Alchemy::Current.preview_page", deprecator: Alchemy::Deprecation
216
-
217
190
  # @return the language root page for given language id.
218
191
  # @param language_id [Fixnum]
219
192
  #
@@ -265,13 +238,13 @@ module Alchemy
265
238
 
266
239
  clipboard_pages = all_from_clipboard(clipboard)
267
240
  allowed_page_layouts = Alchemy::Page.selectable_layouts(language_id, layoutpages: layoutpages)
268
- allowed_page_layout_names = allowed_page_layouts.collect { |p| p["name"] }
241
+ allowed_page_layout_names = allowed_page_layouts.collect(&:name)
269
242
  clipboard_pages.select { |cp| allowed_page_layout_names.include?(cp.page_layout) }
270
243
  end
271
244
 
272
245
  def link_target_options
273
246
  options = [[Alchemy.t(:default, scope: "link_target_options"), ""]]
274
- link_target_options = Config.get(:link_target_options)
247
+ link_target_options = Alchemy.config.link_target_options
275
248
  link_target_options.each do |option|
276
249
  # add an underscore to the options to provide the default syntax
277
250
  # @link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target
@@ -280,6 +253,11 @@ module Alchemy
280
253
  end
281
254
  options
282
255
  end
256
+
257
+ # Allow all string and text attributes to be searchable by Ransack.
258
+ def ransackable_attributes(_auth_object = nil)
259
+ searchable_alchemy_resource_attributes + ["updated_at"]
260
+ end
283
261
  end
284
262
 
285
263
  # Instance methods
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class PageDefinition
5
+ include ActiveModel::Model
6
+ include ActiveModel::Attributes
7
+ include Alchemy::Hints
8
+
9
+ extend ActiveModel::Translation
10
+
11
+ attribute :name, :string
12
+ attribute :elements, default: []
13
+ attribute :autogenerate, default: []
14
+ attribute :layoutpage, :boolean, default: false
15
+ attribute :unique, :boolean, default: false
16
+ attribute :cache, :boolean, default: true
17
+ attribute :insert_elements_at, :string, default: "bottom"
18
+ attribute :fixed_attributes, default: {}
19
+ attribute :searchable, :boolean, default: true
20
+ attribute :searchresults, :boolean, default: false
21
+ attribute :hide, :boolean, default: false
22
+ attribute :editable_by
23
+ attribute :hint
24
+
25
+ validates :name,
26
+ presence: true,
27
+ format: {
28
+ with: /\A[a-z_-]+\z/
29
+ }
30
+
31
+ delegate :[], to: :attributes
32
+ delegate :blank?, to: :name
33
+
34
+ class << self
35
+ # Returns all page layouts.
36
+ #
37
+ # They are defined in +config/alchemy/page_layout.yml+ file.
38
+ #
39
+ def all
40
+ @definitions ||= read_definitions_file.map { new(**_1) }
41
+ end
42
+
43
+ def map(...)
44
+ all.map(...)
45
+ end
46
+ alias_method :collect, :map
47
+
48
+ # Add additional page definitions to collection.
49
+ #
50
+ # Useful for extending the page layouts from an Alchemy module.
51
+ #
52
+ # === Usage Example
53
+ #
54
+ # Call +Alchemy::PageDefinition.add(your_definition)+ in your engine.rb file.
55
+ #
56
+ # @param [Array || Hash]
57
+ # You can pass a single layout definition as Hash, or a collection of page layouts as Array.
58
+ #
59
+ def add(definition)
60
+ all
61
+ @definitions += Array.wrap(definition).map { new(**_1) }
62
+ end
63
+
64
+ # Returns one page definition by given name.
65
+ #
66
+ def get(name)
67
+ return new if name.blank?
68
+
69
+ all.detect { _1.name.casecmp(name).zero? }
70
+ end
71
+
72
+ def reset!
73
+ @definitions = nil
74
+ end
75
+
76
+ # The absolute +page_layouts.yml+ file path
77
+ # @return [Pathname]
78
+ def layouts_file_path
79
+ Rails.root.join("config", "alchemy", "page_layouts.yml")
80
+ end
81
+
82
+ private
83
+
84
+ # Reads the layout definitions from +config/alchemy/page_layouts.yml+.
85
+ #
86
+ def read_definitions_file
87
+ if File.exist?(layouts_file_path)
88
+ Array.wrap(
89
+ YAML.safe_load(
90
+ ERB.new(File.read(layouts_file_path)).result,
91
+ permitted_classes: YAML_PERMITTED_CLASSES,
92
+ aliases: true
93
+ ) || []
94
+ )
95
+ else
96
+ raise LoadError, "Could not find page_layouts.yml file! Please run `rails generate alchemy:install`"
97
+ end
98
+ end
99
+ end
100
+
101
+ def human_name
102
+ Alchemy::Page.human_layout_name(name)
103
+ end
104
+
105
+ def attributes
106
+ super.with_indifferent_access
107
+ end
108
+
109
+ private
110
+
111
+ def hint_translation_scope
112
+ :page_hints
113
+ end
114
+ end
115
+ end
@@ -28,24 +28,10 @@ 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
35
 
50
36
  has_many :picture_ingredients,
51
37
  class_name: "Alchemy::Ingredients::Picture",
@@ -54,7 +40,6 @@ module Alchemy
54
40
 
55
41
  has_many :elements, through: :picture_ingredients
56
42
  has_many :pages, through: :elements
57
- has_many :thumbs, class_name: "Alchemy::PictureThumb", dependent: :destroy
58
43
  has_many :descriptions, class_name: "Alchemy::PictureDescription", dependent: :destroy
59
44
 
60
45
  accepts_nested_attributes_for :descriptions, allow_destroy: true, reject_if: ->(attr) { attr[:text].blank? }
@@ -72,7 +57,7 @@ module Alchemy
72
57
 
73
58
  # Image preprocessing class
74
59
  def self.preprocessor_class
75
- @_preprocessor_class ||= Preprocessor
60
+ @_preprocessor_class ||= Alchemy.storage_adapter.preprocessor_class
76
61
  end
77
62
 
78
63
  # Set a image preprocessing class
@@ -84,35 +69,20 @@ module Alchemy
84
69
  @_preprocessor_class = klass
85
70
  end
86
71
 
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
- # Create important thumbnails upfront
98
- after_create -> { PictureThumb.generate_thumbs!(self) if has_convertible_format? }
72
+ include Alchemy.storage_adapter.picture_class_methods
99
73
 
100
74
  # We need to define this method here to have it available in the validations below.
101
75
  class << self
102
76
  def allowed_filetypes
103
- Config.get(:uploader).fetch("allowed_filetypes", {}).fetch("alchemy/pictures", [])
77
+ Alchemy.config.uploader.allowed_filetypes.alchemy_pictures
104
78
  end
105
79
  end
106
80
 
107
81
  validates_presence_of :image_file
108
- validates_size_of :image_file, maximum: Config.get(:uploader)["file_size_limit"].megabytes
109
- validates_property :format,
110
- of: :image_file,
111
- in: allowed_filetypes,
112
- case_sensitive: false,
113
- message: Alchemy.t("not a valid image")
82
+ validates_size_of :image_file, maximum: Alchemy.config.uploader.file_size_limit.megabytes
83
+ validate :image_file_type_allowed, if: -> { image_file.present? }
114
84
 
115
- stampable stamper_class_name: Alchemy.user_class.name
85
+ stampable stamper_class_name: Alchemy.user_class_name
116
86
 
117
87
  scope :named, ->(name) { where("#{table_name}.name LIKE ?", "%#{name}%") }
118
88
  scope :recent, -> { where("#{table_name}.created_at > ?", Time.current - 24.hours).order(:created_at) }
@@ -121,16 +91,17 @@ module Alchemy
121
91
  where("#{table_name}.id NOT IN (SELECT related_object_id FROM alchemy_ingredients WHERE related_object_type = 'Alchemy::Picture')")
122
92
  }
123
93
  scope :without_tag, -> { left_outer_joins(:taggings).where(gutentag_taggings: {id: nil}) }
124
- scope :by_file_format, ->(format) { where(image_file_format: format) }
94
+ scope :by_file_format, ->(file_format) do
95
+ Alchemy.storage_adapter.by_file_format_scope(file_format)
96
+ end
125
97
 
126
98
  # Class methods
127
99
 
128
100
  class << self
129
101
  # The class used to generate URLs for pictures
130
102
  #
131
- # @see Alchemy::Picture::Url
132
103
  def url_class
133
- @_url_class ||= Alchemy::Picture::Url
104
+ @_url_class ||= Alchemy.storage_adapter.picture_url_class
134
105
  end
135
106
 
136
107
  # Set a different picture url class
@@ -140,22 +111,16 @@ module Alchemy
140
111
  @_url_class = klass
141
112
  end
142
113
 
143
- def alchemy_resource_filters
144
- @_file_formats ||= distinct.pluck(:image_file_format).compact.presence || []
145
- [
146
- {
147
- name: :by_file_format,
148
- values: @_file_formats
149
- },
150
- {
151
- name: :misc,
152
- values: %w[recent last_upload without_tag deletable]
153
- }
154
- ]
114
+ def searchable_alchemy_resource_attributes
115
+ Alchemy.storage_adapter.searchable_alchemy_resource_attributes(name)
155
116
  end
156
117
 
157
- def searchable_alchemy_resource_attributes
158
- %w[name image_file_name]
118
+ def ransackable_attributes(_auth_object = nil)
119
+ Alchemy.storage_adapter.ransackable_attributes(name)
120
+ end
121
+
122
+ def ransackable_associations(_auth_object = nil)
123
+ Alchemy.storage_adapter.ransackable_associations(name)
159
124
  end
160
125
 
161
126
  def last_upload
@@ -164,33 +129,30 @@ module Alchemy
164
129
 
165
130
  Picture.where(upload_hash: last_picture.upload_hash)
166
131
  end
132
+
133
+ def ransackable_scopes(_auth_object = nil)
134
+ [:by_file_format, :recent, :last_upload, :without_tag, :deletable]
135
+ end
136
+
137
+ def file_formats(scope = all)
138
+ Alchemy.storage_adapter.file_formats(name, scope:)
139
+ end
167
140
  end
168
141
 
169
142
  # Instance methods
170
143
 
171
144
  # Returns an url (or relative path) to a processed image for use inside an image_tag helper.
172
145
  #
173
- # Any additional options are passed to the url method, so you can add params to your url.
174
- #
175
146
  # Example:
176
147
  #
177
148
  # <%= image_tag picture.url(size: '320x200', format: 'png') %>
178
149
  #
179
- # @see Alchemy::PictureVariant#call for transformation options
180
- # @see Alchemy::Picture::Url#call for url options
181
150
  # @return [String|Nil]
182
151
  def url(options = {})
183
152
  return unless image_file
184
153
 
185
- variant = PictureVariant.new(self, options.slice(*TRANSFORMATION_OPTIONS))
186
- self.class.url_class.new(variant).call(
187
- options.except(*TRANSFORMATION_OPTIONS).merge(
188
- basename: name,
189
- ext: variant.render_format,
190
- name: name
191
- )
192
- )
193
- rescue ::Dragonfly::Job::Fetch::NotFound => e
154
+ self.class.url_class.new(self).call(options)
155
+ rescue Alchemy.storage_adapter.rescuable_errors => e
194
156
  log_warning(e.message)
195
157
  nil
196
158
  end
@@ -205,7 +167,7 @@ module Alchemy
205
167
 
206
168
  url(
207
169
  flatten: true,
208
- format: image_file_format || "jpg",
170
+ format: image_file_extension || "jpg",
209
171
  size: size
210
172
  )
211
173
  end
@@ -239,18 +201,12 @@ module Alchemy
239
201
  end
240
202
  end
241
203
 
242
- # Returns the suffix of the filename.
243
- #
244
- def suffix
245
- image_file.ext
246
- end
247
-
248
204
  # Returns a humanized, readable name from image filename.
249
205
  #
250
206
  def humanized_name
251
207
  return "" if image_file_name.blank?
252
208
 
253
- convert_to_humanized_name(image_file_name, suffix)
209
+ convert_to_humanized_name(image_file_name, image_file_extension)
254
210
  end
255
211
 
256
212
  # Returns the format the image should be rendered with
@@ -260,9 +216,9 @@ module Alchemy
260
216
  #
261
217
  def default_render_format
262
218
  if convertible?
263
- Config.get(:image_output_format)
219
+ Alchemy.config.image_output_format
264
220
  else
265
- image_file_format
221
+ image_file_extension
266
222
  end
267
223
  end
268
224
 
@@ -272,15 +228,15 @@ module Alchemy
272
228
  # image has not a convertible file format (i.e. SVG) this returns +false+
273
229
  #
274
230
  def convertible?
275
- Config.get(:image_output_format) &&
276
- Config.get(:image_output_format) != "original" &&
231
+ Alchemy.config.image_output_format &&
232
+ Alchemy.config.image_output_format != "original" &&
277
233
  has_convertible_format?
278
234
  end
279
235
 
280
236
  # Returns true if the image can be converted into other formats
281
237
  #
282
238
  def has_convertible_format?
283
- image_file_format.in?(CONVERTIBLE_FILE_FORMATS)
239
+ Alchemy.storage_adapter.has_convertible_format?(self)
284
240
  end
285
241
 
286
242
  # Checks if the picture is restricted.
@@ -301,6 +257,32 @@ module Alchemy
301
257
  picture_ingredients.empty?
302
258
  end
303
259
 
260
+ def image_file_name
261
+ Alchemy.storage_adapter.image_file_name(self)
262
+ end
263
+
264
+ def image_file_format
265
+ Alchemy.storage_adapter.image_file_format(self)
266
+ end
267
+
268
+ def image_file_size
269
+ Alchemy.storage_adapter.image_file_size(self)
270
+ end
271
+
272
+ def image_file_width
273
+ Alchemy.storage_adapter.image_file_width(self)
274
+ end
275
+
276
+ def image_file_height
277
+ Alchemy.storage_adapter.image_file_height(self)
278
+ end
279
+
280
+ def image_file_extension
281
+ Alchemy.storage_adapter.image_file_extension(self)
282
+ end
283
+ alias_method :suffix, :image_file_extension
284
+ deprecate suffix: :image_file_extension, deprecator: Alchemy::Deprecation
285
+
304
286
  # A size String from original image file values.
305
287
  #
306
288
  # == Example
@@ -310,5 +292,13 @@ module Alchemy
310
292
  def image_file_dimensions
311
293
  "#{image_file_width}x#{image_file_height}"
312
294
  end
295
+
296
+ private
297
+
298
+ def image_file_type_allowed
299
+ unless image_file_extension&.in?(self.class.allowed_filetypes)
300
+ errors.add(:image_file, Alchemy.t("not a valid image"))
301
+ end
302
+ end
313
303
  end
314
304
  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