alchemy_cms 8.3.11 → 8.4.0
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/LICENSE +1 -1
- data/README.md +58 -10
- 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/remote-select.css +1 -0
- data/app/assets/builds/alchemy/admin/tags-autocomplete.css +1 -0
- data/app/assets/builds/alchemy/admin.css +4 -4
- data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
- data/app/assets/builds/alchemy/dark-theme.css +1 -1
- data/app/assets/builds/alchemy/light-theme.css +1 -1
- data/app/assets/builds/alchemy/preview.min.js +1 -1
- data/app/assets/builds/alchemy/theme.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/content/alchemy-dark/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
- data/app/assets/images/alchemy/icons-sprite.svg +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/news_reader.html.erb +38 -0
- data/app/components/alchemy/admin/dashboard/widgets/news_reader.rb +54 -0
- data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +11 -3
- data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +6 -1
- data/app/components/alchemy/admin/element_editor.html.erb +1 -1
- data/app/components/alchemy/admin/element_select.rb +30 -20
- data/app/components/alchemy/admin/element_status_indicators.html.erb +16 -0
- data/app/components/alchemy/admin/element_status_indicators.rb +31 -0
- data/app/components/alchemy/admin/link_dialog/external_tab.rb +2 -1
- data/app/components/alchemy/admin/main_navigation/entry.rb +61 -0
- data/app/components/alchemy/admin/main_navigation/menu.rb +21 -0
- data/app/components/alchemy/admin/main_navigation/sub_navigation.rb +31 -0
- data/app/components/alchemy/admin/page_node.html.erb +1 -1
- data/app/components/alchemy/admin/page_permitted_roles_select.html.erb +11 -0
- data/app/components/alchemy/admin/page_permitted_roles_select.rb +35 -0
- data/app/components/alchemy/admin/page_select.rb +18 -8
- data/app/components/alchemy/admin/preview_time_select.rb +1 -1
- data/app/components/alchemy/admin/toolbar_button.rb +43 -20
- data/app/components/alchemy/admin/uploader_button.rb +66 -0
- data/app/components/alchemy/ingredients/base_editor.rb +10 -1
- data/app/components/alchemy/ingredients/boolean_editor.rb +1 -0
- data/app/components/alchemy/ingredients/color_editor.rb +6 -3
- data/app/components/alchemy/ingredients/datetime_view.rb +1 -1
- data/app/components/alchemy/ingredients/picture_view.rb +2 -4
- data/app/components/alchemy/menubar.html.erb +157 -0
- data/app/components/alchemy/menubar.rb +15 -0
- data/app/controllers/alchemy/admin/attachments_controller.rb +12 -9
- data/app/controllers/alchemy/admin/base_controller.rb +34 -21
- data/app/controllers/alchemy/admin/dashboard_controller.rb +4 -0
- data/app/controllers/alchemy/admin/elements_controller.rb +4 -23
- data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
- data/app/controllers/alchemy/admin/nodes_controller.rb +1 -1
- data/app/controllers/alchemy/admin/pages_controller.rb +1 -1
- data/app/controllers/alchemy/admin/pictures_controller.rb +6 -3
- data/app/controllers/alchemy/admin/resources_controller.rb +5 -5
- data/app/controllers/alchemy/admin/sites_controller.rb +1 -1
- data/app/controllers/alchemy/base_controller.rb +24 -16
- data/app/controllers/alchemy/pages_controller.rb +29 -8
- data/app/controllers/alchemy/sitemap_controller.rb +58 -0
- data/app/controllers/concerns/alchemy/admin/csp_protection.rb +43 -0
- data/app/controllers/concerns/alchemy/admin/current_language.rb +1 -1
- data/app/controllers/concerns/alchemy/admin/resource_filter.rb +3 -1
- data/{lib → app/controllers/concerns}/alchemy/controller_actions.rb +13 -0
- data/app/controllers/concerns/alchemy/legacy_page_redirects.rb +4 -4
- data/app/helpers/alchemy/admin/base_helper.rb +4 -1
- data/app/helpers/alchemy/admin/form_helper.rb +0 -2
- data/app/helpers/alchemy/admin/navigation_helper.rb +10 -181
- data/app/helpers/alchemy/elements_block_helper.rb +14 -0
- data/app/helpers/alchemy/resources_helper.rb +15 -1
- data/app/javascript/alchemy_admin/components/attachment_select.js +11 -20
- data/app/javascript/alchemy_admin/components/auto_submit.js +2 -4
- data/app/javascript/alchemy_admin/components/button.js +0 -6
- data/app/javascript/alchemy_admin/components/clear_schedule_button.js +21 -0
- data/app/javascript/alchemy_admin/components/color_select.js +50 -45
- data/app/javascript/alchemy_admin/components/conditional_field.js +50 -0
- data/app/javascript/alchemy_admin/components/dirty_guard.js +77 -0
- data/app/javascript/alchemy_admin/components/dom_id_select.js +17 -10
- data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +7 -2
- data/app/javascript/alchemy_admin/components/element_editor.js +29 -56
- data/app/javascript/alchemy_admin/components/element_select.js +26 -66
- data/app/javascript/alchemy_admin/components/icon.js +9 -4
- data/app/javascript/alchemy_admin/{image_cropper.js → components/image_cropper.js} +39 -53
- data/app/javascript/alchemy_admin/components/image_overlay_link.js +14 -0
- data/app/javascript/alchemy_admin/components/index.js +12 -1
- data/app/javascript/alchemy_admin/components/main_navi_entry.js +75 -0
- data/app/javascript/alchemy_admin/components/menu_node.js +73 -0
- data/app/javascript/alchemy_admin/components/news_reader.js +109 -0
- data/app/javascript/alchemy_admin/components/node_folder.js +36 -0
- data/app/javascript/alchemy_admin/components/node_form.js +34 -0
- data/app/javascript/alchemy_admin/components/node_select.js +25 -30
- data/app/javascript/alchemy_admin/components/node_tree.js +56 -0
- data/app/javascript/alchemy_admin/components/page_node.js +2 -1
- data/app/javascript/alchemy_admin/components/page_select.js +16 -34
- data/app/javascript/alchemy_admin/components/picture_archive.js +92 -0
- data/app/javascript/alchemy_admin/components/picture_editor.js +2 -1
- data/app/javascript/alchemy_admin/components/preview_window.js +11 -3
- data/app/javascript/alchemy_admin/components/remote_select.js +461 -81
- data/app/javascript/alchemy_admin/components/select.js +26 -88
- data/app/javascript/alchemy_admin/components/sitemap.js +2 -1
- data/app/javascript/alchemy_admin/components/sortable_elements.js +2 -1
- data/app/javascript/alchemy_admin/components/tags_autocomplete.js +63 -48
- data/app/javascript/alchemy_admin/components/tinymce.js +2 -1
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +3 -2
- data/app/javascript/alchemy_admin/components/uploader.js +19 -3
- data/app/javascript/alchemy_admin/confirm_dialog.js +7 -1
- data/app/javascript/alchemy_admin/dialog.js +182 -110
- data/app/javascript/alchemy_admin/hotkeys.js +0 -5
- data/app/javascript/alchemy_admin/i18n.js +3 -9
- data/app/javascript/alchemy_admin/image_overlay.js +58 -42
- data/app/javascript/alchemy_admin/initializer.js +2 -2
- data/app/javascript/alchemy_admin/link_dialog.js +15 -2
- data/app/javascript/alchemy_admin/routes.js +18 -0
- data/app/javascript/alchemy_admin/turbo_stream_actions.js +88 -0
- data/app/javascript/alchemy_admin/utils/ajax.js +14 -3
- data/app/javascript/alchemy_admin/utils/events.js +5 -0
- data/app/javascript/alchemy_admin/utils/json_script.js +12 -0
- data/app/javascript/alchemy_admin/utils/tom_select.js +171 -0
- data/app/javascript/alchemy_admin.js +2 -19
- data/app/javascript/preview.js +131 -113
- data/app/jobs/alchemy/invalidate_elements_cache_job.rb +1 -1
- data/app/models/alchemy/admin/content_security_policy.rb +143 -0
- data/app/models/alchemy/admin/filters/base.rb +7 -0
- data/app/models/alchemy/admin/filters/datepicker.rb +1 -1
- data/app/models/alchemy/admin/filters/select.rb +20 -7
- data/app/models/alchemy/attachment.rb +14 -7
- data/app/models/alchemy/cache_control.rb +104 -0
- data/app/models/alchemy/current.rb +1 -1
- data/app/models/alchemy/eager_loading.rb +0 -2
- data/app/models/alchemy/element.rb +23 -8
- data/app/models/alchemy/element_definition.rb +1 -0
- data/app/models/alchemy/elements_repository.rb +11 -0
- data/app/models/alchemy/folded_page.rb +5 -1
- data/app/models/alchemy/ingredient.rb +26 -0
- data/app/models/alchemy/ingredients/datetime.rb +1 -1
- data/app/models/alchemy/ingredients/link.rb +2 -0
- data/app/models/alchemy/ingredients/picture.rb +3 -0
- data/app/models/alchemy/ingredients/text.rb +2 -0
- data/app/models/alchemy/language.rb +19 -7
- data/app/models/alchemy/legacy_page_url.rb +6 -1
- data/app/models/alchemy/node.rb +51 -0
- data/app/models/alchemy/page/page_natures.rb +41 -8
- data/app/models/alchemy/page/page_scopes.rb +23 -0
- data/app/models/alchemy/page/sitemap_cache_key.rb +56 -0
- data/app/models/alchemy/page.rb +88 -32
- data/app/models/alchemy/page_definition.rb +1 -1
- data/app/models/alchemy/page_mutex.rb +16 -0
- data/app/models/alchemy/page_version.rb +24 -1
- data/app/models/alchemy/permissions.rb +6 -5
- data/app/models/alchemy/picture.rb +15 -7
- data/app/models/alchemy/picture_description.rb +22 -0
- data/app/models/alchemy/picture_thumb.rb +18 -0
- data/app/models/alchemy/publishable/timestamp_resolver.rb +70 -0
- data/app/models/alchemy/resource.rb +1 -1
- data/app/models/alchemy/safe_url_validator.rb +34 -0
- data/app/models/alchemy/site.rb +8 -3
- data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +7 -3
- data/app/models/alchemy/storage_adapter/active_storage.rb +4 -1
- data/app/models/alchemy/tag.rb +10 -4
- data/{lib → app/models/concerns}/alchemy/filetypes.rb +5 -0
- data/app/models/concerns/alchemy/picture_thumbnails.rb +61 -6
- data/app/models/concerns/alchemy/publishable.rb +14 -27
- data/app/models/concerns/alchemy/taggable.rb +92 -0
- data/app/models/concerns/alchemy/user_methods.rb +95 -0
- data/app/services/alchemy/duplicate_element.rb +5 -2
- data/app/services/alchemy/element_preloader.rb +1 -2
- data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
- data/app/stylesheets/alchemy/_extends.scss +0 -8
- data/app/stylesheets/alchemy/_themes.scss +10 -9
- data/app/stylesheets/alchemy/admin/_empty-state.scss +29 -0
- data/app/stylesheets/alchemy/admin/_help.scss +10 -0
- data/app/stylesheets/alchemy/admin/_tom-select.scss +104 -15
- data/app/stylesheets/alchemy/admin/archive.scss +56 -11
- data/app/stylesheets/alchemy/admin/base.scss +4 -5
- data/app/stylesheets/alchemy/admin/buttons.scss +12 -1
- data/app/stylesheets/alchemy/admin/dashboard.scss +150 -20
- data/app/stylesheets/alchemy/admin/dialogs.scss +27 -36
- data/app/stylesheets/alchemy/admin/element-select.scss +1 -1
- data/app/stylesheets/alchemy/admin/elements.scss +8 -24
- data/app/stylesheets/alchemy/admin/flash.scss +2 -1
- data/app/stylesheets/alchemy/admin/forms.scss +60 -24
- data/app/stylesheets/alchemy/admin/frame.scss +40 -4
- data/app/stylesheets/alchemy/admin/image_library.scss +9 -12
- data/app/stylesheets/alchemy/admin/navigation.scss +17 -14
- data/app/stylesheets/alchemy/admin/node-select.scss +13 -37
- data/app/stylesheets/alchemy/admin/nodes.scss +1 -1
- data/app/stylesheets/alchemy/admin/page-select.scss +6 -97
- data/app/stylesheets/alchemy/admin/print.scss +0 -4
- data/app/stylesheets/alchemy/admin/remote-select.scss +170 -0
- data/app/stylesheets/alchemy/admin/search.scss +2 -3
- data/app/stylesheets/alchemy/admin/selects.scss +1 -294
- data/app/stylesheets/alchemy/admin/sitemap.scss +2 -1
- data/app/stylesheets/alchemy/admin/tables.scss +7 -1
- data/app/stylesheets/alchemy/admin/tags-autocomplete.scss +17 -0
- data/app/stylesheets/alchemy/admin/tags.scss +0 -13
- data/app/stylesheets/alchemy/admin/toolbar.scss +9 -6
- data/app/stylesheets/alchemy/admin/upload.scss +61 -1
- data/app/stylesheets/alchemy/admin.scss +4 -1
- data/app/types/alchemy/cache_control_type.rb +84 -0
- data/app/views/alchemy/_edit_mode.html.erb +1 -1
- data/app/views/alchemy/_menubar.html.erb +2 -154
- data/app/views/alchemy/_preview_mode_code.html.erb +0 -3
- data/app/views/alchemy/admin/_left_menu.html.erb +18 -5
- data/app/views/alchemy/admin/_top_menu.html.erb +1 -1
- data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +5 -4
- data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
- data/app/views/alchemy/admin/attachments/_files_list.html.erb +71 -83
- data/app/views/alchemy/admin/attachments/assign.turbo_stream.erb +8 -0
- data/app/views/alchemy/admin/attachments/index.html.erb +32 -7
- data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
- data/app/views/alchemy/admin/crop.html.erb +20 -20
- data/app/views/alchemy/admin/dashboard/_footer.html.erb +0 -3
- data/app/views/alchemy/admin/dashboard/_top.html.erb +1 -0
- data/app/views/alchemy/admin/dashboard/help.html.erb +84 -57
- data/app/views/alchemy/admin/dashboard/license.html.erb +6 -0
- data/app/views/alchemy/admin/elements/_footer.html.erb +0 -1
- data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
- data/app/views/alchemy/admin/elements/_header.html.erb +2 -19
- data/app/views/alchemy/admin/elements/_preview_text_quote.html.erb +3 -0
- data/app/views/alchemy/admin/elements/_schedule.html.erb +7 -7
- data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -1
- data/app/views/alchemy/admin/elements/create.turbo_stream.erb +1 -1
- data/app/views/alchemy/admin/elements/index.html.erb +1 -1
- data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +2 -2
- data/app/views/alchemy/admin/elements/update.turbo_stream.erb +31 -0
- data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +6 -3
- data/app/views/alchemy/admin/languages/_table.html.erb +0 -2
- data/app/views/alchemy/admin/languages/index.html.erb +25 -29
- data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +1 -3
- data/app/views/alchemy/admin/layoutpages/edit.html.erb +1 -1
- data/app/views/alchemy/admin/layoutpages/index.html.erb +26 -7
- data/app/views/alchemy/admin/nodes/_form.html.erb +3 -21
- data/app/views/alchemy/admin/nodes/_node.html.erb +93 -88
- data/app/views/alchemy/admin/nodes/index.html.erb +15 -23
- data/app/views/alchemy/admin/pages/_create_language_form.html.erb +2 -6
- data/app/views/alchemy/admin/pages/_form.html.erb +2 -1
- data/app/views/alchemy/admin/pages/_publication_fields.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_table.html.erb +2 -4
- data/app/views/alchemy/admin/pages/_toolbar.html.erb +2 -1
- data/app/views/alchemy/admin/pages/edit.html.erb +10 -21
- data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +1 -1
- data/app/views/alchemy/admin/pages/index.html.erb +26 -31
- data/app/views/alchemy/admin/pages/info.html.erb +2 -2
- data/app/views/alchemy/admin/pages/update.turbo_stream.erb +2 -2
- data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +1 -1
- data/app/views/alchemy/admin/partials/_routes.html.erb +24 -48
- data/app/views/alchemy/admin/pictures/_archive.html.erb +3 -17
- 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 -3
- data/app/views/alchemy/admin/pictures/_form.html.erb +0 -1
- data/app/views/alchemy/admin/pictures/_infos.html.erb +5 -1
- data/app/views/alchemy/admin/pictures/_picture.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -4
- data/app/views/alchemy/admin/pictures/assign.turbo_stream.erb +7 -0
- data/app/views/alchemy/admin/pictures/index.html.erb +38 -30
- data/app/views/alchemy/admin/pictures/show.html.erb +41 -41
- data/app/views/alchemy/admin/resources/_empty_state.html.erb +27 -0
- data/app/views/alchemy/admin/resources/_pagination.html.erb +1 -1
- data/app/views/alchemy/admin/resources/_per_page_select.html.erb +1 -1
- data/app/views/alchemy/admin/resources/_resource_table.html.erb +0 -2
- data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +3 -2
- data/app/views/alchemy/admin/resources/index.html.erb +32 -13
- data/app/views/alchemy/admin/sites/index.html.erb +24 -22
- data/app/views/alchemy/admin/styleguide/index.html.erb +35 -12
- data/app/views/alchemy/admin/tags/index.html.erb +27 -25
- data/app/views/alchemy/admin/tinymce/_setup.html.erb +6 -8
- data/app/views/alchemy/admin/translations/_en.js +3 -0
- data/app/views/alchemy/admin/uploader/_button.html.erb +11 -17
- data/app/views/alchemy/admin/uploader/_setup.html.erb +3 -3
- data/app/views/alchemy/pages/sitemap.xml.erb +2 -0
- data/app/views/kaminari/alchemy/_first_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_gap.html.erb +0 -1
- data/app/views/kaminari/alchemy/_last_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_next_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_paginator.html.erb +0 -1
- data/app/views/kaminari/alchemy/_prev_page.html.erb +1 -2
- data/app/views/layouts/alchemy/admin/dialog.html.erb +3 -0
- data/app/views/layouts/alchemy/admin.html.erb +5 -6
- data/config/brakeman.ignore +23 -1
- data/config/importmap.rb +10 -8
- data/config/initializers/dragonfly.rb +21 -17
- data/config/locales/alchemy.en.yml +52 -11
- data/config/routes.rb +3 -3
- data/db/migrate/20260727120000_add_permitted_roles_to_alchemy_pages.rb +23 -0
- data/db/migrate/20260820120000_add_cached_tag_list_to_taggables.rb +57 -0
- data/lib/alchemy/configuration/number_option.rb +13 -0
- data/lib/alchemy/configuration/proc_option.rb +13 -0
- data/lib/alchemy/configuration.rb +2 -0
- data/lib/alchemy/configurations/admin_components.rb +30 -0
- data/lib/alchemy/configurations/format_matchers.rb +7 -1
- data/lib/alchemy/configurations/main.rb +103 -4
- data/lib/alchemy/configurations/menu.rb +91 -0
- data/lib/alchemy/configurations/menu_item.rb +123 -0
- data/lib/alchemy/configurations/sitemap.rb +1 -0
- data/lib/alchemy/configurations/uploader.rb +15 -1
- data/lib/alchemy/engine.rb +0 -6
- data/lib/alchemy/i18n.rb +26 -0
- data/lib/alchemy/modules.rb +52 -50
- data/lib/alchemy/tasks/generate_thumbnails.rb +99 -0
- data/lib/alchemy/tasks/tidy.rb +1 -1
- data/lib/alchemy/test_support/capybara_helpers.rb +48 -37
- data/lib/alchemy/test_support/current_language_shared_examples.rb +1 -1
- data/lib/alchemy/test_support/factories/dummy_user_factory.rb +4 -1
- data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +132 -0
- data/lib/alchemy/test_support/shared_ingredient_examples.rb +52 -0
- data/lib/alchemy/test_support/shared_publishable_examples.rb +29 -4
- data/lib/alchemy/upgrader/eight_four.rb +237 -0
- data/lib/alchemy/upgrader/register_module_rewriter.rb +177 -0
- data/lib/alchemy/upgrader.rb +2 -1
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy.rb +0 -1
- data/lib/alchemy_cms.rb +0 -13
- data/lib/generators/alchemy/elements/templates/view.html.erb +1 -1
- data/lib/generators/alchemy/elements/templates/view.html.haml +1 -1
- data/lib/generators/alchemy/elements/templates/view.html.slim +1 -1
- data/lib/generators/alchemy/install/install_generator.rb +2 -13
- data/lib/generators/alchemy/install/templates/alchemy.rb.tt +24 -9
- data/lib/generators/alchemy/module/module_generator.rb +1 -0
- data/lib/generators/alchemy/module/templates/ability.rb.tt +1 -1
- data/lib/generators/alchemy/module/templates/module_config.rb.tt +16 -18
- data/lib/generators/alchemy/user_columns_migration/templates/migration.rb.tt +19 -0
- data/lib/generators/alchemy/user_columns_migration/user_columns_migration_generator.rb +30 -0
- data/lib/tasks/alchemy/thumbnails.rake +16 -29
- data/lib/tasks/alchemy/upgrade.rake +45 -3
- data/vendor/javascript/cropperjs.min.js +3 -3
- data/vendor/javascript/floating-ui.min.js +1 -1
- data/vendor/javascript/tinymce.min.js +5 -5
- data/vendor/javascript/tom-select.min.js +1 -1
- metadata +85 -65
- data/app/javascript/alchemy_admin/components/action.js +0 -50
- data/app/javascript/alchemy_admin/dirty.js +0 -55
- data/app/javascript/alchemy_admin/node_tree.js +0 -82
- data/app/javascript/alchemy_admin/picture_selector.js +0 -67
- data/app/javascript/alchemy_admin/templates/compiled.js +0 -1
- data/app/javascript/alchemy_admin/templates/node_folder.hbs +0 -3
- data/app/stylesheets/alchemy/admin/attachment-select.scss +0 -19
- data/app/views/alchemy/admin/_main_navi.html.erb +0 -6
- data/app/views/alchemy/admin/attachments/assign.js.erb +0 -15
- data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +0 -30
- data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +0 -20
- data/app/views/alchemy/admin/partials/_sub_navigation.html.erb +0 -11
- data/app/views/alchemy/admin/pictures/assign.js.erb +0 -13
- data/app/views/alchemy/base/error_notice.js.erb +0 -1
- data/app/views/alchemy/base/permission_denied.js.erb +0 -3
- data/app/views/alchemy/base/redirect.js.erb +0 -11
- data/config/alchemy/config.yml +0 -217
- data/config/alchemy/modules.yml +0 -82
- data/config/initializers/rails_live_reload.rb +0 -13
- data/lib/alchemy/dev_support/live_reload_watcher.rb +0 -5
- data/lib/alchemy/taggable.rb +0 -49
- data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -34
- data/vendor/assets/stylesheets/alchemy_admin/select2.css +0 -1
- data/vendor/javascript/handlebars.min.js +0 -29
- data/vendor/javascript/jquery.min.js +0 -2
- data/vendor/javascript/rails-ujs.min.js +0 -1
- data/vendor/javascript/select2.min.js +0 -23
- /data/{lib → app/controllers/concerns}/alchemy/ability_helper.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/admin/locale.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/admin/preview_time.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/admin/timezone.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/configuration_methods.rb +0 -0
- /data/{lib → app/models}/alchemy/admin/preview_url.rb +0 -0
- /data/{lib → app/models/concerns}/alchemy/hints.rb +0 -0
- /data/{lib → app/models/concerns}/alchemy/name_conversions.rb +0 -0
- /data/{lib → app/services}/alchemy/elements_finder.rb +0 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# Pre-generates picture thumbnails so they do not have to be rendered
|
|
5
|
+
# on the first request.
|
|
6
|
+
#
|
|
7
|
+
# Used by the +alchemy:generate:thumbnails+ rake tasks. Works with both
|
|
8
|
+
# the +active_storage+ and the +dragonfly+ storage adapter.
|
|
9
|
+
#
|
|
10
|
+
# Both methods yield each processed record, so callers (like the rake task)
|
|
11
|
+
# can report progress without this service having to know about the shell.
|
|
12
|
+
#
|
|
13
|
+
# With +async: true+ the +active_storage+ variants are enqueued as
|
|
14
|
+
# +ActiveStorage::TransformJob+s instead of being processed inline. The
|
|
15
|
+
# +dragonfly+ adapter has no background processor and always runs inline.
|
|
16
|
+
class GenerateThumbnails
|
|
17
|
+
class << self
|
|
18
|
+
# Pre-generates the archive thumbnails (+Alchemy::Picture::THUMBNAIL_SIZES+)
|
|
19
|
+
# for every picture. These are the thumbnails used by the picture archive.
|
|
20
|
+
#
|
|
21
|
+
# @param async [Boolean] Enqueue active_storage variants as background jobs
|
|
22
|
+
# @yield [Alchemy::Picture] each picture after it was processed / enqueued
|
|
23
|
+
def pictures(async: false)
|
|
24
|
+
Alchemy::Picture.find_each do |picture|
|
|
25
|
+
generate_for_picture(picture, async: async)
|
|
26
|
+
yield picture if block_given?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Pre-generates the thumbnails rendered for published picture ingredients.
|
|
31
|
+
#
|
|
32
|
+
# Takes the ingredients crop and srcset settings into account, so all
|
|
33
|
+
# variants that the frontend requests are generated upfront.
|
|
34
|
+
#
|
|
35
|
+
# @param element_names [Array<String>, nil] Restrict to these element names
|
|
36
|
+
# @param async [Boolean] Enqueue active_storage variants as background jobs
|
|
37
|
+
# @yield [Alchemy::Ingredients::Picture] each ingredient after it was processed / enqueued
|
|
38
|
+
def ingredients(element_names: nil, async: false)
|
|
39
|
+
ingredient_scope(element_names).find_each do |ingredient|
|
|
40
|
+
generate_for_ingredient(ingredient, async: async)
|
|
41
|
+
yield ingredient if block_given?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def generate_for_picture(picture, async:)
|
|
48
|
+
Alchemy::Picture::THUMBNAIL_SIZES.each_value do |size|
|
|
49
|
+
preprocess(picture, {size: size, flatten: true}, async: async)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def generate_for_ingredient(ingredient, async:)
|
|
54
|
+
picture = ingredient.picture
|
|
55
|
+
return if picture.nil?
|
|
56
|
+
|
|
57
|
+
preprocess(picture, ingredient.picture_url_options, async: async)
|
|
58
|
+
preprocess(picture, ingredient.thumbnail_url_options, async: async)
|
|
59
|
+
ingredient.picture_srcset_variant_options.each do |options|
|
|
60
|
+
preprocess(picture, options, async: async)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Materializes a single picture variant for the given render options
|
|
65
|
+
# using the configured storage adapter.
|
|
66
|
+
#
|
|
67
|
+
# Under +active_storage+ this processes and stores the variant inline, or
|
|
68
|
+
# enqueues a native +ActiveStorage::TransformJob+ when +async+ is true.
|
|
69
|
+
# Under +dragonfly+ requesting the url persists the +Alchemy::PictureThumb+.
|
|
70
|
+
def preprocess(picture, options, async: false)
|
|
71
|
+
return unless picture&.has_convertible_format?
|
|
72
|
+
|
|
73
|
+
if Alchemy.storage_adapter.active_storage?
|
|
74
|
+
transformations = Alchemy::DragonflyToImageProcessing.call(options)
|
|
75
|
+
if async
|
|
76
|
+
ActiveStorage::TransformJob.perform_later(picture.image_file.blob, transformations)
|
|
77
|
+
else
|
|
78
|
+
picture.image_file.variant(transformations).processed
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
picture.url(options)
|
|
82
|
+
end
|
|
83
|
+
rescue => error
|
|
84
|
+
Alchemy::ErrorTracking.notification_handler.call(error)
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ingredient_scope(element_names)
|
|
89
|
+
scope = Alchemy::Ingredients::Picture
|
|
90
|
+
.joins(:element)
|
|
91
|
+
.preload(:related_object)
|
|
92
|
+
.merge(Alchemy::Element.published)
|
|
93
|
+
return scope if element_names.blank?
|
|
94
|
+
|
|
95
|
+
scope.merge(Alchemy::Element.named(element_names))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/alchemy/tasks/tidy.rb
CHANGED
|
@@ -64,8 +64,8 @@ module Alchemy
|
|
|
64
64
|
def remove_duplicate_legacy_urls
|
|
65
65
|
puts "\n## Removing duplicate legacy URLs"
|
|
66
66
|
keep_ids = Alchemy::LegacyPageUrl
|
|
67
|
-
.select("MAX(id)")
|
|
68
67
|
.group(:page_id, :urlname)
|
|
68
|
+
.pluck(Arel.sql("MAX(id)"))
|
|
69
69
|
count = Alchemy::LegacyPageUrl
|
|
70
70
|
.where.not(id: keep_ids)
|
|
71
71
|
.delete_all
|
|
@@ -3,26 +3,12 @@
|
|
|
3
3
|
module Alchemy
|
|
4
4
|
module TestSupport
|
|
5
5
|
module CapybaraHelpers
|
|
6
|
-
#
|
|
6
|
+
# @deprecated Use {#tom_select} instead.
|
|
7
7
|
def select2(value, options)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if label.has_css?(select2_anchor_selector)
|
|
13
|
-
label.find(select2_anchor_selector).click
|
|
14
|
-
else
|
|
15
|
-
within label.find(:xpath, "..") do
|
|
16
|
-
find(select2_anchor_selector).click
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
within_entire_page do
|
|
21
|
-
page.find(
|
|
22
|
-
"div.select2-result-label",
|
|
23
|
-
text: /#{Regexp.escape(value)}/i, match: :prefer_exact
|
|
24
|
-
).click
|
|
25
|
-
end
|
|
8
|
+
Alchemy::Deprecation.warn(
|
|
9
|
+
"Alchemy::TestSupport::CapybaraHelpers#select2 is deprecated. Use #tom_select instead."
|
|
10
|
+
)
|
|
11
|
+
tom_select(value, options)
|
|
26
12
|
end
|
|
27
13
|
|
|
28
14
|
# Tom Select capybara helper
|
|
@@ -42,30 +28,55 @@ module Alchemy
|
|
|
42
28
|
end
|
|
43
29
|
end
|
|
44
30
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
options[:from] = "##{container_id}"
|
|
31
|
+
# Tom Select capybara helper for adding a tag to an
|
|
32
|
+
# alchemy-tags-autocomplete field.
|
|
33
|
+
def add_tom_select_tag(value, options)
|
|
34
|
+
label = find_label_by_text(options[:from])
|
|
35
|
+
|
|
36
|
+
within label.find(:xpath, "..") do
|
|
37
|
+
find(".ts-control").click
|
|
38
|
+
find(".ts-control input").send_keys(value, :return)
|
|
54
39
|
end
|
|
40
|
+
end
|
|
55
41
|
|
|
56
|
-
|
|
42
|
+
# Tom Select capybara helper for remote (ajax) selects. Types the given
|
|
43
|
+
# value into the search field to trigger the server request and selects the
|
|
44
|
+
# matching option. Pass `select: false` to only search without selecting.
|
|
45
|
+
def tom_select_search(value, options)
|
|
46
|
+
scope =
|
|
47
|
+
if options[:from]
|
|
48
|
+
find_label_by_text(options[:from]).find(:xpath, "..")
|
|
49
|
+
elsif options[:element_id] && options[:ingredient_role]
|
|
50
|
+
find("#element_#{options[:element_id]} [data-ingredient-role='#{options[:ingredient_role]}']")
|
|
51
|
+
else
|
|
52
|
+
page
|
|
53
|
+
end
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
find("
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
within scope do
|
|
56
|
+
find(".ts-control").click
|
|
57
|
+
find(".ts-control input").send_keys(value)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The dropdown is appended to the body, so search the whole page for it.
|
|
61
|
+
# `find` waits for the debounced remote request to deliver the option.
|
|
62
|
+
unless options[:select] == false
|
|
63
|
+
within_entire_page do
|
|
64
|
+
find(
|
|
65
|
+
".ts-dropdown .option",
|
|
66
|
+
text: /#{Regexp.escape(value)}/i, match: :prefer_exact
|
|
67
|
+
).click
|
|
65
68
|
end
|
|
66
69
|
end
|
|
67
70
|
end
|
|
68
71
|
|
|
72
|
+
# @deprecated Use {#tom_select_search} instead.
|
|
73
|
+
def select2_search(value, options)
|
|
74
|
+
Alchemy::Deprecation.warn(
|
|
75
|
+
"Alchemy::TestSupport::CapybaraHelpers#select2_search is deprecated. Use #tom_select_search instead."
|
|
76
|
+
)
|
|
77
|
+
tom_select_search(value, options)
|
|
78
|
+
end
|
|
79
|
+
|
|
69
80
|
def click_button_with_tooltip(content)
|
|
70
81
|
find(%([content="#{content}"] button)).click
|
|
71
82
|
end
|
|
@@ -85,7 +96,7 @@ module Alchemy
|
|
|
85
96
|
end
|
|
86
97
|
|
|
87
98
|
def find_label_by_text(text)
|
|
88
|
-
find "label
|
|
99
|
+
find "label",
|
|
89
100
|
text: /#{Regexp.escape(text)}/i,
|
|
90
101
|
match: :one
|
|
91
102
|
rescue Capybara::ElementNotFound
|
|
@@ -24,7 +24,7 @@ RSpec.shared_examples_for "a controller that loads current language" do |args|
|
|
|
24
24
|
it "shows flash warning with redirect" do
|
|
25
25
|
Alchemy::Language.destroy_all
|
|
26
26
|
get :index, params: {site_id: site_1.id}
|
|
27
|
-
expect(flash[:
|
|
27
|
+
expect(flash[:info]).to eq Alchemy.t("Please create a language first.")
|
|
28
28
|
expect(response).to redirect_to admin_languages_path
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -4,7 +4,10 @@ FactoryBot.define do
|
|
|
4
4
|
factory :alchemy_dummy_user, class: "DummyUser" do
|
|
5
5
|
sequence(:email) { |n| "john.#{n}@doe.com" }
|
|
6
6
|
password { "s3cr3t" }
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
trait :as_member do
|
|
9
|
+
alchemy_roles { ["member"] }
|
|
10
|
+
end
|
|
8
11
|
|
|
9
12
|
trait :as_admin do
|
|
10
13
|
alchemy_roles { ["admin"] }
|
|
@@ -183,6 +183,61 @@ RSpec.shared_examples_for "having picture thumbnails" do
|
|
|
183
183
|
end
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
+
context "having format setting" do
|
|
187
|
+
before do
|
|
188
|
+
allow(record).to receive(:settings) { {format: "webp"} }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
context "with picture having a convertible format" do
|
|
192
|
+
before do
|
|
193
|
+
allow(picture).to receive(:has_convertible_format?) { true }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "includes this format" do
|
|
197
|
+
expect(picture_url_options[:format]).to eq "webp"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
context "with format option given" do
|
|
201
|
+
subject(:picture_url_options) { record.picture_url_options(format: "png") }
|
|
202
|
+
|
|
203
|
+
it "prefers the option" do
|
|
204
|
+
expect(picture_url_options[:format]).to eq "png"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
context "with picture not having a convertible format" do
|
|
210
|
+
before do
|
|
211
|
+
allow(picture).to receive(:has_convertible_format?) { false }
|
|
212
|
+
allow(picture).to receive(:default_render_format) { "svg" }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "includes the pictures default render format" do
|
|
216
|
+
expect(picture_url_options[:format]).to eq "svg"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
context "with format option given" do
|
|
220
|
+
subject(:picture_url_options) { record.picture_url_options(format: "png") }
|
|
221
|
+
|
|
222
|
+
it "still includes the pictures default render format" do
|
|
223
|
+
expect(picture_url_options[:format]).to eq "svg"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
context "with size option given" do
|
|
230
|
+
subject(:picture_url_options) { record.picture_url_options(size: "100x100") }
|
|
231
|
+
|
|
232
|
+
before do
|
|
233
|
+
allow(record).to receive(:settings) { {size: "30x70"} }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it "prefers the option" do
|
|
237
|
+
expect(picture_url_options[:size]).to eq "100x100"
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
186
241
|
context "without picture assigned" do
|
|
187
242
|
let(:picture) { nil }
|
|
188
243
|
|
|
@@ -190,6 +245,83 @@ RSpec.shared_examples_for "having picture thumbnails" do
|
|
|
190
245
|
end
|
|
191
246
|
end
|
|
192
247
|
|
|
248
|
+
describe "#picture_srcset_variant_options" do
|
|
249
|
+
subject(:picture_srcset_variant_options) { record.picture_srcset_variant_options }
|
|
250
|
+
|
|
251
|
+
let(:picture) do
|
|
252
|
+
build(:alchemy_picture).tap do |picture|
|
|
253
|
+
allow(picture).to receive(:has_convertible_format?) { true }
|
|
254
|
+
allow(picture).to receive(:default_render_format) { "jpg" }
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
before do
|
|
259
|
+
allow(record).to receive(:settings) do
|
|
260
|
+
{
|
|
261
|
+
size: "1200x800",
|
|
262
|
+
srcset: ["360x360", {size: "360x360", format: "webp"}]
|
|
263
|
+
}.with_indifferent_access
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it "returns url options for each srcset entry" do
|
|
268
|
+
expect(picture_srcset_variant_options.map { |options| options.slice(:size, :format) }).to eq([
|
|
269
|
+
{"size" => "360x360", "format" => "jpg"},
|
|
270
|
+
{"size" => "360x360", "format" => "webp"}
|
|
271
|
+
])
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
context "with format setting" do
|
|
275
|
+
before do
|
|
276
|
+
allow(record).to receive(:settings) do
|
|
277
|
+
{format: "png", srcset: ["360x360", {size: "360x360", format: "webp"}]}.with_indifferent_access
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it "uses the format setting for entries without format" do
|
|
282
|
+
expect(picture_srcset_variant_options.map { |options| options[:format] }).to eq(["png", "webp"])
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
context "without picture assigned" do
|
|
287
|
+
let(:picture) { nil }
|
|
288
|
+
|
|
289
|
+
it { is_expected.to eq([]) }
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
describe "#picture_srcset_options" do
|
|
294
|
+
subject(:picture_srcset_options) { record.picture_srcset_options }
|
|
295
|
+
|
|
296
|
+
let(:picture) do
|
|
297
|
+
create(:alchemy_picture).tap do |picture|
|
|
298
|
+
allow(picture).to receive(:has_convertible_format?) { true }
|
|
299
|
+
allow(picture).to receive(:default_render_format) { "jpg" }
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
before do
|
|
304
|
+
allow(record).to receive(:settings) do
|
|
305
|
+
{srcset: ["360x240", {size: "360x240", format: "webp"}]}.with_indifferent_access
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
it "returns url, dimensions and mime type for each srcset entry", :aggregate_failures do
|
|
310
|
+
expect(picture_srcset_options.map { |src| src.except(:url) }).to eq([
|
|
311
|
+
{desc: "360w", width: "360", height: "240", type: "image/jpeg"},
|
|
312
|
+
{desc: "360w", width: "360", height: "240", type: "image/webp"}
|
|
313
|
+
])
|
|
314
|
+
expect(picture_srcset_options[0][:url]).to match(/\.jpg/)
|
|
315
|
+
expect(picture_srcset_options[1][:url]).to match(/\.webp/)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
context "without picture assigned" do
|
|
319
|
+
let(:picture) { nil }
|
|
320
|
+
|
|
321
|
+
it { is_expected.to eq([]) }
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
193
325
|
describe "#thumbnail_url" do
|
|
194
326
|
subject(:thumbnail_url) { record.thumbnail_url }
|
|
195
327
|
|
|
@@ -80,3 +80,55 @@ RSpec.shared_examples_for "an alchemy ingredient" do
|
|
|
80
80
|
it { is_expected.to be_a("#{described_class}View".constantize) }
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
|
+
|
|
84
|
+
# Shared by the ingredients that store an editor supplied url.
|
|
85
|
+
#
|
|
86
|
+
# it_behaves_like "a linkable alchemy ingredient", :link
|
|
87
|
+
#
|
|
88
|
+
RSpec.shared_examples_for "a linkable alchemy ingredient" do |url_attribute|
|
|
89
|
+
describe "url scheme validation" do
|
|
90
|
+
let(:element) { build(:alchemy_element, name: "article") }
|
|
91
|
+
|
|
92
|
+
subject(:ingredient) do
|
|
93
|
+
described_class.new(element: element, role: "headline")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# captured in a let, a block parameter is not in scope inside a def
|
|
97
|
+
let(:attribute) { url_attribute }
|
|
98
|
+
|
|
99
|
+
def errors_for(url)
|
|
100
|
+
ingredient.public_send(:"#{attribute}=", url)
|
|
101
|
+
ingredient.valid?
|
|
102
|
+
ingredient.errors[attribute]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "allows a blank url" do
|
|
106
|
+
expect(errors_for(nil)).to be_empty
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "allows an allowed scheme" do
|
|
110
|
+
expect(errors_for("https://example.com")).to be_empty
|
|
111
|
+
expect(errors_for("mailto:jane@example.com")).to be_empty
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "allows a relative url" do
|
|
115
|
+
expect(errors_for("/a/path")).to be_empty
|
|
116
|
+
expect(errors_for("#an-anchor")).to be_empty
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "rejects an executable scheme" do
|
|
120
|
+
expect(errors_for("javascript:alert(document.domain)")).to be_present
|
|
121
|
+
expect(errors_for("data:text/html;base64,PHN2Zz4=")).to be_present
|
|
122
|
+
expect(errors_for("vbscript:msgbox(1)")).to be_present
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "rejects an executable scheme obfuscated by whitespace" do
|
|
126
|
+
expect(errors_for(" javascript:alert(1)")).to be_present
|
|
127
|
+
expect(errors_for("java\tscript:alert(1)")).to be_present
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "rejects a javascript url disguised as an authority" do
|
|
131
|
+
expect(errors_for("javascript://%0aalert(1)")).to be_present
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -33,6 +33,11 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
33
33
|
it "only includes records without public_on date" do
|
|
34
34
|
expect(subject).to eq [nil]
|
|
35
35
|
end
|
|
36
|
+
|
|
37
|
+
it "delegates to the resolver" do
|
|
38
|
+
expect(Alchemy.config.publishable_resolver).to receive(:draft)
|
|
39
|
+
described_class.draft
|
|
40
|
+
end
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
describe ".scheduled" do
|
|
@@ -50,6 +55,11 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
50
55
|
public_two
|
|
51
56
|
])
|
|
52
57
|
end
|
|
58
|
+
|
|
59
|
+
it "delegates to the resolver" do
|
|
60
|
+
expect(Alchemy.config.publishable_resolver).to receive(:scheduled)
|
|
61
|
+
subject
|
|
62
|
+
end
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
describe ".published" do
|
|
@@ -57,6 +67,11 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
57
67
|
let!(:public_two) { create(factory_name, public_on: Date.tomorrow) }
|
|
58
68
|
let!(:non_public) { create(factory_name, public_on: nil) }
|
|
59
69
|
|
|
70
|
+
it "delegates to the resolver" do
|
|
71
|
+
expect(Alchemy.config.publishable_resolver).to receive(:published)
|
|
72
|
+
described_class.published
|
|
73
|
+
end
|
|
74
|
+
|
|
60
75
|
context "without time given" do
|
|
61
76
|
subject { described_class.published }
|
|
62
77
|
|
|
@@ -97,13 +112,16 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
97
112
|
subject { publishable.scheduled? }
|
|
98
113
|
|
|
99
114
|
let(:publishable) { build(factory_name, public_on:, public_until:) }
|
|
115
|
+
let(:public_on) { nil }
|
|
116
|
+
let(:public_until) { nil }
|
|
100
117
|
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
it "delegates to the resolver" do
|
|
119
|
+
expect_any_instance_of(Alchemy.config.publishable_resolver).to receive(:scheduled?)
|
|
120
|
+
subject
|
|
121
|
+
end
|
|
103
122
|
|
|
123
|
+
context "when public_on is nil" do
|
|
104
124
|
context "and public_until is nil" do
|
|
105
|
-
let(:public_until) { nil }
|
|
106
|
-
|
|
107
125
|
it { expect(subject).to be(false) }
|
|
108
126
|
end
|
|
109
127
|
|
|
@@ -155,6 +173,13 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
155
173
|
describe "#public?" do
|
|
156
174
|
subject { publishable.public? }
|
|
157
175
|
|
|
176
|
+
let(:publishable) { build(factory_name) }
|
|
177
|
+
|
|
178
|
+
it "delegates to the resolver" do
|
|
179
|
+
expect_any_instance_of(Alchemy.config.publishable_resolver).to receive(:public?)
|
|
180
|
+
subject
|
|
181
|
+
end
|
|
182
|
+
|
|
158
183
|
context "when public_on is not set" do
|
|
159
184
|
let(:publishable) { build(factory_name, public_on: nil) }
|
|
160
185
|
|