-
-
Notifications
You must be signed in to change notification settings - Fork 317
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This class abstracts elements finding logic that we currently have in `render_elements` helper and `Page#find_elements` method. But with pure active record finders and a clean testable interface. It now returns an `ActiveRecord::Relation` that later materializes in the view. The sort_by option and show hidden elements options have been removed. As this uses a very opinionated way of loading and sorting elements that only few people use - if at all. Please implement your own elements finder in your project instead and pass this as options[:finder] to those methods.
- Loading branch information
Showing
3 changed files
with
330 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
# 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>|String] :only | ||
# A list of element names to load only. | ||
# @option options [Array<String>|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]) | ||
.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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
# 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 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 |