EliteJournal 1.9.403 → 1.9.480

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. data/app/controllers/application.rb +6 -4
  2. data/app/controllers/atom_controller.rb +10 -17
  3. data/app/controllers/post_controller.rb +67 -36
  4. data/app/controllers/rss_controller.rb +12 -17
  5. data/app/controllers/tags_controller.rb +2 -2
  6. data/app/controllers/users_controller.rb +9 -3
  7. data/app/helpers/application_helper.rb +13 -23
  8. data/app/helpers/pagination_helper.rb +487 -0
  9. data/app/helpers/tags_helper.rb +1 -1
  10. data/app/models/post.rb +2 -0
  11. data/app/models/user.rb +4 -4
  12. data/app/views/account/info.rhtml +4 -0
  13. data/app/views/atom/feed.rxml +2 -2
  14. data/app/views/css/edit.rhtml +1 -1
  15. data/app/views/layouts/application.rhtml +5 -5
  16. data/app/views/{journal → post}/_comment.rhtml +1 -1
  17. data/app/views/{journal → post}/_post.rhtml +3 -3
  18. data/app/views/{journal → post}/_trackback.rhtml +0 -0
  19. data/app/views/{journal → post}/error.rhtml +0 -0
  20. data/app/views/post/index.rhtml +5 -0
  21. data/app/views/post/reply.rhtml +21 -12
  22. data/app/views/post/replyxml.rxml +2 -2
  23. data/app/views/{journal → post}/view.rhtml +0 -0
  24. data/app/views/rss/{index.rxml → feed.rxml} +1 -1
  25. data/app/views/users/index.rhtml +1 -1
  26. data/config/environment.rb +22 -9
  27. data/config/routes.rb +36 -0
  28. data/db/db-mysql.sql +2 -0
  29. data/db/db-postgresql.sql +2 -0
  30. data/db/db-sqlite.sql +2 -0
  31. data/db/development_structure.sql +67 -65
  32. data/elitejournal +3 -40
  33. data/public/dispatch.rb +2 -2
  34. metadata +12 -22
  35. data/app/controllers/journal_controller.rb +0 -53
  36. data/app/helpers/journal_helper.rb +0 -2
  37. data/app/views/journal/index.rhtml +0 -1
@@ -2,7 +2,7 @@ require_dependency 'user'
2
2
 
3
3
  class ApplicationController < ActionController::Base
4
4
  before_filter :configure_app, :except => [ :search, :face ]
5
- before_filter :collect_info, :except => [ :search, :face ]
5
+ before_filter :collect_info, :except => [ :search, :face, :feed ]
6
6
 
7
7
  private
8
8
  def current_user
@@ -25,8 +25,7 @@ class ApplicationController < ActionController::Base
25
25
  helper_method :owns_post?
26
26
 
27
27
  def user_page?
28
- user_actions = ['view', 'reply', 'info']
29
- ((@params['controller'] == 'users') && (@params['action'] != 'index')) || (user_actions.include? @params['action'])
28
+ @params['user'] ? true : false
30
29
  end
31
30
  helper_method :user_page?
32
31
 
@@ -40,7 +39,7 @@ class ApplicationController < ActionController::Base
40
39
  end
41
40
 
42
41
  def redirect_to_main
43
- redirect_to :controller => 'journal', :action => 'index'
42
+ redirect_to :controller => 'post', :action => 'index'
44
43
  end
45
44
 
46
45
  def redirect_to_user
@@ -65,6 +64,9 @@ class ApplicationController < ActionController::Base
65
64
 
66
65
  def configure_app
67
66
  @app_config = YAML::load(File.open(File.dirname(__FILE__) + "/../../config/app.yml"))
67
+ if @params['user']
68
+ @user = User.find_by_username(@params['user']) || User.new
69
+ end
68
70
  end
69
71
 
70
72
  def collect_info
@@ -5,22 +5,15 @@ class AtomController < ApplicationController
5
5
  caches_page :feed
6
6
 
7
7
  def feed
8
- @posts = Post.find_all(nil, 'updated_at DESC', @app_config['main']['num_posts'])
9
- @title = @app_config['main']['title']
10
- @subtitle = @app_config['main']['subtitle']
8
+ if @params['user']
9
+ @title = @user.title
10
+ @subtitle = @user.subtitle
11
+ @posts = @user.posts.find_all(nil, 'created_at desc', @app_config['main']['num_posts']) rescue []
12
+ else
13
+ @posts = Post.find_all(nil, 'updated_at DESC', @app_config['main']['num_posts'])
14
+ @title = @app_config['main']['title']
15
+ @subtitle = @app_config['main']['subtitle']
16
+ end
17
+ #cache_page if ActionController::Base.perform_caching
11
18
  end
12
-
13
- # +method_missing+ provides the convenient /atom/username URLs to
14
- # access a user's feed. If a non-existent user is passed, it will
15
- # just return an empty feed.
16
- def method_missing(method)
17
- user = User.find_by_username(method.to_s)
18
- @title = user.title
19
- @subtitle = user.subtitle
20
- @posts = user.posts.find_all(nil, 'created_at desc', @app_config['main']['num_posts']) rescue []
21
-
22
- render_action 'feed'
23
- cache_page if ActionController::Base.perform_caching
24
- end
25
-
26
19
  end
@@ -1,11 +1,52 @@
1
1
  require_dependency 'trackback'
2
2
 
3
3
  class PostController < ApplicationController
4
- before_filter :auth_required, :except => [:reply, :replyxml]
4
+ before_filter :auth_required, :except => [:index, :reply, :replyxml, :view, :range]
5
5
  cache_sweeper :feed_killer, :only => [:new, :edit, :destroy, :destroyxml]
6
+ helper :pagination
6
7
 
7
8
  def index
8
- redirect_to :action => 'new'
9
+ @post_pages, @posts = paginate :posts,
10
+ :order_by => 'created_at DESC',
11
+ :per_page => @app_config['main']['num_posts']
12
+ end
13
+
14
+ def view
15
+ @post = Post.find_by_sql("SELECT p.* FROM posts p, users u WHERE p.slug='#{@params['slug']}' AND u.username='#{@params['user']}' and p.user_id=u.id")[0]
16
+ redirect_to_main and return if @post.nil?
17
+ @comments = @post.find_all_in_comments("comment_id=0")
18
+ end
19
+
20
+ # This is used for fancy URL viewing with URLs of the format /year/month/day.
21
+ def range
22
+ unless @params['year']
23
+ @error = 'Invalid date.'
24
+ render 'post/error'
25
+ return
26
+ end
27
+ if @params['day'] and !@params['month']
28
+ @error = 'Invalid date.'
29
+ render 'post/error'
30
+ return
31
+ end
32
+
33
+ year = @params['year']
34
+ fmonth = @params['month'] || '01'
35
+ tmonth = @params['month'] ? fmonth : '12'
36
+ fday = @params['day'] || '01'
37
+ tday = @params['day'] ? fday : '31'
38
+
39
+
40
+ tday = @params['day'] ? fday : (Time.mktime(year.to_i, tmonth.to_i + 1, 1) - 1.day).day.to_s rescue fday
41
+
42
+ if @params['user']
43
+ userid = User.find_by_username(@params['user']).id
44
+ @posts = Post.find_all(["created_at BETWEEN ? AND ? AND user_id=?", "#{year}-#{fmonth}-#{fday} 00:00:00", "#{year}-#{tmonth}-#{tday} 23:59:59", userid], 'created_at DESC') rescue []
45
+ else
46
+ @posts = Post.find_all(["created_at BETWEEN ? AND ?", "#{year}-#{fmonth}-#{fday} 00:00:00", "#{year}-#{tmonth}-#{tday} 23:59:59"], 'created_at DESC') rescue []
47
+ end
48
+
49
+ render_action 'index'
9
50
  end
10
51
 
11
52
  def new
@@ -34,7 +75,7 @@ class PostController < ApplicationController
34
75
  return
35
76
  end
36
77
  end
37
-
78
+
38
79
  def edit
39
80
  begin
40
81
  @post = current_user.posts.find(@params['id'])
@@ -42,7 +83,7 @@ class PostController < ApplicationController
42
83
  redirect_to_main
43
84
  return
44
85
  end
45
-
86
+
46
87
  if @request.post?
47
88
  @post.attributes = @params['post']
48
89
  if @post.save
@@ -51,7 +92,7 @@ class PostController < ApplicationController
51
92
  end
52
93
  end
53
94
  end
54
-
95
+
55
96
  def postxml
56
97
  @post = current_user.posts.find(@params['id']) rescue Post.new
57
98
  @post.update_attributes @params['post'] if @request.post?
@@ -62,17 +103,17 @@ class PostController < ApplicationController
62
103
  current_user.posts.find(@params['id']).destroy rescue nil
63
104
  redirect_to_main
64
105
  end
65
-
106
+
66
107
  def destroyxml
67
108
  @postid = @params['id']
68
109
  current_user.posts.find(@params['id']).destroy rescue nil
69
110
  end
70
-
111
+
71
112
  def destroy_comment
72
113
  Comment.find(@params['id']).destroy rescue nil
73
114
  redirect_to @request.env['HTTP_REFERER']
74
115
  end
75
-
116
+
76
117
  def toggle_commenting
77
118
  if (post = current_user.posts.find(@params['id']) rescue nil)
78
119
  post.allows_comment = (not post.allows_comment?)
@@ -89,37 +130,27 @@ class PostController < ApplicationController
89
130
  end
90
131
 
91
132
  def reply
92
- begin
93
- @post = Post.find(@params['id'])
94
- @user = @post.user
95
- unless @post.allows_comment?
96
- redirect_to :controller => 'journal', :action => 'view', :id => @post.id
97
- return
98
- end
99
-
100
- @comments = @post.find_all_in_comments("comment_id=0")
101
-
102
- if @request.post?
103
- @newcomment = @post.comments.create(@params['newcomment'])
104
- unless @newcomment.save
105
- @error = @newcomment.errors.full_messages
106
- render 'journal/error'
107
- return
108
- end
109
- redirect_to :controller => 'journal', :action => 'view', :id => @newcomment.post_id
110
- return
111
- end
133
+ @post = Post.find_by_slug(@params['slug'])
134
+ unless @post.allows_comment?
135
+ redirect_to :controller => 'post', :action => 'view', :user => @post.user.username, :slug => @post.slug
136
+ return
137
+ end
112
138
 
113
- # A little hack to pre-load the subject line.
114
- # Do this down here so it doesn't get called for the POST.
115
- @newcomment = Comment.new('subject' => 'Re: ' + @post.subject)
116
- if @params['comment_id']
117
- @comment = Comment.find(@params['comment_id'])
118
- end
139
+ if @params['id']
140
+ @comment = Comment.find @params['id']
141
+ end
119
142
 
120
- rescue ActiveRecord::RecordNotFound
121
- redirect_to_main
143
+
144
+ if @request.post?
145
+ @newcomment = @comment ? @comment.comments.create(@params['newcomment']) : @post.comments.create(@params['newcomment'])
146
+ if @newcomment.save
147
+ redirect_to :controller => 'post', :action => 'view', :user => @post.user.username, :slug => @post.slug
148
+ end
122
149
  end
150
+
151
+ # A little hack to pre-load the subject line.
152
+ # Do this down here so it doesn't get called for the POST.
153
+ @newcomment = Comment.new('subject' => 'Re: ' + @post.subject)
123
154
  end
124
155
 
125
156
  def replyxml
@@ -1,22 +1,17 @@
1
1
  class RssController < ApplicationController
2
2
  layout nil
3
- caches_page :index
3
+ caches_page :feed
4
4
 
5
- def index
6
- @posts = Post.find_all(nil, 'updated_at DESC', @app_config['main']['num_posts'])
7
- @title = @app_config['main']['title']
8
- @subtitle = @app_config['main']['subtitle']
9
- end
10
-
11
- # +method_missing+ provides the convenient /atom/username URLs to
12
- # access a user's feed. If a non-existent user is passed, it will
13
- # just return an empty feed.
14
- def method_missing(method)
15
- user = User.find_by_username(method.to_s)
16
- @title = user.title
17
- @subtitle = user.subtitle
18
- @posts = user.posts.find_all(nil, 'created_at desc', @app_config['main']['num_posts']) rescue []
19
- render_action 'index'
20
- cache_page if ActionController::Base.perform_caching
5
+ def feed
6
+ if @params['user']
7
+ @title = @user.title
8
+ @subtitle = @user.subtitle
9
+ @posts = @user.posts.find_all(nil, 'created_at desc', @app_config['main']['num_posts']) rescue []
10
+ else
11
+ @posts = Post.find_all(nil, 'updated_at DESC', @app_config['main']['num_posts'])
12
+ @title = @app_config['main']['title']
13
+ @subtitle = @app_config['main']['subtitle']
14
+ end
15
+ #cache_page if ActionController::Base.perform_caching
21
16
  end
22
17
  end
@@ -6,7 +6,7 @@ class TagsController < ApplicationController
6
6
  def show
7
7
  @tag = Tag.find_by_tag(CGI.unescape(@params['tag'])) rescue Tag.new
8
8
  @posts = @tag.posts
9
- render 'journal/index'
9
+ render 'post/index'
10
10
  end
11
11
 
12
12
  def add
@@ -22,7 +22,7 @@ class TagsController < ApplicationController
22
22
  end
23
23
  end
24
24
  end
25
- redirect_to :controller => 'journal', :action => 'view', :id => post.id
25
+ redirect_to :controller => 'post', :action => 'view', :id => post.id
26
26
  end
27
27
 
28
28
  def addxml
@@ -1,4 +1,6 @@
1
1
  class UsersController < ApplicationController
2
+ helper :pagination
3
+
2
4
  def index
3
5
  @users = User.find_all nil, 'username'
4
6
  end
@@ -6,8 +8,12 @@ class UsersController < ApplicationController
6
8
  def method_missing(method)
7
9
  @user = User.find_by_username(method.to_s)
8
10
  redirect_to_main and return unless @user
9
- @posts = @user.posts.find_all(nil, 'created_at desc', @app_config['main']['num_posts'])
10
-
11
- render 'journal/index'
11
+
12
+ @post_pages, @posts = paginate :posts,
13
+ :conditions => ['user_id = ?', @user.id],
14
+ :order_by => 'created_at DESC',
15
+ :per_page => @user.posts_per_page
16
+
17
+ render 'post/index'
12
18
  end
13
19
  end
@@ -1,18 +1,6 @@
1
1
  # The methods added to this helper will be available to all templates in the application.
2
2
  module ApplicationHelper
3
- def link_to_year(year)
4
- content_tag "a", year, {'href'=> @app_config['main']['app_base'] + year}
5
- end
6
-
7
- def link_to_month(year, month, name)
8
- content_tag "a", name, {'href'=> @app_config['main']['app_base'] + year + '/' + month}
9
- end
10
-
11
- def link_to_day(year, month, day, name)
12
- content_tag "a", name, {'href'=> @app_config['main']['app_base'] + year + '/' + month + '/' + day}
13
- end
14
-
15
- def sexifytime(t)
3
+ def sexifytime(t, username=nil)
16
4
  day = case t.day % 10
17
5
  when 0
18
6
  "#{t.day}th"
@@ -30,9 +18,10 @@ module ApplicationHelper
30
18
 
31
19
 
32
20
  str = ''
33
- str << link_to_day(t.strftime('%Y'), t.strftime('%m'), t.strftime('%d'), day) << ' '
34
- str << link_to_month(t.strftime('%Y'), t.strftime('%m'), t.strftime('%B')) << ' '
35
- str << link_to_year(t.strftime('%Y')) << ', '
21
+ str << link_to(day, :controller => 'post', :action => 'range', :user => username, :year => t.strftime('%Y'), :month => t.strftime('%m'), :day => t.strftime('%d'))
22
+ str << ' ' << link_to(t.strftime('%B'), :controller => 'post', :action => 'range', :user => username, :year => t.strftime('%Y'), :month => t.strftime('%m'), :day => nil)
23
+ str << ' ' << link_to(t.strftime('%Y'), :controller => 'post', :action => 'range', :user => username, :year => t.strftime('%Y'), :month => nil, :day => nil)
24
+ str << ', '
36
25
 
37
26
  str << case t.hour
38
27
  when 0..2
@@ -69,7 +58,7 @@ module ApplicationHelper
69
58
  end
70
59
 
71
60
  def tag_links(post, limit = nil)
72
- tags = post.tags.sort_by {|tag| tag.numitems}.reverse.map { |tag| link_to(tag.tag, :controller => 'tags', :action => 'show', :params => {'tag' => CGI.escape(tag.tag)}) }
61
+ tags = post.tags.sort_by {|tag| tag.numitems}.reverse.map { |tag| link_to(tag.tag, :controller => 'tags', :action => 'show', :tag => CGI.escape(tag.tag)) }
73
62
  (limit ? tags.first(limit) : tags ).join(', ')
74
63
  end
75
64
 
@@ -86,17 +75,17 @@ module ApplicationHelper
86
75
  end
87
76
 
88
77
  str = '<ul class="postlinks">'
89
- str << "<li #{disp} id=\"replylink_#{post.id}\">" + link_to('Reply', {:controller => 'post', :action => 'reply', :id => post.id}, 'onclick' => "return showReply('#{post.id}')", 'id' => "post_reply_#{post.id}") + "</li>"
90
- str << '<li>' + link_to('Add Tags', {:controller => 'post', :action => 'tag', :id => post.id}, 'onclick' => "return showTagger('#{post.id}')", 'id' => "post_tagger_#{post.id}") + '</li>'
91
- str << '<li>' + link_to("View / Permalink", {:controller => 'view', :action => post.id.to_s}, {'class' => 'middle' }) + '</li>'
92
- str << "<li #{disp} id=\"comment_count_#{post.id}\"> " + link_to("Comments (#{post.comments.length})", :controller => 'view', :action => post.id.to_s, :anchor => 'comments') + "</li>"
93
- str << "<li>" + link_to("Trackbacks (#{post.pings.length})", :controller => 'view', :action => post.id.to_s, :anchor => 'trackbacks') + "</li>"
78
+ str << "<li #{disp} id=\"replylink_#{post.id}\">" + link_to('Reply', {:controller => 'post', :action => 'reply', :user => post.user.username, :slug => post.slug}, 'onclick' => "return showReply('#{post.id}')", 'id' => "post_reply_#{post.id}") + "</li>"
79
+ str << '<li>' + link_to('Add Tags', {:controller => 'post', :action => 'tag', :user => post.user.username, :slug => post.slug}, 'onclick' => "return showTagger('#{post.id}')", 'id' => "post_tagger_#{post.id}") + '</li>'
80
+ str << '<li>' + link_to("View / Permalink", {:controller => 'post', :action => 'view', :user => post.user.username, :slug => post.slug}, {'class' => 'middle' }) + '</li>'
81
+ str << "<li #{disp} id=\"comment_count_#{post.id}\"> " + link_to("Comments (#{post.comments.length})", :controller => 'post', :action => 'view', :user => post.user.username, :slug => post.slug, :anchor => 'comments') + "</li>"
82
+ str << "<li>" + link_to("Trackbacks (#{post.pings.length})", :controller => 'post', :action => 'view', :user => post.user.username, :slug => post.slug, :anchor => 'trackbacks') + "</li>"
94
83
  str << '</ul>'
95
84
  end
96
85
 
97
86
  def render_body(post)
98
87
  unless ['view', 'reply'].include? @params['action']
99
- return post.sections.first + (post.sections.length > 1 ? '<p><strong>Read More ...</strong></p>' : '')
88
+ return post.sections.first + (post.sections.length > 1 ? '<p>' + link_to('Read More ...', :controller => 'post', :action => 'view', :user => post.user.username, :slug => post.slug) + '</p>' : '')
100
89
  end
101
90
  post.rendered
102
91
  end
@@ -115,3 +104,4 @@ module ApplicationHelper
115
104
  user_page? ? @user.subtitle : @app_config['main']['subtitle']
116
105
  end
117
106
  end
107
+
@@ -0,0 +1,487 @@
1
+ # == Pagination Helper
2
+ #
3
+ # === Action Pack pagination for Active Record collections
4
+ #
5
+ # <em>Sam Stephenson <sstephenson at gmail dot com></em>
6
+ #
7
+ # Pagination Helper aids in the process of paging large collections of Active
8
+ # Record objects. It offers macro-style automatic fetching of your model for
9
+ # multiple views, or explicit fetching for single actions. And if the magic
10
+ # isn't flexible enough for your needs, you can create your own paginators
11
+ # with a minimal amount of code.
12
+ #
13
+ # Unlike most helpers, Pagination Helper is available to all of Action Pack:
14
+ # it helps you query your models with Action Controller and render links in
15
+ # Action View.
16
+ #
17
+ # ----
18
+ #
19
+ # === Controller examples
20
+ #
21
+ # Pagination Helper can handle as much or as little as you wish. In the
22
+ # controller, have it automatically query your model for pagination; or,
23
+ # if you prefer, create Paginator objects yourself.
24
+ #
25
+ # ==== Automatic pagination for every action in a controller
26
+ #
27
+ # class PersonController < ApplicationController
28
+ # helper :pagination
29
+ # model :person
30
+ #
31
+ # paginate :people, :order_by => 'last_name, first_name',
32
+ # :per_page => 20
33
+ #
34
+ # # ...
35
+ # end
36
+ #
37
+ # Each action in this controller now has access to a <tt>@people</tt> instance
38
+ # variable, which is an ordered collection of model objects for the current
39
+ # page (at most 20, sorted by last name and first name), and a
40
+ # <tt>@person_pages</tt> Paginator instance. The current page is determined by
41
+ # the <tt>@params['page']</tt> variable.
42
+ #
43
+ # ==== Pagination for a single action
44
+ #
45
+ # def list
46
+ # @person_pages, @people =
47
+ # paginate :people, :order_by => 'last_name, first_name'
48
+ # end
49
+ #
50
+ # Like the previous example, but explicitly creates <tt>@person_pages</tt> and
51
+ # <tt>@people</tt> for a single action, and uses the default of 10 items per
52
+ # page.
53
+ #
54
+ # ==== Custom/"classic" pagination
55
+ #
56
+ # def list
57
+ # @person_pages = Paginator.new self, Person.count, 10, @params['page']
58
+ # @people = Person.find_all nil, 'last_name, first_name',
59
+ # @person_pages.current.to_sql
60
+ # end
61
+ #
62
+ # Explicitly creates the paginator from the previous example and uses
63
+ # Paginator#to_sql to retrieve <tt>@people</tt> from the model.
64
+ #
65
+ # === View examples
66
+ #
67
+ # Paginator Helper includes various methods to help you display pagination
68
+ # links in your views. (For information on displaying the paginated
69
+ # collections you retrieve in the controller, see the documentation for
70
+ # ActionView::Partials#render_collection_of_partials.)
71
+ #
72
+ # ==== Using Paginator#basic_html
73
+ #
74
+ # Use the +basic_html+ method to get a simple list of pages (always including
75
+ # the first and last page) with a window around the current page. In your
76
+ # view, using one of the controller examples above,
77
+ #
78
+ # <%= @person_pages.basic_html(self) %>
79
+ #
80
+ # will render a list of links. For a list of parameters, see the documentation
81
+ # for Page#basic_html.
82
+ #
83
+ # ==== Using a paginator partial
84
+ #
85
+ # If you need more advanced control over pagination links and need to paginate
86
+ # multiple actions and controllers, consider using a shared paginator partial.
87
+ # For instance, _why suggests this partial, which you might place in
88
+ # <tt>app/views/partial/_paginator.rhtml</tt>:
89
+ #
90
+ # <div class="counter">
91
+ # Displaying <%= paginator.current.first_item %>
92
+ # - <%= paginator.current.last_item %>
93
+ # of <%= paginator.item_count %> &nbsp; &nbsp;
94
+ #
95
+ # <%= link_to(h('< Previous'), paginator.current.previous.to_link) +
96
+ # " | " if paginator.current.previous %>
97
+ # <%= paginator.basic_html(self, 4) %>
98
+ # <%= " | " + link_to(h('Next >'), paginator.current.next.to_link) if
99
+ # paginator.current.next %>
100
+ # </div>
101
+ #
102
+ # Then in your views, simply call
103
+ #
104
+ # <%= render_partial "partial/paginator", @person_pages %>
105
+ #
106
+ # wherever you want your pagination links to appear.
107
+ #
108
+ # ----
109
+ #
110
+ # ==== Thanks
111
+ #
112
+ # Thanks to the following people for their contributions to Pagination Helper:
113
+ # * Marcel Molina Jr (noradio), who provided the idea for and original
114
+ # implementation of Paginator::Window, as well as endless mental support.
115
+ # * evl, who pointed out that Page#link_to should take and merge in a hash of
116
+ # additional parameters.
117
+ # * why the lucky stiff, who wrote a lovely article on the original Pagination
118
+ # Helper alongside the first implementation of Paginator#base_html, created
119
+ # Page#first_item and Page#last_item, and who provided inspiration for
120
+ # revising Pagination Helper to make it more "Rails-ish."
121
+ # * xal, who saw the limitations of having only the macro-style paginate
122
+ # method and suggested the single-action version, and who also suggested the
123
+ # automatic inclusion into ActionController::Base.
124
+ # * jbd for feedback and debugging help.
125
+ # * ##rubyonrails on Freenode and the Rails mailing list.
126
+ #
127
+ module PaginationHelper
128
+ # A hash holding options for controllers using macro-style pagination
129
+ OPTIONS = Hash.new
130
+
131
+ # The default options for pagination
132
+ DEFAULT_OPTIONS = {
133
+ :class_name => nil,
134
+ :per_page => 10,
135
+ :parameter => 'page',
136
+ :conditions => nil,
137
+ :order_by => nil
138
+ }
139
+
140
+ def self.included(base) #:nodoc:
141
+ super
142
+ base.extend(ClassMethods)
143
+ end
144
+
145
+ def self.validate_options!(collection_id, options, in_action) #:nodoc:
146
+ options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
147
+
148
+ valid_options = [:class_name, :per_page,
149
+ :parameter, :conditions, :order_by]
150
+ valid_options << :actions unless in_action
151
+
152
+ unknown_option_keys = options.keys - valid_options
153
+ raise ActionController::ActionControllerError,
154
+ "Unknown options: #{unknown_option_keys.join(', ')}" unless
155
+ unknown_option_keys.empty?
156
+
157
+ options[:singular_name] = Inflector.singularize(collection_id.to_s)
158
+ options[:class_name] ||= Inflector.camelize(options[:singular_name])
159
+ end
160
+
161
+ # Returns a paginator and a collection of Active Record model instances for
162
+ # the paginator's current page. This is designed to be used in a single
163
+ # action; to automatically paginate multiple actions, consider
164
+ # ClassMethods#paginate.
165
+ #
166
+ # +options+ are:
167
+ # <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
168
+ # singularizing the collection name.
169
+ # <tt>:per_page</tt>:: the maximum number of items to include in a
170
+ # single page. Defaults to 10.
171
+ # <tt>:parameter</tt>:: the CGI parameter from which the current page is
172
+ # determined. Defaults to 'page'; i.e., the current
173
+ # page is specified by <tt>@params['page']</tt>.
174
+ # <tt>:conditions</tt>:: optional conditions passed to Model.find_all.
175
+ # <tt>:order_by</tt>:: optional order parameter passed to Model.find_all
176
+ # and Model.count.
177
+ def paginate(collection_id, options={})
178
+ PaginationHelper.validate_options!(collection_id, options, true)
179
+ paginator_and_collection_for(collection_id, options)
180
+ end
181
+
182
+ # These methods become class methods on any controller which includes
183
+ # PaginationHelper.
184
+ module ClassMethods
185
+ # Creates a +before_filter+ which automatically paginates an Active Record
186
+ # model for all actions in a controller (or certain actions if specified
187
+ # with the <tt>:actions</tt> option).
188
+ #
189
+ # +options+ are the same as PaginationHelper#paginate, with the addition
190
+ # of:
191
+ # <tt>:actions</tt>:: an array of actions for which the pagination is
192
+ # active. Defaults to +nil+ (i.e., every action).
193
+ def paginate(collection_id, options={})
194
+ PaginationHelper.validate_options!(collection_id, options, false)
195
+ module_eval do
196
+ before_filter :create_paginators_and_retrieve_collections
197
+ OPTIONS[self] ||= Hash.new
198
+ OPTIONS[self][collection_id] = options
199
+ end
200
+ end
201
+ end
202
+
203
+ def create_paginators_and_retrieve_collections #:nodoc:
204
+ PaginationHelper::OPTIONS[self.class].each do |collection_id, options|
205
+ next unless options[:actions].include? action_name if options[:actions]
206
+
207
+ paginator, collection =
208
+ paginator_and_collection_for(collection_id, options)
209
+
210
+ paginator_name = "@#{options[:singular_name]}_pages"
211
+ self.instance_variable_set(paginator_name, paginator)
212
+
213
+ collection_name = "@#{collection_id.to_s}"
214
+ self.instance_variable_set(collection_name, collection)
215
+ end
216
+ end
217
+
218
+ # Returns the total number of items in the collection to be paginated for
219
+ # the +model+ and given +conditions+. Override this method to implement a
220
+ # custom counter.
221
+ def count_collection_for_pagination(model, conditions)
222
+ model.count(conditions)
223
+ end
224
+
225
+ # Returns a collection of items for the given +model+ and +conditions+,
226
+ # ordered by +order_by+, for the current page in the given +paginator+.
227
+ # Override this method to implement a custom finder.
228
+ def find_collection_for_pagination(model, conditions, order_by, paginator)
229
+ model.find_all(conditions, order_by, paginator.current.to_sql)
230
+ end
231
+
232
+ protected :create_paginators_and_retrieve_collections,
233
+ :count_collection_for_pagination, :find_collection_for_pagination
234
+
235
+ def paginator_and_collection_for(collection_id, options) #:nodoc:
236
+ klass = eval options[:class_name]
237
+ page = @params[options[:parameter]]
238
+ count = count_collection_for_pagination(klass, options[:conditions])
239
+
240
+ paginator = Paginator.new(self, count,
241
+ options[:per_page], page, options[:parameter])
242
+
243
+ collection = find_collection_for_pagination(klass,
244
+ options[:conditions], options[:order_by], paginator)
245
+
246
+ return paginator, collection
247
+ end
248
+
249
+ private :paginator_and_collection_for
250
+
251
+ # A class representing a paginator for an Active Record collection.
252
+ class Paginator
253
+ include Enumerable
254
+
255
+ # Creates a new Paginator on the given +controller+ for a set of items of
256
+ # size +item_count+ and having +items_per_page+ items per page. Raises
257
+ # ArgumentError if items_per_page is out of bounds (i.e., less than or
258
+ # equal to zero). The page CGI parameter for links defaults to "page" and
259
+ # can be overridden with +page_parameter+.
260
+ def initialize(controller, item_count, items_per_page, current_page=1,
261
+ page_parameter='page')
262
+ raise ArgumentError, 'must have at least one item per page' if
263
+ items_per_page <= 0
264
+
265
+ @controller = controller
266
+ @item_count = item_count || 0
267
+ @items_per_page = items_per_page
268
+ @page_parameter = page_parameter
269
+
270
+ self.current_page = current_page
271
+ end
272
+ attr_reader :controller, :item_count, :items_per_page, :page_parameter
273
+
274
+ # Sets the current page number of this paginator. If +page+ is a Page
275
+ # object, its +number+ attribute is used as the value; if the page does
276
+ # not belong to this Paginator, an ArgumentError is raised.
277
+ def current_page=(page)
278
+ if page.is_a? Page
279
+ raise ArgumentError, 'Page/Paginator mismatch' unless
280
+ page.paginator == self
281
+ end
282
+ page = page.to_i
283
+ @current_page = has_page_number?(page) ? page : 1
284
+ end
285
+
286
+ # Returns a Page object representing this paginator's current page.
287
+ def current_page
288
+ self[@current_page]
289
+ end
290
+ alias current :current_page
291
+
292
+ # Returns a new Page representing the first page in this paginator.
293
+ def first_page
294
+ self[1]
295
+ end
296
+ alias first :first_page
297
+
298
+ # Returns a new Page representing the last page in this paginator.
299
+ def last_page
300
+ self[page_count]
301
+ end
302
+ alias last :last_page
303
+
304
+ # Returns the number of pages in this paginator.
305
+ def page_count
306
+ return 1 if @item_count.zero?
307
+ (@item_count / @items_per_page.to_f).ceil
308
+ end
309
+ alias length :page_count
310
+
311
+ # Returns true if this paginator contains the page of index +number+.
312
+ def has_page_number?(number)
313
+ return false unless number.is_a? Fixnum
314
+ number >= 1 and number <= page_count
315
+ end
316
+
317
+ # Returns a new Page representing the page with the given index +number+.
318
+ def [](number)
319
+ Page.new(self, number)
320
+ end
321
+
322
+ # Successively yields all the paginator's pages to the given block.
323
+ def each(&block)
324
+ page_count.times do |n|
325
+ yield self[n+1]
326
+ end
327
+ end
328
+
329
+ # Creates a basic HTML link bar for the given +view+. The first and last
330
+ # pages of the paginator are always shown, along with +window_size+ pages
331
+ # around the current page. By default, the current page is displayed, but
332
+ # not linked; to change this behavior, pass true to the
333
+ # +link_to_current_page+ argument. Specify additional link_to parameters
334
+ # with +params+.
335
+ def basic_html(view, window_size=2, link_to_current_page=false, params={})
336
+ window_pages = current.window(window_size).pages
337
+ return if window_pages.length <= 1 unless link_to_current_page
338
+
339
+ html = ''
340
+ unless window_pages[0].first?
341
+ html << view.link_to(first.number, first.to_link(params))
342
+ html << ' ... ' if window_pages[0].number - first.number > 1
343
+ html << ' '
344
+ end
345
+
346
+ window_pages.each do |page|
347
+ if current == page and not link_to_current_page
348
+ html << page.number.to_s
349
+ else
350
+ html << view.link_to(page.number, page.to_link(params))
351
+ end
352
+ html << ' '
353
+ end
354
+
355
+ unless window_pages.last.last?
356
+ html << ' ... ' if last.number - window_pages[-1].number > 1
357
+ html << view.link_to(last.number, last.to_link(params))
358
+ end
359
+
360
+ html
361
+ end
362
+
363
+ # A class representing a single page in a paginator.
364
+ class Page
365
+ include Comparable
366
+
367
+ # Creates a new Page for the given +paginator+ with the index +number+.
368
+ # If +number+ is not in the range of valid page numbers or is not a
369
+ # number at all, it defaults to 1.
370
+ def initialize(paginator, number)
371
+ @paginator = paginator
372
+ @number = number.to_i
373
+ @number = 1 unless @paginator.has_page_number? @number
374
+ end
375
+ attr_reader :paginator, :number
376
+ alias to_i :number
377
+
378
+ # Compares two Page objects and returns true when they represent the
379
+ # same page (i.e., their paginators are the same and they have the same
380
+ # page number).
381
+ def ==(page)
382
+ @paginator == page.paginator and
383
+ @number == page.number
384
+ end
385
+
386
+ # Compares two Page objects and returns -1 if the left-hand page comes
387
+ # before the right-hand page, 0 if the pages are equal, and 1 if the
388
+ # left-hand page comes after the right-hand page. Raises ArgumentError
389
+ # if the pages do not belong to the same Paginator object.
390
+ def <=>(page)
391
+ raise ArgumentError unless @paginator == page.paginator
392
+ @number <=> page.number
393
+ end
394
+
395
+ # Returns the item offset for the first item in this page.
396
+ def offset
397
+ @paginator.items_per_page * (@number - 1)
398
+ end
399
+
400
+ # Returns the number of the first item displayed.
401
+ def first_item
402
+ offset + 1
403
+ end
404
+
405
+ # Returns the number of the last item displayed.
406
+ def last_item
407
+ [@paginator.items_per_page * @number, @paginator.item_count].min
408
+ end
409
+
410
+ # Returns true if this page is the first page in the paginator.
411
+ def first?
412
+ self == @paginator.first
413
+ end
414
+
415
+ # Returns true if this page is the last page in the paginator.
416
+ def last?
417
+ self == @paginator.last
418
+ end
419
+
420
+ # Returns a new Page object representing the page just before this page,
421
+ # or nil if this is the first page.
422
+ def previous
423
+ if first? then nil else Page.new(@paginator, @number - 1) end
424
+ end
425
+
426
+ # Returns a new Page object representing the page just after this page,
427
+ # or nil if this is the last page.
428
+ def next
429
+ if last? then nil else Page.new(@paginator, @number + 1) end
430
+ end
431
+
432
+ # Returns a new Window object for this page with the specified
433
+ # +padding+.
434
+ def window(padding=2)
435
+ Window.new(self, padding)
436
+ end
437
+
438
+ # Returns a hash appropriate for using with link_to or url_for. Takes an
439
+ # optional +params+ hash for specifying additional link parameters.
440
+ def to_link(params={})
441
+ {:params => {@paginator.page_parameter => @number.to_s}.merge(params)}
442
+ end
443
+
444
+ # Returns the SQL "LIMIT ... OFFSET" clause for this page.
445
+ def to_sql
446
+ ['? OFFSET ?', @paginator.items_per_page, offset]
447
+ end
448
+ end
449
+
450
+ # A class for representing ranges around a given page.
451
+ class Window
452
+ # Creates a new Window object for the given +page+ with the specified
453
+ # +padding+.
454
+ def initialize(page, padding=2)
455
+ @paginator = page.paginator
456
+ @page = page
457
+ self.padding = padding
458
+ end
459
+ attr_reader :paginator, :page
460
+
461
+ # Sets the window's padding (the number of pages on either side of the
462
+ # window page).
463
+ def padding=(padding)
464
+ @padding = padding < 0 ? 0 : padding
465
+ # Find the beginning and end pages of the window
466
+ @first = @paginator.has_page_number?(@page.number - @padding) ?
467
+ @paginator[@page.number - @padding] : @paginator.first
468
+ @last = @paginator.has_page_number?(@page.number + @padding) ?
469
+ @paginator[@page.number + @padding] : @paginator.last
470
+ end
471
+ attr_reader :padding, :first, :last
472
+
473
+ # Returns an array of Page objects in the current window.
474
+ def pages
475
+ (@first.number..@last.number).to_a.map {|n| @paginator[n]}
476
+ end
477
+ alias to_a :pages
478
+ end
479
+ end
480
+ end
481
+
482
+ module ActionController #:nodoc:
483
+ class Base #:nodoc:
484
+ # Automatically include PaginationHelper.
485
+ include PaginationHelper
486
+ end
487
+ end