will_paginate 2.1.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of will_paginate might be problematic. Click here for more details.
- data/CHANGELOG +32 -2
- data/LICENSE +1 -1
- data/{README → README.rdoc} +42 -59
- data/Rakefile +41 -4
- data/examples/apple-circle.gif +0 -0
- data/examples/index.haml +69 -0
- data/examples/index.html +92 -0
- data/examples/pagination.css +90 -0
- data/examples/pagination.sass +91 -0
- data/init.rb +1 -0
- data/lib/will_paginate.rb +34 -11
- data/lib/will_paginate/array.rb +16 -0
- data/lib/will_paginate/collection.rb +37 -24
- data/lib/will_paginate/core_ext.rb +1 -49
- data/lib/will_paginate/finder.rb +29 -4
- data/lib/will_paginate/named_scope.rb +132 -0
- data/lib/will_paginate/named_scope_patch.rb +50 -0
- data/lib/will_paginate/version.rb +1 -1
- data/lib/will_paginate/view_helpers.rb +123 -21
- data/test/boot.rb +0 -2
- data/test/{array_pagination_test.rb → collection_test.rb} +37 -28
- data/test/console +2 -3
- data/test/database.yml +22 -0
- data/test/finder_test.rb +150 -56
- data/test/fixtures/developer.rb +2 -0
- data/test/fixtures/projects.yml +3 -4
- data/test/fixtures/reply.rb +2 -0
- data/test/fixtures/topic.rb +2 -0
- data/test/helper.rb +13 -1
- data/test/lib/activerecord_test_case.rb +14 -1
- data/test/lib/activerecord_test_connector.rb +19 -10
- data/test/lib/load_fixtures.rb +3 -5
- data/test/lib/view_test_process.rb +73 -0
- data/test/view_test.rb +314 -0
- metadata +32 -31
- data/Manifest.txt +0 -34
- data/config/release.rb +0 -82
- data/setup.rb +0 -1585
- data/test/pagination_test.rb +0 -257
@@ -0,0 +1,91 @@
|
|
1
|
+
.digg_pagination
|
2
|
+
:background white
|
3
|
+
a, span
|
4
|
+
:padding .2em .5em
|
5
|
+
:display block
|
6
|
+
:float left
|
7
|
+
:margin-right 1px
|
8
|
+
span.disabled
|
9
|
+
:color #999
|
10
|
+
:border 1px solid #DDD
|
11
|
+
span.current
|
12
|
+
:font-weight bold
|
13
|
+
:background #2E6AB1
|
14
|
+
:color white
|
15
|
+
:border 1px solid #2E6AB1
|
16
|
+
a
|
17
|
+
:text-decoration none
|
18
|
+
:color #105CB6
|
19
|
+
:border 1px solid #9AAFE5
|
20
|
+
&:hover, &:focus
|
21
|
+
:color #003
|
22
|
+
:border-color #003
|
23
|
+
.page_info
|
24
|
+
:background #2E6AB1
|
25
|
+
:color white
|
26
|
+
:padding .4em .6em
|
27
|
+
:width 22em
|
28
|
+
:margin-bottom .3em
|
29
|
+
:text-align center
|
30
|
+
b
|
31
|
+
:color #003
|
32
|
+
:background = #2E6AB1 + 60
|
33
|
+
:padding .1em .25em
|
34
|
+
|
35
|
+
/* self-clearing method:
|
36
|
+
&:after
|
37
|
+
:content "."
|
38
|
+
:display block
|
39
|
+
:height 0
|
40
|
+
:clear both
|
41
|
+
:visibility hidden
|
42
|
+
* html &
|
43
|
+
:height 1%
|
44
|
+
*:first-child+html &
|
45
|
+
:overflow hidden
|
46
|
+
|
47
|
+
.apple_pagination
|
48
|
+
:background #F1F1F1
|
49
|
+
:border 1px solid #E5E5E5
|
50
|
+
:text-align center
|
51
|
+
:padding 1em
|
52
|
+
a, span
|
53
|
+
:padding .2em .3em
|
54
|
+
span.disabled
|
55
|
+
:color #AAA
|
56
|
+
span.current
|
57
|
+
:font-weight bold
|
58
|
+
:background transparent url(apple-circle.gif) no-repeat 50% 50%
|
59
|
+
a
|
60
|
+
:text-decoration none
|
61
|
+
:color black
|
62
|
+
&:hover, &:focus
|
63
|
+
:text-decoration underline
|
64
|
+
|
65
|
+
.flickr_pagination
|
66
|
+
:text-align center
|
67
|
+
:padding .3em
|
68
|
+
a, span
|
69
|
+
:padding .2em .5em
|
70
|
+
span.disabled
|
71
|
+
:color #AAA
|
72
|
+
span.current
|
73
|
+
:font-weight bold
|
74
|
+
:color #FF0084
|
75
|
+
a
|
76
|
+
:border 1px solid #DDDDDD
|
77
|
+
:color #0063DC
|
78
|
+
:text-decoration none
|
79
|
+
&:hover, &:focus
|
80
|
+
:border-color #003366
|
81
|
+
:background #0063DC
|
82
|
+
:color white
|
83
|
+
.page_info
|
84
|
+
:color #aaa
|
85
|
+
:padding-top .8em
|
86
|
+
.prev_page, .next_page
|
87
|
+
:border-width 2px
|
88
|
+
.prev_page
|
89
|
+
:margin-right 1em
|
90
|
+
.next_page
|
91
|
+
:margin-left 1em
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'will_paginate'
|
data/lib/will_paginate.rb
CHANGED
@@ -20,6 +20,10 @@ module WillPaginate
|
|
20
20
|
return if ActionView::Base.instance_methods.include? 'will_paginate'
|
21
21
|
require 'will_paginate/view_helpers'
|
22
22
|
ActionView::Base.class_eval { include ViewHelpers }
|
23
|
+
|
24
|
+
if defined?(ActionController::Base) and ActionController::Base.respond_to? :rescue_responses
|
25
|
+
ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found
|
26
|
+
end
|
23
27
|
end
|
24
28
|
|
25
29
|
# mixes in WillPaginate::Finder in ActiveRecord::Base and classes that deal
|
@@ -29,28 +33,45 @@ module WillPaginate
|
|
29
33
|
require 'will_paginate/finder'
|
30
34
|
ActiveRecord::Base.class_eval { include Finder }
|
31
35
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
).push(associations::HasManyThroughAssociation).each do |klass|
|
36
|
+
# support pagination on associations
|
37
|
+
a = ActiveRecord::Associations
|
38
|
+
returning([ a::AssociationCollection ]) { |classes|
|
39
|
+
# detect http://dev.rubyonrails.org/changeset/9230
|
40
|
+
unless a::HasManyThroughAssociation.superclass == a::HasManyAssociation
|
41
|
+
classes << a::HasManyThroughAssociation
|
42
|
+
end
|
43
|
+
}.each do |klass|
|
41
44
|
klass.class_eval do
|
42
45
|
include Finder::ClassMethods
|
43
46
|
alias_method_chain :method_missing, :paginate
|
44
47
|
end
|
45
48
|
end
|
46
49
|
end
|
50
|
+
|
51
|
+
# Enable named_scope, a feature of Rails 2.1, even if you have older Rails
|
52
|
+
# (tested on Rails 2.0.2 and 1.2.6).
|
53
|
+
#
|
54
|
+
# You can pass +false+ for +patch+ parameter to skip monkeypatching
|
55
|
+
# *associations*. Use this if you feel that <tt>named_scope</tt> broke
|
56
|
+
# has_many, has_many :through or has_and_belongs_to_many associations in
|
57
|
+
# your app. By passing +false+, you can still use <tt>named_scope</tt> in
|
58
|
+
# your models, but not through associations.
|
59
|
+
def enable_named_scope(patch = true)
|
60
|
+
return if defined? ActiveRecord::NamedScope
|
61
|
+
require 'will_paginate/named_scope'
|
62
|
+
require 'will_paginate/named_scope_patch' if patch
|
63
|
+
|
64
|
+
ActiveRecord::Base.class_eval do
|
65
|
+
include WillPaginate::NamedScope
|
66
|
+
end
|
67
|
+
end
|
47
68
|
end
|
48
69
|
|
49
70
|
module Deprecation #:nodoc:
|
50
71
|
extend ActiveSupport::Deprecation
|
51
72
|
|
52
73
|
def self.warn(message, callstack = caller)
|
53
|
-
message = 'WillPaginate: ' + message.strip.gsub(
|
74
|
+
message = 'WillPaginate: ' + message.strip.gsub(/\s+/, ' ')
|
54
75
|
behavior.call(message, callstack) if behavior && !silenced?
|
55
76
|
end
|
56
77
|
|
@@ -60,4 +81,6 @@ module WillPaginate
|
|
60
81
|
end
|
61
82
|
end
|
62
83
|
|
63
|
-
|
84
|
+
if defined?(Rails) and defined?(ActiveRecord) and defined?(ActionController)
|
85
|
+
WillPaginate.enable
|
86
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'will_paginate/collection'
|
2
|
+
|
3
|
+
# http://www.desimcadam.com/archives/8
|
4
|
+
Array.class_eval do
|
5
|
+
def paginate(options = {})
|
6
|
+
raise ArgumentError, "parameter hash expected (got #{options.inspect})" unless Hash === options
|
7
|
+
|
8
|
+
WillPaginate::Collection.create(
|
9
|
+
options[:page] || 1,
|
10
|
+
options[:per_page] || 30,
|
11
|
+
options[:total_entries] || self.length
|
12
|
+
) { |pager|
|
13
|
+
pager.replace self[pager.offset, pager.per_page].to_a
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
@@ -1,11 +1,18 @@
|
|
1
|
-
require 'will_paginate'
|
2
|
-
|
3
1
|
module WillPaginate
|
4
|
-
# =
|
2
|
+
# = Invalid page number error
|
5
3
|
# This is an ArgumentError raised in case a page was requested that is either
|
6
4
|
# zero or negative number. You should decide how do deal with such errors in
|
7
5
|
# the controller.
|
8
6
|
#
|
7
|
+
# If you're using Rails 2, then this error will automatically get handled like
|
8
|
+
# 404 Not Found. The hook is in "will_paginate.rb":
|
9
|
+
#
|
10
|
+
# ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found
|
11
|
+
#
|
12
|
+
# If you don't like this, use your preffered method of rescuing exceptions in
|
13
|
+
# public from your controllers to handle this differently. The +rescue_from+
|
14
|
+
# method is a nice addition to Rails 2.
|
15
|
+
#
|
9
16
|
# This error is *not* raised when a page further than the last page is
|
10
17
|
# requested. Use <tt>WillPaginate::Collection#out_of_bounds?</tt> method to
|
11
18
|
# check for those cases and manually deal with them as you see fit.
|
@@ -15,26 +22,34 @@ module WillPaginate
|
|
15
22
|
end
|
16
23
|
end
|
17
24
|
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
25
|
+
# = The key to pagination
|
26
|
+
# Arrays returned from paginating finds are, in fact, instances of this little
|
27
|
+
# class. You may think of WillPaginate::Collection as an ordinary array with
|
28
|
+
# some extra properties. Those properties are used by view helpers to generate
|
21
29
|
# correct page links.
|
22
30
|
#
|
23
31
|
# WillPaginate::Collection also assists in rolling out your own pagination
|
24
32
|
# solutions: see +create+.
|
33
|
+
#
|
34
|
+
# If you are writing a library that provides a collection which you would like
|
35
|
+
# to conform to this API, you don't have to copy these methods over; simply
|
36
|
+
# make your plugin/gem dependant on the "will_paginate" gem:
|
25
37
|
#
|
38
|
+
# gem 'will_paginate'
|
39
|
+
# require 'will_paginate/collection'
|
40
|
+
#
|
41
|
+
# # now use WillPaginate::Collection directly or subclass it
|
26
42
|
class Collection < Array
|
27
|
-
attr_reader :current_page, :per_page, :total_entries
|
43
|
+
attr_reader :current_page, :per_page, :total_entries, :total_pages
|
28
44
|
|
29
|
-
# Arguments to
|
45
|
+
# Arguments to the constructor are the current page number, per-page limit
|
30
46
|
# and the total number of entries. The last argument is optional because it
|
31
47
|
# is best to do lazy counting; in other words, count *conditionally* after
|
32
48
|
# populating the collection using the +replace+ method.
|
33
|
-
#
|
34
49
|
def initialize(page, per_page, total = nil)
|
35
50
|
@current_page = page.to_i
|
36
51
|
raise InvalidPage.new(page, @current_page) if @current_page < 1
|
37
|
-
@per_page
|
52
|
+
@per_page = per_page.to_i
|
38
53
|
raise ArgumentError, "`per_page` setting cannot be less than 1 (#{@per_page} given)" if @per_page < 1
|
39
54
|
|
40
55
|
self.total_entries = total if total
|
@@ -65,29 +80,25 @@ module WillPaginate
|
|
65
80
|
# end
|
66
81
|
# end
|
67
82
|
#
|
83
|
+
# The Array#paginate API has since then changed, but this still serves as a
|
84
|
+
# fine example of WillPaginate::Collection usage.
|
68
85
|
def self.create(page, per_page, total = nil, &block)
|
69
86
|
pager = new(page, per_page, total)
|
70
87
|
yield pager
|
71
88
|
pager
|
72
89
|
end
|
73
90
|
|
74
|
-
# The total number of pages.
|
75
|
-
def page_count
|
76
|
-
@total_pages
|
77
|
-
end
|
78
|
-
|
79
91
|
# Helper method that is true when someone tries to fetch a page with a
|
80
92
|
# larger number than the last page. Can be used in combination with flashes
|
81
93
|
# and redirecting.
|
82
94
|
def out_of_bounds?
|
83
|
-
current_page >
|
95
|
+
current_page > total_pages
|
84
96
|
end
|
85
97
|
|
86
98
|
# Current offset of the paginated collection. If we're on the first page,
|
87
99
|
# it is always 0. If we're on the 2nd page and there are 30 entries per page,
|
88
100
|
# the offset is 30. This property is useful if you want to render ordinals
|
89
101
|
# besides your records: simply start with offset + 1.
|
90
|
-
#
|
91
102
|
def offset
|
92
103
|
(current_page - 1) * per_page
|
93
104
|
end
|
@@ -99,7 +110,7 @@ module WillPaginate
|
|
99
110
|
|
100
111
|
# current_page + 1 or nil if there is no next page
|
101
112
|
def next_page
|
102
|
-
current_page <
|
113
|
+
current_page < total_pages ? (current_page + 1) : nil
|
103
114
|
end
|
104
115
|
|
105
116
|
def total_entries=(number)
|
@@ -120,13 +131,15 @@ module WillPaginate
|
|
120
131
|
# +total_entries+ and set it to a proper value if it's +nil+. See the example
|
121
132
|
# in +create+.
|
122
133
|
def replace(array)
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
134
|
+
result = super
|
135
|
+
|
136
|
+
# The collection is shorter then page limit? Rejoice, because
|
137
|
+
# then we know that we are on the last page!
|
138
|
+
if total_entries.nil? and length < per_page and (current_page == 1 or length > 0)
|
139
|
+
self.total_entries = offset + length
|
129
140
|
end
|
141
|
+
|
142
|
+
result
|
130
143
|
end
|
131
144
|
end
|
132
145
|
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
require 'will_paginate'
|
2
1
|
require 'set'
|
2
|
+
require 'will_paginate/array'
|
3
3
|
|
4
4
|
unless Hash.instance_methods.include? 'except'
|
5
5
|
Hash.class_eval do
|
@@ -30,51 +30,3 @@ unless Hash.instance_methods.include? 'slice'
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
33
|
-
|
34
|
-
unless Hash.instance_methods.include? 'rec_merge!'
|
35
|
-
Hash.class_eval do
|
36
|
-
# Same as Hash#merge!, but recursively merges sub-hashes
|
37
|
-
# (stolen from Haml)
|
38
|
-
def rec_merge!(other)
|
39
|
-
other.each do |key, other_value|
|
40
|
-
value = self[key]
|
41
|
-
if value.is_a?(Hash) and other_value.is_a?(Hash)
|
42
|
-
value.rec_merge! other_value
|
43
|
-
else
|
44
|
-
self[key] = other_value
|
45
|
-
end
|
46
|
-
end
|
47
|
-
self
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
require 'will_paginate/collection'
|
53
|
-
|
54
|
-
unless Array.instance_methods.include? 'paginate'
|
55
|
-
# http://www.desimcadam.com/archives/8
|
56
|
-
Array.class_eval do
|
57
|
-
def paginate(options_or_page = {}, per_page = nil)
|
58
|
-
if options_or_page.nil? or Fixnum === options_or_page
|
59
|
-
if defined? WillPaginate::Deprecation
|
60
|
-
WillPaginate::Deprecation.warn <<-DEPR
|
61
|
-
Array#paginate now conforms to the main, ActiveRecord::Base#paginate API. You should \
|
62
|
-
call it with a parameters hash (:page, :per_page). The old API (numbers as arguments) \
|
63
|
-
has been deprecated and is going to be unsupported in future versions of will_paginate.
|
64
|
-
DEPR
|
65
|
-
end
|
66
|
-
page = options_or_page
|
67
|
-
options = {}
|
68
|
-
else
|
69
|
-
options = options_or_page
|
70
|
-
page = options[:page]
|
71
|
-
raise ArgumentError, "wrong number of arguments (1 hash or 2 Fixnums expected)" if per_page
|
72
|
-
per_page = options[:per_page]
|
73
|
-
end
|
74
|
-
|
75
|
-
WillPaginate::Collection.create(page || 1, per_page || 30, options[:total_entries] || size) do |pager|
|
76
|
-
pager.replace self[pager.offset, pager.per_page].to_a
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
data/lib/will_paginate/finder.rb
CHANGED
@@ -2,7 +2,7 @@ require 'will_paginate/core_ext'
|
|
2
2
|
|
3
3
|
module WillPaginate
|
4
4
|
# A mixin for ActiveRecord::Base. Provides +per_page+ class method
|
5
|
-
# and
|
5
|
+
# and hooks things up to provide paginating finders.
|
6
6
|
#
|
7
7
|
# Find out more in WillPaginate::Finder::ClassMethods
|
8
8
|
#
|
@@ -18,9 +18,9 @@ module WillPaginate
|
|
18
18
|
|
19
19
|
# = Paginating finders for ActiveRecord models
|
20
20
|
#
|
21
|
-
# WillPaginate adds +paginate
|
22
|
-
# class methods and associations. It also hooks into
|
23
|
-
# intercept pagination calls to dynamic finders such as
|
21
|
+
# WillPaginate adds +paginate+, +per_page+ and other methods to
|
22
|
+
# ActiveRecord::Base class methods and associations. It also hooks into
|
23
|
+
# +method_missing+ to intercept pagination calls to dynamic finders such as
|
24
24
|
# +paginate_by_user_id+ and translate them to ordinary finders
|
25
25
|
# (+find_all_by_user_id+ in this case).
|
26
26
|
#
|
@@ -85,6 +85,31 @@ module WillPaginate
|
|
85
85
|
pager.total_entries = wp_count(count_options, args, finder) unless pager.total_entries
|
86
86
|
end
|
87
87
|
end
|
88
|
+
|
89
|
+
# Iterates through all records by loading one page at a time. This is useful
|
90
|
+
# for migrations or any other use case where you don't want to load all the
|
91
|
+
# records in memory at once.
|
92
|
+
#
|
93
|
+
# It uses +paginate+ internally; therefore it accepts all of its options.
|
94
|
+
# You can specify a starting page with <tt>:page</tt> (default is 1). Default
|
95
|
+
# <tt>:order</tt> is <tt>"id"</tt>, override if necessary.
|
96
|
+
#
|
97
|
+
# See http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord where
|
98
|
+
# Jamis Buck describes this and also uses a more efficient way for MySQL.
|
99
|
+
def paginated_each(options = {}, &block)
|
100
|
+
options = { :order => 'id', :page => 1 }.merge options
|
101
|
+
options[:page] = options[:page].to_i
|
102
|
+
options[:total_entries] = 0 # skip the individual count queries
|
103
|
+
total = 0
|
104
|
+
|
105
|
+
begin
|
106
|
+
collection = paginate(options)
|
107
|
+
total += collection.each(&block).size
|
108
|
+
options[:page] += 1
|
109
|
+
end until collection.size < collection.per_page
|
110
|
+
|
111
|
+
total
|
112
|
+
end
|
88
113
|
|
89
114
|
# Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string
|
90
115
|
# based on the params otherwise used by paginating finds: +page+ and
|
@@ -0,0 +1,132 @@
|
|
1
|
+
## stolen from: http://dev.rubyonrails.org/browser/trunk/activerecord/lib/active_record/named_scope.rb?rev=9084
|
2
|
+
|
3
|
+
module WillPaginate
|
4
|
+
# This is a feature backported from Rails 2.1 because of its usefullness not only with will_paginate,
|
5
|
+
# but in other aspects when managing complex conditions that you want to be reusable.
|
6
|
+
module NamedScope
|
7
|
+
# All subclasses of ActiveRecord::Base have two named_scopes:
|
8
|
+
# * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
|
9
|
+
# * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly:
|
10
|
+
#
|
11
|
+
# Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)
|
12
|
+
#
|
13
|
+
# These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
|
14
|
+
# intermediate values (scopes) around as first-class objects is convenient.
|
15
|
+
def self.included(base)
|
16
|
+
base.class_eval do
|
17
|
+
extend ClassMethods
|
18
|
+
named_scope :all
|
19
|
+
named_scope :scoped, lambda { |scope| scope }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
def scopes #:nodoc:
|
25
|
+
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
|
26
|
+
end
|
27
|
+
|
28
|
+
# Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
|
29
|
+
# such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
|
30
|
+
#
|
31
|
+
# class Shirt < ActiveRecord::Base
|
32
|
+
# named_scope :red, :conditions => {:color => 'red'}
|
33
|
+
# named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
|
37
|
+
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
|
38
|
+
#
|
39
|
+
# Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
|
40
|
+
# constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
|
41
|
+
# <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
|
42
|
+
# as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
|
43
|
+
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
|
44
|
+
#
|
45
|
+
# These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
|
46
|
+
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
|
47
|
+
# for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
|
48
|
+
#
|
49
|
+
# All scopes are available as class methods on the ActiveRecord descendent upon which the scopes were defined. But they are also available to
|
50
|
+
# <tt>has_many</tt> associations. If,
|
51
|
+
#
|
52
|
+
# class Person < ActiveRecord::Base
|
53
|
+
# has_many :shirts
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
|
57
|
+
# only shirts.
|
58
|
+
#
|
59
|
+
# Named scopes can also be procedural.
|
60
|
+
#
|
61
|
+
# class Shirt < ActiveRecord::Base
|
62
|
+
# named_scope :colored, lambda { |color|
|
63
|
+
# { :conditions => { :color => color } }
|
64
|
+
# }
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
|
68
|
+
#
|
69
|
+
# Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
|
70
|
+
#
|
71
|
+
# class Shirt < ActiveRecord::Base
|
72
|
+
# named_scope :red, :conditions => {:color => 'red'} do
|
73
|
+
# def dom_id
|
74
|
+
# 'red_shirts'
|
75
|
+
# end
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
def named_scope(name, options = {}, &block)
|
80
|
+
scopes[name] = lambda do |parent_scope, *args|
|
81
|
+
Scope.new(parent_scope, case options
|
82
|
+
when Hash
|
83
|
+
options
|
84
|
+
when Proc
|
85
|
+
options.call(*args)
|
86
|
+
end, &block)
|
87
|
+
end
|
88
|
+
(class << self; self end).instance_eval do
|
89
|
+
define_method name do |*args|
|
90
|
+
scopes[name].call(self, *args)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class Scope #:nodoc:
|
97
|
+
attr_reader :proxy_scope, :proxy_options
|
98
|
+
[].methods.each { |m| delegate m, :to => :proxy_found unless m =~ /(^__|^nil\?|^send|class|extend|find|count|sum|average|maximum|minimum|paginate)/ }
|
99
|
+
delegate :scopes, :with_scope, :to => :proxy_scope
|
100
|
+
|
101
|
+
def initialize(proxy_scope, options, &block)
|
102
|
+
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
|
103
|
+
extend Module.new(&block) if block_given?
|
104
|
+
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
|
105
|
+
end
|
106
|
+
|
107
|
+
def reload
|
108
|
+
load_found; self
|
109
|
+
end
|
110
|
+
|
111
|
+
protected
|
112
|
+
def proxy_found
|
113
|
+
@found || load_found
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
def method_missing(method, *args, &block)
|
118
|
+
if scopes.include?(method)
|
119
|
+
scopes[method].call(self, *args)
|
120
|
+
else
|
121
|
+
with_scope :find => proxy_options do
|
122
|
+
proxy_scope.send(method, *args, &block)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def load_found
|
128
|
+
@found = find(:all)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|