paged_scopes 0.1.0

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/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