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,108 +1,110 @@
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_content(maxlength) || preview_text_from_nested_elements(maxlength)
27
50
  end
28
- end
29
51
 
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
37
-
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
50
-
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
52
+ # Generates a preview text containing Element#display_name and Element#preview_text.
53
+ #
54
+ # It is displayed inside the head of the Element in the Elements.list overlay window from the Alchemy Admin::Page#edit view.
55
+ #
56
+ # === Example
57
+ #
58
+ # A Element described as:
59
+ #
60
+ # - name: funky_element
61
+ # display_name: Funky Element
62
+ # contents:
63
+ # - name: headline
64
+ # type: EssenceText
65
+ # - name: text
66
+ # type EssenceRichtext
67
+ # as_element_title: true
68
+ #
69
+ # With "I want to tell you a funky story" as stripped_body for the EssenceRichtext Content produces:
70
+ #
71
+ # Funky Element: I want to tell ...
72
+ #
73
+ # @param maxlength [Fixnum] (30)
74
+ # Length of characters after the text will be cut off.
75
+ #
76
+ def display_name_with_preview_text(maxlength = 30)
77
+ "#{display_name}: #{preview_text(maxlength)}"
78
+ end
78
79
 
79
- # Returns a dom id used for elements html id tag.
80
- #
81
- def dom_id
82
- "#{name}_#{id}"
83
- end
80
+ # Returns a dom id used for elements html id tag.
81
+ #
82
+ def dom_id
83
+ "#{name}_#{id}"
84
+ end
84
85
 
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
86
+ # The content that's used for element's preview text.
87
+ #
88
+ # It tries to find one of element's contents that is defined +as_element_title+.
89
+ # Takes element's first content if no content is defined +as_element_title+.
90
+ #
91
+ # @return (Alchemy::Content)
92
+ #
93
+ def preview_content
94
+ @_preview_content ||= contents.detect(&:preview_content?) || contents.first
95
+ end
95
96
 
96
- private
97
+ private
97
98
 
98
- def preview_text_from_nested_elements(maxlength)
99
- return if all_nested_elements.empty?
99
+ def preview_text_from_nested_elements(maxlength)
100
+ return if all_nested_elements.empty?
100
101
 
101
- all_nested_elements.first.preview_text(maxlength)
102
- end
102
+ all_nested_elements.first.preview_text(maxlength)
103
+ end
103
104
 
104
- def preview_text_from_preview_content(maxlength)
105
- preview_content.try!(:preview_text, maxlength)
105
+ def preview_text_from_preview_content(maxlength)
106
+ preview_content.try!(:preview_text, maxlength)
107
+ end
106
108
  end
107
109
  end
108
110
  end
@@ -7,7 +7,7 @@
7
7
  # id :integer not null, primary key
8
8
  # name :string
9
9
  # position :integer
10
- # page_id :integer not null
10
+ # page_version_id :integer not null
11
11
  # public :boolean default(TRUE)
12
12
  # fixed :boolean default(FALSE)
13
13
  # folded :boolean default(FALSE)
@@ -20,6 +20,12 @@
20
20
  # parent_element_id :integer
21
21
  #
22
22
 
23
+ require_dependency "alchemy/element/definitions"
24
+ require_dependency "alchemy/element/element_contents"
25
+ require_dependency "alchemy/element/element_ingredients"
26
+ require_dependency "alchemy/element/element_essences"
27
+ require_dependency "alchemy/element/presenters"
28
+
23
29
  module Alchemy
24
30
  class Element < BaseRecord
25
31
  NAME_REGEXP = /\A[a-z0-9_-]+\z/
@@ -34,39 +40,31 @@ module Alchemy
34
40
  "nestable_elements",
35
41
  "contents",
36
42
  "hint",
43
+ "ingredients",
37
44
  "taggable",
38
45
  "compact",
39
46
  "message",
40
47
  "deprecated",
41
48
  ].freeze
42
49
 
43
- SKIPPED_ATTRIBUTES_ON_COPY = [
44
- "cached_tag_list",
45
- "created_at",
46
- "creator_id",
47
- "id",
48
- "folded",
49
- "position",
50
- "updated_at",
51
- "updater_id",
52
- ].freeze
53
-
54
- # All Elements that share the same page id and parent element id and are fixed or not are considered a list.
50
+ # All Elements that share the same page version and parent element and are fixed or not are considered a list.
55
51
  #
56
- # If parent element id is nil (typical case for a simple page),
52
+ # If parent_element_id is nil (typical case for a simple page),
57
53
  # then all elements on that page are still in one list,
58
54
  # because acts_as_list correctly creates this statement:
59
55
  #
60
- # WHERE page_id = 1 and fixed = FALSE AND parent_element_id = NULL
56
+ # WHERE page_version_id = 1 and fixed = FALSE AND parent_element_id = NULL
61
57
  #
62
- acts_as_list scope: [:page_id, :fixed, :parent_element_id]
58
+ acts_as_list scope: [:page_version_id, :fixed, :parent_element_id]
63
59
 
64
- stampable stamper_class_name: Alchemy.user_class.name
60
+ stampable stamper_class_name: Alchemy.user_class_name
65
61
 
66
62
  has_many :contents, dependent: :destroy, inverse_of: :element
67
63
 
64
+ before_destroy :delete_all_nested_elements
65
+
68
66
  has_many :all_nested_elements,
69
- -> { order(:position).not_trashed },
67
+ -> { order(:position) },
70
68
  class_name: "Alchemy::Element",
71
69
  foreign_key: :parent_element_id,
72
70
  dependent: :destroy
@@ -78,7 +76,8 @@ module Alchemy
78
76
  dependent: :destroy,
79
77
  inverse_of: :parent_element
80
78
 
81
- belongs_to :page, touch: true, inverse_of: :elements
79
+ belongs_to :page_version, touch: true, inverse_of: :elements
80
+ has_one :page, through: :page_version
82
81
 
83
82
  # A nested element belongs to a parent element.
84
83
  belongs_to :parent_element,
@@ -101,11 +100,10 @@ module Alchemy
101
100
 
102
101
  after_update :touch_touchable_pages
103
102
 
104
- scope :trashed, -> { where(position: nil).order("updated_at DESC") }
105
- scope :not_trashed, -> { where.not(position: nil) }
106
103
  scope :published, -> { where(public: true) }
104
+ scope :hidden, -> { where(public: false) }
107
105
  scope :not_restricted, -> { joins(:page).merge(Page.not_restricted) }
108
- scope :available, -> { published.not_trashed }
106
+ scope :available, -> { published }
109
107
  scope :named, ->(names) { where(name: names) }
110
108
  scope :excluded, ->(names) { where.not(name: names) }
111
109
  scope :fixed, -> { where(fixed: true) }
@@ -118,16 +116,14 @@ module Alchemy
118
116
  delegate :restricted?, to: :page, allow_nil: true
119
117
 
120
118
  # Concerns
121
- include Alchemy::Element::Definitions
122
- include Alchemy::Element::ElementContents
123
- include Alchemy::Element::ElementEssences
124
- include Alchemy::Element::Presenters
119
+ include Definitions
120
+ include ElementContents
121
+ include ElementEssences
122
+ include ElementIngredients
123
+ include Presenters
125
124
 
126
125
  # class methods
127
126
  class << self
128
- deprecate :trashed, deprecator: Alchemy::Deprecation
129
- deprecate :not_trashed, deprecator: Alchemy::Deprecation
130
-
131
127
  # Builds a new element as described in +/config/alchemy/elements.yml+
132
128
  #
133
129
  # - Returns a new Alchemy::Element object if no name is given in attributes,
@@ -159,26 +155,7 @@ module Alchemy
159
155
  # @copy.public? # => false
160
156
  #
161
157
  def copy(source_element, differences = {})
162
- attributes = source_element.attributes.with_indifferent_access
163
- .except(*SKIPPED_ATTRIBUTES_ON_COPY)
164
- .merge(differences)
165
- .merge({
166
- autogenerate_contents: false,
167
- autogenerate_nested_elements: false,
168
- tag_list: source_element.tag_list,
169
- })
170
-
171
- new_element = create!(attributes)
172
-
173
- if source_element.contents.any?
174
- source_element.copy_contents_to(new_element)
175
- end
176
-
177
- if source_element.nested_elements.any?
178
- source_element.copy_nested_elements_to(new_element)
179
- end
180
-
181
- new_element
158
+ Alchemy::DuplicateElement.new(source_element).call(differences)
182
159
  end
183
160
 
184
161
  def all_from_clipboard(clipboard)
@@ -225,19 +202,6 @@ module Alchemy
225
202
  end
226
203
  end
227
204
 
228
- # Trashing an element means nullifying its position, folding and unpublishing it.
229
- def trash!
230
- self.public = false
231
- self.folded = true
232
- remove_from_list
233
- end
234
- deprecate :trash!, deprecator: Alchemy::Deprecation
235
-
236
- def trashed?
237
- position.nil?
238
- end
239
- deprecate :trashed?, deprecator: Alchemy::Deprecation
240
-
241
205
  # Returns true if the definition of this element has a taggable true value.
242
206
  def taggable?
243
207
  definition["taggable"] == true
@@ -322,22 +286,12 @@ module Alchemy
322
286
  definition.fetch("nestable_elements", [])
323
287
  end
324
288
 
325
- # Copy all nested elements from current element to given target element.
326
- def copy_nested_elements_to(target_element)
327
- nested_elements.map do |nested_element|
328
- Element.copy(nested_element, {
329
- parent_element_id: target_element.id,
330
- page_id: target_element.page_id,
331
- })
332
- end
333
- end
334
-
335
289
  private
336
290
 
337
291
  def generate_nested_elements
338
292
  definition.fetch("autogenerate", []).each do |nestable_element|
339
293
  if nestable_elements.include?(nestable_element)
340
- Element.create(page: page, parent_element_id: id, name: nestable_element)
294
+ Element.create(page_version: page_version, parent_element_id: id, name: nestable_element)
341
295
  else
342
296
  log_warning("Element '#{nestable_element}' not a nestable element for '#{name}'. Skipping!")
343
297
  end
@@ -358,5 +312,18 @@ module Alchemy
358
312
 
359
313
  touchable_pages.each(&:touch)
360
314
  end
315
+
316
+ def delete_all_nested_elements
317
+ deeply_nested_elements = descendent_elements(self).flatten
318
+ DeleteElements.new(deeply_nested_elements).call
319
+ nested_elements.reset
320
+ all_nested_elements.reset
321
+ end
322
+
323
+ def descendent_elements(element)
324
+ element.all_nested_elements + element.all_nested_elements.map do |nested_element|
325
+ descendent_elements(nested_element)
326
+ end
327
+ end
361
328
  end
362
329
  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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class EssenceAudio < ActiveRecord::Base
5
+ acts_as_essence(
6
+ ingredient_column: :attachment,
7
+ preview_text_method: :name,
8
+ )
9
+
10
+ belongs_to :attachment, optional: true
11
+ end
12
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class EssenceHeadline < BaseRecord
5
+ acts_as_essence
6
+
7
+ after_initialize :set_level_and_size
8
+
9
+ def preview_text(maxlength = 30)
10
+ "H#{level}: #{body}"[0..maxlength - 1]
11
+ end
12
+
13
+ def level_options
14
+ levels.map { |level| ["H#{level}", level] }
15
+ end
16
+
17
+ def size_options
18
+ sizes.map { |size| ["H#{size}", size] }
19
+ end
20
+
21
+ private
22
+
23
+ def content_settings
24
+ content&.settings || {}
25
+ end
26
+
27
+ def levels
28
+ content_settings.fetch(:levels, (1..6))
29
+ end
30
+
31
+ def sizes
32
+ content_settings.fetch(:sizes, [])
33
+ end
34
+
35
+ def set_level_and_size
36
+ self.level ||= levels.first
37
+ self.size ||= sizes.first
38
+ end
39
+ end
40
+ end