summaryrefslogtreecommitdiff
path: root/vendor/plugins/will_paginate/lib
diff options
context:
space:
mode:
authorhukl <contact@smyck.org>2009-02-17 12:47:49 +0100
committerhukl <contact@smyck.org>2009-02-17 12:47:49 +0100
commit3cdcb5ca02a94b2b342c30903a27d47d35d46e55 (patch)
treeef3e1154fe262561b3955c07edcaee3824a21f47 /vendor/plugins/will_paginate/lib
parent7a282f2605f6fb3689940e5c7072b4801653deb2 (diff)
added will_paginate plugin
Diffstat (limited to 'vendor/plugins/will_paginate/lib')
-rw-r--r--vendor/plugins/will_paginate/lib/will_paginate.rb78
-rw-r--r--vendor/plugins/will_paginate/lib/will_paginate/array.rb16
-rw-r--r--vendor/plugins/will_paginate/lib/will_paginate/collection.rb146
-rw-r--r--vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb32
-rw-r--r--vendor/plugins/will_paginate/lib/will_paginate/finder.rb264
-rw-r--r--vendor/plugins/will_paginate/lib/will_paginate/named_scope.rb170
-rw-r--r--vendor/plugins/will_paginate/lib/will_paginate/named_scope_patch.rb37
-rw-r--r--vendor/plugins/will_paginate/lib/will_paginate/version.rb9
-rw-r--r--vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb389
9 files changed, 1141 insertions, 0 deletions
diff --git a/vendor/plugins/will_paginate/lib/will_paginate.rb b/vendor/plugins/will_paginate/lib/will_paginate.rb
new file mode 100644
index 0000000..e072412
--- /dev/null
+++ b/vendor/plugins/will_paginate/lib/will_paginate.rb
@@ -0,0 +1,78 @@
1require 'active_support'
2
3# = You *will* paginate!
4#
5# First read about WillPaginate::Finder::ClassMethods, then see
6# WillPaginate::ViewHelpers. The magical array you're handling in-between is
7# WillPaginate::Collection.
8#
9# Happy paginating!
10module WillPaginate
11 class << self
12 # shortcut for <tt>enable_actionpack</tt> and <tt>enable_activerecord</tt> combined
13 def enable
14 enable_actionpack
15 enable_activerecord
16 end
17
18 # hooks WillPaginate::ViewHelpers into ActionView::Base
19 def enable_actionpack
20 return if ActionView::Base.instance_methods.include? 'will_paginate'
21 require 'will_paginate/view_helpers'
22 ActionView::Base.send :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
27 end
28
29 # hooks WillPaginate::Finder into ActiveRecord::Base and classes that deal
30 # with associations
31 def enable_activerecord
32 return if ActiveRecord::Base.respond_to? :paginate
33 require 'will_paginate/finder'
34 ActiveRecord::Base.send :include, Finder
35
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|
44 klass.send :include, Finder::ClassMethods
45 klass.class_eval { alias_method_chain :method_missing, :paginate }
46 end
47 end
48
49 # Enable named_scope, a feature of Rails 2.1, even if you have older Rails
50 # (tested on Rails 2.0.2 and 1.2.6).
51 #
52 # You can pass +false+ for +patch+ parameter to skip monkeypatching
53 # *associations*. Use this if you feel that <tt>named_scope</tt> broke
54 # has_many, has_many :through or has_and_belongs_to_many associations in
55 # your app. By passing +false+, you can still use <tt>named_scope</tt> in
56 # your models, but not through associations.
57 def enable_named_scope(patch = true)
58 return if defined? ActiveRecord::NamedScope
59 require 'will_paginate/named_scope'
60 require 'will_paginate/named_scope_patch' if patch
61
62 ActiveRecord::Base.send :include, WillPaginate::NamedScope
63 end
64 end
65
66 module Deprecation # :nodoc:
67 extend ActiveSupport::Deprecation
68
69 def self.warn(message, callstack = caller)
70 message = 'WillPaginate: ' + message.strip.gsub(/\s+/, ' ')
71 ActiveSupport::Deprecation.warn(message, callstack)
72 end
73 end
74end
75
76if defined?(Rails) and defined?(ActiveRecord) and defined?(ActionController)
77 WillPaginate.enable
78end
diff --git a/vendor/plugins/will_paginate/lib/will_paginate/array.rb b/vendor/plugins/will_paginate/lib/will_paginate/array.rb
new file mode 100644
index 0000000..d061d2b
--- /dev/null
+++ b/vendor/plugins/will_paginate/lib/will_paginate/array.rb
@@ -0,0 +1,16 @@
1require 'will_paginate/collection'
2
3# http://www.desimcadam.com/archives/8
4Array.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
16end
diff --git a/vendor/plugins/will_paginate/lib/will_paginate/collection.rb b/vendor/plugins/will_paginate/lib/will_paginate/collection.rb
new file mode 100644
index 0000000..e253570
--- /dev/null
+++ b/vendor/plugins/will_paginate/lib/will_paginate/collection.rb
@@ -0,0 +1,146 @@
1module WillPaginate
2 # = Invalid page number error
3 # This is an ArgumentError raised in case a page was requested that is either
4 # zero or negative number. You should decide how do deal with such errors in
5 # the controller.
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 #
16 # This error is *not* raised when a page further than the last page is
17 # requested. Use <tt>WillPaginate::Collection#out_of_bounds?</tt> method to
18 # check for those cases and manually deal with them as you see fit.
19 class InvalidPage < ArgumentError
20 def initialize(page, page_num)
21 super "#{page.inspect} given as value, which translates to '#{page_num}' as page number"
22 end
23 end
24
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
29 # correct page links.
30 #
31 # WillPaginate::Collection also assists in rolling out your own pagination
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 "mislav-will_paginate" gem:
37 #
38 # gem 'mislav-will_paginate'
39 # require 'will_paginate/collection'
40 #
41 # # WillPaginate::Collection is now available for use
42 class Collection < Array
43 attr_reader :current_page, :per_page, :total_entries, :total_pages
44
45 # Arguments to the constructor are the current page number, per-page limit
46 # and the total number of entries. The last argument is optional because it
47 # is best to do lazy counting; in other words, count *conditionally* after
48 # populating the collection using the +replace+ method.
49 def initialize(page, per_page, total = nil)
50 @current_page = page.to_i
51 raise InvalidPage.new(page, @current_page) if @current_page < 1
52 @per_page = per_page.to_i
53 raise ArgumentError, "`per_page` setting cannot be less than 1 (#{@per_page} given)" if @per_page < 1
54
55 self.total_entries = total if total
56 end
57
58 # Just like +new+, but yields the object after instantiation and returns it
59 # afterwards. This is very useful for manual pagination:
60 #
61 # @entries = WillPaginate::Collection.create(1, 10) do |pager|
62 # result = Post.find(:all, :limit => pager.per_page, :offset => pager.offset)
63 # # inject the result array into the paginated collection:
64 # pager.replace(result)
65 #
66 # unless pager.total_entries
67 # # the pager didn't manage to guess the total count, do it manually
68 # pager.total_entries = Post.count
69 # end
70 # end
71 #
72 # The possibilities with this are endless. For another example, here is how
73 # WillPaginate used to define pagination for Array instances:
74 #
75 # Array.class_eval do
76 # def paginate(page = 1, per_page = 15)
77 # WillPaginate::Collection.create(page, per_page, size) do |pager|
78 # pager.replace self[pager.offset, pager.per_page].to_a
79 # end
80 # end
81 # end
82 #
83 # The Array#paginate API has since then changed, but this still serves as a
84 # fine example of WillPaginate::Collection usage.
85 def self.create(page, per_page, total = nil)
86 pager = new(page, per_page, total)
87 yield pager
88 pager
89 end
90
91 # Helper method that is true when someone tries to fetch a page with a
92 # larger number than the last page. Can be used in combination with flashes
93 # and redirecting.
94 def out_of_bounds?
95 current_page > total_pages
96 end
97
98 # Current offset of the paginated collection. If we're on the first page,
99 # it is always 0. If we're on the 2nd page and there are 30 entries per page,
100 # the offset is 30. This property is useful if you want to render ordinals
101 # side by side with records in the view: simply start with offset + 1.
102 def offset
103 (current_page - 1) * per_page
104 end
105
106 # current_page - 1 or nil if there is no previous page
107 def previous_page
108 current_page > 1 ? (current_page - 1) : nil
109 end
110
111 # current_page + 1 or nil if there is no next page
112 def next_page
113 current_page < total_pages ? (current_page + 1) : nil
114 end
115
116 # sets the <tt>total_entries</tt> property and calculates <tt>total_pages</tt>
117 def total_entries=(number)
118 @total_entries = number.to_i
119 @total_pages = (@total_entries / per_page.to_f).ceil
120 end
121
122 # This is a magic wrapper for the original Array#replace method. It serves
123 # for populating the paginated collection after initialization.
124 #
125 # Why magic? Because it tries to guess the total number of entries judging
126 # by the size of given array. If it is shorter than +per_page+ limit, then we
127 # know we're on the last page. This trick is very useful for avoiding
128 # unnecessary hits to the database to do the counting after we fetched the
129 # data for the current page.
130 #
131 # However, after using +replace+ you should always test the value of
132 # +total_entries+ and set it to a proper value if it's +nil+. See the example
133 # in +create+.
134 def replace(array)
135 result = super
136
137 # The collection is shorter then page limit? Rejoice, because
138 # then we know that we are on the last page!
139 if total_entries.nil? and length < per_page and (current_page == 1 or length > 0)
140 self.total_entries = offset + length
141 end
142
143 result
144 end
145 end
146end
diff --git a/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb b/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb
new file mode 100644
index 0000000..32f10f5
--- /dev/null
+++ b/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb
@@ -0,0 +1,32 @@
1require 'set'
2require 'will_paginate/array'
3
4unless Hash.instance_methods.include? 'except'
5 Hash.class_eval do
6 # Returns a new hash without the given keys.
7 def except(*keys)
8 rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
9 reject { |key,| rejected.include?(key) }
10 end
11
12 # Replaces the hash without only the given keys.
13 def except!(*keys)
14 replace(except(*keys))
15 end
16 end
17end
18
19unless Hash.instance_methods.include? 'slice'
20 Hash.class_eval do
21 # Returns a new hash with only the given keys.
22 def slice(*keys)
23 allowed = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
24 reject { |key,| !allowed.include?(key) }
25 end
26
27 # Replaces the hash with only the given keys.
28 def slice!(*keys)
29 replace(slice(*keys))
30 end
31 end
32end
diff --git a/vendor/plugins/will_paginate/lib/will_paginate/finder.rb b/vendor/plugins/will_paginate/lib/will_paginate/finder.rb
new file mode 100644
index 0000000..d0e5668
--- /dev/null
+++ b/vendor/plugins/will_paginate/lib/will_paginate/finder.rb
@@ -0,0 +1,264 @@
1require 'will_paginate/core_ext'
2
3module WillPaginate
4 # A mixin for ActiveRecord::Base. Provides +per_page+ class method
5 # and hooks things up to provide paginating finders.
6 #
7 # Find out more in WillPaginate::Finder::ClassMethods
8 #
9 module Finder
10 def self.included(base)
11 base.extend ClassMethods
12 class << base
13 alias_method_chain :method_missing, :paginate
14 # alias_method_chain :find_every, :paginate
15 define_method(:per_page) { 30 } unless respond_to?(:per_page)
16 end
17 end
18
19 # = Paginating finders for ActiveRecord models
20 #
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 # +paginate_by_user_id+ and translate them to ordinary finders
25 # (+find_all_by_user_id+ in this case).
26 #
27 # In short, paginating finders are equivalent to ActiveRecord finders; the
28 # only difference is that we start with "paginate" instead of "find" and
29 # that <tt>:page</tt> is required parameter:
30 #
31 # @posts = Post.paginate :all, :page => params[:page], :order => 'created_at DESC'
32 #
33 # In paginating finders, "all" is implicit. There is no sense in paginating
34 # a single record, right? So, you can drop the <tt>:all</tt> argument:
35 #
36 # Post.paginate(...) => Post.find :all
37 # Post.paginate_all_by_something => Post.find_all_by_something
38 # Post.paginate_by_something => Post.find_all_by_something
39 #
40 # == The importance of the <tt>:order</tt> parameter
41 #
42 # In ActiveRecord finders, <tt>:order</tt> parameter specifies columns for
43 # the <tt>ORDER BY</tt> clause in SQL. It is important to have it, since
44 # pagination only makes sense with ordered sets. Without the <tt>ORDER
45 # BY</tt> clause, databases aren't required to do consistent ordering when
46 # performing <tt>SELECT</tt> queries; this is especially true for
47 # PostgreSQL.
48 #
49 # Therefore, make sure you are doing ordering on a column that makes the
50 # most sense in the current context. Make that obvious to the user, also.
51 # For perfomance reasons you will also want to add an index to that column.
52 module ClassMethods
53 # This is the main paginating finder.
54 #
55 # == Special parameters for paginating finders
56 # * <tt>:page</tt> -- REQUIRED, but defaults to 1 if false or nil
57 # * <tt>:per_page</tt> -- defaults to <tt>CurrentModel.per_page</tt> (which is 30 if not overridden)
58 # * <tt>:total_entries</tt> -- use only if you manually count total entries
59 # * <tt>:count</tt> -- additional options that are passed on to +count+
60 # * <tt>:finder</tt> -- name of the ActiveRecord finder used (default: "find")
61 #
62 # All other options (+conditions+, +order+, ...) are forwarded to +find+
63 # and +count+ calls.
64 def paginate(*args)
65 options = args.pop
66 page, per_page, total_entries = wp_parse_options(options)
67 finder = (options[:finder] || 'find').to_s
68
69 if finder == 'find'
70 # an array of IDs may have been given:
71 total_entries ||= (Array === args.first and args.first.size)
72 # :all is implicit
73 args.unshift(:all) if args.empty?
74 end
75
76 WillPaginate::Collection.create(page, per_page, total_entries) do |pager|
77 count_options = options.except :page, :per_page, :total_entries, :finder
78 find_options = count_options.except(:count).update(:offset => pager.offset, :limit => pager.per_page)
79
80 args << find_options
81 # @options_from_last_find = nil
82 pager.replace(send(finder, *args) { |*a| yield(*a) if block_given? })
83
84 # magic counting for user convenience:
85 pager.total_entries = wp_count(count_options, args, finder) unless pager.total_entries
86 end
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 {Faking Cursors in ActiveRecord}[http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord]
98 # where Jamis Buck describes this and a more efficient way for MySQL.
99 def paginated_each(options = {})
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 with_exclusive_scope(:find => {}) do
108 # using exclusive scope so that the block is yielded in scope-free context
109 total += collection.each { |item| yield item }.size
110 end
111 options[:page] += 1
112 end until collection.size < collection.per_page
113
114 total
115 end
116
117 # Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string
118 # based on the params otherwise used by paginating finds: +page+ and
119 # +per_page+.
120 #
121 # Example:
122 #
123 # @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000],
124 # :page => params[:page], :per_page => 3
125 #
126 # A query for counting rows will automatically be generated if you don't
127 # supply <tt>:total_entries</tt>. If you experience problems with this
128 # generated SQL, you might want to perform the count manually in your
129 # application.
130 #
131 def paginate_by_sql(sql, options)
132 WillPaginate::Collection.create(*wp_parse_options(options)) do |pager|
133 query = sanitize_sql(sql.dup)
134 original_query = query.dup
135 # add limit, offset
136 add_limit! query, :offset => pager.offset, :limit => pager.per_page
137 # perfom the find
138 pager.replace find_by_sql(query)
139
140 unless pager.total_entries
141 count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
142 count_query = "SELECT COUNT(*) FROM (#{count_query})"
143
144 unless ['oracle', 'oci'].include?(self.connection.adapter_name.downcase)
145 count_query << ' AS count_table'
146 end
147 # perform the count query
148 pager.total_entries = count_by_sql(count_query)
149 end
150 end
151 end
152
153 def respond_to?(method, include_priv = false) #:nodoc:
154 case method.to_sym
155 when :paginate, :paginate_by_sql
156 true
157 else
158 super(method.to_s.sub(/^paginate/, 'find'), include_priv)
159 end
160 end
161
162 protected
163
164 def method_missing_with_paginate(method, *args) #:nodoc:
165 # did somebody tried to paginate? if not, let them be
166 unless method.to_s.index('paginate') == 0
167 if block_given?
168 return method_missing_without_paginate(method, *args) { |*a| yield(*a) }
169 else
170 return method_missing_without_paginate(method, *args)
171 end
172 end
173
174 # paginate finders are really just find_* with limit and offset
175 finder = method.to_s.sub('paginate', 'find')
176 finder.sub!('find', 'find_all') if finder.index('find_by_') == 0
177
178 options = args.pop
179 raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
180 options = options.dup
181 options[:finder] = finder
182 args << options
183
184 paginate(*args) { |*a| yield(*a) if block_given? }
185 end
186
187 # Does the not-so-trivial job of finding out the total number of entries
188 # in the database. It relies on the ActiveRecord +count+ method.
189 def wp_count(options, args, finder)
190 excludees = [:count, :order, :limit, :offset, :readonly]
191 excludees << :from unless ActiveRecord::Calculations::CALCULATIONS_OPTIONS.include?(:from)
192
193 # we may be in a model or an association proxy
194 klass = (@owner and @reflection) ? @reflection.klass : self
195
196 # Use :select from scope if it isn't already present.
197 options[:select] = scope(:find, :select) unless options[:select]
198
199 if options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
200 # Remove quoting and check for table_name.*-like statement.
201 if options[:select].gsub('`', '') =~ /\w+\.\*/
202 options[:select] = "DISTINCT #{klass.table_name}.#{klass.primary_key}"
203 end
204 else
205 excludees << :select # only exclude the select param if it doesn't begin with DISTINCT
206 end
207
208 # count expects (almost) the same options as find
209 count_options = options.except *excludees
210
211 # merge the hash found in :count
212 # this allows you to specify :select, :order, or anything else just for the count query
213 count_options.update options[:count] if options[:count]
214
215 # forget about includes if they are irrelevant (Rails 2.1)
216 if count_options[:include] and
217 klass.private_methods.include?('references_eager_loaded_tables?') and
218 !klass.send(:references_eager_loaded_tables?, count_options)
219 count_options.delete :include
220 end
221
222 # we may have to scope ...
223 counter = Proc.new { count(count_options) }
224
225 count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
226 # scope_out adds a 'with_finder' method which acts like with_scope, if it's present
227 # then execute the count with the scoping provided by the with_finder
228 send(scoper, &counter)
229 elsif finder =~ /^find_(all_by|by)_([_a-zA-Z]\w*)$/
230 # extract conditions from calls like "paginate_by_foo_and_bar"
231 attribute_names = $2.split('_and_')
232 conditions = construct_attributes_from_arguments(attribute_names, args)
233 with_scope(:find => { :conditions => conditions }, &counter)
234 else
235 counter.call
236 end
237
238 count.respond_to?(:length) ? count.length : count
239 end
240
241 def wp_parse_options(options) #:nodoc:
242 raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
243 options = options.symbolize_keys
244 raise ArgumentError, ':page parameter required' unless options.key? :page
245
246 if options[:count] and options[:total_entries]
247 raise ArgumentError, ':count and :total_entries are mutually exclusive'
248 end
249
250 page = options[:page] || 1
251 per_page = options[:per_page] || self.per_page
252 total = options[:total_entries]
253 [page, per_page, total]
254 end
255
256 private
257
258 # def find_every_with_paginate(options)
259 # @options_from_last_find = options
260 # find_every_without_paginate(options)
261 # end
262 end
263 end
264end
diff --git a/vendor/plugins/will_paginate/lib/will_paginate/named_scope.rb b/vendor/plugins/will_paginate/lib/will_paginate/named_scope.rb
new file mode 100644
index 0000000..5a743d7
--- /dev/null
+++ b/vendor/plugins/will_paginate/lib/will_paginate/named_scope.rb
@@ -0,0 +1,170 @@
1module WillPaginate
2 # This is a feature backported from Rails 2.1 because of its usefullness not only with will_paginate,
3 # but in other aspects when managing complex conditions that you want to be reusable.
4 module NamedScope
5 # All subclasses of ActiveRecord::Base have two named_scopes:
6 # * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
7 # * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
8 #
9 # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
10 # intermediate values (scopes) around as first-class objects is convenient.
11 def self.included(base)
12 base.class_eval do
13 extend ClassMethods
14 named_scope :scoped, lambda { |scope| scope }
15 end
16 end
17
18 module ClassMethods
19 def scopes
20 read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
21 end
22
23 # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
24 # such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
25 #
26 # class Shirt < ActiveRecord::Base
27 # named_scope :red, :conditions => {:color => 'red'}
28 # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
29 # end
30 #
31 # 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>,
32 # in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
33 #
34 # Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
35 # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
36 # <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
37 # as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
38 # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
39 #
40 # 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.
41 # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
42 # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
43 #
44 # All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
45 # <tt>has_many</tt> associations. If,
46 #
47 # class Person < ActiveRecord::Base
48 # has_many :shirts
49 # end
50 #
51 # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
52 # only shirts.
53 #
54 # Named scopes can also be procedural.
55 #
56 # class Shirt < ActiveRecord::Base
57 # named_scope :colored, lambda { |color|
58 # { :conditions => { :color => color } }
59 # }
60 # end
61 #
62 # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
63 #
64 # Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
65 #
66 # class Shirt < ActiveRecord::Base
67 # named_scope :red, :conditions => {:color => 'red'} do
68 # def dom_id
69 # 'red_shirts'
70 # end
71 # end
72 # end
73 #
74 #
75 # For testing complex named scopes, you can examine the scoping options using the
76 # <tt>proxy_options</tt> method on the proxy itself.
77 #
78 # class Shirt < ActiveRecord::Base
79 # named_scope :colored, lambda { |color|
80 # { :conditions => { :color => color } }
81 # }
82 # end
83 #
84 # expected_options = { :conditions => { :colored => 'red' } }
85 # assert_equal expected_options, Shirt.colored('red').proxy_options
86 def named_scope(name, options = {})
87 name = name.to_sym
88 scopes[name] = lambda do |parent_scope, *args|
89 Scope.new(parent_scope, case options
90 when Hash
91 options
92 when Proc
93 options.call(*args)
94 end) { |*a| yield(*a) if block_given? }
95 end
96 (class << self; self end).instance_eval do
97 define_method name do |*args|
98 scopes[name].call(self, *args)
99 end
100 end
101 end
102 end
103
104 class Scope
105 attr_reader :proxy_scope, :proxy_options
106
107 [].methods.each do |m|
108 unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
109 delegate m, :to => :proxy_found
110 end
111 end
112
113 delegate :scopes, :with_scope, :to => :proxy_scope
114
115 def initialize(proxy_scope, options)
116 [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
117 extend Module.new { |*args| yield(*args) } if block_given?
118 @proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
119 end
120
121 def reload
122 load_found; self
123 end
124
125 def first(*args)
126 if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
127 proxy_found.first(*args)
128 else
129 find(:first, *args)
130 end
131 end
132
133 def last(*args)
134 if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
135 proxy_found.last(*args)
136 else
137 find(:last, *args)
138 end
139 end
140
141 def empty?
142 @found ? @found.empty? : count.zero?
143 end
144
145 def respond_to?(method, include_private = false)
146 super || @proxy_scope.respond_to?(method, include_private)
147 end
148
149 protected
150 def proxy_found
151 @found || load_found
152 end
153
154 private
155 def method_missing(method, *args)
156 if scopes.include?(method)
157 scopes[method].call(self, *args)
158 else
159 with_scope :find => proxy_options do
160 proxy_scope.send(method, *args) { |*a| yield(*a) if block_given? }
161 end
162 end
163 end
164
165 def load_found
166 @found = find(:all)
167 end
168 end
169 end
170end
diff --git a/vendor/plugins/will_paginate/lib/will_paginate/named_scope_patch.rb b/vendor/plugins/will_paginate/lib/will_paginate/named_scope_patch.rb
new file mode 100644
index 0000000..7daff59
--- /dev/null
+++ b/vendor/plugins/will_paginate/lib/will_paginate/named_scope_patch.rb
@@ -0,0 +1,37 @@
1ActiveRecord::Associations::AssociationProxy.class_eval do
2 protected
3 def with_scope(*args)
4 @reflection.klass.send(:with_scope, *args) { |*a| yield(*a) if block_given? }
5 end
6end
7
8[ ActiveRecord::Associations::AssociationCollection,
9 ActiveRecord::Associations::HasManyThroughAssociation ].each do |klass|
10 klass.class_eval do
11 protected
12 alias :method_missing_without_scopes :method_missing_without_paginate
13 def method_missing_without_paginate(method, *args)
14 if @reflection.klass.scopes.include?(method)
15 @reflection.klass.scopes[method].call(self, *args) { |*a| yield(*a) if block_given? }
16 else
17 method_missing_without_scopes(method, *args) { |*a| yield(*a) if block_given? }
18 end
19 end
20 end
21end
22
23# Rails 1.2.6
24ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do
25 protected
26 def method_missing(method, *args)
27 if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
28 super
29 elsif @reflection.klass.scopes.include?(method)
30 @reflection.klass.scopes[method].call(self, *args)
31 else
32 @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
33 @reflection.klass.send(method, *args) { |*a| yield(*a) if block_given? }
34 end
35 end
36 end
37end if ActiveRecord::Base.respond_to? :find_first
diff --git a/vendor/plugins/will_paginate/lib/will_paginate/version.rb b/vendor/plugins/will_paginate/lib/will_paginate/version.rb
new file mode 100644
index 0000000..99f3238
--- /dev/null
+++ b/vendor/plugins/will_paginate/lib/will_paginate/version.rb
@@ -0,0 +1,9 @@
1module WillPaginate
2 module VERSION
3 MAJOR = 2
4 MINOR = 3
5 TINY = 5
6
7 STRING = [MAJOR, MINOR, TINY].join('.')
8 end
9end
diff --git a/vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb b/vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb
new file mode 100644
index 0000000..5f7ae42
--- /dev/null
+++ b/vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb
@@ -0,0 +1,389 @@
1require 'will_paginate/core_ext'
2
3module WillPaginate
4 # = Will Paginate view helpers
5 #
6 # The main view helper, #will_paginate, renders
7 # pagination links for the given collection. The helper itself is lightweight
8 # and serves only as a wrapper around LinkRenderer instantiation; the
9 # renderer then does all the hard work of generating the HTML.
10 #
11 # == Global options for helpers
12 #
13 # Options for pagination helpers are optional and get their default values from the
14 # <tt>WillPaginate::ViewHelpers.pagination_options</tt> hash. You can write to this hash to
15 # override default options on the global level:
16 #
17 # WillPaginate::ViewHelpers.pagination_options[:previous_label] = 'Previous page'
18 #
19 # By putting this into "config/initializers/will_paginate.rb" (or simply environment.rb in
20 # older versions of Rails) you can easily translate link texts to previous
21 # and next pages, as well as override some other defaults to your liking.
22 module ViewHelpers
23 # default options that can be overridden on the global level
24 @@pagination_options = {
25 :class => 'pagination',
26 :previous_label => '&laquo; Previous',
27 :next_label => 'Next &raquo;',
28 :inner_window => 4, # links around the current page
29 :outer_window => 1, # links around beginning and end
30 :separator => ' ', # single space is friendly to spiders and non-graphic browsers
31 :param_name => :page,
32 :params => nil,
33 :renderer => 'WillPaginate::LinkRenderer',
34 :page_links => true,
35 :container => true
36 }
37 mattr_reader :pagination_options
38
39 # Renders Digg/Flickr-style pagination for a WillPaginate::Collection
40 # object. Nil is returned if there is only one page in total; no point in
41 # rendering the pagination in that case...
42 #
43 # ==== Options
44 # Display options:
45 # * <tt>:previous_label</tt> -- default: "« Previous" (this parameter is called <tt>:prev_label</tt> in versions <b>2.3.2</b> and older!)
46 # * <tt>:next_label</tt> -- default: "Next »"
47 # * <tt>:page_links</tt> -- when false, only previous/next links are rendered (default: true)
48 # * <tt>:inner_window</tt> -- how many links are shown around the current page (default: 4)
49 # * <tt>:outer_window</tt> -- how many links are around the first and the last page (default: 1)
50 # * <tt>:separator</tt> -- string separator for page HTML elements (default: single space)
51 #
52 # HTML options:
53 # * <tt>:class</tt> -- CSS class name for the generated DIV (default: "pagination")
54 # * <tt>:container</tt> -- toggles rendering of the DIV container for pagination links, set to
55 # false only when you are rendering your own pagination markup (default: true)
56 # * <tt>:id</tt> -- HTML ID for the container (default: nil). Pass +true+ to have the ID
57 # automatically generated from the class name of objects in collection: for example, paginating
58 # ArticleComment models would yield an ID of "article_comments_pagination".
59 #
60 # Advanced options:
61 # * <tt>:param_name</tt> -- parameter name for page number in URLs (default: <tt>:page</tt>)
62 # * <tt>:params</tt> -- additional parameters when generating pagination links
63 # (eg. <tt>:controller => "foo", :action => nil</tt>)
64 # * <tt>:renderer</tt> -- class name, class or instance of a link renderer (default:
65 # <tt>WillPaginate::LinkRenderer</tt>)
66 #
67 # All options not recognized by will_paginate will become HTML attributes on the container
68 # element for pagination links (the DIV). For example:
69 #
70 # <%= will_paginate @posts, :style => 'font-size: small' %>
71 #
72 # ... will result in:
73 #
74 # <div class="pagination" style="font-size: small"> ... </div>
75 #
76 # ==== Using the helper without arguments
77 # If the helper is called without passing in the collection object, it will
78 # try to read from the instance variable inferred by the controller name.
79 # For example, calling +will_paginate+ while the current controller is
80 # PostsController will result in trying to read from the <tt>@posts</tt>
81 # variable. Example:
82 #
83 # <%= will_paginate :id => true %>
84 #
85 # ... will result in <tt>@post</tt> collection getting paginated:
86 #
87 # <div class="pagination" id="posts_pagination"> ... </div>
88 #
89 def will_paginate(collection = nil, options = {})
90 options, collection = collection, nil if collection.is_a? Hash
91 unless collection or !controller
92 collection_name = "@#{controller.controller_name}"
93 collection = instance_variable_get(collection_name)
94 raise ArgumentError, "The #{collection_name} variable appears to be empty. Did you " +
95 "forget to pass the collection object for will_paginate?" unless collection
96 end
97 # early exit if there is nothing to render
98 return nil unless WillPaginate::ViewHelpers.total_pages_for_collection(collection) > 1
99
100 options = options.symbolize_keys.reverse_merge WillPaginate::ViewHelpers.pagination_options
101 if options[:prev_label]
102 WillPaginate::Deprecation::warn(":prev_label view parameter is now :previous_label; the old name has been deprecated", caller)
103 options[:previous_label] = options.delete(:prev_label)
104 end
105
106 # get the renderer instance
107 renderer = case options[:renderer]
108 when String
109 options[:renderer].to_s.constantize.new
110 when Class
111 options[:renderer].new
112 else
113 options[:renderer]
114 end
115 # render HTML for pagination
116 renderer.prepare collection, options, self
117 renderer.to_html
118 end
119
120 # Wrapper for rendering pagination links at both top and bottom of a block
121 # of content.
122 #
123 # <% paginated_section @posts do %>
124 # <ol id="posts">
125 # <% for post in @posts %>
126 # <li> ... </li>
127 # <% end %>
128 # </ol>
129 # <% end %>
130 #
131 # will result in:
132 #
133 # <div class="pagination"> ... </div>
134 # <ol id="posts">
135 # ...
136 # </ol>
137 # <div class="pagination"> ... </div>
138 #
139 # Arguments are passed to a <tt>will_paginate</tt> call, so the same options
140 # apply. Don't use the <tt>:id</tt> option; otherwise you'll finish with two
141 # blocks of pagination links sharing the same ID (which is invalid HTML).
142 def paginated_section(*args, &block)
143 pagination = will_paginate(*args).to_s
144
145 unless ActionView::Base.respond_to? :erb_variable
146 concat pagination
147 yield
148 concat pagination
149 else
150 content = pagination + capture(&block) + pagination
151 concat(content, block.binding)
152 end
153 end
154
155 # Renders a helpful message with numbers of displayed vs. total entries.
156 # You can use this as a blueprint for your own, similar helpers.
157 #
158 # <%= page_entries_info @posts %>
159 # #-> Displaying posts 6 - 10 of 26 in total
160 #
161 # By default, the message will use the humanized class name of objects
162 # in collection: for instance, "project types" for ProjectType models.
163 # Override this with the <tt>:entry_name</tt> parameter:
164 #
165 # <%= page_entries_info @posts, :entry_name => 'item' %>
166 # #-> Displaying items 6 - 10 of 26 in total
167 def page_entries_info(collection, options = {})
168 entry_name = options[:entry_name] ||
169 (collection.empty?? 'entry' : collection.first.class.name.underscore.sub('_', ' '))
170
171 if collection.total_pages < 2
172 case collection.size
173 when 0; "No #{entry_name.pluralize} found"
174 when 1; "Displaying <b>1</b> #{entry_name}"
175 else; "Displaying <b>all #{collection.size}</b> #{entry_name.pluralize}"
176 end
177 else
178 %{Displaying #{entry_name.pluralize} <b>%d&nbsp;-&nbsp;%d</b> of <b>%d</b> in total} % [
179 collection.offset + 1,
180 collection.offset + collection.length,
181 collection.total_entries
182 ]
183 end
184 end
185
186 def self.total_pages_for_collection(collection) #:nodoc:
187 if collection.respond_to?('page_count') and !collection.respond_to?('total_pages')
188 WillPaginate::Deprecation.warn %{
189 You are using a paginated collection of class #{collection.class.name}
190 which conforms to the old API of WillPaginate::Collection by using
191 `page_count`, while the current method name is `total_pages`. Please
192 upgrade yours or 3rd-party code that provides the paginated collection}, caller
193 class << collection
194 def total_pages; page_count; end
195 end
196 end
197 collection.total_pages
198 end
199 end
200
201 # This class does the heavy lifting of actually building the pagination
202 # links. It is used by the <tt>will_paginate</tt> helper internally.
203 class LinkRenderer
204
205 # The gap in page links is represented by:
206 #
207 # <span class="gap">&hellip;</span>
208 attr_accessor :gap_marker
209
210 def initialize
211 @gap_marker = '<span class="gap">&hellip;</span>'
212 end
213
214 # * +collection+ is a WillPaginate::Collection instance or any other object
215 # that conforms to that API
216 # * +options+ are forwarded from +will_paginate+ view helper
217 # * +template+ is the reference to the template being rendered
218 def prepare(collection, options, template)
219 @collection = collection
220 @options = options
221 @template = template
222
223 # reset values in case we're re-using this instance
224 @total_pages = @param_name = @url_string = nil
225 end
226
227 # Process it! This method returns the complete HTML string which contains
228 # pagination links. Feel free to subclass LinkRenderer and change this
229 # method as you see fit.
230 def to_html
231 links = @options[:page_links] ? windowed_links : []
232 # previous/next buttons
233 links.unshift page_link_or_span(@collection.previous_page, 'disabled prev_page', @options[:previous_label])
234 links.push page_link_or_span(@collection.next_page, 'disabled next_page', @options[:next_label])
235
236 html = links.join(@options[:separator])
237 @options[:container] ? @template.content_tag(:div, html, html_attributes) : html
238 end
239
240 # Returns the subset of +options+ this instance was initialized with that
241 # represent HTML attributes for the container element of pagination links.
242 def html_attributes
243 return @html_attributes if @html_attributes
244 @html_attributes = @options.except *(WillPaginate::ViewHelpers.pagination_options.keys - [:class])
245 # pagination of Post models will have the ID of "posts_pagination"
246 if @options[:container] and @options[:id] === true
247 @html_attributes[:id] = @collection.first.class.name.underscore.pluralize + '_pagination'
248 end
249 @html_attributes
250 end
251
252 protected
253
254 # Collects link items for visible page numbers.
255 def windowed_links
256 prev = nil
257
258 visible_page_numbers.inject [] do |links, n|
259 # detect gaps:
260 links << gap_marker if prev and n > prev + 1
261 links << page_link_or_span(n, 'current')
262 prev = n
263 links
264 end
265 end
266
267 # Calculates visible page numbers using the <tt>:inner_window</tt> and
268 # <tt>:outer_window</tt> options.
269 def visible_page_numbers
270 inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i
271 window_from = current_page - inner_window
272 window_to = current_page + inner_window
273
274 # adjust lower or upper limit if other is out of bounds
275 if window_to > total_pages
276 window_from -= window_to - total_pages
277 window_to = total_pages
278 end
279 if window_from < 1
280 window_to += 1 - window_from
281 window_from = 1
282 window_to = total_pages if window_to > total_pages
283 end
284
285 visible = (1..total_pages).to_a
286 left_gap = (2 + outer_window)...window_from
287 right_gap = (window_to + 1)...(total_pages - outer_window)
288 visible -= left_gap.to_a if left_gap.last - left_gap.first > 1
289 visible -= right_gap.to_a if right_gap.last - right_gap.first > 1
290
291 visible
292 end
293
294 def page_link_or_span(page, span_class, text = nil)
295 text ||= page.to_s
296
297 if page and page != current_page
298 classnames = span_class && span_class.index(' ') && span_class.split(' ', 2).last
299 page_link page, text, :rel => rel_value(page), :class => classnames
300 else
301 page_span page, text, :class => span_class
302 end
303 end
304
305 def page_link(page, text, attributes = {})
306 @template.link_to text, url_for(page), attributes
307 end
308
309 def page_span(page, text, attributes = {})
310 @template.content_tag :span, text, attributes
311 end
312
313 # Returns URL params for +page_link_or_span+, taking the current GET params
314 # and <tt>:params</tt> option into account.
315 def url_for(page)
316 page_one = page == 1
317 unless @url_string and !page_one
318 @url_params = {}
319 # page links should preserve GET parameters
320 stringified_merge @url_params, @template.params if @template.request.get?
321 stringified_merge @url_params, @options[:params] if @options[:params]
322
323 if complex = param_name.index(/[^\w-]/)
324 page_param = (defined?(CGIMethods) ? CGIMethods : ActionController::AbstractRequest).
325 parse_query_parameters("#{param_name}=#{page}")
326
327 stringified_merge @url_params, page_param
328 else
329 @url_params[param_name] = page_one ? 1 : 2
330 end
331
332 url = @template.url_for(@url_params)
333 return url if page_one
334
335 if complex
336 @url_string = url.sub(%r!((?:\?|&amp;)#{CGI.escape param_name}=)#{page}!, '\1@')
337 return url
338 else
339 @url_string = url
340 @url_params[param_name] = 3
341 @template.url_for(@url_params).split(//).each_with_index do |char, i|
342 if char == '3' and url[i, 1] == '2'
343 @url_string[i] = '@'
344 break
345 end
346 end
347 end
348 end
349 # finally!
350 @url_string.sub '@', page.to_s
351 end
352
353 private
354
355 def rel_value(page)
356 case page
357 when @collection.previous_page; 'prev' + (page == 1 ? ' start' : '')
358 when @collection.next_page; 'next'
359 when 1; 'start'
360 end
361 end
362
363 def current_page
364 @collection.current_page
365 end
366
367 def total_pages
368 @total_pages ||= WillPaginate::ViewHelpers.total_pages_for_collection(@collection)
369 end
370
371 def param_name
372 @param_name ||= @options[:param_name].to_s
373 end
374
375 # Recursively merge into target hash by using stringified keys from the other one
376 def stringified_merge(target, other)
377 other.each do |key, value|
378 key = key.to_s # this line is what it's all about!
379 existing = target[key]
380
381 if value.is_a?(Hash) and (existing.is_a?(Hash) or existing.nil?)
382 stringified_merge(existing || (target[key] = {}), value)
383 else
384 target[key] = value
385 end
386 end
387 end
388 end
389end