alchemy_cms 8.3.11 → 8.4.1
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
data/app/models/alchemy/node.rb
CHANGED
|
@@ -1,7 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# == Schema Information
|
|
4
|
+
#
|
|
5
|
+
# Table name: alchemy_nodes
|
|
6
|
+
#
|
|
7
|
+
# id :integer not null, primary key
|
|
8
|
+
# depth :integer default(0), not null
|
|
9
|
+
# external :boolean default(FALSE), not null
|
|
10
|
+
# folded :boolean default(FALSE), not null
|
|
11
|
+
# lft :integer not null
|
|
12
|
+
# menu_type :string not null
|
|
13
|
+
# name :string
|
|
14
|
+
# nofollow :boolean default(FALSE), not null
|
|
15
|
+
# rgt :integer not null
|
|
16
|
+
# title :string
|
|
17
|
+
# url :string
|
|
18
|
+
# created_at :datetime not null
|
|
19
|
+
# updated_at :datetime not null
|
|
20
|
+
# creator_id :integer
|
|
21
|
+
# language_id :integer not null
|
|
22
|
+
# page_id :integer
|
|
23
|
+
# parent_id :integer
|
|
24
|
+
# updater_id :integer
|
|
25
|
+
#
|
|
26
|
+
# Indexes
|
|
27
|
+
#
|
|
28
|
+
# index_alchemy_nodes_on_creator_id (creator_id)
|
|
29
|
+
# index_alchemy_nodes_on_language_id (language_id)
|
|
30
|
+
# index_alchemy_nodes_on_lft (lft)
|
|
31
|
+
# index_alchemy_nodes_on_page_id (page_id)
|
|
32
|
+
# index_alchemy_nodes_on_parent_id (parent_id)
|
|
33
|
+
# index_alchemy_nodes_on_rgt (rgt)
|
|
34
|
+
# index_alchemy_nodes_on_updater_id (updater_id)
|
|
35
|
+
#
|
|
36
|
+
# Foreign Keys
|
|
37
|
+
#
|
|
38
|
+
# language_id (language_id => alchemy_languages.id)
|
|
39
|
+
# page_id (page_id => alchemy_pages.id) ON DELETE => restrict
|
|
40
|
+
#
|
|
3
41
|
module Alchemy
|
|
4
42
|
class Node < BaseRecord
|
|
43
|
+
include Alchemy::RelatableResource
|
|
44
|
+
|
|
5
45
|
VALID_URL_REGEX = /\A(\/|\D[a-z+\d.-]+:)/
|
|
6
46
|
SKIPPED_ATTRIBUTES_ON_COPY = %w[id created_at updated_at creator_id updater_id lft rgt depth parent_id]
|
|
7
47
|
|
|
@@ -37,16 +77,27 @@ module Alchemy
|
|
|
37
77
|
# Like #available_to_guests but including nodes attached to restricted pages,
|
|
38
78
|
# which logged in members are allowed to see.
|
|
39
79
|
scope :available_to_members, -> {
|
|
80
|
+
Alchemy::Deprecation.warn("Node.available_to_members is deprecated and will be removed in Alchemy 9.0. Use Node.available_to(user) instead.")
|
|
40
81
|
from_current_public_site.where(page: nil)
|
|
41
82
|
.or(from_current_public_site.where(page: Alchemy::Page.published))
|
|
42
83
|
}
|
|
43
84
|
|
|
85
|
+
# Nodes the given user may see: external links or nodes attached to a
|
|
86
|
+
# published page the user is allowed to read. Mirrors +Page#readable_by?+
|
|
87
|
+
# so a node never reveals a page the user cannot open. Passing +nil+ yields
|
|
88
|
+
# the same result as #available_to_guests.
|
|
89
|
+
scope :available_to, ->(user) {
|
|
90
|
+
from_current_public_site.where(page: nil)
|
|
91
|
+
.or(from_current_public_site.where(page: Alchemy::Page.published.readable_by(user)))
|
|
92
|
+
}
|
|
93
|
+
|
|
44
94
|
before_validation :translate_root_menu_name, if: -> { root? }
|
|
45
95
|
before_validation :set_menu_type_from_root, unless: -> { root? }
|
|
46
96
|
|
|
47
97
|
validates :menu_type, presence: true
|
|
48
98
|
validates :name, presence: true, if: -> { page.nil? }
|
|
49
99
|
validates :url, format: {with: VALID_URL_REGEX}, unless: -> { url.nil? }
|
|
100
|
+
validates_with Alchemy::SafeUrlValidator, attributes: [:url]
|
|
50
101
|
|
|
51
102
|
# Returns the name
|
|
52
103
|
#
|
|
@@ -31,11 +31,7 @@ module Alchemy
|
|
|
31
31
|
def expiration_time
|
|
32
32
|
return 0 unless cache_page?
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
definition.cache.to_i
|
|
36
|
-
else
|
|
37
|
-
Alchemy.config.page_cache.max_age
|
|
38
|
-
end
|
|
34
|
+
cache_control.max_age || Alchemy.config.page_cache.max_age
|
|
39
35
|
end
|
|
40
36
|
|
|
41
37
|
def rootpage?
|
|
@@ -94,7 +90,7 @@ module Alchemy
|
|
|
94
90
|
#
|
|
95
91
|
def status_message(status_type)
|
|
96
92
|
if status_type == :scheduled && scheduled?
|
|
97
|
-
Alchemy.t(public_on&.future? ? :public_on : :public_until, scope: "page_states.scheduled", public_on:
|
|
93
|
+
Alchemy.t(public_on&.future? ? :public_on : :public_until, scope: "page_states.scheduled", public_on: Alchemy.l(public_on), public_until: Alchemy.l(public_until))
|
|
98
94
|
else
|
|
99
95
|
Alchemy.t(status[status_type].to_s, scope: "page_states.#{status_type}")
|
|
100
96
|
end
|
|
@@ -174,9 +170,46 @@ module Alchemy
|
|
|
174
170
|
# @returns Boolean
|
|
175
171
|
#
|
|
176
172
|
def cache_page?
|
|
177
|
-
|
|
173
|
+
!cache_control.no_store?
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The effective Cache-Control directives for this page: the page layout's
|
|
177
|
+
# `cache` setting combined (most restrictively) with the `page_cache` of any
|
|
178
|
+
# published element on the page, then narrowed for restricted / non-public /
|
|
179
|
+
# search-results pages.
|
|
180
|
+
#
|
|
181
|
+
# @returns [Alchemy::CacheControl]
|
|
182
|
+
def cache_control
|
|
183
|
+
return @cache_control if defined?(@cache_control)
|
|
184
|
+
|
|
185
|
+
control = [definition.cache, *element_cache_controls].reduce(:&)
|
|
186
|
+
|
|
187
|
+
control = Alchemy::CacheControl.parse(false) if !public? || definition.searchresults
|
|
188
|
+
|
|
189
|
+
if restricted?
|
|
190
|
+
control = if definition.cache.default?
|
|
191
|
+
Alchemy::CacheControl.parse(false)
|
|
192
|
+
else
|
|
193
|
+
control.restrict_visibility
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
@cache_control = control
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
# CacheControl of every visible element on the page whose definition
|
|
203
|
+
# declares a non-default `page_cache`. Reads through the page version's
|
|
204
|
+
# element repository so it reuses already-loaded elements instead of
|
|
205
|
+
# firing its own query, and only inspects elements at all when at least
|
|
206
|
+
# one definition is non-default.
|
|
207
|
+
def element_cache_controls
|
|
208
|
+
constrained = Alchemy::Element.definitions.reject { |d| d.page_cache.default? }
|
|
209
|
+
return [] if constrained.empty?
|
|
178
210
|
|
|
179
|
-
|
|
211
|
+
present_names = public_version&.element_repository&.visible&.named(*constrained.map(&:name))&.map(&:name) || []
|
|
212
|
+
constrained.select { |d| present_names.include?(d.name) }.map(&:page_cache)
|
|
180
213
|
end
|
|
181
214
|
end
|
|
182
215
|
end
|
|
@@ -45,6 +45,29 @@ module Alchemy
|
|
|
45
45
|
#
|
|
46
46
|
scope :restricted, -> { where(restricted: true) }
|
|
47
47
|
|
|
48
|
+
# All pages readable by the given user. Mirrors the +readable_by?+
|
|
49
|
+
# predicate for a whole relation, so +accessible_by+ and the per
|
|
50
|
+
# instance check stay in sync.
|
|
51
|
+
#
|
|
52
|
+
# Unrestricted pages are readable by everyone. Restricted pages are only
|
|
53
|
+
# readable when one of their +permitted_roles+ matches a role of the
|
|
54
|
+
# user. Roles are stored as a JSON array, so a single quoted role token
|
|
55
|
+
# is matched with a portable LIKE. +sanitize_sql_like+ escapes any LIKE
|
|
56
|
+
# wildcards in the role name so a role cannot smuggle in a +%+ or +_+;
|
|
57
|
+
# the escape character is +!+ instead of the usual backslash to avoid
|
|
58
|
+
# MySQL treating it as a string escape.
|
|
59
|
+
#
|
|
60
|
+
scope :readable_by,
|
|
61
|
+
->(user) {
|
|
62
|
+
roles = Array(user&.alchemy_roles).map(&:to_s).select(&:present?)
|
|
63
|
+
return not_restricted if roles.empty?
|
|
64
|
+
|
|
65
|
+
column = "#{Alchemy::Page.table_name}.permitted_roles"
|
|
66
|
+
conditions = roles.map { "#{column} LIKE ? ESCAPE '!'" }.join(" OR ")
|
|
67
|
+
patterns = roles.map { |role| "%\"#{Alchemy::Page.sanitize_sql_like(role, "!")}\"%" }
|
|
68
|
+
not_restricted.or(restricted.where(conditions, *patterns))
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
# All public pages
|
|
49
72
|
#
|
|
50
73
|
scope :published,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# Builds the cache key for the XML sitemap.
|
|
5
|
+
#
|
|
6
|
+
# The key combines three independent sources of change:
|
|
7
|
+
#
|
|
8
|
+
# 1. The contents of the +Alchemy::Page.sitemap+ scope, as page count and
|
|
9
|
+
# latest +updated_at+.
|
|
10
|
+
# 2. The requested base url (protocol, host and port), because
|
|
11
|
+
# +show_alchemy_page_url+ builds absolute urls from +request.base_url+ and
|
|
12
|
+
# a site can be served under several of them (see +Alchemy::Site.find_for_host+).
|
|
13
|
+
# 3. A coarse time bucket of +max_age+ seconds.
|
|
14
|
+
#
|
|
15
|
+
# The time bucket is what makes scheduled publishing work. Pages enter and
|
|
16
|
+
# leave the sitemap when +public_on+ / +public_until+ pass, without any record
|
|
17
|
+
# being touched, so no content based key can notice that transition. Rotating
|
|
18
|
+
# the key every +max_age+ seconds bounds how long such a page can be missing.
|
|
19
|
+
# It also keeps the ETag from freezing: without it, a revalidating client
|
|
20
|
+
# would receive +304 Not Modified+ forever.
|
|
21
|
+
class Page::SitemapCacheKey
|
|
22
|
+
attr_reader :site, :base_url, :max_age
|
|
23
|
+
|
|
24
|
+
# @param site [Alchemy::Site] The site the sitemap is rendered for.
|
|
25
|
+
# @param base_url [String] The requested base url (e.g. +request.base_url+).
|
|
26
|
+
# @param max_age [Integer] Length of the time bucket in seconds.
|
|
27
|
+
def initialize(site:, base_url:, max_age: Alchemy.config.sitemap.max_age)
|
|
28
|
+
@site = site
|
|
29
|
+
@base_url = base_url
|
|
30
|
+
@max_age = max_age
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Array<Object>]
|
|
34
|
+
def call
|
|
35
|
+
[site&.id, base_url, content_version, time_bucket]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Page count and latest +updated_at+ of the sitemap scope.
|
|
41
|
+
#
|
|
42
|
+
# We can not use +ActiveRecord::Relation#cache_key_with_version+ here. The
|
|
43
|
+
# +published+ scope inlines +Time.current+ into its SQL, so the query digest
|
|
44
|
+
# that +cache_key+ is built from would differ on every single call and the
|
|
45
|
+
# key would never be stable.
|
|
46
|
+
def content_version
|
|
47
|
+
pages = Page.sitemap
|
|
48
|
+
timestamp = pages.maximum(Page.arel_table[:updated_at])
|
|
49
|
+
"#{pages.count}-#{timestamp&.to_fs(:usec)}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def time_bucket
|
|
53
|
+
Time.current.to_i / max_age
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/app/models/alchemy/page.rb
CHANGED
|
@@ -4,35 +4,46 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Table name: alchemy_pages
|
|
6
6
|
#
|
|
7
|
-
# id
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# language_id
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
7
|
+
# id :integer not null, primary key
|
|
8
|
+
# cached_tag_list :text
|
|
9
|
+
# depth :integer
|
|
10
|
+
# language_code :string
|
|
11
|
+
# language_root :boolean default(FALSE), not null
|
|
12
|
+
# layoutpage :boolean default(FALSE), not null
|
|
13
|
+
# lft :integer
|
|
14
|
+
# locked_at :datetime
|
|
15
|
+
# locked_by :integer
|
|
16
|
+
# name :string
|
|
17
|
+
# page_layout :string
|
|
18
|
+
# permitted_roles :text
|
|
19
|
+
# published_at :datetime
|
|
20
|
+
# restricted :boolean default(FALSE), not null
|
|
21
|
+
# rgt :integer
|
|
22
|
+
# robot_follow :boolean default(TRUE), not null
|
|
23
|
+
# robot_index :boolean default(TRUE), not null
|
|
24
|
+
# searchable :boolean default(TRUE), not null
|
|
25
|
+
# sitemap :boolean default(TRUE), not null
|
|
26
|
+
# urlname :string
|
|
27
|
+
# created_at :datetime not null
|
|
28
|
+
# updated_at :datetime not null
|
|
29
|
+
# creator_id :integer
|
|
30
|
+
# language_id :integer not null
|
|
31
|
+
# parent_id :integer
|
|
32
|
+
# updater_id :integer
|
|
33
|
+
#
|
|
34
|
+
# Indexes
|
|
35
|
+
#
|
|
36
|
+
# index_alchemy_pages_on_creator_id (creator_id)
|
|
37
|
+
# index_alchemy_pages_on_language_id (language_id)
|
|
38
|
+
# index_alchemy_pages_on_locked_at_and_locked_by (locked_at,locked_by)
|
|
39
|
+
# index_alchemy_pages_on_rgt (rgt)
|
|
40
|
+
# index_alchemy_pages_on_updater_id (updater_id)
|
|
41
|
+
# index_pages_on_parent_id_and_lft (parent_id,lft)
|
|
42
|
+
# index_pages_on_urlname (urlname)
|
|
43
|
+
#
|
|
44
|
+
# Foreign Keys
|
|
45
|
+
#
|
|
46
|
+
# language_id (language_id => alchemy_languages.id)
|
|
36
47
|
#
|
|
37
48
|
|
|
38
49
|
require_dependency "alchemy/page/fixed_attributes"
|
|
@@ -45,6 +56,7 @@ require_dependency "alchemy/page/page_elements"
|
|
|
45
56
|
module Alchemy
|
|
46
57
|
class Page < BaseRecord
|
|
47
58
|
include Alchemy::Taggable
|
|
59
|
+
include Alchemy::RelatableResource
|
|
48
60
|
|
|
49
61
|
# These columns are deprecated in favor of page versions
|
|
50
62
|
self.ignored_columns += [
|
|
@@ -92,6 +104,7 @@ module Alchemy
|
|
|
92
104
|
:layoutpage,
|
|
93
105
|
:menu_id,
|
|
94
106
|
{
|
|
107
|
+
permitted_roles: [],
|
|
95
108
|
draft_version_attributes: [:id] + PageVersion::METADATA_ATTRIBUTES.map(&:to_sym)
|
|
96
109
|
}
|
|
97
110
|
]
|
|
@@ -135,6 +148,14 @@ module Alchemy
|
|
|
135
148
|
|
|
136
149
|
has_many :page_ingredients, class_name: "Alchemy::Ingredients::Page", foreign_key: :related_object_id, dependent: :nullify
|
|
137
150
|
|
|
151
|
+
# Stored as a JSON array in a plain text column, so the value is byte
|
|
152
|
+
# identical on every supported database and the +readable_by+ scope can
|
|
153
|
+
# match a single quoted role with a portable LIKE. No +type: Array+, as
|
|
154
|
+
# that would serialize an empty array to NULL, which +readable_by?+ cannot
|
|
155
|
+
# intersect; the writer already guarantees an array is stored.
|
|
156
|
+
attribute :permitted_roles, default: -> { ["member"] }
|
|
157
|
+
serialize :permitted_roles, coder: JSON
|
|
158
|
+
|
|
138
159
|
before_destroy :check_descendants_for_menu_nodes
|
|
139
160
|
|
|
140
161
|
before_validation :set_language,
|
|
@@ -143,14 +164,18 @@ module Alchemy
|
|
|
143
164
|
validates_presence_of :page_layout
|
|
144
165
|
validates_format_of :page_layout, with: /\A[a-z0-9_-]+\z/, unless: -> { page_layout.blank? }
|
|
145
166
|
validates_presence_of :parent, unless: -> { layoutpage? || language_root? }
|
|
167
|
+
validates_presence_of :permitted_roles, if: :restricted?
|
|
168
|
+
validate :permitted_roles_are_configured
|
|
146
169
|
|
|
147
170
|
after_initialize :ensure_draft_version, if: :new_record?
|
|
148
171
|
|
|
149
172
|
before_save :set_language_code,
|
|
150
173
|
if: -> { language.present? }
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
# Runs after save, so descendants inheriting from this page see the new
|
|
176
|
+
# values instead of the ones still stored in the database.
|
|
177
|
+
after_save :set_restrictions_to_child_pages,
|
|
178
|
+
if: -> { saved_change_to_restricted? || saved_change_to_permitted_roles? }
|
|
154
179
|
|
|
155
180
|
before_save :inherit_restricted_status,
|
|
156
181
|
if: -> { parent && parent.restricted? }
|
|
@@ -377,12 +402,22 @@ module Alchemy
|
|
|
377
402
|
|
|
378
403
|
def set_restrictions_to_child_pages
|
|
379
404
|
descendants.each do |child|
|
|
380
|
-
child.update(restricted: restricted
|
|
405
|
+
child.update(restricted: restricted?, permitted_roles: permitted_roles)
|
|
381
406
|
end
|
|
382
407
|
end
|
|
383
408
|
|
|
384
409
|
def inherit_restricted_status
|
|
385
410
|
self.restricted = parent.restricted?
|
|
411
|
+
# Roles are only seeded from the parent. An existing page keeps the roles
|
|
412
|
+
# assigned to it, until the parent pushes its own roles down again.
|
|
413
|
+
self.permitted_roles = parent.permitted_roles if new_record?
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def permitted_roles_are_configured
|
|
417
|
+
unknown = permitted_roles - Alchemy.config.user_roles.map(&:to_s)
|
|
418
|
+
return if unknown.empty?
|
|
419
|
+
|
|
420
|
+
errors.add(:permitted_roles, :inclusion, roles: unknown.to_sentence)
|
|
386
421
|
end
|
|
387
422
|
|
|
388
423
|
# Returns the first published child
|
|
@@ -468,6 +503,27 @@ module Alchemy
|
|
|
468
503
|
(editor_roles & user.alchemy_roles).any?
|
|
469
504
|
end
|
|
470
505
|
|
|
506
|
+
# Accepts an array of role names (as sent by the admin form) and normalizes
|
|
507
|
+
# it to an array of unique, non blank strings. Whether the roles are
|
|
508
|
+
# actually configured is enforced by a validation, not silently here.
|
|
509
|
+
#
|
|
510
|
+
def permitted_roles=(roles)
|
|
511
|
+
super(Array(roles).map(&:to_s).select(&:present?).uniq)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# Checks if the given user is allowed to read this page.
|
|
515
|
+
#
|
|
516
|
+
# Unrestricted pages are readable by everyone. A restricted page is only
|
|
517
|
+
# readable by users holding one of the page's +permitted_roles+. A restricted
|
|
518
|
+
# page without any role locks everybody out, which is why at least one role
|
|
519
|
+
# is validated to be present.
|
|
520
|
+
#
|
|
521
|
+
def readable_by?(user)
|
|
522
|
+
return true unless restricted?
|
|
523
|
+
|
|
524
|
+
(permitted_roles & Array(user&.alchemy_roles).map(&:to_s)).any?
|
|
525
|
+
end
|
|
526
|
+
|
|
471
527
|
# Returns the value of +public_on+ attribute from public version
|
|
472
528
|
#
|
|
473
529
|
# If it's a fixed attribute then the fixed value is returned instead
|
|
@@ -12,7 +12,7 @@ module Alchemy
|
|
|
12
12
|
attribute :autogenerate, default: []
|
|
13
13
|
attribute :layoutpage, :boolean, default: false
|
|
14
14
|
attribute :unique, :boolean, default: false
|
|
15
|
-
attribute :cache, default: true
|
|
15
|
+
attribute :cache, Alchemy::CacheControlType.new, default: true
|
|
16
16
|
attribute :insert_elements_at, :string, default: "bottom"
|
|
17
17
|
attribute :fixed_attributes, default: {}
|
|
18
18
|
attribute :searchable, :boolean, default: true
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# == Schema Information
|
|
4
|
+
#
|
|
5
|
+
# Table name: alchemy_page_mutexes
|
|
6
|
+
#
|
|
7
|
+
# id :integer not null, primary key
|
|
8
|
+
# created_at :datetime
|
|
9
|
+
# page_id :integer not null
|
|
10
|
+
#
|
|
11
|
+
# Indexes
|
|
12
|
+
#
|
|
13
|
+
# index_alchemy_page_mutexes_on_page_id (page_id) UNIQUE
|
|
14
|
+
#
|
|
15
|
+
# Foreign Keys
|
|
16
|
+
#
|
|
17
|
+
# page_id (page_id => alchemy_pages.id)
|
|
18
|
+
#
|
|
3
19
|
module Alchemy
|
|
4
20
|
class PageMutex < BaseRecord
|
|
5
21
|
class LockFailed < StandardError; end
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# == Schema Information
|
|
4
|
+
#
|
|
5
|
+
# Table name: alchemy_page_versions
|
|
6
|
+
#
|
|
7
|
+
# id :integer not null, primary key
|
|
8
|
+
# meta_description :text
|
|
9
|
+
# meta_keywords :text
|
|
10
|
+
# public_on :datetime
|
|
11
|
+
# public_until :datetime
|
|
12
|
+
# title :string
|
|
13
|
+
# created_at :datetime not null
|
|
14
|
+
# updated_at :datetime not null
|
|
15
|
+
# page_id :integer not null
|
|
16
|
+
#
|
|
17
|
+
# Indexes
|
|
18
|
+
#
|
|
19
|
+
# index_alchemy_page_versions_on_page_id (page_id)
|
|
20
|
+
# index_alchemy_page_versions_on_public_on_and_public_until (public_on,public_until)
|
|
21
|
+
#
|
|
22
|
+
# Foreign Keys
|
|
23
|
+
#
|
|
24
|
+
# page_id (page_id => alchemy_pages.id) ON DELETE => cascade
|
|
25
|
+
#
|
|
3
26
|
module Alchemy
|
|
4
27
|
class PageVersion < BaseRecord
|
|
5
28
|
include Alchemy::Publishable
|
|
@@ -30,7 +53,7 @@ module Alchemy
|
|
|
30
53
|
before_destroy :delete_elements
|
|
31
54
|
|
|
32
55
|
def element_repository
|
|
33
|
-
ElementsRepository.new(elements)
|
|
56
|
+
@element_repository ||= ElementsRepository.new(elements)
|
|
34
57
|
end
|
|
35
58
|
|
|
36
59
|
private
|
|
@@ -69,13 +69,14 @@ module Alchemy
|
|
|
69
69
|
e.public?
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
can :read, Alchemy::Page,
|
|
73
|
-
|
|
72
|
+
can :read, Alchemy::Page,
|
|
73
|
+
Alchemy::Page.published.from_current_site.readable_by(@user) do |p|
|
|
74
|
+
p.public? && p.site == Alchemy::Current.site && p.readable_by?(@user)
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
can :read, Alchemy::Node, Alchemy::Node.
|
|
77
|
+
can :read, Alchemy::Node, Alchemy::Node.available_to(@user) do |node|
|
|
77
78
|
node.language.public? && node.language.site_id == Alchemy::Current.site&.id &&
|
|
78
|
-
(node.page.nil? || node.page.public?)
|
|
79
|
+
(node.page.nil? || (node.page.public? && node.page.readable_by?(@user)))
|
|
79
80
|
end
|
|
80
81
|
end
|
|
81
82
|
end
|
|
@@ -104,7 +105,7 @@ module Alchemy
|
|
|
104
105
|
|
|
105
106
|
# Controller actions
|
|
106
107
|
can :leave, :alchemy_admin
|
|
107
|
-
can [:info, :help, :show], :alchemy_admin_dashboard
|
|
108
|
+
can [:info, :license, :help, :show], :alchemy_admin_dashboard
|
|
108
109
|
can :manage, :alchemy_admin_clipboard
|
|
109
110
|
can :update, :alchemy_admin_layoutpages
|
|
110
111
|
can :tree, :alchemy_admin_pages
|
|
@@ -5,19 +5,27 @@
|
|
|
5
5
|
# Table name: alchemy_pictures
|
|
6
6
|
#
|
|
7
7
|
# id :integer not null, primary key
|
|
8
|
-
#
|
|
8
|
+
# cached_tag_list :text
|
|
9
|
+
# image_file_format :string
|
|
10
|
+
# image_file_height :integer
|
|
9
11
|
# image_file_name :string
|
|
12
|
+
# image_file_size :integer
|
|
13
|
+
# image_file_uid :string
|
|
10
14
|
# image_file_width :integer
|
|
11
|
-
#
|
|
15
|
+
# name :string
|
|
16
|
+
# upload_hash :string
|
|
12
17
|
# created_at :datetime not null
|
|
13
18
|
# updated_at :datetime not null
|
|
14
19
|
# creator_id :integer
|
|
15
20
|
# updater_id :integer
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
+
#
|
|
22
|
+
# Indexes
|
|
23
|
+
#
|
|
24
|
+
# index_alchemy_pictures_on_created_at (created_at)
|
|
25
|
+
# index_alchemy_pictures_on_creator_id (creator_id)
|
|
26
|
+
# index_alchemy_pictures_on_image_file_name (image_file_name)
|
|
27
|
+
# index_alchemy_pictures_on_name (name)
|
|
28
|
+
# index_alchemy_pictures_on_updater_id (updater_id)
|
|
21
29
|
#
|
|
22
30
|
|
|
23
31
|
module Alchemy
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
# == Schema Information
|
|
2
|
+
#
|
|
3
|
+
# Table name: alchemy_picture_descriptions
|
|
4
|
+
#
|
|
5
|
+
# id :integer not null, primary key
|
|
6
|
+
# text :text
|
|
7
|
+
# created_at :datetime not null
|
|
8
|
+
# updated_at :datetime not null
|
|
9
|
+
# language_id :integer not null
|
|
10
|
+
# picture_id :integer not null
|
|
11
|
+
#
|
|
12
|
+
# Indexes
|
|
13
|
+
#
|
|
14
|
+
# alchemy_picture_descriptions_on_picture_id_and_language_id (picture_id,language_id) UNIQUE
|
|
15
|
+
# index_alchemy_picture_descriptions_on_language_id (language_id)
|
|
16
|
+
# index_alchemy_picture_descriptions_on_picture_id (picture_id)
|
|
17
|
+
#
|
|
18
|
+
# Foreign Keys
|
|
19
|
+
#
|
|
20
|
+
# language_id (language_id => alchemy_languages.id)
|
|
21
|
+
# picture_id (picture_id => alchemy_pictures.id)
|
|
22
|
+
#
|
|
1
23
|
module Alchemy
|
|
2
24
|
class PictureDescription < ActiveRecord::Base
|
|
3
25
|
belongs_to :picture, class_name: "Alchemy::Picture"
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# == Schema Information
|
|
4
|
+
#
|
|
5
|
+
# Table name: alchemy_picture_thumbs
|
|
6
|
+
#
|
|
7
|
+
# id :integer not null, primary key
|
|
8
|
+
# signature :string not null
|
|
9
|
+
# uid :text not null
|
|
10
|
+
# picture_id :integer not null
|
|
11
|
+
#
|
|
12
|
+
# Indexes
|
|
13
|
+
#
|
|
14
|
+
# index_alchemy_picture_thumbs_on_picture_id (picture_id)
|
|
15
|
+
# index_alchemy_picture_thumbs_on_signature (signature) UNIQUE
|
|
16
|
+
#
|
|
17
|
+
# Foreign Keys
|
|
18
|
+
#
|
|
19
|
+
# picture_id (picture_id => alchemy_pictures.id)
|
|
20
|
+
#
|
|
3
21
|
module Alchemy
|
|
4
22
|
# The persisted version of a rendered picture variant
|
|
5
23
|
#
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
module Publishable
|
|
5
|
+
# Default resolver for Publishable records.
|
|
6
|
+
#
|
|
7
|
+
# Uses the +public_on+ and +public_until+ timestamp columns to determine
|
|
8
|
+
# whether a record is public, scheduled, or publishable.
|
|
9
|
+
#
|
|
10
|
+
# This class defines the interface that custom resolvers configured via
|
|
11
|
+
# +Alchemy.config.publishable_resolver+ must implement.
|
|
12
|
+
#
|
|
13
|
+
class TimestampResolver
|
|
14
|
+
class << self
|
|
15
|
+
# Returns records without a +public_on+ date set.
|
|
16
|
+
def draft(publishables)
|
|
17
|
+
publishables.where(public_on: nil)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns records with a +public_on+ date set.
|
|
21
|
+
def scheduled(publishables)
|
|
22
|
+
publishables.where.not(public_on: nil)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns records that are public at the given time.
|
|
26
|
+
def published(publishables, at:)
|
|
27
|
+
table_name = publishables.table_name
|
|
28
|
+
scheduled(publishables)
|
|
29
|
+
.where("#{table_name}.public_on <= :at", at:)
|
|
30
|
+
.where("#{table_name}.public_until IS NULL OR #{table_name}.public_until > :at", at:)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(publishable)
|
|
35
|
+
@publishable = publishable
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Determines if the record is public at the given time.
|
|
39
|
+
def public?(at: Current.preview_time)
|
|
40
|
+
already_public_for?(at:) && still_public_for?(at:)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Determines if the record has a future publication or expiration event.
|
|
44
|
+
def scheduled?(at: Current.preview_time)
|
|
45
|
+
(publishable.public_on.present? && publishable.public_on > at) ||
|
|
46
|
+
(publishable.public_until.present? && publishable.public_until > at)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Determines if the record is publishable.
|
|
50
|
+
#
|
|
51
|
+
# A record is publishable if a +public_on+ timestamp is set and not
|
|
52
|
+
# expired yet.
|
|
53
|
+
def publishable?
|
|
54
|
+
!publishable.public_on.nil? && still_public_for?(at: Time.current)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
attr_reader :publishable
|
|
60
|
+
|
|
61
|
+
def already_public_for?(at: Current.preview_time)
|
|
62
|
+
!publishable.public_on.nil? && publishable.public_on <= at
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def still_public_for?(at: Current.preview_time)
|
|
66
|
+
publishable.public_until.nil? || publishable.public_until > at
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -229,7 +229,7 @@ module Alchemy
|
|
|
229
229
|
# Joins all searchable attribute names into a Ransack compatible search query
|
|
230
230
|
#
|
|
231
231
|
def search_field_name
|
|
232
|
-
searchable_attribute_names.join("_or_") + "
|
|
232
|
+
searchable_attribute_names.join("_or_") + "_i_cont"
|
|
233
233
|
end
|
|
234
234
|
|
|
235
235
|
def in_engine?
|