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,184 @@
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
+ # The value of an ingredient of the element by role
26
+ def value_for(role)
27
+ ingredient_by_role(role)&.value
28
+ end
29
+
30
+ # Find first ingredient from element by given role.
31
+ def ingredient_by_role(role)
32
+ ingredients.detect { |ingredient| ingredient.role == role.to_s }
33
+ end
34
+
35
+ # Find first ingredient from element by given type.
36
+ def ingredient_by_type(type)
37
+ ingredients_by_type(type).first
38
+ end
39
+
40
+ # All ingredients from element by given type.
41
+ def ingredients_by_type(type)
42
+ ingredients.select do |ingredient|
43
+ ingredient.type == Ingredient.normalize_type(type)
44
+ end
45
+ end
46
+
47
+ # Copy current ingredient's ingredients to given target element
48
+ def copy_ingredients_to(element)
49
+ ingredients.map do |ingredient|
50
+ Ingredient.copy(ingredient, element_id: element.id)
51
+ end
52
+ end
53
+
54
+ # Returns all element ingredient definitions from the +elements.yml+ file
55
+ def ingredient_definitions
56
+ definition.fetch(:ingredients, [])
57
+ end
58
+
59
+ # Returns the definition for given ingredient role
60
+ def ingredient_definition_for(role)
61
+ if ingredient_definitions.blank?
62
+ log_warning "Element #{name} is missing the ingredient definition for #{role}"
63
+ nil
64
+ else
65
+ ingredient_definitions.find { |d| d[:role] == role.to_s }
66
+ end
67
+ end
68
+
69
+ # Returns an array of all Richtext ingredients ids from elements
70
+ #
71
+ # This is used to re-initialize the TinyMCE editor in the element editor.
72
+ #
73
+ def richtext_ingredients_ids
74
+ ids = ingredients.select(&:has_tinymce?).collect(&:id)
75
+ expanded_nested_elements = nested_elements.expanded
76
+ if expanded_nested_elements.present?
77
+ ids += expanded_nested_elements.collect(&:richtext_ingredients_ids)
78
+ end
79
+ ids.flatten
80
+ end
81
+
82
+ # Has any of the ingredients validations defined?
83
+ def has_validations?
84
+ ingredients.any?(&:has_validations?)
85
+ end
86
+
87
+ # All element ingredients where the validation has failed.
88
+ def ingredients_with_errors
89
+ ingredients.select { |i| i.errors.any? }
90
+ end
91
+
92
+ # True if the element has a ingredient for given name
93
+ # that has a non blank value.
94
+ def has_value_for?(role)
95
+ value_for(role).present?
96
+ end
97
+
98
+ # Ingredient validation error messages
99
+ #
100
+ # == Error messages are translated via I18n
101
+ #
102
+ # Inside your translation file add translations like:
103
+ #
104
+ # alchemy:
105
+ # ingredient_validations:
106
+ # name_of_the_element:
107
+ # role_of_the_ingredient:
108
+ # validation_error_type: Error Message
109
+ #
110
+ # NOTE: +validation_error_type+ has to be one of:
111
+ #
112
+ # * blank
113
+ # * taken
114
+ # * invalid
115
+ #
116
+ # === Example:
117
+ #
118
+ # de:
119
+ # alchemy:
120
+ # ingredient_validations:
121
+ # contactform:
122
+ # email:
123
+ # invalid: 'Die Email hat nicht das richtige Format'
124
+ #
125
+ #
126
+ # == Error message translation fallbacks
127
+ #
128
+ # In order to not translate every single ingredient for every element
129
+ # you can provide default error messages per content name:
130
+ #
131
+ # === Example
132
+ #
133
+ # en:
134
+ # alchemy:
135
+ # ingredient_validations:
136
+ # fields:
137
+ # email:
138
+ # invalid: E-Mail has wrong format
139
+ # blank: E-Mail can't be blank
140
+ #
141
+ # And even further you can provide general field agnostic error messages:
142
+ #
143
+ # === Example
144
+ #
145
+ # en:
146
+ # alchemy:
147
+ # ingredient_validations:
148
+ # errors:
149
+ # invalid: %{field} has wrong format
150
+ # blank: %{field} can't be blank
151
+ #
152
+ def ingredient_error_messages
153
+ messages = []
154
+ ingredients_with_errors.map { |i| [i.role, i.errors.details] }.each do |role, error_details|
155
+ error_details[:value].each do |error_detail|
156
+ error = error_detail[:error]
157
+ messages << Alchemy.t(
158
+ "#{name}.#{role}.#{error}",
159
+ scope: "ingredient_validations",
160
+ default: [
161
+ "fields.#{role}.#{error}".to_sym,
162
+ "errors.#{error}".to_sym,
163
+ ],
164
+ field: Alchemy::Ingredient.translated_label_for(role, name),
165
+ )
166
+ end
167
+ end
168
+ messages
169
+ end
170
+
171
+ private
172
+
173
+ # Builds ingredients for this element as described in the +elements.yml+
174
+ def build_ingredients
175
+ ingredient_definitions.each do |attributes|
176
+ ingredients.build(
177
+ role: attributes[:role],
178
+ type: Alchemy::Ingredient.normalize_type(attributes[:type]),
179
+ )
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -1,108 +1,127 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- # Methods used for presenting an Alchemy Element.
5
- #
6
- module Element::Presenters
7
- extend ActiveSupport::Concern
4
+ class Element < BaseRecord
5
+ # Methods used for presenting an Alchemy Element.
6
+ #
7
+ module Presenters
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ # Human name for displaying elements in select boxes and element editor views.
12
+ #
13
+ # The name is beeing translated from given name value as described in +config/alchemy/elements.yml+
14
+ #
15
+ # Translate the name in your +config/locales+ language file.
16
+ #
17
+ # == Example:
18
+ #
19
+ # de:
20
+ # alchemy:
21
+ # element_names:
22
+ # contactform: 'Kontakt Formular'
23
+ #
24
+ # If no translation is found a humanized name is used.
25
+ #
26
+ def display_name_for(name)
27
+ Alchemy.t(name, scope: "element_names", default: name.to_s.humanize)
28
+ end
29
+ end
8
30
 
9
- module ClassMethods
10
- # Human name for displaying elements in select boxes and element editor views.
31
+ # Returns the translated name
11
32
  #
12
- # The name is beeing translated from given name value as described in +config/alchemy/elements.yml+
33
+ # @see Alchemy::Element::Presenters#display_name_for
13
34
  #
14
- # Translate the name in your +config/locales+ language file.
35
+ def display_name
36
+ self.class.display_name_for(definition["name"] || name)
37
+ end
38
+
39
+ # Returns a preview text for element.
15
40
  #
16
- # == Example:
41
+ # It's taken from the first Content found in the +elements.yml+ definition file.
17
42
  #
18
- # de:
19
- # alchemy:
20
- # element_names:
21
- # contactform: 'Kontakt Formular'
43
+ # You can flag a Content as +as_element_title+ to take this as preview.
22
44
  #
23
- # If no translation is found a humanized name is used.
45
+ # @param maxlength [Fixnum] (60)
46
+ # Length of characters after the text will be cut off.
24
47
  #
25
- def display_name_for(name)
26
- Alchemy.t(name, scope: "element_names", default: name.to_s.humanize)
48
+ def preview_text(maxlength = 60)
49
+ preview_text_from_preview_ingredient(maxlength) ||
50
+ preview_text_from_preview_content(maxlength) ||
51
+ preview_text_from_nested_elements(maxlength)
27
52
  end
28
- end
29
53
 
30
- # Returns the translated name
31
- #
32
- # @see Alchemy::Element::Presenters#display_name_for
33
- #
34
- def display_name
35
- self.class.display_name_for(definition["name"] || name)
36
- end
54
+ # Generates a preview text containing Element#display_name and Element#preview_text.
55
+ #
56
+ # It is displayed inside the head of the Element in the Elements.list overlay window from the Alchemy Admin::Page#edit view.
57
+ #
58
+ # === Example
59
+ #
60
+ # A Element described as:
61
+ #
62
+ # - name: funky_element
63
+ # display_name: Funky Element
64
+ # contents:
65
+ # - name: headline
66
+ # type: EssenceText
67
+ # - name: text
68
+ # type EssenceRichtext
69
+ # as_element_title: true
70
+ #
71
+ # With "I want to tell you a funky story" as stripped_body for the EssenceRichtext Content produces:
72
+ #
73
+ # Funky Element: I want to tell ...
74
+ #
75
+ # @param maxlength [Fixnum] (30)
76
+ # Length of characters after the text will be cut off.
77
+ #
78
+ def display_name_with_preview_text(maxlength = 30)
79
+ "#{display_name}: #{preview_text(maxlength)}"
80
+ end
37
81
 
38
- # Returns a preview text for element.
39
- #
40
- # It's taken from the first Content found in the +elements.yml+ definition file.
41
- #
42
- # You can flag a Content as +as_element_title+ to take this as preview.
43
- #
44
- # @param maxlength [Fixnum] (60)
45
- # Length of characters after the text will be cut off.
46
- #
47
- def preview_text(maxlength = 60)
48
- preview_text_from_preview_content(maxlength) || preview_text_from_nested_elements(maxlength)
49
- end
82
+ # Returns a dom id used for elements html id tag.
83
+ #
84
+ def dom_id
85
+ "#{name}_#{id}"
86
+ end
50
87
 
51
- # Generates a preview text containing Element#display_name and Element#preview_text.
52
- #
53
- # It is displayed inside the head of the Element in the Elements.list overlay window from the Alchemy Admin::Page#edit view.
54
- #
55
- # === Example
56
- #
57
- # A Element described as:
58
- #
59
- # - name: funky_element
60
- # display_name: Funky Element
61
- # contents:
62
- # - name: headline
63
- # type: EssenceText
64
- # - name: text
65
- # type EssenceRichtext
66
- # as_element_title: true
67
- #
68
- # With "I want to tell you a funky story" as stripped_body for the EssenceRichtext Content produces:
69
- #
70
- # Funky Element: I want to tell ...
71
- #
72
- # @param maxlength [Fixnum] (30)
73
- # Length of characters after the text will be cut off.
74
- #
75
- def display_name_with_preview_text(maxlength = 30)
76
- "#{display_name}: #{preview_text(maxlength)}"
77
- end
88
+ # The content that's used for element's preview text.
89
+ #
90
+ # It tries to find one of element's contents that is defined +as_element_title+.
91
+ # Takes element's first content if no content is defined +as_element_title+.
92
+ #
93
+ # @return (Alchemy::Content)
94
+ #
95
+ def preview_content
96
+ @_preview_content ||= contents.detect(&:preview_content?) || contents.first
97
+ end
78
98
 
79
- # Returns a dom id used for elements html id tag.
80
- #
81
- def dom_id
82
- "#{name}_#{id}"
83
- end
99
+ # The ingredient that's used for element's preview text.
100
+ #
101
+ # It tries to find one of element's ingredients that is defined +as_element_title+.
102
+ # Takes element's first ingredient if no ingredient is defined +as_element_title+.
103
+ #
104
+ # @return (Alchemy::Ingredient)
105
+ #
106
+ def preview_ingredient
107
+ @_preview_ingredient ||= ingredients.detect(&:preview_ingredient?) || ingredients.first
108
+ end
84
109
 
85
- # The content that's used for element's preview text.
86
- #
87
- # It tries to find one of element's contents that is defined +as_element_title+.
88
- # Takes element's first content if no content is defined +as_element_title+.
89
- #
90
- # @return (Alchemy::Content)
91
- #
92
- def preview_content
93
- @_preview_content ||= contents.detect(&:preview_content?) || contents.first
94
- end
110
+ private
95
111
 
96
- private
112
+ def preview_text_from_nested_elements(maxlength)
113
+ return if all_nested_elements.empty?
97
114
 
98
- def preview_text_from_nested_elements(maxlength)
99
- return if all_nested_elements.empty?
115
+ all_nested_elements.first.preview_text(maxlength)
116
+ end
100
117
 
101
- all_nested_elements.first.preview_text(maxlength)
102
- end
118
+ def preview_text_from_preview_content(maxlength)
119
+ preview_content.try!(:preview_text, maxlength)
120
+ end
103
121
 
104
- def preview_text_from_preview_content(maxlength)
105
- preview_content.try!(:preview_text, maxlength)
122
+ def preview_text_from_preview_ingredient(maxlength)
123
+ preview_ingredient&.preview_text(maxlength)
124
+ end
106
125
  end
107
126
  end
108
127
  end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ # Mimics ActiveRecord query interface
5
+ # but does this on the preloaded elements
6
+ class ElementsRepository
7
+ include Enumerable
8
+
9
+ # An empty set of elements
10
+ def self.none
11
+ new([])
12
+ end
13
+
14
+ # @param [ActiveRecord::Relation]
15
+ def initialize(elements)
16
+ @elements = elements.to_a
17
+ end
18
+
19
+ # All visible elements
20
+ # @return [Alchemy::ElementRepository]
21
+ def visible
22
+ self.class.new select(&:public)
23
+ end
24
+
25
+ # All not fixed elements
26
+ # @return [Alchemy::ElementRepository]
27
+ def hidden
28
+ self.class.new reject(&:public)
29
+ end
30
+
31
+ # All elements with given name(s)
32
+ # @param [Array<String|Symbol>|String|Symbol]
33
+ # @return [Alchemy::ElementRepository]
34
+ def named(*names)
35
+ names.flatten!
36
+ self.class.new(select { |e| e.name.in?(names.map!(&:to_s)) })
37
+ end
38
+
39
+ # Filter elements by given attribute and value
40
+ # @param [Array|Hash]
41
+ # @return [Alchemy::ElementRepository]
42
+ def where(attrs)
43
+ self.class.new(
44
+ select do |element|
45
+ attrs.all? do |attr, value|
46
+ element.public_send(attr) == value
47
+ end
48
+ end
49
+ )
50
+ end
51
+
52
+ # All elements excluding those wth given name(s)
53
+ # @param [Array<String|Symbol>|String|Symbol]
54
+ # @return [Alchemy::ElementRepository]
55
+ def excluded(*names)
56
+ names.flatten!
57
+ self.class.new(reject { |e| e.name.in?(names.map!(&:to_s)) })
58
+ end
59
+
60
+ # All fixed elements
61
+ # @return [Alchemy::ElementRepository]
62
+ def fixed
63
+ self.class.new select(&:fixed)
64
+ end
65
+
66
+ # All not fixed elements
67
+ # @return [Alchemy::ElementRepository]
68
+ def unfixed
69
+ self.class.new reject(&:fixed)
70
+ end
71
+
72
+ # All folded elements
73
+ # @return [Alchemy::ElementRepository]
74
+ def folded
75
+ self.class.new select(&:folded)
76
+ end
77
+
78
+ # All expanded elements
79
+ # @return [Alchemy::ElementRepository]
80
+ def expanded
81
+ self.class.new reject(&:folded)
82
+ end
83
+
84
+ # All not nested top level elements
85
+ # @return [Alchemy::ElementRepository]
86
+ def not_nested
87
+ self.class.new(select { |e| e.parent_element_id.nil? })
88
+ end
89
+
90
+ # Elements in reversed order
91
+ # @return [Alchemy::ElementRepository]
92
+ def reverse
93
+ self.class.new elements.reverse
94
+ end
95
+
96
+ # Elements in random order
97
+ # @return [Alchemy::ElementRepository]
98
+ def random
99
+ self.class.new Array(elements).shuffle
100
+ end
101
+
102
+ # Elements off setted by
103
+ # @return [Alchemy::ElementRepository]
104
+ def offset(offset)
105
+ self.class.new elements[offset.to_i..-1]
106
+ end
107
+
108
+ # Elements limitted by
109
+ # @return [Alchemy::ElementRepository]
110
+ def limit(limit)
111
+ self.class.new elements[0..(limit.to_i - 1)]
112
+ end
113
+
114
+ def children_of(parent)
115
+ self.class.new(select { |e| e.parent_element_id == parent.id })
116
+ end
117
+
118
+ def each(&blk)
119
+ elements.each(&blk)
120
+ end
121
+
122
+ private
123
+
124
+ attr_reader :elements
125
+ end
126
+ end