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,171 +1,168 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- module Page::PageNatures
5
- extend ActiveSupport::Concern
6
-
7
- def public?
8
- current_time = Time.current
9
- already_public_for?(current_time) && still_public_for?(current_time)
10
- end
11
-
12
- def expiration_time
13
- public_until? ? public_until - Time.current : nil
14
- end
15
-
16
- def taggable?
17
- definition["taggable"] == true
18
- end
4
+ class Page < BaseRecord
5
+ module PageNatures
6
+ extend ActiveSupport::Concern
7
+
8
+ # Determines if this page has a public version and this version is public.
9
+ #
10
+ # @see PageVersion#public?
11
+ # @returns Boolean
12
+ def public?
13
+ language.public? && !!public_version&.public?
14
+ end
19
15
 
20
- deprecate :taggable?, deprecator: Alchemy::Deprecation
16
+ def expiration_time
17
+ public_until ? public_until - Time.current : nil
18
+ end
21
19
 
22
- def rootpage?
23
- !new_record? && parent_id.blank?
24
- end
20
+ def taggable?
21
+ definition["taggable"] == true
22
+ end
25
23
 
26
- def folded?(user_id)
27
- return unless Alchemy.user_class < ActiveRecord::Base
24
+ deprecate :taggable?, deprecator: Alchemy::Deprecation
28
25
 
29
- folded_pages.where(user_id: user_id, folded: true).any?
30
- end
26
+ def rootpage?
27
+ !new_record? && parent_id.blank?
28
+ end
31
29
 
32
- def contains_feed?
33
- definition["feed"]
34
- end
30
+ def folded?(user_id)
31
+ return unless Alchemy.user_class < ActiveRecord::Base
35
32
 
36
- # Returns an Array of Alchemy roles which are able to edit this template
37
- #
38
- # # config/alchemy/page_layouts.yml
39
- # - name: contact
40
- # editable_by:
41
- # - freelancer
42
- # - admin
43
- #
44
- # @returns Array
45
- #
46
- def has_limited_editors?
47
- definition["editable_by"].present?
48
- end
33
+ folded_pages.where(user_id: user_id, folded: true).any?
34
+ end
49
35
 
50
- def editor_roles
51
- return unless has_limited_editors?
36
+ def contains_feed?
37
+ definition["feed"]
38
+ end
52
39
 
53
- definition["editable_by"]
54
- end
40
+ # Returns an Array of Alchemy roles which are able to edit this template
41
+ #
42
+ # # config/alchemy/page_layouts.yml
43
+ # - name: contact
44
+ # editable_by:
45
+ # - freelancer
46
+ # - admin
47
+ #
48
+ # @returns Array
49
+ #
50
+ def has_limited_editors?
51
+ definition["editable_by"].present?
52
+ end
55
53
 
56
- # True if page locked_at timestamp and locked_by id are set
57
- def locked?
58
- locked_by? && locked_at?
59
- end
54
+ def editor_roles
55
+ return unless has_limited_editors?
60
56
 
61
- # Returns a Hash describing the status of the Page.
62
- #
63
- def status
64
- {
65
- public: public?,
66
- locked: locked?,
67
- restricted: restricted?,
68
- }
69
- end
57
+ definition["editable_by"]
58
+ end
70
59
 
71
- # Returns the translated status for given status type.
72
- #
73
- # @param [Symbol] status_type
74
- #
75
- def status_title(status_type)
76
- Alchemy.t(status[status_type].to_s, scope: "page_states.#{status_type}")
77
- end
60
+ # True if page locked_at timestamp and locked_by id are set
61
+ def locked?
62
+ locked_by? && locked_at?
63
+ end
78
64
 
79
- # Returns the self#page_layout definition from config/alchemy/page_layouts.yml file.
80
- def definition
81
- definition = PageLayout.get(page_layout)
82
- if definition.nil?
83
- log_warning "Page definition for `#{page_layout}` not found. Please check `page_layouts.yml` file."
84
- return {}
65
+ # Returns a Hash describing the status of the Page.
66
+ #
67
+ def status
68
+ {
69
+ public: public?,
70
+ locked: locked?,
71
+ restricted: restricted?,
72
+ }
85
73
  end
86
- definition
87
- end
88
74
 
89
- # Returns translated name of the pages page_layout value.
90
- # Page layout names are defined inside the config/alchemy/page_layouts.yml file.
91
- # Translate the name in your config/locales language yml file.
92
- def layout_display_name
93
- Alchemy.t(page_layout, scope: "page_layout_names")
94
- end
75
+ # Returns the translated status for given status type.
76
+ #
77
+ # @param [Symbol] status_type
78
+ #
79
+ def status_title(status_type)
80
+ Alchemy.t(status[status_type].to_s, scope: "page_states.#{status_type}")
81
+ end
95
82
 
96
- # Returns the name for the layout partial
97
- #
98
- def layout_partial_name
99
- page_layout.parameterize.underscore
100
- end
83
+ # Returns the self#page_layout definition from config/alchemy/page_layouts.yml file.
84
+ def definition
85
+ definition = PageLayout.get(page_layout)
86
+ if definition.nil?
87
+ log_warning "Page definition for `#{page_layout}` not found. Please check `page_layouts.yml` file."
88
+ return {}
89
+ end
90
+ definition
91
+ end
101
92
 
102
- # Returns the key that's taken for cache path.
103
- #
104
- # Uses the +published_at+ value that's updated when the user publishes the page.
105
- #
106
- # If the page is the current preview it uses the updated_at value as cache key.
107
- #
108
- def cache_key
109
- if Page.current_preview == self
110
- "alchemy/pages/#{id}-#{updated_at}"
111
- else
112
- "alchemy/pages/#{id}-#{published_at}"
93
+ # Returns translated name of the pages page_layout value.
94
+ # Page layout names are defined inside the config/alchemy/page_layouts.yml file.
95
+ # Translate the name in your config/locales language yml file.
96
+ def layout_display_name
97
+ Alchemy.t(page_layout, scope: "page_layout_names")
113
98
  end
114
- end
115
99
 
116
- # We use the published_at value for the cache_key.
117
- #
118
- # If no published_at value is set yet, i.e. because it was never published,
119
- # we return the updated_at value.
120
- #
121
- def published_at
122
- read_attribute(:published_at) || updated_at
123
- end
100
+ # Returns the name for the layout partial
101
+ #
102
+ def layout_partial_name
103
+ page_layout.parameterize.underscore
104
+ end
124
105
 
125
- # Returns true if the page cache control headers should be set.
126
- #
127
- # == Disable Alchemy's page caching globally
128
- #
129
- # # config/alchemy/config.yml
130
- # ...
131
- # cache_pages: false
132
- #
133
- # == Disable caching on page layout level
134
- #
135
- # # config/alchemy/page_layouts.yml
136
- # - name: contact
137
- # cache: false
138
- #
139
- # == Note:
140
- #
141
- # This only sets the cache control headers and skips rendering of the page body,
142
- # if the cache is fresh.
143
- # This does not disable the fragment caching in the views.
144
- # So if you don't want a page and it's elements to be cached,
145
- # then be sure to not use <% cache element %> in the views.
146
- #
147
- # @returns Boolean
148
- #
149
- def cache_page?
150
- return false unless caching_enabled?
151
-
152
- page_layout = PageLayout.get(self.page_layout)
153
- page_layout["cache"] != false && page_layout["searchresults"] != true
154
- end
106
+ # Returns the key that's taken for cache path.
107
+ #
108
+ # Uses the +published_at+ value that's updated when the user publishes the page.
109
+ #
110
+ # If the page is the current preview it uses the updated_at value as cache key.
111
+ #
112
+ def cache_key
113
+ if Page.current_preview == self
114
+ "alchemy/pages/#{id}-#{updated_at}"
115
+ else
116
+ "alchemy/pages/#{id}-#{published_at}"
117
+ end
118
+ end
155
119
 
156
- private
120
+ # We use the published_at value for the cache_key.
121
+ #
122
+ # If no published_at value is set yet, i.e. because it was never published,
123
+ # we return the updated_at value.
124
+ #
125
+ def published_at
126
+ read_attribute(:published_at) || updated_at
127
+ end
157
128
 
158
- def caching_enabled?
159
- Alchemy::Config.get(:cache_pages) &&
160
- Rails.application.config.action_controller.perform_caching
161
- end
129
+ # Returns true if the page cache control headers should be set.
130
+ #
131
+ # == Disable Alchemy's page caching globally
132
+ #
133
+ # # config/alchemy/config.yml
134
+ # ...
135
+ # cache_pages: false
136
+ #
137
+ # == Disable caching on page layout level
138
+ #
139
+ # # config/alchemy/page_layouts.yml
140
+ # - name: contact
141
+ # cache: false
142
+ #
143
+ # == Note:
144
+ #
145
+ # This only sets the cache control headers and skips rendering of the page body,
146
+ # if the cache is fresh.
147
+ # This does not disable the fragment caching in the views.
148
+ # So if you don't want a page and it's elements to be cached,
149
+ # then be sure to not use <% cache element %> in the views.
150
+ #
151
+ # @returns Boolean
152
+ #
153
+ def cache_page?
154
+ return false unless caching_enabled?
155
+
156
+ page_layout = PageLayout.get(self.page_layout)
157
+ page_layout["cache"] != false && page_layout["searchresults"] != true
158
+ end
162
159
 
163
- def already_public_for?(time)
164
- !public_on.nil? && public_on <= time
165
- end
160
+ private
166
161
 
167
- def still_public_for?(time)
168
- public_until.nil? || public_until >= time
162
+ def caching_enabled?
163
+ Alchemy::Config.get(:cache_pages) &&
164
+ Rails.application.config.action_controller.perform_caching
165
+ end
169
166
  end
170
167
  end
171
168
  end
@@ -3,110 +3,121 @@
3
3
  module Alchemy
4
4
  # ActiveRecord scopes for Alchemy::Page
5
5
  #
6
- module Page::PageScopes
7
- extend ActiveSupport::Concern
8
-
9
- included do
10
- # All language root pages
11
- #
12
- scope :language_roots, -> { where(language_root: true) }
13
-
14
- # All layout pages
15
- #
16
- scope :layoutpages, -> { where(layoutpage: true) }
17
-
18
- # All locked pages
19
- #
20
- scope :locked, -> { where.not(locked_at: nil).where.not(locked_by: nil) }
21
-
22
- # All pages locked by given user
23
- #
24
- scope :locked_by,
25
- ->(user) {
26
- if user.class.respond_to? :primary_key
27
- locked.where(locked_by: user.send(user.class.primary_key))
28
- end
29
- }
30
-
31
- # All not locked pages
32
- #
33
- scope :not_locked, -> { where(locked_at: nil, locked_by: nil) }
34
-
35
- # All not restricted pages
36
- #
37
- scope :not_restricted, -> { where(restricted: false) }
38
-
39
- # All restricted pages
40
- #
41
- scope :restricted, -> { where(restricted: true) }
42
-
43
- # All pages that are a published language root
44
- #
45
- scope :public_language_roots,
46
- -> {
47
- published.language_roots.where(
48
- language_code: Language.published.pluck(:language_code),
49
- )
50
- }
51
-
52
- # Last 5 pages that where recently edited by given user
53
- #
54
- scope :all_last_edited_from,
55
- ->(user) {
56
- where(updater_id: user.id).order("updated_at DESC").limit(5)
57
- }
58
-
59
- # Returns all pages that have the given +language_id+
60
- #
61
- scope :with_language, ->(language_id) { where(language_id: language_id) }
62
-
63
- # Returns all content pages.
64
- #
65
- scope :contentpages, -> { where(layoutpage: [false, nil]) }
66
-
67
- # Returns all public contentpages that are not locked.
68
- #
69
- # Used for flushing all pages caches at once.
70
- #
71
- scope :flushables, -> { not_locked.published.contentpages }
72
-
73
- # Returns all layoutpages that are not locked.
74
- #
75
- # Used for flushing all pages caches at once.
76
- #
77
- scope :flushable_layoutpages, -> { not_locked.layoutpages }
78
-
79
- # All searchable pages
80
- #
81
- scope :searchables, -> { not_restricted.published.contentpages }
82
-
83
- # All pages from +Alchemy::Site.current+
84
- #
85
- scope :from_current_site,
86
- -> {
87
- where(Language.table_name => { site_id: Site.current || Site.default }).joins(:language)
88
- }
89
-
90
- # All pages for xml sitemap
91
- #
92
- scope :sitemap, -> { from_current_site.published.contentpages.where(sitemap: true) }
93
- end
94
-
95
- module ClassMethods
96
- # All public pages
97
- #
98
- def published
99
- where("#{table_name}.public_on <= :time AND " \
100
- "(#{table_name}.public_until IS NULL " \
101
- "OR #{table_name}.public_until >= :time)", time: Time.current)
6
+ class Page < BaseRecord
7
+ module PageScopes
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # All language root pages
12
+ #
13
+ scope :language_roots, -> { where(language_root: true) }
14
+
15
+ # All layout pages
16
+ #
17
+ scope :layoutpages, -> { where(layoutpage: true) }
18
+
19
+ # All locked pages
20
+ #
21
+ scope :locked, -> { where.not(locked_at: nil).where.not(locked_by: nil) }
22
+
23
+ # All pages locked by given user
24
+ #
25
+ scope :locked_by,
26
+ ->(user) {
27
+ if user.class.respond_to? :primary_key
28
+ locked.where(locked_by: user.send(user.class.primary_key))
29
+ end
30
+ }
31
+
32
+ # All not locked pages
33
+ #
34
+ scope :not_locked, -> { where(locked_at: nil, locked_by: nil) }
35
+
36
+ # All not restricted pages
37
+ #
38
+ scope :not_restricted, -> { where(restricted: false) }
39
+
40
+ # All restricted pages
41
+ #
42
+ scope :restricted, -> { where(restricted: true) }
43
+
44
+ # All public pages
45
+ #
46
+ scope :published,
47
+ -> {
48
+ joins(:language, :versions).
49
+ merge(Language.published).
50
+ merge(PageVersion.public_on(Time.current))
51
+ }
52
+
53
+ # All pages that are a published language root
54
+ #
55
+ scope :public_language_roots,
56
+ -> {
57
+ published.language_roots.where(
58
+ language_code: Language.published.pluck(:language_code),
59
+ )
60
+ }
61
+
62
+ # Last 5 pages that where recently edited by given user
63
+ #
64
+ scope :all_last_edited_from,
65
+ ->(user) {
66
+ where(updater_id: user.id).order("updated_at DESC").limit(5)
67
+ }
68
+
69
+ # Returns all pages that have the given +language_id+
70
+ #
71
+ scope :with_language, ->(language_id) { where(language_id: language_id) }
72
+
73
+ # Returns all content pages.
74
+ #
75
+ scope :contentpages, -> { where(layoutpage: [false, nil]) }
76
+
77
+ # Returns all public contentpages that are not locked.
78
+ #
79
+ # Used for flushing all pages caches at once.
80
+ #
81
+ scope :flushables, -> { not_locked.published.contentpages }
82
+
83
+ # Returns all layoutpages that are not locked.
84
+ #
85
+ # Used for flushing all pages caches at once.
86
+ #
87
+ scope :flushable_layoutpages, -> { not_locked.layoutpages }
88
+
89
+ # All searchable pages
90
+ #
91
+ scope :searchables, -> { not_restricted.published.contentpages }
92
+
93
+ # All pages from +Alchemy::Site.current+
94
+ #
95
+ scope :from_current_site,
96
+ -> {
97
+ where(Language.table_name => { site_id: Site.current || Site.default }).joins(:language)
98
+ }
99
+
100
+ # All pages for xml sitemap
101
+ #
102
+ scope :sitemap, -> { from_current_site.published.contentpages.where(sitemap: true) }
102
103
  end
103
104
 
104
- # All not public pages
105
- #
106
- def not_public
107
- where("#{table_name}.public_on IS NULL OR " \
108
- "#{table_name}.public_on >= :time OR " \
109
- "#{table_name}.public_until <= :time", time: Time.current)
105
+ module ClassMethods
106
+ # All pages that do not have any public version
107
+ #
108
+ def not_public(time = Time.current)
109
+ where <<~SQL
110
+ alchemy_pages.id NOT IN (
111
+ SELECT alchemy_page_versions.page_id
112
+ FROM alchemy_page_versions
113
+ WHERE alchemy_page_versions.public_on <= '#{connection.quoted_date(time)}'
114
+ AND (
115
+ alchemy_page_versions.public_until IS NULL
116
+ OR alchemy_page_versions.public_until >= '#{connection.quoted_date(time)}'
117
+ )
118
+ )
119
+ SQL
120
+ end
110
121
  end
111
122
  end
112
123
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "alchemy/page"
4
+
5
+ module Alchemy
6
+ class Page < BaseRecord
7
+ # Handles publishing of pages
8
+ class Publisher
9
+ def initialize(page)
10
+ @page = page
11
+ end
12
+
13
+ # Copies all currently visible elements to the public version of page
14
+ #
15
+ # Creates a new published version if none exists yet.
16
+ #
17
+ # Sends a publish notification to all registered publish targets
18
+ #
19
+ def publish!(public_on:)
20
+ Page.transaction do
21
+ version = public_version(public_on)
22
+ DeleteElements.new(version.elements).call
23
+
24
+ repository = page.draft_version.element_repository
25
+ ActiveRecord::Base.no_touching do
26
+ Element.acts_as_list_no_update do
27
+ repository.visible.not_nested.each.with_index(1) do |element, position|
28
+ Alchemy::DuplicateElement.new(element, repository: repository).call(
29
+ page_version_id: version.id,
30
+ position: position
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ Alchemy.publish_targets.each { |p| p.perform_later(page) }
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :page
43
+
44
+ # Load the pages public version or create one
45
+ def public_version(public_on)
46
+ page.public_version || page.versions.create!(public_on: public_on)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -29,7 +29,7 @@ module Alchemy
29
29
  def call
30
30
  if @page.language_root?
31
31
  language_root_path
32
- elsif @site.languages.select(&:public?).length > 1
32
+ elsif @site.languages.count(&:public?) > 1
33
33
  page_path_with_language_prefix
34
34
  else
35
35
  page_path_with_leading_slash