paged_scopes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'yaml'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = "paged_scopes"
9
+ gem.summary = <<-EOF
10
+ PagedScopes is an ActiveRecord pagination gem. It lets you easily paginate collection associations and
11
+ named scopes. It also paginates collections which already have :limit and :offset scopes in place. You
12
+ can also find the page containing a given object in a collection, and find the next and previous objects
13
+ for each object in the collection.
14
+ EOF
15
+ gem.email = "mdholling@gmail.com"
16
+ gem.homepage = "http://github.com/mholling/paged_scopes"
17
+ gem.authors = ["Matthew Hollingworth"]
18
+ gem.add_dependency 'activerecord', ">= 2.2.1"
19
+ gem.has_rdoc = false
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
24
+ end
25
+
26
+ require 'spec/rake/spectask'
27
+ Spec::Rake::SpecTask.new(:spec) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.spec_files = FileList['spec/**/*_spec.rb']
30
+ end
31
+
32
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
33
+ spec.libs << 'lib' << 'spec'
34
+ spec.pattern = 'spec/**/*_spec.rb'
35
+ spec.rcov = true
36
+ end
37
+
38
+
39
+ task :default => :spec
40
+
41
+ require 'rake/rdoctask'
42
+ Rake::RDocTask.new do |rdoc|
43
+ if File.exist?('VERSION.yml')
44
+ config = YAML.load(File.read('VERSION.yml'))
45
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
46
+ else
47
+ version = ""
48
+ end
49
+
50
+ rdoc.rdoc_dir = 'rdoc'
51
+ rdoc.title = "paged_scopes #{version}"
52
+ rdoc.rdoc_files.include('README*')
53
+ rdoc.rdoc_files.include('lib/**/*.rb')
54
+ end
55
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 0
3
+ :major: 0
4
+ :minor: 1
@@ -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'
@@ -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
+ found_scope, found_options = (scope(:find) || {}).dup, args.dup.extract_options!.dup
15
+ returning find_without_context(*args) do |results|
16
+ [ results ].flatten.compact.each do |result|
17
+ result.instance_variable_set "@found_scope", found_scope.dup
18
+ result.instance_variable_set "@found_options", found_options.dup
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,54 @@
1
+ module PagedScopes
2
+ module Controller
3
+ module ClassMethods
4
+ def paginate(*args)
5
+ options = args.extract_options!
6
+ raise ArgumentError, "can't paginate multiple collections with one call" if args.many?
7
+ collection_name = args.first || default_collection_name
8
+ callback_method = "paginate_#{collection_name}"
9
+ define_method callback_method do
10
+ collection = instance_variable_get("@#{collection_name.to_s.pluralize}")
11
+ raise RuntimeError, "no @#{collection_name.to_s.pluralize} collection was set" unless collection
12
+ object = instance_variable_get("@#{collection_name.to_s.singularize}")
13
+ page = page_for(collection, object, options)
14
+ instance_variable_set("@#{collection.pages.name.underscore}", page)
15
+ end
16
+ protected callback_method
17
+ before_filter callback_method, options.except(:per_page, :name, :path)
18
+ end
19
+
20
+ private
21
+
22
+ def default_collection_name
23
+ collection_name = name
24
+ raise RuntimeError, "couldn't find controller name" unless collection_name.slice!(/Controller$/)
25
+ collection_name.demodulize.tableize
26
+ end
27
+ end
28
+
29
+ def self.included(base)
30
+ base.extend ClassMethods
31
+ base.rescue_responses.update('PagedScopes::PageNotFound' => :not_found)
32
+ end
33
+
34
+ private
35
+
36
+ def page_for(collection, *args, &block)
37
+ options = args.extract_options!
38
+ collection.per_page = options[:per_page] if options[:per_page]
39
+ collection.page_name = options[:name] if options[:name]
40
+ object = args.first
41
+ returning collection.pages.find_by_object(object) || collection.pages.from_params!(params) || collection.pages.first do |page|
42
+ if options[:path]
43
+ page.paginator.set_path { |pg| send(options[:path], pg) }
44
+ elsif block_given?
45
+ page.paginator.set_path(&block)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ if defined? ActionController::Base
53
+ ActionController::Base.send :include, PagedScopes::Controller
54
+ end
@@ -0,0 +1,62 @@
1
+ module PagedScopes
2
+ module Index
3
+ def index_of(object)
4
+ columns = scope(:find, :order).to_s.split(',').map(&:strip) << "#{table_name}.#{primary_key}"
5
+ operators = columns.map do |column|
6
+ column.slice!(/\s+(asc|ASC)$/)
7
+ column.slice!(/\s+(desc|DESC)$/) ? ">" : "<"
8
+ end
9
+
10
+ attributes = (1..columns.size).map { |n| "attribute_#{n}" }
11
+
12
+ selects = [ columns, attributes ].transpose.map do |column, attribute|
13
+ "#{column} AS #{attribute}"
14
+ end.unshift("#{table_name}.*").join(', ')
15
+
16
+ options = { :select => selects }
17
+ options.merge!(:offset => 0) if scope(:find, :limit) && scope(:find, :offset)
18
+ object_with_attributes = find(object.id, options)
19
+
20
+ values = attributes.map { |attribute| object_with_attributes.send(attribute) }
21
+
22
+ string, hash = "", {}
23
+ [ columns, operators, attributes, values ].transpose.reverse.each do |column, operator, attribute, value|
24
+ string = string.blank? ?
25
+ "#{column} #{operator} :#{attribute}" :
26
+ "#{column} #{operator} :#{attribute} OR (#{column} = :#{attribute} AND (#{string}))"
27
+ hash.merge!(attribute.to_sym => value)
28
+ end
29
+
30
+ options = { :conditions => [ string, hash ], :distinct => true }
31
+ options.merge!(:offset => 0) if scope(:find, :limit) && scope(:find, :offset)
32
+ before_count = count("#{table_name}.#{primary_key}", options)
33
+
34
+ if scope(:find, :limit)
35
+ before_count -= scope(:find, :offset) if scope(:find, :limit) && scope(:find, :offset)
36
+ raise ActiveRecord::RecordNotFound, "Couldn't find #{name} with ID=#{object.id}" if before_count < 0 || before_count >= scope(:find, :limit)
37
+ end
38
+
39
+ before_count
40
+ end
41
+
42
+ def after(object)
43
+ after_index = index_of(object) + 1
44
+ if limit = scope(:find, :limit)
45
+ after_index >= limit ? nil : first(:offset => after_index + (scope(:find, :offset) || 0))
46
+ else
47
+ first(:offset => after_index)
48
+ end
49
+ end
50
+
51
+ def before(object)
52
+ before_index = index_of(object) - 1
53
+ if scope(:find, :limit)
54
+ before_index < 0 ? nil : first(:offset => before_index + (scope(:find, :offset) || 0))
55
+ else
56
+ before_index < 0 ? nil : first(:offset => before_index)
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ ActiveRecord::Base.send :extend, PagedScopes::Index
@@ -0,0 +1,194 @@
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
+ number = params[name.underscore.foreign_key.to_sym]
52
+ number ? find(number) : nil
53
+ end
54
+
55
+ def count
56
+ collection_count = if proxy_scoped?(:limit)
57
+ count_options = { :distinct => true, :limit => 1 }
58
+ count_options.merge!(:offset => 0) if proxy_scoped?(:offset)
59
+ proxy_count = proxy.count("#{proxy.table_name}.#{proxy.primary_key}", count_options)
60
+ proxy_count -= proxy_options[:offset] if proxy_scoped?(:offset)
61
+ [ proxy_count, proxy_options[:limit] ].min
62
+ else
63
+ proxy.count("#{proxy.table_name}.#{proxy.primary_key}", :distinct => true)
64
+ end
65
+ [ (collection_count - 1)/per_page + 1, 1].max
66
+ end
67
+
68
+ memoize :count
69
+
70
+ def first
71
+ find(1)
72
+ end
73
+
74
+ def last
75
+ find(count)
76
+ end
77
+
78
+ def each(&block)
79
+ 1.upto(count) { |number| yield find(number) }
80
+ end
81
+
82
+ def all
83
+ collect { |page| page }
84
+ end
85
+
86
+ def closest_to(number)
87
+ closest_number = [ [ 1, number ].max, count ].min
88
+ closest_number > 0 ? find(closest_number) : nil ## TODO this is unneeded now!
89
+ end
90
+
91
+ def reload!
92
+ unmemoize_all
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 <= page_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 reload!
119
+ self.class.reload!
120
+ self.class.find(number)
121
+ unmemoize_all
122
+ end
123
+
124
+ def page_count
125
+ self.class.count
126
+ end
127
+
128
+ def first?
129
+ number == 1
130
+ end
131
+
132
+ def last?
133
+ number == page_count
134
+ end
135
+
136
+ def <=>(other)
137
+ number <=> other.number
138
+ end
139
+
140
+ def ==(other)
141
+ other.is_a?(self.class) && number == other.number
142
+ end
143
+
144
+ def full?
145
+ !last? || page_scope.all.length == per_page # TODO: could improve this to calculate mathematically
146
+ end
147
+
148
+ def to_param
149
+ number.to_s
150
+ end
151
+
152
+ def id
153
+ number
154
+ end
155
+
156
+ def previous
157
+ self.class.find(number - 1) unless first?
158
+ end
159
+
160
+ def next
161
+ self.class.find(number + 1) unless last?
162
+ end
163
+
164
+ def offset(n)
165
+ self.class.find(number + n)
166
+ rescue PageNotFound
167
+ nil
168
+ end
169
+
170
+ def inspect
171
+ "#<#{self.class.name}, for: #{proxy.name}, number: #{number}>"
172
+ end
173
+
174
+ def page_scope
175
+ if proxy_scoped?(:limit)
176
+ subquery_sql = proxy.name.constantize.send(:construct_finder_sql, proxy_options.merge(:select => "#{proxy.table_name}.#{proxy.primary_key}"))
177
+ paged_conditions = "#{proxy.table_name}.#{proxy.primary_key} IN (#{subquery_sql})"
178
+ paged_options = { :conditions => paged_conditions, :limit => per_page, :offset => (number - 1) * per_page }
179
+ proxy.name.constantize.scoped(proxy_options.except(:limit, :offset)).scoped(paged_options)
180
+ # can't exclude :conditions since it won't work for has_many :through associations (multiple identical records returned)
181
+ else
182
+ proxy.scoped(:limit => per_page, :offset => (number - 1) * per_page)
183
+ end
184
+ end
185
+
186
+ private
187
+
188
+ def subquery_sql
189
+ proxy.name.constantize.send(:construct_finder_sql, proxy_options.merge(:select => "#{proxy.table_name}.#{proxy.primary_key}"))
190
+ end
191
+
192
+ memoize :subquery_sql
193
+ end
194
+ end