alchemy_cms 7.4.10 → 8.0.0.a
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.
Potentially problematic release.
This version of alchemy_cms might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +87 -28
- data/Gemfile +13 -6
- data/README.md +13 -5
- data/alchemy_cms.gemspec +14 -5
- data/app/assets/builds/alchemy/admin/page-select.css +1 -1
- data/app/assets/builds/alchemy/admin/print.css +1 -1
- data/app/assets/builds/alchemy/admin.css +2 -2
- data/app/assets/builds/alchemy/custom-properties.css +1 -1
- data/app/assets/builds/alchemy/welcome.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy/content.min.css +1 -0
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
- data/app/assets/config/alchemy_manifest.js +0 -2
- data/app/assets/images/alchemy/icons-sprite.svg +1 -0
- data/app/components/alchemy/admin/resource/applied_filter.rb +29 -0
- data/app/components/alchemy/admin/resource/checkbox_filter.rb +36 -0
- data/app/components/alchemy/admin/resource/datepicker_filter.rb +42 -0
- data/app/components/alchemy/admin/resource/select_filter.rb +43 -0
- data/app/components/alchemy/admin/toolbar_button.rb +5 -2
- data/app/components/alchemy/ingredients/datetime_view.rb +2 -4
- data/app/components/alchemy/ingredients/number_view.rb +18 -0
- data/app/controllers/alchemy/admin/attachments_controller.rb +8 -15
- data/app/controllers/alchemy/admin/clipboard_controller.rb +2 -6
- data/app/controllers/alchemy/admin/elements_controller.rb +1 -1
- data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
- data/app/controllers/alchemy/admin/pages_controller.rb +15 -17
- data/app/controllers/alchemy/admin/pictures_controller.rb +9 -5
- data/app/controllers/alchemy/admin/resources_controller.rb +16 -106
- data/app/controllers/alchemy/attachments_controller.rb +43 -14
- data/app/controllers/alchemy/messages_controller.rb +1 -1
- data/app/controllers/alchemy/pages_controller.rb +7 -2
- data/app/controllers/concerns/alchemy/admin/resource_filter.rb +92 -0
- data/app/decorators/alchemy/element_editor.rb +5 -48
- data/app/decorators/alchemy/ingredient_editor.rb +3 -53
- data/app/helpers/alchemy/admin/base_helper.rb +14 -84
- data/app/helpers/alchemy/admin/elements_helper.rb +4 -4
- data/app/helpers/alchemy/admin/pages_helper.rb +1 -1
- data/app/helpers/alchemy/base_helper.rb +0 -30
- data/app/helpers/alchemy/elements_block_helper.rb +0 -14
- data/app/helpers/alchemy/pages_helper.rb +2 -2
- data/{lib → app/helpers}/alchemy/resources_helper.rb +5 -45
- data/app/javascript/alchemy_admin/components/action.js +2 -0
- data/app/javascript/alchemy_admin/components/alchemy_html_element.js +3 -3
- data/app/javascript/alchemy_admin/components/datepicker.js +10 -2
- data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +7 -7
- data/app/javascript/alchemy_admin/components/element_editor.js +1 -1
- data/app/javascript/alchemy_admin/components/index.js +1 -3
- data/app/javascript/alchemy_admin/components/remote_select.js +4 -1
- data/app/javascript/alchemy_admin/components/tags_autocomplete.js +5 -1
- data/app/javascript/alchemy_admin/components/tinymce.js +4 -2
- data/app/javascript/alchemy_admin/components/update_check.js +42 -0
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +15 -8
- data/app/javascript/alchemy_admin/components/uploader/progress.js +12 -6
- data/app/javascript/alchemy_admin/components/uploader.js +4 -2
- data/app/javascript/alchemy_admin/confirm_dialog.js +27 -57
- data/app/javascript/alchemy_admin/dirty.js +3 -2
- data/app/javascript/alchemy_admin/i18n.js +15 -16
- data/app/javascript/alchemy_admin/initializer.js +1 -49
- data/app/javascript/alchemy_admin/utils/ajax.js +51 -44
- data/app/javascript/alchemy_admin.js +3 -8
- data/app/models/alchemy/admin/filters/base.rb +38 -0
- data/app/models/alchemy/admin/filters/checkbox.rb +24 -0
- data/app/models/alchemy/admin/filters/datepicker.rb +53 -0
- data/app/models/alchemy/admin/filters/select.rb +70 -0
- data/app/models/alchemy/admin/resource_name.rb +27 -0
- data/app/models/alchemy/attachment.rb +49 -37
- data/app/models/alchemy/base_record.rb +2 -0
- data/app/models/alchemy/element/definitions.rb +1 -1
- data/app/models/alchemy/element/element_ingredients.rb +6 -6
- data/app/models/alchemy/element/presenters.rb +3 -12
- data/app/models/alchemy/element.rb +9 -27
- data/app/models/alchemy/element_definition.rb +160 -0
- data/app/models/alchemy/ingredient.rb +11 -44
- data/app/models/alchemy/ingredient_definition.rb +134 -0
- data/app/models/alchemy/ingredient_validator.rb +7 -3
- data/app/models/alchemy/ingredients/number.rb +19 -0
- data/app/models/alchemy/language.rb +0 -14
- data/app/models/alchemy/message.rb +3 -7
- data/app/models/alchemy/node.rb +1 -1
- data/app/models/alchemy/page/{page_layouts.rb → definitions.rb} +12 -19
- data/app/models/alchemy/page/fixed_attributes.rb +1 -1
- data/app/models/alchemy/page/page_elements.rb +13 -14
- data/app/models/alchemy/page/page_naming.rb +0 -1
- data/app/models/alchemy/page/page_natures.rb +7 -7
- data/app/models/alchemy/page/page_scopes.rb +1 -1
- data/app/models/alchemy/page.rb +11 -33
- data/app/models/alchemy/page_definition.rb +115 -0
- data/app/models/alchemy/picture.rb +71 -88
- data/app/models/alchemy/picture_variant.rb +115 -5
- data/{lib → app/models}/alchemy/resource.rb +4 -18
- data/{lib → app/models}/alchemy/searchable_resource.rb +15 -0
- data/app/models/alchemy/site/layout.rb +5 -6
- data/app/models/alchemy/site.rb +0 -15
- data/app/models/alchemy/storage_adapter/active_storage/attachment_url.rb +41 -0
- data/app/models/alchemy/storage_adapter/active_storage/picture_url.rb +55 -0
- data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +40 -0
- data/app/models/alchemy/storage_adapter/active_storage.rb +173 -0
- data/app/models/alchemy/{attachment/url.rb → storage_adapter/dragonfly/attachment_url.rb} +12 -12
- data/app/models/alchemy/{picture/url.rb → storage_adapter/dragonfly/picture_url.rb} +28 -12
- data/app/models/alchemy/{picture → storage_adapter/dragonfly}/preprocessor.rb +4 -4
- data/app/models/alchemy/storage_adapter/dragonfly.rb +183 -0
- data/app/models/alchemy/storage_adapter.rb +74 -0
- data/app/models/concerns/alchemy/picture_thumbnails.rb +19 -6
- data/app/serializers/alchemy/element_serializer.rb +0 -1
- data/app/services/alchemy/dragonfly_to_image_processing.rb +100 -0
- data/app/stylesheets/alchemy/_defaults.scss +3 -0
- data/app/stylesheets/alchemy/_extends.scss +69 -0
- data/app/{assets/stylesheets → stylesheets}/alchemy/_mixins.scss +33 -49
- data/app/stylesheets/alchemy/_variables.scss +5 -0
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/archive.scss +20 -37
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/base.scss +16 -14
- data/app/stylesheets/alchemy/admin/buttons.scss +160 -0
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/clipboard.scss +2 -2
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/dashboard.scss +13 -16
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/dialogs.scss +23 -16
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/elements.scss +150 -105
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/errors.scss +5 -5
- data/app/stylesheets/alchemy/admin/filters.scss +58 -0
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/flatpickr.scss +53 -60
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/form_fields.scss +21 -7
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/forms.scss +31 -19
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/frame.scss +20 -16
- data/app/stylesheets/alchemy/admin/hints.scss +5 -0
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/icons.scss +10 -1
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/image_library.scss +10 -8
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/images.scss +1 -1
- data/app/stylesheets/alchemy/admin/labels.scss +5 -0
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/lists.scss +3 -3
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/navigation.scss +61 -55
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/nodes.scss +21 -18
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/notices.scss +18 -18
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/page-select.scss +2 -2
- data/app/stylesheets/alchemy/admin/pagination.scss +144 -0
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/preview_window.scss +8 -6
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/print.scss +1 -1
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/resource_info.scss +8 -5
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/search.scss +9 -6
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/selects.scss +49 -37
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/shoelace.scss +5 -6
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/sitemap.scss +38 -33
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/tables.scss +6 -4
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/tags.scss +6 -4
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/toolbar.scss +12 -6
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/typography.scss +2 -2
- data/app/{assets/stylesheets → stylesheets}/alchemy/admin/upload.scss +7 -5
- data/app/stylesheets/alchemy/admin.scss +44 -0
- data/app/stylesheets/alchemy/custom-properties.css +244 -0
- data/app/stylesheets/alchemy/welcome.scss +75 -0
- data/app/{assets/stylesheets → stylesheets}/tinymce/skins/content/alchemy/content.scss +8 -9
- data/app/stylesheets/tinymce/skins/ui/alchemy/content.scss +1 -0
- data/app/{assets/stylesheets → stylesheets}/tinymce/skins/ui/alchemy/skin.scss +133 -136
- data/app/views/alchemy/admin/attachments/_files_list.html.erb +2 -2
- data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
- data/app/views/alchemy/admin/{elements/_clipboard_button.html.erb → clipboard/_button.html.erb} +3 -5
- data/app/views/alchemy/admin/clipboard/_update_nested_element_button.turbo_stream.erb +11 -0
- data/app/views/alchemy/admin/clipboard/clear.turbo_stream.erb +4 -0
- data/app/views/alchemy/admin/clipboard/index.html.erb +15 -13
- data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +18 -0
- data/app/views/alchemy/admin/clipboard/remove.turbo_stream.erb +9 -0
- data/app/views/alchemy/admin/dashboard/info.html.erb +17 -31
- data/app/views/alchemy/admin/elements/_element.html.erb +4 -8
- data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
- data/app/views/alchemy/admin/elements/_header.html.erb +1 -0
- data/app/views/alchemy/admin/elements/_toolbar.html.erb +4 -6
- data/app/views/alchemy/admin/elements/create.turbo_stream.erb +2 -1
- data/app/views/alchemy/admin/elements/index.html.erb +2 -2
- data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +3 -16
- data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +1 -10
- data/app/views/alchemy/admin/languages/_form.html.erb +1 -1
- data/app/views/alchemy/admin/languages/_table.html.erb +1 -1
- data/app/views/alchemy/admin/languages/index.html.erb +5 -2
- data/app/views/alchemy/admin/layoutpages/index.html.erb +1 -12
- data/app/views/alchemy/admin/pages/_form.html.erb +2 -2
- data/app/views/alchemy/admin/pages/_page.html.erb +2 -3
- data/app/views/alchemy/admin/pages/_toolbar.html.erb +1 -15
- data/app/views/alchemy/admin/pages/index.html.erb +1 -1
- data/app/views/alchemy/admin/pages/info.html.erb +1 -1
- data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +9 -12
- data/app/views/alchemy/admin/partials/_search_form.html.erb +4 -10
- data/app/views/alchemy/admin/pictures/_archive.html.erb +4 -7
- data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +2 -1
- data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/index.html.erb +2 -7
- data/app/views/alchemy/admin/resources/_applied_filters.html.erb +8 -0
- data/app/views/alchemy/admin/resources/_filter_bar.html.erb +11 -21
- data/app/views/alchemy/admin/resources/_pagination.html.erb +6 -0
- data/app/views/alchemy/admin/resources/_per_page_select.html.erb +4 -2
- data/app/views/alchemy/admin/resources/_resource_table.html.erb +1 -1
- data/app/views/alchemy/admin/resources/_table_header.html.erb +1 -15
- data/app/views/alchemy/admin/sites/index.html.erb +5 -1
- data/app/views/alchemy/admin/styleguide/index.html.erb +8 -0
- data/app/views/alchemy/admin/tags/index.html.erb +1 -1
- data/app/views/alchemy/admin/tinymce/_setup.html.erb +7 -7
- data/app/{javascript/alchemy_admin/locales/en.js → views/alchemy/admin/translations/_en.js} +5 -2
- data/app/views/alchemy/admin/uploader/_button.html.erb +1 -1
- data/app/views/alchemy/admin/uploader/_setup.html.erb +4 -4
- data/app/views/alchemy/ingredients/_number_editor.html.erb +24 -0
- data/app/views/alchemy/ingredients/_page_editor.html.erb +1 -0
- data/app/views/alchemy/ingredients/_richtext_editor.html.erb +1 -0
- data/app/views/alchemy/ingredients/_select_editor.html.erb +2 -1
- data/app/views/alchemy/no_index.html.erb +31 -0
- data/app/views/alchemy/welcome.html.erb +12 -10
- data/app/views/kaminari/alchemy/_first_page.html.erb +5 -3
- data/app/views/kaminari/alchemy/_last_page.html.erb +5 -3
- data/app/views/kaminari/alchemy/_next_page.html.erb +5 -3
- data/app/views/kaminari/alchemy/_paginator.html.erb +18 -13
- data/app/views/kaminari/alchemy/_prev_page.html.erb +5 -3
- data/app/views/layouts/alchemy/admin.html.erb +5 -9
- data/bun.lockb +0 -0
- data/bundles/remixicon.mjs +153 -0
- data/config/alchemy/config.yml +3 -2
- data/config/initializers/dragonfly.rb +0 -1
- data/config/initializers/mime_types.rb +1 -0
- data/config/locales/alchemy.en.yml +32 -14
- data/config/routes.rb +0 -2
- data/eslint.config.js +2 -1
- data/lib/alchemy/admin/preview_url.rb +4 -5
- data/lib/alchemy/cache_digests/template_tracker.rb +6 -9
- data/lib/alchemy/config_missing.rb +14 -0
- data/lib/alchemy/configuration/base_option.rb +24 -0
- data/lib/alchemy/configuration/boolean_option.rb +16 -0
- data/lib/alchemy/configuration/class_option.rb +15 -0
- data/lib/alchemy/configuration/class_set_option.rb +46 -0
- data/lib/alchemy/configuration/integer_list_option.rb +13 -0
- data/lib/alchemy/configuration/integer_option.rb +12 -0
- data/lib/alchemy/configuration/list_option.rb +22 -0
- data/lib/alchemy/configuration/regexp_option.rb +11 -0
- data/lib/alchemy/configuration/string_list_option.rb +13 -0
- data/lib/alchemy/configuration/string_option.rb +11 -0
- data/lib/alchemy/configuration.rb +115 -0
- data/lib/alchemy/configuration_methods.rb +3 -1
- data/lib/alchemy/configurations/default_language.rb +12 -0
- data/lib/alchemy/configurations/default_site.rb +10 -0
- data/lib/alchemy/configurations/format_matchers.rb +11 -0
- data/lib/alchemy/configurations/mailer.rb +16 -0
- data/lib/alchemy/configurations/main.rb +216 -0
- data/lib/alchemy/configurations/preview.rb +32 -0
- data/lib/alchemy/configurations/sitemap.rb +10 -0
- data/lib/alchemy/configurations/uploader.rb +34 -0
- data/lib/alchemy/engine.rb +65 -17
- data/lib/alchemy/hints.rb +3 -7
- data/lib/alchemy/name_conversions.rb +0 -6
- data/lib/alchemy/on_page_layout.rb +2 -2
- data/lib/alchemy/propshaft/tinymce_asset.rb +15 -0
- data/lib/alchemy/seeder.rb +2 -2
- data/lib/alchemy/tasks/usage.rb +4 -4
- data/lib/alchemy/test_support/config_stubbing.rb +1 -7
- data/lib/alchemy/test_support/factories/attachment_factory.rb +13 -2
- data/lib/alchemy/test_support/factories/language_factory.rb +1 -1
- data/lib/alchemy/test_support/factories/page_factory.rb +2 -3
- data/lib/alchemy/test_support/factories/picture_factory.rb +30 -2
- data/lib/alchemy/test_support/factories/site_factory.rb +2 -2
- data/lib/alchemy/test_support/having_crop_action_examples.rb +2 -2
- data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +80 -26
- data/lib/alchemy/test_support/shared_ingredient_examples.rb +5 -5
- data/lib/alchemy/upgrader/.keep +0 -0
- data/lib/alchemy/upgrader/eight_zero.rb +14 -0
- data/lib/alchemy/upgrader.rb +33 -20
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy.rb +192 -170
- data/lib/alchemy_cms.rb +1 -7
- data/lib/generators/alchemy/ingredient/ingredient_generator.rb +0 -3
- data/lib/generators/alchemy/install/files/_article.html.erb +6 -4
- data/lib/generators/alchemy/install/files/alchemy.en.yml +22 -3
- data/lib/generators/alchemy/install/files/application.html.erb +5 -0
- data/lib/generators/alchemy/install/install_generator.rb +5 -14
- data/lib/generators/alchemy/install/templates/alchemy.rb.tt +196 -0
- data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -1
- data/lib/generators/alchemy/install/templates/elements.yml.tt +3 -1
- data/lib/generators/alchemy/install/templates/menus.yml.tt +1 -1
- data/lib/generators/alchemy/install/templates/page_layouts.yml.tt +2 -2
- data/lib/generators/alchemy/page_layouts/page_layouts_generator.rb +2 -2
- data/lib/tasks/alchemy/assets.rake +14 -0
- data/lib/tasks/alchemy/upgrade.rake +12 -47
- data/lib/tasks/alchemy/usage.rake +0 -2
- data/vendor/javascript/tinymce.min.js +1 -1
- data/vitest.config.js +21 -0
- metadata +184 -183
- data/app/assets/builds/alchemy/admin/page-select.css.map +0 -1
- data/app/assets/builds/alchemy/admin/print.css.map +0 -1
- data/app/assets/builds/alchemy/admin.css.map +0 -1
- data/app/assets/builds/alchemy/custom-properties.css.map +0 -1
- data/app/assets/builds/alchemy/welcome.css.map +0 -1
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css.map +0 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css.map +0 -1
- data/app/assets/javascripts/alchemy/admin.js +0 -10
- data/app/assets/stylesheets/alchemy/_defaults.scss +0 -3
- data/app/assets/stylesheets/alchemy/_deprecated_variables.scss +0 -45
- data/app/assets/stylesheets/alchemy/_deprecation.scss +0 -17
- data/app/assets/stylesheets/alchemy/_extends.scss +0 -62
- data/app/assets/stylesheets/alchemy/_variables.scss +0 -201
- data/app/assets/stylesheets/alchemy/admin/buttons.scss +0 -123
- data/app/assets/stylesheets/alchemy/admin/hints.scss +0 -5
- data/app/assets/stylesheets/alchemy/admin/labels.scss +0 -3
- data/app/assets/stylesheets/alchemy/admin/pagination.scss +0 -92
- data/app/assets/stylesheets/alchemy/admin.scss +0 -42
- data/app/assets/stylesheets/alchemy/custom-properties.css +0 -98
- data/app/assets/stylesheets/alchemy/welcome.scss +0 -57
- data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.css +0 -711
- data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.inline.css +0 -705
- data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.inline.min.css +0 -7
- data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.min.css +0 -7
- data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.mobile.css +0 -29
- data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.mobile.min.css +0 -7
- data/app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.mobile.css +0 -677
- data/app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.mobile.min.css +0 -7
- data/app/controllers/alchemy/elements_controller.rb +0 -32
- data/app/models/alchemy/element/dom_id.rb +0 -31
- data/app/models/alchemy/picture/calculations.rb +0 -49
- data/app/models/alchemy/picture/transformations.rb +0 -115
- data/app/views/alchemy/admin/attachments/destroy.js.erb +0 -1
- data/app/views/alchemy/admin/clipboard/clear.js.erb +0 -3
- data/app/views/alchemy/admin/clipboard/insert.js.erb +0 -29
- data/app/views/alchemy/admin/clipboard/remove.js.erb +0 -10
- data/app/views/alchemy/admin/resources/_filter.html.erb +0 -12
- data/app/views/alchemy/admin/resources/_resource.html.erb +0 -34
- data/app/views/alchemy/admin/resources/_table.html.erb +0 -29
- data/app/views/alchemy/elements/show.html.erb +0 -1
- data/app/views/alchemy/elements/show.js.erb +0 -1
- data/app/views/alchemy/ingredients/_audio_view.html.erb +0 -1
- data/app/views/alchemy/ingredients/_boolean_view.html.erb +0 -1
- data/app/views/alchemy/ingredients/_datetime_view.html.erb +0 -3
- data/app/views/alchemy/ingredients/_file_view.html.erb +0 -4
- data/app/views/alchemy/ingredients/_headline_view.html.erb +0 -4
- data/app/views/alchemy/ingredients/_html_view.html.erb +0 -1
- data/app/views/alchemy/ingredients/_link_view.html.erb +0 -4
- data/app/views/alchemy/ingredients/_node_view.html.erb +0 -1
- data/app/views/alchemy/ingredients/_page_view.html.erb +0 -1
- data/app/views/alchemy/ingredients/_picture_view.html.erb +0 -4
- data/app/views/alchemy/ingredients/_richtext_view.html.erb +0 -3
- data/app/views/alchemy/ingredients/_select_view.html.erb +0 -1
- data/app/views/alchemy/ingredients/_text_view.html.erb +0 -4
- data/app/views/alchemy/ingredients/_video_view.html.erb +0 -3
- data/babel.config.js +0 -12
- data/config/initializers/assets.rb +0 -4
- data/lib/alchemy/config.rb +0 -114
- data/lib/alchemy/element_definition.rb +0 -73
- data/lib/alchemy/page_layout.rb +0 -73
- data/lib/alchemy/resource_filter.rb +0 -40
- data/lib/alchemy/upgrader/seven_point_four.rb +0 -26
- data/lib/alchemy/upgrader/seven_point_three.rb +0 -52
- data/lib/generators/alchemy/ingredient/templates/view.html.erb +0 -1
- data/lib/generators/alchemy/install/files/alchemy_admin.js +0 -1
- data/lib/generators/alchemy/install/files/all.js +0 -11
- data/lib/generators/alchemy/install/files/article.css +0 -25
- data/vendor/assets/images/remixicon.symbol.svg +0 -11
- /data/app/{assets/stylesheets → stylesheets}/alchemy/_fonts.scss +0 -0
- /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/attachment-select.scss +0 -0
- /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/attachments.scss +0 -0
- /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/flash.scss +0 -0
- /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/list_filter.scss +0 -0
- /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/node-select.scss +0 -0
- /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/spinner.scss +0 -0
- /data/app/{assets/stylesheets → stylesheets}/tinymce/skins/skintool.json +0 -0
- /data/app/{assets/stylesheets → stylesheets}/tinymce/skins/ui/alchemy/fonts/tinymce-mobile.woff +0 -0
data/app/models/alchemy/page.rb
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
#
|
|
37
37
|
|
|
38
38
|
require_dependency "alchemy/page/fixed_attributes"
|
|
39
|
-
require_dependency "alchemy/page/
|
|
39
|
+
require_dependency "alchemy/page/definitions"
|
|
40
40
|
require_dependency "alchemy/page/page_scopes"
|
|
41
41
|
require_dependency "alchemy/page/page_natures"
|
|
42
42
|
require_dependency "alchemy/page/page_naming"
|
|
@@ -44,7 +44,6 @@ require_dependency "alchemy/page/page_elements"
|
|
|
44
44
|
|
|
45
45
|
module Alchemy
|
|
46
46
|
class Page < BaseRecord
|
|
47
|
-
include Alchemy::Hints
|
|
48
47
|
include Alchemy::Logger
|
|
49
48
|
include Alchemy::Taggable
|
|
50
49
|
|
|
@@ -90,7 +89,7 @@ module Alchemy
|
|
|
90
89
|
|
|
91
90
|
acts_as_nested_set(dependent: :destroy, scope: [:layoutpage, :language_id])
|
|
92
91
|
|
|
93
|
-
stampable stamper_class_name: Alchemy.
|
|
92
|
+
stampable stamper_class_name: Alchemy.user_class_name
|
|
94
93
|
|
|
95
94
|
belongs_to :language
|
|
96
95
|
|
|
@@ -156,7 +155,7 @@ module Alchemy
|
|
|
156
155
|
after_update :touch_nodes
|
|
157
156
|
|
|
158
157
|
# Concerns
|
|
159
|
-
include
|
|
158
|
+
include Definitions
|
|
160
159
|
include PageScopes
|
|
161
160
|
include PageNatures
|
|
162
161
|
include PageNaming
|
|
@@ -164,6 +163,7 @@ module Alchemy
|
|
|
164
163
|
|
|
165
164
|
# site_name accessor
|
|
166
165
|
delegate :name, to: :site, prefix: true, allow_nil: true
|
|
166
|
+
delegate :has_hint?, :hint, to: :definition
|
|
167
167
|
|
|
168
168
|
# Class methods
|
|
169
169
|
#
|
|
@@ -183,37 +183,10 @@ module Alchemy
|
|
|
183
183
|
@_url_path_class = klass
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
-
def alchemy_resource_filters
|
|
187
|
-
[
|
|
188
|
-
{
|
|
189
|
-
name: :by_page_layout,
|
|
190
|
-
values: PageLayout.all.map { |p| [Alchemy.t(p["name"], scope: "page_layout_names"), p["name"]] }
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
name: :status,
|
|
194
|
-
values: %w[published not_public restricted]
|
|
195
|
-
}
|
|
196
|
-
]
|
|
197
|
-
end
|
|
198
|
-
|
|
199
186
|
def searchable_alchemy_resource_attributes
|
|
200
187
|
%w[name urlname title]
|
|
201
188
|
end
|
|
202
189
|
|
|
203
|
-
# Used to store the current page previewed in the edit page template.
|
|
204
|
-
# @deprecated Use {Alchemy::Current#preview_page=} instead.
|
|
205
|
-
def current_preview=(page)
|
|
206
|
-
Current.preview_page = page
|
|
207
|
-
end
|
|
208
|
-
deprecate "current_preview=": :"Alchemy::Current.preview_page=", deprecator: Alchemy::Deprecation
|
|
209
|
-
|
|
210
|
-
# Returns the current page previewed in the edit page template.
|
|
211
|
-
# @deprecated Use {Alchemy::Current#preview_page} instead.
|
|
212
|
-
def current_preview
|
|
213
|
-
Current.preview_page
|
|
214
|
-
end
|
|
215
|
-
deprecate current_preview: :"Alchemy::Current.preview_page", deprecator: Alchemy::Deprecation
|
|
216
|
-
|
|
217
190
|
# @return the language root page for given language id.
|
|
218
191
|
# @param language_id [Fixnum]
|
|
219
192
|
#
|
|
@@ -265,13 +238,13 @@ module Alchemy
|
|
|
265
238
|
|
|
266
239
|
clipboard_pages = all_from_clipboard(clipboard)
|
|
267
240
|
allowed_page_layouts = Alchemy::Page.selectable_layouts(language_id, layoutpages: layoutpages)
|
|
268
|
-
allowed_page_layout_names = allowed_page_layouts.collect
|
|
241
|
+
allowed_page_layout_names = allowed_page_layouts.collect(&:name)
|
|
269
242
|
clipboard_pages.select { |cp| allowed_page_layout_names.include?(cp.page_layout) }
|
|
270
243
|
end
|
|
271
244
|
|
|
272
245
|
def link_target_options
|
|
273
246
|
options = [[Alchemy.t(:default, scope: "link_target_options"), ""]]
|
|
274
|
-
link_target_options =
|
|
247
|
+
link_target_options = Alchemy.config.link_target_options
|
|
275
248
|
link_target_options.each do |option|
|
|
276
249
|
# add an underscore to the options to provide the default syntax
|
|
277
250
|
# @link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target
|
|
@@ -280,6 +253,11 @@ module Alchemy
|
|
|
280
253
|
end
|
|
281
254
|
options
|
|
282
255
|
end
|
|
256
|
+
|
|
257
|
+
# Allow all string and text attributes to be searchable by Ransack.
|
|
258
|
+
def ransackable_attributes(_auth_object = nil)
|
|
259
|
+
searchable_alchemy_resource_attributes + ["updated_at"]
|
|
260
|
+
end
|
|
283
261
|
end
|
|
284
262
|
|
|
285
263
|
# Instance methods
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
class PageDefinition
|
|
5
|
+
include ActiveModel::Model
|
|
6
|
+
include ActiveModel::Attributes
|
|
7
|
+
include Alchemy::Hints
|
|
8
|
+
|
|
9
|
+
extend ActiveModel::Translation
|
|
10
|
+
|
|
11
|
+
attribute :name, :string
|
|
12
|
+
attribute :elements, default: []
|
|
13
|
+
attribute :autogenerate, default: []
|
|
14
|
+
attribute :layoutpage, :boolean, default: false
|
|
15
|
+
attribute :unique, :boolean, default: false
|
|
16
|
+
attribute :cache, :boolean, default: true
|
|
17
|
+
attribute :insert_elements_at, :string, default: "bottom"
|
|
18
|
+
attribute :fixed_attributes, default: {}
|
|
19
|
+
attribute :searchable, :boolean, default: true
|
|
20
|
+
attribute :searchresults, :boolean, default: false
|
|
21
|
+
attribute :hide, :boolean, default: false
|
|
22
|
+
attribute :editable_by
|
|
23
|
+
attribute :hint
|
|
24
|
+
|
|
25
|
+
validates :name,
|
|
26
|
+
presence: true,
|
|
27
|
+
format: {
|
|
28
|
+
with: /\A[a-z_-]+\z/
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
delegate :[], to: :attributes
|
|
32
|
+
delegate :blank?, to: :name
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# Returns all page layouts.
|
|
36
|
+
#
|
|
37
|
+
# They are defined in +config/alchemy/page_layout.yml+ file.
|
|
38
|
+
#
|
|
39
|
+
def all
|
|
40
|
+
@definitions ||= read_definitions_file.map { new(**_1) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def map(...)
|
|
44
|
+
all.map(...)
|
|
45
|
+
end
|
|
46
|
+
alias_method :collect, :map
|
|
47
|
+
|
|
48
|
+
# Add additional page definitions to collection.
|
|
49
|
+
#
|
|
50
|
+
# Useful for extending the page layouts from an Alchemy module.
|
|
51
|
+
#
|
|
52
|
+
# === Usage Example
|
|
53
|
+
#
|
|
54
|
+
# Call +Alchemy::PageDefinition.add(your_definition)+ in your engine.rb file.
|
|
55
|
+
#
|
|
56
|
+
# @param [Array || Hash]
|
|
57
|
+
# You can pass a single layout definition as Hash, or a collection of page layouts as Array.
|
|
58
|
+
#
|
|
59
|
+
def add(definition)
|
|
60
|
+
all
|
|
61
|
+
@definitions += Array.wrap(definition).map { new(**_1) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns one page definition by given name.
|
|
65
|
+
#
|
|
66
|
+
def get(name)
|
|
67
|
+
return new if name.blank?
|
|
68
|
+
|
|
69
|
+
all.detect { _1.name.casecmp(name).zero? }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def reset!
|
|
73
|
+
@definitions = nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The absolute +page_layouts.yml+ file path
|
|
77
|
+
# @return [Pathname]
|
|
78
|
+
def layouts_file_path
|
|
79
|
+
Rails.root.join("config", "alchemy", "page_layouts.yml")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Reads the layout definitions from +config/alchemy/page_layouts.yml+.
|
|
85
|
+
#
|
|
86
|
+
def read_definitions_file
|
|
87
|
+
if File.exist?(layouts_file_path)
|
|
88
|
+
Array.wrap(
|
|
89
|
+
YAML.safe_load(
|
|
90
|
+
ERB.new(File.read(layouts_file_path)).result,
|
|
91
|
+
permitted_classes: YAML_PERMITTED_CLASSES,
|
|
92
|
+
aliases: true
|
|
93
|
+
) || []
|
|
94
|
+
)
|
|
95
|
+
else
|
|
96
|
+
raise LoadError, "Could not find page_layouts.yml file! Please run `rails generate alchemy:install`"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def human_name
|
|
102
|
+
Alchemy::Page.human_layout_name(name)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def attributes
|
|
106
|
+
super.with_indifferent_access
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def hint_translation_scope
|
|
112
|
+
:page_hints
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -28,24 +28,10 @@ module Alchemy
|
|
|
28
28
|
large: "240x180"
|
|
29
29
|
}.with_indifferent_access.freeze
|
|
30
30
|
|
|
31
|
-
CONVERTIBLE_FILE_FORMATS = %w[gif jpg jpeg png webp].freeze
|
|
32
|
-
|
|
33
|
-
TRANSFORMATION_OPTIONS = [
|
|
34
|
-
:crop,
|
|
35
|
-
:crop_from,
|
|
36
|
-
:crop_size,
|
|
37
|
-
:flatten,
|
|
38
|
-
:format,
|
|
39
|
-
:quality,
|
|
40
|
-
:size,
|
|
41
|
-
:upsample
|
|
42
|
-
]
|
|
43
|
-
|
|
44
31
|
include Alchemy::Logger
|
|
45
32
|
include Alchemy::NameConversions
|
|
46
33
|
include Alchemy::Taggable
|
|
47
34
|
include Alchemy::TouchElements
|
|
48
|
-
include Calculations
|
|
49
35
|
|
|
50
36
|
has_many :picture_ingredients,
|
|
51
37
|
class_name: "Alchemy::Ingredients::Picture",
|
|
@@ -54,7 +40,6 @@ module Alchemy
|
|
|
54
40
|
|
|
55
41
|
has_many :elements, through: :picture_ingredients
|
|
56
42
|
has_many :pages, through: :elements
|
|
57
|
-
has_many :thumbs, class_name: "Alchemy::PictureThumb", dependent: :destroy
|
|
58
43
|
has_many :descriptions, class_name: "Alchemy::PictureDescription", dependent: :destroy
|
|
59
44
|
|
|
60
45
|
accepts_nested_attributes_for :descriptions, allow_destroy: true, reject_if: ->(attr) { attr[:text].blank? }
|
|
@@ -72,7 +57,7 @@ module Alchemy
|
|
|
72
57
|
|
|
73
58
|
# Image preprocessing class
|
|
74
59
|
def self.preprocessor_class
|
|
75
|
-
@_preprocessor_class ||=
|
|
60
|
+
@_preprocessor_class ||= Alchemy.storage_adapter.preprocessor_class
|
|
76
61
|
end
|
|
77
62
|
|
|
78
63
|
# Set a image preprocessing class
|
|
@@ -84,56 +69,39 @@ module Alchemy
|
|
|
84
69
|
@_preprocessor_class = klass
|
|
85
70
|
end
|
|
86
71
|
|
|
87
|
-
|
|
88
|
-
dragonfly_accessor :image_file, app: :alchemy_pictures do
|
|
89
|
-
# Preprocess after uploading the picture
|
|
90
|
-
after_assign do |image|
|
|
91
|
-
if has_convertible_format?
|
|
92
|
-
self.class.preprocessor_class.new(image).call
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
before_save :sanitize_image_file_name
|
|
98
|
-
# Create important thumbnails upfront
|
|
99
|
-
after_create -> { PictureThumb.generate_thumbs!(self) if has_convertible_format? }
|
|
72
|
+
include Alchemy.storage_adapter.picture_class_methods
|
|
100
73
|
|
|
101
74
|
# We need to define this method here to have it available in the validations below.
|
|
102
75
|
class << self
|
|
103
76
|
def allowed_filetypes
|
|
104
|
-
|
|
77
|
+
Alchemy.config.uploader.allowed_filetypes.alchemy_pictures
|
|
105
78
|
end
|
|
106
79
|
end
|
|
107
80
|
|
|
108
81
|
validates_presence_of :image_file
|
|
109
|
-
validates_size_of :image_file, maximum:
|
|
110
|
-
|
|
111
|
-
of: :image_file,
|
|
112
|
-
in: allowed_filetypes,
|
|
113
|
-
case_sensitive: false,
|
|
114
|
-
message: Alchemy.t("not a valid image")
|
|
82
|
+
validates_size_of :image_file, maximum: Alchemy.config.uploader.file_size_limit.megabytes
|
|
83
|
+
validate :image_file_type_allowed, if: -> { image_file.present? }
|
|
115
84
|
|
|
116
|
-
stampable stamper_class_name: Alchemy.
|
|
85
|
+
stampable stamper_class_name: Alchemy.user_class_name
|
|
117
86
|
|
|
118
87
|
scope :named, ->(name) { where("#{table_name}.name LIKE ?", "%#{name}%") }
|
|
119
88
|
scope :recent, -> { where("#{table_name}.created_at > ?", Time.current - 24.hours).order(:created_at) }
|
|
120
|
-
scope :deletable,
|
|
121
|
-
|
|
122
|
-
"#{table_name}.id NOT IN (SELECT related_object_id FROM alchemy_ingredients WHERE
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
end
|
|
89
|
+
scope :deletable,
|
|
90
|
+
-> {
|
|
91
|
+
where("#{table_name}.id NOT IN (SELECT related_object_id FROM alchemy_ingredients WHERE related_object_type = 'Alchemy::Picture')")
|
|
92
|
+
}
|
|
126
93
|
scope :without_tag, -> { left_outer_joins(:taggings).where(gutentag_taggings: {id: nil}) }
|
|
127
|
-
scope :by_file_format, ->(
|
|
94
|
+
scope :by_file_format, ->(file_format) do
|
|
95
|
+
Alchemy.storage_adapter.by_file_format_scope(file_format)
|
|
96
|
+
end
|
|
128
97
|
|
|
129
98
|
# Class methods
|
|
130
99
|
|
|
131
100
|
class << self
|
|
132
101
|
# The class used to generate URLs for pictures
|
|
133
102
|
#
|
|
134
|
-
# @see Alchemy::Picture::Url
|
|
135
103
|
def url_class
|
|
136
|
-
@_url_class ||= Alchemy
|
|
104
|
+
@_url_class ||= Alchemy.storage_adapter.picture_url_class
|
|
137
105
|
end
|
|
138
106
|
|
|
139
107
|
# Set a different picture url class
|
|
@@ -143,22 +111,16 @@ module Alchemy
|
|
|
143
111
|
@_url_class = klass
|
|
144
112
|
end
|
|
145
113
|
|
|
146
|
-
def
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
name: :misc,
|
|
155
|
-
values: %w[recent last_upload without_tag deletable]
|
|
156
|
-
}
|
|
157
|
-
]
|
|
114
|
+
def searchable_alchemy_resource_attributes
|
|
115
|
+
Alchemy.storage_adapter.searchable_alchemy_resource_attributes(name)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def ransackable_attributes(_auth_object = nil)
|
|
119
|
+
Alchemy.storage_adapter.ransackable_attributes(name)
|
|
158
120
|
end
|
|
159
121
|
|
|
160
|
-
def
|
|
161
|
-
|
|
122
|
+
def ransackable_associations(_auth_object = nil)
|
|
123
|
+
Alchemy.storage_adapter.ransackable_associations(name)
|
|
162
124
|
end
|
|
163
125
|
|
|
164
126
|
def last_upload
|
|
@@ -167,33 +129,30 @@ module Alchemy
|
|
|
167
129
|
|
|
168
130
|
Picture.where(upload_hash: last_picture.upload_hash)
|
|
169
131
|
end
|
|
132
|
+
|
|
133
|
+
def ransackable_scopes(_auth_object = nil)
|
|
134
|
+
[:by_file_format, :recent, :last_upload, :without_tag, :deletable]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def file_formats(scope = all)
|
|
138
|
+
Alchemy.storage_adapter.file_formats(name, scope:)
|
|
139
|
+
end
|
|
170
140
|
end
|
|
171
141
|
|
|
172
142
|
# Instance methods
|
|
173
143
|
|
|
174
144
|
# Returns an url (or relative path) to a processed image for use inside an image_tag helper.
|
|
175
145
|
#
|
|
176
|
-
# Any additional options are passed to the url method, so you can add params to your url.
|
|
177
|
-
#
|
|
178
146
|
# Example:
|
|
179
147
|
#
|
|
180
148
|
# <%= image_tag picture.url(size: '320x200', format: 'png') %>
|
|
181
149
|
#
|
|
182
|
-
# @see Alchemy::PictureVariant#call for transformation options
|
|
183
|
-
# @see Alchemy::Picture::Url#call for url options
|
|
184
150
|
# @return [String|Nil]
|
|
185
151
|
def url(options = {})
|
|
186
152
|
return unless image_file
|
|
187
153
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
options.except(*TRANSFORMATION_OPTIONS).merge(
|
|
191
|
-
basename: name,
|
|
192
|
-
ext: variant.render_format,
|
|
193
|
-
name: name
|
|
194
|
-
)
|
|
195
|
-
)
|
|
196
|
-
rescue ::Dragonfly::Job::Fetch::NotFound => e
|
|
154
|
+
self.class.url_class.new(self).call(options)
|
|
155
|
+
rescue Alchemy.storage_adapter.rescuable_errors => e
|
|
197
156
|
log_warning(e.message)
|
|
198
157
|
nil
|
|
199
158
|
end
|
|
@@ -208,7 +167,7 @@ module Alchemy
|
|
|
208
167
|
|
|
209
168
|
url(
|
|
210
169
|
flatten: true,
|
|
211
|
-
format:
|
|
170
|
+
format: image_file_extension || "jpg",
|
|
212
171
|
size: size
|
|
213
172
|
)
|
|
214
173
|
end
|
|
@@ -242,18 +201,12 @@ module Alchemy
|
|
|
242
201
|
end
|
|
243
202
|
end
|
|
244
203
|
|
|
245
|
-
# Returns the suffix of the filename.
|
|
246
|
-
#
|
|
247
|
-
def suffix
|
|
248
|
-
image_file.ext
|
|
249
|
-
end
|
|
250
|
-
|
|
251
204
|
# Returns a humanized, readable name from image filename.
|
|
252
205
|
#
|
|
253
206
|
def humanized_name
|
|
254
207
|
return "" if image_file_name.blank?
|
|
255
208
|
|
|
256
|
-
convert_to_humanized_name(image_file_name,
|
|
209
|
+
convert_to_humanized_name(image_file_name, image_file_extension)
|
|
257
210
|
end
|
|
258
211
|
|
|
259
212
|
# Returns the format the image should be rendered with
|
|
@@ -263,9 +216,9 @@ module Alchemy
|
|
|
263
216
|
#
|
|
264
217
|
def default_render_format
|
|
265
218
|
if convertible?
|
|
266
|
-
|
|
219
|
+
Alchemy.config.image_output_format
|
|
267
220
|
else
|
|
268
|
-
|
|
221
|
+
image_file_extension
|
|
269
222
|
end
|
|
270
223
|
end
|
|
271
224
|
|
|
@@ -275,15 +228,15 @@ module Alchemy
|
|
|
275
228
|
# image has not a convertible file format (i.e. SVG) this returns +false+
|
|
276
229
|
#
|
|
277
230
|
def convertible?
|
|
278
|
-
|
|
279
|
-
|
|
231
|
+
Alchemy.config.image_output_format &&
|
|
232
|
+
Alchemy.config.image_output_format != "original" &&
|
|
280
233
|
has_convertible_format?
|
|
281
234
|
end
|
|
282
235
|
|
|
283
236
|
# Returns true if the image can be converted into other formats
|
|
284
237
|
#
|
|
285
238
|
def has_convertible_format?
|
|
286
|
-
|
|
239
|
+
Alchemy.storage_adapter.has_convertible_format?(self)
|
|
287
240
|
end
|
|
288
241
|
|
|
289
242
|
# Checks if the picture is restricted.
|
|
@@ -304,6 +257,32 @@ module Alchemy
|
|
|
304
257
|
picture_ingredients.empty?
|
|
305
258
|
end
|
|
306
259
|
|
|
260
|
+
def image_file_name
|
|
261
|
+
Alchemy.storage_adapter.image_file_name(self)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def image_file_format
|
|
265
|
+
Alchemy.storage_adapter.image_file_format(self)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def image_file_size
|
|
269
|
+
Alchemy.storage_adapter.image_file_size(self)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def image_file_width
|
|
273
|
+
Alchemy.storage_adapter.image_file_width(self)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def image_file_height
|
|
277
|
+
Alchemy.storage_adapter.image_file_height(self)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def image_file_extension
|
|
281
|
+
Alchemy.storage_adapter.image_file_extension(self)
|
|
282
|
+
end
|
|
283
|
+
alias_method :suffix, :image_file_extension
|
|
284
|
+
deprecate suffix: :image_file_extension, deprecator: Alchemy::Deprecation
|
|
285
|
+
|
|
307
286
|
# A size String from original image file values.
|
|
308
287
|
#
|
|
309
288
|
# == Example
|
|
@@ -314,8 +293,12 @@ module Alchemy
|
|
|
314
293
|
"#{image_file_width}x#{image_file_height}"
|
|
315
294
|
end
|
|
316
295
|
|
|
317
|
-
|
|
318
|
-
|
|
296
|
+
private
|
|
297
|
+
|
|
298
|
+
def image_file_type_allowed
|
|
299
|
+
unless image_file_extension&.in?(self.class.allowed_filetypes)
|
|
300
|
+
errors.add(:image_file, Alchemy.t("not a valid image"))
|
|
301
|
+
end
|
|
319
302
|
end
|
|
320
303
|
end
|
|
321
304
|
end
|
|
@@ -11,7 +11,6 @@ module Alchemy
|
|
|
11
11
|
extend Forwardable
|
|
12
12
|
|
|
13
13
|
include Alchemy::Logger
|
|
14
|
-
include Alchemy::Picture::Transformations
|
|
15
14
|
|
|
16
15
|
ANIMATED_IMAGE_FORMATS = %w[gif webp]
|
|
17
16
|
TRANSPARENT_IMAGE_FORMATS = %w[gif webp png]
|
|
@@ -89,20 +88,20 @@ module Alchemy
|
|
|
89
88
|
end
|
|
90
89
|
|
|
91
90
|
options = {
|
|
92
|
-
flatten: !render_format.in?(ANIMATED_IMAGE_FORMATS) && picture.
|
|
91
|
+
flatten: !render_format.in?(ANIMATED_IMAGE_FORMATS) && picture.image_file_extension == "gif"
|
|
93
92
|
}.with_indifferent_access.merge(options)
|
|
94
93
|
|
|
95
94
|
encoding_options = []
|
|
96
95
|
|
|
97
|
-
convert_format = render_format.sub("jpeg", "jpg") != picture.
|
|
96
|
+
convert_format = render_format.sub("jpeg", "jpg") != picture.image_file_extension.sub("jpeg", "jpg")
|
|
98
97
|
|
|
99
98
|
if encodable_image? && (convert_format || options[:quality])
|
|
100
|
-
quality = options[:quality] ||
|
|
99
|
+
quality = options[:quality] || Alchemy.config.output_image_quality
|
|
101
100
|
encoding_options << "-quality #{quality}"
|
|
102
101
|
end
|
|
103
102
|
|
|
104
103
|
if options[:flatten]
|
|
105
|
-
if render_format.in?(TRANSPARENT_IMAGE_FORMATS) && picture.
|
|
104
|
+
if render_format.in?(TRANSPARENT_IMAGE_FORMATS) && picture.image_file_extension.in?(TRANSPARENT_IMAGE_FORMATS)
|
|
106
105
|
encoding_options << "-background transparent"
|
|
107
106
|
end
|
|
108
107
|
encoding_options << "-flatten"
|
|
@@ -120,5 +119,116 @@ module Alchemy
|
|
|
120
119
|
def encodable_image?
|
|
121
120
|
render_format.in?(ENCODABLE_IMAGE_FORMATS)
|
|
122
121
|
end
|
|
122
|
+
|
|
123
|
+
# Returns the rendered cropped image. Tries to use the crop_from and crop_size
|
|
124
|
+
# parameters. When they can't be parsed, it just crops from the center.
|
|
125
|
+
#
|
|
126
|
+
def crop(size, crop_from = nil, crop_size = nil, upsample = false)
|
|
127
|
+
raise "No size given!" if size.empty?
|
|
128
|
+
|
|
129
|
+
render_to = inferred_sizes_from_string(size)
|
|
130
|
+
if crop_from && crop_size
|
|
131
|
+
top_left = point_from_string(crop_from)
|
|
132
|
+
crop_dimensions = inferred_sizes_from_string(crop_size)
|
|
133
|
+
xy_crop_resize(render_to, top_left, crop_dimensions, upsample)
|
|
134
|
+
else
|
|
135
|
+
center_crop(render_to, upsample)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Returns the rendered resized image using imagemagick directly.
|
|
140
|
+
#
|
|
141
|
+
def resize(size, upsample = false)
|
|
142
|
+
image_file.thumbnail(upsample ? size : "#{size}>")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Given a string with an x, this function return a Hash with key :x and :y
|
|
146
|
+
#
|
|
147
|
+
def point_from_string(string = "0x0")
|
|
148
|
+
string = "0x0" if string.empty?
|
|
149
|
+
raise ArgumentError if !string.match(/(\d*x)|(x\d*)/)
|
|
150
|
+
|
|
151
|
+
x, y = string.scan(/(\d*)x(\d*)/)[0].map(&:to_i)
|
|
152
|
+
|
|
153
|
+
x = 0 if x.nil?
|
|
154
|
+
y = 0 if y.nil?
|
|
155
|
+
{
|
|
156
|
+
x: x,
|
|
157
|
+
y: y
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def inferred_sizes_from_string(string)
|
|
162
|
+
sizes = sizes_from_string(string)
|
|
163
|
+
ratio = image_file_width.to_f / image_file_height
|
|
164
|
+
|
|
165
|
+
if sizes[:width].zero?
|
|
166
|
+
sizes[:width] = (sizes[:height] * ratio).round.to_i
|
|
167
|
+
end
|
|
168
|
+
if sizes[:height].zero?
|
|
169
|
+
sizes[:height] = (sizes[:width] / ratio).round.to_i
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
sizes
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Given a string with an x, this function returns a Hash with point
|
|
176
|
+
# :width and :height.
|
|
177
|
+
#
|
|
178
|
+
def sizes_from_string(string)
|
|
179
|
+
width, height = string.to_s.split("x", 2).map(&:to_i)
|
|
180
|
+
|
|
181
|
+
{
|
|
182
|
+
width: width,
|
|
183
|
+
height: height
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Returns true if both dimensions of the base image are bigger than the dimensions hash.
|
|
188
|
+
#
|
|
189
|
+
def is_bigger_than?(dimensions)
|
|
190
|
+
image_file_width > dimensions[:width] && image_file_height > dimensions[:height]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Returns true is one dimension of the base image is smaller than the dimensions hash.
|
|
194
|
+
#
|
|
195
|
+
def is_smaller_than?(dimensions)
|
|
196
|
+
!is_bigger_than?(dimensions)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Converts a dimensions hash to a string of from "20x20"
|
|
200
|
+
#
|
|
201
|
+
def dimensions_to_string(dimensions)
|
|
202
|
+
"#{dimensions[:width]}x#{dimensions[:height]}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Uses imagemagick to make a centercropped thumbnail. Does not scale the image up.
|
|
206
|
+
#
|
|
207
|
+
def center_crop(dimensions, upsample)
|
|
208
|
+
if is_smaller_than?(dimensions) && upsample == false
|
|
209
|
+
dimensions = reduce_to_image(dimensions)
|
|
210
|
+
end
|
|
211
|
+
image_file.thumbnail("#{dimensions_to_string(dimensions)}#")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Use imagemagick to custom crop an image. Uses -thumbnail for better performance when resizing.
|
|
215
|
+
#
|
|
216
|
+
def xy_crop_resize(dimensions, top_left, crop_dimensions, upsample)
|
|
217
|
+
crop_argument = dimensions_to_string(crop_dimensions)
|
|
218
|
+
crop_argument += "+#{top_left[:x]}+#{top_left[:y]}"
|
|
219
|
+
|
|
220
|
+
resize_argument = dimensions_to_string(dimensions)
|
|
221
|
+
resize_argument += ">" unless upsample
|
|
222
|
+
image_file.crop_resize(crop_argument, resize_argument)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Used when centercropping.
|
|
226
|
+
#
|
|
227
|
+
def reduce_to_image(dimensions)
|
|
228
|
+
{
|
|
229
|
+
width: [dimensions[:width].to_i, image_file_width.to_i].min,
|
|
230
|
+
height: [dimensions[:height].to_i, image_file_height.to_i].min
|
|
231
|
+
}
|
|
232
|
+
end
|
|
123
233
|
end
|
|
124
234
|
end
|