mholling-paged_scopes 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Matthew Hollingworth
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,7 @@
1
+ = paged_scopes
2
+
3
+ Description goes here.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 Matthew Hollingworth. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "paged_scopes"
8
+ gem.summary = <<-EOF
9
+ PagedScopes is an ActiveRecord pagination gem. It lets you easily paginate collection associations and
10
+ named scopes. It also paginates collections which already have :limit and :offset scopes in place. You
11
+ can also find the page containing a given object in a collection, and find the next and previous objects
12
+ for each object in the collection.
13
+ EOF
14
+ gem.email = "mdholling@gmail.com"
15
+ gem.homepage = "http://github.com/mholling/paged_scopes"
16
+ gem.authors = ["Matthew Hollingworth"]
17
+ gem.add_dependency 'activerecord', ">= 2.2.1"
18
+ gem.has_rdoc = false
19
+ end
20
+ rescue LoadError
21
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
22
+ end
23
+
24
+ require 'spec/rake/spectask'
25
+ Spec::Rake::SpecTask.new(:spec) do |spec|
26
+ spec.libs << 'lib' << 'spec'
27
+ spec.spec_files = FileList['spec/**/*_spec.rb']
28
+ end
29
+
30
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
31
+ spec.libs << 'lib' << 'spec'
32
+ spec.pattern = 'spec/**/*_spec.rb'
33
+ spec.rcov = true
34
+ end
35
+
36
+
37
+ task :default => :spec
38
+
39
+ require 'rake/rdoctask'
40
+ Rake::RDocTask.new do |rdoc|
41
+ if File.exist?('VERSION.yml')
42
+ config = YAML.load(File.read('VERSION.yml'))
43
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
44
+ else
45
+ version = ""
46
+ end
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "paged_scopes #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
53
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 0
4
+ :minor: 0
@@ -0,0 +1,45 @@
1
+ module PagedScopes
2
+ module Collection
3
+ module Attributes
4
+ attr_writer :per_page
5
+
6
+ def per_page
7
+ @per_page || case self
8
+ when ActiveRecord::NamedScope::Scope
9
+ @proxy_scope.per_page
10
+ when ActiveRecord::Associations::AssociationCollection
11
+ @reflection.klass.per_page
12
+ end
13
+ end
14
+
15
+ attr_writer :page_name
16
+
17
+ def page_name
18
+ @page_name || case self
19
+ when ActiveRecord::NamedScope::Scope
20
+ @proxy_scope.page_name
21
+ when ActiveRecord::Associations::AssociationCollection
22
+ @reflection.klass.page_name
23
+ else
24
+ "Page"
25
+ end
26
+ end
27
+ end
28
+
29
+ include Attributes
30
+
31
+ def pages
32
+ @pages ||= returning(Class.new) do |klass|
33
+ klass.send :include, Page
34
+ klass.proxy = self
35
+ klass.class_eval "alias :#{name.tableize} :page_scope"
36
+ klass.instance_eval "alias :find_by_#{name.underscore} :find_by_object"
37
+ klass.instance_eval "alias :find_by_#{name.underscore}! :find_by_object!"
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ ActiveRecord::Base.extend PagedScopes::Collection::Attributes
44
+ ActiveRecord::Associations::AssociationCollection.send :include, PagedScopes::Collection
45
+ ActiveRecord::NamedScope::Scope.send :include, PagedScopes::Collection
@@ -0,0 +1,36 @@
1
+ module PagedScopes
2
+ module Context
3
+ extend ActiveSupport::Memoizable
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ class << base
8
+ alias_method_chain :find, :context
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def find_with_context(*args)
14
+ returning find_without_context(*args) do |results|
15
+ found_scope, found_options = scope(:find), args.extract_options!
16
+ [ results ].flatten.each do |result|
17
+ result.instance_variable_set "@found_scope", found_scope
18
+ result.instance_variable_set "@found_options", found_options
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def previous
25
+ self.class.scoped(@found_scope).scoped(@found_options).before(self)
26
+ end
27
+
28
+ def next
29
+ self.class.scoped(@found_scope).scoped(@found_options).after(self)
30
+ end
31
+
32
+ memoize :previous, :next
33
+ end
34
+ end
35
+
36
+ ActiveRecord::Base.send :include, PagedScopes::Context
@@ -0,0 +1,22 @@
1
+ module PagedScopes
2
+ module Controller
3
+ def get_page_for(collection_name, options = {})
4
+ callback_method = "get_page_for_#{collection_name}"
5
+ define_method callback_method do
6
+ collection = instance_variable_get("@#{collection_name.to_s.pluralize}")
7
+ raise RuntimeError, "no @#{collection_name.to_s.pluralize} collection was set" unless collection
8
+ object = instance_variable_get("@#{collection_name.to_s.singularize}")
9
+ collection.per_page = options[:per_page] if options[:per_page]
10
+ collection.name = options[:name] if options[:name]
11
+ page = collection.pages.from_params(params) || (object && collection.pages.find_by_object(object)) || collection.pages.first
12
+ instance_variable_set("@#{collection.pages.name.underscore}", page)
13
+ end
14
+ protected callback_method
15
+ before_filter callback_method
16
+ end
17
+ end
18
+ end
19
+
20
+ if defined? ActionController::Base
21
+ ActionController::Base.extend PagedScopes::Controller
22
+ end
@@ -0,0 +1,89 @@
1
+ module PagedScopes
2
+ module Index
3
+ def index_of(object)
4
+ find_scope = scope(:find) || {}
5
+ primary_key_attribute = "#{table_name}.#{primary_key}"
6
+
7
+ order_attributes = find_scope[:order].to_s.split(',').map(&:strip)
8
+ order_operators = order_attributes.inject({}) do |hash, order_attribute|
9
+ operator = order_attribute.slice!(/\s+(desc|DESC)$/) ? ">" : "<"
10
+ order_attribute.slice!(/\s+(asc|ASC)$/)
11
+ hash.merge(order_attribute => operator)
12
+ end
13
+ unless order_attributes.include? primary_key_attribute
14
+ order_operators[primary_key_attribute] = "<"
15
+ order_attributes << primary_key_attribute
16
+ end
17
+
18
+ attribute_selects = returning([]) do |selects|
19
+ order_attributes.each_with_index do |order_attribute, n|
20
+ selects << "#{order_attribute} AS order_attribute_#{n}"
21
+ end
22
+ end.join(', ')
23
+
24
+ order_attribute_options = { :select => attribute_selects }
25
+ order_attribute_options.merge!(:offset => 0) if find_scope[:offset]
26
+ object_with_order_attributes = find(object.id, order_attribute_options)
27
+
28
+ object_order_attributes = {}
29
+ order_attributes.each_with_index do |order_attribute, n|
30
+ object_order_attributes[order_attribute] = object_with_order_attributes.send("order_attribute_#{n}")
31
+ end
32
+
33
+ order_conditions = order_attributes.reverse.inject([ "", {}, 0 ]) do |args, order_attribute|
34
+ string, hash, n = args
35
+ symbol = "s#{n}".to_sym
36
+ string = string.blank? ?
37
+ "#{order_attribute} #{order_operators[order_attribute]} #{symbol.inspect}" :
38
+ "#{order_attribute} #{order_operators[order_attribute]} #{symbol.inspect} OR (#{order_attribute} = #{symbol.inspect} AND (#{string}))"
39
+ hash.merge!(symbol => object_order_attributes[order_attribute])
40
+ [ string, hash, n + 1 ]
41
+ end
42
+ order_conditions.pop
43
+
44
+ # order_conditions = order_attributes.reverse.inject([]) do |conditions, order_attribute|
45
+ # if conditions.empty?
46
+ # conditions = [ "#{order_attribute} #{order_operators[order_attribute]} ?", object_order_attributes[order_attribute] ]
47
+ # else
48
+ # conditions[0] = "#{order_attribute} #{order_operators[order_attribute]} ? OR (#{order_attribute} = ? AND (#{conditions[0]}))"
49
+ # conditions.insert 1, object_order_attributes[order_attribute]
50
+ # conditions.insert 1, object_order_attributes[order_attribute]
51
+ # end
52
+ # end
53
+
54
+ count_options = { :conditions => order_conditions, :distinct => true }
55
+ count_options.merge!(:offset => 0) if find_scope[:offset]
56
+ before_count = count(primary_key_attribute, count_options)
57
+ if find_scope[:limit]
58
+ before_count -= find_scope[:offset] if find_scope[:offset]
59
+ raise ActiveRecord::RecordNotFound, "Couldn't find #{name} with ID=#{object.id}" if before_count < 0 || before_count >= find_scope[:limit]
60
+ end
61
+
62
+ before_count
63
+ end
64
+
65
+ def after(object)
66
+ after_index = index_of(object) + 1
67
+ find_scope = scope(:find) || {}
68
+ if find_scope[:limit]
69
+ offset = (find_scope[:offset] || 0).to_i
70
+ after_index >= find_scope[:limit] ? nil : first(:offset => after_index + offset)
71
+ else
72
+ first(:offset => after_index)
73
+ end
74
+ end
75
+
76
+ def before(object)
77
+ before_index = index_of(object) - 1
78
+ find_scope = scope(:find) || {}
79
+ if find_scope[:limit]
80
+ offset = find_scope[:offset].to_i
81
+ before_index < 0 ? nil : first(:offset => before_index + offset)
82
+ else
83
+ before_index < 0 ? nil : first(:offset => before_index)
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ ActiveRecord::Base.send :extend, PagedScopes::Index
@@ -0,0 +1,188 @@
1
+ module PagedScopes
2
+ class PageNotFound < ActiveRecord::RecordNotFound
3
+ attr_reader :substitute
4
+ def initialize(message, substitute = nil)
5
+ super(message)
6
+ @substitute = substitute
7
+ end
8
+ end
9
+
10
+ module Page
11
+ extend ActiveSupport::Memoizable
12
+
13
+ def self.included(base)
14
+ base.extend ClassMethods
15
+ end
16
+
17
+ module ClassMethods
18
+ extend ActiveSupport::Memoizable
19
+ include Enumerable
20
+
21
+ attr_accessor :proxy
22
+
23
+ def per_page
24
+ proxy.per_page || raise(RuntimeError, "please specify per_page in your collection")
25
+ end
26
+
27
+ def name
28
+ proxy.page_name
29
+ end
30
+
31
+ def find(id)
32
+ new(id.to_i)
33
+ end
34
+
35
+ memoize :find
36
+
37
+ def find_by_object!(object)
38
+ raise PageNotFound, "#{object.inspect} is not an ActiveRecord instance" unless object.is_a?(ActiveRecord::Base)
39
+ find(1 + proxy.index_of(object) / per_page)
40
+ rescue ActiveRecord::RecordNotFound
41
+ raise PageNotFound, "#{object.inspect} not found in scope"
42
+ end
43
+
44
+ def find_by_object(object)
45
+ find_by_object!(object)
46
+ rescue PageNotFound
47
+ nil
48
+ end
49
+
50
+ def from_params!(params)
51
+ find(params[name.underscore.foreign_key.to_sym])
52
+ end
53
+
54
+ def from_params(params)
55
+ from_params!(params)
56
+ rescue PageNotFound
57
+ nil
58
+ end
59
+
60
+ def count
61
+ collection_count = if proxy_scoped?(:limit)
62
+ count_options = { :distinct => true, :limit => 1 }
63
+ count_options.merge!(:offset => 0) if proxy_scoped?(:offset)
64
+ proxy_count = proxy.count("#{proxy.table_name}.#{proxy.primary_key}", count_options)
65
+ proxy_count -= proxy_options[:offset] if proxy_scoped?(:offset)
66
+ [ proxy_count, proxy_options[:limit] ].min
67
+ else
68
+ proxy.count("#{proxy.table_name}.#{proxy.primary_key}", :distinct => true)
69
+ end
70
+ (collection_count - 1)/per_page + 1
71
+ end
72
+
73
+ memoize :count
74
+
75
+ def first
76
+ find(1)
77
+ end
78
+
79
+ def last
80
+ find(count)
81
+ end
82
+
83
+ def each(&block)
84
+ (1..count).each { |number| yield find(number) }
85
+ end
86
+
87
+ def all
88
+ collect { |page| page }
89
+ end
90
+
91
+ def closest_to(number)
92
+ find([ [ 1, number ].max, count ].min)
93
+ end
94
+
95
+ private
96
+
97
+ def proxy_options
98
+ proxy.send(:scope, :find) || {}
99
+ end
100
+
101
+ def proxy_scoped?(key = nil)
102
+ proxy.send(:scoped?, :find, key)
103
+ end
104
+ end
105
+
106
+ attr_reader :number, :paginator
107
+ delegate :per_page, :proxy, :proxy_options, :proxy_scoped?, :to => "self.class"
108
+ private :proxy, :proxy_options, :proxy_scoped?
109
+
110
+ def initialize(number)
111
+ unless number > 0 && number <= self.class.count
112
+ raise PageNotFound.new("couldn't find page number #{number}", self.class.closest_to(number))
113
+ end
114
+ @number = number
115
+ @paginator = PagedScopes::Paginator.new(self)
116
+ end
117
+
118
+ def page_count
119
+ self.class.count
120
+ end
121
+
122
+ def first?
123
+ number == 1
124
+ end
125
+
126
+ def last?
127
+ number == page_count
128
+ end
129
+
130
+ def <=>(other)
131
+ number <=> other.number
132
+ end
133
+
134
+ def ==(other)
135
+ number == other.number
136
+ end
137
+
138
+ def full?
139
+ !last? || page_scope.all.length == per_page
140
+ end
141
+
142
+ def to_param
143
+ number.to_s
144
+ end
145
+
146
+ def id
147
+ number
148
+ end
149
+
150
+ def previous
151
+ self.class.find(number - 1) unless first?
152
+ end
153
+
154
+ def next
155
+ self.class.find(number + 1) unless last?
156
+ end
157
+
158
+ def offset(n)
159
+ self.class.find(number + n)
160
+ rescue PageNotFound
161
+ nil
162
+ end
163
+
164
+ def inspect
165
+ "#<#{self.class.name}, for: #{proxy.name}, number: #{number}>"
166
+ end
167
+
168
+ def page_scope
169
+ if proxy_scoped?(:limit)
170
+ subquery_sql = proxy.name.constantize.send(:construct_finder_sql, proxy_options.merge(:select => "#{proxy.table_name}.#{proxy.primary_key}"))
171
+ paged_conditions = "#{proxy.table_name}.#{proxy.primary_key} IN (#{subquery_sql})"
172
+ paged_options = { :conditions => paged_conditions, :limit => per_page, :offset => (number - 1) * per_page }
173
+ proxy.name.constantize.scoped(proxy_options.except(:limit, :offset)).scoped(paged_options)
174
+ # can't exclude :conditions since it won't work for has_many :through associations (multiple identical records returned)
175
+ else
176
+ proxy.scoped(:limit => per_page, :offset => (number - 1) * per_page)
177
+ end
178
+ end
179
+
180
+ private
181
+
182
+ def subquery_sql
183
+ proxy.name.constantize.send(:construct_finder_sql, proxy_options.merge(:select => "#{proxy.table_name}.#{proxy.primary_key}"))
184
+ end
185
+
186
+ memoize :subquery_sql
187
+ end
188
+ end
@@ -0,0 +1,47 @@
1
+ module PagedScopes
2
+ class Paginator
3
+ attr_reader :page
4
+
5
+ def initialize(page)
6
+ @page = page
7
+ end
8
+
9
+ def set_path(&block)
10
+ @path = block
11
+ end
12
+
13
+ def path
14
+ @path || raise(ArgumentError, "No path proc supplied.")
15
+ end
16
+
17
+ def previous
18
+ path.call(@page.previous) unless @page.first?
19
+ end
20
+
21
+ def next
22
+ path.call(@page.next) unless @page.last?
23
+ end
24
+
25
+ def window(options)
26
+ size = options[:size]
27
+ extras = [ options[:extras] ].flatten.compact
28
+ raise ArgumentError, "No window block supplied." unless block_given?
29
+ return if @page.page_count < 2
30
+ if @page.number - size > 1
31
+ yield :first, @path.call(@page.class.first) if extras.include? :first
32
+ if extras.include?(:previous) && offset_page = @page.offset(-2 * size - 1)
33
+ yield :previous, @path.call(offset_page)
34
+ end
35
+ end
36
+ (-size..size).map { |offset| @page.offset(offset) }.compact.each do |page|
37
+ yield page, @path.call(page)
38
+ end
39
+ if @page.number + size < @page.page_count
40
+ if extras.include?(:next) && offset_page = @page.offset(2 * size + 1)
41
+ yield :next, @path.call(offset_page)
42
+ end
43
+ yield :last, @path.call(@page.class.last) if extras.include? :last
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+ module PagedScopes
2
+ module Resources
3
+ def resources_with_paged(*entities, &block)
4
+ options = entities.extract_options!
5
+ if page_options = options.delete(:paged)
6
+ resources_without_paged(*(entities.dup << options), &block)
7
+ page_options = {} unless page_options.is_a? Hash
8
+ page_name = page_options.delete(:name)
9
+ page_options.slice!(:as, :name)
10
+ page_options.merge!(:only => :none)
11
+ preserved_options = ActionController::Resources::INHERITABLE_OPTIONS + [ :name_prefix, :path_prefix ]
12
+ with_options(options.slice(*preserved_options)) do |map|
13
+ map.resources_without_paged(page_name || :pages, page_options) do |page|
14
+ page.resources(*(entities.dup << { :only => :index }))
15
+ end
16
+ end
17
+ else
18
+ resources_without_paged(*(entities << options), &block)
19
+ end
20
+ end
21
+
22
+ def self.included(base)
23
+ base.class_eval do
24
+ alias_method_chain :resources, :paged
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ if defined? ActionController::Resources
31
+ ActionController::Resources.send :include, PagedScopes::Resources
32
+ end
@@ -0,0 +1,9 @@
1
+ ActiveRecord::Base
2
+
3
+ require 'paged_scopes/index'
4
+ require 'paged_scopes/context'
5
+ require 'paged_scopes/collection'
6
+ require 'paged_scopes/pages'
7
+ require 'paged_scopes/controller'
8
+ require 'paged_scopes/paginator'
9
+ require 'paged_scopes/resources'
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'paged_scopes'
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Collection" do
4
+ describe "(ActiveRecord::Base class)" do
5
+ it "should have a per_page setter and getter" do
6
+ Article.per_page = 5
7
+ Article.per_page.should == 5
8
+ end
9
+
10
+ it "should have a default page name of 'Page'" do
11
+ Article.page_name.should == "Page"
12
+ end
13
+
14
+ it "should have a page_name setter and getter" do
15
+ Article.page_name = "Group"
16
+ Article.page_name.should == "Group"
17
+ end
18
+
19
+ it "should not have a #pages method" do
20
+ Article.respond_to?(:pages).should be_false
21
+ end
22
+ end
23
+
24
+ [ [ "association", "User.first.articles" ],
25
+ [ "named scope", "Article.scoped(:conditions => 'title IS NULL')" ] ].each do |collection_type, collection|
26
+ describe "(#{collection_type})" do
27
+ before(:each) do
28
+ @collection = eval(collection)
29
+ Article.stub!(:per_page).and_return(10)
30
+ Article.page_name = "Page"
31
+ end
32
+
33
+ it "should have a default per_page of the ActiveRecord::Base class" do
34
+ @collection.per_page.should == 10
35
+ end
36
+
37
+ it "should have a per_page setter and getter" do
38
+ @collection.per_page = 20
39
+ @collection.per_page.should == 20
40
+ end
41
+
42
+ it "should not overwrite the ActiveRecord::Base per_page value when per_page is set" do
43
+ @collection.per_page = 20
44
+ Article.per_page.should == 10
45
+ end
46
+
47
+ it "should have a default page_name of the ActiveRecord::Base class" do
48
+ @collection.page_name.should == "Page"
49
+ end
50
+
51
+ it "should have a page_name setter and getter" do
52
+ @collection.page_name = "Group"
53
+ @collection.page_name.should == "Group"
54
+ end
55
+
56
+ it "should not overwrite the ActiveRecord::Base page_name value when page_name is set" do
57
+ @collection.page_name = "Group"
58
+ Article.page_name.should == "Page"
59
+ end
60
+
61
+ it "should have a #pages method" do
62
+ @collection.respond_to?(:pages).should be_true
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Context" do
4
+ in_contexts do
5
+ it "should know the object after an object in the collection" do
6
+ articles = @articles.all
7
+ until articles.empty? do
8
+ articles.shift.next.should == articles.first
9
+ end
10
+ end
11
+
12
+ it "should know the object before an object in the collection" do
13
+ articles = @articles.all
14
+ until articles.empty? do
15
+ articles.pop.previous.should == articles.last
16
+ end
17
+ end
18
+ end
19
+ end