refactor the paginate liquid tag (+ add specs)

did committed Feb 05, 2015
commit 836cbdf4afd917b57ad2c4621dd275d28426a5d0
Showing 13 changed files with 366 additions and 90 deletions
locomotive/steam.rb b/lib/locomotive/steam.rb +1 -0
@@ @@ -3,6 +3,7 @@ require 'locomotive/common'
require_relative 'steam/core_ext'
require_relative 'steam/exceptions'
require_relative 'steam/configuration'
+ require_relative 'steam/monkey_patches'
require_relative 'steam/liquid'
require_relative 'steam/repositories'
locomotive/steam/core_ext/hash.rb b/lib/locomotive/steam/core_ext/hash.rb +10 -6
@@ @@ -3,8 +3,12 @@
module HashConverter
class << self
- def to_underscore hash
- convert hash, :underscore
+ def to_underscore(hash)
+ convert(hash, :underscore)
+ end
+
+ def to_string(hash)
+ convert(hash, :to_s)
end
# FIXME: not sure it will be ever needed
@@ @@ -12,16 +16,16 @@ module HashConverter
# convert hash, :camelize, :lower
# end
- def convert obj, *method
+ def convert(obj, *method)
case obj
when Hash
- obj.inject({}) do |h,(k,v)|
- v = convert v, *method
+ obj.inject({}) do |h, (k,v)|
+ v = convert(v, *method)
h[k.send(*method)] = v
h
end
when Array
- obj.map {|m| convert m, *method }
+ obj.map { |m| convert(m, *method) }
else
obj
end
locomotive/steam/liquid/tags/consume.rb b/lib/locomotive/steam/liquid/tags/consume.rb +1 -1
@@ @@ -15,7 +15,7 @@ module Locomotive
#
class Consume < ::Liquid::Block
- Syntax = /(#{::Liquid::VariableSignature}+)\s*from\s*(#{::Liquid::QuotedString}|#{::Liquid::VariableSignature}+)(.*)?/
+ Syntax = /(#{::Liquid::VariableSignature}+)\s*from\s*(#{::Liquid::QuotedString}|#{::Liquid::VariableSignature}+)(.*)?/o
def initialize(tag_name, markup, options)
if markup =~ Syntax
locomotive/steam/liquid/tags/editable/base.rb b/lib/locomotive/steam/liquid/tags/editable/base.rb +1 -1
@@ @@ -5,7 +5,7 @@ module Locomotive
module Editable
class Base < ::Liquid::Block
- Syntax = /(#{::Liquid::QuotedFragment})(\s*,\s*#{::Liquid::Expression}+)?/
+ Syntax = /(#{::Liquid::QuotedFragment})(\s*,\s*#{::Liquid::Expression}+)?/o
attr_accessor :slug
locomotive/steam/liquid/tags/fetch_page.rb b/lib/locomotive/steam/liquid/tags/fetch_page.rb +1 -1
@@ @@ -12,7 +12,7 @@ module Locomotive
#
class FetchPage < ::Liquid::Tag
- Syntax = /(#{::Liquid::VariableSignature}+)\s+as\s+(#{::Liquid::VariableSignature}+)/
+ Syntax = /(#{::Liquid::VariableSignature}+)\s+as\s+(#{::Liquid::VariableSignature}+)/o
def initialize(tag_name, markup, options)
if markup =~ Syntax
locomotive/steam/liquid/tags/paginate.rb b/lib/locomotive/steam/liquid/tags/paginate.rb +80 -52
@@ @@ -3,7 +3,7 @@ module Locomotive
module Liquid
module Tags
- # Paginate a collection
+ # Paginate a collection (array or from a DB).
#
# Usage:
#
@@ @@ -13,20 +13,18 @@ module Locomotive
# {% endfor %}
# {% endpaginate %}
#
-
class Paginate < ::Liquid::Block
- Syntax = /(#{::Liquid::Expression}+)\s+by\s+([0-9]+)/
+ Syntax = /(#{::Liquid::QuotedFragment}+)\s+by\s+([0-9]+)/o
- def initialize(tag_name, markup, tokens, context)
+ def initialize(tag_name, markup, options)
if markup =~ Syntax
- @collection_name = $1
- @per_page = $2.to_i
- @options = { }
- markup.scan(::Liquid::TagAttributes) { |key, value| @options[key.to_sym] = value.gsub(/^'/, '').gsub(/'$/, '') }
- @window_size = @options[:window_size] ? @options[:window_size].to_i : 3
+ @collection_name = $1
+ @per_page = $2.to_i
+ @paginate_options = {}
+ markup.scan(::Liquid::TagAttributes) { |key, value| @paginate_options[key.to_sym] = value.gsub(/^'/, '').gsub(/'$/, '') }
else
- raise ::Liquid::SyntaxError.new("Syntax Error in 'paginate' - Valid syntax: paginate <collection> by <number>")
+ raise ::Liquid::SyntaxError.new('Valid syntax: paginate <collection> by <number>')
end
super
@@ @@ -34,56 +32,78 @@ module Locomotive
def render(context)
context.stack do
- collection = context[@collection_name]
+ pagination = context['paginate'] = paginate_collection(context)
+
+ path = sanitize_path(context['fullpath'])
- raise ::Liquid::ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection.nil?
+ build_next_previous_links(pagination, path)
- if collection.is_a? Array
- pagination = Kaminari.paginate_array(collection).page(context['current_page']).per(@per_page).to_liquid.stringify_keys
- else
- pagination = collection.send(:paginate, {
- page: context['current_page'],
- per_page: @per_page
- }).to_liquid.stringify_keys
+ if pagination['total_pages'] > 1
+ build_parts(pagination, path)
end
- page_count, current_page = pagination['total_pages'], pagination['current_page']
- path = sanitize_path(context['fullpath'])
+ super
+ end
+ end
- pagination['previous'] = link(I18n.t('pagination.previous'), current_page - 1, path) if pagination['previous_page']
- pagination['next'] = link(I18n.t('pagination.next'), current_page + 1, path) if pagination['next_page']
- pagination['parts'] = []
-
- hellip_break = false
-
- if page_count > 1
- 1.upto(page_count) do |page|
- if current_page == page
- pagination['parts'] << no_link(page)
- elsif page == 1
- pagination['parts'] << link(page, page, path)
- elsif page == page_count - 1
- pagination['parts'] << link(page, page, path)
- elsif page <= current_page - window_size or page >= current_page + window_size
- next if hellip_break
- pagination['parts'] << no_link('&hellip;')
- hellip_break = true
- next
- else
- pagination['parts'] << link(page, page, path)
- end
-
- hellip_break = false
- end
- end
+ private
- context['paginate'] = pagination
+ # Paginate the collection and returns a pagination
+ # object storing all the information about the paginated
+ # collection.
+ #
+ def paginate_collection(context)
+ collection = context[@collection_name]
+ current_page = context['current_page']
- render_all(@nodelist, context)
+ raise ::Liquid::ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection.nil?
+
+ pagination = (if collection.is_a?(Array)
+ Kaminari.paginate_array(collection).page(current_page).per(@per_page)
+ else
+ collection.send(:paginate, page: current_page, per_page: @per_page)
+ end)
+
+ # make sure the pagination object is a hash with strings as keys (and not symbol)
+ HashConverter.to_string(pagination.to_liquid).tap do |_pagination|
+ _pagination['parts'] = []
end
end
- private
+ def build_next_previous_links(pagination, path)
+ current_page = pagination['current_page']
+
+ if pagination['previous_page']
+ pagination['previous']= link(I18n.t('pagination.previous'), current_page - 1, path)
+ end
+
+ if pagination['next_page']
+ pagination['next'] = link(I18n.t('pagination.next'), current_page + 1, path)
+ end
+ end
+
+ def build_parts(pagination, path)
+ hellip_break = false
+
+ 1.upto(pagination['total_pages']) do |page|
+ hellip_break = _build_parts(pagination, path, page, hellip_break)
+ end
+ end
+
+ def _build_parts(pagination, path, page, hellip_break)
+ if pagination['current_page'] == page
+ pagination['parts'] << no_link(page)
+ elsif is_page_a_bound?(pagination, page)
+ pagination['parts'] << link(page, page, path)
+ elsif is_page_inside_window?(pagination, page)
+ pagination['parts'] << no_link('&hellip;') unless hellip_break
+ return true
+ else
+ pagination['parts'] << link(page, page, path)
+ end
+
+ false
+ end
def sanitize_path(path)
_path = path.gsub(/page=[0-9]+&?/, '').gsub(/_pjax=true&?/, '')
@@ @@ -91,8 +111,16 @@ module Locomotive
_path
end
+ def is_page_inside_window?(pagination, page)
+ page <= pagination['current_page'] - window_size or page >= pagination['current_page'] + window_size
+ end
+
+ def is_page_a_bound?(pagination, page)
+ page == 1 || page == pagination['total_pages']
+ end
+
def window_size
- @window_size
+ @window_size ||= @paginate_options[:window_size] ? @paginate_options[:window_size].to_i : 3
end
def no_link(title)
@@ @@ -105,7 +133,7 @@ module Locomotive
end
end
- ::Liquid::Template.register_tag('paginate', Paginate)
+ ::Liquid::Template.register_tag('paginate'.freeze, Paginate)
end
end
locomotive/steam/liquid/tags/session_assign.rb b/lib/locomotive/steam/liquid/tags/session_assign.rb +1 -1
@@ @@ -12,7 +12,7 @@ module Locomotive
# {{ session.foo }}
#
class SessionAssign < ::Liquid::Tag
- Syntax = /(#{::Liquid::VariableSignature}+)\s*=\s*(#{::Liquid::QuotedFragment}+)/
+ Syntax = /(#{::Liquid::VariableSignature}+)\s*=\s*(#{::Liquid::QuotedFragment}+)/o
def initialize(tag_name, markup, options)
if markup =~ Syntax
locomotive/steam/liquid/tags/with_scope.rb b/lib/locomotive/steam/liquid/tags/with_scope.rb +1 -1
@@ @@ -18,7 +18,7 @@ module Locomotive
OPERATORS = %w(all exists gt gte in lt lte ne nin size near within)
- SYMBOL_OPERATORS_REGEXP = /(\w+\.(#{OPERATORS.join('|')})){1}\s*\:/
+ SYMBOL_OPERATORS_REGEXP = /(\w+\.(#{OPERATORS.join('|')})){1}\s*\:/o
# register the tag
tag_name :with_scope
locomotive/steam/monkey_patches.rb b/lib/locomotive/steam/monkey_patches.rb +1 -0
@@ @@ -1,3 +1,4 @@
# require_relative 'monkey_patches/httparty.rb'
# require_relative 'monkey_patches/dragonfly.rb'
require_relative 'monkey_patches/haml.rb'
+ require_relative 'monkey_patches/kaminari.rb'
locomotive/steam/monkey_patches/haml.rb b/lib/locomotive/steam/monkey_patches/haml.rb +10 -10
@@ @@ -1,17 +1,17 @@
- require 'haml'
+ # require 'haml'
- module Haml::Filters
+ # module Haml::Filters
- remove_filter("Markdown") #remove the existing Markdown filter
+ # remove_filter("Markdown") #remove the existing Markdown filter
- module Markdown # the contents of this are as before, but without the lazy_require call
+ # module Markdown # the contents of this are as before, but without the lazy_require call
- include Haml::Filters::Base
+ # include Haml::Filters::Base
- def render text
- Locomotive::Steam::Markdown.new.render text
- end
+ # def render text
+ # Locomotive::Steam::Markdown.new.render text
+ # end
- end
+ # end
- end
+ # end
locomotive/steam/monkey_patches/kaminari.rb b/lib/locomotive/steam/monkey_patches/kaminari.rb +45 -0
@@ @@ -0,0 +1,45 @@
+ # Note: avoid this issue https://github.com/amatsuda/kaminari/issues/518
+ require 'kaminari/config'
+ require 'kaminari/models/configuration_methods'
+ require 'kaminari/models/page_scope_methods'
+ require 'kaminari/models/array_extension'
+
+ module Kaminari
+
+ module PageScopeMethods
+
+ def to_liquid(options = {})
+ {
+ collection: to_a,
+ current_page: current_page,
+ previous_page: first_page? ? nil : current_page - 1,
+ total_entries: total_count,
+ per_page: limit_value
+ }.tap do |hash|
+ # note: very important to avoid extra and useless mongodb requests
+ hash[:total_pages] = (hash[:total_entries].to_f / limit_value).ceil
+ hash[:next_page] = current_page >= hash[:total_pages] ? nil : current_page + 1
+ end
+ end
+
+ end
+
+ # FIXME: it does not seem to be called
+ # class PaginatableArray < Array
+
+ # def to_liquid(options = {})
+ # puts "PaginatableArray.instance.to_liquid"
+ # {
+ # collection: to_a,
+ # current_page: current_page,
+ # previous_page: first_page? ? nil : current_page - 1,
+ # next_page: last_page? ? nil : current_page + 1,
+ # total_entries: total_count,
+ # total_pages: num_pages,
+ # per_page: limit_value
+ # }
+ # end
+
+ # end
+
+ end
spec/unit/liquid/tags/paginate_spec.rb +214 -0
@@ @@ -0,0 +1,214 @@
+ require 'spec_helper'
+
+ describe Locomotive::Steam::Liquid::Tags::Paginate do
+
+ let(:source) { <<-EOF
+ {% paginate projects by 5 %}
+ {% for project in paginate.collection %}!{{ project }}{% endfor %}
+ {{ paginate.next.url }}
+ {% endpaginate %}'
+ EOF
+ }
+
+ let(:projects) { ['RoR', 'MongoDB', 'Liquid', 'ReactJS', 'DCI', 'Bootstrap'] }
+ let(:page) { 1 }
+ let(:assigns) { { 'projects' => projects, 'current_page' => page, 'fullpath' => '/' } }
+ let(:context) { ::Liquid::Context.new(assigns, {}, {}) }
+ let(:output) { render_template(source, context) }
+
+ describe 'parsing' do
+
+ subject { parse_template(source) }
+
+ describe 'wrong syntax' do
+
+ let(:source) { '{% paginate projects %}{% endpaginate %}' }
+ it { expect { subject }.to raise_error(::Liquid::SyntaxError, 'Liquid syntax error: Valid syntax: paginate <collection> by <number>') }
+
+ end
+
+ describe 'with options for the pagination' do
+
+ let(:source) { '{% paginate projects by 2, window_size: 4 %}{% endpaginate %}' }
+ let(:block) { subject.root.nodelist.first }
+ it { expect(block.send(:window_size)).to eq 4 }
+
+ end
+
+ end
+
+ describe 'rendering' do
+
+ describe 'nil array' do
+
+ let(:projects) { nil }
+
+ subject { output }
+
+ it { expect { subject }.to raise_error(::Liquid::ArgumentError, "Liquid error: Cannot paginate array 'projects'. Not found.") }
+
+ end
+
+ describe 'simple array' do
+
+ subject { output }
+
+ it { is_expected.to include '!RoR!MongoDB!Liquid!ReactJS!DCI' }
+ it { is_expected.not_to include '!Bootstrap' }
+
+ describe 'second page of results: display the last item' do
+
+ let(:page) { 2 }
+ it { is_expected.to include '!Bootstrap' }
+ it { is_expected.not_to include '!RoR!MongoDB!Liquid!ReactJS!DCI' }
+
+ end
+
+ end
+
+ describe 'array from a db collection' do
+
+ let(:projects) { KindaDBCollection.new(['RoR', 'MongoDB', 'Liquid', 'ReactJS', 'DCI', 'Bootstrap']) }
+
+ subject { output }
+
+ it { is_expected.to include '!RoR!MongoDB!Liquid!ReactJS!DCI' }
+ it { is_expected.not_to include '!Bootstrap' }
+
+ end
+
+ describe 'a very big collection' do
+
+ let(:projects) { (1..100).to_a }
+ let(:page) { 20 }
+ let(:source) { '{% paginate projects by 2, window_size: 10 %}{% assign _pagination = paginate %}{% endpaginate %}' }
+
+ before { output }
+ subject { context['_pagination']['parts'] }
+
+ it { expect(subject.first['title']).to eq 1 }
+ it { expect(subject[1]['title']).to eq '&hellip;' }
+ it { expect(subject[2]['title']).to eq 11 }
+ it { expect(subject[21]['title']).to eq '&hellip;' }
+ it { expect(subject.last['title']).to eq 50 }
+
+ end
+
+ describe ''
+
+ end
+
+ class KindaDBCollection < Struct.new(:collection)
+
+ def paginate(options = {})
+ total_pages = (collection.size.to_f / options[:per_page].to_f).to_f.ceil + 1
+ offset = (options[:page] - 1) * options[:per_page]
+
+ {
+ collection: collection[offset..(offset + options[:per_page]) - 1],
+ current_page: options[:page],
+ previous_page: options[:page] == 1 ? 1 : options[:page] - 1,
+ next_page: options[:page] == total_pages ? total_pages : options[:page] + 1,
+ total_entries: collection.size,
+ total_pages: total_pages,
+ per_page: options[:per_page]
+ }
+ end
+
+ def each(&block)
+ collection.each(&block)
+ end
+
+ def method_missing(method, *args)
+ collection.send(method, *args)
+ end
+
+ def to_liquid
+ self
+ end
+ end
+
+
+ # it 'keeps the original GET parameters' do
+ # context = liquid_context(fullpath: '/products?foo=1&bar=1&baz=1')
+ # template = Liquid::Template.parse(default_template)
+ # text = template.render!(context)
+ # expect(text).to match /\/products\?foo=1&bar=1&baz=1&page=2/
+ # end
+
+ # it 'does not include twice the page parameter' do
+ # context = liquid_context(fullpath: '/products?page=1')
+ # template = Liquid::Template.parse(default_template)
+ # text = template.render!(context)
+ # expect(text).to match /\/products\?page=2/
+ # end
+
+ # # ___ helpers methods ___ #
+
+ # def liquid_context(options = {})
+ # ::Liquid::Context.new(
+ # {},
+ # {
+ # 'projects' => options.has_key?(:collection) ? options[:collection] : PaginatedCollection.new(['Ruby on Rails', 'jQuery', 'mongodb', 'Liquid', 'sqlite3']),
+ # 'current_page' => options[:page] || 1,
+ # 'path' => '/',
+ # 'fullpath' => options[:fullpath] || '/'
+ # }, {
+ # page: FactoryGirl.build(:page)
+ # }, true)
+ # end
+
+ # def default_template
+ # "{% paginate projects by 2 %}
+ # {% for project in paginate.collection %}
+ # !{{ project }}!
+ # {% endfor %}
+ # {{ paginate.next.url }}
+ # {% endpaginate %}"
+ # end
+
+ # def default_pagination_template(options='')
+ # "{% paginate projects by 2 #{options} %}
+ # {% for project in paginate.collection %}
+ # !{{ project }}!
+ # {% endfor %}
+ # {{ paginate.next.url }}
+ # {{ paginate | default_pagination }}
+ # {% endpaginate %}"
+ # end
+
+ # class PaginatedCollection
+
+ # def initialize(collection)
+ # @collection = collection || []
+ # end
+
+ # def paginate(options = {})
+ # total_pages = (@collection.size.to_f / options[:per_page].to_f).to_f.ceil + 1
+ # offset = (options[:page] - 1) * options[:per_page]
+
+ # {
+ # collection: @collection[offset..(offset + options[:per_page]) - 1],
+ # current_page: options[:page],
+ # previous_page: options[:page] == 1 ? 1 : options[:page] - 1,
+ # next_page: options[:page] == total_pages ? total_pages : options[:page] + 1,
+ # total_entries: @collection.size,
+ # total_pages: total_pages,
+ # per_page: options[:per_page]
+ # }
+ # end
+
+ # def each(&block)
+ # @collection.each(&block)
+ # end
+
+ # def method_missing(method, *args)
+ # @collection.send(method, *args)
+ # end
+
+ # def to_liquid
+ # self
+ # end
+ # end
+
+ end
spec/unit/liquid/tags/with_scope_spec.rb +0 -17
@@ @@ -63,21 +63,4 @@ describe Locomotive::Steam::Liquid::Tags::WithScope do
end
- # describe "advanced queries thanks to h4s" do
-
- # it 'decodes criteria with gt and lt' do
- # scope = Locomotive::Liquid::Tags::WithScope.new('with_scope', 'price.gt:42.0, price.lt:50', ["{% endwith_scope %}"], {})
- # options = decode_options(scope)
- # expect(options[:price.gt]).to eq(42.0)
- # expect(options[:price.lt]).to eq(50)
- # end
-
- # end
-
- # def decode_options(tag, assigns = {})
- # context = ::Liquid::Context.new(assigns)
- # arguments = tag.instance_variable_get(:@arguments)
- # tag.send(:decode, *arguments.interpolate(context))
- # end
-
end