mholling-paged_scopes 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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