<%= f.label :tag_list %>
diff --git a/app/views/alchemy/admin/elements/update.js.erb b/app/views/alchemy/admin/elements/update.js.erb
index 8b4f9c7c17..15c64d4bf6 100644
--- a/app/views/alchemy/admin/elements/update.js.erb
+++ b/app/views/alchemy/admin/elements/update.js.erb
@@ -1,7 +1,7 @@
(function() {
var $el = $('#element_<%= @element.id %>');
var $errors = $('#element_<%= @element.id %>_errors');
- $('> .element-content .content_editor', $el).removeClass('validation_failed');
+ $('> .element-content .content_editor, > .element-content .ingredient_editor', $el).removeClass('validation_failed');
<%- if @element_validated -%>
@@ -15,9 +15,10 @@
<%- else -%>
Alchemy.growl('<%= j @notice %>', 'warn');
- $errors.html('<%= j @error_message %>
- <%== j @element.essence_error_messages.join("
- ") %>
');
+ $errors.html('<%= j @error_message %>
- <%== j @error_messages.join("
- ") %>
');
$errors.show();
$('<%= @element.contents_with_errors.map { |content| "#" + content.dom_id }.join(", ") %>').addClass('validation_failed');
+ $('<%== @element.ingredients_with_errors.map { |ingredient| "[data-ingredient-id=\"#{ingredient.id}\"]" }.join(", ") %>').addClass('validation_failed');
Alchemy.Buttons.enable($el);
<%- end -%>
diff --git a/app/views/alchemy/admin/ingredients/_audio_fields.html.erb b/app/views/alchemy/admin/ingredients/_audio_fields.html.erb
new file mode 100644
index 0000000000..052e93f2c7
--- /dev/null
+++ b/app/views/alchemy/admin/ingredients/_audio_fields.html.erb
@@ -0,0 +1,4 @@
+<%= f.input :autoplay, as: :boolean %>
+<%= f.input :controls, as: :boolean %>
+<%= f.input :loop, as: :boolean %>
+<%= f.input :muted, as: :boolean %>
diff --git a/app/views/alchemy/admin/ingredients/_file_fields.html.erb b/app/views/alchemy/admin/ingredients/_file_fields.html.erb
new file mode 100644
index 0000000000..ab4a675bfa
--- /dev/null
+++ b/app/views/alchemy/admin/ingredients/_file_fields.html.erb
@@ -0,0 +1,18 @@
+<% css_classes = ingredient.settings[:css_classes] %>
+
+<%= f.input :link_text %>
+<%= f.input :title %>
+<%- if css_classes.present? -%>
+ <%= f.input :css_class,
+ collection: css_classes,
+ include_blank: Alchemy.t('None'),
+ input_html: {class: 'alchemy_selectbox'} %>
+<%- else -%>
+ <%= f.input :css_class,
+ label: Alchemy.t(:position_in_text),
+ collection: [
+ [Alchemy.t(:above), "no_float"],
+ [Alchemy.t(:left), "left"],
+ [Alchemy.t(:right), "right"]
+ ], include_blank: Alchemy.t('Layout default'), input_html: {class: 'alchemy_selectbox'} %>
+<%- end -%>
diff --git a/app/views/alchemy/admin/ingredients/_picture_fields.html.erb b/app/views/alchemy/admin/ingredients/_picture_fields.html.erb
new file mode 100644
index 0000000000..f0d826e4a1
--- /dev/null
+++ b/app/views/alchemy/admin/ingredients/_picture_fields.html.erb
@@ -0,0 +1,25 @@
+<%= f.input :caption, as: ingredient.settings[:caption_as_textarea] ? 'text' : 'string' %>
+<%= f.input :title %>
+<%= f.input :alt_tag %>
+<%- if ingredient.settings[:sizes].present? && ingredient.settings[:srcset].blank? -%>
+ <%= f.input :render_size,
+ collection: [
+ [Alchemy.t('Layout default'), ""]
+ ] + ingredient.settings[:sizes].to_a,
+ include_blank: false,
+ input_html: {class: 'alchemy_selectbox'} %>
+<%- end -%>
+<%- if ingredient.settings[:css_classes].present? -%>
+ <%= f.input :css_class,
+ collection: ingredient.settings[:css_classes],
+ include_blank: Alchemy.t('None'),
+ input_html: {class: 'alchemy_selectbox'} %>
+<%- else -%>
+ <%= f.input :css_class,
+ label: Alchemy.t(:position_in_text),
+ collection: [
+ [Alchemy.t(:above), "no_float"],
+ [Alchemy.t(:left), "left"],
+ [Alchemy.t(:right), "right"]
+ ], include_blank: Alchemy.t("Layout default"), input_html: {class: 'alchemy_selectbox'} %>
+<%- end -%>
diff --git a/app/views/alchemy/admin/ingredients/_video_fields.html.erb b/app/views/alchemy/admin/ingredients/_video_fields.html.erb
new file mode 100644
index 0000000000..e618f26301
--- /dev/null
+++ b/app/views/alchemy/admin/ingredients/_video_fields.html.erb
@@ -0,0 +1,8 @@
+<%= f.input :width %>
+<%= f.input :height %>
+<%= f.input :autoplay, as: :boolean %>
+<%= f.input :controls, as: :boolean %>
+<%= f.input :loop, as: :boolean %>
+<%= f.input :muted, as: :boolean %>
+<%= f.input :preload, collection: %w(auto none metadata),
+ include_blank: false, input_html: {class: 'alchemy_selectbox'} %>
diff --git a/app/views/alchemy/admin/ingredients/edit.html.erb b/app/views/alchemy/admin/ingredients/edit.html.erb
new file mode 100644
index 0000000000..27d5a8c0ff
--- /dev/null
+++ b/app/views/alchemy/admin/ingredients/edit.html.erb
@@ -0,0 +1,4 @@
+<%= alchemy_form_for @ingredient, as: :ingredient, url: alchemy.admin_ingredient_path(@ingredient) do |f| %>
+ <%= render "alchemy/admin/ingredients/#{@ingredient.partial_name}_fields", ingredient: @ingredient, f: f %>
+ <%= f.submit Alchemy.t(:save) %>
+<% end %>
diff --git a/app/views/alchemy/admin/pages/_tinymce_custom_config.html.erb b/app/views/alchemy/admin/pages/_tinymce_custom_config.html.erb
index 850c18f837..09c14da524 100644
--- a/app/views/alchemy/admin/pages/_tinymce_custom_config.html.erb
+++ b/app/views/alchemy/admin/pages/_tinymce_custom_config.html.erb
@@ -1,7 +1,10 @@
diff --git a/app/views/alchemy/ingredients/_link_view.html.erb b/app/views/alchemy/ingredients/_link_view.html.erb
new file mode 100644
index 0000000000..e7e54711a8
--- /dev/null
+++ b/app/views/alchemy/ingredients/_link_view.html.erb
@@ -0,0 +1,9 @@
+<%- if link_view.value.present? -%>
+<%- html_options = {
+ target: link_view.link_target == "blank" ? "_blank" : nil
+}.merge(local_assigns.fetch(:html_options, {})) -%>
+<%= link_to(link_view.value, html_options) do -%>
+<%= link_view.settings_value(:text, local_assigns.fetch(:options, {})) ||
+ link_view.value -%>
+<%- end -%>
+<%- end -%>
diff --git a/app/views/alchemy/ingredients/_node_editor.html.erb b/app/views/alchemy/ingredients/_node_editor.html.erb
new file mode 100644
index 0000000000..5d9bdacfab
--- /dev/null
+++ b/app/views/alchemy/ingredients/_node_editor.html.erb
@@ -0,0 +1,25 @@
+<%= content_tag :div,
+ class: node_editor.css_classes,
+ data: node_editor.data_attributes do %>
+ <%= element_form.fields_for(:ingredients, node_editor.ingredient) do |f| %>
+ <%= ingredient_label(node_editor, :node_id) %>
+ <%= f.text_field :node_id,
+ value: node_editor.node&.id,
+ class: 'alchemy_selectbox full_width' %>
+ <% end %>
+<% end %>
+
+
diff --git a/app/views/alchemy/ingredients/_node_view.html.erb b/app/views/alchemy/ingredients/_node_view.html.erb
new file mode 100644
index 0000000000..99a6c4b4eb
--- /dev/null
+++ b/app/views/alchemy/ingredients/_node_view.html.erb
@@ -0,0 +1 @@
+<%= render node_view.node if node_view.node %>
diff --git a/app/views/alchemy/ingredients/_page_editor.html.erb b/app/views/alchemy/ingredients/_page_editor.html.erb
new file mode 100644
index 0000000000..b3fde2f273
--- /dev/null
+++ b/app/views/alchemy/ingredients/_page_editor.html.erb
@@ -0,0 +1,24 @@
+<%= content_tag :div,
+ class: page_editor.css_classes,
+ data: page_editor.data_attributes do %>
+ <%= element_form.fields_for(:ingredients, page_editor.ingredient) do |f| %>
+ <%= ingredient_label(page_editor, :page_id) %>
+ <%= f.text_field :page_id,
+ value: page_editor.page&.id,
+ class: 'alchemy_selectbox full_width' %>
+ <% end %>
+<% end %>
+
+
diff --git a/app/views/alchemy/ingredients/_page_view.html.erb b/app/views/alchemy/ingredients/_page_view.html.erb
new file mode 100644
index 0000000000..3848505c95
--- /dev/null
+++ b/app/views/alchemy/ingredients/_page_view.html.erb
@@ -0,0 +1,4 @@
+<% page = page_view.page %>
+<% if page %>
+<%= link_to page.name, alchemy.show_page_path(urlname: page.urlname) %>
+<% end %>
diff --git a/app/views/alchemy/ingredients/_picture_editor.html.erb b/app/views/alchemy/ingredients/_picture_editor.html.erb
new file mode 100644
index 0000000000..2ed11e315f
--- /dev/null
+++ b/app/views/alchemy/ingredients/_picture_editor.html.erb
@@ -0,0 +1,59 @@
+<% options = local_assigns.fetch(:options, {}) %>
+
+<%= content_tag :div,
+ class: picture_editor.css_classes,
+ data: picture_editor.data_attributes do %>
+ <%= element_form.fields_for(:ingredients, picture_editor.ingredient) do |f| %>
+ <%= ingredient_label(picture_editor, :picture_id) %>
+ <%= content_tag :div,
+ data: {
+ target_size: picture_editor.settings[:size] || [
+ picture_editor.image_file_width.to_i,
+ picture_editor.image_file_height.to_i
+ ].join("x"),
+ image_cropper: picture_editor.thumbnail_url_options[:crop],
+ },
+ class: "picture_thumbnail" do %>
+
+ <%= render_icon(:times) %>
+
+
+
+ <%- if picture_editor.picture -%>
+ <%= image_tag(
+ picture_editor.thumbnail_url,
+ alt: picture_editor.picture.name,
+ class: "img_paddingtop",
+ title: Alchemy.t(:image_name, name: picture_editor.picture.name),
+ ) %>
+ <% else %>
+ <%= render_icon(:image, style: "regular") %>
+ <% end %>
+
+
+ <%- if picture_editor.essence.css_class.present? -%>
+
+ <%= Alchemy.t("alchemy.essence_pictures.css_classes.#{picture_editor.essence.css_class}",
+ default: picture_editor.essence.css_class.camelcase) %>
+
+ <%- end -%>
+
+ <%= render "alchemy/ingredients/shared/picture_tools", {
+ picture_editor: picture_editor
+ } %>
+
+ <% end %>
+ <%= f.hidden_field :picture_id, value: picture_editor.picture&.id,
+ data: {
+ picture_id: true,
+ image_file_width: picture_editor.image_file_width,
+ image_file_height: picture_editor.image_file_height
+ } %>
+ <%= f.hidden_field :link, data: { link_value: true } %>
+ <%= f.hidden_field :link_title, data: { link_title: true } %>
+ <%= f.hidden_field :link_class_name, data: { link_class: true } %>
+ <%= f.hidden_field :link_target, data: { link_target: true } %>
+ <%= f.hidden_field :crop_from, data: { crop_from: true } %>
+ <%= f.hidden_field :crop_size, data: { crop_size: true } %>
+ <% end %>
+<% end %>
diff --git a/app/views/alchemy/ingredients/_picture_view.html.erb b/app/views/alchemy/ingredients/_picture_view.html.erb
new file mode 100644
index 0000000000..14c1d770c0
--- /dev/null
+++ b/app/views/alchemy/ingredients/_picture_view.html.erb
@@ -0,0 +1,5 @@
+<%= Alchemy::PictureView.new(
+ picture_view,
+ local_assigns[:options],
+ local_assigns[:html_options]
+).render %>
diff --git a/app/views/alchemy/ingredients/_richtext_editor.html.erb b/app/views/alchemy/ingredients/_richtext_editor.html.erb
new file mode 100644
index 0000000000..97732bd655
--- /dev/null
+++ b/app/views/alchemy/ingredients/_richtext_editor.html.erb
@@ -0,0 +1,12 @@
+<%= content_tag :div,
+ class: richtext_editor.css_classes,
+ data: richtext_editor.data_attributes do %>
+ <%= element_form.fields_for(:ingredients, richtext_editor.ingredient) do |f| %>
+ <%= ingredient_label(richtext_editor) %>
+
+ <%= f.text_area :value,
+ class: richtext_editor.tinymce_class_name,
+ id: "tinymce_#{richtext_editor.id}" %>
+
+ <% end %>
+<% end %>
diff --git a/app/views/alchemy/ingredients/_richtext_view.html.erb b/app/views/alchemy/ingredients/_richtext_view.html.erb
new file mode 100644
index 0000000000..10433c7963
--- /dev/null
+++ b/app/views/alchemy/ingredients/_richtext_view.html.erb
@@ -0,0 +1,3 @@
+<%- options = local_assigns.fetch(:options, {}) -%>
+<%- plain_text = !!richtext_view.settings_value(:plain_text, options) -%>
+<%= raw richtext_view.public_send(plain_text ? :stripped_body : :value) -%>
diff --git a/app/views/alchemy/ingredients/_select_editor.html.erb b/app/views/alchemy/ingredients/_select_editor.html.erb
new file mode 100644
index 0000000000..65065c9688
--- /dev/null
+++ b/app/views/alchemy/ingredients/_select_editor.html.erb
@@ -0,0 +1,29 @@
+<% select_values = select_editor.settings[:select_values] %>
+
+<%= content_tag :div,
+ class: [
+ select_editor.css_classes,
+ select_editor.settings[:display_inline] ? 'display_inline' : ''
+ ], data: select_editor.data_attributes do %>
+ <%= element_form.fields_for(:ingredients, select_editor.ingredient) do |f| %>
+ <%= ingredient_label(select_editor) %>
+
+ <% if select_values.nil? %>
+ <%= warning(':select_values is nil',
+ "
No select values given.
+
Please provide
select_values
on the
+ content definition
settings
in
+
elements.yml
.") %>
+ <% else %>
+ <%
+ if select_values.is_a?(Hash)
+ options_tags = grouped_options_for_select(select_values, select_editor.value)
+ else
+ options_tags = options_for_select(select_values, select_editor.value)
+ end %>
+ <%= f.select :value, options_tags, {}, {
+ class: ["alchemy_selectbox", "ingredient-editor-select"]
+ } %>
+ <% end %>
+ <% end %>
+<% end %>
diff --git a/app/views/alchemy/ingredients/_select_view.html.erb b/app/views/alchemy/ingredients/_select_view.html.erb
new file mode 100644
index 0000000000..abe3d8edf2
--- /dev/null
+++ b/app/views/alchemy/ingredients/_select_view.html.erb
@@ -0,0 +1 @@
+<%= select_view.value %>
diff --git a/app/views/alchemy/ingredients/_text_editor.html.erb b/app/views/alchemy/ingredients/_text_editor.html.erb
new file mode 100644
index 0000000000..61da1de59f
--- /dev/null
+++ b/app/views/alchemy/ingredients/_text_editor.html.erb
@@ -0,0 +1,19 @@
+<%= content_tag :div,
+ class: [
+ text_editor.css_classes,
+ text_editor.settings[:display_inline] ? "display_inline" : ""
+ ], data: text_editor.data_attributes do %>
+ <%= element_form.fields_for(:ingredients, text_editor.ingredient) do |f| %>
+ <%= ingredient_label(text_editor) %>
+ <%= f.text_field :value,
+ class: text_editor.settings[:linkable] ? "text_with_icon" : "",
+ type: text_editor.settings[:input_type] || "text" %>
+ <% if text_editor.settings[:linkable] %>
+ <%= f.hidden_field :link, "data-link-value": true %>
+ <%= f.hidden_field :link_title, "data-link-title": true %>
+ <%= f.hidden_field :link_class_name, "data-link-class": true%>
+ <%= f.hidden_field :link_target, "data-link-target": true %>
+ <%= render "alchemy/ingredients/shared/link_tools", ingredient_editor: text_editor %>
+ <% end %>
+ <% end %>
+<% end %>
diff --git a/app/views/alchemy/ingredients/_text_view.html.erb b/app/views/alchemy/ingredients/_text_view.html.erb
new file mode 100644
index 0000000000..00f48e3777
--- /dev/null
+++ b/app/views/alchemy/ingredients/_text_view.html.erb
@@ -0,0 +1,16 @@
+<%- options = local_assigns.fetch(:options, {}) -%>
+<%- html_options = local_assigns.fetch(:html_options, {}) -%>
+<%- if text_view.link.blank? ||
+ text_view.settings_value(:disable_link, options) -%>
+<%= text_view.value -%>
+<%- else -%>
+ <%= link_to(
+ text_view.value,
+ url_for(text_view.link),
+ {
+ title: text_view.link_title,
+ target: (text_view.link_target == "blank" ? "_blank" : nil),
+ 'data-link-target' => text_view.link_target
+ }.merge(html_options)
+) -%>
+<%- end -%>
diff --git a/app/views/alchemy/ingredients/_video_editor.html.erb b/app/views/alchemy/ingredients/_video_editor.html.erb
new file mode 100644
index 0000000000..8af3980b3c
--- /dev/null
+++ b/app/views/alchemy/ingredients/_video_editor.html.erb
@@ -0,0 +1,5 @@
+<%= render(
+ "alchemy/ingredients/file_editor",
+ element_form: element_form,
+ file_editor: video_editor,
+) %>
diff --git a/app/views/alchemy/ingredients/_video_view.html.erb b/app/views/alchemy/ingredients/_video_view.html.erb
new file mode 100644
index 0000000000..e4a22ae641
--- /dev/null
+++ b/app/views/alchemy/ingredients/_video_view.html.erb
@@ -0,0 +1,17 @@
+<%- if video_view.attachment -%>
+ <%= content_tag :video,
+ controls: video_view.controls,
+ autoplay: video_view.autoplay,
+ loop: video_view.loop,
+ muted: video_view.muted,
+ preload: video_view.preload.presence,
+ width: video_view.width.presence,
+ height: video_view.height.presence do %>
+ <%= tag :source,
+ src: alchemy.show_attachment_path(
+ video_view.attachment,
+ format: video_view.attachment.suffix
+ ),
+ type: video_view.attachment.file_mime_type %>
+ <% end %>
+<%- end -%>
diff --git a/app/views/alchemy/ingredients/shared/_link_tools.html.erb b/app/views/alchemy/ingredients/shared/_link_tools.html.erb
new file mode 100644
index 0000000000..bce06a6242
--- /dev/null
+++ b/app/views/alchemy/ingredients/shared/_link_tools.html.erb
@@ -0,0 +1,20 @@
+
+ <%= link_to(
+ render_icon(:link),
+ '#',
+ onclick: 'new Alchemy.LinkDialog(this).open(); return false;',
+ class: "icon_button#{ingredient_editor.linked? ? ' linked' : ''} link-essence",
+ "data-parent-selector": "[data-ingredient-id='#{ingredient_editor.id}']",
+ title: Alchemy.t(:place_link),
+ id: "edit_link_#{ingredient_editor.id}"
+ ) %>
+ <%= link_to(
+ render_icon(:unlink),
+ '#',
+ onclick: "return Alchemy.LinkDialog.removeLink(this, '[data-ingredient-id=\"#{ingredient_editor.id}\"]')",
+ class: "icon_button unlink-essence #{ingredient_editor.linked? ? 'linked' : 'disabled'}",
+ tabindex: ingredient_editor.linked? ? nil : '-1',
+ 'data-ingredient-id' => ingredient_editor.id,
+ title: Alchemy.t(:unlink)
+ ) %>
+
diff --git a/app/views/alchemy/ingredients/shared/_picture_tools.html.erb b/app/views/alchemy/ingredients/shared/_picture_tools.html.erb
new file mode 100644
index 0000000000..2dc4142af0
--- /dev/null
+++ b/app/views/alchemy/ingredients/shared/_picture_tools.html.erb
@@ -0,0 +1,57 @@
+<% linkable = picture_editor.settings[:linkable] != false %>
+<% croppable = picture_editor.allow_image_cropping? %>
+
+<%= link_to_dialog render_icon(:crop),
+ alchemy.crop_admin_ingredient_path(picture_editor.ingredient, {
+ crop_from_form_field_id: picture_editor.form_field_id(:crop_from),
+ crop_size_form_field_id: picture_editor.form_field_id(:crop_size),
+ picture_id: picture_editor.picture&.id
+ }), {
+ size: "1080x615",
+ title: Alchemy.t("Edit Picturemask"),
+ image_loader: false,
+ padding: false
+ }, {
+ title: Alchemy.t("Edit Picturemask"),
+ class: croppable ? "crop_link" : "disabled crop_link",
+ tabindex: croppable ? nil : "-1",
+ onclick: "return false"
+ } %>
+
+<%= link_to_dialog render_icon("file-image", style: "regular"),
+ alchemy.admin_pictures_path(
+ form_field_id: picture_editor.form_field_id(:picture_id)
+ ),
+ {
+ title: (picture_editor.picture ? Alchemy.t(:swap_image) : Alchemy.t(:insert_image)),
+ size: "790x590",
+ padding: false
+ },
+ title: (picture_editor.picture ? Alchemy.t(:swap_image) : Alchemy.t(:insert_image)) %>
+
+<%= link_to_if linkable, render_icon(:link), "", {
+ onclick: "new Alchemy.LinkDialog(this).open(); return false;",
+ class: picture_editor.linked? ? "linked" : nil,
+ title: Alchemy.t(:link_image),
+ "data-parent-selector": "[data-ingredient-id='#{picture_editor.id}']",
+ id: "edit_link_#{picture_editor.id}"
+} do %>
+
<%= render_icon(:link) %>
+<% end %>
+
+<%= link_to_if linkable, render_icon(:unlink), "", {
+ onclick: "return Alchemy.LinkDialog.removeLink(this, '[data-ingredient-id=\"#{picture_editor.id}\"]')",
+ class: picture_editor.linked? ? "linked" : "disabled",
+ tabindex: picture_editor.linked? ? nil : "-1",
+ title: Alchemy.t(:unlink)
+} do %>
+
<%= render_icon(:unlink) %>
+<% end %>
+
+<%= link_to_dialog render_icon(:edit),
+ alchemy.edit_admin_ingredient_path(id: picture_editor.id),
+ {
+ title: Alchemy.t(:edit_image_properties),
+ size: "380x255"
+ },
+ title: Alchemy.t(:edit_image_properties) %>
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
index aee428dd6f..b3781127bb 100644
--- a/config/brakeman.ignore
+++ b/config/brakeman.ignore
@@ -3,19 +3,19 @@
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
- "fingerprint": "0551e3f9180b85fca4b17fe3c7cbbac1611d2ef8d385f77e9445c562c471d688",
+ "fingerprint": "068b12d24047e2ece633115ba065ce46fc8c8a26827be7de2565ab721e1c2e82",
"check_name": "CrossSiteScripting",
"message": "Unescaped parameter value",
"file": "app/views/alchemy/admin/elements/update.js.erb",
- "line": 18,
+ "line": 21,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
- "code": "j(Element.find(params[:id]).essence_error_messages.join(\"
\"))",
+ "code": "Element.find(params[:id]).ingredients_with_errors.map do\n \"[data-ingredient-id=\\\"#{ingredient.id}\\\"]\"\n end.join(\", \")",
"render_path": [
{
"type": "controller",
"class": "Alchemy::Admin::ElementsController",
"method": "update",
- "line": 59,
+ "line": 61,
"file": "app/controllers/alchemy/admin/elements_controller.rb",
"rendered": {
"name": "alchemy/admin/elements/update",
@@ -38,7 +38,7 @@
"check_name": "SendFile",
"message": "Parameter value used in file name",
"file": "app/controllers/alchemy/admin/attachments_controller.rb",
- "line": 65,
+ "line": 69,
"link": "https://brakemanscanner.org/docs/warning_types/file_access/",
"code": "send_file(Attachment.find(params[:id]).file.path, :filename => Attachment.find(params[:id]).file_name, :type => Attachment.find(params[:id]).file_mime_type)",
"render_path": null,
@@ -71,37 +71,6 @@
"confidence": "Medium",
"note": "Because we actually can't know all attributes each inheriting controller supports, we permit all resource model params. It is adviced that all inheriting controllers implement this method and provide its own set of permitted attributes. As this all happens inside the password protected /admin namespace this can be considered a false positive."
},
- {
- "warning_type": "Dynamic Render Path",
- "warning_code": 15,
- "fingerprint": "2fa9bf5c73b4e6e3c272f0b14635f96efbd763e9a2c5b785caefffe3589ac461",
- "check_name": "Render",
- "message": "Render path contains parameter value",
- "file": "app/views/alchemy/admin/essence_pictures/assign.js.erb",
- "line": 2,
- "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
- "code": "render(action => Alchemy::ContentEditor.new(Content.find(params[:content_id])), {})",
- "render_path": [
- {
- "type": "controller",
- "class": "Alchemy::Admin::EssencePicturesController",
- "method": "assign",
- "line": 49,
- "file": "app/controllers/alchemy/admin/essence_pictures_controller.rb",
- "rendered": {
- "name": "alchemy/admin/essence_pictures/assign",
- "file": "app/views/alchemy/admin/essence_pictures/assign.js.erb"
- }
- }
- ],
- "location": {
- "type": "template",
- "template": "alchemy/admin/essence_pictures/assign"
- },
- "user_input": "params[:content_id]",
- "confidence": "Weak",
- "note": ""
- },
{
"warning_type": "Dynamic Render Path",
"warning_code": 15,
@@ -117,7 +86,7 @@
"type": "controller",
"class": "Alchemy::Admin::ElementsController",
"method": "fold",
- "line": 94,
+ "line": 97,
"file": "app/controllers/alchemy/admin/elements_controller.rb",
"rendered": {
"name": "alchemy/admin/elements/fold",
@@ -140,7 +109,7 @@
"check_name": "MassAssignment",
"message": "Specify exact keys allowed for mass assignment instead of using `permit!` which allows any keys",
"file": "app/controllers/alchemy/admin/elements_controller.rb",
- "line": 145,
+ "line": 150,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.fetch(:contents, {}).permit!",
"render_path": null,
@@ -284,39 +253,8 @@
"user_input": "params[:id]",
"confidence": "Weak",
"note": ""
- },
- {
- "warning_type": "Dynamic Render Path",
- "warning_code": 15,
- "fingerprint": "b9f63fd46d0ebd6684b649ab260f27df8a6422d44fed4769273d8e6a6a30397c",
- "check_name": "Render",
- "message": "Render path contains parameter value",
- "file": "app/views/alchemy/admin/essence_files/assign.js.erb",
- "line": 1,
- "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
- "code": "render(action => Alchemy::ContentEditor.new(Content.find_by(:id => params[:content_id])), {})",
- "render_path": [
- {
- "type": "controller",
- "class": "Alchemy::Admin::EssenceFilesController",
- "method": "assign",
- "line": 32,
- "file": "app/controllers/alchemy/admin/essence_files_controller.rb",
- "rendered": {
- "name": "alchemy/admin/essence_files/assign",
- "file": "app/views/alchemy/admin/essence_files/assign.js.erb"
- }
- }
- ],
- "location": {
- "type": "template",
- "template": "alchemy/admin/essence_files/assign"
- },
- "user_input": "params[:content_id]",
- "confidence": "Weak",
- "note": ""
}
],
- "updated": "2021-02-15 11:47:56 +0100",
- "brakeman_version": "5.0.0"
+ "updated": "2021-06-29 20:56:10 +0200",
+ "brakeman_version": "5.0.1"
}
diff --git a/config/locales/alchemy.en.yml b/config/locales/alchemy.en.yml
index c730461421..2aac564173 100644
--- a/config/locales/alchemy.en.yml
+++ b/config/locales/alchemy.en.yml
@@ -89,6 +89,11 @@ en:
right: 'Right from text'
no_float: 'Above the text'
+ ingredient_values:
+ boolean:
+ true: "True"
+ false: "False"
+
# == Contactform translations
contactform:
labels:
@@ -307,6 +312,7 @@ en:
"Warning!": "Warning!"
content_definition_missing: "Warning: Content is missing its definition. Please check the elements.yml"
content_deprecated: "WARNING! This content is deprecated and will be removed soon. Please do not use it anymore."
+ ingredient_deprecated: "WARNING! This content is deprecated and will be removed soon. Please do not use it anymore."
element_definition_missing: "WARNING! Missing element definition. Please check your elements.yml file."
element_deprecated: "WARNING! This element is deprecated and will be removed soon. Please do not use it anymore."
page_definition_missing: "WARNING! Missing page layout definition. Please check your page_layouts.yml file."
diff --git a/config/routes.rb b/config/routes.rb
index 43500c4634..bd5c4e3e35 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -88,6 +88,8 @@
resources :essence_videos, only: [:edit, :update]
+ resources :ingredients, only: [:edit, :update], concerns: [:croppable]
+
resources :legacy_page_urls
resources :languages do
collection do
diff --git a/db/migrate/20210508091432_create_alchemy_ingredients.rb b/db/migrate/20210508091432_create_alchemy_ingredients.rb
new file mode 100644
index 0000000000..5e7e392dcd
--- /dev/null
+++ b/db/migrate/20210508091432_create_alchemy_ingredients.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class CreateAlchemyIngredients < ActiveRecord::Migration[6.0]
+ def change
+ create_table :alchemy_ingredients do |t|
+ t.references :element, null: false, foreign_key: { to_table: :alchemy_elements, on_delete: :cascade }
+ t.string :type, index: true, null: false
+ t.string :role, null: false
+ t.text :value
+ if ActiveRecord::Migration.connection.adapter_name.match?(/postgres/i)
+ t.jsonb :data, default: {}
+ else
+ t.json :data
+ end
+ t.belongs_to :related_object, null: true, polymorphic: true, index: false
+ t.index [:element_id, :role], unique: true
+ t.index [:related_object_id, :related_object_type], name: "idx_alchemy_ingredient_relation"
+
+ t.timestamps
+ end
+ end
+end
diff --git a/lib/alchemy/hints.rb b/lib/alchemy/hints.rb
index e767626bc2..d548c4e0ff 100644
--- a/lib/alchemy/hints.rb
+++ b/lib/alchemy/hints.rb
@@ -35,21 +35,25 @@ module Hints
# @return String
#
def hint
- hint = definition["hint"]
+ hint = definition[:hint]
if hint == true
- Alchemy.t(name, scope: hint_translation_scope)
+ Alchemy.t(hint_translation_attribute, scope: hint_translation_scope)
else
hint
end
end
- # Returns true if the element has a hint
+ # Returns true if the element has a hint defined
def has_hint?
- hint.present?
+ !!definition[:hint]
end
private
+ def hint_translation_attribute
+ name
+ end
+
def hint_translation_scope
"#{self.class.model_name.to_s.demodulize.downcase}_hints"
end
diff --git a/lib/alchemy/permissions.rb b/lib/alchemy/permissions.rb
index 6b641a7cec..ac0b508fd3 100644
--- a/lib/alchemy/permissions.rb
+++ b/lib/alchemy/permissions.rb
@@ -115,6 +115,8 @@ def alchemy_author_rules
can :manage, Alchemy::EssenceFile
can :manage, Alchemy::EssencePicture
can :manage, Alchemy::EssenceVideo
+ can :manage, Alchemy::Ingredient
+ can [:crop], Alchemy::Ingredients::Picture
can :manage, Alchemy::LegacyPageUrl
can :manage, Alchemy::Node
can [:read, :url], Alchemy::Picture
diff --git a/lib/alchemy/test_support/factories/element_factory.rb b/lib/alchemy/test_support/factories/element_factory.rb
index b0dbaafaf8..7db7670c46 100644
--- a/lib/alchemy/test_support/factories/element_factory.rb
+++ b/lib/alchemy/test_support/factories/element_factory.rb
@@ -4,6 +4,7 @@
factory :alchemy_element, class: "Alchemy::Element" do
name { "article" }
autogenerate_contents { false }
+ autogenerate_ingredients { false }
association :page_version, factory: :alchemy_page_version
trait :fixed do
@@ -28,5 +29,10 @@
trait :with_contents do
autogenerate_contents { true }
end
+
+ trait :with_ingredients do
+ name { "element_with_ingredients" }
+ autogenerate_ingredients { true }
+ end
end
end
diff --git a/lib/alchemy/test_support/factories/ingredient_factory.rb b/lib/alchemy/test_support/factories/ingredient_factory.rb
new file mode 100644
index 0000000000..907e8bcffa
--- /dev/null
+++ b/lib/alchemy/test_support/factories/ingredient_factory.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ %w[
+ audio
+ boolean
+ datetime
+ file
+ headline
+ html
+ link
+ node
+ page
+ picture
+ richtext
+ select
+ text
+ ].each do |ingredient|
+ factory :"alchemy_ingredient_#{ingredient}", class: "Alchemy::Ingredients::#{ingredient.classify}" do
+ role { ingredient }
+ type { "Alchemy::Ingredients::#{ingredient.classify}" }
+ association :element, name: "all_you_can_eat_ingredients", factory: :alchemy_element
+ end
+ end
+end
diff --git a/lib/alchemy/test_support/shared_ingredient_editor_examples.rb b/lib/alchemy/test_support/shared_ingredient_editor_examples.rb
new file mode 100644
index 0000000000..08552b32b1
--- /dev/null
+++ b/lib/alchemy/test_support/shared_ingredient_editor_examples.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples_for "an alchemy ingredient editor" do
+ let(:ingredient_editor) { Alchemy::IngredientEditor.new(ingredient) }
+
+ before do
+ view.class.send :include, Alchemy::Admin::BaseHelper
+ view.class.send :include, Alchemy::Admin::IngredientsHelper
+ allow(element_editor).to receive(:ingredients) { [ingredient_editor] }
+ end
+
+ subject do
+ render element_editor
+ rendered
+ end
+
+ it "renders a ingredient editor", :aggregate_failures do
+ is_expected.to have_css(".ingredient-editor.#{ingredient_editor.partial_name}")
+ is_expected.to have_css("[data-ingredient-role]")
+ end
+end
diff --git a/lib/alchemy/test_support/shared_ingredient_examples.rb b/lib/alchemy/test_support/shared_ingredient_examples.rb
new file mode 100644
index 0000000000..78a9aceae9
--- /dev/null
+++ b/lib/alchemy/test_support/shared_ingredient_examples.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require "shoulda-matchers"
+
+RSpec.shared_examples_for "an alchemy ingredient" do
+ let(:element) { build(:alchemy_element, name: "element_with_ingredients") }
+
+ subject(:ingredient) do
+ described_class.new(
+ element: element,
+ role: "headline",
+ )
+ end
+
+ it { is_expected.to belong_to(:element).touch(true).class_name("Alchemy::Element") }
+ it { is_expected.to belong_to(:related_object).optional }
+ it { is_expected.to validate_presence_of(:role) }
+ it { is_expected.to validate_presence_of(:type) }
+ it { expect(subject.data).to eq({}) }
+
+ describe "#settings" do
+ subject { ingredient.settings }
+
+ context "without element" do
+ let(:element) { nil }
+
+ it { is_expected.to eq({}) }
+ end
+
+ context "with element" do
+ it { is_expected.to eq({ linkable: true }.with_indifferent_access) }
+ end
+ end
+
+ describe "#definition" do
+ subject { ingredient.definition }
+
+ context "without element" do
+ let(:element) { nil }
+
+ it { is_expected.to eq({}) }
+ end
+
+ context "with element" do
+ it do
+ is_expected.to eq({
+ role: "headline",
+ type: "Text",
+ default: "Hello World",
+ settings: {
+ linkable: true,
+ },
+ }.with_indifferent_access)
+ end
+ end
+ end
+end
diff --git a/lib/alchemy/tinymce.rb b/lib/alchemy/tinymce.rb
index 27f3862187..156bc4d5a3 100644
--- a/lib/alchemy/tinymce.rb
+++ b/lib/alchemy/tinymce.rb
@@ -38,6 +38,10 @@ def custom_config_contents(page)
content_definitions_from_elements(page.descendent_element_definitions)
end
+ def custom_config_ingredients(page)
+ ingredient_definitions_from_elements(page.descendent_element_definitions)
+ end
+
private
def content_definitions_from_elements(definitions)
@@ -52,6 +56,19 @@ def content_definitions_from_elements(definitions)
contents.map { |c| c.merge("element" => el["name"]) }
end.flatten.compact
end
+
+ def ingredient_definitions_from_elements(definitions)
+ definitions.collect do |el|
+ next if el["ingredients"].blank?
+
+ ingredients = el["ingredients"].select do |c|
+ c["settings"] && c["settings"]["tinymce"].is_a?(Hash)
+ end
+ next if ingredients.blank?
+
+ ingredients.map { |c| c.merge("element" => el["name"]) }
+ end.flatten.compact
+ end
end
end
end
diff --git a/lib/alchemy/upgrader/six_point_zero.rb b/lib/alchemy/upgrader/six_point_zero.rb
index 0a4900273e..e8384615ce 100644
--- a/lib/alchemy/upgrader/six_point_zero.rb
+++ b/lib/alchemy/upgrader/six_point_zero.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require_relative "tasks/add_page_versions"
+require_relative "tasks/ingredients_migrator"
module Alchemy
class Upgrader::SixPointZero < Upgrader
@@ -9,6 +10,12 @@ def create_public_page_versions
desc "Create public page versions for pages"
Alchemy::Upgrader::Tasks::AddPageVersions.new.create_public_page_versions
end
+
+ def create_ingredients
+ desc "Create ingredients for elements with ingredients defined"
+ Alchemy::Upgrader::Tasks::IngredientsMigrator.new.create_ingredients
+ log "Done.", :success
+ end
end
end
end
diff --git a/lib/alchemy/upgrader/tasks/ingredients_migrator.rb b/lib/alchemy/upgrader/tasks/ingredients_migrator.rb
new file mode 100644
index 0000000000..a4f1255639
--- /dev/null
+++ b/lib/alchemy/upgrader/tasks/ingredients_migrator.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require "alchemy/upgrader"
+
+module Alchemy::Upgrader::Tasks
+ class IngredientsMigrator < Thor
+ include Thor::Actions
+
+ no_tasks do
+ def create_ingredients
+ Alchemy::Deprecation.silence do
+ elements_with_ingredients = Alchemy::ElementDefinition.all.select { |d| d.key?(:ingredients) }
+ # eager load all elements that have ingredients defined but no ingredient records yet.
+ all_elements = Alchemy::Element
+ .named(elements_with_ingredients.map { |d| d[:name] })
+ .includes(contents: { essence: :ingredient_association })
+ .left_outer_joins(:ingredients).where(alchemy_ingredients: { id: nil })
+ .to_a
+ elements_with_ingredients.map do |element_definition|
+ elements = all_elements.select { |e| e.name == element_definition[:name] }
+ if elements.any?
+ puts "-- Creating ingredients for #{elements.count} #{element_definition[:name]}(s)"
+ elements.each do |element|
+ Alchemy::Element.transaction do
+ element.ingredients = element_definition[:ingredients].map do |ingredient_definition|
+ content = element.content_by_name(ingredient_definition[:role])
+ next unless content
+
+ ingredient = Alchemy::Ingredient.build(role: ingredient_definition[:role], element: element)
+ belongs_to_associations = content.essence.class.reflect_on_all_associations(:belongs_to)
+ if belongs_to_associations.any?
+ ingredient.related_object = content.essence.public_send(belongs_to_associations.first.name)
+ else
+ ingredient.value = content.ingredient
+ end
+ content.destroy!
+ print "."
+ ingredient
+ end.compact
+ end
+ end
+ puts "\n"
+ else
+ puts "-- No #{element_definition[:name]} elements found for migration."
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/generators/alchemy/elements/elements_generator.rb b/lib/generators/alchemy/elements/elements_generator.rb
index 966dc85af6..d2123ab497 100644
--- a/lib/generators/alchemy/elements/elements_generator.rb
+++ b/lib/generators/alchemy/elements/elements_generator.rb
@@ -14,6 +14,7 @@ def create_partials
@elements.each do |element|
@element = element
@contents = element["contents"] || []
+ @ingredients = element["ingredients"] || []
@element_name = element_name(element)
conditional_template "view.html.#{template_engine}", "#{elements_dir}/_#{@element_name}.html.#{template_engine}"
end
diff --git a/lib/generators/alchemy/elements/templates/view.html.erb b/lib/generators/alchemy/elements/templates/view.html.erb
index db23ac69b6..9b6c681c4b 100644
--- a/lib/generators/alchemy/elements/templates/view.html.erb
+++ b/lib/generators/alchemy/elements/templates/view.html.erb
@@ -9,6 +9,15 @@
<%%= el.render :<%= content["name"] %> %>
<%- end -%>
<%- end -%>
+ <%- @ingredients.each do |ingredient| -%>
+ <%- if @ingredients.length > 1 -%>
+ ">
+ <%%= el.render(:<%= ingredient["role"] %>) %>
+
+ <%- else -%>
+ <%%= el.render(:<%= ingredient["role"] %>) %>
+ <%- end -%>
+ <%- end -%>
<%- if @element['nestable_elements'].present? -%>
<%%= render <%= @element_name %>.nested_elements.available %>
<%- end -%>
diff --git a/lib/generators/alchemy/elements/templates/view.html.haml b/lib/generators/alchemy/elements/templates/view.html.haml
index 01d6506078..3c5c7dccc1 100644
--- a/lib/generators/alchemy/elements/templates/view.html.haml
+++ b/lib/generators/alchemy/elements/templates/view.html.haml
@@ -8,6 +8,15 @@
= el.render :<%= content["name"] %>
<%- end -%>
<%- end -%>
+ <%- @ingredients.each do |ingredient| -%>
+ <%- if @ingredients.length > 1 -%>
+ .<%= ingredient["role"] %>
+ = el.render(:<%= ingredient["role"] %>)
+
+ <%- else -%>
+ = el.render(:<%= ingredient["role"] %>)
+ <%- end -%>
+ <%- end -%>
<%- if @element['nestable_elements'].present? -%>
= render <%= @element_name -%>.nested_elements.available
<%- end -%>
diff --git a/lib/generators/alchemy/elements/templates/view.html.slim b/lib/generators/alchemy/elements/templates/view.html.slim
index 01d6506078..3c5c7dccc1 100644
--- a/lib/generators/alchemy/elements/templates/view.html.slim
+++ b/lib/generators/alchemy/elements/templates/view.html.slim
@@ -8,6 +8,15 @@
= el.render :<%= content["name"] %>
<%- end -%>
<%- end -%>
+ <%- @ingredients.each do |ingredient| -%>
+ <%- if @ingredients.length > 1 -%>
+ .<%= ingredient["role"] %>
+ = el.render(:<%= ingredient["role"] %>)
+
+ <%- else -%>
+ = el.render(:<%= ingredient["role"] %>)
+ <%- end -%>
+ <%- end -%>
<%- if @element['nestable_elements'].present? -%>
= render <%= @element_name -%>.nested_elements.available
<%- end -%>
diff --git a/lib/generators/alchemy/ingredient/ingredient_generator.rb b/lib/generators/alchemy/ingredient/ingredient_generator.rb
new file mode 100644
index 0000000000..82afa0daac
--- /dev/null
+++ b/lib/generators/alchemy/ingredient/ingredient_generator.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+require "rails"
+
+module Alchemy
+ module Generators
+ class IngredientGenerator < ::Rails::Generators::Base
+ desc "This generator generates an Alchemy ingredient class for you."
+ argument :class_name, banner: "ingredient_class_name"
+ source_root File.expand_path("templates", __dir__)
+
+ def init
+ @class_name = class_name.classify
+ @ingredients_view_path = "app/views/alchemy/ingredients"
+ end
+
+ def create_model
+ template "model.rb.tt", "app/models/alchemy/ingredients/#{file_name}.rb"
+ end
+
+ def copy_templates
+ @ingredient_editor_local = "#{file_name}_editor"
+ @ingredient_view_local = "#{file_name}_view"
+ template "view.html.erb", "#{@ingredients_view_path}/_#{file_name}_view.html.erb"
+ template "editor.html.erb", "#{@ingredients_view_path}/_#{file_name}_editor.html.erb"
+ end
+
+ def show_todo
+ say "\nPlease check the generated files and alter them to fit your needs."
+ end
+
+ private
+
+ def file_name
+ @_file_name ||= @class_name.classify.demodulize.underscore
+ end
+ end
+ end
+end
diff --git a/lib/generators/alchemy/ingredient/templates/editor.html.erb b/lib/generators/alchemy/ingredient/templates/editor.html.erb
new file mode 100644
index 0000000000..4b69a830c7
--- /dev/null
+++ b/lib/generators/alchemy/ingredient/templates/editor.html.erb
@@ -0,0 +1,14 @@
+<%%#
+ Available locals:
+ * <%= @ingredient_editor_local %> - An Alchemy::IngredientEditor instance
+
+ Please consult Alchemy::IngredientEditor.rb docs for further methods on the ingredient object
+%>
+<%%= content_tag :div,
+ class: <%= @ingredient_editor_local %>.css_classes,
+ data: <%= @ingredient_editor_local %>.data_attributes do %>
+ <%%= element_form.fields_for(:ingredients, <%= @ingredient_editor_local %>.ingredient) do |f| %>
+ <%%= ingredient_label(<%= @ingredient_editor_local %>) %>
+ <%%= f.text_field :value %>
+ <%% end %>
+<%% end %>
diff --git a/lib/generators/alchemy/ingredient/templates/model.rb.tt b/lib/generators/alchemy/ingredient/templates/model.rb.tt
new file mode 100644
index 0000000000..fda21a74ec
--- /dev/null
+++ b/lib/generators/alchemy/ingredient/templates/model.rb.tt
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Alchemy
+ module Ingredients
+ class <%= @class_name %> < Alchemy::Ingredient
+ # Set additional attributes that get stored in the `data` JSON column
+ # store_accessor :data, :some, :attribute
+
+ # Set a related_object alias for convenience
+ # related_object_alias :some_association_name, class_name: "Some::Klass"
+ end
+ end
+end
diff --git a/lib/generators/alchemy/ingredient/templates/view.html.erb b/lib/generators/alchemy/ingredient/templates/view.html.erb
new file mode 100644
index 0000000000..8d942fd7d8
--- /dev/null
+++ b/lib/generators/alchemy/ingredient/templates/view.html.erb
@@ -0,0 +1 @@
+<%%= <%= @ingredient_view_local %>.value %>
diff --git a/lib/tasks/alchemy/upgrade.rake b/lib/tasks/alchemy/upgrade.rake
index 6283acd79c..61e21fccd7 100644
--- a/lib/tasks/alchemy/upgrade.rake
+++ b/lib/tasks/alchemy/upgrade.rake
@@ -72,12 +72,18 @@ namespace :alchemy do
namespace "6.0" do
task "run" => [
"alchemy:upgrade:6.0:create_public_page_versions",
+ "alchemy:upgrade:6.0:create_ingredients",
]
- desc "Install Gutentag migrations"
+ desc "Create public page versions"
task create_public_page_versions: [:environment] do
Alchemy::Upgrader::SixPointZero.create_public_page_versions
end
+
+ desc "Create ingredients for elements with ingredients defined"
+ task create_ingredients: [:environment] do
+ Alchemy::Upgrader::SixPointZero.create_ingredients
+ end
end
end
end
diff --git a/spec/controllers/alchemy/admin/elements_controller_spec.rb b/spec/controllers/alchemy/admin/elements_controller_spec.rb
index edeef7bde0..44832be29e 100644
--- a/spec/controllers/alchemy/admin/elements_controller_spec.rb
+++ b/spec/controllers/alchemy/admin/elements_controller_spec.rb
@@ -6,18 +6,18 @@ module Alchemy
describe Admin::ElementsController do
routes { Alchemy::Engine.routes }
- let(:page_version) { create(:alchemy_page_version) }
- let(:element) { create(:alchemy_element, page_version: page_version) }
+ let(:page_version) { create(:alchemy_page_version) }
+ let(:element) { create(:alchemy_element, page_version: page_version) }
let(:element_in_clipboard) { create(:alchemy_element, page_version: page_version) }
- let(:clipboard) { session[:alchemy_clipboard] = {} }
+ let(:clipboard) { session[:alchemy_clipboard] = {} }
before { authorize_user(:as_author) }
describe "#index" do
- let!(:page_version) { create(:alchemy_page_version) }
- let!(:element) { create(:alchemy_element, page_version: page_version) }
- let!(:nested_element) { create(:alchemy_element, :nested, page_version: page_version) }
- let!(:hidden_element) { create(:alchemy_element, page_version: page_version, public: false) }
+ let!(:page_version) { create(:alchemy_page_version) }
+ let!(:element) { create(:alchemy_element, page_version: page_version) }
+ let!(:nested_element) { create(:alchemy_element, :nested, page_version: page_version) }
+ let!(:hidden_element) { create(:alchemy_element, page_version: page_version, public: false) }
context "with fixed elements" do
let!(:fixed_element) do
@@ -41,9 +41,9 @@ module Alchemy
end
describe "#order" do
- let!(:element_1) { create(:alchemy_element) }
- let!(:element_2) { create(:alchemy_element, page_version: page_version) }
- let!(:element_3) { create(:alchemy_element, page_version: page_version) }
+ let!(:element_1) { create(:alchemy_element) }
+ let!(:element_2) { create(:alchemy_element, page_version: page_version) }
+ let!(:element_3) { create(:alchemy_element, page_version: page_version) }
let(:element_ids) { [element_1.id, element_3.id, element_2.id] }
let(:page_version) { element_1.page_version }
@@ -69,17 +69,17 @@ module Alchemy
parent.update_column(:updated_at, 3.days.ago)
expect {
post :order, params: {
- element_ids: element_ids,
- parent_element_id: parent.id,
- }, xhr: true
+ element_ids: element_ids,
+ parent_element_id: parent.id,
+ }, xhr: true
}.to change { parent.reload.updated_at }
end
it "assigns parent element id to each element" do
post :order, params: {
- element_ids: element_ids,
- parent_element_id: parent.id,
- }, xhr: true
+ element_ids: element_ids,
+ parent_element_id: parent.id,
+ }, xhr: true
[element_1, element_2, element_3].each do |element|
expect(element.reload.parent_element_id).to eq parent.id
end
@@ -92,18 +92,18 @@ module Alchemy
it "assign variable for all available element definitions" do
expect_any_instance_of(Alchemy::Page).to receive(:available_element_definitions)
- get :new, params: {page_version_id: page_version.id}
+ get :new, params: { page_version_id: page_version.id }
end
context "with elements in clipboard" do
let(:element) { create(:alchemy_element, page_version: page_version) }
- let(:clipboard_items) { [{"id" => element.id.to_s, "action" => "copy"}] }
+ let(:clipboard_items) { [{ "id" => element.id.to_s, "action" => "copy" }] }
before { clipboard["elements"] = clipboard_items }
it "should load all elements from clipboard" do
expect(Element).to receive(:all_from_clipboard_for_page).and_return(clipboard_items)
- get :new, params: {page_version_id: page_version.id}
+ get :new, params: { page_version_id: page_version.id }
expect(assigns(:clipboard_items)).to eq(clipboard_items)
end
end
@@ -151,7 +151,7 @@ module Alchemy
render_views
before do
- clipboard["elements"] = [{"id" => element_in_clipboard.id.to_s, "action" => "cut"}]
+ clipboard["elements"] = [{ "id" => element_in_clipboard.id.to_s, "action" => "cut" }]
end
it "should create an element from clipboard" do
@@ -192,97 +192,77 @@ module Alchemy
end
describe "#update" do
- let(:element) { build_stubbed(:alchemy_element) }
- let(:contents_parameters) { ActionController::Parameters.new(1 => {ingredient: "Title"}) }
- let(:element_parameters) { ActionController::Parameters.new(tag_list: "Tag 1", public: false) }
-
before do
- expect(Element).to receive(:find).and_return element
- expect(controller).to receive(:contents_params).and_return(contents_parameters)
- end
-
- it "updates all contents in element" do
- expect(element).to receive(:update_contents).with(contents_parameters)
- put :update, params: {id: element.id}, xhr: true
+ expect(Element).to receive(:find).at_least(:once).and_return(element)
end
- it "updates the element" do
- expect(controller).to receive(:element_params).and_return(element_parameters)
- expect(element).to receive(:update_contents).and_return(true)
- expect(element).to receive(:update).with(element_parameters).and_return(true)
- put :update, params: {id: element.id}, xhr: true
- end
-
- context "failed validations" do
- it "displays validation failed notice" do
- expect(element).to receive(:update_contents).and_return(false)
- put :update, params: {id: element.id}, xhr: true
- expect(assigns(:element_validated)).to be_falsey
+ context "with element having contents" do
+ subject do
+ put :update, params: { id: element.id, element: element_params, contents: contents_params }, xhr: true
end
- end
- end
- describe "#destroy" do
- subject { delete :destroy, params: { id: element.id }, xhr: true }
-
- let!(:element) { create(:alchemy_element) }
+ let(:element) { create(:alchemy_element, :with_contents) }
+ let(:content) { element.contents.first }
+ let(:element_params) { { tag_list: "Tag 1", public: false } }
+ let(:contents_params) { { content.id => { ingredient: "Title" } } }
- it "deletes the element" do
- expect { subject }.to change(Alchemy::Element, :count).to(0)
- end
- end
-
- describe "params security" do
- context "contents params" do
- let(:parameters) { ActionController::Parameters.new(contents: {1 => {ingredient: "Title"}}) }
+ it "updates all contents in element" do
+ expect { subject }.to change { content.reload.ingredient }.to("Title")
+ end
- specify ":contents is required" do
- expect(controller.params).to receive(:fetch).and_return(parameters)
- controller.send :contents_params
+ it "updates the element" do
+ expect { subject }.to change { element.tag_list }.to(["Tag 1"])
end
- specify "everything is permitted" do
- expect(controller).to receive(:params).and_return(parameters)
- expect(parameters).to receive(:fetch).and_return(parameters)
- expect(parameters).to receive(:permit!)
- controller.send :contents_params
+ context "failed validations" do
+ it "displays validation failed notice" do
+ expect(element).to receive(:update_contents).and_return(false)
+ subject
+ expect(assigns(:element_validated)).to be_falsey
+ end
end
end
- context "element params" do
- let(:parameters) { ActionController::Parameters.new(element: {public: true}) }
-
- before do
- expect(controller).to receive(:params).and_return(parameters)
- expect(parameters).to receive(:fetch).with(:element, {}).and_return(parameters)
+ context "with element having ingredients" do
+ subject do
+ put :update, params: { id: element.id, element: element_params }, xhr: true
end
- context "with taggable element" do
- before do
- controller.instance_variable_set(:'@element', mock_model(Element, taggable?: true))
- end
+ let(:element) { create(:alchemy_element, :with_ingredients) }
+ let(:ingredient) { element.ingredients.first }
+ let(:ingredients_attributes) { { 0 => { id: ingredient.id, value: "Title" } } }
+ let(:element_params) { { tag_list: "Tag 1", public: false, ingredients_attributes: ingredients_attributes } }
- specify ":tag_list is permitted" do
- expect(parameters).to receive(:permit).with(:tag_list)
- controller.send :element_params
- end
+ it "updates all ingredients in element" do
+ expect { subject }.to change { ingredient.value }.to("Title")
end
- context "with not taggable element" do
- before do
- controller.instance_variable_set(:'@element', mock_model(Element, taggable?: false))
- end
+ it "updates the element" do
+ expect { subject }.to change { element.tag_list }.to(["Tag 1"])
+ end
- specify ":tag_list is not permitted" do
- expect(parameters).to_not receive(:permit)
- controller.send :element_params
+ context "failed validations" do
+ it "displays validation failed notice" do
+ expect(element).to receive(:update).and_return(false)
+ subject
+ expect(assigns(:element_validated)).to be_falsey
end
end
end
end
+ describe "#destroy" do
+ subject { delete :destroy, params: { id: element.id }, xhr: true }
+
+ let!(:element) { create(:alchemy_element) }
+
+ it "deletes the element" do
+ expect { subject }.to change(Alchemy::Element, :count).to(0)
+ end
+ end
+
describe "#fold" do
- subject { post :fold, params: {id: element.id}, xhr: true }
+ subject { post :fold, params: { id: element.id }, xhr: true }
let(:element) { build_stubbed(:alchemy_element) }
diff --git a/spec/controllers/alchemy/admin/ingredients_controller_spec.rb b/spec/controllers/alchemy/admin/ingredients_controller_spec.rb
new file mode 100644
index 0000000000..bebe87a40d
--- /dev/null
+++ b/spec/controllers/alchemy/admin/ingredients_controller_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Alchemy::Admin::IngredientsController do
+ routes { Alchemy::Engine.routes }
+
+ let(:attachment) { build_stubbed(:alchemy_attachment) }
+ let(:element) { build(:alchemy_element, name: "all_you_can_eat_ingredients") }
+
+ let(:ingredient) do
+ stub_model(
+ Alchemy::Ingredients::File,
+ type: "Alchemy::Ingredients::File",
+ element: element,
+ attachment: attachment,
+ role: "file",
+ )
+ end
+
+ before do
+ allow(Alchemy::Ingredient).to receive(:find).with(ingredient.id.to_s) { ingredient }
+ end
+
+ context "without authorized user" do
+ describe "get :edit" do
+ it "redirects to login path" do
+ get :edit, params: { id: ingredient.id }
+ expect(response).to redirect_to(Alchemy.login_path)
+ end
+ end
+
+ describe "patch :update" do
+ it "redirects to login path" do
+ patch :update, params: { id: ingredient.id }
+ expect(response).to redirect_to(Alchemy.login_path)
+ end
+ end
+ end
+
+ context "with autorized user" do
+ before do
+ authorize_user(:as_admin)
+ end
+
+ describe "get :edit" do
+ subject { get(:edit, params: { id: ingredient.id }) }
+
+ it "assigns @ingredient with the Ingredient found by id" do
+ subject
+ expect(assigns(:ingredient)).to eq(ingredient)
+ end
+
+ it "renders edit template" do
+ expect(subject).to render_template("alchemy/admin/ingredients/edit")
+ end
+ end
+
+ describe "patch :update" do
+ context "with permitted attributes" do
+ let(:params) do
+ {
+ id: ingredient.id,
+ ingredient: {
+ title: "new title",
+ css_class: "left",
+ link_text: "Download this file",
+ },
+ }
+ end
+
+ it "updates the attributes of ingredient" do
+ patch :update, params: params, xhr: true
+ expect(ingredient.title).to eq "new title"
+ expect(ingredient.css_class).to eq "left"
+ expect(ingredient.link_text).to eq "Download this file"
+ end
+ end
+
+ context "with unpermitted attributes" do
+ let(:params) do
+ {
+ id: ingredient.id,
+ ingredient: {
+ foo: "Baz",
+ },
+ }
+ end
+
+ it "does not update the attributes of ingredient" do
+ expect(ingredient).to receive(:update).with({})
+ patch :update, params: params, xhr: true
+ end
+ end
+ end
+
+ it_behaves_like "having crop action", model_class: Alchemy::Ingredient do
+ let(:croppable_resource) do
+ Alchemy::Ingredient.build(
+ type: "Alchemy::Ingredients::Picture",
+ element: element,
+ attachment: attachment,
+ role: "picture",
+ )
+ end
+ end
+ end
+end
diff --git a/spec/decorators/alchemy/element_editor_spec.rb b/spec/decorators/alchemy/element_editor_spec.rb
index bca04dc838..a23cd85143 100644
--- a/spec/decorators/alchemy/element_editor_spec.rb
+++ b/spec/decorators/alchemy/element_editor_spec.rb
@@ -178,6 +178,18 @@
it { is_expected.to eq(false) }
end
+
+ context "but element has ingredients defined" do
+ before {
+ expect(element).to receive(:ingredient_definitions) {
+ [{
+ role: "headline", type: "Headline",
+ }]
+ }
+ }
+
+ it { is_expected.to eq(true) }
+ end
end
end
end
diff --git a/spec/decorators/alchemy/ingredient_editor_spec.rb b/spec/decorators/alchemy/ingredient_editor_spec.rb
new file mode 100644
index 0000000000..d773e21068
--- /dev/null
+++ b/spec/decorators/alchemy/ingredient_editor_spec.rb
@@ -0,0 +1,203 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Alchemy::IngredientEditor do
+ let(:element) { build(:alchemy_element, name: "element_with_ingredients") }
+ let(:ingredient) { Alchemy::Ingredients::Text.build(role: "headline", element: element) }
+ let(:ingredient_editor) { described_class.new(ingredient) }
+
+ describe "#ingredient" do
+ it "returns ingredient object" do
+ expect(ingredient_editor.ingredient).to be(ingredient)
+ end
+ end
+
+ describe "#css_classes" do
+ subject { ingredient_editor.css_classes }
+
+ it "includes ingredient_editor class" do
+ is_expected.to include("ingredient-editor")
+ end
+
+ it "includes essence partial class" do
+ is_expected.to include(ingredient.partial_name)
+ end
+
+ context "when deprecated" do
+ before do
+ expect(ingredient).to receive(:deprecated?) { true }
+ end
+
+ it "includes deprecated" do
+ is_expected.to include("deprecated")
+ end
+ end
+ end
+
+ describe "#data_attributes" do
+ it "includes ingredient_id" do
+ expect(ingredient_editor.data_attributes[:ingredient_id]).to eq(ingredient.id)
+ end
+
+ it "includes ingredient_role" do
+ expect(ingredient_editor.data_attributes[:ingredient_role]).to eq(ingredient.role)
+ end
+ end
+
+ describe "#to_partial_path" do
+ subject { ingredient_editor.to_partial_path }
+
+ it "returns the editor partial path" do
+ is_expected.to eq("alchemy/ingredients/text_editor")
+ end
+ end
+
+ describe "#form_field_name" do
+ it "returns a name for form fields with value as default" do
+ expect(ingredient_editor.form_field_name).to eq("element[ingredients_attributes][0][value]")
+ end
+
+ context "with a value given" do
+ it "returns a name for form fields for that column" do
+ expect(ingredient_editor.form_field_name(:link_title)).to eq("element[ingredients_attributes][0][link_title]")
+ end
+ end
+ end
+
+ describe "#form_field_id" do
+ it "returns a id value for form fields with ingredient as default" do
+ expect(ingredient_editor.form_field_id).to eq("element_ingredients_attributes_0_value")
+ end
+
+ context "with a value given" do
+ it "returns a id value for form fields for that column" do
+ expect(ingredient_editor.form_field_id(:link_title)).to eq("element_ingredients_attributes_0_link_title")
+ end
+ end
+ end
+
+ describe "#respond_to?(:to_model)" do
+ subject { ingredient_editor.respond_to?(:to_model) }
+
+ it { is_expected.to be(false) }
+ end
+
+ describe "#has_warnings?" do
+ subject { ingredient_editor.has_warnings? }
+
+ context "when ingredient is not deprecated" do
+ it { is_expected.to be(false) }
+ end
+
+ context "when ingredient is deprecated" do
+ let(:ingredient) do
+ mock_model("Alchemy::Ingredients::Text", definition: { deprecated: true }, deprecated?: true)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context "when ingredient is missing its definition" do
+ let(:ingredient) do
+ mock_model("Alchemy::Ingredients::Text", definition: {})
+ end
+
+ it { is_expected.to be(true) }
+ end
+ end
+
+ describe "#warnings" do
+ subject { ingredient_editor.warnings }
+
+ context "when ingredient has no warnings" do
+ it { is_expected.to be_nil }
+ end
+
+ context "when ingredient is missing its definition" do
+ let(:ingredient) do
+ mock_model("Alchemy::Ingredients::Text", definition: {})
+ end
+
+ it { is_expected.to eq Alchemy.t(:ingredient_definition_missing) }
+
+ it "logs a warning" do
+ expect(Alchemy::Logger).to receive(:warn)
+ subject
+ end
+ end
+
+ context "when ingredient is deprecated" do
+ let(:ingredient) do
+ mock_model(
+ "Alchemy::Ingredients::Text",
+ definition: {
+ role: "foo",
+ deprecated: "Deprecated",
+ }, deprecated?: true,
+ )
+ end
+
+ it "returns a deprecation notice" do
+ is_expected.to eq("Deprecated")
+ end
+ end
+ end
+
+ describe "#deprecation_notice" do
+ subject { ingredient_editor.deprecation_notice }
+
+ context "when ingredient is not deprecated" do
+ it { is_expected.to be_nil }
+ end
+
+ context "when ingredient is deprecated" do
+ context "with String as deprecation" do
+ let(:ingredient) do
+ mock_model(
+ "Alchemy::Ingredients::Text",
+ definition: {
+ role: "foo",
+ deprecated: "Ingredient is deprecated",
+ }, deprecated?: true,
+ )
+ end
+
+ it { is_expected.to eq("Ingredient is deprecated") }
+ end
+
+ context "without custom ingredient translation" do
+ let(:ingredient) do
+ mock_model(
+ "Alchemy::Ingredients::Text",
+ definition: {
+ role: "foo",
+ deprecated: true,
+ }, deprecated?: true,
+ element: element,
+ )
+ end
+
+ it do
+ is_expected.to eq(
+ "WARNING! This content is deprecated and will be removed soon. " \
+ "Please do not use it anymore."
+ )
+ end
+ end
+
+ context "with custom ingredient translation" do
+ let(:element) { build(:alchemy_element, name: "all_you_can_eat_ingredients") }
+
+ let(:ingredient) do
+ Alchemy::Ingredients::Html.build(
+ role: "html",
+ element: element,
+ )
+ end
+
+ it { is_expected.to eq("Old ingredient is deprecated") }
+ end
+ end
+ end
+end
diff --git a/spec/dummy/app/views/alchemy/elements/_all_you_can_eat_ingredients.html.erb b/spec/dummy/app/views/alchemy/elements/_all_you_can_eat_ingredients.html.erb
new file mode 100644
index 0000000000..23bb9b9070
--- /dev/null
+++ b/spec/dummy/app/views/alchemy/elements/_all_you_can_eat_ingredients.html.erb
@@ -0,0 +1,46 @@
+<%- cache(all_you_can_eat_ingredients) do -%>
+ <%= element_view_for(all_you_can_eat_ingredients) do |el| -%>
+ Welcome to Peters Petshop.
",
+ )
+ end
+
+ it "has a HTML tag free version of body column" do
+ richtext_ingredient.save
+ expect(richtext_ingredient.stripped_body).to eq("Hello!Welcome to Peters Petshop.")
+ end
+
+ it "has a sanitized version of body column" do
+ richtext_ingredient.save
+ expect(richtext_ingredient.sanitized_body).to eq("Welcome to Peters Petshop.
")
+ end
+
+ describe "#tinymce_class_name" do
+ subject { richtext_ingredient.tinymce_class_name }
+
+ it { is_expected.to eq("has_tinymce") }
+
+ context "having custom tinymce config" do
+ before do
+ expect(richtext_ingredient).to receive(:settings) do
+ { tinymce: { toolbar: [] } }
+ end
+ end
+
+ it "returns role including element name" do
+ is_expected.to eq("has_tinymce element_with_ingredients_text")
+ end
+ end
+ end
+
+ describe "#has_tinymce?" do
+ subject { richtext_ingredient.has_tinymce? }
+
+ it { is_expected.to be(true) }
+ end
+
+ describe "preview_text" do
+ subject { richtext_ingredient.tap(&:save).preview_text }
+
+ it "returns the first 30 chars of the stripped body column" do
+ is_expected.to eq("Hello!Welcome to Peters Petsho")
+ end
+ end
+end
diff --git a/spec/models/alchemy/ingredients/select_spec.rb b/spec/models/alchemy/ingredients/select_spec.rb
new file mode 100644
index 0000000000..fde2629a72
--- /dev/null
+++ b/spec/models/alchemy/ingredients/select_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Alchemy::Ingredients::Select do
+ it_behaves_like "an alchemy ingredient"
+
+ let(:element) { build(:alchemy_element) }
+
+ let(:select_ingredient) do
+ described_class.new(
+ element: element,
+ type: described_class.name,
+ role: "color",
+ value: "A very nice bright color for the button",
+ )
+ end
+
+ describe "preview_text" do
+ subject { select_ingredient.preview_text }
+
+ it "returns first 30 characters of value" do
+ is_expected.to eq("A very nice bright color for t")
+ end
+ end
+end
diff --git a/spec/models/alchemy/ingredients/text_spec.rb b/spec/models/alchemy/ingredients/text_spec.rb
new file mode 100644
index 0000000000..9f8f874b73
--- /dev/null
+++ b/spec/models/alchemy/ingredients/text_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Alchemy::Ingredients::Text do
+ it_behaves_like "an alchemy ingredient"
+
+ let(:element) { build(:alchemy_element, name: "element_with_ingredients") }
+
+ let(:text_ingredient) do
+ described_class.new(
+ element: element,
+ type: described_class.name,
+ role: "headline",
+ value: "A brown fox quickly jumps over the lazy dog",
+ data: {
+ link: "https://example.com",
+ link_target: "_blank",
+ link_title: "Click here",
+ link_class_name: "button",
+ },
+ )
+ end
+
+ describe "#link" do
+ subject { text_ingredient.link }
+
+ it { is_expected.to eq("https://example.com") }
+ end
+
+ describe "#link_target" do
+ subject { text_ingredient.link_target }
+
+ it { is_expected.to eq("_blank") }
+ end
+
+ describe "#link_title" do
+ subject { text_ingredient.link_title }
+
+ it { is_expected.to eq("Click here") }
+ end
+
+ describe "#link_class_name" do
+ subject { text_ingredient.link_class_name }
+
+ it { is_expected.to eq("button") }
+ end
+
+ describe "#link=" do
+ before { text_ingredient.link = "https://foobar.io" }
+ subject { text_ingredient.link }
+ it { is_expected.to eq("https://foobar.io") }
+ end
+
+ describe "#link_target=" do
+ before { text_ingredient.link_target = "" }
+ subject { text_ingredient.link_target }
+ it { is_expected.to eq("") }
+ end
+
+ describe "#link_title=" do
+ before { text_ingredient.link_title = "Follow me" }
+ subject { text_ingredient.link_title }
+ it { is_expected.to eq("Follow me") }
+ end
+
+ describe "#link_class_name=" do
+ before { text_ingredient.link_class_name = "btn btn-default" }
+ subject { text_ingredient.link_class_name }
+ it { is_expected.to eq("btn btn-default") }
+ end
+
+ describe "preview_text" do
+ subject { text_ingredient.preview_text }
+
+ it "returns the first 30 chars of the value" do
+ is_expected.to eq("A brown fox quickly jumps over")
+ end
+ end
+end
diff --git a/spec/models/alchemy/ingredients/video_spec.rb b/spec/models/alchemy/ingredients/video_spec.rb
new file mode 100644
index 0000000000..beb5747d43
--- /dev/null
+++ b/spec/models/alchemy/ingredients/video_spec.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Alchemy::Ingredients::Video do
+ it_behaves_like "an alchemy ingredient"
+
+ let(:element) { build(:alchemy_element) }
+ let(:attachment) { build_stubbed(:alchemy_attachment) }
+
+ let(:video_ingredient) do
+ described_class.new(
+ element: element,
+ type: described_class.name,
+ role: "podcast",
+ related_object: attachment,
+ )
+ end
+
+ describe "#allow_fullscreen" do
+ subject { video_ingredient.allow_fullscreen }
+ before { video_ingredient.allow_fullscreen = true }
+ it { is_expected.to eq(true) }
+ end
+
+ describe "#autoplay" do
+ subject { video_ingredient.autoplay }
+ before { video_ingredient.autoplay = false }
+ it { is_expected.to eq(false) }
+ end
+
+ describe "#controls" do
+ subject { video_ingredient.controls }
+ before { video_ingredient.controls = true }
+ it { is_expected.to eq(true) }
+ end
+
+ describe "#height" do
+ subject { video_ingredient.height }
+ before { video_ingredient.height = 720 }
+ it { is_expected.to eq(720) }
+ end
+
+ describe "#loop" do
+ subject { video_ingredient.loop }
+ before { video_ingredient.loop = false }
+ it { is_expected.to eq(false) }
+ end
+
+ describe "#muted" do
+ subject { video_ingredient.muted }
+ before { video_ingredient.muted = true }
+ it { is_expected.to eq(true) }
+ end
+
+ describe "#preload" do
+ subject { video_ingredient.preload }
+ before { video_ingredient.preload = "auto" }
+ it { is_expected.to eq("auto") }
+ end
+
+ describe "#width" do
+ subject { video_ingredient.width }
+ before { video_ingredient.width = 1280 }
+ it { is_expected.to eq(1280) }
+ end
+
+ describe "#attachment" do
+ subject { video_ingredient.attachment }
+
+ it { is_expected.to be_an(Alchemy::Attachment) }
+ end
+
+ describe "#attachment=" do
+ let(:attachment) { Alchemy::Attachment.new }
+
+ subject { video_ingredient.attachment = attachment }
+
+ it { is_expected.to be(attachment) }
+ end
+
+ describe "#attachment_id" do
+ subject { video_ingredient.attachment_id }
+
+ it { is_expected.to be_an(Integer) }
+ end
+
+ describe "#attachment_id=" do
+ let(:attachment) { Alchemy::Attachment.new(id: 111) }
+
+ subject { video_ingredient.attachment_id = attachment.id }
+
+ it { is_expected.to be(111) }
+ it { expect(video_ingredient.related_object_type).to eq("Alchemy::Attachment") }
+ end
+
+ describe "#preview_text" do
+ subject { video_ingredient.preview_text }
+
+ context "with a attachment" do
+ let(:attachment) do
+ Alchemy::Attachment.new(name: "A very long file name that would not fit")
+ end
+
+ it "returns first 30 characters of the attachment name" do
+ is_expected.to eq("A very long file name that wou")
+ end
+ end
+
+ context "with no attachment" do
+ let(:attachment) { nil }
+
+ it { is_expected.to eq("") }
+ end
+ end
+
+ describe "#value" do
+ subject { video_ingredient.value }
+
+ context "with attachment assigned" do
+ it "returns attachment" do
+ is_expected.to be(attachment)
+ end
+ end
+
+ context "with no attachment assigned" do
+ let(:attachment) { nil }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/models/alchemy/page_spec.rb b/spec/models/alchemy/page_spec.rb
index c6b6014e9a..ad907047ec 100644
--- a/spec/models/alchemy/page_spec.rb
+++ b/spec/models/alchemy/page_spec.rb
@@ -1933,6 +1933,62 @@ class AnotherUrlPathClass; end
end
end
+ describe "#richtext_ingredients_ids" do
+ let!(:page) { create(:alchemy_page) }
+
+ let!(:expanded_element) do
+ create :alchemy_element, :with_ingredients,
+ name: "element_with_ingredients",
+ page_version: page.draft_version,
+ folded: false
+ end
+
+ let!(:folded_element) do
+ create :alchemy_element, :with_ingredients,
+ name: "element_with_ingredients",
+ page_version: page.draft_version,
+ folded: true
+ end
+
+ subject(:richtext_ingredients_ids) { page.richtext_ingredients_ids }
+
+ it "returns ingredient ids for all expanded elements that have tinymce enabled" do
+ expanded_rtf_ingredients = expanded_element.ingredients.richtexts
+ expect(richtext_ingredients_ids).to eq(expanded_rtf_ingredients.pluck(:id))
+ folded_rtf_ingredient = folded_element.ingredients.richtexts.first
+ expect(richtext_ingredients_ids).to_not include(folded_rtf_ingredient.id)
+ end
+
+ context "with nested elements" do
+ let!(:nested_expanded_element) do
+ create :alchemy_element, :with_ingredients,
+ name: "element_with_ingredients",
+ page_version: page.draft_version,
+ parent_element: expanded_element,
+ folded: false
+ end
+
+ let!(:nested_folded_element) do
+ create :alchemy_element, :with_ingredients,
+ name: "element_with_ingredients",
+ page_version: page.draft_version,
+ parent_element: folded_element,
+ folded: true
+ end
+
+ it "returns ingredient ids for all expanded nested elements that have tinymce enabled" do
+ expanded_rtf_ingredients = expanded_element.ingredients.richtexts
+ nested_expanded_rtf_ingredients = nested_expanded_element.ingredients.richtexts
+ rtf_ingredient_ids = expanded_rtf_ingredients.pluck(:id) + nested_expanded_rtf_ingredients.pluck(:id)
+ expect(richtext_ingredients_ids.sort).to eq(rtf_ingredient_ids)
+
+ nested_folded_rtf_ingredient = nested_folded_element.ingredients.richtexts.first
+
+ expect(richtext_ingredients_ids).to_not include(nested_folded_rtf_ingredient.id)
+ end
+ end
+ end
+
describe "#fixed_attributes" do
let(:page) { Alchemy::Page.new }
diff --git a/spec/models/alchemy/site_spec.rb b/spec/models/alchemy/site_spec.rb
index f80654df50..896ecc4847 100644
--- a/spec/models/alchemy/site_spec.rb
+++ b/spec/models/alchemy/site_spec.rb
@@ -50,7 +50,7 @@ module Alchemy
# No need to create a default site, as it has already been added through the seeds.
# But let's add some more:
#
- let(:default_site) { Site.default }
+ let(:default_site) { Site.default }
let!(:magiclabs_site) { create(:alchemy_site, host: "www.magiclabs.de", aliases: "magiclabs.de magiclabs.com www.magiclabs.com") }
subject { Site.find_for_host(host) }
@@ -108,7 +108,7 @@ module Alchemy
subject { Site.definitions }
context "with file present" do
- let(:definitions) { [{"name" => "lala"}] }
+ let(:definitions) { [{ "name" => "lala" }] }
before { expect(YAML).to receive(:load_file).and_return(definitions) }
it { is_expected.to eq(definitions) }
end
@@ -166,7 +166,7 @@ module Alchemy
describe "#definition" do
let(:site) { Site.new(name: "My custom site") }
- let(:definitions) { [{"name" => "my_custom_site", "page_layouts" => %w(standard)}] }
+ let(:definitions) { [{ "name" => "my_custom_site", "page_layouts" => %w(standard) }] }
it "returns layout definition from site_layouts.yml file" do
allow(Site).to receive(:definitions).and_return(definitions)
@@ -257,6 +257,12 @@ module Alchemy
{
"name" => "index",
"unique" => true,
+ "elements" => [
+ "all_you_can_eat_ingredients",
+ ],
+ "autogenerate" => [
+ "all_you_can_eat_ingredients",
+ ],
},
{
"name" => "readonly",
@@ -301,6 +307,8 @@ module Alchemy
"right_column",
"left_column",
"old",
+ "all_you_can_eat_ingredients",
+ "element_with_ingredients",
],
},
{
diff --git a/spec/presenters/alchemy/picture_view_spec.rb b/spec/presenters/alchemy/picture_view_spec.rb
new file mode 100644
index 0000000000..59f48b3215
--- /dev/null
+++ b/spec/presenters/alchemy/picture_view_spec.rb
@@ -0,0 +1,334 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Alchemy::PictureView do
+ include Capybara::RSpecMatchers
+
+ let(:image) do
+ File.new(File.expand_path("../../fixtures/image.png", __dir__))
+ end
+
+ let(:picture) do
+ stub_model Alchemy::Picture,
+ image_file_format: "png",
+ image_file: image
+ end
+
+ let(:ingredient) do
+ stub_model Alchemy::Ingredients::Picture,
+ role: "image",
+ picture: picture,
+ data: {
+ caption: "This is a cute cat",
+ }
+ end
+
+ let(:picture_url) { "/pictures/1/image.png" }
+
+ before do
+ allow(picture).to receive(:url) { picture_url }
+ end
+
+ describe "DEFAULT_OPTIONS" do
+ subject { Alchemy::PictureView::DEFAULT_OPTIONS }
+
+ it do
+ is_expected.to eq({
+ show_caption: true,
+ disable_link: false,
+ srcset: [],
+ sizes: [],
+ }.with_indifferent_access)
+ end
+ end
+
+ context "with caption" do
+ let(:options) do
+ {}
+ end
+
+ let(:html_options) do
+ {}
+ end
+
+ subject(:view) do
+ described_class.new(ingredient, options, html_options).render
+ end
+
+ it "should enclose the image in a