Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduces an Elements finder class #1544

Merged
merged 7 commits into from
Feb 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ Layout/MultilineMethodCallBraceLayout:
Layout/MultilineMethodCallIndentation:
Enabled: false

Layout/MultilineOperationIndentation:
EnforcedStyle: indented

Layout/SpaceBeforeBlockBraces:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#stylespacebeforeblockbraces
Expand Down
133 changes: 48 additions & 85 deletions app/helpers/alchemy/elements_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module ElementsHelper
include Alchemy::UrlHelper
include Alchemy::ElementsBlockHelper

# Renders all elements from current page
# Renders elements from given page
#
# == Examples:
#
Expand Down Expand Up @@ -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:
#
# <div class="news-archive">
# <%= render_elements finder: MyCustomNewsArchive.new %>
# </div>
#
# @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>|String] :only
# A list of element names only to be rendered.
# @option options [Array<String>|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.
Expand Down Expand Up @@ -231,73 +247,20 @@ 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 : ''
end

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
2 changes: 1 addition & 1 deletion app/models/alchemy/element.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
32 changes: 32 additions & 0 deletions app/models/alchemy/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,38 @@ def new_name_for_copy(custom_name, source_name)
# Instance methods
#

# Returns elements from page.
#
# @option options [Array<String>|String] :only
# Returns only elements with given names
# @option options [Array<String>|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
Expand Down
37 changes: 0 additions & 37 deletions app/models/alchemy/page/page_elements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
111 changes: 111 additions & 0 deletions lib/alchemy/elements_finder.rb
Original file line number Diff line number Diff line change
@@ -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>|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])
.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
Loading