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
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ module CropAction
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before_action :load_croppable_resource, only: [:crop]
10
+ end
11
+
12
+ def crop
13
+ @picture = Alchemy::Picture.find_by(id: params[:picture_id])
14
+ if @picture
15
+ @croppable_resource.picture = @picture
16
+ @settings = @croppable_resource.image_cropper_settings
17
+ @element = @croppable_resource.element
18
+ else
19
+ @no_image_notice = Alchemy.t(:no_image_for_cropper_found)
20
+ end
21
+
22
+ render template: "alchemy/admin/crop"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -19,6 +19,23 @@ module Alchemy
19
19
  end
20
20
  end
21
21
 
22
+ # Returns ingredient editor instances for defined ingredients
23
+ #
24
+ # Creates ingredient on demand if the ingredient is not yet present on the element
25
+ #
26
+ # @return Array<Alchemy::IngredientEditor>
27
+ def ingredients
28
+ element.definition.fetch(:ingredients, []).map do |ingredient|
29
+ Alchemy::IngredientEditor.new(find_or_create_ingredient(ingredient[:role]))
30
+ end
31
+ end
32
+
33
+ # Are any ingredients defined?
34
+ # @return [Boolean]
35
+ def has_ingredients_defined?
36
+ element.definition.fetch(:ingredients, []).any?
37
+ end
38
+
22
39
  # CSS classes for the element editor partial.
23
40
  def css_classes
24
41
  [
@@ -38,7 +55,7 @@ module Alchemy
38
55
  def editable?
39
56
  return false if folded?
40
57
 
41
- content_definitions.present? || taggable?
58
+ content_definitions.present? || ingredient_definitions.any? || taggable?
42
59
  end
43
60
 
44
61
  # Fixes Rails partial renderer calling to_model on the object
@@ -103,5 +120,10 @@ module Alchemy
103
120
  def create_content(name)
104
121
  Alchemy::Content.create(element: element, name: name)
105
122
  end
123
+
124
+ def find_or_create_ingredient(role)
125
+ element.ingredients.find { |i| i.role == role } ||
126
+ Ingredient.create(element: element, role: role)
127
+ end
106
128
  end
107
129
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class IngredientEditor < SimpleDelegator
5
+ alias_method :ingredient, :__getobj__
6
+
7
+ def to_partial_path
8
+ "alchemy/ingredients/#{partial_name}_editor"
9
+ end
10
+
11
+ # Returns the translated role for displaying in labels
12
+ #
13
+ # Translate it in your locale yml file:
14
+ #
15
+ # alchemy:
16
+ # ingredient_roles:
17
+ # foo: Bar
18
+ #
19
+ # Optionally you can scope your ingredient role to an element:
20
+ #
21
+ # alchemy:
22
+ # ingredient_roles:
23
+ # article:
24
+ # foo: Baz
25
+ #
26
+ def translated_role
27
+ Alchemy.t(
28
+ role,
29
+ scope: "ingredient_roles.#{element.name}",
30
+ default: Alchemy.t("ingredient_roles.#{role}", default: role.humanize),
31
+ )
32
+ end
33
+
34
+ def css_classes
35
+ [
36
+ "ingredient-editor",
37
+ partial_name,
38
+ deprecated? ? "deprecated" : nil,
39
+ ].compact
40
+ end
41
+
42
+ def data_attributes
43
+ {
44
+ ingredient_id: id,
45
+ ingredient_role: role,
46
+ }
47
+ end
48
+
49
+ # Returns a string to be passed to Rails form field tags to ensure it can be used with Rails' nested attributes.
50
+ #
51
+ # === Example:
52
+ #
53
+ # <%= text_field_tag text_editor.form_field_name, text_editor.value %>
54
+ #
55
+ # === Options:
56
+ #
57
+ # You can pass an Ingredient column_name. Default is 'value'
58
+ #
59
+ # ==== Example:
60
+ #
61
+ # <%= text_field_tag text_editor.form_field_name(:link), text_editor.value %>
62
+ #
63
+ def form_field_name(column = "value")
64
+ "element[ingredients_attributes][#{form_field_counter}][#{column}]"
65
+ end
66
+
67
+ def form_field_id(column = "value")
68
+ "element_ingredients_attributes_#{form_field_counter}_#{column}"
69
+ end
70
+
71
+ # Fixes Rails partial renderer calling to_model on the object
72
+ # which reveals the delegated ingredient instead of this decorator.
73
+ def respond_to?(method_name)
74
+ return false if method_name == :to_model
75
+
76
+ super
77
+ end
78
+
79
+ def has_warnings?
80
+ definition.blank? || deprecated?
81
+ end
82
+
83
+ def linked?
84
+ link.try(:present?)
85
+ end
86
+
87
+ def warnings
88
+ return unless has_warnings?
89
+
90
+ if definition.blank?
91
+ Logger.warn("ingredient #{role} is missing its definition", caller(1..1))
92
+ Alchemy.t(:ingredient_definition_missing)
93
+ else
94
+ deprecation_notice
95
+ end
96
+ end
97
+
98
+ # Returns a deprecation notice for ingredients marked deprecated
99
+ #
100
+ # You can either use localizations or pass a String as notice
101
+ # in the ingredient definition.
102
+ #
103
+ # == Custom deprecation notices
104
+ #
105
+ # Use general ingredient deprecation notice
106
+ #
107
+ # - name: element_name
108
+ # ingredients:
109
+ # - role: old_ingredient
110
+ # type: Text
111
+ # deprecated: true
112
+ #
113
+ # Add a translation to your locale file for a per ingredient notice.
114
+ #
115
+ # en:
116
+ # alchemy:
117
+ # ingredient_deprecation_notices:
118
+ # element_name:
119
+ # old_ingredient: Foo baz widget is deprecated
120
+ #
121
+ # or use the global translation that apply to all deprecated ingredients.
122
+ #
123
+ # en:
124
+ # alchemy:
125
+ # ingredient_deprecation_notice: Foo baz widget is deprecated
126
+ #
127
+ # or pass string as deprecation notice.
128
+ #
129
+ # - name: element_name
130
+ # ingredients:
131
+ # - role: old_ingredient
132
+ # type: Text
133
+ # deprecated: This ingredient will be removed soon.
134
+ #
135
+ def deprecation_notice
136
+ case definition[:deprecated]
137
+ when String
138
+ definition[:deprecated]
139
+ when TrueClass
140
+ Alchemy.t(
141
+ role,
142
+ scope: [:ingredient_deprecation_notices, element.name],
143
+ default: Alchemy.t(:ingredient_deprecated),
144
+ )
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ def form_field_counter
151
+ element.definition.fetch(:ingredients, []).index { |i| i[:role] == role }
152
+ end
153
+ end
154
+ end
@@ -3,6 +3,7 @@
3
3
  module Alchemy
4
4
  module Admin
5
5
  module ElementsHelper
6
+ include Alchemy::Admin::IngredientsHelper
6
7
  include Alchemy::Admin::ContentsHelper
7
8
  include Alchemy::Admin::EssencesHelper
8
9
 
@@ -14,7 +14,7 @@ module Alchemy
14
14
  essence.thumbnail_url,
15
15
  alt: picture.name,
16
16
  class: "img_paddingtop",
17
- title: Alchemy.t(:image_name) + ": #{picture.name}",
17
+ title: Alchemy.t(:image_name, name: picture.name),
18
18
  )
19
19
  end
20
20
 
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ module IngredientsHelper
6
+ include Alchemy::Admin::BaseHelper
7
+
8
+ # Renders the translated role of ingredient.
9
+ #
10
+ # Displays a warning icon if ingredient is missing its definition.
11
+ #
12
+ # Displays a mandatory field indicator, if the ingredient has validations.
13
+ #
14
+ def render_ingredient_role(ingredient)
15
+ if ingredient.blank?
16
+ warning("Ingredient is nil")
17
+ return
18
+ end
19
+
20
+ content = ingredient.translated_role
21
+
22
+ if ingredient.has_warnings?
23
+ icon = hint_with_tooltip(ingredient.warnings)
24
+ content = "#{icon} #{content}".html_safe
25
+ end
26
+
27
+ if ingredient.has_validations?
28
+ "#{content}<span class='validation_indicator'>*</span>".html_safe
29
+ else
30
+ content
31
+ end
32
+ end
33
+
34
+ # Renders the label and hint for a ingredient.
35
+ def ingredient_label(ingredient, column = :value)
36
+ label_tag ingredient.form_field_id(column) do
37
+ [render_ingredient_role(ingredient), render_hint_for(ingredient)].compact.join("&nbsp;").html_safe
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -26,12 +26,13 @@ module Alchemy
26
26
  class ElementViewHelper < BlockHelper
27
27
  # Renders one of the element's contents.
28
28
  #
29
+ # If the element uses +ingredients+ it renders the ingredient record.
30
+ #
29
31
  def render(name, options = {}, html_options = {})
30
- content = element.content_by_name(name)
31
- return if content.nil?
32
+ renderable = element.ingredient_by_role(name) || content(name)
33
+ return if renderable.nil?
32
34
 
33
- helpers.render(content, {
34
- content: content,
35
+ helpers.render(renderable, {
35
36
  options: options,
36
37
  html_options: html_options,
37
38
  })
@@ -43,16 +44,28 @@ module Alchemy
43
44
  element.content_by_name(name)
44
45
  end
45
46
 
47
+ deprecate content: "Use `ingredient_by_role` instead", deprecator: Alchemy::Deprecation
48
+
46
49
  # Returns the ingredient of one of the element's contents.
47
50
  #
51
+ # If the element uses +ingredients+ it returns the +value+ of the ingredient record.
52
+ #
48
53
  def ingredient(name)
49
- element.ingredient(name)
54
+ element.ingredient(name).presence || element.ingredient_by_role(name)&.value
55
+ end
56
+
57
+ deprecate ingredient: :value, deprecator: Alchemy::Deprecation
58
+
59
+ # Returns the value of one of the element's ingredients.
60
+ #
61
+ def value(name)
62
+ element.ingredient_by_role(name)&.value
50
63
  end
51
64
 
52
- # Returns true if the given content has been filled by the user.
65
+ # Returns true if the given content or ingredient has been filled by the user.
53
66
  #
54
67
  def has?(name)
55
- element.has_ingredient?(name)
68
+ element.has_ingredient?(name) || element.has_value_for?(name)
56
69
  end
57
70
 
58
71
  # Return's the given content's essence.
@@ -60,6 +73,8 @@ module Alchemy
60
73
  def essence(name)
61
74
  content(name).try(:essence)
62
75
  end
76
+
77
+ deprecate essence: "Use `ingredient_by_role` instead", deprecator: Alchemy::Deprecation
63
78
  end
64
79
 
65
80
  # Block-level helper for element views. Constructs a DOM element wrapping
@@ -25,7 +25,7 @@ module Alchemy
25
25
  # === Render elements from global page:
26
26
  #
27
27
  # <footer>
28
- # <%= render_elements from_page: 'footer' %>
28
+ # <%= render_elements from_page: Alchemy::Page.find_by(page_layout: 'footer') %>
29
29
  # </footer>
30
30
  #
31
31
  # === Custom elements finder:
@@ -50,8 +50,8 @@ module Alchemy
50
50
  # <%= render_elements finder: MyCustomNewsArchive.new %>
51
51
  # </div>
52
52
  #
53
- # @option options [Alchemy::Page|String] :from_page (@page)
54
- # The page the elements are rendered from. You can pass a page_layout String or a {Alchemy::Page} object.
53
+ # @option options [Alchemy::Page] :from_page (@page)
54
+ # The page the elements are rendered from.
55
55
  # @option options [Array<String>|String] :only
56
56
  # A list of element names only to be rendered.
57
57
  # @option options [Array<String>|String] :except
@@ -77,7 +77,14 @@ module Alchemy
77
77
  }.update(options)
78
78
 
79
79
  finder = options[:finder] || Alchemy::ElementsFinder.new(options)
80
- elements = finder.elements(page: options[:from_page])
80
+
81
+ page_version = if @preview_mode
82
+ options[:from_page]&.draft_version
83
+ else
84
+ options[:from_page]&.public_version
85
+ end
86
+
87
+ elements = finder.elements(page_version: page_version)
81
88
 
82
89
  buff = []
83
90
  elements.each_with_index do |element, i|
@@ -133,7 +140,7 @@ module Alchemy
133
140
  def render_element(element, options = {}, counter = 1)
134
141
  if element.nil?
135
142
  warning("Element is nil")
136
- render "alchemy/elements/view_not_found", {name: "nil"}
143
+ render "alchemy/elements/view_not_found", { name: "nil" }
137
144
  return
138
145
  end
139
146
 
@@ -66,8 +66,8 @@ module Alchemy
66
66
  #
67
67
  # renders +app/views/alchemy/site_layouts/_default_site.html.erb+ for the site named "Default Site".
68
68
  #
69
- def render_site_layout
70
- render current_alchemy_site
69
+ def render_site_layout(&block)
70
+ render current_alchemy_site, &block
71
71
  rescue ActionView::MissingTemplate
72
72
  warning("Site layout for #{current_alchemy_site.try(:name)} not found. Please run `rails g alchemy:site_layouts`")
73
73
  ""
@@ -98,14 +98,6 @@ module Alchemy
98
98
  WARN
99
99
  end
100
100
 
101
- # Returns true if page is in the active branch
102
- def page_active?(page)
103
- Alchemy::Deprecation.warn("page_active? helper is deprecated and will be removed from Alchemy 6.0")
104
-
105
- @_page_ancestors ||= @page.self_and_ancestors.contentpages
106
- @_page_ancestors.include?(page)
107
- end
108
-
109
101
  # Returns page links in a breadcrumb beginning from root to current page.
110
102
  #
111
103
  # === Options:
@@ -127,7 +119,7 @@ module Alchemy
127
119
 
128
120
  pages = options[:page].
129
121
  self_and_ancestors.contentpages.
130
- accessible_by(current_ability, :see)
122
+ published
131
123
 
132
124
  if options.delete(:restricted_only)
133
125
  pages = pages.restricted
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class BaseJob < ActiveJob::Base
5
+ # Automatically retry jobs that encountered a deadlock
6
+ # retry_on ActiveRecord::Deadlocked
7
+
8
+ # Most jobs are safe to ignore if the underlying records are no longer available
9
+ # discard_on ActiveJob::DeserializationError
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class PublishPageJob < BaseJob
5
+ queue_as :default
6
+
7
+ def perform(page, public_on:)
8
+ Alchemy::Page::Publisher.new(page).publish!(public_on: public_on)
9
+ end
10
+ end
11
+ end
@@ -28,7 +28,7 @@ module Alchemy
28
28
  after_assign { |f| write_attribute(:file_mime_type, f.mime_type) }
29
29
  end
30
30
 
31
- stampable stamper_class_name: Alchemy.user_class.name
31
+ stampable stamper_class_name: Alchemy.user_class_name
32
32
 
33
33
  has_many :essence_files, class_name: "Alchemy::EssenceFile", foreign_key: "attachment_id"
34
34
  has_many :contents, through: :essence_files
@@ -7,7 +7,7 @@ module Alchemy
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  module ClassMethods
10
- SKIPPED_ATTRIBUTES_ON_COPY = %w(position created_at updated_at creator_id updater_id id)
10
+ SKIPPED_ATTRIBUTES_ON_COPY = %w(position created_at updated_at creator_id updater_id element_id id)
11
11
 
12
12
  # Builds a new content as descriped in the elements.yml file.
13
13
  #
@@ -19,14 +19,14 @@ module Alchemy
19
19
  return super if attributes.empty? || element.nil?
20
20
 
21
21
  definition = element.content_definition_for(attributes[:name])
22
- if definition.blank?
22
+ if definition.blank? && attributes[:essence_type].nil?
23
23
  raise ContentDefinitionError, "No definition found in elements.yml for #{attributes.inspect} and #{element.inspect}"
24
24
  end
25
25
 
26
26
  super(
27
- name: definition[:name],
28
- essence_type: normalize_essence_type(definition[:type]),
29
- element_id: element.id
27
+ name: attributes[:name],
28
+ essence_type: attributes[:essence_type] || normalize_essence_type(definition[:type]),
29
+ element: element
30
30
  ).tap(&:build_essence)
31
31
  end
32
32
 
@@ -53,18 +53,16 @@ module Alchemy
53
53
  # @copy.element_id # => 3
54
54
  #
55
55
  def copy(source, differences = {})
56
- new_content = Content.new(
57
- source.attributes.
56
+ Content.new(
57
+ source.attributes.with_indifferent_access.
58
58
  except(*SKIPPED_ATTRIBUTES_ON_COPY).
59
- merge(differences.with_indifferent_access),
60
- )
61
- new_essence = source.essence.class.create!(
62
- source.essence.attributes.
63
- except(*SKIPPED_ATTRIBUTES_ON_COPY),
64
- )
65
- new_content.tap do |content|
66
- content.essence = new_essence
67
- content.save
59
+ merge(differences.with_indifferent_access)
60
+ ).tap do |new_content|
61
+ new_content.build_essence(
62
+ source.essence.attributes.
63
+ except(*SKIPPED_ATTRIBUTES_ON_COPY)
64
+ )
65
+ new_content.save
68
66
  end
69
67
  end
70
68
 
@@ -117,29 +115,27 @@ module Alchemy
117
115
  #
118
116
  # If an optional type is passed, this type of essence gets created.
119
117
  #
120
- def build_essence(type = essence_type)
121
- self.essence = essence_class(type).new({
122
- ingredient: default_value,
123
- })
118
+ def build_essence(attributes = {})
119
+ self.essence = essence_class.new(
120
+ { content: self, ingredient: default_value }.merge(attributes)
121
+ )
124
122
  end
125
123
 
126
124
  # Creates essence from definition.
127
125
  #
128
126
  # If an optional type is passed, this type of essence gets created.
129
127
  #
130
- def create_essence!(type = nil)
131
- build_essence(type).save!
128
+ def create_essence!(attrs = {})
129
+ build_essence(attrs).save!
132
130
  save!
133
131
  end
134
132
 
135
133
  private
136
134
 
137
- # Returns a class constant from definition's type field.
135
+ # Returns a class constant from definition's type field or the essence_type column
138
136
  #
139
- # If an optional type is passed, this type of essence gets constantized.
140
- #
141
- def essence_class(type = nil)
142
- Content.normalize_essence_type(type || definition["type"]).constantize
137
+ def essence_class
138
+ (essence_type || Content.normalize_essence_type(definition["type"])).constantize
143
139
  end
144
140
  end
145
141
  end
@@ -39,19 +39,14 @@ module Alchemy
39
39
  scope :essence_selects, -> { where(essence_type: "Alchemy::EssenceSelect") }
40
40
  scope :essence_texts, -> { where(essence_type: "Alchemy::EssenceText") }
41
41
  scope :named, ->(name) { where(name: name) }
42
- scope :available, -> { published.not_trashed }
42
+ scope :available, -> { published }
43
43
  scope :published, -> { joins(:element).merge(Element.published) }
44
- scope :not_trashed, -> { joins(:element).merge(Element.not_trashed) }
45
44
  scope :not_restricted, -> { joins(:element).merge(Element.not_restricted) }
46
45
 
47
46
  delegate :restricted?, to: :page, allow_nil: true
48
- delegate :trashed?, to: :element, allow_nil: true
49
- deprecate :trashed?, deprecator: Alchemy::Deprecation
50
47
  delegate :public?, to: :element, allow_nil: true
51
48
 
52
49
  class << self
53
- deprecate :not_trashed, deprecator: Alchemy::Deprecation
54
-
55
50
  # Returns the translated label for a content name.
56
51
  #
57
52
  # Translate it in your locale yml file:
@@ -1,37 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- # Module concerning element definitions
5
- #
6
- module Element::Definitions
7
- extend ActiveSupport::Concern
4
+ class Element < BaseRecord
5
+ # Module concerning element definitions
6
+ #
7
+ module Definitions
8
+ extend ActiveSupport::Concern
8
9
 
9
- module ClassMethods
10
- # Returns the definitions from elements.yml file.
11
- #
12
- # Place a +elements.yml+ file inside your apps +config/alchemy+ folder to define
13
- # your own set of elements
14
- #
15
- def definitions
16
- ElementDefinition.all
17
- end
10
+ module ClassMethods
11
+ # Returns the definitions from elements.yml file.
12
+ #
13
+ # Place a +elements.yml+ file inside your apps +config/alchemy+ folder to define
14
+ # your own set of elements
15
+ #
16
+ def definitions
17
+ ElementDefinition.all
18
+ end
18
19
 
19
- # Returns one element definition by given name.
20
- #
21
- def definition_by_name(name)
22
- ElementDefinition.get(name)
20
+ # Returns one element definition by given name.
21
+ #
22
+ def definition_by_name(name)
23
+ ElementDefinition.get(name)
24
+ end
23
25
  end
24
- end
25
26
 
26
- # The definition of this element.
27
- #
28
- def definition
29
- if definition = self.class.definition_by_name(name)
30
- definition
31
- else
32
- log_warning "Could not find element definition for #{name}. " \
33
- "Please check your elements.yml file!"
34
- {}
27
+ # The definition of this element.
28
+ #
29
+ def definition
30
+ if definition = self.class.definition_by_name(name)
31
+ definition
32
+ else
33
+ log_warning "Could not find element definition for #{name}. " \
34
+ "Please check your elements.yml file!"
35
+ {}
36
+ end
35
37
  end
36
38
  end
37
39
  end