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