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,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
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
|
|
24
24
|
module Alchemy
|
|
25
25
|
class EssencePicture < BaseRecord
|
|
26
|
+
include Alchemy::PictureThumbnails
|
|
27
|
+
|
|
26
28
|
acts_as_essence ingredient_column: :picture, belongs_to: {
|
|
27
29
|
class_name: "Alchemy::Picture",
|
|
28
30
|
foreign_key: :picture_id,
|
|
@@ -30,81 +32,9 @@ module Alchemy
|
|
|
30
32
|
optional: true,
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
delegate :
|
|
34
|
-
before_save :fix_crop_values
|
|
35
|
-
before_save :replace_newlines
|
|
36
|
-
|
|
37
|
-
include Alchemy::Picture::Transformations
|
|
38
|
-
|
|
39
|
-
# The url to show the picture.
|
|
40
|
-
#
|
|
41
|
-
# Takes all values like +name+ and crop sizes (+crop_from+, +crop_size+ from the build in graphical image cropper)
|
|
42
|
-
# and also adds the security token.
|
|
43
|
-
#
|
|
44
|
-
# You typically want to set the size the picture should be resized to.
|
|
45
|
-
#
|
|
46
|
-
# === Example:
|
|
47
|
-
#
|
|
48
|
-
# essence_picture.picture_url(size: '200x300', crop: true, format: 'gif')
|
|
49
|
-
# # '/pictures/1/show/200x300/crop/cats.gif?sh=765rfghj'
|
|
50
|
-
#
|
|
51
|
-
# @option options size [String]
|
|
52
|
-
# The size the picture should be resized to.
|
|
53
|
-
#
|
|
54
|
-
# @option options format [String]
|
|
55
|
-
# The format the picture should be rendered in.
|
|
56
|
-
# Defaults to the +image_output_format+ from the +Alchemy::Config+.
|
|
57
|
-
#
|
|
58
|
-
# @option options crop [Boolean]
|
|
59
|
-
# If set to true the picture will be cropped to fit the size value.
|
|
60
|
-
#
|
|
61
|
-
# @return [String]
|
|
62
|
-
def picture_url(options = {})
|
|
63
|
-
return if picture.nil?
|
|
64
|
-
|
|
65
|
-
picture.url(picture_url_options.merge(options)) || "missing-image.png"
|
|
66
|
-
end
|
|
35
|
+
delegate :settings, to: :content
|
|
67
36
|
|
|
68
|
-
|
|
69
|
-
#
|
|
70
|
-
# Returns the +default_render_format+ of the associated +Alchemy::Picture+
|
|
71
|
-
# together with the +crop_from+ and +crop_size+ values
|
|
72
|
-
#
|
|
73
|
-
# @return [HashWithIndifferentAccess]
|
|
74
|
-
def picture_url_options
|
|
75
|
-
return {} if picture.nil?
|
|
76
|
-
|
|
77
|
-
{
|
|
78
|
-
format: picture.default_render_format,
|
|
79
|
-
crop_from: crop_from.presence,
|
|
80
|
-
crop_size: crop_size.presence,
|
|
81
|
-
size: content.settings[:size],
|
|
82
|
-
}.with_indifferent_access
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Returns an url for the thumbnail representation of the assigned picture
|
|
86
|
-
#
|
|
87
|
-
# It takes cropping values into account, so it always represents the current
|
|
88
|
-
# image displayed in the frontend.
|
|
89
|
-
#
|
|
90
|
-
# @return [String]
|
|
91
|
-
def thumbnail_url
|
|
92
|
-
return if picture.nil?
|
|
93
|
-
|
|
94
|
-
crop = crop_values_present? || content.settings[:crop]
|
|
95
|
-
size = render_size || content.settings[:size]
|
|
96
|
-
|
|
97
|
-
options = {
|
|
98
|
-
size: thumbnail_size(size, crop),
|
|
99
|
-
crop: !!crop,
|
|
100
|
-
crop_from: crop_from.presence,
|
|
101
|
-
crop_size: crop_size.presence,
|
|
102
|
-
flatten: true,
|
|
103
|
-
format: picture.image_file_format,
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
picture.url(options) || "alchemy/missing-image.svg"
|
|
107
|
-
end
|
|
37
|
+
before_save :replace_newlines
|
|
108
38
|
|
|
109
39
|
# The name of the picture used as preview text in element editor views.
|
|
110
40
|
#
|
|
@@ -118,18 +48,6 @@ module Alchemy
|
|
|
118
48
|
picture.name.to_s[0..max - 1]
|
|
119
49
|
end
|
|
120
50
|
|
|
121
|
-
# A Hash of coordinates suitable for the graphical image cropper.
|
|
122
|
-
#
|
|
123
|
-
# @return [Hash]
|
|
124
|
-
def cropping_mask
|
|
125
|
-
return if crop_from.blank? || crop_size.blank?
|
|
126
|
-
|
|
127
|
-
crop_from = point_from_string(read_attribute(:crop_from))
|
|
128
|
-
crop_size = sizes_from_string(read_attribute(:crop_size))
|
|
129
|
-
|
|
130
|
-
point_and_mask_to_points(crop_from, crop_size)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
51
|
# Returns a serialized ingredient value for json api
|
|
134
52
|
#
|
|
135
53
|
# @return [String]
|
|
@@ -137,38 +55,8 @@ module Alchemy
|
|
|
137
55
|
picture_url(content.settings)
|
|
138
56
|
end
|
|
139
57
|
|
|
140
|
-
# Show image cropping link for content
|
|
141
|
-
def allow_image_cropping?
|
|
142
|
-
content && content.settings[:crop] && picture &&
|
|
143
|
-
picture.can_be_cropped_to?(
|
|
144
|
-
content.settings[:size],
|
|
145
|
-
content.settings[:upsample],
|
|
146
|
-
) && !!picture.image_file
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def crop_values_present?
|
|
150
|
-
crop_from.present? && crop_size.present?
|
|
151
|
-
end
|
|
152
|
-
|
|
153
58
|
private
|
|
154
59
|
|
|
155
|
-
def fix_crop_values
|
|
156
|
-
%i(crop_from crop_size).each do |crop_value|
|
|
157
|
-
if self[crop_value].is_a?(String)
|
|
158
|
-
write_attribute crop_value, normalize_crop_value(crop_value)
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def normalize_crop_value(crop_value)
|
|
164
|
-
self[crop_value].split("x").map { |n| normalize_number(n) }.join("x")
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def normalize_number(number)
|
|
168
|
-
number = number.to_f.round
|
|
169
|
-
number.negative? ? 0 : number
|
|
170
|
-
end
|
|
171
|
-
|
|
172
60
|
def replace_newlines
|
|
173
61
|
return nil if caption.nil?
|
|
174
62
|
|
|
@@ -17,6 +17,7 @@ module Alchemy
|
|
|
17
17
|
acts_as_essence preview_text_column: "stripped_body"
|
|
18
18
|
|
|
19
19
|
before_save :strip_content
|
|
20
|
+
before_save :sanitize_content
|
|
20
21
|
|
|
21
22
|
def has_tinymce?
|
|
22
23
|
true
|
|
@@ -27,5 +28,16 @@ module Alchemy
|
|
|
27
28
|
def strip_content
|
|
28
29
|
self.stripped_body = Rails::Html::FullSanitizer.new.sanitize(body)
|
|
29
30
|
end
|
|
31
|
+
|
|
32
|
+
def sanitize_content
|
|
33
|
+
self.sanitized_body = Rails::Html::SafeListSanitizer.new.sanitize(
|
|
34
|
+
body,
|
|
35
|
+
content_sanitizer_settings
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def content_sanitizer_settings
|
|
40
|
+
content&.settings&.fetch(:sanitizer, {}) || {}
|
|
41
|
+
end
|
|
30
42
|
end
|
|
31
43
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# Settings for the graphical JS image cropper
|
|
5
|
+
class ImageCropperSettings
|
|
6
|
+
attr_reader :render_size, :default_crop_from, :default_crop_size, :fixed_ratio, :image_width, :image_height
|
|
7
|
+
|
|
8
|
+
def initialize(render_size:, default_crop_from:, default_crop_size:, fixed_ratio:, image_width:, image_height:)
|
|
9
|
+
@render_size = render_size || [0, 0]
|
|
10
|
+
@fixed_ratio = fixed_ratio
|
|
11
|
+
@image_width = image_width.to_i
|
|
12
|
+
@image_height = image_height.to_i
|
|
13
|
+
@default_crop_from = default_crop_from || [0, 0]
|
|
14
|
+
@default_crop_size = default_crop_size || [@image_width, @image_height]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
return {} if image_width.zero? || image_height.zero?
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
min_size: large_enough? ? min_size : false,
|
|
22
|
+
ratio: ratio,
|
|
23
|
+
default_box: default_box,
|
|
24
|
+
image_size: [image_width, image_height],
|
|
25
|
+
}.freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def [](key)
|
|
29
|
+
to_h[key]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def ratio
|
|
35
|
+
return false if fixed_ratio == false
|
|
36
|
+
return ratio_from_size if fixed_ratio.nil?
|
|
37
|
+
|
|
38
|
+
Float(fixed_ratio)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Only returns an array of width and height if image is large enough
|
|
42
|
+
# or false to disable min size option of the image cropper
|
|
43
|
+
def large_enough?
|
|
44
|
+
return true if render_size.any?(&:zero?)
|
|
45
|
+
|
|
46
|
+
image_width >= render_size[0] && image_height >= render_size[1]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Infers the aspect ratio from size or fixed_ratio. If you don't want a fixed
|
|
50
|
+
# aspect ratio, don't specify a size or only width or height.
|
|
51
|
+
#
|
|
52
|
+
def ratio_from_size
|
|
53
|
+
if render_size.none?(&:zero?)
|
|
54
|
+
render_size[0].to_f / render_size[1]
|
|
55
|
+
elsif [image_width, image_height].none?(&:zero?)
|
|
56
|
+
image_width.to_f / image_height
|
|
57
|
+
else
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Infers the minimum width or height
|
|
63
|
+
# if the aspect ratio and one dimension is specified.
|
|
64
|
+
#
|
|
65
|
+
def min_size
|
|
66
|
+
return render_size unless ratio
|
|
67
|
+
|
|
68
|
+
if render_size[1].zero?
|
|
69
|
+
[render_size[0], (render_size[0] / ratio).to_i]
|
|
70
|
+
else
|
|
71
|
+
[(render_size[1] * ratio).to_i, render_size[1]]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Given a point and a mask, this function returns the area on the
|
|
76
|
+
# underlying canvas as a Hash of two points
|
|
77
|
+
#
|
|
78
|
+
def default_box
|
|
79
|
+
[
|
|
80
|
+
default_crop_from[0],
|
|
81
|
+
default_crop_from[1],
|
|
82
|
+
default_crop_from[0] + default_crop_size[0],
|
|
83
|
+
default_crop_from[1] + default_crop_size[1],
|
|
84
|
+
]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
class Ingredient < BaseRecord
|
|
5
|
+
class DefinitionError < StandardError; end
|
|
6
|
+
|
|
7
|
+
include Hints
|
|
8
|
+
|
|
9
|
+
self.table_name = "alchemy_ingredients"
|
|
10
|
+
|
|
11
|
+
belongs_to :element, touch: true, class_name: "Alchemy::Element", inverse_of: :ingredients
|
|
12
|
+
belongs_to :related_object, polymorphic: true, optional: true
|
|
13
|
+
|
|
14
|
+
before_validation(on: :create) { self.value ||= default_value }
|
|
15
|
+
|
|
16
|
+
validates :type, presence: true
|
|
17
|
+
validates :role, presence: true
|
|
18
|
+
|
|
19
|
+
validates_with Alchemy::IngredientValidator, on: :update, if: :has_validations?
|
|
20
|
+
|
|
21
|
+
scope :audios, -> { where(type: "Alchemy::Ingredients::Audio") }
|
|
22
|
+
scope :booleans, -> { where(type: "Alchemy::Ingredients::Boolean") }
|
|
23
|
+
scope :datetimes, -> { where(type: "Alchemy::Ingredients::Datetime") }
|
|
24
|
+
scope :files, -> { where(type: "Alchemy::Ingredients::File") }
|
|
25
|
+
scope :headlines, -> { where(type: "Alchemy::Ingredients::Headline") }
|
|
26
|
+
scope :htmls, -> { where(type: "Alchemy::Ingredients::Html") }
|
|
27
|
+
scope :links, -> { where(type: "Alchemy::Ingredients::Link") }
|
|
28
|
+
scope :nodes, -> { where(type: "Alchemy::Ingredients::Node") }
|
|
29
|
+
scope :pages, -> { where(type: "Alchemy::Ingredients::Page") }
|
|
30
|
+
scope :pictures, -> { where(type: "Alchemy::Ingredients::Picture") }
|
|
31
|
+
scope :richtexts, -> { where(type: "Alchemy::Ingredients::Richtext") }
|
|
32
|
+
scope :selects, -> { where(type: "Alchemy::Ingredients::Select") }
|
|
33
|
+
scope :texts, -> { where(type: "Alchemy::Ingredients::Text") }
|
|
34
|
+
scope :videos, -> { where(type: "Alchemy::Ingredients::Video") }
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# Defines getter and setter method aliases for related object
|
|
38
|
+
#
|
|
39
|
+
# @param [String|Symbol] The name of the alias
|
|
40
|
+
# @param [String] The class name of the related object
|
|
41
|
+
def related_object_alias(name, class_name:)
|
|
42
|
+
alias_method name, :related_object
|
|
43
|
+
alias_method "#{name}=", :related_object=
|
|
44
|
+
|
|
45
|
+
# Somehow Rails STI does not allow us to use `alias_method` for the related_object_id
|
|
46
|
+
define_method "#{name}_id" do
|
|
47
|
+
related_object_id
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
define_method "#{name}_id=" do |id|
|
|
51
|
+
self.related_object_id = id
|
|
52
|
+
self.related_object_type = class_name
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Modulize ingredient type
|
|
57
|
+
#
|
|
58
|
+
# Makes sure the passed ingredient type is in the +Alchemy::Ingredients+
|
|
59
|
+
# module namespace.
|
|
60
|
+
#
|
|
61
|
+
# If you add custom ingredient class,
|
|
62
|
+
# put them in the +Alchemy::Ingredients+ module namespace
|
|
63
|
+
# @param [String] Ingredient class name
|
|
64
|
+
# @return [String]
|
|
65
|
+
def normalize_type(ingredient_type)
|
|
66
|
+
"Alchemy::Ingredients::#{ingredient_type.to_s.classify.demodulize}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def translated_label_for(role, element_name = nil)
|
|
70
|
+
Alchemy.t(
|
|
71
|
+
role,
|
|
72
|
+
scope: "ingredient_roles.#{element_name}",
|
|
73
|
+
default: Alchemy.t("ingredient_roles.#{role}", default: role.humanize),
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Compatibility method for access from element
|
|
79
|
+
def essence
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The value or the related object if present
|
|
84
|
+
def value
|
|
85
|
+
related_object || self[:value]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Settings for this ingredient from the +elements.yml+ definition.
|
|
89
|
+
def settings
|
|
90
|
+
definition[:settings] || {}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Fetches value from settings
|
|
94
|
+
#
|
|
95
|
+
# @param key [Symbol] - The hash key you want to fetch the value from
|
|
96
|
+
# @param options [Hash] - An optional Hash that can override the settings.
|
|
97
|
+
# Normally passed as options hash into the content
|
|
98
|
+
# editor view.
|
|
99
|
+
def settings_value(key, options = {})
|
|
100
|
+
settings.merge(options || {})[key.to_sym]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Definition hash for this ingredient from +elements.yml+ file.
|
|
104
|
+
#
|
|
105
|
+
def definition
|
|
106
|
+
return {} unless element
|
|
107
|
+
|
|
108
|
+
element.ingredient_definition_for(role) || {}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The first 30 characters of the value
|
|
112
|
+
#
|
|
113
|
+
# Used by the Element#preview_text method.
|
|
114
|
+
#
|
|
115
|
+
# @param [Integer] max_length (30)
|
|
116
|
+
#
|
|
117
|
+
def preview_text(maxlength = 30)
|
|
118
|
+
value.to_s[0..maxlength - 1]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Cross DB adapter data accessor that works
|
|
122
|
+
def data
|
|
123
|
+
@_data ||= (self[:data] || {}).with_indifferent_access
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The path to the view partial of the ingredient
|
|
127
|
+
# @return [String]
|
|
128
|
+
def to_partial_path
|
|
129
|
+
"alchemy/ingredients/#{partial_name}_view"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The demodulized underscored class name of the ingredient
|
|
133
|
+
# @return [String]
|
|
134
|
+
def partial_name
|
|
135
|
+
self.class.name.demodulize.underscore
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @return [Boolean]
|
|
139
|
+
def has_validations?
|
|
140
|
+
!!definition[:validate]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @return [Boolean]
|
|
144
|
+
def has_hint?
|
|
145
|
+
!!definition[:hint]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @return [Boolean]
|
|
149
|
+
def deprecated?
|
|
150
|
+
!!definition[:deprecated]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# @return [Boolean]
|
|
154
|
+
def has_tinymce?
|
|
155
|
+
false
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# @return [Boolean]
|
|
159
|
+
def preview_ingredient?
|
|
160
|
+
!!definition[:as_element_title]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def hint_translation_attribute
|
|
166
|
+
role
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Returns the default value from ingredient definition
|
|
170
|
+
#
|
|
171
|
+
# If the value is a symbol it gets passed through i18n
|
|
172
|
+
# inside the +alchemy.default_ingredient_texts+ scope
|
|
173
|
+
def default_value
|
|
174
|
+
default = definition[:default]
|
|
175
|
+
case default
|
|
176
|
+
when Symbol
|
|
177
|
+
Alchemy.t(default, scope: :default_ingredient_texts)
|
|
178
|
+
else
|
|
179
|
+
default
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|