alchemy_cms 5.2.4 → 6.0.0.b1

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 (269) 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 +80 -25
  7. data/Gemfile +4 -2
  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/admin.scss +1 -1
  23. data/app/assets/stylesheets/alchemy/archive.scss +4 -4
  24. data/app/assets/stylesheets/alchemy/buttons.scss +0 -4
  25. data/app/assets/stylesheets/alchemy/elements.scss +73 -61
  26. data/app/assets/stylesheets/alchemy/images.scss +8 -0
  27. data/app/assets/stylesheets/alchemy/node-select.scss +4 -3
  28. data/app/assets/stylesheets/alchemy/page-select.scss +1 -0
  29. data/app/assets/stylesheets/tinymce/skins/alchemy/skin.min.css.scss +6 -6
  30. data/app/controllers/alchemy/admin/attachments_controller.rb +6 -2
  31. data/app/controllers/alchemy/admin/base_controller.rb +5 -7
  32. data/app/controllers/alchemy/admin/elements_controller.rb +58 -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 +6 -13
  40. data/app/controllers/alchemy/admin/pictures_controller.rb +35 -9
  41. data/app/controllers/alchemy/api/elements_controller.rb +10 -5
  42. data/app/controllers/alchemy/api/pages_controller.rb +2 -4
  43. data/app/controllers/concerns/alchemy/admin/archive_overlay.rb +13 -3
  44. data/app/controllers/concerns/alchemy/admin/crop_action.rb +26 -0
  45. data/app/decorators/alchemy/element_editor.rb +23 -1
  46. data/app/decorators/alchemy/ingredient_editor.rb +154 -0
  47. data/app/helpers/alchemy/admin/elements_helper.rb +1 -0
  48. data/app/helpers/alchemy/admin/essences_helper.rb +1 -1
  49. data/app/helpers/alchemy/admin/ingredients_helper.rb +42 -0
  50. data/app/helpers/alchemy/elements_block_helper.rb +22 -7
  51. data/app/helpers/alchemy/elements_helper.rb +12 -5
  52. data/app/helpers/alchemy/pages_helper.rb +3 -11
  53. data/app/jobs/alchemy/base_job.rb +11 -0
  54. data/app/jobs/alchemy/publish_page_job.rb +11 -0
  55. data/app/models/alchemy/attachment.rb +1 -1
  56. data/app/models/alchemy/content/factory.rb +23 -27
  57. data/app/models/alchemy/content.rb +1 -6
  58. data/app/models/alchemy/element/definitions.rb +29 -27
  59. data/app/models/alchemy/element/element_contents.rb +131 -122
  60. data/app/models/alchemy/element/element_essences.rb +100 -98
  61. data/app/models/alchemy/element/element_ingredients.rb +176 -0
  62. data/app/models/alchemy/element/presenters.rb +89 -87
  63. data/app/models/alchemy/element.rb +40 -73
  64. data/app/models/alchemy/elements_repository.rb +126 -0
  65. data/app/models/alchemy/essence_audio.rb +12 -0
  66. data/app/models/alchemy/essence_headline.rb +40 -0
  67. data/app/models/alchemy/essence_picture.rb +4 -116
  68. data/app/models/alchemy/essence_richtext.rb +12 -0
  69. data/app/models/alchemy/essence_video.rb +12 -0
  70. data/app/models/alchemy/image_cropper_settings.rb +87 -0
  71. data/app/models/alchemy/ingredient.rb +219 -0
  72. data/app/models/alchemy/ingredient_validator.rb +97 -0
  73. data/app/models/alchemy/ingredients/audio.rb +29 -0
  74. data/app/models/alchemy/ingredients/boolean.rb +21 -0
  75. data/app/models/alchemy/ingredients/datetime.rb +20 -0
  76. data/app/models/alchemy/ingredients/file.rb +30 -0
  77. data/app/models/alchemy/ingredients/headline.rb +42 -0
  78. data/app/models/alchemy/ingredients/html.rb +19 -0
  79. data/app/models/alchemy/ingredients/link.rb +16 -0
  80. data/app/models/alchemy/ingredients/node.rb +23 -0
  81. data/app/models/alchemy/ingredients/page.rb +23 -0
  82. data/app/models/alchemy/ingredients/picture.rb +41 -0
  83. data/app/models/alchemy/ingredients/richtext.rb +57 -0
  84. data/app/models/alchemy/ingredients/select.rb +10 -0
  85. data/app/models/alchemy/ingredients/text.rb +17 -0
  86. data/app/models/alchemy/ingredients/video.rb +33 -0
  87. data/app/models/alchemy/language.rb +0 -11
  88. data/app/models/alchemy/node.rb +1 -1
  89. data/app/models/alchemy/page/fixed_attributes.rb +53 -51
  90. data/app/models/alchemy/page/page_elements.rb +186 -205
  91. data/app/models/alchemy/page/page_naming.rb +66 -64
  92. data/app/models/alchemy/page/page_natures.rb +139 -142
  93. data/app/models/alchemy/page/page_scopes.rb +113 -102
  94. data/app/models/alchemy/page/publisher.rb +50 -0
  95. data/app/models/alchemy/page/url_path.rb +1 -1
  96. data/app/models/alchemy/page.rb +67 -33
  97. data/app/models/alchemy/page_version.rb +58 -0
  98. data/app/models/alchemy/picture/calculations.rb +2 -8
  99. data/app/models/alchemy/picture/preprocessor.rb +2 -0
  100. data/app/models/alchemy/picture/transformations.rb +24 -96
  101. data/app/models/alchemy/picture.rb +4 -2
  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 +1 -2
  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/crop.html.erb +36 -0
  113. data/app/views/alchemy/admin/elements/_element.html.erb +14 -10
  114. data/app/views/alchemy/admin/elements/{_element_footer.html.erb → _footer.html.erb} +0 -0
  115. data/app/views/alchemy/admin/elements/{_new_element_form.html.erb → _form.html.erb} +1 -1
  116. data/app/views/alchemy/admin/elements/{_element_header.html.erb → _header.html.erb} +1 -1
  117. data/app/views/alchemy/admin/elements/{_element_toolbar.html.erb → _toolbar.html.erb} +5 -6
  118. data/app/views/alchemy/admin/elements/{trash.js.erb → destroy.js.erb} +1 -3
  119. data/app/views/alchemy/admin/elements/new.html.erb +3 -3
  120. data/app/views/alchemy/admin/elements/order.js.erb +0 -17
  121. data/app/views/alchemy/admin/elements/update.js.erb +3 -2
  122. data/app/views/alchemy/admin/essence_audios/edit.html.erb +7 -0
  123. data/app/views/alchemy/admin/essence_pictures/update.js.erb +0 -1
  124. data/app/views/alchemy/admin/essence_videos/edit.html.erb +11 -0
  125. data/app/views/alchemy/admin/ingredients/_audio_fields.html.erb +4 -0
  126. data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +18 -0
  127. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +25 -0
  128. data/app/views/alchemy/admin/ingredients/_video_fields.html.erb +8 -0
  129. data/app/views/alchemy/admin/ingredients/edit.html.erb +4 -0
  130. data/app/views/alchemy/admin/layoutpages/edit.html.erb +0 -5
  131. data/app/views/alchemy/admin/nodes/_node.html.erb +2 -2
  132. data/app/views/alchemy/admin/pages/_anchor_link.html.erb +1 -1
  133. data/app/views/alchemy/admin/pages/_external_link.html.erb +1 -1
  134. data/app/views/alchemy/admin/pages/_file_link.html.erb +1 -1
  135. data/app/views/alchemy/admin/pages/_form.html.erb +0 -6
  136. data/app/views/alchemy/admin/pages/_internal_link.html.erb +1 -1
  137. data/app/views/alchemy/admin/pages/_tinymce_custom_config.html.erb +5 -2
  138. data/app/views/alchemy/admin/pages/edit.html.erb +36 -24
  139. data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +2 -4
  140. data/app/views/alchemy/admin/partials/_routes.html.erb +7 -11
  141. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +4 -8
  142. data/app/views/alchemy/admin/pictures/_infos.html.erb +0 -1
  143. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +4 -4
  144. data/app/views/alchemy/admin/pictures/assign.js.erb +10 -0
  145. data/app/views/alchemy/admin/resources/_form.html.erb +1 -0
  146. data/app/views/alchemy/essences/_essence_audio_editor.html.erb +4 -0
  147. data/app/views/alchemy/essences/_essence_audio_view.html.erb +15 -0
  148. data/app/views/alchemy/essences/_essence_file_editor.html.erb +15 -6
  149. data/app/views/alchemy/essences/_essence_headline_editor.html.erb +36 -0
  150. data/app/views/alchemy/essences/_essence_headline_view.html.erb +10 -0
  151. data/app/views/alchemy/essences/_essence_link_editor.html.erb +8 -4
  152. data/app/views/alchemy/essences/_essence_picture_editor.html.erb +27 -12
  153. data/app/views/alchemy/essences/_essence_text_editor.html.erb +12 -4
  154. data/app/views/alchemy/essences/_essence_video_editor.html.erb +4 -0
  155. data/app/views/alchemy/essences/_essence_video_view.html.erb +18 -0
  156. data/app/views/alchemy/essences/shared/_essence_picture_tools.html.erb +21 -16
  157. data/app/views/alchemy/essences/shared/_linkable_essence_tools.html.erb +2 -2
  158. data/app/views/alchemy/ingredients/_audio_editor.html.erb +5 -0
  159. data/app/views/alchemy/ingredients/_audio_view.html.erb +14 -0
  160. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +11 -0
  161. data/app/views/alchemy/ingredients/_boolean_view.html.erb +1 -0
  162. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +17 -0
  163. data/app/views/alchemy/ingredients/_datetime_view.html.erb +9 -0
  164. data/app/views/alchemy/ingredients/_file_editor.html.erb +50 -0
  165. data/app/views/alchemy/ingredients/_file_view.html.erb +17 -0
  166. data/app/views/alchemy/ingredients/_headline_editor.html.erb +30 -0
  167. data/app/views/alchemy/ingredients/_headline_view.html.erb +9 -0
  168. data/app/views/alchemy/ingredients/_html_editor.html.erb +8 -0
  169. data/app/views/alchemy/ingredients/_html_view.html.erb +1 -0
  170. data/app/views/alchemy/ingredients/_link_editor.html.erb +24 -0
  171. data/app/views/alchemy/ingredients/_link_view.html.erb +9 -0
  172. data/app/views/alchemy/ingredients/_node_editor.html.erb +25 -0
  173. data/app/views/alchemy/ingredients/_node_view.html.erb +1 -0
  174. data/app/views/alchemy/ingredients/_page_editor.html.erb +24 -0
  175. data/app/views/alchemy/ingredients/_page_view.html.erb +4 -0
  176. data/app/views/alchemy/ingredients/_picture_editor.html.erb +59 -0
  177. data/app/views/alchemy/ingredients/_picture_view.html.erb +5 -0
  178. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +12 -0
  179. data/app/views/alchemy/ingredients/_richtext_view.html.erb +3 -0
  180. data/app/views/alchemy/ingredients/_select_editor.html.erb +29 -0
  181. data/app/views/alchemy/ingredients/_select_view.html.erb +1 -0
  182. data/app/views/alchemy/ingredients/_text_editor.html.erb +19 -0
  183. data/app/views/alchemy/ingredients/_text_view.html.erb +16 -0
  184. data/app/views/alchemy/ingredients/_video_editor.html.erb +5 -0
  185. data/app/views/alchemy/ingredients/_video_view.html.erb +17 -0
  186. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +20 -0
  187. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +57 -0
  188. data/config/brakeman.ignore +66 -159
  189. data/config/initializers/dragonfly.rb +10 -0
  190. data/config/locales/alchemy.en.yml +23 -15
  191. data/config/routes.rb +17 -22
  192. data/db/migrate/20201207131309_create_page_versions.rb +19 -0
  193. data/db/migrate/20201207135820_add_page_version_id_to_alchemy_elements.rb +76 -0
  194. data/db/migrate/20210205143548_rename_public_on_and_public_until_on_alchemy_pages.rb +10 -0
  195. data/db/migrate/20210326105046_add_sanitized_body_to_alchemy_essence_richtexts.rb +7 -0
  196. data/db/migrate/20210406093436_add_alchemy_essence_headlines.rb +12 -0
  197. data/db/migrate/20210506135919_create_essence_audios.rb +19 -0
  198. data/db/migrate/20210506140258_create_essence_videos.rb +23 -0
  199. data/db/migrate/20210508091432_create_alchemy_ingredients.rb +22 -0
  200. data/lib/alchemy/admin/preview_url.rb +2 -0
  201. data/lib/alchemy/deprecation.rb +1 -1
  202. data/lib/alchemy/dragonfly/processors/auto_orient.rb +18 -0
  203. data/lib/alchemy/dragonfly/processors/crop_resize.rb +35 -0
  204. data/lib/alchemy/elements_finder.rb +14 -60
  205. data/lib/alchemy/engine.rb +1 -1
  206. data/lib/alchemy/essence.rb +1 -2
  207. data/lib/alchemy/hints.rb +8 -4
  208. data/lib/alchemy/page_layout.rb +0 -13
  209. data/lib/alchemy/permissions.rb +30 -29
  210. data/lib/alchemy/resource.rb +13 -3
  211. data/lib/alchemy/tasks/tidy.rb +29 -0
  212. data/lib/alchemy/test_support/essence_shared_examples.rb +0 -1
  213. data/lib/alchemy/test_support/factories/element_factory.rb +8 -8
  214. data/lib/alchemy/test_support/factories/essence_audio_factory.rb +7 -0
  215. data/lib/alchemy/test_support/factories/essence_video_factory.rb +7 -0
  216. data/lib/alchemy/test_support/factories/ingredient_factory.rb +25 -0
  217. data/lib/alchemy/test_support/factories/page_factory.rb +20 -1
  218. data/lib/alchemy/test_support/factories/page_version_factory.rb +23 -0
  219. data/lib/alchemy/test_support/having_crop_action_examples.rb +170 -0
  220. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +646 -0
  221. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +21 -0
  222. data/lib/alchemy/test_support/shared_ingredient_examples.rb +57 -0
  223. data/lib/alchemy/test_support.rb +2 -11
  224. data/lib/alchemy/tinymce.rb +17 -0
  225. data/lib/alchemy/upgrader/five_point_zero.rb +0 -32
  226. data/lib/alchemy/upgrader/six_point_zero.rb +21 -0
  227. data/lib/alchemy/upgrader/tasks/add_page_versions.rb +33 -0
  228. data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +51 -0
  229. data/lib/alchemy/version.rb +1 -1
  230. data/lib/generators/alchemy/elements/elements_generator.rb +1 -0
  231. data/lib/generators/alchemy/elements/templates/view.html.erb +9 -0
  232. data/lib/generators/alchemy/elements/templates/view.html.haml +9 -0
  233. data/lib/generators/alchemy/elements/templates/view.html.slim +9 -0
  234. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +38 -0
  235. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +14 -0
  236. data/lib/generators/alchemy/ingredient/templates/model.rb.tt +13 -0
  237. data/lib/generators/alchemy/ingredient/templates/view.html.erb +1 -0
  238. data/lib/generators/alchemy/install/install_generator.rb +1 -2
  239. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +1 -1
  240. data/lib/generators/alchemy/menus/templates/node.html.erb +1 -1
  241. data/lib/generators/alchemy/menus/templates/node.html.haml +1 -1
  242. data/lib/generators/alchemy/menus/templates/node.html.slim +1 -1
  243. data/lib/generators/alchemy/menus/templates/wrapper.html.erb +1 -1
  244. data/lib/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
  245. data/lib/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
  246. data/lib/tasks/alchemy/tidy.rake +12 -0
  247. data/lib/tasks/alchemy/upgrade.rake +21 -15
  248. data/package/admin.js +9 -1
  249. data/package/src/file_editors.js +28 -0
  250. data/package/src/image_cropper.js +103 -0
  251. data/package/src/image_loader.js +58 -0
  252. data/package/src/node_tree.js +5 -5
  253. data/package/src/picture_editors.js +169 -0
  254. data/package/src/utils/__tests__/ajax.spec.js +20 -12
  255. data/package/src/utils/ajax.js +8 -3
  256. data/package.json +3 -2
  257. data/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +3 -18
  258. data/vendor/assets/stylesheets/jquery.Jcrop.min.scss +2 -28
  259. metadata +285 -56
  260. data/app/assets/javascripts/alchemy/alchemy.image_cropper.js.coffee +0 -44
  261. data/app/assets/javascripts/alchemy/alchemy.trash_window.js.coffee +0 -30
  262. data/app/assets/stylesheets/alchemy/trash.scss +0 -8
  263. data/app/controllers/alchemy/admin/trash_controller.rb +0 -44
  264. data/app/views/alchemy/admin/essence_files/assign.js.erb +0 -3
  265. data/app/views/alchemy/admin/essence_pictures/assign.js.erb +0 -4
  266. data/app/views/alchemy/admin/essence_pictures/crop.html.erb +0 -48
  267. data/app/views/alchemy/admin/trash/clear.js.erb +0 -4
  268. data/app/views/alchemy/admin/trash/index.html.erb +0 -31
  269. data/lib/alchemy/test_support/factories.rb +0 -20
@@ -1,151 +1,160 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Methods concerning contents for elements
4
- #
5
3
  module Alchemy
6
- module Element::ElementContents
7
- # Find first content from element by given name.
8
- def content_by_name(name)
9
- contents_by_name(name).first
10
- end
4
+ class Element < BaseRecord
5
+ # Methods concerning contents for elements
6
+ #
7
+ module ElementContents
8
+ # Find first content from element by given name.
9
+ def content_by_name(name)
10
+ contents_by_name(name).first
11
+ end
11
12
 
12
- # Find first content from element by given essence type.
13
- def content_by_type(essence_type)
14
- contents_by_type(essence_type).first
15
- end
13
+ # Find first content from element by given essence type.
14
+ def content_by_type(essence_type)
15
+ contents_by_type(essence_type).first
16
+ end
16
17
 
17
- # All contents from element by given name.
18
- def contents_by_name(name)
19
- contents.select { |content| content.name == name.to_s }
20
- end
21
- alias_method :all_contents_by_name, :contents_by_name
18
+ # All contents from element by given name.
19
+ def contents_by_name(name)
20
+ contents.select { |content| content.name == name.to_s }
21
+ end
22
+
23
+ alias_method :all_contents_by_name, :contents_by_name
22
24
 
23
- # All contents from element by given essence type.
24
- def contents_by_type(essence_type)
25
- contents.select do |content|
26
- content.essence_type == Content.normalize_essence_type(essence_type)
25
+ # All contents from element by given essence type.
26
+ def contents_by_type(essence_type)
27
+ contents.select do |content|
28
+ content.essence_type == Content.normalize_essence_type(essence_type)
29
+ end
27
30
  end
28
- end
29
- alias_method :all_contents_by_type, :contents_by_type
30
31
 
31
- # Updates all related contents by calling +update_essence+ on each of them.
32
- #
33
- # @param contents_attributes [Hash]
34
- # Hash of contents attributes.
35
- # The keys has to be the #id of the content to update.
36
- # The values a Hash of attribute names and values
37
- #
38
- # @return [Boolean]
39
- # True if +errors+ are blank or +contents_attributes+ hash is nil
40
- #
41
- # == Example
42
- #
43
- # @element.update_contents(
44
- # "1" => {ingredient: "Title"},
45
- # "2" => {link: "https://google.com"}
46
- # )
47
- #
48
- def update_contents(contents_attributes)
49
- return true if contents_attributes.nil?
32
+ alias_method :all_contents_by_type, :contents_by_type
33
+
34
+ # Updates all related contents by calling +update_essence+ on each of them.
35
+ #
36
+ # @param contents_attributes [Hash]
37
+ # Hash of contents attributes.
38
+ # The keys has to be the #id of the content to update.
39
+ # The values a Hash of attribute names and values
40
+ #
41
+ # @return [Boolean]
42
+ # True if +errors+ are blank or +contents_attributes+ hash is nil
43
+ #
44
+ # == Example
45
+ #
46
+ # @element.update_contents(
47
+ # "1" => {ingredient: "Title"},
48
+ # "2" => {link: "https://google.com"}
49
+ # )
50
+ #
51
+ def update_contents(contents_attributes)
52
+ return true if contents_attributes.nil?
53
+
54
+ contents.each do |content|
55
+ content_hash = contents_attributes[content.id.to_s] || next
56
+ content.update_essence(content_hash) || errors.add(:base, :essence_validation_failed)
57
+ end
58
+ errors.blank?
59
+ end
50
60
 
51
- contents.each do |content|
52
- content_hash = contents_attributes[content.id.to_s] || next
53
- content.update_essence(content_hash) || errors.add(:base, :essence_validation_failed)
61
+ # Copy current content's contents to given target element
62
+ def copy_contents_to(element)
63
+ contents.map do |content|
64
+ Content.copy(content, element_id: element.id)
65
+ end
54
66
  end
55
- errors.blank?
56
- end
57
67
 
58
- # Copy current content's contents to given target element
59
- def copy_contents_to(element)
60
- contents.map do |content|
61
- Content.copy(content, element_id: element.id)
68
+ # Returns the content that is marked as rss title.
69
+ #
70
+ # Mark a content as rss title in your +elements.yml+ file:
71
+ #
72
+ # - name: news
73
+ # contents:
74
+ # - name: headline
75
+ # type: EssenceText
76
+ # rss_title: true
77
+ #
78
+ def content_for_rss_title
79
+ content_for_rss_meta("title")
62
80
  end
63
- end
64
81
 
65
- # Returns the content that is marked as rss title.
66
- #
67
- # Mark a content as rss title in your +elements.yml+ file:
68
- #
69
- # - name: news
70
- # contents:
71
- # - name: headline
72
- # type: EssenceText
73
- # rss_title: true
74
- #
75
- def content_for_rss_title
76
- content_for_rss_meta("title")
77
- end
82
+ # Returns the content that is marked as rss description.
83
+ #
84
+ # Mark a content as rss description in your +elements.yml+ file:
85
+ #
86
+ # - name: news
87
+ # contents:
88
+ # - name: body
89
+ # type: EssenceRichtext
90
+ # rss_description: true
91
+ #
92
+ def content_for_rss_description
93
+ content_for_rss_meta("description")
94
+ end
78
95
 
79
- # Returns the content that is marked as rss description.
80
- #
81
- # Mark a content as rss description in your +elements.yml+ file:
82
- #
83
- # - name: news
84
- # contents:
85
- # - name: body
86
- # type: EssenceRichtext
87
- # rss_description: true
88
- #
89
- def content_for_rss_description
90
- content_for_rss_meta("description")
91
- end
96
+ # Returns the array with the hashes for all element contents in the elements.yml file
97
+ def content_definitions
98
+ return nil if definition.blank?
92
99
 
93
- # Returns the array with the hashes for all element contents in the elements.yml file
94
- def content_definitions
95
- return nil if definition.blank?
100
+ definition["contents"]
101
+ end
96
102
 
97
- definition["contents"]
98
- end
103
+ # Returns the definition for given content_name
104
+ def content_definition_for(content_name)
105
+ if content_definitions.blank?
106
+ log_warning "Element #{name} is missing the content definition for #{content_name}"
107
+ nil
108
+ else
109
+ content_definitions.detect { |d| d["name"] == content_name.to_s }
110
+ end
111
+ end
99
112
 
100
- # Returns the definition for given content_name
101
- def content_definition_for(content_name)
102
- if content_definitions.blank?
103
- log_warning "Element #{name} is missing the content definition for #{content_name}"
104
- nil
105
- else
106
- content_definitions.detect { |d| d["name"] == content_name.to_s }
113
+ # Returns an array of all EssenceRichtext contents ids from elements
114
+ #
115
+ # This is used to re-initialize the TinyMCE editor in the element editor.
116
+ #
117
+ def richtext_contents_ids
118
+ # This is not very efficient SQL wise I know, but we need to iterate
119
+ # recursivly through all descendent elements and I don't know how to do this
120
+ # in pure SQL. Anyone with a better idea is welcome to submit a patch.
121
+ ids = contents.select(&:has_tinymce?).collect(&:id)
122
+ expanded_nested_elements = nested_elements.expanded
123
+ if expanded_nested_elements.present?
124
+ ids += expanded_nested_elements.collect(&:richtext_contents_ids)
125
+ end
126
+ ids.flatten
107
127
  end
108
- end
109
128
 
110
- # Returns an array of all EssenceRichtext contents ids from elements
111
- #
112
- # This is used to re-initialize the TinyMCE editor in the element editor.
113
- #
114
- def richtext_contents_ids
115
- # This is not very efficient SQL wise I know, but we need to iterate
116
- # recursivly through all descendent elements and I don't know how to do this
117
- # in pure SQL. Anyone with a better idea is welcome to submit a patch.
118
- ids = contents.select(&:has_tinymce?).collect(&:id)
119
- expanded_nested_elements = nested_elements.expanded
120
- if expanded_nested_elements.present?
121
- ids += expanded_nested_elements.collect(&:richtext_contents_ids)
129
+ # True, if any of the element's contents has essence validations defined.
130
+ def has_validations?
131
+ !contents.detect(&:has_validations?).blank?
122
132
  end
123
- ids.flatten
124
- end
125
133
 
126
- # True, if any of the element's contents has essence validations defined.
127
- def has_validations?
128
- !contents.detect(&:has_validations?).blank?
129
- end
134
+ # All element contents where the essence validation has failed.
135
+ def contents_with_errors
136
+ contents.select(&:essence_validation_failed?)
137
+ end
130
138
 
131
- # All element contents where the essence validation has failed.
132
- def contents_with_errors
133
- contents.select(&:essence_validation_failed?)
134
- end
139
+ private
135
140
 
136
- private
141
+ def content_for_rss_meta(type)
142
+ definition = content_definitions.detect { |c| c["rss_#{type}"] }
143
+ return if definition.blank?
137
144
 
138
- def content_for_rss_meta(type)
139
- definition = content_definitions.detect { |c| c["rss_#{type}"] }
140
- return if definition.blank?
145
+ contents.detect { |content| content.name == definition["name"] }
146
+ end
141
147
 
142
- contents.detect { |content| content.name == definition["name"] }
143
- end
148
+ # creates the contents for this element as described in the elements.yml
149
+ #
150
+ # If ingredients are defined as well no contents get created,
151
+ # ingredients get created instead.
152
+ def create_contents
153
+ return if definition.fetch(:ingredients, []).any?
144
154
 
145
- # creates the contents for this element as described in the elements.yml
146
- def create_contents
147
- definition.fetch("contents", []).each do |attributes|
148
- Content.create(attributes.merge(element: self))
155
+ definition.fetch("contents", []).each do |attributes|
156
+ Content.create(attributes.merge(element: self))
157
+ end
149
158
  end
150
159
  end
151
160
  end
@@ -1,112 +1,114 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- module Element::ElementEssences
5
- # Returns the contents essence value (aka. ingredient) for passed content name.
6
- def ingredient(name)
7
- content = content_by_name(name)
8
- return nil if content.blank?
4
+ class Element < BaseRecord
5
+ module ElementEssences
6
+ # Returns the contents essence value (aka. ingredient) for passed content name.
7
+ def ingredient(name)
8
+ content = content_by_name(name)
9
+ return nil if content.blank?
9
10
 
10
- content.ingredient
11
- end
11
+ content.ingredient
12
+ end
12
13
 
13
- # True if the element has a content for given name,
14
- # that has an essence value (aka. ingredient) that is not blank.
15
- def has_ingredient?(name)
16
- ingredient(name).present?
17
- end
14
+ # True if the element has a content for given name,
15
+ # that has an essence value (aka. ingredient) that is not blank.
16
+ def has_ingredient?(name)
17
+ ingredient(name).present?
18
+ end
18
19
 
19
- # Returns all essence errors in the format of:
20
- #
21
- # {
22
- # content.name => [
23
- # error_message_for_validation_1,
24
- # error_message_for_validation_2
25
- # ]
26
- # }
27
- #
28
- # Get translated error messages with +Element#essence_error_messages+
29
- #
30
- def essence_errors
31
- essence_errors = {}
32
- contents.each do |content|
33
- if content.essence_validation_failed?
34
- essence_errors[content.name] = content.essence.validation_errors
20
+ # Returns all essence errors in the format of:
21
+ #
22
+ # {
23
+ # content.name => [
24
+ # error_message_for_validation_1,
25
+ # error_message_for_validation_2
26
+ # ]
27
+ # }
28
+ #
29
+ # Get translated error messages with +Element#essence_error_messages+
30
+ #
31
+ def essence_errors
32
+ essence_errors = {}
33
+ contents.each do |content|
34
+ if content.essence_validation_failed?
35
+ essence_errors[content.name] = content.essence.validation_errors
36
+ end
35
37
  end
38
+ essence_errors
36
39
  end
37
- essence_errors
38
- end
39
40
 
40
- # Essence validation errors
41
- #
42
- # == Error messages are translated via I18n
43
- #
44
- # Inside your translation file add translations like:
45
- #
46
- # alchemy:
47
- # content_validations:
48
- # name_of_the_element:
49
- # name_of_the_content:
50
- # validation_error_type: Error Message
51
- #
52
- # NOTE: +validation_error_type+ has to be one of:
53
- #
54
- # * blank
55
- # * taken
56
- # * invalid
57
- #
58
- # === Example:
59
- #
60
- # de:
61
- # alchemy:
62
- # content_validations:
63
- # contactform:
64
- # email:
65
- # invalid: 'Die Email hat nicht das richtige Format'
66
- #
67
- #
68
- # == Error message translation fallbacks
69
- #
70
- # In order to not translate every single content for every element
71
- # you can provide default error messages per content name:
72
- #
73
- # === Example
74
- #
75
- # en:
76
- # alchemy:
77
- # content_validations:
78
- # fields:
79
- # email:
80
- # invalid: E-Mail has wrong format
81
- # blank: E-Mail can't be blank
82
- #
83
- # And even further you can provide general field agnostic error messages:
84
- #
85
- # === Example
86
- #
87
- # en:
88
- # alchemy:
89
- # content_validations:
90
- # errors:
91
- # invalid: %{field} has wrong format
92
- # blank: %{field} can't be blank
93
- #
94
- def essence_error_messages
95
- messages = []
96
- essence_errors.each do |content_name, errors|
97
- errors.each do |error|
98
- messages << Alchemy.t(
99
- "#{name}.#{content_name}.#{error}",
100
- scope: "content_validations",
101
- default: [
102
- "fields.#{content_name}.#{error}".to_sym,
103
- "errors.#{error}".to_sym,
104
- ],
105
- field: Content.translated_label_for(content_name, name),
106
- )
41
+ # Essence validation errors
42
+ #
43
+ # == Error messages are translated via I18n
44
+ #
45
+ # Inside your translation file add translations like:
46
+ #
47
+ # alchemy:
48
+ # content_validations:
49
+ # name_of_the_element:
50
+ # name_of_the_content:
51
+ # validation_error_type: Error Message
52
+ #
53
+ # NOTE: +validation_error_type+ has to be one of:
54
+ #
55
+ # * blank
56
+ # * taken
57
+ # * invalid
58
+ #
59
+ # === Example:
60
+ #
61
+ # de:
62
+ # alchemy:
63
+ # content_validations:
64
+ # contactform:
65
+ # email:
66
+ # invalid: 'Die Email hat nicht das richtige Format'
67
+ #
68
+ #
69
+ # == Error message translation fallbacks
70
+ #
71
+ # In order to not translate every single content for every element
72
+ # you can provide default error messages per content name:
73
+ #
74
+ # === Example
75
+ #
76
+ # en:
77
+ # alchemy:
78
+ # content_validations:
79
+ # fields:
80
+ # email:
81
+ # invalid: E-Mail has wrong format
82
+ # blank: E-Mail can't be blank
83
+ #
84
+ # And even further you can provide general field agnostic error messages:
85
+ #
86
+ # === Example
87
+ #
88
+ # en:
89
+ # alchemy:
90
+ # content_validations:
91
+ # errors:
92
+ # invalid: %{field} has wrong format
93
+ # blank: %{field} can't be blank
94
+ #
95
+ def essence_error_messages
96
+ messages = []
97
+ essence_errors.each do |content_name, errors|
98
+ errors.each do |error|
99
+ messages << Alchemy.t(
100
+ "#{name}.#{content_name}.#{error}",
101
+ scope: "content_validations",
102
+ default: [
103
+ "fields.#{content_name}.#{error}".to_sym,
104
+ "errors.#{error}".to_sym,
105
+ ],
106
+ field: Content.translated_label_for(content_name, name),
107
+ )
108
+ end
107
109
  end
110
+ messages
108
111
  end
109
- messages
110
112
  end
111
113
  end
112
114
  end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class Element < BaseRecord
5
+ # Methods concerning ingredients for elements
6
+ #
7
+ module ElementIngredients
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ attr_accessor :autogenerate_ingredients
12
+
13
+ has_many :ingredients,
14
+ class_name: "Alchemy::Ingredient",
15
+ inverse_of: :element,
16
+ dependent: :destroy
17
+
18
+ before_create :build_ingredients,
19
+ unless: -> { autogenerate_ingredients == false }
20
+
21
+ accepts_nested_attributes_for :ingredients
22
+ validates_associated :ingredients, on: :update
23
+ end
24
+
25
+ # Find first ingredient from element by given role.
26
+ def ingredient_by_role(role)
27
+ ingredients.detect { |ingredient| ingredient.role == role.to_s }
28
+ end
29
+
30
+ # Find first ingredient from element by given type.
31
+ def ingredient_by_type(type)
32
+ ingredients_by_type(type).first
33
+ end
34
+
35
+ # All ingredients from element by given type.
36
+ def ingredients_by_type(type)
37
+ ingredients.select do |ingredient|
38
+ ingredient.type == Ingredient.normalize_type(type)
39
+ end
40
+ end
41
+
42
+ # Copy current ingredient's ingredients to given target element
43
+ def copy_ingredients_to(element)
44
+ ingredients.map do |ingredient|
45
+ Ingredient.copy(ingredient, element_id: element.id)
46
+ end
47
+ end
48
+
49
+ # Returns all element ingredient definitions from the +elements.yml+ file
50
+ def ingredient_definitions
51
+ definition.fetch(:ingredients, [])
52
+ end
53
+
54
+ # Returns the definition for given ingredient role
55
+ def ingredient_definition_for(role)
56
+ if ingredient_definitions.blank?
57
+ log_warning "Element #{name} is missing the ingredient definition for #{role}"
58
+ nil
59
+ else
60
+ ingredient_definitions.find { |d| d[:role] == role.to_s }
61
+ end
62
+ end
63
+
64
+ # Returns an array of all Richtext ingredients ids from elements
65
+ #
66
+ # This is used to re-initialize the TinyMCE editor in the element editor.
67
+ #
68
+ def richtext_ingredients_ids
69
+ ids = ingredients.select(&:has_tinymce?).collect(&:id)
70
+ expanded_nested_elements = nested_elements.expanded
71
+ if expanded_nested_elements.present?
72
+ ids += expanded_nested_elements.collect(&:richtext_ingredients_ids)
73
+ end
74
+ ids.flatten
75
+ end
76
+
77
+ # Has any of the ingredients validations defined?
78
+ def has_validations?
79
+ ingredients.any?(&:has_validations?)
80
+ end
81
+
82
+ # All element ingredients where the validation has failed.
83
+ def ingredients_with_errors
84
+ ingredients.select { |i| i.errors.any? }
85
+ end
86
+
87
+ # True if the element has a ingredient for given name
88
+ # that has a non blank value.
89
+ def has_value_for?(role)
90
+ ingredient_by_role(role)&.value.present?
91
+ end
92
+
93
+ # Ingredient validation error messages
94
+ #
95
+ # == Error messages are translated via I18n
96
+ #
97
+ # Inside your translation file add translations like:
98
+ #
99
+ # alchemy:
100
+ # ingredient_validations:
101
+ # name_of_the_element:
102
+ # role_of_the_ingredient:
103
+ # validation_error_type: Error Message
104
+ #
105
+ # NOTE: +validation_error_type+ has to be one of:
106
+ #
107
+ # * blank
108
+ # * taken
109
+ # * invalid
110
+ #
111
+ # === Example:
112
+ #
113
+ # de:
114
+ # alchemy:
115
+ # ingredient_validations:
116
+ # contactform:
117
+ # email:
118
+ # invalid: 'Die Email hat nicht das richtige Format'
119
+ #
120
+ #
121
+ # == Error message translation fallbacks
122
+ #
123
+ # In order to not translate every single ingredient for every element
124
+ # you can provide default error messages per content name:
125
+ #
126
+ # === Example
127
+ #
128
+ # en:
129
+ # alchemy:
130
+ # ingredient_validations:
131
+ # fields:
132
+ # email:
133
+ # invalid: E-Mail has wrong format
134
+ # blank: E-Mail can't be blank
135
+ #
136
+ # And even further you can provide general field agnostic error messages:
137
+ #
138
+ # === Example
139
+ #
140
+ # en:
141
+ # alchemy:
142
+ # ingredient_validations:
143
+ # errors:
144
+ # invalid: %{field} has wrong format
145
+ # blank: %{field} can't be blank
146
+ #
147
+ def ingredient_error_messages
148
+ messages = []
149
+ ingredients_with_errors.map { |i| [i.role, i.errors.details] }.each do |role, error_details|
150
+ error_details[:value].each do |error_detail|
151
+ error = error_detail[:error]
152
+ messages << Alchemy.t(
153
+ "#{name}.#{role}.#{error}",
154
+ scope: "ingredient_validations",
155
+ default: [
156
+ "fields.#{role}.#{error}".to_sym,
157
+ "errors.#{error}".to_sym,
158
+ ],
159
+ field: Alchemy::Ingredient.translated_label_for(role, name),
160
+ )
161
+ end
162
+ end
163
+ messages
164
+ end
165
+
166
+ private
167
+
168
+ # Builds ingredients for this element as described in the +elements.yml+
169
+ def build_ingredients
170
+ self.ingredients = ingredient_definitions.map do |attributes|
171
+ Ingredient.build(role: attributes[:role], element: self)
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end