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
data/app/models/alchemy/page.rb
CHANGED
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
# locked_at :datetime
|
|
36
36
|
#
|
|
37
37
|
|
|
38
|
+
require_dependency "alchemy/page/fixed_attributes"
|
|
39
|
+
require_dependency "alchemy/page/page_scopes"
|
|
40
|
+
require_dependency "alchemy/page/page_natures"
|
|
41
|
+
require_dependency "alchemy/page/page_naming"
|
|
42
|
+
require_dependency "alchemy/page/page_elements"
|
|
43
|
+
|
|
38
44
|
module Alchemy
|
|
39
45
|
class Page < BaseRecord
|
|
40
46
|
include Alchemy::Hints
|
|
@@ -82,7 +88,7 @@ module Alchemy
|
|
|
82
88
|
|
|
83
89
|
acts_as_nested_set(dependent: :destroy, scope: [:layoutpage, :language_id])
|
|
84
90
|
|
|
85
|
-
stampable stamper_class_name: Alchemy.
|
|
91
|
+
stampable stamper_class_name: Alchemy.user_class_name
|
|
86
92
|
|
|
87
93
|
belongs_to :language
|
|
88
94
|
|
|
@@ -109,6 +115,9 @@ module Alchemy
|
|
|
109
115
|
has_many :folded_pages
|
|
110
116
|
has_many :legacy_urls, class_name: "Alchemy::LegacyPageUrl"
|
|
111
117
|
has_many :nodes, class_name: "Alchemy::Node", inverse_of: :page
|
|
118
|
+
has_many :versions, class_name: "Alchemy::PageVersion", inverse_of: :page, dependent: :destroy
|
|
119
|
+
has_one :draft_version, -> { drafts }, class_name: "Alchemy::PageVersion"
|
|
120
|
+
has_one :public_version, -> { published }, class_name: "Alchemy::PageVersion"
|
|
112
121
|
|
|
113
122
|
before_validation :set_language,
|
|
114
123
|
if: -> { language.nil? }
|
|
@@ -117,6 +126,9 @@ module Alchemy
|
|
|
117
126
|
validates_format_of :page_layout, with: /\A[a-z0-9_-]+\z/, unless: -> { page_layout.blank? }
|
|
118
127
|
validates_presence_of :parent, unless: -> { layoutpage? || language_root? }
|
|
119
128
|
|
|
129
|
+
before_create -> { versions.build },
|
|
130
|
+
if: -> { versions.none? }
|
|
131
|
+
|
|
120
132
|
before_save :set_language_code,
|
|
121
133
|
if: -> { language.present? }
|
|
122
134
|
|
|
@@ -126,9 +138,6 @@ module Alchemy
|
|
|
126
138
|
before_save :inherit_restricted_status,
|
|
127
139
|
if: -> { parent && parent.restricted? }
|
|
128
140
|
|
|
129
|
-
before_save :set_published_at,
|
|
130
|
-
if: -> { public_on.present? && published_at.nil? }
|
|
131
|
-
|
|
132
141
|
before_save :set_fixed_attributes,
|
|
133
142
|
if: -> { fixed_attributes.any? }
|
|
134
143
|
|
|
@@ -138,14 +147,22 @@ module Alchemy
|
|
|
138
147
|
after_update -> { nodes.update_all(updated_at: Time.current) }
|
|
139
148
|
|
|
140
149
|
# Concerns
|
|
141
|
-
include
|
|
142
|
-
include
|
|
143
|
-
include
|
|
144
|
-
include
|
|
150
|
+
include PageScopes
|
|
151
|
+
include PageNatures
|
|
152
|
+
include PageNaming
|
|
153
|
+
include PageElements
|
|
145
154
|
|
|
146
155
|
# site_name accessor
|
|
147
156
|
delegate :name, to: :site, prefix: true, allow_nil: true
|
|
148
157
|
|
|
158
|
+
# Old public_on and public_until attributes for historical reasons
|
|
159
|
+
#
|
|
160
|
+
# These attributes now exist on the page versions
|
|
161
|
+
#
|
|
162
|
+
attr_readonly :legacy_public_on, :legacy_public_until
|
|
163
|
+
deprecate :legacy_public_on, deprecator: Alchemy::Deprecation
|
|
164
|
+
deprecate :legacy_public_until, deprecator: Alchemy::Deprecation
|
|
165
|
+
|
|
149
166
|
# Class methods
|
|
150
167
|
#
|
|
151
168
|
class << self
|
|
@@ -207,11 +224,13 @@ module Alchemy
|
|
|
207
224
|
# @return [Alchemy::Page]
|
|
208
225
|
#
|
|
209
226
|
def copy(source, differences = {})
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
227
|
+
transaction do
|
|
228
|
+
page = Alchemy::Page.new(attributes_from_source_for_copy(source, differences))
|
|
229
|
+
page.tag_list = source.tag_list
|
|
230
|
+
if page.save!
|
|
231
|
+
copy_elements(source, page)
|
|
232
|
+
page
|
|
233
|
+
end
|
|
215
234
|
end
|
|
216
235
|
end
|
|
217
236
|
|
|
@@ -291,7 +310,9 @@ module Alchemy
|
|
|
291
310
|
# Instance methods
|
|
292
311
|
#
|
|
293
312
|
|
|
294
|
-
# Returns elements from
|
|
313
|
+
# Returns elements from pages public version.
|
|
314
|
+
#
|
|
315
|
+
# You can pass another page_version to load elements from in the options.
|
|
295
316
|
#
|
|
296
317
|
# @option options [Array<String>|String] :only
|
|
297
318
|
# Returns only elements with given names
|
|
@@ -310,11 +331,14 @@ module Alchemy
|
|
|
310
331
|
# @option options [Class] :finder (Alchemy::ElementsFinder)
|
|
311
332
|
# A class that will return elements from page.
|
|
312
333
|
# Use this for your custom element loading logic.
|
|
334
|
+
# @option options [Alchemy::PageVersion] :page_version
|
|
335
|
+
# A page version to load elements from.
|
|
336
|
+
# Uses the pages public_version by default.
|
|
313
337
|
#
|
|
314
338
|
# @return [ActiveRecord::Relation]
|
|
315
339
|
def find_elements(options = {})
|
|
316
340
|
finder = options[:finder] || Alchemy::ElementsFinder.new(options)
|
|
317
|
-
finder.elements(
|
|
341
|
+
finder.elements(page_version: options[:page_version] || public_version)
|
|
318
342
|
end
|
|
319
343
|
|
|
320
344
|
# = The url_path for this page
|
|
@@ -423,22 +447,36 @@ module Alchemy
|
|
|
423
447
|
end
|
|
424
448
|
end
|
|
425
449
|
|
|
426
|
-
#
|
|
450
|
+
# Creates a public version of the page.
|
|
427
451
|
#
|
|
428
|
-
# Sets
|
|
429
|
-
# and resets +public_until+ to nil
|
|
452
|
+
# Sets the +published_at+ value to current time
|
|
430
453
|
#
|
|
431
454
|
# The +published_at+ attribute is used as +cache_key+.
|
|
432
455
|
#
|
|
433
|
-
def publish!
|
|
434
|
-
current_time
|
|
435
|
-
|
|
436
|
-
published_at: current_time,
|
|
437
|
-
public_on: already_public_for?(current_time) ? public_on : current_time,
|
|
438
|
-
public_until: still_public_for?(current_time) ? public_until : nil,
|
|
439
|
-
)
|
|
456
|
+
def publish!(current_time = Time.current)
|
|
457
|
+
update(published_at: current_time)
|
|
458
|
+
PublishPageJob.perform_later(self, public_on: current_time)
|
|
440
459
|
end
|
|
441
460
|
|
|
461
|
+
# Sets the public_on date on the published version
|
|
462
|
+
#
|
|
463
|
+
# Builds a new version if none exists yet.
|
|
464
|
+
# Destroys public version if empty time is set
|
|
465
|
+
#
|
|
466
|
+
def public_on=(time)
|
|
467
|
+
if public_version && time.blank?
|
|
468
|
+
public_version.destroy!
|
|
469
|
+
# Need to reset the public version on the instance so we do not need to reload
|
|
470
|
+
self.public_version = nil
|
|
471
|
+
elsif public_version
|
|
472
|
+
public_version.public_on = time
|
|
473
|
+
elsif time.present?
|
|
474
|
+
versions.build(public_on: time)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
delegate :public_until=, to: :public_version, allow_nil: true
|
|
479
|
+
|
|
442
480
|
# Updates an Alchemy::Page based on a new ordering to be applied to it
|
|
443
481
|
#
|
|
444
482
|
# Note: Page's urls should not be updated (and a legacy URL created) if nesting is OFF
|
|
@@ -460,7 +498,7 @@ module Alchemy
|
|
|
460
498
|
|
|
461
499
|
# Holds an instance of +FixedAttributes+
|
|
462
500
|
def fixed_attributes
|
|
463
|
-
@_fixed_attributes ||=
|
|
501
|
+
@_fixed_attributes ||= FixedAttributes.new(self)
|
|
464
502
|
end
|
|
465
503
|
|
|
466
504
|
# True if given attribute name is defined as fixed
|
|
@@ -479,12 +517,12 @@ module Alchemy
|
|
|
479
517
|
(editor_roles & user.alchemy_roles).any?
|
|
480
518
|
end
|
|
481
519
|
|
|
482
|
-
# Returns the value of +public_on+ attribute
|
|
520
|
+
# Returns the value of +public_on+ attribute from public version
|
|
483
521
|
#
|
|
484
522
|
# If it's a fixed attribute then the fixed value is returned instead
|
|
485
523
|
#
|
|
486
524
|
def public_on
|
|
487
|
-
attribute_fixed?(:public_on) ? fixed_attributes[:public_on] :
|
|
525
|
+
attribute_fixed?(:public_on) ? fixed_attributes[:public_on] : public_version&.public_on
|
|
488
526
|
end
|
|
489
527
|
|
|
490
528
|
# Returns the value of +public_until+ attribute
|
|
@@ -492,7 +530,7 @@ module Alchemy
|
|
|
492
530
|
# If it's a fixed attribute then the fixed value is returned instead
|
|
493
531
|
#
|
|
494
532
|
def public_until
|
|
495
|
-
attribute_fixed?(:public_until) ? fixed_attributes[:public_until] :
|
|
533
|
+
attribute_fixed?(:public_until) ? fixed_attributes[:public_until] : public_version&.public_until
|
|
496
534
|
end
|
|
497
535
|
|
|
498
536
|
# Returns the name of the creator of this page.
|
|
@@ -556,9 +594,5 @@ module Alchemy
|
|
|
556
594
|
def create_legacy_url
|
|
557
595
|
legacy_urls.find_or_create_by(urlname: urlname_before_last_save)
|
|
558
596
|
end
|
|
559
|
-
|
|
560
|
-
def set_published_at
|
|
561
|
-
self.published_at = Time.current
|
|
562
|
-
end
|
|
563
597
|
end
|
|
564
598
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
class PageVersion < BaseRecord
|
|
5
|
+
belongs_to :page, class_name: "Alchemy::Page", inverse_of: :versions
|
|
6
|
+
|
|
7
|
+
has_many :elements, -> { order(:position) },
|
|
8
|
+
class_name: "Alchemy::Element",
|
|
9
|
+
inverse_of: :page_version
|
|
10
|
+
|
|
11
|
+
scope :drafts, -> { where(public_on: nil).order(updated_at: :desc) }
|
|
12
|
+
scope :published, -> { where.not(public_on: nil).order(public_on: :desc) }
|
|
13
|
+
|
|
14
|
+
def self.public_on(time = Time.current)
|
|
15
|
+
where("#{table_name}.public_on <= :time AND " \
|
|
16
|
+
"(#{table_name}.public_until IS NULL " \
|
|
17
|
+
"OR #{table_name}.public_until >= :time)", time: time)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
before_destroy :delete_elements
|
|
21
|
+
|
|
22
|
+
# Determines if this version is public
|
|
23
|
+
#
|
|
24
|
+
# Takes the two timestamps +public_on+ and +public_until+
|
|
25
|
+
# and returns true if the time given (+Time.current+ per default)
|
|
26
|
+
# is in this timespan.
|
|
27
|
+
#
|
|
28
|
+
# @param time [DateTime] (Time.current)
|
|
29
|
+
# @returns Boolean
|
|
30
|
+
def public?(time = Time.current)
|
|
31
|
+
already_public_for?(time) && still_public_for?(time)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Determines if this version is already public for given time
|
|
35
|
+
# @param time [DateTime] (Time.current)
|
|
36
|
+
# @returns Boolean
|
|
37
|
+
def already_public_for?(time = Time.current)
|
|
38
|
+
!public_on.nil? && public_on <= time
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Determines if this version is still public for given time
|
|
42
|
+
# @param time [DateTime] (Time.current)
|
|
43
|
+
# @returns Boolean
|
|
44
|
+
def still_public_for?(time = Time.current)
|
|
45
|
+
public_until.nil? || public_until >= time
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def element_repository
|
|
49
|
+
ElementsRepository.new(elements.includes({ contents: :essence }, :tags))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def delete_elements
|
|
55
|
+
DeleteElements.new(elements).call
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -27,15 +27,9 @@ module Alchemy
|
|
|
27
27
|
# Given a string with an x, this function returns a Hash with point
|
|
28
28
|
# :width and :height.
|
|
29
29
|
#
|
|
30
|
-
def sizes_from_string(string
|
|
31
|
-
|
|
30
|
+
def sizes_from_string(string)
|
|
31
|
+
width, height = string.to_s.split("x", 2).map(&:to_i)
|
|
32
32
|
|
|
33
|
-
raise ArgumentError unless string =~ /(\d*x\d*)/
|
|
34
|
-
|
|
35
|
-
width, height = string.scan(/(\d*)x(\d*)/)[0].map(&:to_i)
|
|
36
|
-
|
|
37
|
-
width = 0 if width.nil?
|
|
38
|
-
height = 0 if height.nil?
|
|
39
33
|
{
|
|
40
34
|
width: width,
|
|
41
35
|
height: height,
|
|
@@ -16,6 +16,8 @@ module Alchemy
|
|
|
16
16
|
def call
|
|
17
17
|
max_image_size = Alchemy::Config.get(:preprocess_image_resize)
|
|
18
18
|
image_file.thumb!(max_image_size) if max_image_size.present?
|
|
19
|
+
# Auto orient the image so EXIF orientation data is taken into account
|
|
20
|
+
image_file.auto_orient!
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
private
|
|
@@ -11,53 +11,16 @@ module Alchemy
|
|
|
11
11
|
include Alchemy::Picture::Calculations
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
THUMBNAIL_WIDTH = 160
|
|
15
|
-
THUMBNAIL_HEIGHT = 120
|
|
16
|
-
|
|
17
|
-
# Returns the default centered image mask for a given size.
|
|
18
|
-
# If the mask is bigger than the image, the mask is scaled down
|
|
19
|
-
# so the largest possible part of the image is visible.
|
|
20
|
-
#
|
|
21
|
-
def default_mask(mask_arg)
|
|
22
|
-
mask = mask_arg.dup
|
|
23
|
-
mask[:width] = image_file_width if mask[:width].zero?
|
|
24
|
-
mask[:height] = image_file_height if mask[:height].zero?
|
|
25
|
-
|
|
26
|
-
crop_size = size_when_fitting({width: image_file_width, height: image_file_height}, mask)
|
|
27
|
-
top_left = get_top_left_crop_corner(crop_size)
|
|
28
|
-
|
|
29
|
-
point_and_mask_to_points(top_left, crop_size)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Returns a size value String for the thumbnail used in essence picture editors.
|
|
33
|
-
#
|
|
34
|
-
def thumbnail_size(size_string = "0x0", crop = false)
|
|
35
|
-
size = sizes_from_string(size_string)
|
|
36
|
-
|
|
37
|
-
# only if crop is set do we need to actually parse the size string, otherwise
|
|
38
|
-
# we take the base image size.
|
|
39
|
-
if crop
|
|
40
|
-
size[:width] = get_base_dimensions[:width] if size[:width].zero?
|
|
41
|
-
size[:height] = get_base_dimensions[:height] if size[:height].zero?
|
|
42
|
-
size = reduce_to_image(size)
|
|
43
|
-
else
|
|
44
|
-
size = get_base_dimensions
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
size = size_when_fitting({width: THUMBNAIL_WIDTH, height: THUMBNAIL_HEIGHT}, size)
|
|
48
|
-
"#{size[:width]}x#{size[:height]}"
|
|
49
|
-
end
|
|
50
|
-
|
|
51
14
|
# Returns the rendered cropped image. Tries to use the crop_from and crop_size
|
|
52
15
|
# parameters. When they can't be parsed, it just crops from the center.
|
|
53
16
|
#
|
|
54
17
|
def crop(size, crop_from = nil, crop_size = nil, upsample = false)
|
|
55
18
|
raise "No size given!" if size.empty?
|
|
56
19
|
|
|
57
|
-
render_to =
|
|
20
|
+
render_to = inferred_sizes_from_string(size)
|
|
58
21
|
if crop_from && crop_size
|
|
59
22
|
top_left = point_from_string(crop_from)
|
|
60
|
-
crop_dimensions =
|
|
23
|
+
crop_dimensions = inferred_sizes_from_string(crop_size)
|
|
61
24
|
xy_crop_resize(render_to, top_left, crop_dimensions, upsample)
|
|
62
25
|
else
|
|
63
26
|
center_crop(render_to, upsample)
|
|
@@ -75,21 +38,30 @@ module Alchemy
|
|
|
75
38
|
def landscape_format?
|
|
76
39
|
image_file.landscape?
|
|
77
40
|
end
|
|
41
|
+
|
|
78
42
|
alias_method :landscape?, :landscape_format?
|
|
43
|
+
deprecate landscape_format?: "Use image_file.landscape? instead", deprecator: Alchemy::Deprecation
|
|
44
|
+
deprecate landscape?: "Use image_file.landscape? instead", deprecator: Alchemy::Deprecation
|
|
79
45
|
|
|
80
46
|
# Returns true if picture's width is smaller than it's height
|
|
81
47
|
#
|
|
82
48
|
def portrait_format?
|
|
83
49
|
image_file.portrait?
|
|
84
50
|
end
|
|
51
|
+
|
|
85
52
|
alias_method :portrait?, :portrait_format?
|
|
53
|
+
deprecate portrait_format?: "Use image_file.portrait? instead", deprecator: Alchemy::Deprecation
|
|
54
|
+
deprecate portrait?: "Use image_file.portrait? instead", deprecator: Alchemy::Deprecation
|
|
86
55
|
|
|
87
56
|
# Returns true if picture's width and height is equal
|
|
88
57
|
#
|
|
89
58
|
def square_format?
|
|
90
59
|
image_file.aspect_ratio == 1.0
|
|
91
60
|
end
|
|
61
|
+
|
|
92
62
|
alias_method :square?, :square_format?
|
|
63
|
+
deprecate square_format?: "Use image_file.aspect_ratio instead", deprecator: Alchemy::Deprecation
|
|
64
|
+
deprecate square?: "Use image_file.aspect_ratio instead", deprecator: Alchemy::Deprecation
|
|
93
65
|
|
|
94
66
|
# Returns true if the class we're included in has a meaningful render_size attribute
|
|
95
67
|
#
|
|
@@ -121,62 +93,18 @@ module Alchemy
|
|
|
121
93
|
}
|
|
122
94
|
end
|
|
123
95
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
#
|
|
128
|
-
def get_top_left_crop_corner(dimensions)
|
|
129
|
-
{
|
|
130
|
-
x: (image_file_width - dimensions[:width]) / 2,
|
|
131
|
-
y: (image_file_height - dimensions[:height]) / 2,
|
|
132
|
-
}
|
|
133
|
-
end
|
|
96
|
+
def inferred_sizes_from_string(string)
|
|
97
|
+
sizes = sizes_from_string(string)
|
|
98
|
+
ratio = image_file_width.to_f / image_file_height
|
|
134
99
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# This is the order of precedence: crop_size > image_size
|
|
138
|
-
def get_base_dimensions
|
|
139
|
-
if crop_size?
|
|
140
|
-
sizes_from_string(crop_size)
|
|
141
|
-
else
|
|
142
|
-
image_size
|
|
100
|
+
if sizes[:width].zero?
|
|
101
|
+
sizes[:width] = image_file_width * ratio
|
|
143
102
|
end
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# This function takes a target and a base dimensions hash and returns
|
|
147
|
-
# the dimensions of the image when the base dimensions hash fills
|
|
148
|
-
# the target.
|
|
149
|
-
#
|
|
150
|
-
# Aspect ratio will be preserved.
|
|
151
|
-
#
|
|
152
|
-
def size_when_fitting(target, dimensions = get_base_dimensions)
|
|
153
|
-
zoom = [
|
|
154
|
-
dimensions[:width].to_f / target[:width],
|
|
155
|
-
dimensions[:height].to_f / target[:height],
|
|
156
|
-
].max
|
|
157
|
-
|
|
158
|
-
if zoom == 0.0
|
|
159
|
-
width = target[:width]
|
|
160
|
-
height = target[:height]
|
|
161
|
-
else
|
|
162
|
-
width = (dimensions[:width] / zoom).round
|
|
163
|
-
height = (dimensions[:height] / zoom).round
|
|
103
|
+
if sizes[:height].zero?
|
|
104
|
+
sizes[:height] = image_file_width / ratio
|
|
164
105
|
end
|
|
165
106
|
|
|
166
|
-
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# Given a point as a Hash with :x and :y, and a mask with
|
|
170
|
-
# :width and :height, this function returns the area on the
|
|
171
|
-
# underlying canvas as a Hash of two points
|
|
172
|
-
#
|
|
173
|
-
def point_and_mask_to_points(point, mask)
|
|
174
|
-
{
|
|
175
|
-
x1: point[:x],
|
|
176
|
-
y1: point[:y],
|
|
177
|
-
x2: point[:x] + mask[:width],
|
|
178
|
-
y2: point[:y] + mask[:height],
|
|
179
|
-
}
|
|
107
|
+
sizes
|
|
180
108
|
end
|
|
181
109
|
|
|
182
110
|
# Converts a dimensions hash to a string of from "20x20"
|
|
@@ -197,20 +125,20 @@ module Alchemy
|
|
|
197
125
|
# Use imagemagick to custom crop an image. Uses -thumbnail for better performance when resizing.
|
|
198
126
|
#
|
|
199
127
|
def xy_crop_resize(dimensions, top_left, crop_dimensions, upsample)
|
|
200
|
-
crop_argument =
|
|
128
|
+
crop_argument = dimensions_to_string(crop_dimensions)
|
|
201
129
|
crop_argument += "+#{top_left[:x]}+#{top_left[:y]}"
|
|
202
130
|
|
|
203
|
-
resize_argument =
|
|
131
|
+
resize_argument = dimensions_to_string(dimensions)
|
|
204
132
|
resize_argument += ">" unless upsample
|
|
205
|
-
image_file.
|
|
133
|
+
image_file.crop_resize(crop_argument, resize_argument)
|
|
206
134
|
end
|
|
207
135
|
|
|
208
136
|
# Used when centercropping.
|
|
209
137
|
#
|
|
210
138
|
def reduce_to_image(dimensions)
|
|
211
139
|
{
|
|
212
|
-
width: [dimensions[:width], image_file_width].min,
|
|
213
|
-
height: [dimensions[:height], image_file_height].min,
|
|
140
|
+
width: [dimensions[:width].to_i, image_file_width.to_i].min,
|
|
141
|
+
height: [dimensions[:height].to_i, image_file_height.to_i].min,
|
|
214
142
|
}
|
|
215
143
|
end
|
|
216
144
|
end
|
|
@@ -86,7 +86,9 @@ module Alchemy
|
|
|
86
86
|
dragonfly_accessor :image_file, app: :alchemy_pictures do
|
|
87
87
|
# Preprocess after uploading the picture
|
|
88
88
|
after_assign do |image|
|
|
89
|
-
|
|
89
|
+
if has_convertible_format?
|
|
90
|
+
self.class.preprocessor_class.new(image).call
|
|
91
|
+
end
|
|
90
92
|
end
|
|
91
93
|
end
|
|
92
94
|
|
|
@@ -108,7 +110,7 @@ module Alchemy
|
|
|
108
110
|
case_sensitive: false,
|
|
109
111
|
message: Alchemy.t("not a valid image")
|
|
110
112
|
|
|
111
|
-
stampable stamper_class_name: Alchemy.
|
|
113
|
+
stampable stamper_class_name: Alchemy.user_class_name
|
|
112
114
|
|
|
113
115
|
scope :named, ->(name) { where("#{table_name}.name LIKE ?", "%#{name}%") }
|
|
114
116
|
scope :recent, -> { where("#{table_name}.created_at > ?", Time.current - 24.hours).order(:created_at) }
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# Picture thumbnails and cropping concerns
|
|
5
|
+
module PictureThumbnails
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
before_save :fix_crop_values
|
|
10
|
+
|
|
11
|
+
delegate :image_file_width, :image_file_height, :image_file, to: :picture, allow_nil: true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The url to show the picture.
|
|
15
|
+
#
|
|
16
|
+
# Takes all values like +name+ and crop sizes (+crop_from+, +crop_size+ from the build in graphical image cropper)
|
|
17
|
+
# and also adds the security token.
|
|
18
|
+
#
|
|
19
|
+
# You typically want to set the size the picture should be resized to.
|
|
20
|
+
#
|
|
21
|
+
# === Example:
|
|
22
|
+
#
|
|
23
|
+
# essence_picture.picture_url(size: '200x300', crop: true, format: 'gif')
|
|
24
|
+
# # '/pictures/1/show/200x300/crop/cats.gif?sh=765rfghj'
|
|
25
|
+
#
|
|
26
|
+
# @option options size [String]
|
|
27
|
+
# The size the picture should be resized to.
|
|
28
|
+
#
|
|
29
|
+
# @option options format [String]
|
|
30
|
+
# The format the picture should be rendered in.
|
|
31
|
+
# Defaults to the +image_output_format+ from the +Alchemy::Config+.
|
|
32
|
+
#
|
|
33
|
+
# @option options crop [Boolean]
|
|
34
|
+
# If set to true the picture will be cropped to fit the size value.
|
|
35
|
+
#
|
|
36
|
+
# @return [String]
|
|
37
|
+
def picture_url(options = {})
|
|
38
|
+
return if picture.nil?
|
|
39
|
+
|
|
40
|
+
picture.url(picture_url_options.merge(options)) || "missing-image.png"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Picture rendering options
|
|
44
|
+
#
|
|
45
|
+
# Returns the +default_render_format+ of the associated +Alchemy::Picture+
|
|
46
|
+
# together with the +crop_from+ and +crop_size+ values
|
|
47
|
+
#
|
|
48
|
+
# @return [HashWithIndifferentAccess]
|
|
49
|
+
def picture_url_options
|
|
50
|
+
return {} if picture.nil?
|
|
51
|
+
|
|
52
|
+
crop = !!settings[:crop]
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
format: picture.default_render_format,
|
|
56
|
+
crop: crop,
|
|
57
|
+
crop_from: crop && crop_from.presence || nil,
|
|
58
|
+
crop_size: crop && crop_size.presence || nil,
|
|
59
|
+
size: settings[:size],
|
|
60
|
+
}.with_indifferent_access
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns an url for the thumbnail representation of the assigned picture
|
|
64
|
+
#
|
|
65
|
+
# It takes cropping values into account, so it always represents the current
|
|
66
|
+
# image displayed in the frontend.
|
|
67
|
+
#
|
|
68
|
+
# @return [String]
|
|
69
|
+
def thumbnail_url
|
|
70
|
+
return if picture.nil?
|
|
71
|
+
|
|
72
|
+
picture.url(thumbnail_url_options) || "alchemy/missing-image.svg"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Thumbnail rendering options
|
|
76
|
+
#
|
|
77
|
+
# @return [HashWithIndifferentAccess]
|
|
78
|
+
def thumbnail_url_options
|
|
79
|
+
crop = !!settings[:crop]
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
size: "160x120",
|
|
83
|
+
crop: crop,
|
|
84
|
+
crop_from: crop && crop_from.presence || default_crop_from&.join("x"),
|
|
85
|
+
crop_size: crop && crop_size.presence || default_crop_size&.join("x"),
|
|
86
|
+
flatten: true,
|
|
87
|
+
format: picture&.image_file_format || "jpg",
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Settings for the graphical JS image cropper
|
|
92
|
+
def image_cropper_settings
|
|
93
|
+
Alchemy::ImageCropperSettings.new(
|
|
94
|
+
render_size: dimensions_from_string(render_size.presence || settings[:size]),
|
|
95
|
+
default_crop_from: default_crop_from,
|
|
96
|
+
default_crop_size: default_crop_size,
|
|
97
|
+
fixed_ratio: settings[:fixed_ratio],
|
|
98
|
+
image_width: picture&.image_file_width,
|
|
99
|
+
image_height: picture&.image_file_height,
|
|
100
|
+
).to_h
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Show image cropping link for content
|
|
104
|
+
def allow_image_cropping?
|
|
105
|
+
settings[:crop] && picture &&
|
|
106
|
+
picture.can_be_cropped_to?(
|
|
107
|
+
settings[:size],
|
|
108
|
+
settings[:upsample],
|
|
109
|
+
) && !!picture.image_file
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def default_crop_size
|
|
115
|
+
return nil unless settings[:crop] && settings[:size]
|
|
116
|
+
|
|
117
|
+
mask = inferred_dimensions_from_string(settings[:size])
|
|
118
|
+
zoom = thumbnail_zoom_factor(mask)
|
|
119
|
+
return nil if zoom.zero?
|
|
120
|
+
|
|
121
|
+
[(mask[0] / zoom), (mask[1] / zoom)].map(&:round)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def thumbnail_zoom_factor(mask)
|
|
125
|
+
[
|
|
126
|
+
mask[0].to_f / (image_file_width || 1),
|
|
127
|
+
mask[1].to_f / (image_file_height || 1),
|
|
128
|
+
].max
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def default_crop_from
|
|
132
|
+
return nil unless settings[:crop]
|
|
133
|
+
return nil if default_crop_size.nil?
|
|
134
|
+
|
|
135
|
+
[
|
|
136
|
+
((image_file_width || 0) - default_crop_size[0]) / 2,
|
|
137
|
+
((image_file_height || 0) - default_crop_size[1]) / 2,
|
|
138
|
+
].map(&:round)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def dimensions_from_string(string)
|
|
142
|
+
return if string.nil?
|
|
143
|
+
|
|
144
|
+
string.split("x", 2).map(&:to_i)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def inferred_dimensions_from_string(string)
|
|
148
|
+
return if string.nil?
|
|
149
|
+
|
|
150
|
+
width, height = dimensions_from_string(string)
|
|
151
|
+
ratio = image_file_width.to_f / image_file_height.to_i
|
|
152
|
+
|
|
153
|
+
if width.zero? && ratio.is_a?(Float)
|
|
154
|
+
width = height * ratio
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
if height.zero? && ratio.is_a?(Float)
|
|
158
|
+
height = width / ratio
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
[width.to_i, height.to_i]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def fix_crop_values
|
|
165
|
+
%i[crop_from crop_size].each do |crop_value|
|
|
166
|
+
if public_send(crop_value).is_a?(String)
|
|
167
|
+
public_send("#{crop_value}=", normalize_crop_value(crop_value))
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def normalize_crop_value(crop_value)
|
|
173
|
+
public_send(crop_value).split("x").map { |n| normalize_number(n) }.join("x")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def normalize_number(number)
|
|
177
|
+
number = number.to_f.round
|
|
178
|
+
number.negative? ? 0 : number
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|