alchemy_cms 5.2.0 → 6.0.0.b3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -14
  3. data/.gitignore +0 -1
  4. data/.hound.yml +1 -1
  5. data/.rubocop.yml +46 -4
  6. data/CHANGELOG.md +114 -5
  7. data/Gemfile +8 -1
  8. data/README.md +5 -2
  9. data/alchemy_cms.gemspec +78 -65
  10. data/app/assets/javascripts/alchemy/admin.js +0 -2
  11. data/app/assets/javascripts/alchemy/alchemy.base.js.coffee +0 -27
  12. data/app/assets/javascripts/alchemy/alchemy.confirm_dialog.js.coffee +2 -1
  13. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +1 -1
  14. data/app/assets/javascripts/alchemy/alchemy.dragndrop.js.coffee +0 -25
  15. data/app/assets/javascripts/alchemy/alchemy.element_editors.js.coffee +1 -1
  16. data/app/assets/javascripts/alchemy/alchemy.elements_window.js.coffee +2 -0
  17. data/app/assets/javascripts/alchemy/alchemy.fixed_elements.js +1 -1
  18. data/app/assets/javascripts/alchemy/alchemy.gui.js.coffee +3 -1
  19. data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +1 -1
  20. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +40 -27
  21. data/app/assets/javascripts/alchemy/templates/node_folder.hbs +1 -1
  22. data/app/assets/stylesheets/alchemy/_extends.scss +15 -2
  23. data/app/assets/stylesheets/alchemy/admin.scss +1 -1
  24. data/app/assets/stylesheets/alchemy/archive.scss +20 -5
  25. data/app/assets/stylesheets/alchemy/buttons.scss +0 -4
  26. data/app/assets/stylesheets/alchemy/elements.scss +73 -61
  27. data/app/assets/stylesheets/alchemy/images.scss +8 -0
  28. data/app/assets/stylesheets/alchemy/node-select.scss +4 -3
  29. data/app/assets/stylesheets/alchemy/page-select.scss +1 -0
  30. data/app/controllers/alchemy/admin/attachments_controller.rb +8 -4
  31. data/app/controllers/alchemy/admin/base_controller.rb +5 -7
  32. data/app/controllers/alchemy/admin/elements_controller.rb +59 -34
  33. data/app/controllers/alchemy/admin/essence_audios_controller.rb +30 -0
  34. data/app/controllers/alchemy/admin/essence_files_controller.rb +0 -14
  35. data/app/controllers/alchemy/admin/essence_pictures_controller.rb +8 -79
  36. data/app/controllers/alchemy/admin/essence_videos_controller.rb +33 -0
  37. data/app/controllers/alchemy/admin/ingredients_controller.rb +30 -0
  38. data/app/controllers/alchemy/admin/layoutpages_controller.rb +0 -1
  39. data/app/controllers/alchemy/admin/pages_controller.rb +7 -22
  40. data/app/controllers/alchemy/admin/pictures_controller.rb +56 -17
  41. data/app/controllers/alchemy/admin/resources_controller.rb +84 -10
  42. data/app/controllers/alchemy/api/elements_controller.rb +13 -4
  43. data/app/controllers/alchemy/api/pages_controller.rb +4 -3
  44. data/app/controllers/concerns/alchemy/admin/archive_overlay.rb +13 -3
  45. data/app/controllers/concerns/alchemy/admin/crop_action.rb +26 -0
  46. data/app/decorators/alchemy/element_editor.rb +26 -1
  47. data/app/decorators/alchemy/ingredient_editor.rb +158 -0
  48. data/app/helpers/alchemy/admin/elements_helper.rb +1 -0
  49. data/app/helpers/alchemy/admin/essences_helper.rb +1 -1
  50. data/app/helpers/alchemy/admin/ingredients_helper.rb +42 -0
  51. data/app/helpers/alchemy/elements_block_helper.rb +23 -6
  52. data/app/helpers/alchemy/elements_helper.rb +12 -5
  53. data/app/helpers/alchemy/pages_helper.rb +3 -11
  54. data/app/jobs/alchemy/base_job.rb +11 -0
  55. data/app/jobs/alchemy/publish_page_job.rb +11 -0
  56. data/app/models/alchemy/attachment.rb +24 -7
  57. data/app/models/alchemy/content.rb +1 -6
  58. data/app/models/alchemy/content/factory.rb +23 -27
  59. data/app/models/alchemy/element.rb +39 -72
  60. data/app/models/alchemy/element/definitions.rb +29 -27
  61. data/app/models/alchemy/element/element_contents.rb +131 -122
  62. data/app/models/alchemy/element/element_essences.rb +111 -98
  63. data/app/models/alchemy/element/element_ingredients.rb +184 -0
  64. data/app/models/alchemy/element/presenters.rb +104 -85
  65. data/app/models/alchemy/elements_repository.rb +126 -0
  66. data/app/models/alchemy/essence_audio.rb +12 -0
  67. data/app/models/alchemy/essence_headline.rb +40 -0
  68. data/app/models/alchemy/essence_picture.rb +4 -116
  69. data/app/models/alchemy/essence_richtext.rb +12 -0
  70. data/app/models/alchemy/essence_video.rb +12 -0
  71. data/app/models/alchemy/image_cropper_settings.rb +87 -0
  72. data/app/models/alchemy/ingredient.rb +183 -0
  73. data/app/models/alchemy/ingredient_validator.rb +97 -0
  74. data/app/models/alchemy/ingredients/audio.rb +29 -0
  75. data/app/models/alchemy/ingredients/boolean.rb +21 -0
  76. data/app/models/alchemy/ingredients/datetime.rb +20 -0
  77. data/app/models/alchemy/ingredients/file.rb +30 -0
  78. data/app/models/alchemy/ingredients/headline.rb +42 -0
  79. data/app/models/alchemy/ingredients/html.rb +19 -0
  80. data/app/models/alchemy/ingredients/link.rb +16 -0
  81. data/app/models/alchemy/ingredients/node.rb +23 -0
  82. data/app/models/alchemy/ingredients/page.rb +23 -0
  83. data/app/models/alchemy/ingredients/picture.rb +41 -0
  84. data/app/models/alchemy/ingredients/richtext.rb +57 -0
  85. data/app/models/alchemy/ingredients/select.rb +10 -0
  86. data/app/models/alchemy/ingredients/text.rb +17 -0
  87. data/app/models/alchemy/ingredients/video.rb +33 -0
  88. data/app/models/alchemy/language.rb +0 -11
  89. data/app/models/alchemy/page.rb +76 -33
  90. data/app/models/alchemy/page/fixed_attributes.rb +53 -51
  91. data/app/models/alchemy/page/page_elements.rb +186 -205
  92. data/app/models/alchemy/page/page_naming.rb +66 -64
  93. data/app/models/alchemy/page/page_natures.rb +139 -142
  94. data/app/models/alchemy/page/page_scopes.rb +117 -102
  95. data/app/models/alchemy/page/publisher.rb +50 -0
  96. data/app/models/alchemy/page/url_path.rb +1 -1
  97. data/app/models/alchemy/page_version.rb +58 -0
  98. data/app/models/alchemy/picture.rb +18 -40
  99. data/app/models/alchemy/picture/calculations.rb +2 -8
  100. data/app/models/alchemy/picture/preprocessor.rb +2 -0
  101. data/app/models/alchemy/picture/transformations.rb +24 -96
  102. data/app/models/concerns/alchemy/picture_thumbnails.rb +181 -0
  103. data/app/models/concerns/alchemy/touch_elements.rb +2 -2
  104. data/app/presenters/alchemy/picture_view.rb +88 -0
  105. data/app/serializers/alchemy/element_serializer.rb +5 -0
  106. data/app/serializers/alchemy/page_tree_serializer.rb +3 -2
  107. data/app/services/alchemy/delete_elements.rb +44 -0
  108. data/app/services/alchemy/duplicate_element.rb +56 -0
  109. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +2 -3
  110. data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
  111. data/app/views/alchemy/admin/attachments/assign.js.erb +11 -0
  112. data/app/views/alchemy/admin/attachments/index.html.erb +2 -3
  113. data/app/views/alchemy/admin/crop.html.erb +36 -0
  114. data/app/views/alchemy/admin/elements/_element.html.erb +14 -10
  115. data/app/views/alchemy/admin/elements/{_element_footer.html.erb → _footer.html.erb} +0 -0
  116. data/app/views/alchemy/admin/elements/{_new_element_form.html.erb → _form.html.erb} +1 -1
  117. data/app/views/alchemy/admin/elements/{_element_header.html.erb → _header.html.erb} +1 -1
  118. data/app/views/alchemy/admin/elements/{_element_toolbar.html.erb → _toolbar.html.erb} +5 -6
  119. data/app/views/alchemy/admin/elements/create.js.erb +1 -1
  120. data/app/views/alchemy/admin/elements/{trash.js.erb → destroy.js.erb} +2 -6
  121. data/app/views/alchemy/admin/elements/fold.js.erb +2 -2
  122. data/app/views/alchemy/admin/elements/new.html.erb +3 -3
  123. data/app/views/alchemy/admin/elements/order.js.erb +0 -17
  124. data/app/views/alchemy/admin/elements/update.js.erb +3 -2
  125. data/app/views/alchemy/admin/essence_audios/edit.html.erb +7 -0
  126. data/app/views/alchemy/admin/essence_pictures/update.js.erb +0 -1
  127. data/app/views/alchemy/admin/essence_videos/edit.html.erb +11 -0
  128. data/app/views/alchemy/admin/ingredients/_audio_fields.html.erb +4 -0
  129. data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +18 -0
  130. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +25 -0
  131. data/app/views/alchemy/admin/ingredients/_video_fields.html.erb +8 -0
  132. data/app/views/alchemy/admin/ingredients/edit.html.erb +4 -0
  133. data/app/views/alchemy/admin/layoutpages/edit.html.erb +0 -5
  134. data/app/views/alchemy/admin/nodes/_node.html.erb +2 -2
  135. data/app/views/alchemy/admin/pages/_anchor_link.html.erb +1 -1
  136. data/app/views/alchemy/admin/pages/_external_link.html.erb +1 -1
  137. data/app/views/alchemy/admin/pages/_file_link.html.erb +1 -1
  138. data/app/views/alchemy/admin/pages/_form.html.erb +0 -6
  139. data/app/views/alchemy/admin/pages/_internal_link.html.erb +1 -1
  140. data/app/views/alchemy/admin/pages/_tinymce_custom_config.html.erb +5 -2
  141. data/app/views/alchemy/admin/pages/_toolbar.html.erb +1 -1
  142. data/app/views/alchemy/admin/pages/edit.html.erb +36 -24
  143. data/app/views/alchemy/admin/pages/index.html.erb +2 -9
  144. data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +2 -4
  145. data/app/views/alchemy/admin/partials/_routes.html.erb +7 -11
  146. data/app/views/alchemy/admin/partials/_search_form.html.erb +9 -0
  147. data/app/views/alchemy/admin/pictures/_archive.html.erb +1 -1
  148. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
  149. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +5 -7
  150. data/app/views/alchemy/admin/pictures/_infos.html.erb +0 -1
  151. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +4 -4
  152. data/app/views/alchemy/admin/pictures/assign.js.erb +10 -0
  153. data/app/views/alchemy/admin/pictures/index.html.erb +8 -3
  154. data/app/views/alchemy/admin/resources/_filter.html.erb +12 -0
  155. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +14 -17
  156. data/app/views/alchemy/admin/resources/_form.html.erb +3 -0
  157. data/app/views/alchemy/admin/resources/_table_header.html.erb +15 -0
  158. data/app/views/alchemy/admin/resources/index.html.erb +3 -11
  159. data/app/views/alchemy/essences/_essence_audio_editor.html.erb +4 -0
  160. data/app/views/alchemy/essences/_essence_audio_view.html.erb +15 -0
  161. data/app/views/alchemy/essences/_essence_file_editor.html.erb +15 -6
  162. data/app/views/alchemy/essences/_essence_headline_editor.html.erb +36 -0
  163. data/app/views/alchemy/essences/_essence_headline_view.html.erb +10 -0
  164. data/app/views/alchemy/essences/_essence_link_editor.html.erb +8 -4
  165. data/app/views/alchemy/essences/_essence_picture_editor.html.erb +27 -12
  166. data/app/views/alchemy/essences/_essence_picture_view.html.erb +3 -3
  167. data/app/views/alchemy/essences/_essence_text_editor.html.erb +12 -4
  168. data/app/views/alchemy/essences/_essence_video_editor.html.erb +4 -0
  169. data/app/views/alchemy/essences/_essence_video_view.html.erb +18 -0
  170. data/app/views/alchemy/essences/shared/_essence_picture_tools.html.erb +21 -16
  171. data/app/views/alchemy/essences/shared/_linkable_essence_tools.html.erb +2 -2
  172. data/app/views/alchemy/ingredients/_audio_editor.html.erb +5 -0
  173. data/app/views/alchemy/ingredients/_audio_view.html.erb +14 -0
  174. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +11 -0
  175. data/app/views/alchemy/ingredients/_boolean_view.html.erb +1 -0
  176. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +17 -0
  177. data/app/views/alchemy/ingredients/_datetime_view.html.erb +9 -0
  178. data/app/views/alchemy/ingredients/_file_editor.html.erb +52 -0
  179. data/app/views/alchemy/ingredients/_file_view.html.erb +17 -0
  180. data/app/views/alchemy/ingredients/_headline_editor.html.erb +30 -0
  181. data/app/views/alchemy/ingredients/_headline_view.html.erb +9 -0
  182. data/app/views/alchemy/ingredients/_html_editor.html.erb +8 -0
  183. data/app/views/alchemy/ingredients/_html_view.html.erb +1 -0
  184. data/app/views/alchemy/ingredients/_link_editor.html.erb +24 -0
  185. data/app/views/alchemy/ingredients/_link_view.html.erb +9 -0
  186. data/app/views/alchemy/ingredients/_node_editor.html.erb +26 -0
  187. data/app/views/alchemy/ingredients/_node_view.html.erb +1 -0
  188. data/app/views/alchemy/ingredients/_page_editor.html.erb +25 -0
  189. data/app/views/alchemy/ingredients/_page_view.html.erb +4 -0
  190. data/app/views/alchemy/ingredients/_picture_editor.html.erb +60 -0
  191. data/app/views/alchemy/ingredients/_picture_view.html.erb +5 -0
  192. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +12 -0
  193. data/app/views/alchemy/ingredients/_richtext_view.html.erb +3 -0
  194. data/app/views/alchemy/ingredients/_select_editor.html.erb +30 -0
  195. data/app/views/alchemy/ingredients/_select_view.html.erb +1 -0
  196. data/app/views/alchemy/ingredients/_text_editor.html.erb +20 -0
  197. data/app/views/alchemy/ingredients/_text_view.html.erb +16 -0
  198. data/app/views/alchemy/ingredients/_video_editor.html.erb +5 -0
  199. data/app/views/alchemy/ingredients/_video_view.html.erb +17 -0
  200. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +20 -0
  201. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +57 -0
  202. data/config/brakeman.ignore +66 -159
  203. data/config/initializers/dragonfly.rb +10 -0
  204. data/config/locales/alchemy.en.yml +108 -64
  205. data/config/routes.rb +17 -22
  206. data/db/migrate/20201207131309_create_page_versions.rb +19 -0
  207. data/db/migrate/20201207135820_add_page_version_id_to_alchemy_elements.rb +76 -0
  208. data/db/migrate/20210205143548_rename_public_on_and_public_until_on_alchemy_pages.rb +10 -0
  209. data/db/migrate/20210326105046_add_sanitized_body_to_alchemy_essence_richtexts.rb +7 -0
  210. data/db/migrate/20210406093436_add_alchemy_essence_headlines.rb +12 -0
  211. data/db/migrate/20210506135919_create_essence_audios.rb +19 -0
  212. data/db/migrate/20210506140258_create_essence_videos.rb +23 -0
  213. data/db/migrate/20210508091432_create_alchemy_ingredients.rb +22 -0
  214. data/lib/alchemy/admin/preview_url.rb +2 -0
  215. data/lib/alchemy/deprecation.rb +1 -1
  216. data/lib/alchemy/dragonfly/processors/auto_orient.rb +18 -0
  217. data/lib/alchemy/dragonfly/processors/crop_resize.rb +35 -0
  218. data/lib/alchemy/elements_finder.rb +14 -60
  219. data/lib/alchemy/essence.rb +1 -2
  220. data/lib/alchemy/forms/builder.rb +21 -1
  221. data/lib/alchemy/hints.rb +8 -4
  222. data/lib/alchemy/page_layout.rb +0 -13
  223. data/lib/alchemy/permissions.rb +30 -29
  224. data/lib/alchemy/resource.rb +13 -3
  225. data/lib/alchemy/resource_filter.rb +40 -0
  226. data/lib/alchemy/resources_helper.rb +1 -16
  227. data/lib/alchemy/tasks/tidy.rb +29 -0
  228. data/lib/alchemy/test_support.rb +2 -11
  229. data/lib/alchemy/test_support/essence_shared_examples.rb +0 -1
  230. data/lib/alchemy/test_support/factories/element_factory.rb +8 -8
  231. data/lib/alchemy/test_support/factories/essence_audio_factory.rb +7 -0
  232. data/lib/alchemy/test_support/factories/essence_video_factory.rb +7 -0
  233. data/lib/alchemy/test_support/factories/ingredient_factory.rb +25 -0
  234. data/lib/alchemy/test_support/factories/page_factory.rb +20 -1
  235. data/lib/alchemy/test_support/factories/page_version_factory.rb +23 -0
  236. data/lib/alchemy/test_support/having_crop_action_examples.rb +170 -0
  237. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +646 -0
  238. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +21 -0
  239. data/lib/alchemy/test_support/shared_ingredient_examples.rb +75 -0
  240. data/lib/alchemy/tinymce.rb +17 -0
  241. data/lib/alchemy/upgrader/six_point_zero.rb +21 -0
  242. data/lib/alchemy/upgrader/tasks/add_page_versions.rb +33 -0
  243. data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +62 -0
  244. data/lib/alchemy/version.rb +1 -1
  245. data/lib/alchemy_cms.rb +1 -0
  246. data/lib/generators/alchemy/elements/elements_generator.rb +1 -0
  247. data/lib/generators/alchemy/elements/templates/view.html.erb +9 -0
  248. data/lib/generators/alchemy/elements/templates/view.html.haml +9 -0
  249. data/lib/generators/alchemy/elements/templates/view.html.slim +9 -0
  250. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +38 -0
  251. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +14 -0
  252. data/lib/generators/alchemy/ingredient/templates/model.rb.tt +13 -0
  253. data/lib/generators/alchemy/ingredient/templates/view.html.erb +1 -0
  254. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +1 -1
  255. data/lib/generators/alchemy/menus/templates/node.html.erb +1 -1
  256. data/lib/generators/alchemy/menus/templates/node.html.haml +1 -1
  257. data/lib/generators/alchemy/menus/templates/node.html.slim +1 -1
  258. data/lib/generators/alchemy/menus/templates/wrapper.html.erb +1 -1
  259. data/lib/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
  260. data/lib/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
  261. data/lib/tasks/alchemy/thumbnails.rake +4 -2
  262. data/lib/tasks/alchemy/tidy.rake +12 -0
  263. data/lib/tasks/alchemy/upgrade.rake +26 -0
  264. data/package.json +3 -2
  265. data/package/admin.js +11 -1
  266. data/package/src/__tests__/i18n.spec.js +23 -0
  267. data/package/src/file_editors.js +28 -0
  268. data/package/src/i18n.js +1 -3
  269. data/package/src/image_cropper.js +103 -0
  270. data/package/src/image_loader.js +58 -0
  271. data/package/src/node_tree.js +5 -5
  272. data/package/src/picture_editors.js +169 -0
  273. data/package/src/utils/__tests__/ajax.spec.js +20 -12
  274. data/package/src/utils/ajax.js +8 -3
  275. data/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +3 -18
  276. data/vendor/assets/stylesheets/jquery.Jcrop.min.scss +2 -28
  277. metadata +292 -55
  278. data/app/assets/javascripts/alchemy/alchemy.image_cropper.js.coffee +0 -44
  279. data/app/assets/javascripts/alchemy/alchemy.trash_window.js.coffee +0 -30
  280. data/app/assets/stylesheets/alchemy/trash.scss +0 -8
  281. data/app/controllers/alchemy/admin/trash_controller.rb +0 -44
  282. data/app/views/alchemy/admin/attachments/_filter_bar.html.erb +0 -29
  283. data/app/views/alchemy/admin/essence_files/assign.js.erb +0 -3
  284. data/app/views/alchemy/admin/essence_pictures/assign.js.erb +0 -4
  285. data/app/views/alchemy/admin/essence_pictures/crop.html.erb +0 -48
  286. data/app/views/alchemy/admin/pictures/_filter_bar.html.erb +0 -30
  287. data/app/views/alchemy/admin/trash/clear.js.erb +0 -4
  288. data/app/views/alchemy/admin/trash/index.html.erb +0 -31
  289. data/lib/alchemy/test_support/factories.rb +0 -16
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class EssenceAudio < ActiveRecord::Base
5
+ acts_as_essence(
6
+ ingredient_column: :attachment,
7
+ preview_text_method: :name,
8
+ )
9
+
10
+ belongs_to :attachment, optional: true
11
+ end
12
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class EssenceHeadline < BaseRecord
5
+ acts_as_essence
6
+
7
+ after_initialize :set_level_and_size
8
+
9
+ def preview_text(maxlength = 30)
10
+ "H#{level}: #{body}"[0..maxlength - 1]
11
+ end
12
+
13
+ def level_options
14
+ levels.map { |level| ["H#{level}", level] }
15
+ end
16
+
17
+ def size_options
18
+ sizes.map { |size| ["H#{size}", size] }
19
+ end
20
+
21
+ private
22
+
23
+ def content_settings
24
+ content&.settings || {}
25
+ end
26
+
27
+ def levels
28
+ content_settings.fetch(:levels, (1..6))
29
+ end
30
+
31
+ def sizes
32
+ content_settings.fetch(:sizes, [])
33
+ end
34
+
35
+ def set_level_and_size
36
+ self.level ||= levels.first
37
+ self.size ||= sizes.first
38
+ end
39
+ end
40
+ end
@@ -23,6 +23,8 @@
23
23
 
24
24
  module Alchemy
25
25
  class EssencePicture < BaseRecord
26
+ include Alchemy::PictureThumbnails
27
+
26
28
  acts_as_essence ingredient_column: :picture, belongs_to: {
27
29
  class_name: "Alchemy::Picture",
28
30
  foreign_key: :picture_id,
@@ -30,81 +32,9 @@ module Alchemy
30
32
  optional: true,
31
33
  }
32
34
 
33
- delegate :image_file_width, :image_file_height, :image_file, to: :picture
34
- before_save :fix_crop_values
35
- before_save :replace_newlines
36
-
37
- include Alchemy::Picture::Transformations
38
-
39
- # The url to show the picture.
40
- #
41
- # Takes all values like +name+ and crop sizes (+crop_from+, +crop_size+ from the build in graphical image cropper)
42
- # and also adds the security token.
43
- #
44
- # You typically want to set the size the picture should be resized to.
45
- #
46
- # === Example:
47
- #
48
- # essence_picture.picture_url(size: '200x300', crop: true, format: 'gif')
49
- # # '/pictures/1/show/200x300/crop/cats.gif?sh=765rfghj'
50
- #
51
- # @option options size [String]
52
- # The size the picture should be resized to.
53
- #
54
- # @option options format [String]
55
- # The format the picture should be rendered in.
56
- # Defaults to the +image_output_format+ from the +Alchemy::Config+.
57
- #
58
- # @option options crop [Boolean]
59
- # If set to true the picture will be cropped to fit the size value.
60
- #
61
- # @return [String]
62
- def picture_url(options = {})
63
- return if picture.nil?
64
-
65
- picture.url(picture_url_options.merge(options)) || "missing-image.png"
66
- end
35
+ delegate :settings, to: :content
67
36
 
68
- # Picture rendering options
69
- #
70
- # Returns the +default_render_format+ of the associated +Alchemy::Picture+
71
- # together with the +crop_from+ and +crop_size+ values
72
- #
73
- # @return [HashWithIndifferentAccess]
74
- def picture_url_options
75
- return {} if picture.nil?
76
-
77
- {
78
- format: picture.default_render_format,
79
- crop_from: crop_from.presence,
80
- crop_size: crop_size.presence,
81
- size: content.settings[:size],
82
- }.with_indifferent_access
83
- end
84
-
85
- # Returns an url for the thumbnail representation of the assigned picture
86
- #
87
- # It takes cropping values into account, so it always represents the current
88
- # image displayed in the frontend.
89
- #
90
- # @return [String]
91
- def thumbnail_url
92
- return if picture.nil?
93
-
94
- crop = crop_values_present? || content.settings[:crop]
95
- size = render_size || content.settings[:size]
96
-
97
- options = {
98
- size: thumbnail_size(size, crop),
99
- crop: !!crop,
100
- crop_from: crop_from.presence,
101
- crop_size: crop_size.presence,
102
- flatten: true,
103
- format: picture.image_file_format,
104
- }
105
-
106
- picture.url(options) || "alchemy/missing-image.svg"
107
- end
37
+ before_save :replace_newlines
108
38
 
109
39
  # The name of the picture used as preview text in element editor views.
110
40
  #
@@ -118,18 +48,6 @@ module Alchemy
118
48
  picture.name.to_s[0..max - 1]
119
49
  end
120
50
 
121
- # A Hash of coordinates suitable for the graphical image cropper.
122
- #
123
- # @return [Hash]
124
- def cropping_mask
125
- return if crop_from.blank? || crop_size.blank?
126
-
127
- crop_from = point_from_string(read_attribute(:crop_from))
128
- crop_size = sizes_from_string(read_attribute(:crop_size))
129
-
130
- point_and_mask_to_points(crop_from, crop_size)
131
- end
132
-
133
51
  # Returns a serialized ingredient value for json api
134
52
  #
135
53
  # @return [String]
@@ -137,38 +55,8 @@ module Alchemy
137
55
  picture_url(content.settings)
138
56
  end
139
57
 
140
- # Show image cropping link for content
141
- def allow_image_cropping?
142
- content && content.settings[:crop] && picture &&
143
- picture.can_be_cropped_to?(
144
- content.settings[:size],
145
- content.settings[:upsample],
146
- ) && !!picture.image_file
147
- end
148
-
149
- def crop_values_present?
150
- crop_from.present? && crop_size.present?
151
- end
152
-
153
58
  private
154
59
 
155
- def fix_crop_values
156
- %i(crop_from crop_size).each do |crop_value|
157
- if self[crop_value].is_a?(String)
158
- write_attribute crop_value, normalize_crop_value(crop_value)
159
- end
160
- end
161
- end
162
-
163
- def normalize_crop_value(crop_value)
164
- self[crop_value].split("x").map { |n| normalize_number(n) }.join("x")
165
- end
166
-
167
- def normalize_number(number)
168
- number = number.to_f.round
169
- number.negative? ? 0 : number
170
- end
171
-
172
60
  def replace_newlines
173
61
  return nil if caption.nil?
174
62
 
@@ -17,6 +17,7 @@ module Alchemy
17
17
  acts_as_essence preview_text_column: "stripped_body"
18
18
 
19
19
  before_save :strip_content
20
+ before_save :sanitize_content
20
21
 
21
22
  def has_tinymce?
22
23
  true
@@ -27,5 +28,16 @@ module Alchemy
27
28
  def strip_content
28
29
  self.stripped_body = Rails::Html::FullSanitizer.new.sanitize(body)
29
30
  end
31
+
32
+ def sanitize_content
33
+ self.sanitized_body = Rails::Html::SafeListSanitizer.new.sanitize(
34
+ body,
35
+ content_sanitizer_settings
36
+ )
37
+ end
38
+
39
+ def content_sanitizer_settings
40
+ content&.settings&.fetch(:sanitizer, {}) || {}
41
+ end
30
42
  end
31
43
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class EssenceVideo < ActiveRecord::Base
5
+ acts_as_essence(
6
+ ingredient_column: :attachment,
7
+ preview_text_method: :name,
8
+ )
9
+
10
+ belongs_to :attachment, optional: true
11
+ end
12
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ # Settings for the graphical JS image cropper
5
+ class ImageCropperSettings
6
+ attr_reader :render_size, :default_crop_from, :default_crop_size, :fixed_ratio, :image_width, :image_height
7
+
8
+ def initialize(render_size:, default_crop_from:, default_crop_size:, fixed_ratio:, image_width:, image_height:)
9
+ @render_size = render_size || [0, 0]
10
+ @fixed_ratio = fixed_ratio
11
+ @image_width = image_width.to_i
12
+ @image_height = image_height.to_i
13
+ @default_crop_from = default_crop_from || [0, 0]
14
+ @default_crop_size = default_crop_size || [@image_width, @image_height]
15
+ end
16
+
17
+ def to_h
18
+ return {} if image_width.zero? || image_height.zero?
19
+
20
+ {
21
+ min_size: large_enough? ? min_size : false,
22
+ ratio: ratio,
23
+ default_box: default_box,
24
+ image_size: [image_width, image_height],
25
+ }.freeze
26
+ end
27
+
28
+ def [](key)
29
+ to_h[key]
30
+ end
31
+
32
+ private
33
+
34
+ def ratio
35
+ return false if fixed_ratio == false
36
+ return ratio_from_size if fixed_ratio.nil?
37
+
38
+ Float(fixed_ratio)
39
+ end
40
+
41
+ # Only returns an array of width and height if image is large enough
42
+ # or false to disable min size option of the image cropper
43
+ def large_enough?
44
+ return true if render_size.any?(&:zero?)
45
+
46
+ image_width >= render_size[0] && image_height >= render_size[1]
47
+ end
48
+
49
+ # Infers the aspect ratio from size or fixed_ratio. If you don't want a fixed
50
+ # aspect ratio, don't specify a size or only width or height.
51
+ #
52
+ def ratio_from_size
53
+ if render_size.none?(&:zero?)
54
+ render_size[0].to_f / render_size[1]
55
+ elsif [image_width, image_height].none?(&:zero?)
56
+ image_width.to_f / image_height
57
+ else
58
+ false
59
+ end
60
+ end
61
+
62
+ # Infers the minimum width or height
63
+ # if the aspect ratio and one dimension is specified.
64
+ #
65
+ def min_size
66
+ return render_size unless ratio
67
+
68
+ if render_size[1].zero?
69
+ [render_size[0], (render_size[0] / ratio).to_i]
70
+ else
71
+ [(render_size[1] * ratio).to_i, render_size[1]]
72
+ end
73
+ end
74
+
75
+ # Given a point and a mask, this function returns the area on the
76
+ # underlying canvas as a Hash of two points
77
+ #
78
+ def default_box
79
+ [
80
+ default_crop_from[0],
81
+ default_crop_from[1],
82
+ default_crop_from[0] + default_crop_size[0],
83
+ default_crop_from[1] + default_crop_size[1],
84
+ ]
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class Ingredient < BaseRecord
5
+ class DefinitionError < StandardError; end
6
+
7
+ include Hints
8
+
9
+ self.table_name = "alchemy_ingredients"
10
+
11
+ belongs_to :element, touch: true, class_name: "Alchemy::Element", inverse_of: :ingredients
12
+ belongs_to :related_object, polymorphic: true, optional: true
13
+
14
+ before_validation(on: :create) { self.value ||= default_value }
15
+
16
+ validates :type, presence: true
17
+ validates :role, presence: true
18
+
19
+ validates_with Alchemy::IngredientValidator, on: :update, if: :has_validations?
20
+
21
+ scope :audios, -> { where(type: "Alchemy::Ingredients::Audio") }
22
+ scope :booleans, -> { where(type: "Alchemy::Ingredients::Boolean") }
23
+ scope :datetimes, -> { where(type: "Alchemy::Ingredients::Datetime") }
24
+ scope :files, -> { where(type: "Alchemy::Ingredients::File") }
25
+ scope :headlines, -> { where(type: "Alchemy::Ingredients::Headline") }
26
+ scope :htmls, -> { where(type: "Alchemy::Ingredients::Html") }
27
+ scope :links, -> { where(type: "Alchemy::Ingredients::Link") }
28
+ scope :nodes, -> { where(type: "Alchemy::Ingredients::Node") }
29
+ scope :pages, -> { where(type: "Alchemy::Ingredients::Page") }
30
+ scope :pictures, -> { where(type: "Alchemy::Ingredients::Picture") }
31
+ scope :richtexts, -> { where(type: "Alchemy::Ingredients::Richtext") }
32
+ scope :selects, -> { where(type: "Alchemy::Ingredients::Select") }
33
+ scope :texts, -> { where(type: "Alchemy::Ingredients::Text") }
34
+ scope :videos, -> { where(type: "Alchemy::Ingredients::Video") }
35
+
36
+ class << self
37
+ # Defines getter and setter method aliases for related object
38
+ #
39
+ # @param [String|Symbol] The name of the alias
40
+ # @param [String] The class name of the related object
41
+ def related_object_alias(name, class_name:)
42
+ alias_method name, :related_object
43
+ alias_method "#{name}=", :related_object=
44
+
45
+ # Somehow Rails STI does not allow us to use `alias_method` for the related_object_id
46
+ define_method "#{name}_id" do
47
+ related_object_id
48
+ end
49
+
50
+ define_method "#{name}_id=" do |id|
51
+ self.related_object_id = id
52
+ self.related_object_type = class_name
53
+ end
54
+ end
55
+
56
+ # Modulize ingredient type
57
+ #
58
+ # Makes sure the passed ingredient type is in the +Alchemy::Ingredients+
59
+ # module namespace.
60
+ #
61
+ # If you add custom ingredient class,
62
+ # put them in the +Alchemy::Ingredients+ module namespace
63
+ # @param [String] Ingredient class name
64
+ # @return [String]
65
+ def normalize_type(ingredient_type)
66
+ "Alchemy::Ingredients::#{ingredient_type.to_s.classify.demodulize}"
67
+ end
68
+
69
+ def translated_label_for(role, element_name = nil)
70
+ Alchemy.t(
71
+ role,
72
+ scope: "ingredient_roles.#{element_name}",
73
+ default: Alchemy.t("ingredient_roles.#{role}", default: role.humanize),
74
+ )
75
+ end
76
+ end
77
+
78
+ # Compatibility method for access from element
79
+ def essence
80
+ self
81
+ end
82
+
83
+ # The value or the related object if present
84
+ def value
85
+ related_object || self[:value]
86
+ end
87
+
88
+ # Settings for this ingredient from the +elements.yml+ definition.
89
+ def settings
90
+ definition[:settings] || {}
91
+ end
92
+
93
+ # Fetches value from settings
94
+ #
95
+ # @param key [Symbol] - The hash key you want to fetch the value from
96
+ # @param options [Hash] - An optional Hash that can override the settings.
97
+ # Normally passed as options hash into the content
98
+ # editor view.
99
+ def settings_value(key, options = {})
100
+ settings.merge(options || {})[key.to_sym]
101
+ end
102
+
103
+ # Definition hash for this ingredient from +elements.yml+ file.
104
+ #
105
+ def definition
106
+ return {} unless element
107
+
108
+ element.ingredient_definition_for(role) || {}
109
+ end
110
+
111
+ # The first 30 characters of the value
112
+ #
113
+ # Used by the Element#preview_text method.
114
+ #
115
+ # @param [Integer] max_length (30)
116
+ #
117
+ def preview_text(maxlength = 30)
118
+ value.to_s[0..maxlength - 1]
119
+ end
120
+
121
+ # Cross DB adapter data accessor that works
122
+ def data
123
+ @_data ||= (self[:data] || {}).with_indifferent_access
124
+ end
125
+
126
+ # The path to the view partial of the ingredient
127
+ # @return [String]
128
+ def to_partial_path
129
+ "alchemy/ingredients/#{partial_name}_view"
130
+ end
131
+
132
+ # The demodulized underscored class name of the ingredient
133
+ # @return [String]
134
+ def partial_name
135
+ self.class.name.demodulize.underscore
136
+ end
137
+
138
+ # @return [Boolean]
139
+ def has_validations?
140
+ !!definition[:validate]
141
+ end
142
+
143
+ # @return [Boolean]
144
+ def has_hint?
145
+ !!definition[:hint]
146
+ end
147
+
148
+ # @return [Boolean]
149
+ def deprecated?
150
+ !!definition[:deprecated]
151
+ end
152
+
153
+ # @return [Boolean]
154
+ def has_tinymce?
155
+ false
156
+ end
157
+
158
+ # @return [Boolean]
159
+ def preview_ingredient?
160
+ !!definition[:as_element_title]
161
+ end
162
+
163
+ private
164
+
165
+ def hint_translation_attribute
166
+ role
167
+ end
168
+
169
+ # Returns the default value from ingredient definition
170
+ #
171
+ # If the value is a symbol it gets passed through i18n
172
+ # inside the +alchemy.default_ingredient_texts+ scope
173
+ def default_value
174
+ default = definition[:default]
175
+ case default
176
+ when Symbol
177
+ Alchemy.t(default, scope: :default_ingredient_texts)
178
+ else
179
+ default
180
+ end
181
+ end
182
+ end
183
+ end