alchemy_cms 5.2.0 → 6.0.0.b3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +114 -5
- data/Gemfile +8 -1
- 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/_extends.scss +15 -2
- data/app/assets/stylesheets/alchemy/admin.scss +1 -1
- data/app/assets/stylesheets/alchemy/archive.scss +20 -5
- 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/controllers/alchemy/admin/attachments_controller.rb +8 -4
- data/app/controllers/alchemy/admin/base_controller.rb +5 -7
- data/app/controllers/alchemy/admin/elements_controller.rb +59 -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 +7 -22
- data/app/controllers/alchemy/admin/pictures_controller.rb +56 -17
- data/app/controllers/alchemy/admin/resources_controller.rb +84 -10
- data/app/controllers/alchemy/api/elements_controller.rb +13 -4
- data/app/controllers/alchemy/api/pages_controller.rb +4 -3
- 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 +26 -1
- data/app/decorators/alchemy/ingredient_editor.rb +158 -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 +23 -6
- 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 +24 -7
- data/app/models/alchemy/content.rb +1 -6
- data/app/models/alchemy/content/factory.rb +23 -27
- data/app/models/alchemy/element.rb +39 -72
- 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 +111 -98
- data/app/models/alchemy/element/element_ingredients.rb +184 -0
- data/app/models/alchemy/element/presenters.rb +104 -85
- 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 +183 -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/page.rb +76 -33
- 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 +117 -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_version.rb +58 -0
- data/app/models/alchemy/picture.rb +18 -40
- 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/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 +2 -3
- 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/attachments/index.html.erb +2 -3
- 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/create.js.erb +1 -1
- data/app/views/alchemy/admin/elements/{trash.js.erb → destroy.js.erb} +2 -6
- data/app/views/alchemy/admin/elements/fold.js.erb +2 -2
- 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/_toolbar.html.erb +1 -1
- data/app/views/alchemy/admin/pages/edit.html.erb +36 -24
- data/app/views/alchemy/admin/pages/index.html.erb +2 -9
- 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/partials/_search_form.html.erb +9 -0
- data/app/views/alchemy/admin/pictures/_archive.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +5 -7
- 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/pictures/index.html.erb +8 -3
- data/app/views/alchemy/admin/resources/_filter.html.erb +12 -0
- data/app/views/alchemy/admin/resources/_filter_bar.html.erb +14 -17
- data/app/views/alchemy/admin/resources/_form.html.erb +3 -0
- data/app/views/alchemy/admin/resources/_table_header.html.erb +15 -0
- data/app/views/alchemy/admin/resources/index.html.erb +3 -11
- 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_picture_view.html.erb +3 -3
- 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 +52 -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 +26 -0
- data/app/views/alchemy/ingredients/_node_view.html.erb +1 -0
- data/app/views/alchemy/ingredients/_page_editor.html.erb +25 -0
- data/app/views/alchemy/ingredients/_page_view.html.erb +4 -0
- data/app/views/alchemy/ingredients/_picture_editor.html.erb +60 -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 +30 -0
- data/app/views/alchemy/ingredients/_select_view.html.erb +1 -0
- data/app/views/alchemy/ingredients/_text_editor.html.erb +20 -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 +108 -64
- 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/essence.rb +1 -2
- data/lib/alchemy/forms/builder.rb +21 -1
- 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/resource_filter.rb +40 -0
- data/lib/alchemy/resources_helper.rb +1 -16
- data/lib/alchemy/tasks/tidy.rb +29 -0
- data/lib/alchemy/test_support.rb +2 -11
- 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 +75 -0
- data/lib/alchemy/tinymce.rb +17 -0
- 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 +62 -0
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy_cms.rb +1 -0
- 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/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/thumbnails.rake +4 -2
- data/lib/tasks/alchemy/tidy.rake +12 -0
- data/lib/tasks/alchemy/upgrade.rake +26 -0
- data/package.json +3 -2
- data/package/admin.js +11 -1
- data/package/src/__tests__/i18n.spec.js +23 -0
- data/package/src/file_editors.js +28 -0
- data/package/src/i18n.js +1 -3
- 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/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +3 -18
- data/vendor/assets/stylesheets/jquery.Jcrop.min.scss +2 -28
- metadata +292 -55
- 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/attachments/_filter_bar.html.erb +0 -29
- 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/pictures/_filter_bar.html.erb +0 -30
- 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 -16
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
class Element < BaseRecord
|
|
5
|
+
# Methods concerning ingredients for elements
|
|
6
|
+
#
|
|
7
|
+
module ElementIngredients
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
attr_accessor :autogenerate_ingredients
|
|
12
|
+
|
|
13
|
+
has_many :ingredients,
|
|
14
|
+
class_name: "Alchemy::Ingredient",
|
|
15
|
+
inverse_of: :element,
|
|
16
|
+
dependent: :destroy
|
|
17
|
+
|
|
18
|
+
before_create :build_ingredients,
|
|
19
|
+
unless: -> { autogenerate_ingredients == false }
|
|
20
|
+
|
|
21
|
+
accepts_nested_attributes_for :ingredients
|
|
22
|
+
validates_associated :ingredients, on: :update
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The value of an ingredient of the element by role
|
|
26
|
+
def value_for(role)
|
|
27
|
+
ingredient_by_role(role)&.value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Find first ingredient from element by given role.
|
|
31
|
+
def ingredient_by_role(role)
|
|
32
|
+
ingredients.detect { |ingredient| ingredient.role == role.to_s }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Find first ingredient from element by given type.
|
|
36
|
+
def ingredient_by_type(type)
|
|
37
|
+
ingredients_by_type(type).first
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# All ingredients from element by given type.
|
|
41
|
+
def ingredients_by_type(type)
|
|
42
|
+
ingredients.select do |ingredient|
|
|
43
|
+
ingredient.type == Ingredient.normalize_type(type)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Copy current ingredient's ingredients to given target element
|
|
48
|
+
def copy_ingredients_to(element)
|
|
49
|
+
ingredients.map do |ingredient|
|
|
50
|
+
Ingredient.copy(ingredient, element_id: element.id)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns all element ingredient definitions from the +elements.yml+ file
|
|
55
|
+
def ingredient_definitions
|
|
56
|
+
definition.fetch(:ingredients, [])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns the definition for given ingredient role
|
|
60
|
+
def ingredient_definition_for(role)
|
|
61
|
+
if ingredient_definitions.blank?
|
|
62
|
+
log_warning "Element #{name} is missing the ingredient definition for #{role}"
|
|
63
|
+
nil
|
|
64
|
+
else
|
|
65
|
+
ingredient_definitions.find { |d| d[:role] == role.to_s }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns an array of all Richtext ingredients ids from elements
|
|
70
|
+
#
|
|
71
|
+
# This is used to re-initialize the TinyMCE editor in the element editor.
|
|
72
|
+
#
|
|
73
|
+
def richtext_ingredients_ids
|
|
74
|
+
ids = ingredients.select(&:has_tinymce?).collect(&:id)
|
|
75
|
+
expanded_nested_elements = nested_elements.expanded
|
|
76
|
+
if expanded_nested_elements.present?
|
|
77
|
+
ids += expanded_nested_elements.collect(&:richtext_ingredients_ids)
|
|
78
|
+
end
|
|
79
|
+
ids.flatten
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Has any of the ingredients validations defined?
|
|
83
|
+
def has_validations?
|
|
84
|
+
ingredients.any?(&:has_validations?)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# All element ingredients where the validation has failed.
|
|
88
|
+
def ingredients_with_errors
|
|
89
|
+
ingredients.select { |i| i.errors.any? }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# True if the element has a ingredient for given name
|
|
93
|
+
# that has a non blank value.
|
|
94
|
+
def has_value_for?(role)
|
|
95
|
+
value_for(role).present?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Ingredient validation error messages
|
|
99
|
+
#
|
|
100
|
+
# == Error messages are translated via I18n
|
|
101
|
+
#
|
|
102
|
+
# Inside your translation file add translations like:
|
|
103
|
+
#
|
|
104
|
+
# alchemy:
|
|
105
|
+
# ingredient_validations:
|
|
106
|
+
# name_of_the_element:
|
|
107
|
+
# role_of_the_ingredient:
|
|
108
|
+
# validation_error_type: Error Message
|
|
109
|
+
#
|
|
110
|
+
# NOTE: +validation_error_type+ has to be one of:
|
|
111
|
+
#
|
|
112
|
+
# * blank
|
|
113
|
+
# * taken
|
|
114
|
+
# * invalid
|
|
115
|
+
#
|
|
116
|
+
# === Example:
|
|
117
|
+
#
|
|
118
|
+
# de:
|
|
119
|
+
# alchemy:
|
|
120
|
+
# ingredient_validations:
|
|
121
|
+
# contactform:
|
|
122
|
+
# email:
|
|
123
|
+
# invalid: 'Die Email hat nicht das richtige Format'
|
|
124
|
+
#
|
|
125
|
+
#
|
|
126
|
+
# == Error message translation fallbacks
|
|
127
|
+
#
|
|
128
|
+
# In order to not translate every single ingredient for every element
|
|
129
|
+
# you can provide default error messages per content name:
|
|
130
|
+
#
|
|
131
|
+
# === Example
|
|
132
|
+
#
|
|
133
|
+
# en:
|
|
134
|
+
# alchemy:
|
|
135
|
+
# ingredient_validations:
|
|
136
|
+
# fields:
|
|
137
|
+
# email:
|
|
138
|
+
# invalid: E-Mail has wrong format
|
|
139
|
+
# blank: E-Mail can't be blank
|
|
140
|
+
#
|
|
141
|
+
# And even further you can provide general field agnostic error messages:
|
|
142
|
+
#
|
|
143
|
+
# === Example
|
|
144
|
+
#
|
|
145
|
+
# en:
|
|
146
|
+
# alchemy:
|
|
147
|
+
# ingredient_validations:
|
|
148
|
+
# errors:
|
|
149
|
+
# invalid: %{field} has wrong format
|
|
150
|
+
# blank: %{field} can't be blank
|
|
151
|
+
#
|
|
152
|
+
def ingredient_error_messages
|
|
153
|
+
messages = []
|
|
154
|
+
ingredients_with_errors.map { |i| [i.role, i.errors.details] }.each do |role, error_details|
|
|
155
|
+
error_details[:value].each do |error_detail|
|
|
156
|
+
error = error_detail[:error]
|
|
157
|
+
messages << Alchemy.t(
|
|
158
|
+
"#{name}.#{role}.#{error}",
|
|
159
|
+
scope: "ingredient_validations",
|
|
160
|
+
default: [
|
|
161
|
+
"fields.#{role}.#{error}".to_sym,
|
|
162
|
+
"errors.#{error}".to_sym,
|
|
163
|
+
],
|
|
164
|
+
field: Alchemy::Ingredient.translated_label_for(role, name),
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
messages
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
# Builds ingredients for this element as described in the +elements.yml+
|
|
174
|
+
def build_ingredients
|
|
175
|
+
ingredient_definitions.each do |attributes|
|
|
176
|
+
ingredients.build(
|
|
177
|
+
role: attributes[:role],
|
|
178
|
+
type: Alchemy::Ingredient.normalize_type(attributes[:type]),
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -1,108 +1,127 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Alchemy
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
# Human name for displaying elements in select boxes and element editor views.
|
|
31
|
+
# Returns the translated name
|
|
11
32
|
#
|
|
12
|
-
#
|
|
33
|
+
# @see Alchemy::Element::Presenters#display_name_for
|
|
13
34
|
#
|
|
14
|
-
|
|
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
|
-
#
|
|
41
|
+
# It's taken from the first Content found in the +elements.yml+ definition file.
|
|
17
42
|
#
|
|
18
|
-
#
|
|
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
|
-
#
|
|
45
|
+
# @param maxlength [Fixnum] (60)
|
|
46
|
+
# Length of characters after the text will be cut off.
|
|
24
47
|
#
|
|
25
|
-
def
|
|
26
|
-
|
|
48
|
+
def preview_text(maxlength = 60)
|
|
49
|
+
preview_text_from_preview_ingredient(maxlength) ||
|
|
50
|
+
preview_text_from_preview_content(maxlength) ||
|
|
51
|
+
preview_text_from_nested_elements(maxlength)
|
|
27
52
|
end
|
|
28
|
-
end
|
|
29
53
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
# Generates a preview text containing Element#display_name and Element#preview_text.
|
|
55
|
+
#
|
|
56
|
+
# It is displayed inside the head of the Element in the Elements.list overlay window from the Alchemy Admin::Page#edit view.
|
|
57
|
+
#
|
|
58
|
+
# === Example
|
|
59
|
+
#
|
|
60
|
+
# A Element described as:
|
|
61
|
+
#
|
|
62
|
+
# - name: funky_element
|
|
63
|
+
# display_name: Funky Element
|
|
64
|
+
# contents:
|
|
65
|
+
# - name: headline
|
|
66
|
+
# type: EssenceText
|
|
67
|
+
# - name: text
|
|
68
|
+
# type EssenceRichtext
|
|
69
|
+
# as_element_title: true
|
|
70
|
+
#
|
|
71
|
+
# With "I want to tell you a funky story" as stripped_body for the EssenceRichtext Content produces:
|
|
72
|
+
#
|
|
73
|
+
# Funky Element: I want to tell ...
|
|
74
|
+
#
|
|
75
|
+
# @param maxlength [Fixnum] (30)
|
|
76
|
+
# Length of characters after the text will be cut off.
|
|
77
|
+
#
|
|
78
|
+
def display_name_with_preview_text(maxlength = 30)
|
|
79
|
+
"#{display_name}: #{preview_text(maxlength)}"
|
|
80
|
+
end
|
|
37
81
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
#
|
|
44
|
-
# @param maxlength [Fixnum] (60)
|
|
45
|
-
# Length of characters after the text will be cut off.
|
|
46
|
-
#
|
|
47
|
-
def preview_text(maxlength = 60)
|
|
48
|
-
preview_text_from_preview_content(maxlength) || preview_text_from_nested_elements(maxlength)
|
|
49
|
-
end
|
|
82
|
+
# Returns a dom id used for elements html id tag.
|
|
83
|
+
#
|
|
84
|
+
def dom_id
|
|
85
|
+
"#{name}_#{id}"
|
|
86
|
+
end
|
|
50
87
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# contents:
|
|
62
|
-
# - name: headline
|
|
63
|
-
# type: EssenceText
|
|
64
|
-
# - name: text
|
|
65
|
-
# type EssenceRichtext
|
|
66
|
-
# as_element_title: true
|
|
67
|
-
#
|
|
68
|
-
# With "I want to tell you a funky story" as stripped_body for the EssenceRichtext Content produces:
|
|
69
|
-
#
|
|
70
|
-
# Funky Element: I want to tell ...
|
|
71
|
-
#
|
|
72
|
-
# @param maxlength [Fixnum] (30)
|
|
73
|
-
# Length of characters after the text will be cut off.
|
|
74
|
-
#
|
|
75
|
-
def display_name_with_preview_text(maxlength = 30)
|
|
76
|
-
"#{display_name}: #{preview_text(maxlength)}"
|
|
77
|
-
end
|
|
88
|
+
# The content that's used for element's preview text.
|
|
89
|
+
#
|
|
90
|
+
# It tries to find one of element's contents that is defined +as_element_title+.
|
|
91
|
+
# Takes element's first content if no content is defined +as_element_title+.
|
|
92
|
+
#
|
|
93
|
+
# @return (Alchemy::Content)
|
|
94
|
+
#
|
|
95
|
+
def preview_content
|
|
96
|
+
@_preview_content ||= contents.detect(&:preview_content?) || contents.first
|
|
97
|
+
end
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
99
|
+
# The ingredient that's used for element's preview text.
|
|
100
|
+
#
|
|
101
|
+
# It tries to find one of element's ingredients that is defined +as_element_title+.
|
|
102
|
+
# Takes element's first ingredient if no ingredient is defined +as_element_title+.
|
|
103
|
+
#
|
|
104
|
+
# @return (Alchemy::Ingredient)
|
|
105
|
+
#
|
|
106
|
+
def preview_ingredient
|
|
107
|
+
@_preview_ingredient ||= ingredients.detect(&:preview_ingredient?) || ingredients.first
|
|
108
|
+
end
|
|
84
109
|
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
# It tries to find one of element's contents that is defined +as_element_title+.
|
|
88
|
-
# Takes element's first content if no content is defined +as_element_title+.
|
|
89
|
-
#
|
|
90
|
-
# @return (Alchemy::Content)
|
|
91
|
-
#
|
|
92
|
-
def preview_content
|
|
93
|
-
@_preview_content ||= contents.detect(&:preview_content?) || contents.first
|
|
94
|
-
end
|
|
110
|
+
private
|
|
95
111
|
|
|
96
|
-
|
|
112
|
+
def preview_text_from_nested_elements(maxlength)
|
|
113
|
+
return if all_nested_elements.empty?
|
|
97
114
|
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
all_nested_elements.first.preview_text(maxlength)
|
|
116
|
+
end
|
|
100
117
|
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
def preview_text_from_preview_content(maxlength)
|
|
119
|
+
preview_content.try!(:preview_text, maxlength)
|
|
120
|
+
end
|
|
103
121
|
|
|
104
|
-
|
|
105
|
-
|
|
122
|
+
def preview_text_from_preview_ingredient(maxlength)
|
|
123
|
+
preview_ingredient&.preview_text(maxlength)
|
|
124
|
+
end
|
|
106
125
|
end
|
|
107
126
|
end
|
|
108
127
|
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# Mimics ActiveRecord query interface
|
|
5
|
+
# but does this on the preloaded elements
|
|
6
|
+
class ElementsRepository
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
# An empty set of elements
|
|
10
|
+
def self.none
|
|
11
|
+
new([])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param [ActiveRecord::Relation]
|
|
15
|
+
def initialize(elements)
|
|
16
|
+
@elements = elements.to_a
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# All visible elements
|
|
20
|
+
# @return [Alchemy::ElementRepository]
|
|
21
|
+
def visible
|
|
22
|
+
self.class.new select(&:public)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# All not fixed elements
|
|
26
|
+
# @return [Alchemy::ElementRepository]
|
|
27
|
+
def hidden
|
|
28
|
+
self.class.new reject(&:public)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# All elements with given name(s)
|
|
32
|
+
# @param [Array<String|Symbol>|String|Symbol]
|
|
33
|
+
# @return [Alchemy::ElementRepository]
|
|
34
|
+
def named(*names)
|
|
35
|
+
names.flatten!
|
|
36
|
+
self.class.new(select { |e| e.name.in?(names.map!(&:to_s)) })
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Filter elements by given attribute and value
|
|
40
|
+
# @param [Array|Hash]
|
|
41
|
+
# @return [Alchemy::ElementRepository]
|
|
42
|
+
def where(attrs)
|
|
43
|
+
self.class.new(
|
|
44
|
+
select do |element|
|
|
45
|
+
attrs.all? do |attr, value|
|
|
46
|
+
element.public_send(attr) == value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# All elements excluding those wth given name(s)
|
|
53
|
+
# @param [Array<String|Symbol>|String|Symbol]
|
|
54
|
+
# @return [Alchemy::ElementRepository]
|
|
55
|
+
def excluded(*names)
|
|
56
|
+
names.flatten!
|
|
57
|
+
self.class.new(reject { |e| e.name.in?(names.map!(&:to_s)) })
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# All fixed elements
|
|
61
|
+
# @return [Alchemy::ElementRepository]
|
|
62
|
+
def fixed
|
|
63
|
+
self.class.new select(&:fixed)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# All not fixed elements
|
|
67
|
+
# @return [Alchemy::ElementRepository]
|
|
68
|
+
def unfixed
|
|
69
|
+
self.class.new reject(&:fixed)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# All folded elements
|
|
73
|
+
# @return [Alchemy::ElementRepository]
|
|
74
|
+
def folded
|
|
75
|
+
self.class.new select(&:folded)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# All expanded elements
|
|
79
|
+
# @return [Alchemy::ElementRepository]
|
|
80
|
+
def expanded
|
|
81
|
+
self.class.new reject(&:folded)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# All not nested top level elements
|
|
85
|
+
# @return [Alchemy::ElementRepository]
|
|
86
|
+
def not_nested
|
|
87
|
+
self.class.new(select { |e| e.parent_element_id.nil? })
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Elements in reversed order
|
|
91
|
+
# @return [Alchemy::ElementRepository]
|
|
92
|
+
def reverse
|
|
93
|
+
self.class.new elements.reverse
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Elements in random order
|
|
97
|
+
# @return [Alchemy::ElementRepository]
|
|
98
|
+
def random
|
|
99
|
+
self.class.new Array(elements).shuffle
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Elements off setted by
|
|
103
|
+
# @return [Alchemy::ElementRepository]
|
|
104
|
+
def offset(offset)
|
|
105
|
+
self.class.new elements[offset.to_i..-1]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Elements limitted by
|
|
109
|
+
# @return [Alchemy::ElementRepository]
|
|
110
|
+
def limit(limit)
|
|
111
|
+
self.class.new elements[0..(limit.to_i - 1)]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def children_of(parent)
|
|
115
|
+
self.class.new(select { |e| e.parent_element_id == parent.id })
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def each(&blk)
|
|
119
|
+
elements.each(&blk)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
attr_reader :elements
|
|
125
|
+
end
|
|
126
|
+
end
|