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/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.textile +471 -0
- data/Rakefile +55 -0
- data/VERSION.yml +4 -0
- data/lib/paged_scopes.rb +9 -0
- data/lib/paged_scopes/collection.rb +45 -0
- data/lib/paged_scopes/context.rb +36 -0
- data/lib/paged_scopes/controller.rb +54 -0
- data/lib/paged_scopes/index.rb +62 -0
- data/lib/paged_scopes/pages.rb +194 -0
- data/lib/paged_scopes/paginator.rb +68 -0
- data/lib/paged_scopes/resources.rb +32 -0
- data/paged_scopes.gemspec +72 -0
- data/rails/init.rb +1 -0
- data/spec/collection_spec.rb +66 -0
- data/spec/context_spec.rb +27 -0
- data/spec/controller_spec.rb +234 -0
- data/spec/index_spec.rb +31 -0
- data/spec/page_spec.rb +249 -0
- data/spec/paginator_spec.rb +256 -0
- data/spec/resources_spec.rb +72 -0
- data/spec/spec_helper.rb +160 -0
- metadata +95 -0
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
data/lib/paged_scopes.rb
ADDED
@@ -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
|