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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -14
- data/.gitignore +0 -1
- data/.hound.yml +1 -1
- data/.rubocop.yml +46 -4
- data/CHANGELOG.md +80 -25
- data/Gemfile +4 -2
- data/README.md +5 -2
- data/alchemy_cms.gemspec +78 -65
- data/app/assets/javascripts/alchemy/admin.js +0 -2
- data/app/assets/javascripts/alchemy/alchemy.base.js.coffee +0 -27
- data/app/assets/javascripts/alchemy/alchemy.confirm_dialog.js.coffee +2 -1
- data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +1 -1
- data/app/assets/javascripts/alchemy/alchemy.dragndrop.js.coffee +0 -25
- data/app/assets/javascripts/alchemy/alchemy.element_editors.js.coffee +1 -1
- data/app/assets/javascripts/alchemy/alchemy.elements_window.js.coffee +2 -0
- data/app/assets/javascripts/alchemy/alchemy.fixed_elements.js +1 -1
- data/app/assets/javascripts/alchemy/alchemy.gui.js.coffee +3 -1
- data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +1 -1
- data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +40 -27
- data/app/assets/javascripts/alchemy/templates/node_folder.hbs +1 -1
- data/app/assets/stylesheets/alchemy/admin.scss +1 -1
- data/app/assets/stylesheets/alchemy/archive.scss +4 -4
- data/app/assets/stylesheets/alchemy/buttons.scss +0 -4
- data/app/assets/stylesheets/alchemy/elements.scss +73 -61
- data/app/assets/stylesheets/alchemy/images.scss +8 -0
- data/app/assets/stylesheets/alchemy/node-select.scss +4 -3
- data/app/assets/stylesheets/alchemy/page-select.scss +1 -0
- data/app/assets/stylesheets/tinymce/skins/alchemy/skin.min.css.scss +6 -6
- data/app/controllers/alchemy/admin/attachments_controller.rb +6 -2
- data/app/controllers/alchemy/admin/base_controller.rb +5 -7
- data/app/controllers/alchemy/admin/elements_controller.rb +58 -34
- data/app/controllers/alchemy/admin/essence_audios_controller.rb +30 -0
- data/app/controllers/alchemy/admin/essence_files_controller.rb +0 -14
- data/app/controllers/alchemy/admin/essence_pictures_controller.rb +8 -79
- data/app/controllers/alchemy/admin/essence_videos_controller.rb +33 -0
- data/app/controllers/alchemy/admin/ingredients_controller.rb +30 -0
- data/app/controllers/alchemy/admin/layoutpages_controller.rb +0 -1
- data/app/controllers/alchemy/admin/pages_controller.rb +6 -13
- data/app/controllers/alchemy/admin/pictures_controller.rb +35 -9
- data/app/controllers/alchemy/api/elements_controller.rb +10 -5
- data/app/controllers/alchemy/api/pages_controller.rb +2 -4
- data/app/controllers/concerns/alchemy/admin/archive_overlay.rb +13 -3
- data/app/controllers/concerns/alchemy/admin/crop_action.rb +26 -0
- data/app/decorators/alchemy/element_editor.rb +23 -1
- data/app/decorators/alchemy/ingredient_editor.rb +154 -0
- data/app/helpers/alchemy/admin/elements_helper.rb +1 -0
- data/app/helpers/alchemy/admin/essences_helper.rb +1 -1
- data/app/helpers/alchemy/admin/ingredients_helper.rb +42 -0
- data/app/helpers/alchemy/elements_block_helper.rb +22 -7
- data/app/helpers/alchemy/elements_helper.rb +12 -5
- data/app/helpers/alchemy/pages_helper.rb +3 -11
- data/app/jobs/alchemy/base_job.rb +11 -0
- data/app/jobs/alchemy/publish_page_job.rb +11 -0
- data/app/models/alchemy/attachment.rb +1 -1
- data/app/models/alchemy/content/factory.rb +23 -27
- data/app/models/alchemy/content.rb +1 -6
- data/app/models/alchemy/element/definitions.rb +29 -27
- data/app/models/alchemy/element/element_contents.rb +131 -122
- data/app/models/alchemy/element/element_essences.rb +100 -98
- data/app/models/alchemy/element/element_ingredients.rb +176 -0
- data/app/models/alchemy/element/presenters.rb +89 -87
- data/app/models/alchemy/element.rb +40 -73
- data/app/models/alchemy/elements_repository.rb +126 -0
- data/app/models/alchemy/essence_audio.rb +12 -0
- data/app/models/alchemy/essence_headline.rb +40 -0
- data/app/models/alchemy/essence_picture.rb +4 -116
- data/app/models/alchemy/essence_richtext.rb +12 -0
- data/app/models/alchemy/essence_video.rb +12 -0
- data/app/models/alchemy/image_cropper_settings.rb +87 -0
- data/app/models/alchemy/ingredient.rb +219 -0
- data/app/models/alchemy/ingredient_validator.rb +97 -0
- data/app/models/alchemy/ingredients/audio.rb +29 -0
- data/app/models/alchemy/ingredients/boolean.rb +21 -0
- data/app/models/alchemy/ingredients/datetime.rb +20 -0
- data/app/models/alchemy/ingredients/file.rb +30 -0
- data/app/models/alchemy/ingredients/headline.rb +42 -0
- data/app/models/alchemy/ingredients/html.rb +19 -0
- data/app/models/alchemy/ingredients/link.rb +16 -0
- data/app/models/alchemy/ingredients/node.rb +23 -0
- data/app/models/alchemy/ingredients/page.rb +23 -0
- data/app/models/alchemy/ingredients/picture.rb +41 -0
- data/app/models/alchemy/ingredients/richtext.rb +57 -0
- data/app/models/alchemy/ingredients/select.rb +10 -0
- data/app/models/alchemy/ingredients/text.rb +17 -0
- data/app/models/alchemy/ingredients/video.rb +33 -0
- data/app/models/alchemy/language.rb +0 -11
- data/app/models/alchemy/node.rb +1 -1
- data/app/models/alchemy/page/fixed_attributes.rb +53 -51
- data/app/models/alchemy/page/page_elements.rb +186 -205
- data/app/models/alchemy/page/page_naming.rb +66 -64
- data/app/models/alchemy/page/page_natures.rb +139 -142
- data/app/models/alchemy/page/page_scopes.rb +113 -102
- data/app/models/alchemy/page/publisher.rb +50 -0
- data/app/models/alchemy/page/url_path.rb +1 -1
- data/app/models/alchemy/page.rb +67 -33
- data/app/models/alchemy/page_version.rb +58 -0
- data/app/models/alchemy/picture/calculations.rb +2 -8
- data/app/models/alchemy/picture/preprocessor.rb +2 -0
- data/app/models/alchemy/picture/transformations.rb +24 -96
- data/app/models/alchemy/picture.rb +4 -2
- data/app/models/concerns/alchemy/picture_thumbnails.rb +181 -0
- data/app/models/concerns/alchemy/touch_elements.rb +2 -2
- data/app/presenters/alchemy/picture_view.rb +88 -0
- data/app/serializers/alchemy/element_serializer.rb +5 -0
- data/app/serializers/alchemy/page_tree_serializer.rb +3 -2
- data/app/services/alchemy/delete_elements.rb +44 -0
- data/app/services/alchemy/duplicate_element.rb +56 -0
- data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +1 -2
- data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
- data/app/views/alchemy/admin/attachments/assign.js.erb +11 -0
- data/app/views/alchemy/admin/crop.html.erb +36 -0
- data/app/views/alchemy/admin/elements/_element.html.erb +14 -10
- data/app/views/alchemy/admin/elements/{_element_footer.html.erb → _footer.html.erb} +0 -0
- data/app/views/alchemy/admin/elements/{_new_element_form.html.erb → _form.html.erb} +1 -1
- data/app/views/alchemy/admin/elements/{_element_header.html.erb → _header.html.erb} +1 -1
- data/app/views/alchemy/admin/elements/{_element_toolbar.html.erb → _toolbar.html.erb} +5 -6
- data/app/views/alchemy/admin/elements/{trash.js.erb → destroy.js.erb} +1 -3
- data/app/views/alchemy/admin/elements/new.html.erb +3 -3
- data/app/views/alchemy/admin/elements/order.js.erb +0 -17
- data/app/views/alchemy/admin/elements/update.js.erb +3 -2
- data/app/views/alchemy/admin/essence_audios/edit.html.erb +7 -0
- data/app/views/alchemy/admin/essence_pictures/update.js.erb +0 -1
- data/app/views/alchemy/admin/essence_videos/edit.html.erb +11 -0
- data/app/views/alchemy/admin/ingredients/_audio_fields.html.erb +4 -0
- data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +18 -0
- data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +25 -0
- data/app/views/alchemy/admin/ingredients/_video_fields.html.erb +8 -0
- data/app/views/alchemy/admin/ingredients/edit.html.erb +4 -0
- data/app/views/alchemy/admin/layoutpages/edit.html.erb +0 -5
- data/app/views/alchemy/admin/nodes/_node.html.erb +2 -2
- data/app/views/alchemy/admin/pages/_anchor_link.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_external_link.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_file_link.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_form.html.erb +0 -6
- data/app/views/alchemy/admin/pages/_internal_link.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_tinymce_custom_config.html.erb +5 -2
- data/app/views/alchemy/admin/pages/edit.html.erb +36 -24
- data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +2 -4
- data/app/views/alchemy/admin/partials/_routes.html.erb +7 -11
- data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +4 -8
- data/app/views/alchemy/admin/pictures/_infos.html.erb +0 -1
- data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +4 -4
- data/app/views/alchemy/admin/pictures/assign.js.erb +10 -0
- data/app/views/alchemy/admin/resources/_form.html.erb +1 -0
- data/app/views/alchemy/essences/_essence_audio_editor.html.erb +4 -0
- data/app/views/alchemy/essences/_essence_audio_view.html.erb +15 -0
- data/app/views/alchemy/essences/_essence_file_editor.html.erb +15 -6
- data/app/views/alchemy/essences/_essence_headline_editor.html.erb +36 -0
- data/app/views/alchemy/essences/_essence_headline_view.html.erb +10 -0
- data/app/views/alchemy/essences/_essence_link_editor.html.erb +8 -4
- data/app/views/alchemy/essences/_essence_picture_editor.html.erb +27 -12
- data/app/views/alchemy/essences/_essence_text_editor.html.erb +12 -4
- data/app/views/alchemy/essences/_essence_video_editor.html.erb +4 -0
- data/app/views/alchemy/essences/_essence_video_view.html.erb +18 -0
- data/app/views/alchemy/essences/shared/_essence_picture_tools.html.erb +21 -16
- data/app/views/alchemy/essences/shared/_linkable_essence_tools.html.erb +2 -2
- data/app/views/alchemy/ingredients/_audio_editor.html.erb +5 -0
- data/app/views/alchemy/ingredients/_audio_view.html.erb +14 -0
- data/app/views/alchemy/ingredients/_boolean_editor.html.erb +11 -0
- data/app/views/alchemy/ingredients/_boolean_view.html.erb +1 -0
- data/app/views/alchemy/ingredients/_datetime_editor.html.erb +17 -0
- data/app/views/alchemy/ingredients/_datetime_view.html.erb +9 -0
- data/app/views/alchemy/ingredients/_file_editor.html.erb +50 -0
- data/app/views/alchemy/ingredients/_file_view.html.erb +17 -0
- data/app/views/alchemy/ingredients/_headline_editor.html.erb +30 -0
- data/app/views/alchemy/ingredients/_headline_view.html.erb +9 -0
- data/app/views/alchemy/ingredients/_html_editor.html.erb +8 -0
- data/app/views/alchemy/ingredients/_html_view.html.erb +1 -0
- data/app/views/alchemy/ingredients/_link_editor.html.erb +24 -0
- data/app/views/alchemy/ingredients/_link_view.html.erb +9 -0
- data/app/views/alchemy/ingredients/_node_editor.html.erb +25 -0
- data/app/views/alchemy/ingredients/_node_view.html.erb +1 -0
- data/app/views/alchemy/ingredients/_page_editor.html.erb +24 -0
- data/app/views/alchemy/ingredients/_page_view.html.erb +4 -0
- data/app/views/alchemy/ingredients/_picture_editor.html.erb +59 -0
- data/app/views/alchemy/ingredients/_picture_view.html.erb +5 -0
- data/app/views/alchemy/ingredients/_richtext_editor.html.erb +12 -0
- data/app/views/alchemy/ingredients/_richtext_view.html.erb +3 -0
- data/app/views/alchemy/ingredients/_select_editor.html.erb +29 -0
- data/app/views/alchemy/ingredients/_select_view.html.erb +1 -0
- data/app/views/alchemy/ingredients/_text_editor.html.erb +19 -0
- data/app/views/alchemy/ingredients/_text_view.html.erb +16 -0
- data/app/views/alchemy/ingredients/_video_editor.html.erb +5 -0
- data/app/views/alchemy/ingredients/_video_view.html.erb +17 -0
- data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +20 -0
- data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +57 -0
- data/config/brakeman.ignore +66 -159
- data/config/initializers/dragonfly.rb +10 -0
- data/config/locales/alchemy.en.yml +23 -15
- data/config/routes.rb +17 -22
- data/db/migrate/20201207131309_create_page_versions.rb +19 -0
- data/db/migrate/20201207135820_add_page_version_id_to_alchemy_elements.rb +76 -0
- data/db/migrate/20210205143548_rename_public_on_and_public_until_on_alchemy_pages.rb +10 -0
- data/db/migrate/20210326105046_add_sanitized_body_to_alchemy_essence_richtexts.rb +7 -0
- data/db/migrate/20210406093436_add_alchemy_essence_headlines.rb +12 -0
- data/db/migrate/20210506135919_create_essence_audios.rb +19 -0
- data/db/migrate/20210506140258_create_essence_videos.rb +23 -0
- data/db/migrate/20210508091432_create_alchemy_ingredients.rb +22 -0
- data/lib/alchemy/admin/preview_url.rb +2 -0
- data/lib/alchemy/deprecation.rb +1 -1
- data/lib/alchemy/dragonfly/processors/auto_orient.rb +18 -0
- data/lib/alchemy/dragonfly/processors/crop_resize.rb +35 -0
- data/lib/alchemy/elements_finder.rb +14 -60
- data/lib/alchemy/engine.rb +1 -1
- data/lib/alchemy/essence.rb +1 -2
- data/lib/alchemy/hints.rb +8 -4
- data/lib/alchemy/page_layout.rb +0 -13
- data/lib/alchemy/permissions.rb +30 -29
- data/lib/alchemy/resource.rb +13 -3
- data/lib/alchemy/tasks/tidy.rb +29 -0
- data/lib/alchemy/test_support/essence_shared_examples.rb +0 -1
- data/lib/alchemy/test_support/factories/element_factory.rb +8 -8
- data/lib/alchemy/test_support/factories/essence_audio_factory.rb +7 -0
- data/lib/alchemy/test_support/factories/essence_video_factory.rb +7 -0
- data/lib/alchemy/test_support/factories/ingredient_factory.rb +25 -0
- data/lib/alchemy/test_support/factories/page_factory.rb +20 -1
- data/lib/alchemy/test_support/factories/page_version_factory.rb +23 -0
- data/lib/alchemy/test_support/having_crop_action_examples.rb +170 -0
- data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +646 -0
- data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +21 -0
- data/lib/alchemy/test_support/shared_ingredient_examples.rb +57 -0
- data/lib/alchemy/test_support.rb +2 -11
- data/lib/alchemy/tinymce.rb +17 -0
- data/lib/alchemy/upgrader/five_point_zero.rb +0 -32
- data/lib/alchemy/upgrader/six_point_zero.rb +21 -0
- data/lib/alchemy/upgrader/tasks/add_page_versions.rb +33 -0
- data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +51 -0
- data/lib/alchemy/version.rb +1 -1
- data/lib/generators/alchemy/elements/elements_generator.rb +1 -0
- data/lib/generators/alchemy/elements/templates/view.html.erb +9 -0
- data/lib/generators/alchemy/elements/templates/view.html.haml +9 -0
- data/lib/generators/alchemy/elements/templates/view.html.slim +9 -0
- data/lib/generators/alchemy/ingredient/ingredient_generator.rb +38 -0
- data/lib/generators/alchemy/ingredient/templates/editor.html.erb +14 -0
- data/lib/generators/alchemy/ingredient/templates/model.rb.tt +13 -0
- data/lib/generators/alchemy/ingredient/templates/view.html.erb +1 -0
- data/lib/generators/alchemy/install/install_generator.rb +1 -2
- data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +1 -1
- data/lib/generators/alchemy/menus/templates/node.html.erb +1 -1
- data/lib/generators/alchemy/menus/templates/node.html.haml +1 -1
- data/lib/generators/alchemy/menus/templates/node.html.slim +1 -1
- data/lib/generators/alchemy/menus/templates/wrapper.html.erb +1 -1
- data/lib/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
- data/lib/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
- data/lib/tasks/alchemy/tidy.rake +12 -0
- data/lib/tasks/alchemy/upgrade.rake +21 -15
- data/package/admin.js +9 -1
- data/package/src/file_editors.js +28 -0
- data/package/src/image_cropper.js +103 -0
- data/package/src/image_loader.js +58 -0
- data/package/src/node_tree.js +5 -5
- data/package/src/picture_editors.js +169 -0
- data/package/src/utils/__tests__/ajax.spec.js +20 -12
- data/package/src/utils/ajax.js +8 -3
- data/package.json +3 -2
- data/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +3 -18
- data/vendor/assets/stylesheets/jquery.Jcrop.min.scss +2 -28
- metadata +285 -56
- data/app/assets/javascripts/alchemy/alchemy.image_cropper.js.coffee +0 -44
- data/app/assets/javascripts/alchemy/alchemy.trash_window.js.coffee +0 -30
- data/app/assets/stylesheets/alchemy/trash.scss +0 -8
- data/app/controllers/alchemy/admin/trash_controller.rb +0 -44
- data/app/views/alchemy/admin/essence_files/assign.js.erb +0 -3
- data/app/views/alchemy/admin/essence_pictures/assign.js.erb +0 -4
- data/app/views/alchemy/admin/essence_pictures/crop.html.erb +0 -48
- data/app/views/alchemy/admin/trash/clear.js.erb +0 -4
- data/app/views/alchemy/admin/trash/index.html.erb +0 -31
- 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
|
|
@@ -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(" ").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
|
-
|
|
31
|
-
return if
|
|
32
|
+
renderable = element.ingredient_by_role(name) || content(name)
|
|
33
|
+
return if renderable.nil?
|
|
32
34
|
|
|
33
|
-
helpers.render(
|
|
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
|
|
54
|
-
# The page the elements are rendered from.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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.
|
|
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:
|
|
28
|
-
essence_type: normalize_essence_type(definition[:type]),
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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(
|
|
121
|
-
self.essence = essence_class
|
|
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!(
|
|
131
|
-
build_essence(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
class Element < BaseRecord
|
|
5
|
+
# Module concerning element definitions
|
|
6
|
+
#
|
|
7
|
+
module Definitions
|
|
8
|
+
extend ActiveSupport::Concern
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|