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 | 16 | :redirect_to => { :action => :list } |
|
17 | 17 | |
|
18 | 18 | def list |
|
19 |
- @user |
|
|
19 | + @user_count = User.count | |
|
20 | + @users = User.paginate :page => params[:page] | |
|
20 | 21 | @hidden_columns = ['hashed_password', 'salt', 'created_at', 'updated_at'] |
|
21 | 22 | @contests = Contest.enabled |
|
22 | 23 | end |
@@ -54,6 +54,10 | |||
|
54 | 54 | before_save :encrypt_new_password |
|
55 | 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 | 61 | def self.authenticate(login, password) |
|
58 | 62 | user = find_by_login(login) |
|
59 | 63 | return user if user && user.authenticated?(password) |
@@ -44,6 +44,7 | |||
|
44 | 44 | <% end %> |
|
45 | 45 | </div> |
|
46 | 46 | |
|
47 | + Total <%= @user_count %> users | <%= will_paginate @users, :container => false %> | |
|
47 | 48 | <table class="info"> |
|
48 | 49 | <tr class="info-head"> |
|
49 | 50 | <% for column in User.content_columns %> |
@@ -70,8 +71,7 | |||
|
70 | 71 | <% end %> |
|
71 | 72 | </table> |
|
72 | 73 | |
|
73 | - | |
|
74 | 74 | <br /> |
|
75 | 75 | |
|
76 | - <%= link_to 'New user', :action => 'new' %> | |
|
77 | - <%= link_to 'New list of users', :action => 'new_list' %> | |
|
76 | + <%= link_to '[New user]', :action => 'new' %> | |
|
77 | + <%= link_to '[New list of users]', :action => 'new_list' %> |
You need to be logged in to leave comments.
Login now