diff --git a/.rubocop.yml b/.rubocop.yml
index 9ae1bc81eb..644dbeaa92 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -94,6 +94,9 @@ Layout/MultilineMethodCallBraceLayout:
Layout/MultilineMethodCallIndentation:
Enabled: false
+Layout/MultilineOperationIndentation:
+ EnforcedStyle: indented
+
Layout/SpaceBeforeBlockBraces:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylespacebeforeblockbraces
diff --git a/app/helpers/alchemy/elements_helper.rb b/app/helpers/alchemy/elements_helper.rb
index b4adbc560e..ebc4ab9214 100644
--- a/app/helpers/alchemy/elements_helper.rb
+++ b/app/helpers/alchemy/elements_helper.rb
@@ -10,7 +10,7 @@ module ElementsHelper
include Alchemy::UrlHelper
include Alchemy::ElementsBlockHelper
- # Renders all elements from current page
+ # Renders elements from given page
#
# == Examples:
#
@@ -45,53 +45,69 @@ module ElementsHelper
# with: 'contact_teaser'
# }) %>
#
- # @param [Hash] options
- # Additional options.
+ # === Custom elements finder:
+ #
+ # Having a custom element finder class:
+ #
+ # class MyCustomNewsArchive
+ # def elements(page:)
+ # news_page.elements.available.named('news').order(created_at: :desc)
+ # end
+ #
+ # private
+ #
+ # def news_page
+ # Alchemy::Page.where(page_layout: 'news-archive')
+ # end
+ # end
+ #
+ # In your view:
#
+ #
+ # <%= render_elements finder: MyCustomNewsArchive.new %>
+ #
+ #
+ # @option options [Alchemy::Page|String] :from_page (@page)
+ # The page the elements are rendered from. You can pass a page_layout String or a {Alchemy::Page} object.
+ # @option options [Array|String] :only
+ # A list of element names only to be rendered.
+ # @option options [Array|String] :except
+ # A list of element names not to be rendered.
# @option options [Number] :count
# The amount of elements to be rendered (begins with first element found)
- # @option options [Array or String] :except ([])
- # A list of element names not to be rendered.
+ # @option options [Number] :offset
+ # The offset to begin loading elements from
# @option options [Hash] :fallback
# Define elements that are rendered from another page.
- # @option options [Alchemy::Page or String] :from_page (@page)
- # The page the elements are rendered from. You can pass a page_layout String or a {Alchemy::Page} object.
- # @option options [Array or String] :only ([])
- # A list of element names only to be rendered.
- # @option options [Boolean] :random
+ # @option options [Boolean] :random (false)
# Randomize the output of elements
- # @option options [Boolean] :reverse
+ # @option options [Boolean] :reverse (false)
# Reverse the rendering order
- # @option options [String] :sort_by
- # The name of a {Alchemy::Content} to sort the elements by
# @option options [String] :separator
- # A string that will be used to join the element partials. Default nil
+ # A string that will be used to join the element partials.
+ # @option options [Class] :finder (Alchemy::ElementsFinder)
+ # A class instance that will return elements that get rendered.
+ # Use this for your custom element loading logic in views.
#
def render_elements(options = {})
options = {
from_page: @page,
- render_format: 'html',
- reverse: false
+ render_format: 'html'
}.update(options)
- pages = pages_holding_elements(options.delete(:from_page))
-
- if pages.blank?
- warning('No page to get elements from was found')
- return
+ if options[:sort_by]
+ Alchemy::Deprecation.warn "options[:sort_by] has been removed without replacement. " /
+ "Please implement your own element sorting by passing a custom finder instance to options[:finder]."
end
- elements = collect_elements_from_pages(pages, options)
+ finder = options[:finder] || Alchemy::ElementsFinder.new(options)
+ elements = finder.elements(page: options[:from_page])
- if options[:sort_by].present?
- elements = sort_elements_by_content(
- elements,
- options.delete(:sort_by),
- options[:reverse]
- )
+ buff = []
+ elements.each_with_index do |element, i|
+ buff << render_element(element, :view, options, i + 1)
end
-
- render_element_view_partials(elements, options)
+ buff.join(options[:separator]).html_safe
end
# This helper renders a {Alchemy::Element} partial.
@@ -231,14 +247,14 @@ def element_tags_attributes(element, options = {})
end
# Sort given elements by content.
- #
+ # @deprecated
# @param [Array] elements - The elements you want to sort
# @param [String] content_name - The name of the content you want to sort by
# @param [Boolean] reverse - Reverse the sorted elements order
#
# @return [Array]
- #
def sort_elements_by_content(elements, content_name, reverse = false)
+ Alchemy::Deprecation.warn "options[:sort_by] is deprecated. Please implement your own element sorting."
sorted_elements = elements.sort_by do |element|
content = element.content_by_name(content_name)
content ? content.ingredient.to_s : ''
@@ -246,58 +262,5 @@ def sort_elements_by_content(elements, content_name, reverse = false)
reverse ? sorted_elements.reverse : sorted_elements
end
-
- private
-
- def pages_holding_elements(page)
- case page
- when String
- Language.current.pages.where(
- page_layout: page,
- restricted: false
- ).to_a
- when Page
- page
- end
- end
-
- def collect_elements_from_pages(page, options)
- if page.is_a? Array
- elements = page.collect { |p| p.find_elements(options) }.flatten
- else
- elements = page.find_elements(options)
- end
- if fallback_required?(elements, options)
- elements += fallback_elements(options)
- end
- elements
- end
-
- def fallback_required?(elements, options)
- options[:fallback] && elements.detect { |e| e.name == options[:fallback][:for] }.nil?
- end
-
- def fallback_elements(options)
- fallback_options = options.delete(:fallback)
- case fallback_options[:from]
- when String
- page = Language.current.pages.find_by(
- page_layout: fallback_options[:from],
- restricted: false
- )
- when Page
- page = fallback_options[:from]
- end
- return [] if page.blank?
- page.elements.not_trashed.named(fallback_options[:with].presence || fallback_options[:for])
- end
-
- def render_element_view_partials(elements, options = {})
- buff = []
- elements.each_with_index do |element, i|
- buff << render_element(element, :view, options, i + 1)
- end
- buff.join(options[:separator]).html_safe
- end
end
end
diff --git a/app/models/alchemy/element.rb b/app/models/alchemy/element.rb
index f5c9e8531f..b557518d31 100644
--- a/app/models/alchemy/element.rb
+++ b/app/models/alchemy/element.rb
@@ -93,7 +93,7 @@ class Element < BaseRecord
after_update :touch_touchable_pages
scope :trashed, -> { where(position: nil).order('updated_at DESC') }
- scope :not_trashed, -> { where(Element.arel_table[:position].not_eq(nil)) }
+ scope :not_trashed, -> { where.not(position: nil) }
scope :published, -> { where(public: true) }
scope :not_restricted, -> { joins(:page).merge(Page.not_restricted) }
scope :available, -> { published.not_trashed }
diff --git a/app/models/alchemy/page.rb b/app/models/alchemy/page.rb
index 766be8a949..acca8640f7 100644
--- a/app/models/alchemy/page.rb
+++ b/app/models/alchemy/page.rb
@@ -289,6 +289,38 @@ def new_name_for_copy(custom_name, source_name)
# Instance methods
#
+ # Returns elements from page.
+ #
+ # @option options [Array|String] :only
+ # Returns only elements with given names
+ # @option options [Array|String] :except
+ # Returns all elements except the ones with given names
+ # @option options [Integer] :count
+ # Limit the count of returned elements
+ # @option options [Integer] :offset
+ # Starts with an offset while returning elements
+ # @option options [Boolean] :include_hidden (false)
+ # Return hidden elements as well
+ # @option options [Boolean] :random (false)
+ # Return elements randomly shuffled
+ # @option options [Boolean] :reverse (false)
+ # Reverse the load order
+ # @option options [Class] :finder (Alchemy::ElementsFinder)
+ # A class that will return elements from page.
+ # Use this for your custom element loading logic.
+ #
+ # @return [ActiveRecord::Relation]
+ def find_elements(options = {}, show_non_public = false)
+ if show_non_public
+ Alchemy::Deprecation.warn "Passing true as second argument to page#find_elements to include" /
+ " invisible elements has been removed. Please implement your own ElementsFinder" /
+ " and pass it with options[:finder]."
+ end
+
+ finder = options[:finder] || Alchemy::ElementsFinder.new(options)
+ finder.elements(page: self)
+ end
+
# The page's view partial is dependent from its page layout
#
# == Define page layouts
diff --git a/app/models/alchemy/page/page_elements.rb b/app/models/alchemy/page/page_elements.rb
index 1eb24d919e..6d1bf66c5f 100644
--- a/app/models/alchemy/page/page_elements.rb
+++ b/app/models/alchemy/page/page_elements.rb
@@ -61,43 +61,6 @@ def copy_elements(source, target)
end
end
- # Finds elements of page.
- #
- # @param [Hash]
- # options hash
- # @param [Boolean] (false)
- # Pass true, if you want to also have not published elements.
- #
- # @option options [Array] only
- # Returns only elements with given names
- # @option options [Array] except
- # Returns all elements except the ones with given names
- # @option options [Fixnum] count
- # Limit the count of returned elements
- # @option options [Fixnum] offset
- # Starts with an offset while returning elements
- # @option options [Boolean] random (false)
- # Return elements randomly shuffled
- #
- # @return [ActiveRecord::Relation]
- #
- def find_elements(options = {}, show_non_public = false)
- elements = self.elements
- if options[:only].present?
- elements = elements.named(options[:only])
- elsif options[:except].present?
- elements = elements.excluded(options[:except])
- end
- if options[:reverse_sort] || options[:reverse]
- elements = elements.reverse_order
- end
- elements = elements.offset(options[:offset]).limit(options[:count])
- if options[:random]
- elements = elements.order("RAND()")
- end
- show_non_public ? elements : elements.published
- end
-
# All available element definitions that can actually be placed on current page.
#
# It extracts all definitions that are unique or limited and already on page.
diff --git a/lib/alchemy/elements_finder.rb b/lib/alchemy/elements_finder.rb
new file mode 100644
index 0000000000..4ef295f991
--- /dev/null
+++ b/lib/alchemy/elements_finder.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'alchemy/logger'
+
+module Alchemy
+ # Loads elements from given page
+ #
+ # Used by {Alchemy::Page#find_elements} and {Alchemy::ElementsHelper#render_elements} helper.
+ #
+ # If you need custom element loading logic in your views you can create your own finder class and
+ # tell the {Alchemy::ElementsHelper#render_elements} helper or {Alchemy::Page#find_elements}
+ # to use that finder instead of this one.
+ #
+ class ElementsFinder
+ # @option options [Array|String] :only
+ # A list of element names to load only.
+ # @option options [Array|String] :except
+ # A list of element names not to load.
+ # @option options [Boolean] :fixed (false)
+ # Return only fixed elements
+ # @option options [Integer] :count
+ # The amount of elements to load
+ # @option options [Integer] :offset
+ # The offset to begin loading elements from
+ # @option options [Boolean] :random (false)
+ # Randomize the output of elements
+ # @option options [Boolean] :reverse (false)
+ # Reverse the load order
+ # @option options [Hash] :fallback
+ # Define elements that are loaded from another page if no element was found on given page.
+ def initialize(options = {})
+ @options = options
+ end
+
+ # @param page [Alchemy::Page|String]
+ # The page the elements are loaded from. You can pass a page_layout String or a {Alchemy::Page} object.
+ # @return [ActiveRecord::Relation]
+ def elements(page:)
+ elements = find_elements(page)
+
+ if fallback_required?(elements)
+ elements = elements.merge(fallback_elements)
+ end
+
+ if options[:reverse]
+ elements = elements.reverse_order
+ end
+
+ if options[:random]
+ elements = elements.reorder(Arel.sql(random_function))
+ end
+
+ elements.offset(options[:offset]).limit(options[:count])
+ end
+
+ private
+
+ attr_reader :page, :options
+
+ def find_elements(page)
+ elements = Alchemy::Element
+ .where(page_id: page_ids(page))
+ .merge(Alchemy::Element.not_nested)
+ .where(fixed: !!options[:fixed])
+ .order(position: :asc)
+ .available
+
+ if options[:only]
+ elements = elements.named(options[:only])
+ end
+
+ if options[:except]
+ elements = elements.excluded(options[:except])
+ end
+
+ elements
+ end
+
+ def page_ids(page)
+ case page
+ when String
+ Alchemy::Language.current.pages.where(
+ page_layout: page,
+ restricted: false
+ ).pluck("#{Alchemy::Page.table_name}.id")
+ when Alchemy::Page
+ page.id
+ end
+ end
+
+ def fallback_required?(elements)
+ options[:fallback] && elements
+ .where(Alchemy::Element.table_name => {name: options[:fallback][:for]})
+ .none?
+ end
+
+ def fallback_elements
+ find_elements(options[:fallback][:from])
+ .named(options[:fallback][:with] || options[:fallback][:for])
+ end
+
+ def random_function
+ case ActiveRecord::Base.connection_config[:adapter]
+ when 'postgresql', 'sqlite3'
+ then 'RANDOM()'
+ else
+ 'RAND()'
+ end
+ end
+ end
+end
diff --git a/lib/alchemy_cms.rb b/lib/alchemy_cms.rb
index 11df9471fb..410572b0b0 100644
--- a/lib/alchemy_cms.rb
+++ b/lib/alchemy_cms.rb
@@ -33,6 +33,7 @@ module Alchemy
require_relative 'alchemy/configuration_methods'
require_relative 'alchemy/controller_actions'
require_relative 'alchemy/deprecation'
+require_relative 'alchemy/elements_finder'
require_relative 'alchemy/errors'
require_relative 'alchemy/essence'
require_relative 'alchemy/filetypes'
diff --git a/spec/helpers/alchemy/elements_helper_spec.rb b/spec/helpers/alchemy/elements_helper_spec.rb
index 3a40bae61f..94d2fd7e1a 100644
--- a/spec/helpers/alchemy/elements_helper_spec.rb
+++ b/spec/helpers/alchemy/elements_helper_spec.rb
@@ -68,17 +68,14 @@ module Alchemy
describe "#render_elements" do
subject { helper.render_elements(options) }
- let(:another_element) { build_stubbed(:alchemy_element, page: page) }
- let(:elements) { [element, another_element] }
+ let(:page) { create(:alchemy_page, :public) }
+ let!(:element) { create(:alchemy_element, name: 'headline', page: page) }
+ let!(:another_element) { create(:alchemy_element, page: page) }
context 'without any options' do
let(:options) { {} }
- before do
- expect(page).to receive(:find_elements).and_return(elements)
- end
-
- it "should render all elements from page." do
+ it "should render all elements from current page." do
is_expected.to have_selector("##{element.name}_#{element.id}")
is_expected.to have_selector("##{another_element.name}_#{another_element.id}")
end
@@ -86,128 +83,45 @@ module Alchemy
context "with from_page option" do
context 'is a page object' do
- let(:another_page) { build_stubbed(:alchemy_page, :public) }
- let(:options) { {from_page: another_page} }
+ let(:another_page) { create(:alchemy_page, :public) }
- before do
- expect(another_page).to receive(:find_elements).and_return(elements)
+ let(:options) do
+ { from_page: another_page }
end
+ let!(:element) { create(:alchemy_element, name: 'headline', page: another_page) }
+ let!(:another_element) { create(:alchemy_element, page: another_page) }
+
it "should render all elements from that page." do
is_expected.to have_selector("##{element.name}_#{element.id}")
is_expected.to have_selector("##{another_element.name}_#{another_element.id}")
end
end
- context 'is a string' do
- let(:another_page) { build_stubbed(:alchemy_page, :public) }
- let(:another_element) { build_stubbed(:alchemy_element, page: another_page) }
- let(:other_elements) { [another_element] }
- let(:options) { {from_page: 'news'} }
-
- before do
- allow(Language).to receive(:current).and_return double(pages: double(where: pages))
- expect(another_page).to receive(:find_elements).and_return(other_elements)
+ context 'if from_page is nil' do
+ let(:options) do
+ { from_page: nil }
end
- context 'and one page can be found by page layout' do
- let(:pages) { [another_page] }
-
- it "it renders all elements from that page." do
- is_expected.to have_selector("##{another_element.name}_#{another_element.id}")
- end
- end
-
- context 'and an array of pages has been found' do
- let(:pages) { [page, another_page] }
-
- before do
- expect(page).to receive(:find_elements).and_return(elements)
- end
-
- it 'renders elements from these pages' do
- is_expected.to have_selector("##{element.name}_#{element.id}")
- is_expected.to have_selector("##{another_element.name}_#{another_element.id}")
- end
- end
+ it { is_expected.to be_empty }
end
end
- context 'if page is nil' do
- let(:options) { {from_page: nil} }
- it { is_expected.to be_blank }
- end
-
- context 'with sort_by and reverse option given' do
- let(:options) { {sort_by: true, reverse: true} }
- let(:sorted_elements) { [another_element, element] }
-
- before do
- expect(elements).to receive(:sort_by).and_return(sorted_elements)
- expect(sorted_elements).to receive(:reverse).and_return(elements)
- expect(page).to receive(:find_elements).and_return(elements)
- end
-
- it "renders the sorted elements in reverse order" do
- is_expected.not_to be_blank
- end
- end
-
- context 'with sort_by option given' do
- let(:options) { {sort_by: 'title'} }
- let(:sorted_elements) { [another_element, element] }
-
- before do
- expect(elements).to receive(:sort_by).and_return(sorted_elements)
- expect(elements).not_to receive(:reverse)
- expect(page).to receive(:find_elements).and_return(elements)
- end
-
- it "renders the elements in the order of given content name" do
- is_expected.not_to be_blank
- end
- end
-
- context "with option fallback" do
- let(:another_page) { build_stubbed(:alchemy_page, :public, name: 'Another Page', page_layout: 'news') }
- let(:another_element) { build_stubbed(:alchemy_element, page: another_page, name: 'news') }
- let(:elements) { [another_element] }
-
- context 'with string given as :fallback_from' do
- let(:options) { {fallback: {for: 'higgs', with: 'news', from: 'news'}} }
-
- before do
- allow(Language).to receive(:current).and_return double(pages: double(find_by: another_page))
- allow(another_page).to receive(:elements).and_return double(not_trashed: double(named: elements))
- end
-
- it "renders the fallback element" do
- is_expected.to have_selector("#news_#{another_element.id}")
- end
- end
-
- context 'with page given as :fallback_from' do
- let(:options) { {fallback: {for: 'higgs', with: 'news', from: another_page}} }
-
- before do
- allow(another_page).to receive(:elements).and_return double(not_trashed: double(named: elements))
- end
+ context 'with option separator given' do
+ let(:options) { {separator: '
'} }
- it "renders the fallback element" do
- is_expected.to have_selector("#news_#{another_element.id}")
- end
+ it "joins element partials with given string" do
+ is_expected.to have_selector('hr')
end
end
- context 'with option separator given' do
- let(:options) { {separator: '
'} }
-
- before do
- expect(page).to receive(:find_elements).and_return(elements)
+ context 'with custom elements finder' do
+ let(:options) do
+ { finder: CustomNewsElementsFinder.new }
end
- it "joins element partials with given string" do
- is_expected.to have_selector('hr')
+ it 'uses that to load elements to render' do
+ is_expected.to have_selector("#news_1001")
end
end
end
@@ -276,53 +190,5 @@ module Alchemy
it { is_expected.to be_blank }
end
end
-
- describe '#sort_elements_by_content' do
- subject { sort_elements_by_content(elements, 'headline') }
-
- let(:element_1) { build_stubbed(:alchemy_element) }
- let(:element_2) { build_stubbed(:alchemy_element) }
- let(:element_3) { build_stubbed(:alchemy_element) }
- let(:ingredient_a) { double(ingredient: 'a') }
- let(:ingredient_b) { double(ingredient: 'b') }
- let(:ingredient_c) { double(ingredient: 'c') }
- let(:elements) { [element_1, element_2, element_3] }
-
- before do
- expect(element_1).to receive(:content_by_name).and_return(ingredient_b)
- expect(element_2).to receive(:content_by_name).and_return(ingredient_c)
- expect(element_3).to receive(:content_by_name).and_return(ingredient_a)
- end
-
- it "sorts the elements by content" do
- is_expected.to eq [element_3, element_1, element_2]
- end
-
- context 'with element not having this content' do
- let(:element_4) { build_stubbed(:alchemy_element) }
- let(:elements) { [element_1, element_2, element_3, element_4] }
-
- before do
- expect(element_4).to receive(:content_by_name).and_return(nil)
- end
-
- it "puts it at first place" do
- is_expected.to eq [element_4, element_3, element_1, element_2]
- end
- end
-
- context 'with element having content with nil as ingredient' do
- let(:element_4) { build_stubbed(:alchemy_element) }
- let(:elements) { [element_1, element_2, element_3, element_4] }
-
- before do
- expect(element_4).to receive(:content_by_name).and_return(double(ingredient: nil))
- end
-
- it "puts it at first place" do
- is_expected.to eq [element_4, element_3, element_1, element_2]
- end
- end
- end
end
end
diff --git a/spec/libraries/controller_actions_spec.rb b/spec/libraries/controller_actions_spec.rb
index 5ba4073d56..c53c27e3f3 100644
--- a/spec/libraries/controller_actions_spec.rb
+++ b/spec/libraries/controller_actions_spec.rb
@@ -26,8 +26,10 @@
end
context "with custom current_user_method" do
- before do
+ around do |example|
Alchemy.current_user_method = 'current_admin'
+ example.run
+ Alchemy.current_user_method = 'current_user'
end
it "calls the custom method" do
@@ -37,16 +39,14 @@
end
context "with not implemented current_user_method" do
- before do
+ around do |example|
Alchemy.current_user_method = 'not_implemented_method'
- end
-
- after do
+ example.run
Alchemy.current_user_method = 'current_user'
end
it "raises an error" do
- expect{
+ expect {
controller.send :current_alchemy_user
}.to raise_error(Alchemy::NoCurrentUserFoundError)
end
@@ -60,6 +60,8 @@
after do
# We must never change the app's locale
expect(::I18n.locale).to eq(:en)
+ # Reset the current language so its fresh for every subsequent test
+ RequestStore.store[:alchemy_current_language] = nil
end
context "with a Language argument" do
diff --git a/spec/libraries/elements_finder_spec.rb b/spec/libraries/elements_finder_spec.rb
new file mode 100644
index 0000000000..8f02b1a91e
--- /dev/null
+++ b/spec/libraries/elements_finder_spec.rb
@@ -0,0 +1,233 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Alchemy::ElementsFinder do
+ let(:finder) { described_class.new(options) }
+ let(:options) { {} }
+
+ describe '#elements' do
+ subject { finder.elements }
+
+ let(:page) { create(:alchemy_page, :public) }
+ let!(:visible_element) { create(:alchemy_element, public: true, page: page) }
+ let!(:hidden_element) { create(:alchemy_element, public: false, page: page) }
+
+ context 'without page given' do
+ it do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with page object given' do
+ subject { finder.elements(page: page) }
+
+ it 'returns all public elements from page' do
+ is_expected.to eq([visible_element])
+ end
+
+ it 'does not return trashed elements' do
+ visible_element.remove_from_list
+ is_expected.to eq([])
+ end
+
+ context 'with multiple ordered elements' do
+ let!(:element_2) do
+ create(:alchemy_element, public: true, page: page).tap { |el| el.update_columns(position: 3) }
+ end
+
+ let!(:element_3) do
+ create(:alchemy_element, public: true, page: page).tap { |el| el.update_columns(position: 2) }
+ end
+
+ it 'returns elements ordered by position' do
+ is_expected.to eq([visible_element, element_3, element_2])
+ end
+ end
+
+ context 'with fixed elements present' do
+ let!(:fixed_element) { create(:alchemy_element, :fixed, page: page) }
+
+ it 'does not include fixed elements' do
+ is_expected.to_not include(fixed_element)
+ end
+
+ context 'with options[:fixed] set to true' do
+ let(:options) do
+ { fixed: true }
+ end
+
+ it 'includes only fixed elements' do
+ is_expected.to eq([fixed_element])
+ end
+ end
+ end
+
+ context 'with nested elements present' do
+ let!(:nested_element) { create(:alchemy_element, :nested, page: page) }
+
+ it 'does not include nested elements' do
+ is_expected.to_not include(nested_element)
+ end
+ end
+
+ context 'with options[:only] given' do
+ let(:options) do
+ { only: 'article' }
+ end
+
+ it "returns only the elements with that name" do
+ is_expected.to eq([visible_element])
+ end
+ end
+
+ context 'with options[:except] given' do
+ let(:options) do
+ { except: 'article' }
+ end
+
+ it "does not return the elements with that name" do
+ is_expected.to eq([])
+ end
+ end
+
+ context 'with options[:offset] given' do
+ let(:options) do
+ { offset: 2 }
+ end
+
+ let!(:visible_element_2) { create(:alchemy_element, public: true, page: page) }
+ let!(:visible_element_3) { create(:alchemy_element, public: true, page: page) }
+
+ it "returns elements beginning from that offset" do
+ is_expected.to eq([visible_element_3])
+ end
+ end
+
+ context 'with options[:count] given' do
+ let(:options) do
+ { count: 1 }
+ end
+
+ let!(:visible_element_2) { create(:alchemy_element, public: true, page: page) }
+
+ it "returns elements beginning from that offset" do
+ is_expected.to eq([visible_element])
+ end
+ end
+
+ context 'with options[:reverse] given' do
+ let(:options) do
+ { reverse: true }
+ end
+
+ let!(:visible_element_2) { create(:alchemy_element, public: true, page: page) }
+
+ it "returns elements in reverse order" do
+ is_expected.to eq([visible_element_2, visible_element])
+ end
+ end
+
+ context 'with options[:random] given' do
+ let(:options) do
+ { random: true }
+ end
+
+ let(:random_function) do
+ case ActiveRecord::Base.connection_config[:adapter]
+ when 'postgresql', 'sqlite3'
+ then 'RANDOM()'
+ else
+ 'RAND()'
+ end
+ end
+
+ it "returns elements in random order" do
+ expect_any_instance_of(ActiveRecord::Relation).to \
+ receive(:reorder).with(random_function).and_call_original
+ subject
+ end
+ end
+ end
+
+ context 'with page layout name given as options[:from_page]' do
+ subject { finder.elements(page: 'standard') }
+
+ let(:page) { create(:alchemy_page, :public, page_layout: 'standard') }
+ let!(:visible_element) { create(:alchemy_element, public: true, page: page) }
+ let!(:hidden_element) { create(:alchemy_element, public: false, page: page) }
+
+ let(:page_2) { create(:alchemy_page, :public, page_layout: 'standard') }
+ let!(:visible_element_2) { create(:alchemy_element, public: true, page: page_2) }
+ let!(:hidden_element_2) { create(:alchemy_element, public: false, page: page_2) }
+
+ it 'returns all public elements from all pages with given page layout' do
+ is_expected.to eq([visible_element, visible_element_2])
+ end
+ end
+
+ context 'with fallback options given' do
+ subject { finder.elements(page: page) }
+
+ let(:options) do
+ {
+ fallback: {
+ for: 'download',
+ from: page_2
+ }
+ }
+ end
+
+ context 'and no element from that kind on current page' do
+ let(:page) { create(:alchemy_page, :public, page_layout: 'standard') }
+
+ context 'but element of that kind on fallback page' do
+ let(:page_2) { create(:alchemy_page, :public, page_layout: 'standard') }
+ let!(:visible_element_2) { create(:alchemy_element, name: 'download', public: true, page: page_2) }
+
+ it 'loads elements from fallback page' do
+ is_expected.to eq([visible_element_2])
+ end
+ end
+
+ context 'with fallback element defined' do
+ let(:options) do
+ {
+ fallback: {
+ for: 'download',
+ with: 'header',
+ from: page_2
+ }
+ }
+ end
+
+ let(:page_2) { create(:alchemy_page, :public, page_layout: 'standard') }
+ let!(:visible_element_2) { create(:alchemy_element, name: 'header', public: true, page: page_2) }
+
+ it 'loads fallback element from fallback page' do
+ is_expected.to eq([visible_element_2])
+ end
+ end
+
+ context 'with fallback page defined as pagelayout name' do
+ let(:options) do
+ {
+ fallback: {
+ for: 'download',
+ with: 'text',
+ from: 'everything'
+ }
+ }
+ end
+
+ let(:page_2) { create(:alchemy_page, :public, page_layout: 'everything') }
+ let!(:visible_element_2) { create(:alchemy_element, name: 'text', public: true, page: page_2) }
+
+ it 'loads fallback element from fallback page' do
+ is_expected.to eq([visible_element_2])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/alchemy/page_spec.rb b/spec/models/alchemy/page_spec.rb
index 46d66c7d52..786b5184cd 100644
--- a/spec/models/alchemy/page_spec.rb
+++ b/spec/models/alchemy/page_spec.rb
@@ -1103,52 +1103,25 @@ module Alchemy
end
describe '#find_elements' do
- before do
- create(:alchemy_element, public: false, page: public_page)
- create(:alchemy_element, public: false, page: public_page)
- end
+ subject { page.find_elements(options) }
- context "with show_non_public argument TRUE" do
- it "should return all elements from empty options" do
- expect(public_page.find_elements({}, true).to_a).to eq(public_page.elements.to_a)
- end
-
- it "should only return the elements passed as options[:only]" do
- expect(public_page.find_elements({only: ['article']}, true).to_a).to eq(public_page.elements.named('article').to_a)
- end
-
- it "should not return the elements passed as options[:except]" do
- expect(public_page.find_elements({except: ['article']}, true).to_a).to eq(public_page.elements - public_page.elements.named('article').to_a)
- end
-
- it "should return elements offsetted" do
- expect(public_page.find_elements({offset: 2}, true).to_a).to eq(public_page.elements.offset(2))
- end
+ let(:page) { build(:alchemy_page) }
+ let(:options) { {} }
+ let(:finder) { instance_double(Alchemy::ElementsFinder) }
- it "should return elements limitted in count" do
- expect(public_page.find_elements({count: 1}, true).to_a).to eq(public_page.elements.limit(1))
- end
+ it 'passes self and all options to elements finder' do
+ expect(Alchemy::ElementsFinder).to receive(:new).with(options) { finder }
+ expect(finder).to receive(:elements).with(page: page)
+ subject
end
- context "with show_non_public argument FALSE" do
- it "should return all elements from empty arguments" do
- expect(public_page.find_elements.to_a).to eq(public_page.elements.published.to_a)
- end
-
- it "should only return the public elements passed as options[:only]" do
- expect(public_page.find_elements(only: ['article']).to_a).to eq(public_page.elements.published.named('article').to_a)
- end
-
- it "should return all public elements except the ones passed as options[:except]" do
- expect(public_page.find_elements(except: ['article']).to_a).to eq(public_page.elements.published.to_a - public_page.elements.published.named('article').to_a)
- end
-
- it "should return elements offsetted" do
- expect(public_page.find_elements({offset: 2}).to_a).to eq(public_page.elements.published.offset(2))
+ context 'with a custom finder given in options' do
+ let(:options) do
+ { finder: CustomNewsElementsFinder.new }
end
- it "should return elements limitted in count" do
- expect(public_page.find_elements({count: 1}).to_a).to eq(public_page.elements.published.limit(1))
+ it 'uses that to load elements to render' do
+ expect(subject.map(&:name)).to eq(['news'])
end
end
end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 8b4aebdcae..387d48f828 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -26,6 +26,7 @@
require_relative "support/hint_examples.rb"
require_relative "support/transformation_examples.rb"
require_relative "support/capybara_select2.rb"
+require_relative 'support/custom_news_elements_finder'
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
diff --git a/spec/support/custom_news_elements_finder.rb b/spec/support/custom_news_elements_finder.rb
new file mode 100644
index 0000000000..1c32d5f170
--- /dev/null
+++ b/spec/support/custom_news_elements_finder.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class CustomNewsElementsFinder
+ def elements(*)
+ [Alchemy::Element.new(name: 'news', id: 1001)]
+ end
+end