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,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
module Admin
|
|
5
|
+
# The Content Security Policy Alchemy applies to its own admin responses.
|
|
6
|
+
#
|
|
7
|
+
# Not configured by default. Opt in with:
|
|
8
|
+
#
|
|
9
|
+
# Alchemy.config.admin_content_security_policy = "Alchemy::Admin::ContentSecurityPolicy"
|
|
10
|
+
#
|
|
11
|
+
# 'self' follows the origin the admin is served from, so mounting it under
|
|
12
|
+
# its own subdomain through +Alchemy.admin_constraints+ needs no extra
|
|
13
|
+
# configuration. The page preview is same origin as well, because it falls
|
|
14
|
+
# back to an admin path, unless a preview host is configured, in which case
|
|
15
|
+
# that host is added to +frame-src+.
|
|
16
|
+
#
|
|
17
|
+
# +img-src+ is the exception: it allows any https host, because the host
|
|
18
|
+
# serving the pictures is not knowable here. Narrow it in a subclass, or
|
|
19
|
+
# keep pictures same origin with
|
|
20
|
+
# +config.active_storage.resolve_model_to_route = :rails_storage_proxy+.
|
|
21
|
+
#
|
|
22
|
+
# Assets your application adds through +admin_stylesheets+ or through
|
|
23
|
+
# +Alchemy.importmap+ are picked up automatically, so a module pinned to a
|
|
24
|
+
# CDN does not need any extra configuration. Subclass to allow anything
|
|
25
|
+
# else, and configure the subclass instead:
|
|
26
|
+
#
|
|
27
|
+
# class AdminContentSecurityPolicy < Alchemy::Admin::ContentSecurityPolicy
|
|
28
|
+
# def call
|
|
29
|
+
# super.tap do |policy|
|
|
30
|
+
# policy.connect_src(*policy.directives["connect-src"], "https://api.example.com")
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
class ContentSecurityPolicy
|
|
36
|
+
class InvalidSourceError < StandardError; end
|
|
37
|
+
|
|
38
|
+
# Rails calls an asset host proc with the asset source, so we need one to
|
|
39
|
+
# evaluate it with. Any source resolves to the same origin unless the
|
|
40
|
+
# host shards, which the %d form covers separately.
|
|
41
|
+
ASSET_SOURCE = "/"
|
|
42
|
+
|
|
43
|
+
def initialize(request = nil)
|
|
44
|
+
@request = request
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
attr_reader :request
|
|
48
|
+
# Send the policy as Content-Security-Policy-Report-Only, which reports
|
|
49
|
+
# violations to the browser console and to +report_uri+ without blocking
|
|
50
|
+
# anything. Worth running first, to find what a policy would break.
|
|
51
|
+
def report_only? = false
|
|
52
|
+
|
|
53
|
+
# Authorizes the inline scripts Alchemy renders. It has to be
|
|
54
|
+
# unguessable, otherwise injected markup could carry a valid nonce.
|
|
55
|
+
def nonce_generator = ->(_request) { SecureRandom.base64(16) }
|
|
56
|
+
|
|
57
|
+
def call
|
|
58
|
+
ActionDispatch::ContentSecurityPolicy.new do |policy|
|
|
59
|
+
policy.default_src :self
|
|
60
|
+
# script-src and style-src have to be named explicitly, because Rails
|
|
61
|
+
# only appends the nonce to directives the policy actually declares.
|
|
62
|
+
policy.script_src :self, *asset_hosts, *importmap_origins
|
|
63
|
+
policy.style_src :self, *asset_hosts, *admin_stylesheet_origins
|
|
64
|
+
# Inline style attributes cannot carry a nonce. Turbo sets them for
|
|
65
|
+
# its progress bar, and so do several of our bundled dependencies.
|
|
66
|
+
policy.style_src_attr :unsafe_inline
|
|
67
|
+
# Pictures come from wherever the storage backend puts them, which
|
|
68
|
+
# cannot be read from here. ActiveStorage redirects to a signed
|
|
69
|
+
# service URL, and a redirect still has to match the host.
|
|
70
|
+
policy.img_src :self, :data, :blob, :https
|
|
71
|
+
policy.font_src :self, :data, *asset_hosts
|
|
72
|
+
policy.media_src :self, :blob, *asset_hosts
|
|
73
|
+
policy.connect_src :self, *asset_hosts
|
|
74
|
+
policy.frame_src :self, *preview_hosts
|
|
75
|
+
policy.object_src :none
|
|
76
|
+
policy.base_uri :self
|
|
77
|
+
policy.form_action :self
|
|
78
|
+
# An endpoint of your own that the browser POSTs a JSON report to
|
|
79
|
+
# whenever it blocks something:
|
|
80
|
+
#
|
|
81
|
+
# policy.report_uri "/csp-violation-reports"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def asset_hosts
|
|
88
|
+
host = ActionController::Base.asset_host
|
|
89
|
+
return [] if host.blank?
|
|
90
|
+
|
|
91
|
+
hosts = if host.respond_to?(:call)
|
|
92
|
+
[call_asset_host(host)]
|
|
93
|
+
elsif host.include?("%d")
|
|
94
|
+
# Rails shards these over four hosts.
|
|
95
|
+
4.times.map { host % _1 }
|
|
96
|
+
else
|
|
97
|
+
[host]
|
|
98
|
+
end
|
|
99
|
+
hosts.filter_map { origin(_1) }.uniq
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def call_asset_host(host)
|
|
103
|
+
arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity
|
|
104
|
+
args = [ASSET_SOURCE]
|
|
105
|
+
args << request if request && (arity > 1 || arity < 0)
|
|
106
|
+
host.call(*args)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Modules can be pinned to a CDN, in which case the pin holds an absolute
|
|
110
|
+
# URL instead of an asset name.
|
|
111
|
+
def importmap_origins
|
|
112
|
+
Alchemy.importmap.packages.values.filter_map { origin(_1.path) }.uniq
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Admin stylesheets are usually asset names, but an absolute URL is
|
|
116
|
+
# allowed here too.
|
|
117
|
+
def admin_stylesheet_origins
|
|
118
|
+
Alchemy.config.admin_stylesheets.filter_map { origin(_1) }.uniq
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A CSP source is an origin, so anything with a host contributes one and
|
|
122
|
+
# everything else (asset names, module specifiers) is same origin already.
|
|
123
|
+
def origin(url)
|
|
124
|
+
uri = URI.parse(url.to_s)
|
|
125
|
+
return nil unless uri.host
|
|
126
|
+
|
|
127
|
+
port = ":#{uri.port}" if uri.port && uri.port != uri.default_port
|
|
128
|
+
"#{uri.scheme ? "#{uri.scheme}://" : "//"}#{uri.host}#{port}"
|
|
129
|
+
rescue URI::InvalidURIError => error
|
|
130
|
+
raise InvalidSourceError, "Cannot build a Content Security Policy source from " \
|
|
131
|
+
"#{url.inspect} (#{error.message}). It came from your asset host, your " \
|
|
132
|
+
"admin stylesheets, an importmap pin or a preview host."
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The page editor renders the frontend of the application in an iframe,
|
|
136
|
+
# which is a different host whenever a preview host is configured.
|
|
137
|
+
def preview_hosts
|
|
138
|
+
preview = Alchemy.config.preview
|
|
139
|
+
([preview.host] + preview.per_site_configs.map(&:host)).filter_map { origin(_1) }.uniq
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -11,6 +11,13 @@ module Alchemy
|
|
|
11
11
|
@resource_name = resource_name
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# The strong parameters entries needed to permit this filter's value(s)
|
|
15
|
+
# under `q`. Override in subclasses whose input can submit a shape
|
|
16
|
+
# other than a single scalar value (e.g. an array from `<select multiple>`).
|
|
17
|
+
def permitted_search_params
|
|
18
|
+
[name]
|
|
19
|
+
end
|
|
20
|
+
|
|
14
21
|
def applied_filter_component(search_filter_params:, resource_url_proxy:, query:)
|
|
15
22
|
Alchemy::Admin::Resource::AppliedFilter.new(
|
|
16
23
|
link: dismiss_filter_url(search_filter_params, resource_url_proxy),
|
|
@@ -10,9 +10,13 @@ module Alchemy
|
|
|
10
10
|
# @param name [String] The name of the filter.
|
|
11
11
|
# @param resource_name [String] The name of the resource.
|
|
12
12
|
# @param options [Proc, Array] A proc that returns the options for the select, or an array of options.
|
|
13
|
-
|
|
13
|
+
# @param multiple [Boolean, Proc] Whether multiple values can be selected, or a proc called with the search filter params that returns a Boolean.
|
|
14
|
+
# @param include_blank [Boolean, String, Proc] Whether (or with what label) to include a blank option, or a proc called with the search filter params that returns a Boolean/String.
|
|
15
|
+
def initialize(name:, resource_name:, options:, multiple: false, include_blank: true)
|
|
14
16
|
super(name:, resource_name:)
|
|
15
17
|
@options = options_to_proc(options)
|
|
18
|
+
@multiple = bool_or_proc(multiple)
|
|
19
|
+
@include_blank = bool_or_proc(include_blank)
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
# Returns a select filter component.
|
|
@@ -31,18 +35,27 @@ module Alchemy
|
|
|
31
35
|
)
|
|
32
36
|
end
|
|
33
37
|
|
|
38
|
+
# `multiple` can be enabled dynamically via a Proc, so both the
|
|
39
|
+
# scalar and array shape of the submitted value need to be permitted.
|
|
40
|
+
def permitted_search_params
|
|
41
|
+
[name, {name => []}]
|
|
42
|
+
end
|
|
43
|
+
|
|
34
44
|
private
|
|
35
45
|
|
|
36
46
|
def multiple?(params)
|
|
37
|
-
|
|
47
|
+
@multiple.call(params)
|
|
38
48
|
end
|
|
39
49
|
|
|
40
50
|
def include_blank(params)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
value = @include_blank.call(params)
|
|
52
|
+
return value unless value == true
|
|
53
|
+
|
|
54
|
+
Alchemy.t(:all, scope: [:filters, resource_name, name])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def bool_or_proc(value)
|
|
58
|
+
value.is_a?(Proc) ? value : ->(_params) { value }
|
|
46
59
|
end
|
|
47
60
|
|
|
48
61
|
def options_to_proc(options)
|
|
@@ -5,16 +5,23 @@
|
|
|
5
5
|
# Table name: alchemy_attachments
|
|
6
6
|
#
|
|
7
7
|
# id :integer not null, primary key
|
|
8
|
-
#
|
|
9
|
-
# file_name :string
|
|
8
|
+
# cached_tag_list :text
|
|
10
9
|
# file_mime_type :string
|
|
10
|
+
# file_name :string
|
|
11
11
|
# file_size :integer
|
|
12
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# file_uid :string
|
|
13
|
+
# name :string
|
|
14
14
|
# created_at :datetime not null
|
|
15
15
|
# updated_at :datetime not null
|
|
16
|
-
#
|
|
17
|
-
#
|
|
16
|
+
# creator_id :integer
|
|
17
|
+
# updater_id :integer
|
|
18
|
+
#
|
|
19
|
+
# Indexes
|
|
20
|
+
#
|
|
21
|
+
# index_alchemy_attachments_on_created_at (created_at)
|
|
22
|
+
# index_alchemy_attachments_on_creator_id (creator_id)
|
|
23
|
+
# index_alchemy_attachments_on_file_uid (file_uid)
|
|
24
|
+
# index_alchemy_attachments_on_updater_id (updater_id)
|
|
18
25
|
#
|
|
19
26
|
|
|
20
27
|
module Alchemy
|
|
@@ -199,7 +206,7 @@ module Alchemy
|
|
|
199
206
|
private
|
|
200
207
|
|
|
201
208
|
def file_type_allowed
|
|
202
|
-
unless extension&.in?(self.class.allowed_filetypes)
|
|
209
|
+
unless extension&.downcase&.in?(self.class.allowed_filetypes.map(&:downcase))
|
|
203
210
|
errors.add(:file, Alchemy.t("not a valid file"))
|
|
204
211
|
end
|
|
205
212
|
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# Normalized HTTP Cache-Control directives derived from a page layout `cache`
|
|
5
|
+
# or element `page_cache` config value. Combine two values with `#&` to get the
|
|
6
|
+
# most restrictive of the two — this is how the page layout and its elements
|
|
7
|
+
# are merged (elements can only tighten, never loosen).
|
|
8
|
+
class CacheControl
|
|
9
|
+
VISIBILITIES = [:public, :private].freeze
|
|
10
|
+
|
|
11
|
+
# @param value [nil, true, false, Integer, Hash, CacheControl]
|
|
12
|
+
# @return [CacheControl]
|
|
13
|
+
def self.parse(value)
|
|
14
|
+
case value
|
|
15
|
+
when CacheControl then value
|
|
16
|
+
when nil, true then new(default: true)
|
|
17
|
+
when false then new(no_store: true)
|
|
18
|
+
when Integer then new(max_age: value)
|
|
19
|
+
when Hash then from_hash(value)
|
|
20
|
+
else new(default: true)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.from_hash(hash)
|
|
25
|
+
options = hash.transform_keys(&:to_sym)
|
|
26
|
+
new(
|
|
27
|
+
no_store: !!options[:no_store],
|
|
28
|
+
no_cache: !!options[:no_cache],
|
|
29
|
+
max_age: options[:max_age],
|
|
30
|
+
visibility: (options[:visibility] || :public).to_sym
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
private_class_method :from_hash
|
|
34
|
+
|
|
35
|
+
attr_reader :max_age, :visibility
|
|
36
|
+
|
|
37
|
+
def initialize(no_store: false, no_cache: false, max_age: nil, visibility: :public, default: false)
|
|
38
|
+
@no_store = no_store
|
|
39
|
+
@no_cache = no_cache
|
|
40
|
+
@max_age = max_age
|
|
41
|
+
@visibility = visibility
|
|
42
|
+
@default = default
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def no_store?
|
|
46
|
+
@no_store
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def no_cache?
|
|
50
|
+
@no_cache
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def default?
|
|
54
|
+
@default
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Whether the response is flagged `public` to HTTP caches. A no-cache
|
|
58
|
+
# response is never public: Rails only pairs no-cache with public, and
|
|
59
|
+
# "public" adds nothing to an always-revalidated response.
|
|
60
|
+
def public?
|
|
61
|
+
visibility == :public && !no_cache?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def private?
|
|
65
|
+
visibility == :private
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Most restrictive of self and other.
|
|
69
|
+
def &(other)
|
|
70
|
+
return self if other.nil?
|
|
71
|
+
return self.class.new(no_store: true) if no_store? || other.no_store?
|
|
72
|
+
|
|
73
|
+
vis = (private? || other.private?) ? :private : :public
|
|
74
|
+
return self.class.new(no_cache: true, visibility: vis) if no_cache? || other.no_cache?
|
|
75
|
+
|
|
76
|
+
self.class.new(visibility: vis, max_age: min_max_age(other))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def restrict_visibility
|
|
80
|
+
return self if no_store? || private?
|
|
81
|
+
|
|
82
|
+
self.class.new(no_cache: no_cache?, max_age: max_age, visibility: :private)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def ==(other)
|
|
86
|
+
other.is_a?(CacheControl) &&
|
|
87
|
+
no_store? == other.no_store? &&
|
|
88
|
+
no_cache? == other.no_cache? &&
|
|
89
|
+
max_age == other.max_age &&
|
|
90
|
+
visibility == other.visibility
|
|
91
|
+
end
|
|
92
|
+
alias_method :eql?, :==
|
|
93
|
+
|
|
94
|
+
def hash
|
|
95
|
+
[no_store?, no_cache?, max_age, visibility].hash
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def min_max_age(other)
|
|
101
|
+
[max_age, other.max_age].compact.min
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -5,20 +5,34 @@
|
|
|
5
5
|
# Table name: alchemy_elements
|
|
6
6
|
#
|
|
7
7
|
# id :integer not null, primary key
|
|
8
|
+
# cached_tag_list :text
|
|
9
|
+
# fixed :boolean default(FALSE), not null
|
|
10
|
+
# folded :boolean default(FALSE), not null
|
|
8
11
|
# name :string
|
|
9
12
|
# position :integer
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# fixed :boolean default(FALSE)
|
|
14
|
-
# folded :boolean default(FALSE)
|
|
15
|
-
# unique :boolean default(FALSE)
|
|
13
|
+
# public_on :datetime
|
|
14
|
+
# public_until :datetime
|
|
15
|
+
# unique :boolean default(FALSE), not null
|
|
16
16
|
# created_at :datetime not null
|
|
17
17
|
# updated_at :datetime not null
|
|
18
18
|
# creator_id :integer
|
|
19
|
-
#
|
|
20
|
-
# cached_tag_list :text
|
|
19
|
+
# page_version_id :integer not null
|
|
21
20
|
# parent_element_id :integer
|
|
21
|
+
# updater_id :integer
|
|
22
|
+
#
|
|
23
|
+
# Indexes
|
|
24
|
+
#
|
|
25
|
+
# idx_alchemy_elements_on_page_version_id_and_parent_element_id (page_version_id,parent_element_id)
|
|
26
|
+
# idx_alchemy_elements_on_page_version_id_and_position (page_version_id,position)
|
|
27
|
+
# index_alchemy_elements_on_creator_id (creator_id)
|
|
28
|
+
# index_alchemy_elements_on_fixed (fixed)
|
|
29
|
+
# index_alchemy_elements_on_page_version_id (page_version_id)
|
|
30
|
+
# index_alchemy_elements_on_public_on_and_public_until (public_on,public_until)
|
|
31
|
+
# index_alchemy_elements_on_updater_id (updater_id)
|
|
32
|
+
#
|
|
33
|
+
# Foreign Keys
|
|
34
|
+
#
|
|
35
|
+
# page_version_id (page_version_id => alchemy_page_versions.id) ON DELETE => cascade
|
|
22
36
|
#
|
|
23
37
|
|
|
24
38
|
require_dependency "alchemy/element/definitions"
|
|
@@ -49,6 +63,7 @@ module Alchemy
|
|
|
49
63
|
"ingredients",
|
|
50
64
|
"message",
|
|
51
65
|
"nestable_elements",
|
|
66
|
+
"page_cache",
|
|
52
67
|
"searchable",
|
|
53
68
|
"taggable",
|
|
54
69
|
"warning"
|
|
@@ -13,6 +13,7 @@ module Alchemy
|
|
|
13
13
|
attribute :taggable, :boolean, default: false
|
|
14
14
|
attribute :compact, :boolean, default: false
|
|
15
15
|
attribute :fixed, :boolean, default: false
|
|
16
|
+
attribute :page_cache, Alchemy::CacheControlType.new, default: true
|
|
16
17
|
attribute :ingredients, default: []
|
|
17
18
|
attribute :nestable_elements, default: []
|
|
18
19
|
attribute :autogenerate, default: []
|
|
@@ -125,6 +125,17 @@ module Alchemy
|
|
|
125
125
|
elements.each(&)
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
+
# Allows the repository to be passed to the ActionView collection renderer
|
|
129
|
+
# (+render repository+), which detects a collection by +to_ary+.
|
|
130
|
+
def to_ary
|
|
131
|
+
elements
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def size
|
|
135
|
+
elements.size
|
|
136
|
+
end
|
|
137
|
+
alias_method :length, :size
|
|
138
|
+
|
|
128
139
|
private
|
|
129
140
|
|
|
130
141
|
attr_reader :elements
|
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
# Table name: alchemy_folded_pages
|
|
6
6
|
#
|
|
7
7
|
# id :integer not null, primary key
|
|
8
|
+
# folded :boolean default(FALSE), not null
|
|
8
9
|
# page_id :integer not null
|
|
9
10
|
# user_id :integer not null
|
|
10
|
-
#
|
|
11
|
+
#
|
|
12
|
+
# Indexes
|
|
13
|
+
#
|
|
14
|
+
# index_alchemy_folded_pages_on_page_id_and_user_id (page_id,user_id) UNIQUE
|
|
11
15
|
#
|
|
12
16
|
|
|
13
17
|
module Alchemy
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# == Schema Information
|
|
4
|
+
#
|
|
5
|
+
# Table name: alchemy_ingredients
|
|
6
|
+
#
|
|
7
|
+
# id :integer not null, primary key
|
|
8
|
+
# data :json
|
|
9
|
+
# related_object_type :string
|
|
10
|
+
# role :string not null
|
|
11
|
+
# type :string not null
|
|
12
|
+
# value :text
|
|
13
|
+
# created_at :datetime not null
|
|
14
|
+
# updated_at :datetime not null
|
|
15
|
+
# element_id :integer not null
|
|
16
|
+
# related_object_id :integer
|
|
17
|
+
#
|
|
18
|
+
# Indexes
|
|
19
|
+
#
|
|
20
|
+
# idx_alchemy_ingredient_relation (related_object_id,related_object_type)
|
|
21
|
+
# index_alchemy_ingredients_on_element_id (element_id)
|
|
22
|
+
# index_alchemy_ingredients_on_element_id_and_role (element_id,role) UNIQUE
|
|
23
|
+
# index_alchemy_ingredients_on_type (type)
|
|
24
|
+
#
|
|
25
|
+
# Foreign Keys
|
|
26
|
+
#
|
|
27
|
+
# element_id (element_id => alchemy_elements.id) ON DELETE => cascade
|
|
28
|
+
#
|
|
3
29
|
module Alchemy
|
|
4
30
|
class Ingredient < BaseRecord
|
|
5
31
|
class DefinitionError < StandardError; end
|
|
@@ -31,6 +31,7 @@ module Alchemy
|
|
|
31
31
|
crop
|
|
32
32
|
css_classes
|
|
33
33
|
fixed_ratio
|
|
34
|
+
format
|
|
34
35
|
linkable
|
|
35
36
|
size
|
|
36
37
|
sizes
|
|
@@ -38,6 +39,8 @@ module Alchemy
|
|
|
38
39
|
upsample
|
|
39
40
|
]
|
|
40
41
|
|
|
42
|
+
validates_with Alchemy::SafeUrlValidator, attributes: [:link]
|
|
43
|
+
|
|
41
44
|
delegate :description_for, :name, to: :picture, allow_nil: true
|
|
42
45
|
|
|
43
46
|
def alt_text(language: Alchemy::Current.language)
|
|
@@ -5,19 +5,31 @@
|
|
|
5
5
|
# Table name: alchemy_languages
|
|
6
6
|
#
|
|
7
7
|
# id :integer not null, primary key
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# country_code :string default(""), not null
|
|
9
|
+
# default :boolean default(FALSE), not null
|
|
10
10
|
# frontpage_name :string
|
|
11
|
+
# language_code :string
|
|
12
|
+
# locale :string
|
|
13
|
+
# name :string
|
|
11
14
|
# page_layout :string default("intro")
|
|
12
|
-
# public :boolean default(FALSE)
|
|
15
|
+
# public :boolean default(FALSE), not null
|
|
13
16
|
# created_at :datetime not null
|
|
14
17
|
# updated_at :datetime not null
|
|
15
18
|
# creator_id :integer
|
|
16
|
-
# updater_id :integer
|
|
17
|
-
# default :boolean default(FALSE)
|
|
18
|
-
# country_code :string default(""), not null
|
|
19
19
|
# site_id :integer not null
|
|
20
|
-
#
|
|
20
|
+
# updater_id :integer
|
|
21
|
+
#
|
|
22
|
+
# Indexes
|
|
23
|
+
#
|
|
24
|
+
# index_alchemy_languages_on_creator_id (creator_id)
|
|
25
|
+
# index_alchemy_languages_on_language_code (language_code)
|
|
26
|
+
# index_alchemy_languages_on_language_code_and_country_code (language_code,country_code)
|
|
27
|
+
# index_alchemy_languages_on_site_id (site_id)
|
|
28
|
+
# index_alchemy_languages_on_updater_id (updater_id)
|
|
29
|
+
#
|
|
30
|
+
# Foreign Keys
|
|
31
|
+
#
|
|
32
|
+
# site_id (site_id => alchemy_sites.id)
|
|
21
33
|
#
|
|
22
34
|
|
|
23
35
|
require_dependency "alchemy/site"
|
|
@@ -6,9 +6,14 @@
|
|
|
6
6
|
#
|
|
7
7
|
# id :integer not null, primary key
|
|
8
8
|
# urlname :string not null
|
|
9
|
-
# page_id :integer not null
|
|
10
9
|
# created_at :datetime not null
|
|
11
10
|
# updated_at :datetime not null
|
|
11
|
+
# page_id :integer not null
|
|
12
|
+
#
|
|
13
|
+
# Indexes
|
|
14
|
+
#
|
|
15
|
+
# index_alchemy_legacy_page_urls_on_page_id (page_id)
|
|
16
|
+
# index_alchemy_legacy_page_urls_on_urlname (urlname)
|
|
12
17
|
#
|
|
13
18
|
|
|
14
19
|
class Alchemy::LegacyPageUrl < ActiveRecord::Base
|