Description:
added pagination to user_admin, using will_paginate plugin
Commit status:
[Not Reviewed]
References:
Diff options:
Comments:
0 Commit comments
0 Inline Comments
Unresolved TODOs:
There are no unresolved TODOs
r299:3203cbaf7f2a - - 49 files changed: 3564 inserted, 4 deleted
@@ -0,0 +1,43 | |||||
|
|
1 | + CHANGELOG.rdoc | ||
|
|
2 | + LICENSE | ||
|
|
3 | + README.rdoc | ||
|
|
4 | + Rakefile | ||
|
|
5 | + examples/apple-circle.gif | ||
|
|
6 | + examples/index.haml | ||
|
|
7 | + examples/index.html | ||
|
|
8 | + examples/pagination.css | ||
|
|
9 | + examples/pagination.sass | ||
|
|
10 | + init.rb | ||
|
|
11 | + lib/will_paginate.rb | ||
|
|
12 | + lib/will_paginate/array.rb | ||
|
|
13 | + lib/will_paginate/collection.rb | ||
|
|
14 | + lib/will_paginate/core_ext.rb | ||
|
|
15 | + lib/will_paginate/finder.rb | ||
|
|
16 | + lib/will_paginate/named_scope.rb | ||
|
|
17 | + lib/will_paginate/named_scope_patch.rb | ||
|
|
18 | + lib/will_paginate/version.rb | ||
|
|
19 | + lib/will_paginate/view_helpers.rb | ||
|
|
20 | + test/boot.rb | ||
|
|
21 | + test/collection_test.rb | ||
|
|
22 | + test/console | ||
|
|
23 | + test/database.yml | ||
|
|
24 | + test/finder_test.rb | ||
|
|
25 | + test/fixtures/admin.rb | ||
|
|
26 | + test/fixtures/developer.rb | ||
|
|
27 | + test/fixtures/developers_projects.yml | ||
|
|
28 | + test/fixtures/project.rb | ||
|
|
29 | + test/fixtures/projects.yml | ||
|
|
30 | + test/fixtures/replies.yml | ||
|
|
31 | + test/fixtures/reply.rb | ||
|
|
32 | + test/fixtures/schema.rb | ||
|
|
33 | + test/fixtures/topic.rb | ||
|
|
34 | + test/fixtures/topics.yml | ||
|
|
35 | + test/fixtures/user.rb | ||
|
|
36 | + test/fixtures/users.yml | ||
|
|
37 | + test/helper.rb | ||
|
|
38 | + test/lib/activerecord_test_case.rb | ||
|
|
39 | + test/lib/activerecord_test_connector.rb | ||
|
|
40 | + test/lib/load_fixtures.rb | ||
|
|
41 | + test/lib/view_test_process.rb | ||
|
|
42 | + test/tasks.rake | ||
|
|
43 | + test/view_test.rb No newline at end of file |
@@ -0,0 +1,139 | |||||
|
|
1 | + = 2.3.12, released 2009-12-01 | ||
|
|
2 | + | ||
|
|
3 | + * make view helpers "HTML safe" for Rails 2.3.5 with rails_xss plugin | ||
|
|
4 | + | ||
|
|
5 | + = 2.3.11, released 2009-06-02 | ||
|
|
6 | + | ||
|
|
7 | + * fix `enable_actionpack` | ||
|
|
8 | + | ||
|
|
9 | + = 2.3.10, released 2009-05-21 | ||
|
|
10 | + | ||
|
|
11 | + * count_by_sql: don't use table alias with any adapters starting with "oracle" | ||
|
|
12 | + * Add back "AS count_table" alias to `paginate_by_sql` counter SQL | ||
|
|
13 | + | ||
|
|
14 | + = 2.3.9, released 2009-05-29 | ||
|
|
15 | + | ||
|
|
16 | + * remove "AS count_table" alias from `paginate_by_sql` counter SQL | ||
|
|
17 | + * Rails 2.3.2 compat: monkeypatch Rails issue #2189 (count breaks has_many :through) | ||
|
|
18 | + * fix generation of page URLs that contain the "@" character | ||
|
|
19 | + * check for method existance in a ruby 1.8- and 1.9-compatible way | ||
|
|
20 | + * load will_paginate view helpers even if ActiveRecord is not loaded | ||
|
|
21 | + | ||
|
|
22 | + == 2.3.8, released 2009-03-09 | ||
|
|
23 | + | ||
|
|
24 | + * Rails 2.3 compat: query parameter parsing with Rack | ||
|
|
25 | + | ||
|
|
26 | + == 2.3.7, released 2009-02-09 | ||
|
|
27 | + | ||
|
|
28 | + * Removed all unnecessary &block variables since they cause serious memory damage and lots of subsequent gc runs. | ||
|
|
29 | + | ||
|
|
30 | + == 2.3.6, released 2008-10-26 | ||
|
|
31 | + | ||
|
|
32 | + * Rails 2.2 fix: stop using `extract_attribute_names_from_match` inernal AR method, it no longer exists | ||
|
|
33 | + | ||
|
|
34 | + == 2.3.5, released 2008-10-07 | ||
|
|
35 | + | ||
|
|
36 | + * update the backported named_scope implementation for Rails versions older than 2.1 | ||
|
|
37 | + * break out of scope of paginated_each() yielded block when used on named scopes | ||
|
|
38 | + * fix paginate(:from) | ||
|
|
39 | + | ||
|
|
40 | + == 2.3.4, released 2008-09-16 | ||
|
|
41 | + | ||
|
|
42 | + * Removed gem dependency to Active Support (causes trouble with vendored rails). | ||
|
|
43 | + * Rails 2.1: fix a failing test and a deprecation warning. | ||
|
|
44 | + * Cope with scoped :select when counting. | ||
|
|
45 | + | ||
|
|
46 | + == 2.3.3, released 2008-08-29 | ||
|
|
47 | + | ||
|
|
48 | + * Ensure that paginate_by_sql doesn't change the original SQL query. | ||
|
|
49 | + * RDoc love (now live at http://gitrdoc.com/mislav/will_paginate/tree/master) | ||
|
|
50 | + * Rename :prev_label to :previous_label for consistency. old name still functions but is deprecated | ||
|
|
51 | + * ActiveRecord 2.1: Remove :include option from count_all query when it's possible. | ||
|
|
52 | + | ||
|
|
53 | + == 2.3.2, released 2008-05-16 | ||
|
|
54 | + | ||
|
|
55 | + * Fixed LinkRenderer#stringified_merge by removing "return" from iterator block | ||
|
|
56 | + * Ensure that 'href' values in pagination links are escaped URLs | ||
|
|
57 | + | ||
|
|
58 | + == 2.3.1, released 2008-05-04 | ||
|
|
59 | + | ||
|
|
60 | + * Fixed page numbers not showing with custom routes and implicit first page | ||
|
|
61 | + * Try to use Hanna for documentation (falls back to default RDoc template if not) | ||
|
|
62 | + | ||
|
|
63 | + == 2.3.0, released 2008-04-29 | ||
|
|
64 | + | ||
|
|
65 | + * Changed LinkRenderer to receive collection, options and reference to view template NOT in | ||
|
|
66 | + constructor, but with the #prepare method. This is a step towards supporting passing of | ||
|
|
67 | + LinkRenderer (or subclass) instances that may be preconfigured in some way | ||
|
|
68 | + * LinkRenderer now has #page_link and #page_span methods for easier customization of output in | ||
|
|
69 | + subclasses | ||
|
|
70 | + * Changed page_entries_info() method to adjust its output according to humanized class name of | ||
|
|
71 | + collection items. Override this with :entry_name parameter (singular). | ||
|
|
72 | + | ||
|
|
73 | + page_entries_info(@posts) | ||
|
|
74 | + #-> "Displaying all 12 posts" | ||
|
|
75 | + page_entries_info(@posts, :entry_name => 'item') | ||
|
|
76 | + #-> "Displaying all 12 items" | ||
|
|
77 | + | ||
|
|
78 | + == 2.2.3, released 2008-04-26 | ||
|
|
79 | + | ||
|
|
80 | + * will_paginate gem is no longer published on RubyForge, but on | ||
|
|
81 | + gems.github.com: | ||
|
|
82 | + | ||
|
|
83 | + gem sources -a http://gems.github.com/ (you only need to do this once) | ||
|
|
84 | + gem install mislav-will_paginate | ||
|
|
85 | + | ||
|
|
86 | + * extract reusable pagination testing stuff into WillPaginate::View | ||
|
|
87 | + * rethink the page URL construction mechanizm to be more bulletproof when | ||
|
|
88 | + combined with custom routing for page parameter | ||
|
|
89 | + * test that anchor parameter can be used in pagination links | ||
|
|
90 | + | ||
|
|
91 | + == 2.2.2, released 2008-04-21 | ||
|
|
92 | + | ||
|
|
93 | + * Add support for page parameter in custom routes like "/foo/page/2" | ||
|
|
94 | + * Change output of "page_entries_info" on single-page collection and erraneous | ||
|
|
95 | + output with empty collection as reported by Tim Chater | ||
|
|
96 | + | ||
|
|
97 | + == 2.2.1, released 2008-04-08 | ||
|
|
98 | + | ||
|
|
99 | + * take less risky path when monkeypatching named_scope; fix that it no longer | ||
|
|
100 | + requires ActiveRecord::VERSION | ||
|
|
101 | + * use strings in "respond_to?" calls to work around a bug in acts_as_ferret | ||
|
|
102 | + stable (ugh) | ||
|
|
103 | + * add rake release task | ||
|
|
104 | + | ||
|
|
105 | + | ||
|
|
106 | + == 2.2.0, released 2008-04-07 | ||
|
|
107 | + | ||
|
|
108 | + === API changes | ||
|
|
109 | + * Rename WillPaginate::Collection#page_count to "total_pages" for consistency. | ||
|
|
110 | + If you implemented this interface, change your implementation accordingly. | ||
|
|
111 | + * Remove old, deprecated style of calling Array#paginate as "paginate(page, | ||
|
|
112 | + per_page)". If you want to specify :page, :per_page or :total_entries, use a | ||
|
|
113 | + parameter hash. | ||
|
|
114 | + * Rename LinkRenderer#url_options to "url_for" and drastically optimize it | ||
|
|
115 | + | ||
|
|
116 | + === View changes | ||
|
|
117 | + * Added "prev_page" and "next_page" CSS classes on previous/next page buttons | ||
|
|
118 | + * Add examples of pagination links styling in "examples/index.html" | ||
|
|
119 | + * Change gap in pagination links from "..." to | ||
|
|
120 | + "<span class="gap">…</span>". | ||
|
|
121 | + * Add "paginated_section", a block helper that renders pagination both above and | ||
|
|
122 | + below content in the block | ||
|
|
123 | + * Add rel="prev|next|start" to page links | ||
|
|
124 | + | ||
|
|
125 | + === Other | ||
|
|
126 | + | ||
|
|
127 | + * Add ability to opt-in for Rails 2.1 feature "named_scope" by calling | ||
|
|
128 | + WillPaginate.enable_named_scope (tested in Rails 1.2.6 and 2.0.2) | ||
|
|
129 | + * Support complex page parameters like "developers[page]" | ||
|
|
130 | + * Move Array#paginate definition to will_paginate/array.rb. You can now easily | ||
|
|
131 | + use pagination on arrays outside of Rails: | ||
|
|
132 | + | ||
|
|
133 | + gem 'will_paginate' | ||
|
|
134 | + require 'will_paginate/array' | ||
|
|
135 | + | ||
|
|
136 | + * Add "paginated_each" method for iterating through every record by loading only | ||
|
|
137 | + one page of records at the time | ||
|
|
138 | + * Rails 2: Rescue from WillPaginate::InvalidPage error with 404 Not Found by | ||
|
|
139 | + default |
@@ -0,0 +1,18 | |||||
|
|
1 | + Copyright (c) 2007 PJ Hyett and Mislav Marohnić | ||
|
|
2 | + | ||
|
|
3 | + Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
|
|
4 | + this software and associated documentation files (the "Software"), to deal in | ||
|
|
5 | + the Software without restriction, including without limitation the rights to | ||
|
|
6 | + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | ||
|
|
7 | + the Software, and to permit persons to whom the Software is furnished to do so, | ||
|
|
8 | + subject to the following conditions: | ||
|
|
9 | + | ||
|
|
10 | + The above copyright notice and this permission notice shall be included in all | ||
|
|
11 | + copies or substantial portions of the Software. | ||
|
|
12 | + | ||
|
|
13 | + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
|
|
14 | + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | ||
|
|
15 | + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
|
|
16 | + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | ||
|
|
17 | + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | ||
|
|
18 | + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
@@ -0,0 +1,107 | |||||
|
|
1 | + = WillPaginate | ||
|
|
2 | + | ||
|
|
3 | + Pagination is just limiting the number of records displayed. Why should you let | ||
|
|
4 | + it get in your way while developing, then? This plugin makes magic happen. Did | ||
|
|
5 | + you ever want to be able to do just this on a model: | ||
|
|
6 | + | ||
|
|
7 | + Post.paginate :page => 1, :order => 'created_at DESC' | ||
|
|
8 | + | ||
|
|
9 | + ... and then render the page links with a single view helper? Well, now you | ||
|
|
10 | + can. | ||
|
|
11 | + | ||
|
|
12 | + Some resources to get you started: | ||
|
|
13 | + | ||
|
|
14 | + * {Installation instructions}[http://github.com/mislav/will_paginate/wikis/installation] | ||
|
|
15 | + on {the wiki}[http://github.com/mislav/will_paginate/wikis] | ||
|
|
16 | + * Your mind reels with questions? Join our | ||
|
|
17 | + {Google group}[http://groups.google.com/group/will_paginate]. | ||
|
|
18 | + * {How to report bugs}[http://github.com/mislav/will_paginate/wikis/report-bugs] | ||
|
|
19 | + | ||
|
|
20 | + | ||
|
|
21 | + == Example usage | ||
|
|
22 | + | ||
|
|
23 | + Use a paginate finder in the controller: | ||
|
|
24 | + | ||
|
|
25 | + @posts = Post.paginate_by_board_id @board.id, :page => params[:page], :order => 'updated_at DESC' | ||
|
|
26 | + | ||
|
|
27 | + Yeah, +paginate+ works just like +find+ -- it just doesn't fetch all the | ||
|
|
28 | + records. Don't forget to tell it which page you want, or it will complain! | ||
|
|
29 | + Read more on WillPaginate::Finder::ClassMethods. | ||
|
|
30 | + | ||
|
|
31 | + Render the posts in your view like you would normally do. When you need to render | ||
|
|
32 | + pagination, just stick this in: | ||
|
|
33 | + | ||
|
|
34 | + <%= will_paginate @posts %> | ||
|
|
35 | + | ||
|
|
36 | + You're done. (You can find the option list at WillPaginate::ViewHelpers.) | ||
|
|
37 | + | ||
|
|
38 | + How does it know how much items to fetch per page? It asks your model by calling | ||
|
|
39 | + its <tt>per_page</tt> class method. You can define it like this: | ||
|
|
40 | + | ||
|
|
41 | + class Post < ActiveRecord::Base | ||
|
|
42 | + cattr_reader :per_page | ||
|
|
43 | + @@per_page = 50 | ||
|
|
44 | + end | ||
|
|
45 | + | ||
|
|
46 | + ... or like this: | ||
|
|
47 | + | ||
|
|
48 | + class Post < ActiveRecord::Base | ||
|
|
49 | + def self.per_page | ||
|
|
50 | + 50 | ||
|
|
51 | + end | ||
|
|
52 | + end | ||
|
|
53 | + | ||
|
|
54 | + ... or don't worry about it at all. WillPaginate defines it to be <b>30</b> by default. | ||
|
|
55 | + But you can always specify the count explicitly when calling +paginate+: | ||
|
|
56 | + | ||
|
|
57 | + @posts = Post.paginate :page => params[:page], :per_page => 50 | ||
|
|
58 | + | ||
|
|
59 | + The +paginate+ finder wraps the original finder and returns your resultset that now has | ||
|
|
60 | + some new properties. You can use the collection as you would with any ActiveRecord | ||
|
|
61 | + resultset. WillPaginate view helpers also need that object to be able to render pagination: | ||
|
|
62 | + | ||
|
|
63 | + <ol> | ||
|
|
64 | + <% for post in @posts -%> | ||
|
|
65 | + <li>Render `post` in some nice way.</li> | ||
|
|
66 | + <% end -%> | ||
|
|
67 | + </ol> | ||
|
|
68 | + | ||
|
|
69 | + <p>Now let's render us some pagination!</p> | ||
|
|
70 | + <%= will_paginate @posts %> | ||
|
|
71 | + | ||
|
|
72 | + More detailed documentation: | ||
|
|
73 | + | ||
|
|
74 | + * WillPaginate::Finder::ClassMethods for pagination on your models; | ||
|
|
75 | + * WillPaginate::ViewHelpers for your views. | ||
|
|
76 | + | ||
|
|
77 | + | ||
|
|
78 | + == Authors and credits | ||
|
|
79 | + | ||
|
|
80 | + Authors:: Mislav Marohnić, PJ Hyett | ||
|
|
81 | + Original announcement:: http://errtheblog.com/post/929 | ||
|
|
82 | + Original PHP source:: http://www.strangerstudios.com/sandbox/pagination/diggstyle.php | ||
|
|
83 | + | ||
|
|
84 | + All these people helped making will_paginate what it is now with their code | ||
|
|
85 | + contributions or just simply awesome ideas: | ||
|
|
86 | + | ||
|
|
87 | + Chris Wanstrath, Dr. Nic Williams, K. Adam Christensen, Mike Garey, Bence | ||
|
|
88 | + Golda, Matt Aimonetti, Charles Brian Quinn, Desi McAdam, James Coglan, Matijs | ||
|
|
89 | + van Zuijlen, Maria, Brendan Ribera, Todd Willey, Bryan Helmkamp, Jan Berkel, | ||
|
|
90 | + Lourens Naudé, Rick Olson, Russell Norris, Piotr Usewicz, Chris Eppstein, | ||
|
|
91 | + Denis Barushev, Ben Pickles. | ||
|
|
92 | + | ||
|
|
93 | + | ||
|
|
94 | + == Usable pagination in the UI | ||
|
|
95 | + | ||
|
|
96 | + There are some CSS styles to get you started in the "examples/" directory. They | ||
|
|
97 | + are {showcased online here}[http://mislav.uniqpath.com/will_paginate/]. | ||
|
|
98 | + | ||
|
|
99 | + More reading about pagination as design pattern: | ||
|
|
100 | + | ||
|
|
101 | + * {Pagination 101}[http://kurafire.net/log/archive/2007/06/22/pagination-101] | ||
|
|
102 | + * {Pagination gallery}[http://www.smashingmagazine.com/2007/11/16/pagination-gallery-examples-and-good-practices/] | ||
|
|
103 | + * {Pagination on Yahoo Design Pattern Library}[http://developer.yahoo.com/ypatterns/parent.php?pattern=pagination] | ||
|
|
104 | + | ||
|
|
105 | + Want to discuss, request features, ask questions? Join the | ||
|
|
106 | + {Google group}[http://groups.google.com/group/will_paginate]. | ||
|
|
107 | + |
@@ -0,0 +1,53 | |||||
|
|
1 | + require 'rubygems' | ||
|
|
2 | + begin | ||
|
|
3 | + hanna_dir = '/Users/mislav/Projects/Hanna/lib' | ||
|
|
4 | + $:.unshift hanna_dir if File.exists? hanna_dir | ||
|
|
5 | + require 'hanna/rdoctask' | ||
|
|
6 | + rescue LoadError | ||
|
|
7 | + require 'rake' | ||
|
|
8 | + require 'rake/rdoctask' | ||
|
|
9 | + end | ||
|
|
10 | + load 'test/tasks.rake' | ||
|
|
11 | + | ||
|
|
12 | + desc 'Default: run unit tests.' | ||
|
|
13 | + task :default => :test | ||
|
|
14 | + | ||
|
|
15 | + desc 'Generate RDoc documentation for the will_paginate plugin.' | ||
|
|
16 | + Rake::RDocTask.new(:rdoc) do |rdoc| | ||
|
|
17 | + rdoc.rdoc_files.include('README.rdoc', 'LICENSE', 'CHANGELOG.rdoc'). | ||
|
|
18 | + include('lib/**/*.rb'). | ||
|
|
19 | + exclude('lib/will_paginate/named_scope*'). | ||
|
|
20 | + exclude('lib/will_paginate/array.rb'). | ||
|
|
21 | + exclude('lib/will_paginate/version.rb') | ||
|
|
22 | + | ||
|
|
23 | + rdoc.main = "README.rdoc" # page to start on | ||
|
|
24 | + rdoc.title = "will_paginate documentation" | ||
|
|
25 | + | ||
|
|
26 | + rdoc.rdoc_dir = 'doc' # rdoc output folder | ||
|
|
27 | + rdoc.options << '--inline-source' << '--charset=UTF-8' | ||
|
|
28 | + rdoc.options << '--webcvs=http://github.com/mislav/will_paginate/tree/master/' | ||
|
|
29 | + end | ||
|
|
30 | + | ||
|
|
31 | + desc %{Update ".manifest" with the latest list of project filenames. Respect\ | ||
|
|
32 | + .gitignore by excluding everything that git ignores. Update `files` and\ | ||
|
|
33 | + `test_files` arrays in "*.gemspec" file if it's present.} | ||
|
|
34 | + task :manifest do | ||
|
|
35 | + list = `git ls-files --full-name --exclude=*.gemspec --exclude=.*`.chomp.split("\n") | ||
|
|
36 | + | ||
|
|
37 | + if spec_file = Dir['*.gemspec'].first | ||
|
|
38 | + spec = File.read spec_file | ||
|
|
39 | + spec.gsub! /^(\s* s.(test_)?files \s* = \s* )( \[ [^\]]* \] | %w\( [^)]* \) )/mx do | ||
|
|
40 | + assignment = $1 | ||
|
|
41 | + bunch = $2 ? list.grep(/^test\//) : list | ||
|
|
42 | + '%s%%w(%s)' % [assignment, bunch.join(' ')] | ||
|
|
43 | + end | ||
|
|
44 | + | ||
|
|
45 | + File.open(spec_file, 'w') { |f| f << spec } | ||
|
|
46 | + end | ||
|
|
47 | + File.open('.manifest', 'w') { |f| f << list.join("\n") } | ||
|
|
48 | + end | ||
|
|
49 | + | ||
|
|
50 | + task :examples do | ||
|
|
51 | + %x(haml examples/index.haml examples/index.html) | ||
|
|
52 | + %x(sass examples/pagination.sass examples/pagination.css) | ||
|
|
53 | + end |
new file 100644 | |||||
binary diff hidden |
@@ -0,0 +1,69 | |||||
|
|
1 | + !!! | ||
|
|
2 | + %html | ||
|
|
3 | + %head | ||
|
|
4 | + %title Samples of pagination styling for will_paginate | ||
|
|
5 | + %link{ :rel => 'stylesheet', :type => 'text/css', :href => 'pagination.css' } | ||
|
|
6 | + %style{ :type => 'text/css' } | ||
|
|
7 | + :sass | ||
|
|
8 | + html | ||
|
|
9 | + :margin 0 | ||
|
|
10 | + :padding 0 | ||
|
|
11 | + :background #999 | ||
|
|
12 | + :font normal 76% "Lucida Grande", Verdana, Helvetica, sans-serif | ||
|
|
13 | + body | ||
|
|
14 | + :margin 2em | ||
|
|
15 | + :padding 2em | ||
|
|
16 | + :border 2px solid gray | ||
|
|
17 | + :background white | ||
|
|
18 | + :color #222 | ||
|
|
19 | + h1 | ||
|
|
20 | + :font-size 2em | ||
|
|
21 | + :font-weight normal | ||
|
|
22 | + :margin 0 0 1em 0 | ||
|
|
23 | + h2 | ||
|
|
24 | + :font-size 1.4em | ||
|
|
25 | + :margin 1em 0 .5em 0 | ||
|
|
26 | + pre | ||
|
|
27 | + :font-size 13px | ||
|
|
28 | + :font-family Monaco, "DejaVu Sans Mono", "Bitstream Vera Mono", "Courier New", monospace | ||
|
|
29 | + | ||
|
|
30 | + - pagination = '<span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a>' | ||
|
|
31 | + - pagination_no_page_links = '<span class="disabled prev_page">« Previous</span> <a href="./?page=2" rel="next" class="next_page">Next »</a>' | ||
|
|
32 | + | ||
|
|
33 | + %body | ||
|
|
34 | + %h1 Samples of pagination styling for will_paginate | ||
|
|
35 | + %p | ||
|
|
36 | + Find these styles in <b>"examples/pagination.css"</b> of <i>will_paginate</i> library. | ||
|
|
37 | + There is a Sass version of it for all you sassy people. | ||
|
|
38 | + %p | ||
|
|
39 | + Read about good rules for pagination: | ||
|
|
40 | + %a{ :href => 'http://kurafire.net/log/archive/2007/06/22/pagination-101' } Pagination 101 | ||
|
|
41 | + %p | ||
|
|
42 | + %em Warning: | ||
|
|
43 | + page links below don't lead anywhere (so don't click on them). | ||
|
|
44 | + | ||
|
|
45 | + %h2 Unstyled pagination <span style="font-weight:normal">(<i>ewww!</i>)</span> | ||
|
|
46 | + %div= pagination | ||
|
|
47 | + | ||
|
|
48 | + %h2 Digg.com | ||
|
|
49 | + .digg_pagination= pagination | ||
|
|
50 | + | ||
|
|
51 | + %h2 Digg-style, no page links | ||
|
|
52 | + .digg_pagination= pagination_no_page_links | ||
|
|
53 | + %p Code that renders this: | ||
|
|
54 | + %pre= '<code>%s</code>' % %[<%= will_paginate @posts, :page_links => false %>].gsub('<', '<').gsub('>', '>') | ||
|
|
55 | + | ||
|
|
56 | + %h2 Digg-style, extra content | ||
|
|
57 | + .digg_pagination | ||
|
|
58 | + .page_info Displaying entries <b>1 - 6</b> of <b>180</b> in total | ||
|
|
59 | + = pagination | ||
|
|
60 | + %p Code that renders this: | ||
|
|
61 | + %pre= '<code>%s</code>' % %[<div class="digg_pagination">\n <div clas="page_info">\n <%= page_entries_info @posts %>\n </div>\n <%= will_paginate @posts, :container => false %>\n</div>].gsub('<', '<').gsub('>', '>') | ||
|
|
62 | + | ||
|
|
63 | + %h2 Apple.com store | ||
|
|
64 | + .apple_pagination= pagination | ||
|
|
65 | + | ||
|
|
66 | + %h2 Flickr.com | ||
|
|
67 | + .flickr_pagination | ||
|
|
68 | + = pagination | ||
|
|
69 | + .page_info (118 photos) |
@@ -0,0 +1,92 | |||||
|
|
1 | + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | ||
|
|
2 | + <html> | ||
|
|
3 | + </html> | ||
|
|
4 | + <head> | ||
|
|
5 | + <title>Samples of pagination styling for will_paginate</title> | ||
|
|
6 | + <link href='pagination.css' rel='stylesheet' type='text/css' /> | ||
|
|
7 | + <style type='text/css'> | ||
|
|
8 | + html { | ||
|
|
9 | + margin: 0; | ||
|
|
10 | + padding: 0; | ||
|
|
11 | + background: #999; | ||
|
|
12 | + font: normal 76% "Lucida Grande", Verdana, Helvetica, sans-serif; } | ||
|
|
13 | + | ||
|
|
14 | + body { | ||
|
|
15 | + margin: 2em; | ||
|
|
16 | + padding: 2em; | ||
|
|
17 | + border: 2px solid gray; | ||
|
|
18 | + background: white; | ||
|
|
19 | + color: #222; } | ||
|
|
20 | + | ||
|
|
21 | + h1 { | ||
|
|
22 | + font-size: 2em; | ||
|
|
23 | + font-weight: normal; | ||
|
|
24 | + margin: 0 0 1em 0; } | ||
|
|
25 | + | ||
|
|
26 | + h2 { | ||
|
|
27 | + font-size: 1.4em; | ||
|
|
28 | + margin: 1em 0 .5em 0; } | ||
|
|
29 | + | ||
|
|
30 | + pre { | ||
|
|
31 | + font-size: 13px; | ||
|
|
32 | + font-family: Monaco, "DejaVu Sans Mono", "Bitstream Vera Mono", "Courier New", monospace; } | ||
|
|
33 | + </style> | ||
|
|
34 | + </head> | ||
|
|
35 | + <body> | ||
|
|
36 | + <h1>Samples of pagination styling for will_paginate</h1> | ||
|
|
37 | + <p> | ||
|
|
38 | + Find these styles in <b>"examples/pagination.css"</b> of <i>will_paginate</i> library. | ||
|
|
39 | + There is a Sass version of it for all you sassy people. | ||
|
|
40 | + </p> | ||
|
|
41 | + <p> | ||
|
|
42 | + Read about good rules for pagination: | ||
|
|
43 | + <a href='http://kurafire.net/log/archive/2007/06/22/pagination-101'>Pagination 101</a> | ||
|
|
44 | + </p> | ||
|
|
45 | + <p> | ||
|
|
46 | + <em>Warning:</em> | ||
|
|
47 | + page links below don't lead anywhere (so don't click on them). | ||
|
|
48 | + </p> | ||
|
|
49 | + <h2> | ||
|
|
50 | + Unstyled pagination <span style="font-weight:normal">(<i>ewww!</i>)</span> | ||
|
|
51 | + </h2> | ||
|
|
52 | + <div> | ||
|
|
53 | + <span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a> | ||
|
|
54 | + </div> | ||
|
|
55 | + <h2>Digg.com</h2> | ||
|
|
56 | + <div class='digg_pagination'> | ||
|
|
57 | + <span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a> | ||
|
|
58 | + </div> | ||
|
|
59 | + <h2>Digg-style, no page links</h2> | ||
|
|
60 | + <div class='digg_pagination'> | ||
|
|
61 | + <span class="disabled prev_page">« Previous</span> <a href="./?page=2" rel="next" class="next_page">Next »</a> | ||
|
|
62 | + </div> | ||
|
|
63 | + <p>Code that renders this:</p> | ||
|
|
64 | + <pre> | ||
|
|
65 | + <code><%= will_paginate @posts, :page_links => false %></code> | ||
|
|
66 | + </pre> | ||
|
|
67 | + <h2>Digg-style, extra content</h2> | ||
|
|
68 | + <div class='digg_pagination'> | ||
|
|
69 | + <div class='page_info'> | ||
|
|
70 | + Displaying entries <b>1 - 6</b> of <b>180</b> in total | ||
|
|
71 | + </div> | ||
|
|
72 | + <span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a> | ||
|
|
73 | + </div> | ||
|
|
74 | + <p>Code that renders this:</p> | ||
|
|
75 | + <pre> | ||
|
|
76 | + <code><div class="digg_pagination"> | ||
|
|
77 | + <div clas="page_info"> | ||
|
|
78 | + <%= page_entries_info @posts %> | ||
|
|
79 | + </div> | ||
|
|
80 | + <%= will_paginate @posts, :container => false %> | ||
|
|
81 | + </div></code> | ||
|
|
82 | + </pre> | ||
|
|
83 | + <h2>Apple.com store</h2> | ||
|
|
84 | + <div class='apple_pagination'> | ||
|
|
85 | + <span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a> | ||
|
|
86 | + </div> | ||
|
|
87 | + <h2>Flickr.com</h2> | ||
|
|
88 | + <div class='flickr_pagination'> | ||
|
|
89 | + <span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a> | ||
|
|
90 | + <div class='page_info'>(118 photos)</div> | ||
|
|
91 | + </div> | ||
|
|
92 | + </body> |
@@ -0,0 +1,90 | |||||
|
|
1 | + .digg_pagination { | ||
|
|
2 | + background: white; | ||
|
|
3 | + /* self-clearing method: */ } | ||
|
|
4 | + .digg_pagination a, .digg_pagination span { | ||
|
|
5 | + padding: .2em .5em; | ||
|
|
6 | + display: block; | ||
|
|
7 | + float: left; | ||
|
|
8 | + margin-right: 1px; } | ||
|
|
9 | + .digg_pagination span.disabled { | ||
|
|
10 | + color: #999; | ||
|
|
11 | + border: 1px solid #DDD; } | ||
|
|
12 | + .digg_pagination span.current { | ||
|
|
13 | + font-weight: bold; | ||
|
|
14 | + background: #2E6AB1; | ||
|
|
15 | + color: white; | ||
|
|
16 | + border: 1px solid #2E6AB1; } | ||
|
|
17 | + .digg_pagination a { | ||
|
|
18 | + text-decoration: none; | ||
|
|
19 | + color: #105CB6; | ||
|
|
20 | + border: 1px solid #9AAFE5; } | ||
|
|
21 | + .digg_pagination a:hover, .digg_pagination a:focus { | ||
|
|
22 | + color: #003; | ||
|
|
23 | + border-color: #003; } | ||
|
|
24 | + .digg_pagination .page_info { | ||
|
|
25 | + background: #2E6AB1; | ||
|
|
26 | + color: white; | ||
|
|
27 | + padding: .4em .6em; | ||
|
|
28 | + width: 22em; | ||
|
|
29 | + margin-bottom: .3em; | ||
|
|
30 | + text-align: center; } | ||
|
|
31 | + .digg_pagination .page_info b { | ||
|
|
32 | + color: #003; | ||
|
|
33 | + background: #6aa6ed; | ||
|
|
34 | + padding: .1em .25em; } | ||
|
|
35 | + .digg_pagination:after { | ||
|
|
36 | + content: "."; | ||
|
|
37 | + display: block; | ||
|
|
38 | + height: 0; | ||
|
|
39 | + clear: both; | ||
|
|
40 | + visibility: hidden; } | ||
|
|
41 | + * html .digg_pagination { | ||
|
|
42 | + height: 1%; } | ||
|
|
43 | + *:first-child+html .digg_pagination { | ||
|
|
44 | + overflow: hidden; } | ||
|
|
45 | + | ||
|
|
46 | + .apple_pagination { | ||
|
|
47 | + background: #F1F1F1; | ||
|
|
48 | + border: 1px solid #E5E5E5; | ||
|
|
49 | + text-align: center; | ||
|
|
50 | + padding: 1em; } | ||
|
|
51 | + .apple_pagination a, .apple_pagination span { | ||
|
|
52 | + padding: .2em .3em; } | ||
|
|
53 | + .apple_pagination span.disabled { | ||
|
|
54 | + color: #AAA; } | ||
|
|
55 | + .apple_pagination span.current { | ||
|
|
56 | + font-weight: bold; | ||
|
|
57 | + background: transparent url(apple-circle.gif) no-repeat 50% 50%; } | ||
|
|
58 | + .apple_pagination a { | ||
|
|
59 | + text-decoration: none; | ||
|
|
60 | + color: black; } | ||
|
|
61 | + .apple_pagination a:hover, .apple_pagination a:focus { | ||
|
|
62 | + text-decoration: underline; } | ||
|
|
63 | + | ||
|
|
64 | + .flickr_pagination { | ||
|
|
65 | + text-align: center; | ||
|
|
66 | + padding: .3em; } | ||
|
|
67 | + .flickr_pagination a, .flickr_pagination span { | ||
|
|
68 | + padding: .2em .5em; } | ||
|
|
69 | + .flickr_pagination span.disabled { | ||
|
|
70 | + color: #AAA; } | ||
|
|
71 | + .flickr_pagination span.current { | ||
|
|
72 | + font-weight: bold; | ||
|
|
73 | + color: #FF0084; } | ||
|
|
74 | + .flickr_pagination a { | ||
|
|
75 | + border: 1px solid #DDDDDD; | ||
|
|
76 | + color: #0063DC; | ||
|
|
77 | + text-decoration: none; } | ||
|
|
78 | + .flickr_pagination a:hover, .flickr_pagination a:focus { | ||
|
|
79 | + border-color: #003366; | ||
|
|
80 | + background: #0063DC; | ||
|
|
81 | + color: white; } | ||
|
|
82 | + .flickr_pagination .page_info { | ||
|
|
83 | + color: #aaa; | ||
|
|
84 | + padding-top: .8em; } | ||
|
|
85 | + .flickr_pagination .prev_page, .flickr_pagination .next_page { | ||
|
|
86 | + border-width: 2px; } | ||
|
|
87 | + .flickr_pagination .prev_page { | ||
|
|
88 | + margin-right: 1em; } | ||
|
|
89 | + .flickr_pagination .next_page { | ||
|
|
90 | + margin-left: 1em; } |
@@ -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 |
@@ -0,0 +1,1 | |||||
|
|
1 | + require 'will_paginate' |
@@ -0,0 +1,90 | |||||
|
|
1 | + require 'active_support' | ||
|
|
2 | + require 'will_paginate/core_ext' | ||
|
|
3 | + | ||
|
|
4 | + # = You *will* paginate! | ||
|
|
5 | + # | ||
|
|
6 | + # First read about WillPaginate::Finder::ClassMethods, then see | ||
|
|
7 | + # WillPaginate::ViewHelpers. The magical array you're handling in-between is | ||
|
|
8 | + # WillPaginate::Collection. | ||
|
|
9 | + # | ||
|
|
10 | + # Happy paginating! | ||
|
|
11 | + module WillPaginate | ||
|
|
12 | + class << self | ||
|
|
13 | + # shortcut for <tt>enable_actionpack</tt> and <tt>enable_activerecord</tt> combined | ||
|
|
14 | + def enable | ||
|
|
15 | + enable_actionpack | ||
|
|
16 | + enable_activerecord | ||
|
|
17 | + end | ||
|
|
18 | + | ||
|
|
19 | + # hooks WillPaginate::ViewHelpers into ActionView::Base | ||
|
|
20 | + def enable_actionpack | ||
|
|
21 | + return if ActionView::Base.instance_methods.include_method? :will_paginate | ||
|
|
22 | + require 'will_paginate/view_helpers' | ||
|
|
23 | + ActionView::Base.send :include, ViewHelpers | ||
|
|
24 | + | ||
|
|
25 | + if defined?(ActionController::Base) and ActionController::Base.respond_to? :rescue_responses | ||
|
|
26 | + ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found | ||
|
|
27 | + end | ||
|
|
28 | + end | ||
|
|
29 | + | ||
|
|
30 | + # hooks WillPaginate::Finder into ActiveRecord::Base and classes that deal | ||
|
|
31 | + # with associations | ||
|
|
32 | + def enable_activerecord | ||
|
|
33 | + return if ActiveRecord::Base.respond_to? :paginate | ||
|
|
34 | + require 'will_paginate/finder' | ||
|
|
35 | + ActiveRecord::Base.send :include, Finder | ||
|
|
36 | + | ||
|
|
37 | + # support pagination on associations | ||
|
|
38 | + a = ActiveRecord::Associations | ||
|
|
39 | + returning([ a::AssociationCollection ]) { |classes| | ||
|
|
40 | + # detect http://dev.rubyonrails.org/changeset/9230 | ||
|
|
41 | + unless a::HasManyThroughAssociation.superclass == a::HasManyAssociation | ||
|
|
42 | + classes << a::HasManyThroughAssociation | ||
|
|
43 | + end | ||
|
|
44 | + }.each do |klass| | ||
|
|
45 | + klass.send :include, Finder::ClassMethods | ||
|
|
46 | + klass.class_eval { alias_method_chain :method_missing, :paginate } | ||
|
|
47 | + end | ||
|
|
48 | + | ||
|
|
49 | + # monkeypatch Rails ticket #2189: "count breaks has_many :through" | ||
|
|
50 | + ActiveRecord::Base.class_eval do | ||
|
|
51 | + protected | ||
|
|
52 | + def self.construct_count_options_from_args(*args) | ||
|
|
53 | + result = super | ||
|
|
54 | + result[0] = '*' if result[0].is_a?(String) and result[0] =~ /\.\*$/ | ||
|
|
55 | + result | ||
|
|
56 | + end | ||
|
|
57 | + end | ||
|
|
58 | + end | ||
|
|
59 | + | ||
|
|
60 | + # Enable named_scope, a feature of Rails 2.1, even if you have older Rails | ||
|
|
61 | + # (tested on Rails 2.0.2 and 1.2.6). | ||
|
|
62 | + # | ||
|
|
63 | + # You can pass +false+ for +patch+ parameter to skip monkeypatching | ||
|
|
64 | + # *associations*. Use this if you feel that <tt>named_scope</tt> broke | ||
|
|
65 | + # has_many, has_many :through or has_and_belongs_to_many associations in | ||
|
|
66 | + # your app. By passing +false+, you can still use <tt>named_scope</tt> in | ||
|
|
67 | + # your models, but not through associations. | ||
|
|
68 | + def enable_named_scope(patch = true) | ||
|
|
69 | + return if defined? ActiveRecord::NamedScope | ||
|
|
70 | + require 'will_paginate/named_scope' | ||
|
|
71 | + require 'will_paginate/named_scope_patch' if patch | ||
|
|
72 | + | ||
|
|
73 | + ActiveRecord::Base.send :include, WillPaginate::NamedScope | ||
|
|
74 | + end | ||
|
|
75 | + end | ||
|
|
76 | + | ||
|
|
77 | + module Deprecation # :nodoc: | ||
|
|
78 | + extend ActiveSupport::Deprecation | ||
|
|
79 | + | ||
|
|
80 | + def self.warn(message, callstack = caller) | ||
|
|
81 | + message = 'WillPaginate: ' + message.strip.gsub(/\s+/, ' ') | ||
|
|
82 | + ActiveSupport::Deprecation.warn(message, callstack) | ||
|
|
83 | + end | ||
|
|
84 | + end | ||
|
|
85 | + end | ||
|
|
86 | + | ||
|
|
87 | + if defined? Rails | ||
|
|
88 | + WillPaginate.enable_activerecord if defined? ActiveRecord | ||
|
|
89 | + WillPaginate.enable_actionpack if defined? ActionController | ||
|
|
90 | + 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 |
@@ -0,0 +1,144 | |||||
|
|
1 | + module 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 this library and do: | ||
|
|
37 | + # | ||
|
|
38 | + # require 'will_paginate/collection' | ||
|
|
39 | + # # WillPaginate::Collection is now available for use | ||
|
|
40 | + class Collection < Array | ||
|
|
41 | + attr_reader :current_page, :per_page, :total_entries, :total_pages | ||
|
|
42 | + | ||
|
|
43 | + # Arguments to the constructor are the current page number, per-page limit | ||
|
|
44 | + # and the total number of entries. The last argument is optional because it | ||
|
|
45 | + # is best to do lazy counting; in other words, count *conditionally* after | ||
|
|
46 | + # populating the collection using the +replace+ method. | ||
|
|
47 | + def initialize(page, per_page, total = nil) | ||
|
|
48 | + @current_page = page.to_i | ||
|
|
49 | + raise InvalidPage.new(page, @current_page) if @current_page < 1 | ||
|
|
50 | + @per_page = per_page.to_i | ||
|
|
51 | + raise ArgumentError, "`per_page` setting cannot be less than 1 (#{@per_page} given)" if @per_page < 1 | ||
|
|
52 | + | ||
|
|
53 | + self.total_entries = total if total | ||
|
|
54 | + end | ||
|
|
55 | + | ||
|
|
56 | + # Just like +new+, but yields the object after instantiation and returns it | ||
|
|
57 | + # afterwards. This is very useful for manual pagination: | ||
|
|
58 | + # | ||
|
|
59 | + # @entries = WillPaginate::Collection.create(1, 10) do |pager| | ||
|
|
60 | + # result = Post.find(:all, :limit => pager.per_page, :offset => pager.offset) | ||
|
|
61 | + # # inject the result array into the paginated collection: | ||
|
|
62 | + # pager.replace(result) | ||
|
|
63 | + # | ||
|
|
64 | + # unless pager.total_entries | ||
|
|
65 | + # # the pager didn't manage to guess the total count, do it manually | ||
|
|
66 | + # pager.total_entries = Post.count | ||
|
|
67 | + # end | ||
|
|
68 | + # end | ||
|
|
69 | + # | ||
|
|
70 | + # The possibilities with this are endless. For another example, here is how | ||
|
|
71 | + # WillPaginate used to define pagination for Array instances: | ||
|
|
72 | + # | ||
|
|
73 | + # Array.class_eval do | ||
|
|
74 | + # def paginate(page = 1, per_page = 15) | ||
|
|
75 | + # WillPaginate::Collection.create(page, per_page, size) do |pager| | ||
|
|
76 | + # pager.replace self[pager.offset, pager.per_page].to_a | ||
|
|
77 | + # end | ||
|
|
78 | + # end | ||
|
|
79 | + # end | ||
|
|
80 | + # | ||
|
|
81 | + # The Array#paginate API has since then changed, but this still serves as a | ||
|
|
82 | + # fine example of WillPaginate::Collection usage. | ||
|
|
83 | + def self.create(page, per_page, total = nil) | ||
|
|
84 | + pager = new(page, per_page, total) | ||
|
|
85 | + yield pager | ||
|
|
86 | + pager | ||
|
|
87 | + end | ||
|
|
88 | + | ||
|
|
89 | + # Helper method that is true when someone tries to fetch a page with a | ||
|
|
90 | + # larger number than the last page. Can be used in combination with flashes | ||
|
|
91 | + # and redirecting. | ||
|
|
92 | + def out_of_bounds? | ||
|
|
93 | + current_page > total_pages | ||
|
|
94 | + end | ||
|
|
95 | + | ||
|
|
96 | + # Current offset of the paginated collection. If we're on the first page, | ||
|
|
97 | + # it is always 0. If we're on the 2nd page and there are 30 entries per page, | ||
|
|
98 | + # the offset is 30. This property is useful if you want to render ordinals | ||
|
|
99 | + # side by side with records in the view: simply start with offset + 1. | ||
|
|
100 | + def offset | ||
|
|
101 | + (current_page - 1) * per_page | ||
|
|
102 | + end | ||
|
|
103 | + | ||
|
|
104 | + # current_page - 1 or nil if there is no previous page | ||
|
|
105 | + def previous_page | ||
|
|
106 | + current_page > 1 ? (current_page - 1) : nil | ||
|
|
107 | + end | ||
|
|
108 | + | ||
|
|
109 | + # current_page + 1 or nil if there is no next page | ||
|
|
110 | + def next_page | ||
|
|
111 | + current_page < total_pages ? (current_page + 1) : nil | ||
|
|
112 | + end | ||
|
|
113 | + | ||
|
|
114 | + # sets the <tt>total_entries</tt> property and calculates <tt>total_pages</tt> | ||
|
|
115 | + def total_entries=(number) | ||
|
|
116 | + @total_entries = number.to_i | ||
|
|
117 | + @total_pages = (@total_entries / per_page.to_f).ceil | ||
|
|
118 | + end | ||
|
|
119 | + | ||
|
|
120 | + # This is a magic wrapper for the original Array#replace method. It serves | ||
|
|
121 | + # for populating the paginated collection after initialization. | ||
|
|
122 | + # | ||
|
|
123 | + # Why magic? Because it tries to guess the total number of entries judging | ||
|
|
124 | + # by the size of given array. If it is shorter than +per_page+ limit, then we | ||
|
|
125 | + # know we're on the last page. This trick is very useful for avoiding | ||
|
|
126 | + # unnecessary hits to the database to do the counting after we fetched the | ||
|
|
127 | + # data for the current page. | ||
|
|
128 | + # | ||
|
|
129 | + # However, after using +replace+ you should always test the value of | ||
|
|
130 | + # +total_entries+ and set it to a proper value if it's +nil+. See the example | ||
|
|
131 | + # in +create+. | ||
|
|
132 | + def replace(array) | ||
|
|
133 | + result = super | ||
|
|
134 | + | ||
|
|
135 | + # The collection is shorter then page limit? Rejoice, because | ||
|
|
136 | + # then we know that we are on the last page! | ||
|
|
137 | + if total_entries.nil? and length < per_page and (current_page == 1 or length > 0) | ||
|
|
138 | + self.total_entries = offset + length | ||
|
|
139 | + end | ||
|
|
140 | + | ||
|
|
141 | + result | ||
|
|
142 | + end | ||
|
|
143 | + end | ||
|
|
144 | + end |
@@ -0,0 +1,43 | |||||
|
|
1 | + require 'set' | ||
|
|
2 | + require 'will_paginate/array' | ||
|
|
3 | + | ||
|
|
4 | + # helper to check for method existance in ruby 1.8- and 1.9-compatible way | ||
|
|
5 | + # because `methods`, `instance_methods` and others return strings in 1.8 and symbols in 1.9 | ||
|
|
6 | + # | ||
|
|
7 | + # ['foo', 'bar'].include_method?(:foo) # => true | ||
|
|
8 | + class Array | ||
|
|
9 | + def include_method?(name) | ||
|
|
10 | + name = name.to_sym | ||
|
|
11 | + !!(find { |item| item.to_sym == name }) | ||
|
|
12 | + end | ||
|
|
13 | + end | ||
|
|
14 | + | ||
|
|
15 | + unless Hash.instance_methods.include_method? :except | ||
|
|
16 | + Hash.class_eval do | ||
|
|
17 | + # Returns a new hash without the given keys. | ||
|
|
18 | + def except(*keys) | ||
|
|
19 | + rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys) | ||
|
|
20 | + reject { |key,| rejected.include?(key) } | ||
|
|
21 | + end | ||
|
|
22 | + | ||
|
|
23 | + # Replaces the hash without only the given keys. | ||
|
|
24 | + def except!(*keys) | ||
|
|
25 | + replace(except(*keys)) | ||
|
|
26 | + end | ||
|
|
27 | + end | ||
|
|
28 | + end | ||
|
|
29 | + | ||
|
|
30 | + unless Hash.instance_methods.include_method? :slice | ||
|
|
31 | + Hash.class_eval do | ||
|
|
32 | + # Returns a new hash with only the given keys. | ||
|
|
33 | + def slice(*keys) | ||
|
|
34 | + allowed = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys) | ||
|
|
35 | + reject { |key,| !allowed.include?(key) } | ||
|
|
36 | + end | ||
|
|
37 | + | ||
|
|
38 | + # Replaces the hash with only the given keys. | ||
|
|
39 | + def slice!(*keys) | ||
|
|
40 | + replace(slice(*keys)) | ||
|
|
41 | + end | ||
|
|
42 | + end | ||
|
|
43 | + end |
@@ -0,0 +1,264 | |||||
|
|
1 | + require 'will_paginate/core_ext' | ||
|
|
2 | + | ||
|
|
3 | + module 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 self.connection.adapter_name =~ /^(oracle|oci$)/i | ||
|
|
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_method?(: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 | ||
|
|
264 | + end |
@@ -0,0 +1,170 | |||||
|
|
1 | + module 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 | ||
|
|
170 | + end |
@@ -0,0 +1,37 | |||||
|
|
1 | + ActiveRecord::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 | ||
|
|
6 | + end | ||
|
|
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 | ||
|
|
21 | + end | ||
|
|
22 | + | ||
|
|
23 | + # Rails 1.2.6 | ||
|
|
24 | + ActiveRecord::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 | ||
|
|
37 | + end if ActiveRecord::Base.respond_to? :find_first |
@@ -0,0 +1,9 | |||||
|
|
1 | + module WillPaginate | ||
|
|
2 | + module VERSION | ||
|
|
3 | + MAJOR = 2 | ||
|
|
4 | + MINOR = 3 | ||
|
|
5 | + TINY = 12 | ||
|
|
6 | + | ||
|
|
7 | + STRING = [MAJOR, MINOR, TINY].join('.') | ||
|
|
8 | + end | ||
|
|
9 | + end |
@@ -0,0 +1,408 | |||||
|
|
1 | + require 'will_paginate/core_ext' | ||
|
|
2 | + | ||
|
|
3 | + module 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 => '« Previous', | ||
|
|
27 | + :next_label => 'Next »', | ||
|
|
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 - %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 | + if respond_to? :safe_helper | ||
|
|
187 | + safe_helper :will_paginate, :paginated_section, :page_entries_info | ||
|
|
188 | + end | ||
|
|
189 | + | ||
|
|
190 | + def self.total_pages_for_collection(collection) #:nodoc: | ||
|
|
191 | + if collection.respond_to?('page_count') and !collection.respond_to?('total_pages') | ||
|
|
192 | + WillPaginate::Deprecation.warn %{ | ||
|
|
193 | + You are using a paginated collection of class #{collection.class.name} | ||
|
|
194 | + which conforms to the old API of WillPaginate::Collection by using | ||
|
|
195 | + `page_count`, while the current method name is `total_pages`. Please | ||
|
|
196 | + upgrade yours or 3rd-party code that provides the paginated collection}, caller | ||
|
|
197 | + class << collection | ||
|
|
198 | + def total_pages; page_count; end | ||
|
|
199 | + end | ||
|
|
200 | + end | ||
|
|
201 | + collection.total_pages | ||
|
|
202 | + end | ||
|
|
203 | + end | ||
|
|
204 | + | ||
|
|
205 | + # This class does the heavy lifting of actually building the pagination | ||
|
|
206 | + # links. It is used by the <tt>will_paginate</tt> helper internally. | ||
|
|
207 | + class LinkRenderer | ||
|
|
208 | + | ||
|
|
209 | + # The gap in page links is represented by: | ||
|
|
210 | + # | ||
|
|
211 | + # <span class="gap">…</span> | ||
|
|
212 | + attr_accessor :gap_marker | ||
|
|
213 | + | ||
|
|
214 | + def initialize | ||
|
|
215 | + @gap_marker = '<span class="gap">…</span>' | ||
|
|
216 | + end | ||
|
|
217 | + | ||
|
|
218 | + # * +collection+ is a WillPaginate::Collection instance or any other object | ||
|
|
219 | + # that conforms to that API | ||
|
|
220 | + # * +options+ are forwarded from +will_paginate+ view helper | ||
|
|
221 | + # * +template+ is the reference to the template being rendered | ||
|
|
222 | + def prepare(collection, options, template) | ||
|
|
223 | + @collection = collection | ||
|
|
224 | + @options = options | ||
|
|
225 | + @template = template | ||
|
|
226 | + | ||
|
|
227 | + # reset values in case we're re-using this instance | ||
|
|
228 | + @total_pages = @param_name = @url_string = nil | ||
|
|
229 | + end | ||
|
|
230 | + | ||
|
|
231 | + # Process it! This method returns the complete HTML string which contains | ||
|
|
232 | + # pagination links. Feel free to subclass LinkRenderer and change this | ||
|
|
233 | + # method as you see fit. | ||
|
|
234 | + def to_html | ||
|
|
235 | + links = @options[:page_links] ? windowed_links : [] | ||
|
|
236 | + # previous/next buttons | ||
|
|
237 | + links.unshift page_link_or_span(@collection.previous_page, 'disabled prev_page', @options[:previous_label]) | ||
|
|
238 | + links.push page_link_or_span(@collection.next_page, 'disabled next_page', @options[:next_label]) | ||
|
|
239 | + | ||
|
|
240 | + html = links.join(@options[:separator]) | ||
|
|
241 | + @options[:container] ? @template.content_tag(:div, html, html_attributes) : html | ||
|
|
242 | + end | ||
|
|
243 | + | ||
|
|
244 | + # Returns the subset of +options+ this instance was initialized with that | ||
|
|
245 | + # represent HTML attributes for the container element of pagination links. | ||
|
|
246 | + def html_attributes | ||
|
|
247 | + return @html_attributes if @html_attributes | ||
|
|
248 | + @html_attributes = @options.except *(WillPaginate::ViewHelpers.pagination_options.keys - [:class]) | ||
|
|
249 | + # pagination of Post models will have the ID of "posts_pagination" | ||
|
|
250 | + if @options[:container] and @options[:id] === true | ||
|
|
251 | + @html_attributes[:id] = @collection.first.class.name.underscore.pluralize + '_pagination' | ||
|
|
252 | + end | ||
|
|
253 | + @html_attributes | ||
|
|
254 | + end | ||
|
|
255 | + | ||
|
|
256 | + protected | ||
|
|
257 | + | ||
|
|
258 | + # Collects link items for visible page numbers. | ||
|
|
259 | + def windowed_links | ||
|
|
260 | + prev = nil | ||
|
|
261 | + | ||
|
|
262 | + visible_page_numbers.inject [] do |links, n| | ||
|
|
263 | + # detect gaps: | ||
|
|
264 | + links << gap_marker if prev and n > prev + 1 | ||
|
|
265 | + links << page_link_or_span(n, 'current') | ||
|
|
266 | + prev = n | ||
|
|
267 | + links | ||
|
|
268 | + end | ||
|
|
269 | + end | ||
|
|
270 | + | ||
|
|
271 | + # Calculates visible page numbers using the <tt>:inner_window</tt> and | ||
|
|
272 | + # <tt>:outer_window</tt> options. | ||
|
|
273 | + def visible_page_numbers | ||
|
|
274 | + inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i | ||
|
|
275 | + window_from = current_page - inner_window | ||
|
|
276 | + window_to = current_page + inner_window | ||
|
|
277 | + | ||
|
|
278 | + # adjust lower or upper limit if other is out of bounds | ||
|
|
279 | + if window_to > total_pages | ||
|
|
280 | + window_from -= window_to - total_pages | ||
|
|
281 | + window_to = total_pages | ||
|
|
282 | + end | ||
|
|
283 | + if window_from < 1 | ||
|
|
284 | + window_to += 1 - window_from | ||
|
|
285 | + window_from = 1 | ||
|
|
286 | + window_to = total_pages if window_to > total_pages | ||
|
|
287 | + end | ||
|
|
288 | + | ||
|
|
289 | + visible = (1..total_pages).to_a | ||
|
|
290 | + left_gap = (2 + outer_window)...window_from | ||
|
|
291 | + right_gap = (window_to + 1)...(total_pages - outer_window) | ||
|
|
292 | + visible -= left_gap.to_a if left_gap.last - left_gap.first > 1 | ||
|
|
293 | + visible -= right_gap.to_a if right_gap.last - right_gap.first > 1 | ||
|
|
294 | + | ||
|
|
295 | + visible | ||
|
|
296 | + end | ||
|
|
297 | + | ||
|
|
298 | + def page_link_or_span(page, span_class, text = nil) | ||
|
|
299 | + text ||= page.to_s | ||
|
|
300 | + | ||
|
|
301 | + if page and page != current_page | ||
|
|
302 | + classnames = span_class && span_class.index(' ') && span_class.split(' ', 2).last | ||
|
|
303 | + page_link page, text, :rel => rel_value(page), :class => classnames | ||
|
|
304 | + else | ||
|
|
305 | + page_span page, text, :class => span_class | ||
|
|
306 | + end | ||
|
|
307 | + end | ||
|
|
308 | + | ||
|
|
309 | + def page_link(page, text, attributes = {}) | ||
|
|
310 | + @template.link_to text, url_for(page), attributes | ||
|
|
311 | + end | ||
|
|
312 | + | ||
|
|
313 | + def page_span(page, text, attributes = {}) | ||
|
|
314 | + @template.content_tag :span, text, attributes | ||
|
|
315 | + end | ||
|
|
316 | + | ||
|
|
317 | + # Returns URL params for +page_link_or_span+, taking the current GET params | ||
|
|
318 | + # and <tt>:params</tt> option into account. | ||
|
|
319 | + def url_for(page) | ||
|
|
320 | + page_one = page == 1 | ||
|
|
321 | + unless @url_string and !page_one | ||
|
|
322 | + @url_params = {} | ||
|
|
323 | + # page links should preserve GET parameters | ||
|
|
324 | + stringified_merge @url_params, @template.params if @template.request.get? | ||
|
|
325 | + stringified_merge @url_params, @options[:params] if @options[:params] | ||
|
|
326 | + | ||
|
|
327 | + if complex = param_name.index(/[^\w-]/) | ||
|
|
328 | + page_param = parse_query_parameters("#{param_name}=#{page}") | ||
|
|
329 | + | ||
|
|
330 | + stringified_merge @url_params, page_param | ||
|
|
331 | + else | ||
|
|
332 | + @url_params[param_name] = page_one ? 1 : 2 | ||
|
|
333 | + end | ||
|
|
334 | + | ||
|
|
335 | + url = @template.url_for(@url_params) | ||
|
|
336 | + return url if page_one | ||
|
|
337 | + | ||
|
|
338 | + if complex | ||
|
|
339 | + @url_string = url.sub(%r!((?:\?|&)#{CGI.escape param_name}=)#{page}!, "\\1\0") | ||
|
|
340 | + return url | ||
|
|
341 | + else | ||
|
|
342 | + @url_string = url | ||
|
|
343 | + @url_params[param_name] = 3 | ||
|
|
344 | + @template.url_for(@url_params).split(//).each_with_index do |char, i| | ||
|
|
345 | + if char == '3' and url[i, 1] == '2' | ||
|
|
346 | + @url_string[i] = "\0" | ||
|
|
347 | + break | ||
|
|
348 | + end | ||
|
|
349 | + end | ||
|
|
350 | + end | ||
|
|
351 | + end | ||
|
|
352 | + # finally! | ||
|
|
353 | + @url_string.sub "\0", page.to_s | ||
|
|
354 | + end | ||
|
|
355 | + | ||
|
|
356 | + private | ||
|
|
357 | + | ||
|
|
358 | + def rel_value(page) | ||
|
|
359 | + case page | ||
|
|
360 | + when @collection.previous_page; 'prev' + (page == 1 ? ' start' : '') | ||
|
|
361 | + when @collection.next_page; 'next' | ||
|
|
362 | + when 1; 'start' | ||
|
|
363 | + end | ||
|
|
364 | + end | ||
|
|
365 | + | ||
|
|
366 | + def current_page | ||
|
|
367 | + @collection.current_page | ||
|
|
368 | + end | ||
|
|
369 | + | ||
|
|
370 | + def total_pages | ||
|
|
371 | + @total_pages ||= WillPaginate::ViewHelpers.total_pages_for_collection(@collection) | ||
|
|
372 | + end | ||
|
|
373 | + | ||
|
|
374 | + def param_name | ||
|
|
375 | + @param_name ||= @options[:param_name].to_s | ||
|
|
376 | + end | ||
|
|
377 | + | ||
|
|
378 | + # Recursively merge into target hash by using stringified keys from the other one | ||
|
|
379 | + def stringified_merge(target, other) | ||
|
|
380 | + other.each do |key, value| | ||
|
|
381 | + key = key.to_s # this line is what it's all about! | ||
|
|
382 | + existing = target[key] | ||
|
|
383 | + | ||
|
|
384 | + if value.is_a?(Hash) and (existing.is_a?(Hash) or existing.nil?) | ||
|
|
385 | + stringified_merge(existing || (target[key] = {}), value) | ||
|
|
386 | + else | ||
|
|
387 | + target[key] = value | ||
|
|
388 | + end | ||
|
|
389 | + end | ||
|
|
390 | + end | ||
|
|
391 | + | ||
|
|
392 | + def parse_query_parameters(params) | ||
|
|
393 | + if defined? Rack::Utils | ||
|
|
394 | + # For Rails > 2.3 | ||
|
|
395 | + Rack::Utils.parse_nested_query(params) | ||
|
|
396 | + elsif defined?(ActionController::AbstractRequest) | ||
|
|
397 | + ActionController::AbstractRequest.parse_query_parameters(params) | ||
|
|
398 | + elsif defined?(ActionController::UrlEncodedPairParser) | ||
|
|
399 | + # For Rails > 2.2 | ||
|
|
400 | + ActionController::UrlEncodedPairParser.parse_query_parameters(params) | ||
|
|
401 | + elsif defined?(CGIMethods) | ||
|
|
402 | + CGIMethods.parse_query_parameters(params) | ||
|
|
403 | + else | ||
|
|
404 | + raise "unsupported ActionPack version" | ||
|
|
405 | + end | ||
|
|
406 | + end | ||
|
|
407 | + end | ||
|
|
408 | + end |
@@ -0,0 +1,21 | |||||
|
|
1 | + plugin_root = File.join(File.dirname(__FILE__), '..') | ||
|
|
2 | + version = ENV['RAILS_VERSION'] | ||
|
|
3 | + version = nil if version and version == "" | ||
|
|
4 | + | ||
|
|
5 | + # first look for a symlink to a copy of the framework | ||
|
|
6 | + if !version and framework_root = ["#{plugin_root}/rails", "#{plugin_root}/../../rails"].find { |p| File.directory? p } | ||
|
|
7 | + puts "found framework root: #{framework_root}" | ||
|
|
8 | + # this allows for a plugin to be tested outside of an app and without Rails gems | ||
|
|
9 | + $:.unshift "#{framework_root}/activesupport/lib", "#{framework_root}/activerecord/lib", "#{framework_root}/actionpack/lib" | ||
|
|
10 | + else | ||
|
|
11 | + # simply use installed gems if available | ||
|
|
12 | + puts "using Rails#{version ? ' ' + version : nil} gems" | ||
|
|
13 | + require 'rubygems' | ||
|
|
14 | + | ||
|
|
15 | + if version | ||
|
|
16 | + gem 'rails', version | ||
|
|
17 | + else | ||
|
|
18 | + gem 'actionpack' | ||
|
|
19 | + gem 'activerecord' | ||
|
|
20 | + end | ||
|
|
21 | + end |
@@ -0,0 +1,143 | |||||
|
|
1 | + require 'helper' | ||
|
|
2 | + require 'will_paginate/array' | ||
|
|
3 | + | ||
|
|
4 | + class ArrayPaginationTest < Test::Unit::TestCase | ||
|
|
5 | + | ||
|
|
6 | + def setup ; end | ||
|
|
7 | + | ||
|
|
8 | + def test_simple | ||
|
|
9 | + collection = ('a'..'e').to_a | ||
|
|
10 | + | ||
|
|
11 | + [{ :page => 1, :per_page => 3, :expected => %w( a b c ) }, | ||
|
|
12 | + { :page => 2, :per_page => 3, :expected => %w( d e ) }, | ||
|
|
13 | + { :page => 1, :per_page => 5, :expected => %w( a b c d e ) }, | ||
|
|
14 | + { :page => 3, :per_page => 5, :expected => [] }, | ||
|
|
15 | + ]. | ||
|
|
16 | + each do |conditions| | ||
|
|
17 | + expected = conditions.delete :expected | ||
|
|
18 | + assert_equal expected, collection.paginate(conditions) | ||
|
|
19 | + end | ||
|
|
20 | + end | ||
|
|
21 | + | ||
|
|
22 | + def test_defaults | ||
|
|
23 | + result = (1..50).to_a.paginate | ||
|
|
24 | + assert_equal 1, result.current_page | ||
|
|
25 | + assert_equal 30, result.size | ||
|
|
26 | + end | ||
|
|
27 | + | ||
|
|
28 | + def test_deprecated_api | ||
|
|
29 | + assert_raise(ArgumentError) { [].paginate(2) } | ||
|
|
30 | + assert_raise(ArgumentError) { [].paginate(2, 10) } | ||
|
|
31 | + end | ||
|
|
32 | + | ||
|
|
33 | + def test_total_entries_has_precedence | ||
|
|
34 | + result = %w(a b c).paginate :total_entries => 5 | ||
|
|
35 | + assert_equal 5, result.total_entries | ||
|
|
36 | + end | ||
|
|
37 | + | ||
|
|
38 | + def test_argument_error_with_params_and_another_argument | ||
|
|
39 | + assert_raise ArgumentError do | ||
|
|
40 | + [].paginate({}, 5) | ||
|
|
41 | + end | ||
|
|
42 | + end | ||
|
|
43 | + | ||
|
|
44 | + def test_paginated_collection | ||
|
|
45 | + entries = %w(a b c) | ||
|
|
46 | + collection = create(2, 3, 10) do |pager| | ||
|
|
47 | + assert_equal entries, pager.replace(entries) | ||
|
|
48 | + end | ||
|
|
49 | + | ||
|
|
50 | + assert_equal entries, collection | ||
|
|
51 | + assert_respond_to_all collection, %w(total_pages each offset size current_page per_page total_entries) | ||
|
|
52 | + assert_kind_of Array, collection | ||
|
|
53 | + assert_instance_of Array, collection.entries | ||
|
|
54 | + assert_equal 3, collection.offset | ||
|
|
55 | + assert_equal 4, collection.total_pages | ||
|
|
56 | + assert !collection.out_of_bounds? | ||
|
|
57 | + end | ||
|
|
58 | + | ||
|
|
59 | + def test_previous_next_pages | ||
|
|
60 | + collection = create(1, 1, 3) | ||
|
|
61 | + assert_nil collection.previous_page | ||
|
|
62 | + assert_equal 2, collection.next_page | ||
|
|
63 | + | ||
|
|
64 | + collection = create(2, 1, 3) | ||
|
|
65 | + assert_equal 1, collection.previous_page | ||
|
|
66 | + assert_equal 3, collection.next_page | ||
|
|
67 | + | ||
|
|
68 | + collection = create(3, 1, 3) | ||
|
|
69 | + assert_equal 2, collection.previous_page | ||
|
|
70 | + assert_nil collection.next_page | ||
|
|
71 | + end | ||
|
|
72 | + | ||
|
|
73 | + def test_out_of_bounds | ||
|
|
74 | + entries = create(2, 3, 2){} | ||
|
|
75 | + assert entries.out_of_bounds? | ||
|
|
76 | + | ||
|
|
77 | + entries = create(1, 3, 2){} | ||
|
|
78 | + assert !entries.out_of_bounds? | ||
|
|
79 | + end | ||
|
|
80 | + | ||
|
|
81 | + def test_guessing_total_count | ||
|
|
82 | + entries = create do |pager| | ||
|
|
83 | + # collection is shorter than limit | ||
|
|
84 | + pager.replace array | ||
|
|
85 | + end | ||
|
|
86 | + assert_equal 8, entries.total_entries | ||
|
|
87 | + | ||
|
|
88 | + entries = create(2, 5, 10) do |pager| | ||
|
|
89 | + # collection is shorter than limit, but we have an explicit count | ||
|
|
90 | + pager.replace array | ||
|
|
91 | + end | ||
|
|
92 | + assert_equal 10, entries.total_entries | ||
|
|
93 | + | ||
|
|
94 | + entries = create do |pager| | ||
|
|
95 | + # collection is the same as limit; we can't guess | ||
|
|
96 | + pager.replace array(5) | ||
|
|
97 | + end | ||
|
|
98 | + assert_equal nil, entries.total_entries | ||
|
|
99 | + | ||
|
|
100 | + entries = create do |pager| | ||
|
|
101 | + # collection is empty; we can't guess | ||
|
|
102 | + pager.replace array(0) | ||
|
|
103 | + end | ||
|
|
104 | + assert_equal nil, entries.total_entries | ||
|
|
105 | + | ||
|
|
106 | + entries = create(1) do |pager| | ||
|
|
107 | + # collection is empty and we're on page 1, | ||
|
|
108 | + # so the whole thing must be empty, too | ||
|
|
109 | + pager.replace array(0) | ||
|
|
110 | + end | ||
|
|
111 | + assert_equal 0, entries.total_entries | ||
|
|
112 | + end | ||
|
|
113 | + | ||
|
|
114 | + def test_invalid_page | ||
|
|
115 | + bad_inputs = [0, -1, nil, '', 'Schnitzel'] | ||
|
|
116 | + | ||
|
|
117 | + bad_inputs.each do |bad| | ||
|
|
118 | + assert_raise(WillPaginate::InvalidPage) { create bad } | ||
|
|
119 | + end | ||
|
|
120 | + end | ||
|
|
121 | + | ||
|
|
122 | + def test_invalid_per_page_setting | ||
|
|
123 | + assert_raise(ArgumentError) { create(1, -1) } | ||
|
|
124 | + end | ||
|
|
125 | + | ||
|
|
126 | + def test_page_count_was_removed | ||
|
|
127 | + assert_raise(NoMethodError) { create.page_count } | ||
|
|
128 | + # It's `total_pages` now. | ||
|
|
129 | + end | ||
|
|
130 | + | ||
|
|
131 | + private | ||
|
|
132 | + def create(page = 2, limit = 5, total = nil, &block) | ||
|
|
133 | + if block_given? | ||
|
|
134 | + WillPaginate::Collection.create(page, limit, total, &block) | ||
|
|
135 | + else | ||
|
|
136 | + WillPaginate::Collection.new(page, limit, total) | ||
|
|
137 | + end | ||
|
|
138 | + end | ||
|
|
139 | + | ||
|
|
140 | + def array(size = 3) | ||
|
|
141 | + Array.new(size) | ||
|
|
142 | + end | ||
|
|
143 | + end |
@@ -0,0 +1,8 | |||||
|
|
1 | + #!/usr/bin/env ruby | ||
|
|
2 | + irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' | ||
|
|
3 | + libs = [] | ||
|
|
4 | + | ||
|
|
5 | + libs << 'irb/completion' | ||
|
|
6 | + libs << File.join('lib', 'load_fixtures') | ||
|
|
7 | + | ||
|
|
8 | + exec "#{irb} -Ilib:test#{libs.map{ |l| " -r #{l}" }.join} --simple-prompt" |
@@ -0,0 +1,22 | |||||
|
|
1 | + sqlite3: | ||
|
|
2 | + database: ":memory:" | ||
|
|
3 | + adapter: sqlite3 | ||
|
|
4 | + timeout: 500 | ||
|
|
5 | + | ||
|
|
6 | + sqlite2: | ||
|
|
7 | + database: ":memory:" | ||
|
|
8 | + adapter: sqlite2 | ||
|
|
9 | + | ||
|
|
10 | + mysql: | ||
|
|
11 | + adapter: mysql | ||
|
|
12 | + username: root | ||
|
|
13 | + password: | ||
|
|
14 | + encoding: utf8 | ||
|
|
15 | + database: will_paginate_unittest | ||
|
|
16 | + | ||
|
|
17 | + postgres: | ||
|
|
18 | + adapter: postgresql | ||
|
|
19 | + username: mislav | ||
|
|
20 | + password: | ||
|
|
21 | + database: will_paginate_unittest | ||
|
|
22 | + min_messages: warning |
@@ -0,0 +1,473 | |||||
|
|
1 | + require 'helper' | ||
|
|
2 | + require 'lib/activerecord_test_case' | ||
|
|
3 | + | ||
|
|
4 | + require 'will_paginate' | ||
|
|
5 | + WillPaginate.enable_activerecord | ||
|
|
6 | + WillPaginate.enable_named_scope | ||
|
|
7 | + | ||
|
|
8 | + class FinderTest < ActiveRecordTestCase | ||
|
|
9 | + fixtures :topics, :replies, :users, :projects, :developers_projects | ||
|
|
10 | + | ||
|
|
11 | + def test_new_methods_presence | ||
|
|
12 | + assert_respond_to_all Topic, %w(per_page paginate paginate_by_sql) | ||
|
|
13 | + end | ||
|
|
14 | + | ||
|
|
15 | + def test_simple_paginate | ||
|
|
16 | + assert_queries(1) do | ||
|
|
17 | + entries = Topic.paginate :page => nil | ||
|
|
18 | + assert_equal 1, entries.current_page | ||
|
|
19 | + assert_equal 1, entries.total_pages | ||
|
|
20 | + assert_equal 4, entries.size | ||
|
|
21 | + end | ||
|
|
22 | + | ||
|
|
23 | + assert_queries(2) do | ||
|
|
24 | + entries = Topic.paginate :page => 2 | ||
|
|
25 | + assert_equal 1, entries.total_pages | ||
|
|
26 | + assert entries.empty? | ||
|
|
27 | + end | ||
|
|
28 | + end | ||
|
|
29 | + | ||
|
|
30 | + def test_parameter_api | ||
|
|
31 | + # :page parameter in options is required! | ||
|
|
32 | + assert_raise(ArgumentError){ Topic.paginate } | ||
|
|
33 | + assert_raise(ArgumentError){ Topic.paginate({}) } | ||
|
|
34 | + | ||
|
|
35 | + # explicit :all should not break anything | ||
|
|
36 | + assert_equal Topic.paginate(:page => nil), Topic.paginate(:all, :page => 1) | ||
|
|
37 | + | ||
|
|
38 | + # :count could be nil and we should still not cry | ||
|
|
39 | + assert_nothing_raised { Topic.paginate :page => 1, :count => nil } | ||
|
|
40 | + end | ||
|
|
41 | + | ||
|
|
42 | + def test_paginate_with_per_page | ||
|
|
43 | + entries = Topic.paginate :page => 1, :per_page => 1 | ||
|
|
44 | + assert_equal 1, entries.size | ||
|
|
45 | + assert_equal 4, entries.total_pages | ||
|
|
46 | + | ||
|
|
47 | + # Developer class has explicit per_page at 10 | ||
|
|
48 | + entries = Developer.paginate :page => 1 | ||
|
|
49 | + assert_equal 10, entries.size | ||
|
|
50 | + assert_equal 2, entries.total_pages | ||
|
|
51 | + | ||
|
|
52 | + entries = Developer.paginate :page => 1, :per_page => 5 | ||
|
|
53 | + assert_equal 11, entries.total_entries | ||
|
|
54 | + assert_equal 5, entries.size | ||
|
|
55 | + assert_equal 3, entries.total_pages | ||
|
|
56 | + end | ||
|
|
57 | + | ||
|
|
58 | + def test_paginate_with_order | ||
|
|
59 | + entries = Topic.paginate :page => 1, :order => 'created_at desc' | ||
|
|
60 | + expected = [topics(:futurama), topics(:harvey_birdman), topics(:rails), topics(:ar)].reverse | ||
|
|
61 | + assert_equal expected, entries.to_a | ||
|
|
62 | + assert_equal 1, entries.total_pages | ||
|
|
63 | + end | ||
|
|
64 | + | ||
|
|
65 | + def test_paginate_with_conditions | ||
|
|
66 | + entries = Topic.paginate :page => 1, :conditions => ["created_at > ?", 30.minutes.ago] | ||
|
|
67 | + expected = [topics(:rails), topics(:ar)] | ||
|
|
68 | + assert_equal expected, entries.to_a | ||
|
|
69 | + assert_equal 1, entries.total_pages | ||
|
|
70 | + end | ||
|
|
71 | + | ||
|
|
72 | + def test_paginate_with_include_and_conditions | ||
|
|
73 | + entries = Topic.paginate \ | ||
|
|
74 | + :page => 1, | ||
|
|
75 | + :include => :replies, | ||
|
|
76 | + :conditions => "replies.content LIKE 'Bird%' ", | ||
|
|
77 | + :per_page => 10 | ||
|
|
78 | + | ||
|
|
79 | + expected = Topic.find :all, | ||
|
|
80 | + :include => 'replies', | ||
|
|
81 | + :conditions => "replies.content LIKE 'Bird%' ", | ||
|
|
82 | + :limit => 10 | ||
|
|
83 | + | ||
|
|
84 | + assert_equal expected, entries.to_a | ||
|
|
85 | + assert_equal 1, entries.total_entries | ||
|
|
86 | + end | ||
|
|
87 | + | ||
|
|
88 | + def test_paginate_with_include_and_order | ||
|
|
89 | + entries = nil | ||
|
|
90 | + assert_queries(2) do | ||
|
|
91 | + entries = Topic.paginate \ | ||
|
|
92 | + :page => 1, | ||
|
|
93 | + :include => :replies, | ||
|
|
94 | + :order => 'replies.created_at asc, topics.created_at asc', | ||
|
|
95 | + :per_page => 10 | ||
|
|
96 | + end | ||
|
|
97 | + | ||
|
|
98 | + expected = Topic.find :all, | ||
|
|
99 | + :include => 'replies', | ||
|
|
100 | + :order => 'replies.created_at asc, topics.created_at asc', | ||
|
|
101 | + :limit => 10 | ||
|
|
102 | + | ||
|
|
103 | + assert_equal expected, entries.to_a | ||
|
|
104 | + assert_equal 4, entries.total_entries | ||
|
|
105 | + end | ||
|
|
106 | + | ||
|
|
107 | + def test_paginate_associations_with_include | ||
|
|
108 | + entries, project = nil, projects(:active_record) | ||
|
|
109 | + | ||
|
|
110 | + assert_nothing_raised "THIS IS A BUG in Rails 1.2.3 that was fixed in [7326]. " + | ||
|
|
111 | + "Please upgrade to a newer version of Rails." do | ||
|
|
112 | + entries = project.topics.paginate \ | ||
|
|
113 | + :page => 1, | ||
|
|
114 | + :include => :replies, | ||
|
|
115 | + :conditions => "replies.content LIKE 'Nice%' ", | ||
|
|
116 | + :per_page => 10 | ||
|
|
117 | + end | ||
|
|
118 | + | ||
|
|
119 | + expected = Topic.find :all, | ||
|
|
120 | + :include => 'replies', | ||
|
|
121 | + :conditions => "project_id = #{project.id} AND replies.content LIKE 'Nice%' ", | ||
|
|
122 | + :limit => 10 | ||
|
|
123 | + | ||
|
|
124 | + assert_equal expected, entries.to_a | ||
|
|
125 | + end | ||
|
|
126 | + | ||
|
|
127 | + def test_paginate_associations | ||
|
|
128 | + dhh = users :david | ||
|
|
129 | + expected_name_ordered = [projects(:action_controller), projects(:active_record)] | ||
|
|
130 | + expected_id_ordered = [projects(:active_record), projects(:action_controller)] | ||
|
|
131 | + | ||
|
|
132 | + assert_queries(2) do | ||
|
|
133 | + # with association-specified order | ||
|
|
134 | + entries = dhh.projects.paginate(:page => 1) | ||
|
|
135 | + assert_equal expected_name_ordered, entries | ||
|
|
136 | + assert_equal 2, entries.total_entries | ||
|
|
137 | + end | ||
|
|
138 | + | ||
|
|
139 | + # with explicit order | ||
|
|
140 | + entries = dhh.projects.paginate(:page => 1, :order => 'projects.id') | ||
|
|
141 | + assert_equal expected_id_ordered, entries | ||
|
|
142 | + assert_equal 2, entries.total_entries | ||
|
|
143 | + | ||
|
|
144 | + assert_nothing_raised { dhh.projects.find(:all, :order => 'projects.id', :limit => 4) } | ||
|
|
145 | + entries = dhh.projects.paginate(:page => 1, :order => 'projects.id', :per_page => 4) | ||
|
|
146 | + assert_equal expected_id_ordered, entries | ||
|
|
147 | + | ||
|
|
148 | + # has_many with implicit order | ||
|
|
149 | + topic = Topic.find(1) | ||
|
|
150 | + expected = [replies(:spam), replies(:witty_retort)] | ||
|
|
151 | + assert_equal expected.map(&:id).sort, topic.replies.paginate(:page => 1).map(&:id).sort | ||
|
|
152 | + assert_equal expected.reverse, topic.replies.paginate(:page => 1, :order => 'replies.id ASC') | ||
|
|
153 | + end | ||
|
|
154 | + | ||
|
|
155 | + def test_paginate_association_extension | ||
|
|
156 | + project = Project.find(:first) | ||
|
|
157 | + | ||
|
|
158 | + assert_queries(2) do | ||
|
|
159 | + entries = project.replies.paginate_recent :page => 1 | ||
|
|
160 | + assert_equal [replies(:brave)], entries | ||
|
|
161 | + end | ||
|
|
162 | + end | ||
|
|
163 | + | ||
|
|
164 | + def test_paginate_with_joins | ||
|
|
165 | + entries = nil | ||
|
|
166 | + | ||
|
|
167 | + assert_queries(1) do | ||
|
|
168 | + entries = Developer.paginate :page => 1, | ||
|
|
169 | + :joins => 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id', | ||
|
|
170 | + :conditions => 'project_id = 1' | ||
|
|
171 | + assert_equal 2, entries.size | ||
|
|
172 | + developer_names = entries.map &:name | ||
|
|
173 | + assert developer_names.include?('David') | ||
|
|
174 | + assert developer_names.include?('Jamis') | ||
|
|
175 | + end | ||
|
|
176 | + | ||
|
|
177 | + assert_queries(1) do | ||
|
|
178 | + expected = entries.to_a | ||
|
|
179 | + entries = Developer.paginate :page => 1, | ||
|
|
180 | + :joins => 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id', | ||
|
|
181 | + :conditions => 'project_id = 1', :count => { :select => "users.id" } | ||
|
|
182 | + assert_equal expected, entries.to_a | ||
|
|
183 | + assert_equal 2, entries.total_entries | ||
|
|
184 | + end | ||
|
|
185 | + end | ||
|
|
186 | + | ||
|
|
187 | + def test_paginate_with_group | ||
|
|
188 | + entries = nil | ||
|
|
189 | + assert_queries(1) do | ||
|
|
190 | + entries = Developer.paginate :page => 1, :per_page => 10, | ||
|
|
191 | + :group => 'salary', :select => 'salary', :order => 'salary' | ||
|
|
192 | + end | ||
|
|
193 | + | ||
|
|
194 | + expected = [ users(:david), users(:jamis), users(:dev_10), users(:poor_jamis) ].map(&:salary).sort | ||
|
|
195 | + assert_equal expected, entries.map(&:salary) | ||
|
|
196 | + end | ||
|
|
197 | + | ||
|
|
198 | + def test_paginate_with_dynamic_finder | ||
|
|
199 | + expected = [replies(:witty_retort), replies(:spam)] | ||
|
|
200 | + assert_equal expected, Reply.paginate_by_topic_id(1, :page => 1) | ||
|
|
201 | + | ||
|
|
202 | + entries = Developer.paginate :conditions => { :salary => 100000 }, :page => 1, :per_page => 5 | ||
|
|
203 | + assert_equal 8, entries.total_entries | ||
|
|
204 | + assert_equal entries, Developer.paginate_by_salary(100000, :page => 1, :per_page => 5) | ||
|
|
205 | + | ||
|
|
206 | + # dynamic finder + conditions | ||
|
|
207 | + entries = Developer.paginate_by_salary(100000, :page => 1, | ||
|
|
208 | + :conditions => ['id > ?', 6]) | ||
|
|
209 | + assert_equal 4, entries.total_entries | ||
|
|
210 | + assert_equal (7..10).to_a, entries.map(&:id) | ||
|
|
211 | + | ||
|
|
212 | + assert_raises NoMethodError do | ||
|
|
213 | + Developer.paginate_by_inexistent_attribute 100000, :page => 1 | ||
|
|
214 | + end | ||
|
|
215 | + end | ||
|
|
216 | + | ||
|
|
217 | + def test_scoped_paginate | ||
|
|
218 | + entries = Developer.with_poor_ones { Developer.paginate :page => 1 } | ||
|
|
219 | + | ||
|
|
220 | + assert_equal 2, entries.size | ||
|
|
221 | + assert_equal 2, entries.total_entries | ||
|
|
222 | + end | ||
|
|
223 | + | ||
|
|
224 | + ## named_scope ## | ||
|
|
225 | + | ||
|
|
226 | + def test_paginate_in_named_scope | ||
|
|
227 | + entries = Developer.poor.paginate :page => 1, :per_page => 1 | ||
|
|
228 | + | ||
|
|
229 | + assert_equal 1, entries.size | ||
|
|
230 | + assert_equal 2, entries.total_entries | ||
|
|
231 | + end | ||
|
|
232 | + | ||
|
|
233 | + def test_paginate_in_named_scope_on_habtm_association | ||
|
|
234 | + project = projects(:active_record) | ||
|
|
235 | + assert_queries(2) do | ||
|
|
236 | + entries = project.developers.poor.paginate :page => 1, :per_page => 1 | ||
|
|
237 | + | ||
|
|
238 | + assert_equal 1, entries.size, 'one developer should be found' | ||
|
|
239 | + assert_equal 1, entries.total_entries, 'only one developer should be found' | ||
|
|
240 | + end | ||
|
|
241 | + end | ||
|
|
242 | + | ||
|
|
243 | + def test_paginate_in_named_scope_on_hmt_association | ||
|
|
244 | + project = projects(:active_record) | ||
|
|
245 | + expected = [replies(:brave)] | ||
|
|
246 | + | ||
|
|
247 | + assert_queries(2) do | ||
|
|
248 | + entries = project.replies.recent.paginate :page => 1, :per_page => 1 | ||
|
|
249 | + assert_equal expected, entries | ||
|
|
250 | + assert_equal 1, entries.total_entries, 'only one reply should be found' | ||
|
|
251 | + end | ||
|
|
252 | + end | ||
|
|
253 | + | ||
|
|
254 | + def test_paginate_in_named_scope_on_has_many_association | ||
|
|
255 | + project = projects(:active_record) | ||
|
|
256 | + expected = [topics(:ar)] | ||
|
|
257 | + | ||
|
|
258 | + assert_queries(2) do | ||
|
|
259 | + entries = project.topics.mentions_activerecord.paginate :page => 1, :per_page => 1 | ||
|
|
260 | + assert_equal expected, entries | ||
|
|
261 | + assert_equal 1, entries.total_entries, 'only one topic should be found' | ||
|
|
262 | + end | ||
|
|
263 | + end | ||
|
|
264 | + | ||
|
|
265 | + def test_named_scope_with_include | ||
|
|
266 | + project = projects(:active_record) | ||
|
|
267 | + entries = project.topics.with_replies_starting_with('AR ').paginate(:page => 1, :per_page => 1) | ||
|
|
268 | + assert_equal 1, entries.size | ||
|
|
269 | + end | ||
|
|
270 | + | ||
|
|
271 | + ## misc ## | ||
|
|
272 | + | ||
|
|
273 | + def test_count_and_total_entries_options_are_mutually_exclusive | ||
|
|
274 | + e = assert_raise ArgumentError do | ||
|
|
275 | + Developer.paginate :page => 1, :count => {}, :total_entries => 1 | ||
|
|
276 | + end | ||
|
|
277 | + assert_match /exclusive/, e.to_s | ||
|
|
278 | + end | ||
|
|
279 | + | ||
|
|
280 | + def test_readonly | ||
|
|
281 | + assert_nothing_raised { Developer.paginate :readonly => true, :page => 1 } | ||
|
|
282 | + end | ||
|
|
283 | + | ||
|
|
284 | + # this functionality is temporarily removed | ||
|
|
285 | + def xtest_pagination_defines_method | ||
|
|
286 | + pager = "paginate_by_created_at" | ||
|
|
287 | + assert !User.methods.include_method?(pager), "User methods should not include `#{pager}` method" | ||
|
|
288 | + # paginate! | ||
|
|
289 | + assert 0, User.send(pager, nil, :page => 1).total_entries | ||
|
|
290 | + # the paging finder should now be defined | ||
|
|
291 | + assert User.methods.include_method?(pager), "`#{pager}` method should be defined on User" | ||
|
|
292 | + end | ||
|
|
293 | + | ||
|
|
294 | + # Is this Rails 2.0? Find out by testing find_all which was removed in [6998] | ||
|
|
295 | + unless ActiveRecord::Base.respond_to? :find_all | ||
|
|
296 | + def test_paginate_array_of_ids | ||
|
|
297 | + # AR finders also accept arrays of IDs | ||
|
|
298 | + # (this was broken in Rails before [6912]) | ||
|
|
299 | + assert_queries(1) do | ||
|
|
300 | + entries = Developer.paginate((1..8).to_a, :per_page => 3, :page => 2, :order => 'id') | ||
|
|
301 | + assert_equal (4..6).to_a, entries.map(&:id) | ||
|
|
302 | + assert_equal 8, entries.total_entries | ||
|
|
303 | + end | ||
|
|
304 | + end | ||
|
|
305 | + end | ||
|
|
306 | + | ||
|
|
307 | + uses_mocha 'internals' do | ||
|
|
308 | + def test_implicit_all_with_dynamic_finders | ||
|
|
309 | + Topic.expects(:find_all_by_foo).returns([]) | ||
|
|
310 | + Topic.expects(:count).returns(0) | ||
|
|
311 | + Topic.paginate_by_foo :page => 2 | ||
|
|
312 | + end | ||
|
|
313 | + | ||
|
|
314 | + def test_guessing_the_total_count | ||
|
|
315 | + Topic.expects(:find).returns(Array.new(2)) | ||
|
|
316 | + Topic.expects(:count).never | ||
|
|
317 | + | ||
|
|
318 | + entries = Topic.paginate :page => 2, :per_page => 4 | ||
|
|
319 | + assert_equal 6, entries.total_entries | ||
|
|
320 | + end | ||
|
|
321 | + | ||
|
|
322 | + def test_guessing_that_there_are_no_records | ||
|
|
323 | + Topic.expects(:find).returns([]) | ||
|
|
324 | + Topic.expects(:count).never | ||
|
|
325 | + | ||
|
|
326 | + entries = Topic.paginate :page => 1, :per_page => 4 | ||
|
|
327 | + assert_equal 0, entries.total_entries | ||
|
|
328 | + end | ||
|
|
329 | + | ||
|
|
330 | + def test_extra_parameters_stay_untouched | ||
|
|
331 | + Topic.expects(:find).with(:all, {:foo => 'bar', :limit => 4, :offset => 0 }).returns(Array.new(5)) | ||
|
|
332 | + Topic.expects(:count).with({:foo => 'bar'}).returns(1) | ||
|
|
333 | + | ||
|
|
334 | + Topic.paginate :foo => 'bar', :page => 1, :per_page => 4 | ||
|
|
335 | + end | ||
|
|
336 | + | ||
|
|
337 | + def test_count_skips_select | ||
|
|
338 | + Developer.stubs(:find).returns([]) | ||
|
|
339 | + Developer.expects(:count).with({}).returns(0) | ||
|
|
340 | + Developer.paginate :select => 'salary', :page => 2 | ||
|
|
341 | + end | ||
|
|
342 | + | ||
|
|
343 | + def test_count_select_when_distinct | ||
|
|
344 | + Developer.stubs(:find).returns([]) | ||
|
|
345 | + Developer.expects(:count).with(:select => 'DISTINCT salary').returns(0) | ||
|
|
346 | + Developer.paginate :select => 'DISTINCT salary', :page => 2 | ||
|
|
347 | + end | ||
|
|
348 | + | ||
|
|
349 | + def test_count_with_scoped_select_when_distinct | ||
|
|
350 | + Developer.stubs(:find).returns([]) | ||
|
|
351 | + Developer.expects(:count).with(:select => 'DISTINCT users.id').returns(0) | ||
|
|
352 | + Developer.distinct.paginate :page => 2 | ||
|
|
353 | + end | ||
|
|
354 | + | ||
|
|
355 | + def test_should_use_scoped_finders_if_present | ||
|
|
356 | + # scope-out compatibility | ||
|
|
357 | + Topic.expects(:find_best).returns(Array.new(5)) | ||
|
|
358 | + Topic.expects(:with_best).returns(1) | ||
|
|
359 | + | ||
|
|
360 | + Topic.paginate_best :page => 1, :per_page => 4 | ||
|
|
361 | + end | ||
|
|
362 | + | ||
|
|
363 | + def test_paginate_by_sql | ||
|
|
364 | + sql = "SELECT * FROM users WHERE type = 'Developer' ORDER BY id" | ||
|
|
365 | + entries = Developer.paginate_by_sql(sql, :page => 2, :per_page => 3) | ||
|
|
366 | + assert_equal 11, entries.total_entries | ||
|
|
367 | + assert_equal [users(:dev_4), users(:dev_5), users(:dev_6)], entries | ||
|
|
368 | + end | ||
|
|
369 | + | ||
|
|
370 | + def test_paginate_by_sql_respects_total_entries_setting | ||
|
|
371 | + sql = "SELECT * FROM users" | ||
|
|
372 | + entries = Developer.paginate_by_sql(sql, :page => 1, :total_entries => 999) | ||
|
|
373 | + assert_equal 999, entries.total_entries | ||
|
|
374 | + end | ||
|
|
375 | + | ||
|
|
376 | + def test_paginate_by_sql_strips_order_by_when_counting | ||
|
|
377 | + Developer.expects(:find_by_sql).returns([]) | ||
|
|
378 | + Developer.expects(:count_by_sql).with("SELECT COUNT(*) FROM (sql\n ) AS count_table").returns(0) | ||
|
|
379 | + | ||
|
|
380 | + Developer.paginate_by_sql "sql\n ORDER\nby foo, bar, `baz` ASC", :page => 2 | ||
|
|
381 | + end | ||
|
|
382 | + | ||
|
|
383 | + # TODO: counts are still wrong | ||
|
|
384 | + def test_ability_to_use_with_custom_finders | ||
|
|
385 | + # acts_as_taggable defines find_tagged_with(tag, options) | ||
|
|
386 | + Topic.expects(:find_tagged_with).with('will_paginate', :offset => 5, :limit => 5).returns([]) | ||
|
|
387 | + Topic.expects(:count).with({}).returns(0) | ||
|
|
388 | + | ||
|
|
389 | + Topic.paginate_tagged_with 'will_paginate', :page => 2, :per_page => 5 | ||
|
|
390 | + end | ||
|
|
391 | + | ||
|
|
392 | + def test_array_argument_doesnt_eliminate_count | ||
|
|
393 | + ids = (1..8).to_a | ||
|
|
394 | + Developer.expects(:find_all_by_id).returns([]) | ||
|
|
395 | + Developer.expects(:count).returns(0) | ||
|
|
396 | + | ||
|
|
397 | + Developer.paginate_by_id(ids, :per_page => 3, :page => 2, :order => 'id') | ||
|
|
398 | + end | ||
|
|
399 | + | ||
|
|
400 | + def test_paginating_finder_doesnt_mangle_options | ||
|
|
401 | + Developer.expects(:find).returns([]) | ||
|
|
402 | + options = { :page => 1, :per_page => 2, :foo => 'bar' } | ||
|
|
403 | + options_before = options.dup | ||
|
|
404 | + | ||
|
|
405 | + Developer.paginate(options) | ||
|
|
406 | + assert_equal options_before, options | ||
|
|
407 | + end | ||
|
|
408 | + | ||
|
|
409 | + def test_paginate_by_sql_doesnt_change_original_query | ||
|
|
410 | + query = 'SQL QUERY' | ||
|
|
411 | + original_query = query.dup | ||
|
|
412 | + Developer.expects(:find_by_sql).returns([]) | ||
|
|
413 | + | ||
|
|
414 | + Developer.paginate_by_sql query, :page => 1 | ||
|
|
415 | + assert_equal original_query, query | ||
|
|
416 | + end | ||
|
|
417 | + | ||
|
|
418 | + def test_paginated_each | ||
|
|
419 | + collection = stub('collection', :size => 5, :empty? => false, :per_page => 5) | ||
|
|
420 | + collection.expects(:each).times(2).returns(collection) | ||
|
|
421 | + last_collection = stub('collection', :size => 4, :empty? => false, :per_page => 5) | ||
|
|
422 | + last_collection.expects(:each).returns(last_collection) | ||
|
|
423 | + | ||
|
|
424 | + params = { :order => 'id', :total_entries => 0 } | ||
|
|
425 | + | ||
|
|
426 | + Developer.expects(:paginate).with(params.merge(:page => 2)).returns(collection) | ||
|
|
427 | + Developer.expects(:paginate).with(params.merge(:page => 3)).returns(collection) | ||
|
|
428 | + Developer.expects(:paginate).with(params.merge(:page => 4)).returns(last_collection) | ||
|
|
429 | + | ||
|
|
430 | + assert_equal 14, Developer.paginated_each(:page => '2') { } | ||
|
|
431 | + end | ||
|
|
432 | + | ||
|
|
433 | + def test_paginated_each_with_named_scope | ||
|
|
434 | + assert_equal 2, Developer.poor.paginated_each(:per_page => 1) { | ||
|
|
435 | + assert_equal 11, Developer.count | ||
|
|
436 | + } | ||
|
|
437 | + end | ||
|
|
438 | + | ||
|
|
439 | + # detect ActiveRecord 2.1 | ||
|
|
440 | + if ActiveRecord::Base.private_methods.include_method?(:references_eager_loaded_tables?) | ||
|
|
441 | + def test_removes_irrelevant_includes_in_count | ||
|
|
442 | + Developer.expects(:find).returns([1]) | ||
|
|
443 | + Developer.expects(:count).with({}).returns(0) | ||
|
|
444 | + | ||
|
|
445 | + Developer.paginate :page => 1, :per_page => 1, :include => :projects | ||
|
|
446 | + end | ||
|
|
447 | + | ||
|
|
448 | + def test_doesnt_remove_referenced_includes_in_count | ||
|
|
449 | + Developer.expects(:find).returns([1]) | ||
|
|
450 | + Developer.expects(:count).with({ :include => :projects, :conditions => 'projects.id > 2' }).returns(0) | ||
|
|
451 | + | ||
|
|
452 | + Developer.paginate :page => 1, :per_page => 1, | ||
|
|
453 | + :include => :projects, :conditions => 'projects.id > 2' | ||
|
|
454 | + end | ||
|
|
455 | + end | ||
|
|
456 | + | ||
|
|
457 | + def test_paginate_from | ||
|
|
458 | + result = Developer.paginate(:from => 'users', :page => 1, :per_page => 1) | ||
|
|
459 | + assert_equal 1, result.size | ||
|
|
460 | + end | ||
|
|
461 | + | ||
|
|
462 | + def test_hmt_with_include | ||
|
|
463 | + # ticket #220 | ||
|
|
464 | + reply = projects(:active_record).replies.find(:first, :order => 'replies.id') | ||
|
|
465 | + assert_equal replies(:decisive), reply | ||
|
|
466 | + | ||
|
|
467 | + # ticket #223 | ||
|
|
468 | + Project.find(1, :include => :replies) | ||
|
|
469 | + | ||
|
|
470 | + # I cannot reproduce any of the failures from those reports :( | ||
|
|
471 | + end | ||
|
|
472 | + end | ||
|
|
473 | + end |
@@ -0,0 +1,3 | |||||
|
|
1 | + class Admin < User | ||
|
|
2 | + has_many :companies, :finder_sql => 'SELECT * FROM companies' | ||
|
|
3 | + end |
@@ -0,0 +1,14 | |||||
|
|
1 | + class Developer < User | ||
|
|
2 | + has_and_belongs_to_many :projects, :include => :topics, :order => 'projects.name' | ||
|
|
3 | + | ||
|
|
4 | + def self.with_poor_ones(&block) | ||
|
|
5 | + with_scope :find => { :conditions => ['salary <= ?', 80000], :order => 'salary' } do | ||
|
|
6 | + yield | ||
|
|
7 | + end | ||
|
|
8 | + end | ||
|
|
9 | + | ||
|
|
10 | + named_scope :distinct, :select => 'DISTINCT `users`.*' | ||
|
|
11 | + named_scope :poor, :conditions => ['salary <= ?', 80000], :order => 'salary' | ||
|
|
12 | + | ||
|
|
13 | + def self.per_page() 10 end | ||
|
|
14 | + end |
@@ -0,0 +1,13 | |||||
|
|
1 | + david_action_controller: | ||
|
|
2 | + developer_id: 1 | ||
|
|
3 | + project_id: 2 | ||
|
|
4 | + joined_on: 2004-10-10 | ||
|
|
5 | + | ||
|
|
6 | + david_active_record: | ||
|
|
7 | + developer_id: 1 | ||
|
|
8 | + project_id: 1 | ||
|
|
9 | + joined_on: 2004-10-10 | ||
|
|
10 | + | ||
|
|
11 | + jamis_active_record: | ||
|
|
12 | + developer_id: 2 | ||
|
|
13 | + project_id: 1 No newline at end of file |
@@ -0,0 +1,15 | |||||
|
|
1 | + class Project < ActiveRecord::Base | ||
|
|
2 | + has_and_belongs_to_many :developers, :uniq => true | ||
|
|
3 | + | ||
|
|
4 | + has_many :topics | ||
|
|
5 | + # :finder_sql => 'SELECT * FROM topics WHERE (topics.project_id = #{id})', | ||
|
|
6 | + # :counter_sql => 'SELECT COUNT(*) FROM topics WHERE (topics.project_id = #{id})' | ||
|
|
7 | + | ||
|
|
8 | + has_many :replies, :through => :topics do | ||
|
|
9 | + def find_recent(params = {}) | ||
|
|
10 | + with_scope :find => { :conditions => ['replies.created_at > ?', 15.minutes.ago] } do | ||
|
|
11 | + find :all, params | ||
|
|
12 | + end | ||
|
|
13 | + end | ||
|
|
14 | + end | ||
|
|
15 | + end |
@@ -0,0 +1,6 | |||||
|
|
1 | + active_record: | ||
|
|
2 | + id: 1 | ||
|
|
3 | + name: Active Record | ||
|
|
4 | + action_controller: | ||
|
|
5 | + id: 2 | ||
|
|
6 | + name: Active Controller |
@@ -0,0 +1,29 | |||||
|
|
1 | + witty_retort: | ||
|
|
2 | + id: 1 | ||
|
|
3 | + topic_id: 1 | ||
|
|
4 | + content: Birdman is better! | ||
|
|
5 | + created_at: <%= 6.hours.ago.to_s(:db) %> | ||
|
|
6 | + | ||
|
|
7 | + another: | ||
|
|
8 | + id: 2 | ||
|
|
9 | + topic_id: 2 | ||
|
|
10 | + content: Nuh uh! | ||
|
|
11 | + created_at: <%= 1.hour.ago.to_s(:db) %> | ||
|
|
12 | + | ||
|
|
13 | + spam: | ||
|
|
14 | + id: 3 | ||
|
|
15 | + topic_id: 1 | ||
|
|
16 | + content: Nice site! | ||
|
|
17 | + created_at: <%= 1.hour.ago.to_s(:db) %> | ||
|
|
18 | + | ||
|
|
19 | + decisive: | ||
|
|
20 | + id: 4 | ||
|
|
21 | + topic_id: 4 | ||
|
|
22 | + content: "I'm getting to the bottom of this" | ||
|
|
23 | + created_at: <%= 30.minutes.ago.to_s(:db) %> | ||
|
|
24 | + | ||
|
|
25 | + brave: | ||
|
|
26 | + id: 5 | ||
|
|
27 | + topic_id: 4 | ||
|
|
28 | + content: "AR doesn't scare me a bit" | ||
|
|
29 | + created_at: <%= 10.minutes.ago.to_s(:db) %> |
@@ -0,0 +1,7 | |||||
|
|
1 | + class Reply < ActiveRecord::Base | ||
|
|
2 | + belongs_to :topic, :include => [:replies] | ||
|
|
3 | + | ||
|
|
4 | + named_scope :recent, :conditions => ['replies.created_at > ?', 15.minutes.ago] | ||
|
|
5 | + | ||
|
|
6 | + validates_presence_of :content | ||
|
|
7 | + end |
@@ -0,0 +1,38 | |||||
|
|
1 | + ActiveRecord::Schema.define do | ||
|
|
2 | + | ||
|
|
3 | + create_table "users", :force => true do |t| | ||
|
|
4 | + t.column "name", :text | ||
|
|
5 | + t.column "salary", :integer, :default => 70000 | ||
|
|
6 | + t.column "created_at", :datetime | ||
|
|
7 | + t.column "updated_at", :datetime | ||
|
|
8 | + t.column "type", :text | ||
|
|
9 | + end | ||
|
|
10 | + | ||
|
|
11 | + create_table "projects", :force => true do |t| | ||
|
|
12 | + t.column "name", :text | ||
|
|
13 | + end | ||
|
|
14 | + | ||
|
|
15 | + create_table "developers_projects", :id => false, :force => true do |t| | ||
|
|
16 | + t.column "developer_id", :integer, :null => false | ||
|
|
17 | + t.column "project_id", :integer, :null => false | ||
|
|
18 | + t.column "joined_on", :date | ||
|
|
19 | + t.column "access_level", :integer, :default => 1 | ||
|
|
20 | + end | ||
|
|
21 | + | ||
|
|
22 | + create_table "topics", :force => true do |t| | ||
|
|
23 | + t.column "project_id", :integer | ||
|
|
24 | + t.column "title", :string | ||
|
|
25 | + t.column "subtitle", :string | ||
|
|
26 | + t.column "content", :text | ||
|
|
27 | + t.column "created_at", :datetime | ||
|
|
28 | + t.column "updated_at", :datetime | ||
|
|
29 | + end | ||
|
|
30 | + | ||
|
|
31 | + create_table "replies", :force => true do |t| | ||
|
|
32 | + t.column "content", :text | ||
|
|
33 | + t.column "created_at", :datetime | ||
|
|
34 | + t.column "updated_at", :datetime | ||
|
|
35 | + t.column "topic_id", :integer | ||
|
|
36 | + end | ||
|
|
37 | + | ||
|
|
38 | + end |
@@ -0,0 +1,10 | |||||
|
|
1 | + class Topic < ActiveRecord::Base | ||
|
|
2 | + has_many :replies, :dependent => :destroy, :order => 'replies.created_at DESC' | ||
|
|
3 | + belongs_to :project | ||
|
|
4 | + | ||
|
|
5 | + named_scope :mentions_activerecord, :conditions => ['topics.title LIKE ?', '%ActiveRecord%'] | ||
|
|
6 | + | ||
|
|
7 | + named_scope :with_replies_starting_with, lambda { |text| | ||
|
|
8 | + { :conditions => "replies.content LIKE '#{text}%' ", :include => :replies } | ||
|
|
9 | + } | ||
|
|
10 | + end |
@@ -0,0 +1,30 | |||||
|
|
1 | + futurama: | ||
|
|
2 | + id: 1 | ||
|
|
3 | + title: Isnt futurama awesome? | ||
|
|
4 | + subtitle: It really is, isnt it. | ||
|
|
5 | + content: I like futurama | ||
|
|
6 | + created_at: <%= 1.day.ago.to_s(:db) %> | ||
|
|
7 | + updated_at: | ||
|
|
8 | + | ||
|
|
9 | + harvey_birdman: | ||
|
|
10 | + id: 2 | ||
|
|
11 | + title: Harvey Birdman is the king of all men | ||
|
|
12 | + subtitle: yup | ||
|
|
13 | + content: He really is | ||
|
|
14 | + created_at: <%= 2.hours.ago.to_s(:db) %> | ||
|
|
15 | + updated_at: | ||
|
|
16 | + | ||
|
|
17 | + rails: | ||
|
|
18 | + id: 3 | ||
|
|
19 | + project_id: 1 | ||
|
|
20 | + title: Rails is nice | ||
|
|
21 | + subtitle: It makes me happy | ||
|
|
22 | + content: except when I have to hack internals to fix pagination. even then really. | ||
|
|
23 | + created_at: <%= 20.minutes.ago.to_s(:db) %> | ||
|
|
24 | + | ||
|
|
25 | + ar: | ||
|
|
26 | + id: 4 | ||
|
|
27 | + project_id: 1 | ||
|
|
28 | + title: ActiveRecord sometimes freaks me out | ||
|
|
29 | + content: "I mean, what's the deal with eager loading?" | ||
|
|
30 | + created_at: <%= 15.minutes.ago.to_s(:db) %> |
@@ -0,0 +1,35 | |||||
|
|
1 | + david: | ||
|
|
2 | + id: 1 | ||
|
|
3 | + name: David | ||
|
|
4 | + salary: 80000 | ||
|
|
5 | + type: Developer | ||
|
|
6 | + | ||
|
|
7 | + jamis: | ||
|
|
8 | + id: 2 | ||
|
|
9 | + name: Jamis | ||
|
|
10 | + salary: 150000 | ||
|
|
11 | + type: Developer | ||
|
|
12 | + | ||
|
|
13 | + <% for digit in 3..10 %> | ||
|
|
14 | + dev_<%= digit %>: | ||
|
|
15 | + id: <%= digit %> | ||
|
|
16 | + name: fixture_<%= digit %> | ||
|
|
17 | + salary: 100000 | ||
|
|
18 | + type: Developer | ||
|
|
19 | + <% end %> | ||
|
|
20 | + | ||
|
|
21 | + poor_jamis: | ||
|
|
22 | + id: 11 | ||
|
|
23 | + name: Jamis | ||
|
|
24 | + salary: 9000 | ||
|
|
25 | + type: Developer | ||
|
|
26 | + | ||
|
|
27 | + admin: | ||
|
|
28 | + id: 12 | ||
|
|
29 | + name: admin | ||
|
|
30 | + type: Admin | ||
|
|
31 | + | ||
|
|
32 | + goofy: | ||
|
|
33 | + id: 13 | ||
|
|
34 | + name: Goofy | ||
|
|
35 | + type: Admin |
@@ -0,0 +1,37 | |||||
|
|
1 | + require 'test/unit' | ||
|
|
2 | + require 'rubygems' | ||
|
|
3 | + | ||
|
|
4 | + # gem install redgreen for colored test output | ||
|
|
5 | + begin require 'redgreen'; rescue LoadError; end | ||
|
|
6 | + | ||
|
|
7 | + require 'boot' unless defined?(ActiveRecord) | ||
|
|
8 | + | ||
|
|
9 | + class Test::Unit::TestCase | ||
|
|
10 | + protected | ||
|
|
11 | + def assert_respond_to_all object, methods | ||
|
|
12 | + methods.each do |method| | ||
|
|
13 | + [method.to_s, method.to_sym].each { |m| assert_respond_to object, m } | ||
|
|
14 | + end | ||
|
|
15 | + end | ||
|
|
16 | + | ||
|
|
17 | + def collect_deprecations | ||
|
|
18 | + old_behavior = WillPaginate::Deprecation.behavior | ||
|
|
19 | + deprecations = [] | ||
|
|
20 | + WillPaginate::Deprecation.behavior = Proc.new do |message, callstack| | ||
|
|
21 | + deprecations << message | ||
|
|
22 | + end | ||
|
|
23 | + result = yield | ||
|
|
24 | + [result, deprecations] | ||
|
|
25 | + ensure | ||
|
|
26 | + WillPaginate::Deprecation.behavior = old_behavior | ||
|
|
27 | + end | ||
|
|
28 | + end | ||
|
|
29 | + | ||
|
|
30 | + # Wrap tests that use Mocha and skip if unavailable. | ||
|
|
31 | + def uses_mocha(test_name) | ||
|
|
32 | + require 'mocha' | ||
|
|
33 | + rescue LoadError | ||
|
|
34 | + $stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again." | ||
|
|
35 | + else | ||
|
|
36 | + yield | ||
|
|
37 | + end |
@@ -0,0 +1,43 | |||||
|
|
1 | + require 'lib/activerecord_test_connector' | ||
|
|
2 | + | ||
|
|
3 | + class ActiveRecordTestCase < Test::Unit::TestCase | ||
|
|
4 | + if defined?(ActiveSupport::Testing::SetupAndTeardown) | ||
|
|
5 | + include ActiveSupport::Testing::SetupAndTeardown | ||
|
|
6 | + end | ||
|
|
7 | + | ||
|
|
8 | + if defined?(ActiveRecord::TestFixtures) | ||
|
|
9 | + include ActiveRecord::TestFixtures | ||
|
|
10 | + end | ||
|
|
11 | + # Set our fixture path | ||
|
|
12 | + if ActiveRecordTestConnector.able_to_connect | ||
|
|
13 | + self.fixture_path = File.join(File.dirname(__FILE__), '..', 'fixtures') | ||
|
|
14 | + self.use_transactional_fixtures = true | ||
|
|
15 | + end | ||
|
|
16 | + | ||
|
|
17 | + def self.fixtures(*args) | ||
|
|
18 | + super if ActiveRecordTestConnector.connected | ||
|
|
19 | + end | ||
|
|
20 | + | ||
|
|
21 | + def run(*args) | ||
|
|
22 | + super if ActiveRecordTestConnector.connected | ||
|
|
23 | + end | ||
|
|
24 | + | ||
|
|
25 | + # Default so Test::Unit::TestCase doesn't complain | ||
|
|
26 | + def test_truth | ||
|
|
27 | + end | ||
|
|
28 | + | ||
|
|
29 | + protected | ||
|
|
30 | + | ||
|
|
31 | + def assert_queries(num = 1) | ||
|
|
32 | + $query_count = 0 | ||
|
|
33 | + yield | ||
|
|
34 | + ensure | ||
|
|
35 | + assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed." | ||
|
|
36 | + end | ||
|
|
37 | + | ||
|
|
38 | + def assert_no_queries(&block) | ||
|
|
39 | + assert_queries(0, &block) | ||
|
|
40 | + end | ||
|
|
41 | + end | ||
|
|
42 | + | ||
|
|
43 | + ActiveRecordTestConnector.setup |
@@ -0,0 +1,75 | |||||
|
|
1 | + require 'active_record' | ||
|
|
2 | + require 'active_record/version' | ||
|
|
3 | + require 'active_record/fixtures' | ||
|
|
4 | + | ||
|
|
5 | + class ActiveRecordTestConnector | ||
|
|
6 | + cattr_accessor :able_to_connect | ||
|
|
7 | + cattr_accessor :connected | ||
|
|
8 | + | ||
|
|
9 | + FIXTURES_PATH = File.join(File.dirname(__FILE__), '..', 'fixtures') | ||
|
|
10 | + | ||
|
|
11 | + # Set our defaults | ||
|
|
12 | + self.connected = false | ||
|
|
13 | + self.able_to_connect = true | ||
|
|
14 | + | ||
|
|
15 | + def self.setup | ||
|
|
16 | + unless self.connected || !self.able_to_connect | ||
|
|
17 | + setup_connection | ||
|
|
18 | + load_schema | ||
|
|
19 | + add_load_path FIXTURES_PATH | ||
|
|
20 | + self.connected = true | ||
|
|
21 | + end | ||
|
|
22 | + rescue Exception => e # errors from ActiveRecord setup | ||
|
|
23 | + $stderr.puts "\nSkipping ActiveRecord tests: #{e}\n\n" | ||
|
|
24 | + self.able_to_connect = false | ||
|
|
25 | + end | ||
|
|
26 | + | ||
|
|
27 | + private | ||
|
|
28 | + | ||
|
|
29 | + def self.add_load_path(path) | ||
|
|
30 | + dep = defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : ::Dependencies | ||
|
|
31 | + dep.load_paths.unshift path | ||
|
|
32 | + end | ||
|
|
33 | + | ||
|
|
34 | + def self.setup_connection | ||
|
|
35 | + db = ENV['DB'].blank?? 'sqlite3' : ENV['DB'] | ||
|
|
36 | + | ||
|
|
37 | + configurations = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'database.yml')) | ||
|
|
38 | + raise "no configuration for '#{db}'" unless configurations.key? db | ||
|
|
39 | + configuration = configurations[db] | ||
|
|
40 | + | ||
|
|
41 | + ActiveRecord::Base.logger = Logger.new(STDOUT) if $0 == 'irb' | ||
|
|
42 | + puts "using #{configuration['adapter']} adapter" unless ENV['DB'].blank? | ||
|
|
43 | + | ||
|
|
44 | + gem 'sqlite3-ruby' if 'sqlite3' == db | ||
|
|
45 | + | ||
|
|
46 | + ActiveRecord::Base.establish_connection(configuration) | ||
|
|
47 | + ActiveRecord::Base.configurations = { db => configuration } | ||
|
|
48 | + prepare ActiveRecord::Base.connection | ||
|
|
49 | + | ||
|
|
50 | + unless Object.const_defined?(:QUOTED_TYPE) | ||
|
|
51 | + Object.send :const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type') | ||
|
|
52 | + end | ||
|
|
53 | + end | ||
|
|
54 | + | ||
|
|
55 | + def self.load_schema | ||
|
|
56 | + ActiveRecord::Base.silence do | ||
|
|
57 | + ActiveRecord::Migration.verbose = false | ||
|
|
58 | + load File.join(FIXTURES_PATH, 'schema.rb') | ||
|
|
59 | + end | ||
|
|
60 | + end | ||
|
|
61 | + | ||
|
|
62 | + def self.prepare(conn) | ||
|
|
63 | + class << conn | ||
|
|
64 | + IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SHOW FIELDS /] | ||
|
|
65 | + | ||
|
|
66 | + def execute_with_counting(sql, name = nil, &block) | ||
|
|
67 | + $query_count ||= 0 | ||
|
|
68 | + $query_count += 1 unless IGNORED_SQL.any? { |r| sql =~ r } | ||
|
|
69 | + execute_without_counting(sql, name, &block) | ||
|
|
70 | + end | ||
|
|
71 | + | ||
|
|
72 | + alias_method_chain :execute, :counting | ||
|
|
73 | + end | ||
|
|
74 | + end | ||
|
|
75 | + end |
@@ -0,0 +1,11 | |||||
|
|
1 | + require 'boot' | ||
|
|
2 | + require 'lib/activerecord_test_connector' | ||
|
|
3 | + | ||
|
|
4 | + # setup the connection | ||
|
|
5 | + ActiveRecordTestConnector.setup | ||
|
|
6 | + | ||
|
|
7 | + # load all fixtures | ||
|
|
8 | + Fixtures.create_fixtures(ActiveRecordTestConnector::FIXTURES_PATH, ActiveRecord::Base.connection.tables) | ||
|
|
9 | + | ||
|
|
10 | + require 'will_paginate' | ||
|
|
11 | + WillPaginate.enable_activerecord |
@@ -0,0 +1,179 | |||||
|
|
1 | + require 'will_paginate/core_ext' | ||
|
|
2 | + require 'action_controller' | ||
|
|
3 | + require 'action_controller/test_process' | ||
|
|
4 | + | ||
|
|
5 | + require 'will_paginate' | ||
|
|
6 | + WillPaginate.enable_actionpack | ||
|
|
7 | + | ||
|
|
8 | + ActionController::Routing::Routes.draw do |map| | ||
|
|
9 | + map.connect 'dummy/page/:page', :controller => 'dummy' | ||
|
|
10 | + map.connect 'dummy/dots/page.:page', :controller => 'dummy', :action => 'dots' | ||
|
|
11 | + map.connect 'ibocorp/:page', :controller => 'ibocorp', | ||
|
|
12 | + :requirements => { :page => /\d+/ }, | ||
|
|
13 | + :defaults => { :page => 1 } | ||
|
|
14 | + | ||
|
|
15 | + map.connect ':controller/:action/:id' | ||
|
|
16 | + end | ||
|
|
17 | + | ||
|
|
18 | + ActionController::Base.perform_caching = false | ||
|
|
19 | + | ||
|
|
20 | + class WillPaginate::ViewTestCase < Test::Unit::TestCase | ||
|
|
21 | + if defined?(ActionController::TestCase::Assertions) | ||
|
|
22 | + include ActionController::TestCase::Assertions | ||
|
|
23 | + end | ||
|
|
24 | + if defined?(ActiveSupport::Testing::Deprecation) | ||
|
|
25 | + include ActiveSupport::Testing::Deprecation | ||
|
|
26 | + end | ||
|
|
27 | + | ||
|
|
28 | + def setup | ||
|
|
29 | + super | ||
|
|
30 | + @controller = DummyController.new | ||
|
|
31 | + @request = @controller.request | ||
|
|
32 | + @html_result = nil | ||
|
|
33 | + @template = '<%= will_paginate collection, options %>' | ||
|
|
34 | + | ||
|
|
35 | + @view = ActionView::Base.new | ||
|
|
36 | + @view.assigns['controller'] = @controller | ||
|
|
37 | + @view.assigns['_request'] = @request | ||
|
|
38 | + @view.assigns['_params'] = @request.params | ||
|
|
39 | + end | ||
|
|
40 | + | ||
|
|
41 | + def test_no_complain; end | ||
|
|
42 | + | ||
|
|
43 | + protected | ||
|
|
44 | + | ||
|
|
45 | + def paginate(collection = {}, options = {}, &block) | ||
|
|
46 | + if collection.instance_of? Hash | ||
|
|
47 | + page_options = { :page => 1, :total_entries => 11, :per_page => 4 }.merge(collection) | ||
|
|
48 | + collection = [1].paginate(page_options) | ||
|
|
49 | + end | ||
|
|
50 | + | ||
|
|
51 | + locals = { :collection => collection, :options => options } | ||
|
|
52 | + | ||
|
|
53 | + unless @view.respond_to? :render_template | ||
|
|
54 | + # Rails 2.2 | ||
|
|
55 | + @html_result = ActionView::InlineTemplate.new(@template).render(@view, locals) | ||
|
|
56 | + else | ||
|
|
57 | + if defined? ActionView::InlineTemplate | ||
|
|
58 | + # Rails 2.1 | ||
|
|
59 | + args = [ ActionView::InlineTemplate.new(@view, @template, locals) ] | ||
|
|
60 | + else | ||
|
|
61 | + # older Rails versions | ||
|
|
62 | + args = [nil, @template, nil, locals] | ||
|
|
63 | + end | ||
|
|
64 | + | ||
|
|
65 | + @html_result = @view.render_template(*args) | ||
|
|
66 | + end | ||
|
|
67 | + | ||
|
|
68 | + @html_document = HTML::Document.new(@html_result, true, false) | ||
|
|
69 | + | ||
|
|
70 | + if block_given? | ||
|
|
71 | + classname = options[:class] || WillPaginate::ViewHelpers.pagination_options[:class] | ||
|
|
72 | + assert_select("div.#{classname}", 1, 'no main DIV', &block) | ||
|
|
73 | + end | ||
|
|
74 | + end | ||
|
|
75 | + | ||
|
|
76 | + def response_from_page_or_rjs | ||
|
|
77 | + @html_document.root | ||
|
|
78 | + end | ||
|
|
79 | + | ||
|
|
80 | + def validate_page_numbers expected, links, param_name = :page | ||
|
|
81 | + param_pattern = /\W#{CGI.escape(param_name.to_s)}=([^&]*)/ | ||
|
|
82 | + | ||
|
|
83 | + assert_equal(expected, links.map { |e| | ||
|
|
84 | + e['href'] =~ param_pattern | ||
|
|
85 | + $1 ? $1.to_i : $1 | ||
|
|
86 | + }) | ||
|
|
87 | + end | ||
|
|
88 | + | ||
|
|
89 | + def assert_links_match pattern, links = nil, numbers = nil | ||
|
|
90 | + links ||= assert_select 'div.pagination a[href]' do |elements| | ||
|
|
91 | + elements | ||
|
|
92 | + end | ||
|
|
93 | + | ||
|
|
94 | + pages = [] if numbers | ||
|
|
95 | + | ||
|
|
96 | + links.each do |el| | ||
|
|
97 | + assert_match pattern, el['href'] | ||
|
|
98 | + if numbers | ||
|
|
99 | + el['href'] =~ pattern | ||
|
|
100 | + pages << ($1.nil?? nil : $1.to_i) | ||
|
|
101 | + end | ||
|
|
102 | + end | ||
|
|
103 | + | ||
|
|
104 | + assert_equal numbers, pages, "page numbers don't match" if numbers | ||
|
|
105 | + end | ||
|
|
106 | + | ||
|
|
107 | + def assert_no_links_match pattern | ||
|
|
108 | + assert_select 'div.pagination a[href]' do |elements| | ||
|
|
109 | + elements.each do |el| | ||
|
|
110 | + assert_no_match pattern, el['href'] | ||
|
|
111 | + end | ||
|
|
112 | + end | ||
|
|
113 | + end | ||
|
|
114 | + end | ||
|
|
115 | + | ||
|
|
116 | + class DummyRequest | ||
|
|
117 | + attr_accessor :symbolized_path_parameters | ||
|
|
118 | + | ||
|
|
119 | + def initialize | ||
|
|
120 | + @get = true | ||
|
|
121 | + @params = {} | ||
|
|
122 | + @symbolized_path_parameters = { :controller => 'foo', :action => 'bar' } | ||
|
|
123 | + end | ||
|
|
124 | + | ||
|
|
125 | + def get? | ||
|
|
126 | + @get | ||
|
|
127 | + end | ||
|
|
128 | + | ||
|
|
129 | + def post | ||
|
|
130 | + @get = false | ||
|
|
131 | + end | ||
|
|
132 | + | ||
|
|
133 | + def relative_url_root | ||
|
|
134 | + '' | ||
|
|
135 | + end | ||
|
|
136 | + | ||
|
|
137 | + def params(more = nil) | ||
|
|
138 | + @params.update(more) if more | ||
|
|
139 | + @params | ||
|
|
140 | + end | ||
|
|
141 | + end | ||
|
|
142 | + | ||
|
|
143 | + class DummyController | ||
|
|
144 | + attr_reader :request | ||
|
|
145 | + attr_accessor :controller_name | ||
|
|
146 | + | ||
|
|
147 | + def initialize | ||
|
|
148 | + @request = DummyRequest.new | ||
|
|
149 | + @url = ActionController::UrlRewriter.new(@request, @request.params) | ||
|
|
150 | + end | ||
|
|
151 | + | ||
|
|
152 | + def params | ||
|
|
153 | + @request.params | ||
|
|
154 | + end | ||
|
|
155 | + | ||
|
|
156 | + def url_for(params) | ||
|
|
157 | + @url.rewrite(params) | ||
|
|
158 | + end | ||
|
|
159 | + end | ||
|
|
160 | + | ||
|
|
161 | + module HTML | ||
|
|
162 | + Node.class_eval do | ||
|
|
163 | + def inner_text | ||
|
|
164 | + children.map(&:inner_text).join('') | ||
|
|
165 | + end | ||
|
|
166 | + end | ||
|
|
167 | + | ||
|
|
168 | + Text.class_eval do | ||
|
|
169 | + def inner_text | ||
|
|
170 | + self.to_s | ||
|
|
171 | + end | ||
|
|
172 | + end | ||
|
|
173 | + | ||
|
|
174 | + Tag.class_eval do | ||
|
|
175 | + def inner_text | ||
|
|
176 | + childless?? '' : super | ||
|
|
177 | + end | ||
|
|
178 | + end | ||
|
|
179 | + end |
@@ -0,0 +1,59 | |||||
|
|
1 | + require 'rake/testtask' | ||
|
|
2 | + | ||
|
|
3 | + desc 'Test the will_paginate plugin.' | ||
|
|
4 | + Rake::TestTask.new(:test) do |t| | ||
|
|
5 | + t.pattern = 'test/**/*_test.rb' | ||
|
|
6 | + t.verbose = true | ||
|
|
7 | + t.libs << 'test' | ||
|
|
8 | + end | ||
|
|
9 | + | ||
|
|
10 | + # I want to specify environment variables at call time | ||
|
|
11 | + class EnvTestTask < Rake::TestTask | ||
|
|
12 | + attr_accessor :env | ||
|
|
13 | + | ||
|
|
14 | + def ruby(*args) | ||
|
|
15 | + env.each { |key, value| ENV[key] = value } if env | ||
|
|
16 | + super | ||
|
|
17 | + env.keys.each { |key| ENV.delete key } if env | ||
|
|
18 | + end | ||
|
|
19 | + end | ||
|
|
20 | + | ||
|
|
21 | + for configuration in %w( sqlite3 mysql postgres ) | ||
|
|
22 | + EnvTestTask.new("test_#{configuration}") do |t| | ||
|
|
23 | + t.pattern = 'test/finder_test.rb' | ||
|
|
24 | + t.verbose = true | ||
|
|
25 | + t.env = { 'DB' => configuration } | ||
|
|
26 | + t.libs << 'test' | ||
|
|
27 | + end | ||
|
|
28 | + end | ||
|
|
29 | + | ||
|
|
30 | + task :test_databases => %w(test_mysql test_sqlite3 test_postgres) | ||
|
|
31 | + | ||
|
|
32 | + desc %{Test everything on SQLite3, MySQL and PostgreSQL} | ||
|
|
33 | + task :test_full => %w(test test_mysql test_postgres) | ||
|
|
34 | + | ||
|
|
35 | + desc %{Test everything with Rails 2.1.x, 2.0.x & 1.2.x gems} | ||
|
|
36 | + task :test_all do | ||
|
|
37 | + all = Rake::Task['test_full'] | ||
|
|
38 | + versions = %w(2.3.2 2.2.2 2.1.0 2.0.4 1.2.6) | ||
|
|
39 | + versions.each do |version| | ||
|
|
40 | + ENV['RAILS_VERSION'] = "~> #{version}" | ||
|
|
41 | + all.invoke | ||
|
|
42 | + reset_invoked unless version == versions.last | ||
|
|
43 | + end | ||
|
|
44 | + end | ||
|
|
45 | + | ||
|
|
46 | + def reset_invoked | ||
|
|
47 | + %w( test_full test test_mysql test_postgres ).each do |name| | ||
|
|
48 | + Rake::Task[name].instance_variable_set '@already_invoked', false | ||
|
|
49 | + end | ||
|
|
50 | + end | ||
|
|
51 | + | ||
|
|
52 | + task :rcov do | ||
|
|
53 | + excludes = %w( lib/will_paginate/named_scope* | ||
|
|
54 | + lib/will_paginate/core_ext.rb | ||
|
|
55 | + lib/will_paginate.rb | ||
|
|
56 | + rails* ) | ||
|
|
57 | + | ||
|
|
58 | + system %[rcov -Itest:lib test/*.rb -x #{excludes.join(',')}] | ||
|
|
59 | + end |
@@ -0,0 +1,373 | |||||
|
|
1 | + require 'helper' | ||
|
|
2 | + require 'lib/view_test_process' | ||
|
|
3 | + | ||
|
|
4 | + class AdditionalLinkAttributesRenderer < WillPaginate::LinkRenderer | ||
|
|
5 | + def initialize(link_attributes = nil) | ||
|
|
6 | + super() | ||
|
|
7 | + @additional_link_attributes = link_attributes || { :default => 'true' } | ||
|
|
8 | + end | ||
|
|
9 | + | ||
|
|
10 | + def page_link(page, text, attributes = {}) | ||
|
|
11 | + @template.link_to text, url_for(page), attributes.merge(@additional_link_attributes) | ||
|
|
12 | + end | ||
|
|
13 | + end | ||
|
|
14 | + | ||
|
|
15 | + class ViewTest < WillPaginate::ViewTestCase | ||
|
|
16 | + | ||
|
|
17 | + ## basic pagination ## | ||
|
|
18 | + | ||
|
|
19 | + def test_will_paginate | ||
|
|
20 | + paginate do |pagination| | ||
|
|
21 | + assert_select 'a[href]', 3 do |elements| | ||
|
|
22 | + validate_page_numbers [2,3,2], elements | ||
|
|
23 | + assert_select elements.last, ':last-child', "Next »" | ||
|
|
24 | + end | ||
|
|
25 | + assert_select 'span', 2 | ||
|
|
26 | + assert_select 'span.disabled:first-child', '« Previous' | ||
|
|
27 | + assert_select 'span.current', '1' | ||
|
|
28 | + assert_equal '« Previous 1 2 3 Next »', pagination.first.inner_text | ||
|
|
29 | + end | ||
|
|
30 | + end | ||
|
|
31 | + | ||
|
|
32 | + def test_no_pagination_when_page_count_is_one | ||
|
|
33 | + paginate :per_page => 30 | ||
|
|
34 | + assert_equal '', @html_result | ||
|
|
35 | + end | ||
|
|
36 | + | ||
|
|
37 | + def test_will_paginate_with_options | ||
|
|
38 | + paginate({ :page => 2 }, | ||
|
|
39 | + :class => 'will_paginate', :previous_label => 'Prev', :next_label => 'Next') do | ||
|
|
40 | + assert_select 'a[href]', 4 do |elements| | ||
|
|
41 | + validate_page_numbers [1,1,3,3], elements | ||
|
|
42 | + # test rel attribute values: | ||
|
|
43 | + assert_select elements[1], 'a', '1' do |link| | ||
|
|
44 | + assert_equal 'prev start', link.first['rel'] | ||
|
|
45 | + end | ||
|
|
46 | + assert_select elements.first, 'a', "Prev" do |link| | ||
|
|
47 | + assert_equal 'prev start', link.first['rel'] | ||
|
|
48 | + end | ||
|
|
49 | + assert_select elements.last, 'a', "Next" do |link| | ||
|
|
50 | + assert_equal 'next', link.first['rel'] | ||
|
|
51 | + end | ||
|
|
52 | + end | ||
|
|
53 | + assert_select 'span.current', '2' | ||
|
|
54 | + end | ||
|
|
55 | + end | ||
|
|
56 | + | ||
|
|
57 | + def test_will_paginate_using_renderer_class | ||
|
|
58 | + paginate({}, :renderer => AdditionalLinkAttributesRenderer) do | ||
|
|
59 | + assert_select 'a[default=true]', 3 | ||
|
|
60 | + end | ||
|
|
61 | + end | ||
|
|
62 | + | ||
|
|
63 | + def test_will_paginate_using_renderer_instance | ||
|
|
64 | + renderer = WillPaginate::LinkRenderer.new | ||
|
|
65 | + renderer.gap_marker = '<span class="my-gap">~~</span>' | ||
|
|
66 | + | ||
|
|
67 | + paginate({ :per_page => 2 }, :inner_window => 0, :outer_window => 0, :renderer => renderer) do | ||
|
|
68 | + assert_select 'span.my-gap', '~~' | ||
|
|
69 | + end | ||
|
|
70 | + | ||
|
|
71 | + renderer = AdditionalLinkAttributesRenderer.new(:title => 'rendered') | ||
|
|
72 | + paginate({}, :renderer => renderer) do | ||
|
|
73 | + assert_select 'a[title=rendered]', 3 | ||
|
|
74 | + end | ||
|
|
75 | + end | ||
|
|
76 | + | ||
|
|
77 | + def test_prev_next_links_have_classnames | ||
|
|
78 | + paginate do |pagination| | ||
|
|
79 | + assert_select 'span.disabled.prev_page:first-child' | ||
|
|
80 | + assert_select 'a.next_page[href]:last-child' | ||
|
|
81 | + end | ||
|
|
82 | + end | ||
|
|
83 | + | ||
|
|
84 | + def test_prev_label_deprecated | ||
|
|
85 | + assert_deprecated ':previous_label' do | ||
|
|
86 | + paginate({ :page => 2 }, :prev_label => 'Deprecated') do | ||
|
|
87 | + assert_select 'a[href]:first-child', 'Deprecated' | ||
|
|
88 | + end | ||
|
|
89 | + end | ||
|
|
90 | + end | ||
|
|
91 | + | ||
|
|
92 | + def test_full_output | ||
|
|
93 | + paginate | ||
|
|
94 | + expected = <<-HTML | ||
|
|
95 | + <div class="pagination"><span class="disabled prev_page">« Previous</span> | ||
|
|
96 | + <span class="current">1</span> | ||
|
|
97 | + <a href="/foo/bar?page=2" rel="next">2</a> | ||
|
|
98 | + <a href="/foo/bar?page=3">3</a> | ||
|
|
99 | + <a href="/foo/bar?page=2" class="next_page" rel="next">Next »</a></div> | ||
|
|
100 | + HTML | ||
|
|
101 | + expected.strip!.gsub!(/\s{2,}/, ' ') | ||
|
|
102 | + | ||
|
|
103 | + assert_dom_equal expected, @html_result | ||
|
|
104 | + end | ||
|
|
105 | + | ||
|
|
106 | + def test_escaping_of_urls | ||
|
|
107 | + paginate({:page => 1, :per_page => 1, :total_entries => 2}, | ||
|
|
108 | + :page_links => false, :params => { :tag => '<br>' }) | ||
|
|
109 | + | ||
|
|
110 | + assert_select 'a[href]', 1 do |links| | ||
|
|
111 | + query = links.first['href'].split('?', 2)[1] | ||
|
|
112 | + assert_equal %w(page=2 tag=%3Cbr%3E), query.split('&').sort | ||
|
|
113 | + end | ||
|
|
114 | + end | ||
|
|
115 | + | ||
|
|
116 | + ## advanced options for pagination ## | ||
|
|
117 | + | ||
|
|
118 | + def test_will_paginate_without_container | ||
|
|
119 | + paginate({}, :container => false) | ||
|
|
120 | + assert_select 'div.pagination', 0, 'main DIV present when it shouldn\'t' | ||
|
|
121 | + assert_select 'a[href]', 3 | ||
|
|
122 | + end | ||
|
|
123 | + | ||
|
|
124 | + def test_will_paginate_without_page_links | ||
|
|
125 | + paginate({ :page => 2 }, :page_links => false) do | ||
|
|
126 | + assert_select 'a[href]', 2 do |elements| | ||
|
|
127 | + validate_page_numbers [1,3], elements | ||
|
|
128 | + end | ||
|
|
129 | + end | ||
|
|
130 | + end | ||
|
|
131 | + | ||
|
|
132 | + def test_will_paginate_windows | ||
|
|
133 | + paginate({ :page => 6, :per_page => 1 }, :inner_window => 1) do |pagination| | ||
|
|
134 | + assert_select 'a[href]', 8 do |elements| | ||
|
|
135 | + validate_page_numbers [5,1,2,5,7,10,11,7], elements | ||
|
|
136 | + assert_select elements.first, 'a', '« Previous' | ||
|
|
137 | + assert_select elements.last, 'a', 'Next »' | ||
|
|
138 | + end | ||
|
|
139 | + assert_select 'span.current', '6' | ||
|
|
140 | + assert_equal '« Previous 1 2 … 5 6 7 … 10 11 Next »', pagination.first.inner_text | ||
|
|
141 | + end | ||
|
|
142 | + end | ||
|
|
143 | + | ||
|
|
144 | + def test_will_paginate_eliminates_small_gaps | ||
|
|
145 | + paginate({ :page => 6, :per_page => 1 }, :inner_window => 2) do | ||
|
|
146 | + assert_select 'a[href]', 12 do |elements| | ||
|
|
147 | + validate_page_numbers [5,1,2,3,4,5,7,8,9,10,11,7], elements | ||
|
|
148 | + end | ||
|
|
149 | + end | ||
|
|
150 | + end | ||
|
|
151 | + | ||
|
|
152 | + def test_container_id | ||
|
|
153 | + paginate do |div| | ||
|
|
154 | + assert_nil div.first['id'] | ||
|
|
155 | + end | ||
|
|
156 | + | ||
|
|
157 | + # magic ID | ||
|
|
158 | + paginate({}, :id => true) do |div| | ||
|
|
159 | + assert_equal 'fixnums_pagination', div.first['id'] | ||
|
|
160 | + end | ||
|
|
161 | + | ||
|
|
162 | + # explicit ID | ||
|
|
163 | + paginate({}, :id => 'custom_id') do |div| | ||
|
|
164 | + assert_equal 'custom_id', div.first['id'] | ||
|
|
165 | + end | ||
|
|
166 | + end | ||
|
|
167 | + | ||
|
|
168 | + ## other helpers ## | ||
|
|
169 | + | ||
|
|
170 | + def test_paginated_section | ||
|
|
171 | + @template = <<-ERB | ||
|
|
172 | + <% paginated_section collection, options do %> | ||
|
|
173 | + <%= content_tag :div, '', :id => "developers" %> | ||
|
|
174 | + <% end %> | ||
|
|
175 | + ERB | ||
|
|
176 | + | ||
|
|
177 | + paginate | ||
|
|
178 | + assert_select 'div.pagination', 2 | ||
|
|
179 | + assert_select 'div.pagination + div#developers', 1 | ||
|
|
180 | + end | ||
|
|
181 | + | ||
|
|
182 | + def test_page_entries_info | ||
|
|
183 | + @template = '<%= page_entries_info collection %>' | ||
|
|
184 | + array = ('a'..'z').to_a | ||
|
|
185 | + | ||
|
|
186 | + paginate array.paginate(:page => 2, :per_page => 5) | ||
|
|
187 | + assert_equal %{Displaying strings <b>6 - 10</b> of <b>26</b> in total}, | ||
|
|
188 | + @html_result | ||
|
|
189 | + | ||
|
|
190 | + paginate array.paginate(:page => 7, :per_page => 4) | ||
|
|
191 | + assert_equal %{Displaying strings <b>25 - 26</b> of <b>26</b> in total}, | ||
|
|
192 | + @html_result | ||
|
|
193 | + end | ||
|
|
194 | + | ||
|
|
195 | + uses_mocha 'class name' do | ||
|
|
196 | + def test_page_entries_info_with_longer_class_name | ||
|
|
197 | + @template = '<%= page_entries_info collection %>' | ||
|
|
198 | + collection = ('a'..'z').to_a.paginate | ||
|
|
199 | + collection.first.stubs(:class).returns(mock('class', :name => 'ProjectType')) | ||
|
|
200 | + | ||
|
|
201 | + paginate collection | ||
|
|
202 | + assert @html_result.index('project types'), "expected <#{@html_result.inspect}> to mention 'project types'" | ||
|
|
203 | + end | ||
|
|
204 | + end | ||
|
|
205 | + | ||
|
|
206 | + def test_page_entries_info_with_single_page_collection | ||
|
|
207 | + @template = '<%= page_entries_info collection %>' | ||
|
|
208 | + | ||
|
|
209 | + paginate(('a'..'d').to_a.paginate(:page => 1, :per_page => 5)) | ||
|
|
210 | + assert_equal %{Displaying <b>all 4</b> strings}, @html_result | ||
|
|
211 | + | ||
|
|
212 | + paginate(['a'].paginate(:page => 1, :per_page => 5)) | ||
|
|
213 | + assert_equal %{Displaying <b>1</b> string}, @html_result | ||
|
|
214 | + | ||
|
|
215 | + paginate([].paginate(:page => 1, :per_page => 5)) | ||
|
|
216 | + assert_equal %{No entries found}, @html_result | ||
|
|
217 | + end | ||
|
|
218 | + | ||
|
|
219 | + def test_page_entries_info_with_custom_entry_name | ||
|
|
220 | + @template = '<%= page_entries_info collection, :entry_name => "author" %>' | ||
|
|
221 | + | ||
|
|
222 | + entries = (1..20).to_a | ||
|
|
223 | + | ||
|
|
224 | + paginate(entries.paginate(:page => 1, :per_page => 5)) | ||
|
|
225 | + assert_equal %{Displaying authors <b>1 - 5</b> of <b>20</b> in total}, @html_result | ||
|
|
226 | + | ||
|
|
227 | + paginate(entries.paginate(:page => 1, :per_page => 20)) | ||
|
|
228 | + assert_equal %{Displaying <b>all 20</b> authors}, @html_result | ||
|
|
229 | + | ||
|
|
230 | + paginate(['a'].paginate(:page => 1, :per_page => 5)) | ||
|
|
231 | + assert_equal %{Displaying <b>1</b> author}, @html_result | ||
|
|
232 | + | ||
|
|
233 | + paginate([].paginate(:page => 1, :per_page => 5)) | ||
|
|
234 | + assert_equal %{No authors found}, @html_result | ||
|
|
235 | + end | ||
|
|
236 | + | ||
|
|
237 | + ## parameter handling in page links ## | ||
|
|
238 | + | ||
|
|
239 | + def test_will_paginate_preserves_parameters_on_get | ||
|
|
240 | + @request.params :foo => { :bar => 'baz' } | ||
|
|
241 | + paginate | ||
|
|
242 | + assert_links_match /foo%5Bbar%5D=baz/ | ||
|
|
243 | + end | ||
|
|
244 | + | ||
|
|
245 | + def test_will_paginate_doesnt_preserve_parameters_on_post | ||
|
|
246 | + @request.post | ||
|
|
247 | + @request.params :foo => 'bar' | ||
|
|
248 | + paginate | ||
|
|
249 | + assert_no_links_match /foo=bar/ | ||
|
|
250 | + end | ||
|
|
251 | + | ||
|
|
252 | + def test_adding_additional_parameters | ||
|
|
253 | + paginate({}, :params => { :foo => 'bar' }) | ||
|
|
254 | + assert_links_match /foo=bar/ | ||
|
|
255 | + end | ||
|
|
256 | + | ||
|
|
257 | + def test_adding_anchor_parameter | ||
|
|
258 | + paginate({}, :params => { :anchor => 'anchor' }) | ||
|
|
259 | + assert_links_match /#anchor$/ | ||
|
|
260 | + end | ||
|
|
261 | + | ||
|
|
262 | + def test_removing_arbitrary_parameters | ||
|
|
263 | + @request.params :foo => 'bar' | ||
|
|
264 | + paginate({}, :params => { :foo => nil }) | ||
|
|
265 | + assert_no_links_match /foo=bar/ | ||
|
|
266 | + end | ||
|
|
267 | + | ||
|
|
268 | + def test_adding_additional_route_parameters | ||
|
|
269 | + paginate({}, :params => { :controller => 'baz', :action => 'list' }) | ||
|
|
270 | + assert_links_match %r{\Wbaz/list\W} | ||
|
|
271 | + end | ||
|
|
272 | + | ||
|
|
273 | + def test_will_paginate_with_custom_page_param | ||
|
|
274 | + paginate({ :page => 2 }, :param_name => :developers_page) do | ||
|
|
275 | + assert_select 'a[href]', 4 do |elements| | ||
|
|
276 | + validate_page_numbers [1,1,3,3], elements, :developers_page | ||
|
|
277 | + end | ||
|
|
278 | + end | ||
|
|
279 | + end | ||
|
|
280 | + | ||
|
|
281 | + def test_will_paginate_with_atmark_url | ||
|
|
282 | + @request.symbolized_path_parameters[:action] = "@tag" | ||
|
|
283 | + renderer = WillPaginate::LinkRenderer.new | ||
|
|
284 | + | ||
|
|
285 | + paginate({ :page => 1 }, :renderer=>renderer) | ||
|
|
286 | + assert_links_match %r[/foo/@tag\?page=\d] | ||
|
|
287 | + end | ||
|
|
288 | + | ||
|
|
289 | + def test_complex_custom_page_param | ||
|
|
290 | + @request.params :developers => { :page => 2 } | ||
|
|
291 | + | ||
|
|
292 | + paginate({ :page => 2 }, :param_name => 'developers[page]') do | ||
|
|
293 | + assert_select 'a[href]', 4 do |links| | ||
|
|
294 | + assert_links_match /\?developers%5Bpage%5D=\d+$/, links | ||
|
|
295 | + validate_page_numbers [1,1,3,3], links, 'developers[page]' | ||
|
|
296 | + end | ||
|
|
297 | + end | ||
|
|
298 | + end | ||
|
|
299 | + | ||
|
|
300 | + def test_custom_routing_page_param | ||
|
|
301 | + @request.symbolized_path_parameters.update :controller => 'dummy', :action => nil | ||
|
|
302 | + paginate :per_page => 2 do | ||
|
|
303 | + assert_select 'a[href]', 6 do |links| | ||
|
|
304 | + assert_links_match %r{/page/(\d+)$}, links, [2, 3, 4, 5, 6, 2] | ||
|
|
305 | + end | ||
|
|
306 | + end | ||
|
|
307 | + end | ||
|
|
308 | + | ||
|
|
309 | + def test_custom_routing_page_param_with_dot_separator | ||
|
|
310 | + @request.symbolized_path_parameters.update :controller => 'dummy', :action => 'dots' | ||
|
|
311 | + paginate :per_page => 2 do | ||
|
|
312 | + assert_select 'a[href]', 6 do |links| | ||
|
|
313 | + assert_links_match %r{/page\.(\d+)$}, links, [2, 3, 4, 5, 6, 2] | ||
|
|
314 | + end | ||
|
|
315 | + end | ||
|
|
316 | + end | ||
|
|
317 | + | ||
|
|
318 | + def test_custom_routing_with_first_page_hidden | ||
|
|
319 | + @request.symbolized_path_parameters.update :controller => 'ibocorp', :action => nil | ||
|
|
320 | + paginate :page => 2, :per_page => 2 do | ||
|
|
321 | + assert_select 'a[href]', 7 do |links| | ||
|
|
322 | + assert_links_match %r{/ibocorp(?:/(\d+))?$}, links, [nil, nil, 3, 4, 5, 6, 3] | ||
|
|
323 | + end | ||
|
|
324 | + end | ||
|
|
325 | + end | ||
|
|
326 | + | ||
|
|
327 | + ## internal hardcore stuff ## | ||
|
|
328 | + | ||
|
|
329 | + class LegacyCollection < WillPaginate::Collection | ||
|
|
330 | + alias :page_count :total_pages | ||
|
|
331 | + undef :total_pages | ||
|
|
332 | + end | ||
|
|
333 | + | ||
|
|
334 | + def test_deprecation_notices_with_page_count | ||
|
|
335 | + collection = LegacyCollection.new(1, 1, 2) | ||
|
|
336 | + | ||
|
|
337 | + assert_deprecated collection.class.name do | ||
|
|
338 | + paginate collection | ||
|
|
339 | + end | ||
|
|
340 | + end | ||
|
|
341 | + | ||
|
|
342 | + uses_mocha 'view internals' do | ||
|
|
343 | + def test_collection_name_can_be_guessed | ||
|
|
344 | + collection = mock | ||
|
|
345 | + collection.expects(:total_pages).returns(1) | ||
|
|
346 | + | ||
|
|
347 | + @template = '<%= will_paginate options %>' | ||
|
|
348 | + @controller.controller_name = 'developers' | ||
|
|
349 | + @view.assigns['developers'] = collection | ||
|
|
350 | + | ||
|
|
351 | + paginate(nil) | ||
|
|
352 | + end | ||
|
|
353 | + end | ||
|
|
354 | + | ||
|
|
355 | + def test_inferred_collection_name_raises_error_when_nil | ||
|
|
356 | + @template = '<%= will_paginate options %>' | ||
|
|
357 | + @controller.controller_name = 'developers' | ||
|
|
358 | + | ||
|
|
359 | + e = assert_raise ArgumentError do | ||
|
|
360 | + paginate(nil) | ||
|
|
361 | + end | ||
|
|
362 | + assert e.message.include?('@developers') | ||
|
|
363 | + end | ||
|
|
364 | + | ||
|
|
365 | + if ActionController::Base.respond_to? :rescue_responses | ||
|
|
366 | + # only on Rails 2 | ||
|
|
367 | + def test_rescue_response_hook_presence | ||
|
|
368 | + assert_equal :not_found, | ||
|
|
369 | + ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] | ||
|
|
370 | + end | ||
|
|
371 | + end | ||
|
|
372 | + | ||
|
|
373 | + end |
@@ -0,0 +1,21 | |||||
|
|
1 | + require File.expand_path('../lib/will_paginate/version', __FILE__) | ||
|
|
2 | + | ||
|
|
3 | + Gem::Specification.new do |gem| | ||
|
|
4 | + gem.name = 'will_paginate' | ||
|
|
5 | + gem.version = WillPaginate::VERSION::STRING | ||
|
|
6 | + gem.date = Date.today.to_s | ||
|
|
7 | + | ||
|
|
8 | + gem.summary = "Pagination for Rails" | ||
|
|
9 | + gem.description = "The will_paginate library provides a simple, yet powerful and extensible API for ActiveRecord pagination and rendering of pagination links in ActionView templates." | ||
|
|
10 | + | ||
|
|
11 | + gem.authors = ['Mislav Marohnić', 'PJ Hyett'] | ||
|
|
12 | + gem.email = 'mislav.marohnic@gmail.com' | ||
|
|
13 | + gem.homepage = 'http://github.com/mislav/will_paginate/wikis' | ||
|
|
14 | + | ||
|
|
15 | + gem.rubyforge_project = nil | ||
|
|
16 | + gem.has_rdoc = true | ||
|
|
17 | + gem.rdoc_options = ['--main', 'README.rdoc', '--charset=UTF-8'] | ||
|
|
18 | + gem.extra_rdoc_files = ['README.rdoc', 'LICENSE', 'CHANGELOG.rdoc'] | ||
|
|
19 | + | ||
|
|
20 | + gem.files = Dir['Rakefile', '{bin,lib,rails,test,spec}/**/*', 'README*', 'LICENSE*'] & `git ls-files -z`.split("\0") | ||
|
|
21 | + end |
@@ -16,7 +16,8 | |||||
|
16 | :redirect_to => { :action => :list } |
|
16 | :redirect_to => { :action => :list } |
|
17 |
|
17 | ||
|
18 | def list |
|
18 | def list |
|
19 |
- @user |
|
19 | + @user_count = User.count |
|
|
20 | + @users = User.paginate :page => params[:page] | ||
|
20 | @hidden_columns = ['hashed_password', 'salt', 'created_at', 'updated_at'] |
|
21 | @hidden_columns = ['hashed_password', 'salt', 'created_at', 'updated_at'] |
|
21 | @contests = Contest.enabled |
|
22 | @contests = Contest.enabled |
|
22 | end |
|
23 | end |
@@ -54,6 +54,10 | |||||
|
54 | before_save :encrypt_new_password |
|
54 | before_save :encrypt_new_password |
|
55 | before_save :assign_default_site |
|
55 | before_save :assign_default_site |
|
56 |
|
56 | ||
|
|
57 | + # this is for will_paginate | ||
|
|
58 | + cattr_reader :per_page | ||
|
|
59 | + @@per_page = 50 | ||
|
|
60 | + | ||
|
57 | def self.authenticate(login, password) |
|
61 | def self.authenticate(login, password) |
|
58 | user = find_by_login(login) |
|
62 | user = find_by_login(login) |
|
59 | return user if user && user.authenticated?(password) |
|
63 | return user if user && user.authenticated?(password) |
@@ -44,6 +44,7 | |||||
|
44 | <% end %> |
|
44 | <% end %> |
|
45 | </div> |
|
45 | </div> |
|
46 |
|
46 | ||
|
|
47 | + Total <%= @user_count %> users | <%= will_paginate @users, :container => false %> | ||
|
47 | <table class="info"> |
|
48 | <table class="info"> |
|
48 | <tr class="info-head"> |
|
49 | <tr class="info-head"> |
|
49 | <% for column in User.content_columns %> |
|
50 | <% for column in User.content_columns %> |
@@ -70,8 +71,7 | |||||
|
70 | <% end %> |
|
71 | <% end %> |
|
71 | </table> |
|
72 | </table> |
|
72 |
|
73 | ||
|
73 | - |
|
||
|
74 | <br /> |
|
74 | <br /> |
|
75 |
|
75 | ||
|
76 | - <%= link_to 'New user', :action => 'new' %> |
|
76 | + <%= link_to '[New user]', :action => 'new' %> |
|
77 | - <%= link_to 'New list of users', :action => 'new_list' %> |
|
77 | + <%= link_to '[New list of users]', :action => 'new_list' %> |
You need to be logged in to leave comments.
Login now