##// END OF EJS Templates
Rails 2.1.2 deprecations (#2332)....
Jean-Philippe Lang -
r2132:e2952d3e5fc4
parent child
Show More
@@ -1,294 +1,294
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < ActiveRecord::Base
20 class User < ActiveRecord::Base
21
21
22 # Account statuses
22 # Account statuses
23 STATUS_ANONYMOUS = 0
23 STATUS_ANONYMOUS = 0
24 STATUS_ACTIVE = 1
24 STATUS_ACTIVE = 1
25 STATUS_REGISTERED = 2
25 STATUS_REGISTERED = 2
26 STATUS_LOCKED = 3
26 STATUS_LOCKED = 3
27
27
28 USER_FORMATS = {
28 USER_FORMATS = {
29 :firstname_lastname => '#{firstname} #{lastname}',
29 :firstname_lastname => '#{firstname} #{lastname}',
30 :firstname => '#{firstname}',
30 :firstname => '#{firstname}',
31 :lastname_firstname => '#{lastname} #{firstname}',
31 :lastname_firstname => '#{lastname} #{firstname}',
32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 :username => '#{login}'
33 :username => '#{login}'
34 }
34 }
35
35
36 has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
36 has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
37 has_many :members, :dependent => :delete_all
37 has_many :members, :dependent => :delete_all
38 has_many :projects, :through => :memberships
38 has_many :projects, :through => :memberships
39 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
39 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
40 has_many :changesets, :dependent => :nullify
40 has_many :changesets, :dependent => :nullify
41 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
41 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
42 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
42 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
43 belongs_to :auth_source
43 belongs_to :auth_source
44
44
45 # Active non-anonymous users scope
45 # Active non-anonymous users scope
46 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
46 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
47
47
48 acts_as_customizable
48 acts_as_customizable
49
49
50 attr_accessor :password, :password_confirmation
50 attr_accessor :password, :password_confirmation
51 attr_accessor :last_before_login_on
51 attr_accessor :last_before_login_on
52 # Prevents unauthorized assignments
52 # Prevents unauthorized assignments
53 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
53 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
54
54
55 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
55 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
56 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
56 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
57 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
57 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
58 # Login must contain lettres, numbers, underscores only
58 # Login must contain lettres, numbers, underscores only
59 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
59 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
60 validates_length_of :login, :maximum => 30
60 validates_length_of :login, :maximum => 30
61 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
61 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
62 validates_length_of :firstname, :lastname, :maximum => 30
62 validates_length_of :firstname, :lastname, :maximum => 30
63 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
63 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
64 validates_length_of :mail, :maximum => 60, :allow_nil => true
64 validates_length_of :mail, :maximum => 60, :allow_nil => true
65 validates_length_of :password, :minimum => 4, :allow_nil => true
65 validates_length_of :password, :minimum => 4, :allow_nil => true
66 validates_confirmation_of :password, :allow_nil => true
66 validates_confirmation_of :password, :allow_nil => true
67
67
68 def before_create
68 def before_create
69 self.mail_notification = false
69 self.mail_notification = false
70 true
70 true
71 end
71 end
72
72
73 def before_save
73 def before_save
74 # update hashed_password if password was set
74 # update hashed_password if password was set
75 self.hashed_password = User.hash_password(self.password) if self.password
75 self.hashed_password = User.hash_password(self.password) if self.password
76 end
76 end
77
77
78 def reload(*args)
78 def reload(*args)
79 @name = nil
79 @name = nil
80 super
80 super
81 end
81 end
82
82
83 # Returns the user that matches provided login and password, or nil
83 # Returns the user that matches provided login and password, or nil
84 def self.try_to_login(login, password)
84 def self.try_to_login(login, password)
85 # Make sure no one can sign in with an empty password
85 # Make sure no one can sign in with an empty password
86 return nil if password.to_s.empty?
86 return nil if password.to_s.empty?
87 user = find(:first, :conditions => ["login=?", login])
87 user = find(:first, :conditions => ["login=?", login])
88 if user
88 if user
89 # user is already in local database
89 # user is already in local database
90 return nil if !user.active?
90 return nil if !user.active?
91 if user.auth_source
91 if user.auth_source
92 # user has an external authentication method
92 # user has an external authentication method
93 return nil unless user.auth_source.authenticate(login, password)
93 return nil unless user.auth_source.authenticate(login, password)
94 else
94 else
95 # authentication with local password
95 # authentication with local password
96 return nil unless User.hash_password(password) == user.hashed_password
96 return nil unless User.hash_password(password) == user.hashed_password
97 end
97 end
98 else
98 else
99 # user is not yet registered, try to authenticate with available sources
99 # user is not yet registered, try to authenticate with available sources
100 attrs = AuthSource.authenticate(login, password)
100 attrs = AuthSource.authenticate(login, password)
101 if attrs
101 if attrs
102 user = new(*attrs)
102 user = new(*attrs)
103 user.login = login
103 user.login = login
104 user.language = Setting.default_language
104 user.language = Setting.default_language
105 if user.save
105 if user.save
106 user.reload
106 user.reload
107 logger.info("User '#{user.login}' created from the LDAP") if logger
107 logger.info("User '#{user.login}' created from the LDAP") if logger
108 end
108 end
109 end
109 end
110 end
110 end
111 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
111 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
112 user
112 user
113 rescue => text
113 rescue => text
114 raise text
114 raise text
115 end
115 end
116
116
117 # Return user's full name for display
117 # Return user's full name for display
118 def name(formatter = nil)
118 def name(formatter = nil)
119 if formatter
119 if formatter
120 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
120 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
121 else
121 else
122 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
122 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
123 end
123 end
124 end
124 end
125
125
126 def active?
126 def active?
127 self.status == STATUS_ACTIVE
127 self.status == STATUS_ACTIVE
128 end
128 end
129
129
130 def registered?
130 def registered?
131 self.status == STATUS_REGISTERED
131 self.status == STATUS_REGISTERED
132 end
132 end
133
133
134 def locked?
134 def locked?
135 self.status == STATUS_LOCKED
135 self.status == STATUS_LOCKED
136 end
136 end
137
137
138 def check_password?(clear_password)
138 def check_password?(clear_password)
139 User.hash_password(clear_password) == self.hashed_password
139 User.hash_password(clear_password) == self.hashed_password
140 end
140 end
141
141
142 def pref
142 def pref
143 self.preference ||= UserPreference.new(:user => self)
143 self.preference ||= UserPreference.new(:user => self)
144 end
144 end
145
145
146 def time_zone
146 def time_zone
147 @time_zone ||= (self.pref.time_zone.blank? ? nil : TimeZone[self.pref.time_zone])
147 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
148 end
148 end
149
149
150 def wants_comments_in_reverse_order?
150 def wants_comments_in_reverse_order?
151 self.pref[:comments_sorting] == 'desc'
151 self.pref[:comments_sorting] == 'desc'
152 end
152 end
153
153
154 # Return user's RSS key (a 40 chars long string), used to access feeds
154 # Return user's RSS key (a 40 chars long string), used to access feeds
155 def rss_key
155 def rss_key
156 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
156 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
157 token.value
157 token.value
158 end
158 end
159
159
160 # Return an array of project ids for which the user has explicitly turned mail notifications on
160 # Return an array of project ids for which the user has explicitly turned mail notifications on
161 def notified_projects_ids
161 def notified_projects_ids
162 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
162 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
163 end
163 end
164
164
165 def notified_project_ids=(ids)
165 def notified_project_ids=(ids)
166 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
166 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
167 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
167 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
168 @notified_projects_ids = nil
168 @notified_projects_ids = nil
169 notified_projects_ids
169 notified_projects_ids
170 end
170 end
171
171
172 def self.find_by_rss_key(key)
172 def self.find_by_rss_key(key)
173 token = Token.find_by_value(key)
173 token = Token.find_by_value(key)
174 token && token.user.active? ? token.user : nil
174 token && token.user.active? ? token.user : nil
175 end
175 end
176
176
177 def self.find_by_autologin_key(key)
177 def self.find_by_autologin_key(key)
178 token = Token.find_by_action_and_value('autologin', key)
178 token = Token.find_by_action_and_value('autologin', key)
179 token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
179 token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
180 end
180 end
181
181
182 # Makes find_by_mail case-insensitive
182 # Makes find_by_mail case-insensitive
183 def self.find_by_mail(mail)
183 def self.find_by_mail(mail)
184 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
184 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
185 end
185 end
186
186
187 # Sort users by their display names
187 # Sort users by their display names
188 def <=>(user)
188 def <=>(user)
189 self.to_s.downcase <=> user.to_s.downcase
189 self.to_s.downcase <=> user.to_s.downcase
190 end
190 end
191
191
192 def to_s
192 def to_s
193 name
193 name
194 end
194 end
195
195
196 def logged?
196 def logged?
197 true
197 true
198 end
198 end
199
199
200 def anonymous?
200 def anonymous?
201 !logged?
201 !logged?
202 end
202 end
203
203
204 # Return user's role for project
204 # Return user's role for project
205 def role_for_project(project)
205 def role_for_project(project)
206 # No role on archived projects
206 # No role on archived projects
207 return nil unless project && project.active?
207 return nil unless project && project.active?
208 if logged?
208 if logged?
209 # Find project membership
209 # Find project membership
210 membership = memberships.detect {|m| m.project_id == project.id}
210 membership = memberships.detect {|m| m.project_id == project.id}
211 if membership
211 if membership
212 membership.role
212 membership.role
213 else
213 else
214 @role_non_member ||= Role.non_member
214 @role_non_member ||= Role.non_member
215 end
215 end
216 else
216 else
217 @role_anonymous ||= Role.anonymous
217 @role_anonymous ||= Role.anonymous
218 end
218 end
219 end
219 end
220
220
221 # Return true if the user is a member of project
221 # Return true if the user is a member of project
222 def member_of?(project)
222 def member_of?(project)
223 role_for_project(project).member?
223 role_for_project(project).member?
224 end
224 end
225
225
226 # Return true if the user is allowed to do the specified action on project
226 # Return true if the user is allowed to do the specified action on project
227 # action can be:
227 # action can be:
228 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
228 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
229 # * a permission Symbol (eg. :edit_project)
229 # * a permission Symbol (eg. :edit_project)
230 def allowed_to?(action, project, options={})
230 def allowed_to?(action, project, options={})
231 if project
231 if project
232 # No action allowed on archived projects
232 # No action allowed on archived projects
233 return false unless project.active?
233 return false unless project.active?
234 # No action allowed on disabled modules
234 # No action allowed on disabled modules
235 return false unless project.allows_to?(action)
235 return false unless project.allows_to?(action)
236 # Admin users are authorized for anything else
236 # Admin users are authorized for anything else
237 return true if admin?
237 return true if admin?
238
238
239 role = role_for_project(project)
239 role = role_for_project(project)
240 return false unless role
240 return false unless role
241 role.allowed_to?(action) && (project.is_public? || role.member?)
241 role.allowed_to?(action) && (project.is_public? || role.member?)
242
242
243 elsif options[:global]
243 elsif options[:global]
244 # authorize if user has at least one role that has this permission
244 # authorize if user has at least one role that has this permission
245 roles = memberships.collect {|m| m.role}.uniq
245 roles = memberships.collect {|m| m.role}.uniq
246 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
246 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
247 else
247 else
248 false
248 false
249 end
249 end
250 end
250 end
251
251
252 def self.current=(user)
252 def self.current=(user)
253 @current_user = user
253 @current_user = user
254 end
254 end
255
255
256 def self.current
256 def self.current
257 @current_user ||= User.anonymous
257 @current_user ||= User.anonymous
258 end
258 end
259
259
260 def self.anonymous
260 def self.anonymous
261 anonymous_user = AnonymousUser.find(:first)
261 anonymous_user = AnonymousUser.find(:first)
262 if anonymous_user.nil?
262 if anonymous_user.nil?
263 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
263 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
264 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
264 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
265 end
265 end
266 anonymous_user
266 anonymous_user
267 end
267 end
268
268
269 private
269 private
270 # Return password digest
270 # Return password digest
271 def self.hash_password(clear_password)
271 def self.hash_password(clear_password)
272 Digest::SHA1.hexdigest(clear_password || "")
272 Digest::SHA1.hexdigest(clear_password || "")
273 end
273 end
274 end
274 end
275
275
276 class AnonymousUser < User
276 class AnonymousUser < User
277
277
278 def validate_on_create
278 def validate_on_create
279 # There should be only one AnonymousUser in the database
279 # There should be only one AnonymousUser in the database
280 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
280 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
281 end
281 end
282
282
283 def available_custom_fields
283 def available_custom_fields
284 []
284 []
285 end
285 end
286
286
287 # Overrides a few properties
287 # Overrides a few properties
288 def logged?; false end
288 def logged?; false end
289 def admin; false end
289 def admin; false end
290 def name; 'Anonymous' end
290 def name; 'Anonymous' end
291 def mail; nil end
291 def mail; nil end
292 def time_zone; nil end
292 def time_zone; nil end
293 def rss_key; nil end
293 def rss_key; nil end
294 end
294 end
@@ -1,52 +1,52
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to(l(:button_change_password), :action => 'password') unless @user.auth_source_id %>
2 <%= link_to(l(:button_change_password), :action => 'password') unless @user.auth_source_id %>
3 </div>
3 </div>
4 <h2><%=l(:label_my_account)%></h2>
4 <h2><%=l(:label_my_account)%></h2>
5 <%= error_messages_for 'user' %>
5 <%= error_messages_for 'user' %>
6
6
7 <% form_for :user, @user, :url => { :action => "account" },
7 <% form_for :user, @user, :url => { :action => "account" },
8 :builder => TabularFormBuilder,
8 :builder => TabularFormBuilder,
9 :lang => current_language,
9 :lang => current_language,
10 :html => { :id => 'my_account_form' } do |f| %>
10 :html => { :id => 'my_account_form' } do |f| %>
11 <div class="splitcontentleft">
11 <div class="splitcontentleft">
12 <h3><%=l(:label_information_plural)%></h3>
12 <h3><%=l(:label_information_plural)%></h3>
13 <div class="box tabular">
13 <div class="box tabular">
14 <p><%= f.text_field :firstname, :required => true %></p>
14 <p><%= f.text_field :firstname, :required => true %></p>
15 <p><%= f.text_field :lastname, :required => true %></p>
15 <p><%= f.text_field :lastname, :required => true %></p>
16 <p><%= f.text_field :mail, :required => true %></p>
16 <p><%= f.text_field :mail, :required => true %></p>
17 <p><%= f.select :language, lang_options_for_select %></p>
17 <p><%= f.select :language, lang_options_for_select %></p>
18 </div>
18 </div>
19
19
20 <%= submit_tag l(:button_save) %>
20 <%= submit_tag l(:button_save) %>
21 </div>
21 </div>
22
22
23 <div class="splitcontentright">
23 <div class="splitcontentright">
24 <h3><%=l(:field_mail_notification)%></h3>
24 <h3><%=l(:field_mail_notification)%></h3>
25 <div class="box">
25 <div class="box">
26 <%= select_tag 'notification_option', options_for_select(@notification_options, @notification_option),
26 <%= select_tag 'notification_option', options_for_select(@notification_options, @notification_option),
27 :onchange => 'if ($("notification_option").value == "selected") {Element.show("notified-projects")} else {Element.hide("notified-projects")}' %>
27 :onchange => 'if ($("notification_option").value == "selected") {Element.show("notified-projects")} else {Element.hide("notified-projects")}' %>
28 <% content_tag 'div', :id => 'notified-projects', :style => (@notification_option == 'selected' ? '' : 'display:none;') do %>
28 <% content_tag 'div', :id => 'notified-projects', :style => (@notification_option == 'selected' ? '' : 'display:none;') do %>
29 <p><% User.current.projects.each do |project| %>
29 <p><% User.current.projects.each do |project| %>
30 <label><%= check_box_tag 'notified_project_ids[]', project.id, @user.notified_projects_ids.include?(project.id) %> <%=h project.name %></label><br />
30 <label><%= check_box_tag 'notified_project_ids[]', project.id, @user.notified_projects_ids.include?(project.id) %> <%=h project.name %></label><br />
31 <% end %></p>
31 <% end %></p>
32 <p><em><%= l(:text_user_mail_option) %></em></p>
32 <p><em><%= l(:text_user_mail_option) %></em></p>
33 <% end %>
33 <% end %>
34 <p><label><%= check_box_tag 'no_self_notified', 1, @user.pref[:no_self_notified] %> <%= l(:label_user_mail_no_self_notified) %></label></p>
34 <p><label><%= check_box_tag 'no_self_notified', 1, @user.pref[:no_self_notified] %> <%= l(:label_user_mail_no_self_notified) %></label></p>
35 </div>
35 </div>
36
36
37 <h3><%=l(:label_preferences)%></h3>
37 <h3><%=l(:label_preferences)%></h3>
38 <div class="box tabular">
38 <div class="box tabular">
39 <% fields_for :pref, @user.pref, :builder => TabularFormBuilder, :lang => current_language do |pref_fields| %>
39 <% fields_for :pref, @user.pref, :builder => TabularFormBuilder, :lang => current_language do |pref_fields| %>
40 <p><%= pref_fields.check_box :hide_mail %></p>
40 <p><%= pref_fields.check_box :hide_mail %></p>
41 <p><%= pref_fields.select :time_zone, TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %></p>
41 <p><%= pref_fields.select :time_zone, ActiveSupport::TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %></p>
42 <p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
42 <p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
43 <% end %>
43 <% end %>
44 </div>
44 </div>
45 </div>
45 </div>
46 <% end %>
46 <% end %>
47
47
48 <% content_for :sidebar do %>
48 <% content_for :sidebar do %>
49 <%= render :partial => 'sidebar' %>
49 <%= render :partial => 'sidebar' %>
50 <% end %>
50 <% end %>
51
51
52 <% html_title(l(:label_my_account)) -%>
52 <% html_title(l(:label_my_account)) -%>
@@ -1,7 +1,7
1 require 'action_web_service'
1 require 'action_web_service'
2
2
3 # These need to be in the load path for action_web_service to work
3 # These need to be in the load path for action_web_service to work
4 Dependencies.load_paths += ["#{RAILS_ROOT}/app/apis"]
4 ActiveSupport::Dependencies.load_paths += ["#{RAILS_ROOT}/app/apis"]
5
5
6 # AWS Test helpers
6 # AWS Test helpers
7 require 'action_web_service/test_invoke' if ENV['RAILS_ENV'] && ENV['RAILS_ENV'] =~ /^test/
7 require 'action_web_service/test_invoke' if ENV['RAILS_ENV'] && ENV['RAILS_ENV'] =~ /^test/
@@ -1,405 +1,405
1 module ActionController
1 module ActionController
2 # === Action Pack pagination for Active Record collections
2 # === Action Pack pagination for Active Record collections
3 #
3 #
4 # The Pagination module aids in the process of paging large collections of
4 # The Pagination module aids in the process of paging large collections of
5 # Active Record objects. It offers macro-style automatic fetching of your
5 # Active Record objects. It offers macro-style automatic fetching of your
6 # model for multiple views, or explicit fetching for single actions. And if
6 # model for multiple views, or explicit fetching for single actions. And if
7 # the magic isn't flexible enough for your needs, you can create your own
7 # the magic isn't flexible enough for your needs, you can create your own
8 # paginators with a minimal amount of code.
8 # paginators with a minimal amount of code.
9 #
9 #
10 # The Pagination module can handle as much or as little as you wish. In the
10 # The Pagination module can handle as much or as little as you wish. In the
11 # controller, have it automatically query your model for pagination; or,
11 # controller, have it automatically query your model for pagination; or,
12 # if you prefer, create Paginator objects yourself.
12 # if you prefer, create Paginator objects yourself.
13 #
13 #
14 # Pagination is included automatically for all controllers.
14 # Pagination is included automatically for all controllers.
15 #
15 #
16 # For help rendering pagination links, see
16 # For help rendering pagination links, see
17 # ActionView::Helpers::PaginationHelper.
17 # ActionView::Helpers::PaginationHelper.
18 #
18 #
19 # ==== Automatic pagination for every action in a controller
19 # ==== Automatic pagination for every action in a controller
20 #
20 #
21 # class PersonController < ApplicationController
21 # class PersonController < ApplicationController
22 # model :person
22 # model :person
23 #
23 #
24 # paginate :people, :order => 'last_name, first_name',
24 # paginate :people, :order => 'last_name, first_name',
25 # :per_page => 20
25 # :per_page => 20
26 #
26 #
27 # # ...
27 # # ...
28 # end
28 # end
29 #
29 #
30 # Each action in this controller now has access to a <tt>@people</tt>
30 # Each action in this controller now has access to a <tt>@people</tt>
31 # instance variable, which is an ordered collection of model objects for the
31 # instance variable, which is an ordered collection of model objects for the
32 # current page (at most 20, sorted by last name and first name), and a
32 # current page (at most 20, sorted by last name and first name), and a
33 # <tt>@person_pages</tt> Paginator instance. The current page is determined
33 # <tt>@person_pages</tt> Paginator instance. The current page is determined
34 # by the <tt>params[:page]</tt> variable.
34 # by the <tt>params[:page]</tt> variable.
35 #
35 #
36 # ==== Pagination for a single action
36 # ==== Pagination for a single action
37 #
37 #
38 # def list
38 # def list
39 # @person_pages, @people =
39 # @person_pages, @people =
40 # paginate :people, :order => 'last_name, first_name'
40 # paginate :people, :order => 'last_name, first_name'
41 # end
41 # end
42 #
42 #
43 # Like the previous example, but explicitly creates <tt>@person_pages</tt>
43 # Like the previous example, but explicitly creates <tt>@person_pages</tt>
44 # and <tt>@people</tt> for a single action, and uses the default of 10 items
44 # and <tt>@people</tt> for a single action, and uses the default of 10 items
45 # per page.
45 # per page.
46 #
46 #
47 # ==== Custom/"classic" pagination
47 # ==== Custom/"classic" pagination
48 #
48 #
49 # def list
49 # def list
50 # @person_pages = Paginator.new self, Person.count, 10, params[:page]
50 # @person_pages = Paginator.new self, Person.count, 10, params[:page]
51 # @people = Person.find :all, :order => 'last_name, first_name',
51 # @people = Person.find :all, :order => 'last_name, first_name',
52 # :limit => @person_pages.items_per_page,
52 # :limit => @person_pages.items_per_page,
53 # :offset => @person_pages.current.offset
53 # :offset => @person_pages.current.offset
54 # end
54 # end
55 #
55 #
56 # Explicitly creates the paginator from the previous example and uses
56 # Explicitly creates the paginator from the previous example and uses
57 # Paginator#to_sql to retrieve <tt>@people</tt> from the model.
57 # Paginator#to_sql to retrieve <tt>@people</tt> from the model.
58 #
58 #
59 module Pagination
59 module Pagination
60 unless const_defined?(:OPTIONS)
60 unless const_defined?(:OPTIONS)
61 # A hash holding options for controllers using macro-style pagination
61 # A hash holding options for controllers using macro-style pagination
62 OPTIONS = Hash.new
62 OPTIONS = Hash.new
63
63
64 # The default options for pagination
64 # The default options for pagination
65 DEFAULT_OPTIONS = {
65 DEFAULT_OPTIONS = {
66 :class_name => nil,
66 :class_name => nil,
67 :singular_name => nil,
67 :singular_name => nil,
68 :per_page => 10,
68 :per_page => 10,
69 :conditions => nil,
69 :conditions => nil,
70 :order_by => nil,
70 :order_by => nil,
71 :order => nil,
71 :order => nil,
72 :join => nil,
72 :join => nil,
73 :joins => nil,
73 :joins => nil,
74 :count => nil,
74 :count => nil,
75 :include => nil,
75 :include => nil,
76 :select => nil,
76 :select => nil,
77 :group => nil,
77 :group => nil,
78 :parameter => 'page'
78 :parameter => 'page'
79 }
79 }
80 else
80 else
81 DEFAULT_OPTIONS[:group] = nil
81 DEFAULT_OPTIONS[:group] = nil
82 end
82 end
83
83
84 def self.included(base) #:nodoc:
84 def self.included(base) #:nodoc:
85 super
85 super
86 base.extend(ClassMethods)
86 base.extend(ClassMethods)
87 end
87 end
88
88
89 def self.validate_options!(collection_id, options, in_action) #:nodoc:
89 def self.validate_options!(collection_id, options, in_action) #:nodoc:
90 options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
90 options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
91
91
92 valid_options = DEFAULT_OPTIONS.keys
92 valid_options = DEFAULT_OPTIONS.keys
93 valid_options << :actions unless in_action
93 valid_options << :actions unless in_action
94
94
95 unknown_option_keys = options.keys - valid_options
95 unknown_option_keys = options.keys - valid_options
96 raise ActionController::ActionControllerError,
96 raise ActionController::ActionControllerError,
97 "Unknown options: #{unknown_option_keys.join(', ')}" unless
97 "Unknown options: #{unknown_option_keys.join(', ')}" unless
98 unknown_option_keys.empty?
98 unknown_option_keys.empty?
99
99
100 options[:singular_name] ||= Inflector.singularize(collection_id.to_s)
100 options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s)
101 options[:class_name] ||= Inflector.camelize(options[:singular_name])
101 options[:class_name] ||= ActiveSupport::Inflector.camelize(options[:singular_name])
102 end
102 end
103
103
104 # Returns a paginator and a collection of Active Record model instances
104 # Returns a paginator and a collection of Active Record model instances
105 # for the paginator's current page. This is designed to be used in a
105 # for the paginator's current page. This is designed to be used in a
106 # single action; to automatically paginate multiple actions, consider
106 # single action; to automatically paginate multiple actions, consider
107 # ClassMethods#paginate.
107 # ClassMethods#paginate.
108 #
108 #
109 # +options+ are:
109 # +options+ are:
110 # <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
110 # <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
111 # <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
111 # <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
112 # camelizing the singular name
112 # camelizing the singular name
113 # <tt>:per_page</tt>:: the maximum number of items to include in a
113 # <tt>:per_page</tt>:: the maximum number of items to include in a
114 # single page. Defaults to 10
114 # single page. Defaults to 10
115 # <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
115 # <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
116 # Model.count
116 # Model.count
117 # <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
117 # <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
118 # <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
118 # <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
119 # <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
119 # <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
120 # and Model.count
120 # and Model.count
121 # <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
121 # <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
122 # and Model.count
122 # and Model.count
123 # <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
123 # <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
124 # and Model.count
124 # and Model.count
125 # <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
125 # <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
126 #
126 #
127 # <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
127 # <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
128 #
128 #
129 # <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
129 # <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
130 #
130 #
131 def paginate(collection_id, options={})
131 def paginate(collection_id, options={})
132 Pagination.validate_options!(collection_id, options, true)
132 Pagination.validate_options!(collection_id, options, true)
133 paginator_and_collection_for(collection_id, options)
133 paginator_and_collection_for(collection_id, options)
134 end
134 end
135
135
136 # These methods become class methods on any controller
136 # These methods become class methods on any controller
137 module ClassMethods
137 module ClassMethods
138 # Creates a +before_filter+ which automatically paginates an Active
138 # Creates a +before_filter+ which automatically paginates an Active
139 # Record model for all actions in a controller (or certain actions if
139 # Record model for all actions in a controller (or certain actions if
140 # specified with the <tt>:actions</tt> option).
140 # specified with the <tt>:actions</tt> option).
141 #
141 #
142 # +options+ are the same as PaginationHelper#paginate, with the addition
142 # +options+ are the same as PaginationHelper#paginate, with the addition
143 # of:
143 # of:
144 # <tt>:actions</tt>:: an array of actions for which the pagination is
144 # <tt>:actions</tt>:: an array of actions for which the pagination is
145 # active. Defaults to +nil+ (i.e., every action)
145 # active. Defaults to +nil+ (i.e., every action)
146 def paginate(collection_id, options={})
146 def paginate(collection_id, options={})
147 Pagination.validate_options!(collection_id, options, false)
147 Pagination.validate_options!(collection_id, options, false)
148 module_eval do
148 module_eval do
149 before_filter :create_paginators_and_retrieve_collections
149 before_filter :create_paginators_and_retrieve_collections
150 OPTIONS[self] ||= Hash.new
150 OPTIONS[self] ||= Hash.new
151 OPTIONS[self][collection_id] = options
151 OPTIONS[self][collection_id] = options
152 end
152 end
153 end
153 end
154 end
154 end
155
155
156 def create_paginators_and_retrieve_collections #:nodoc:
156 def create_paginators_and_retrieve_collections #:nodoc:
157 Pagination::OPTIONS[self.class].each do |collection_id, options|
157 Pagination::OPTIONS[self.class].each do |collection_id, options|
158 next unless options[:actions].include? action_name if
158 next unless options[:actions].include? action_name if
159 options[:actions]
159 options[:actions]
160
160
161 paginator, collection =
161 paginator, collection =
162 paginator_and_collection_for(collection_id, options)
162 paginator_and_collection_for(collection_id, options)
163
163
164 paginator_name = "@#{options[:singular_name]}_pages"
164 paginator_name = "@#{options[:singular_name]}_pages"
165 self.instance_variable_set(paginator_name, paginator)
165 self.instance_variable_set(paginator_name, paginator)
166
166
167 collection_name = "@#{collection_id.to_s}"
167 collection_name = "@#{collection_id.to_s}"
168 self.instance_variable_set(collection_name, collection)
168 self.instance_variable_set(collection_name, collection)
169 end
169 end
170 end
170 end
171
171
172 # Returns the total number of items in the collection to be paginated for
172 # Returns the total number of items in the collection to be paginated for
173 # the +model+ and given +conditions+. Override this method to implement a
173 # the +model+ and given +conditions+. Override this method to implement a
174 # custom counter.
174 # custom counter.
175 def count_collection_for_pagination(model, options)
175 def count_collection_for_pagination(model, options)
176 model.count(:conditions => options[:conditions],
176 model.count(:conditions => options[:conditions],
177 :joins => options[:join] || options[:joins],
177 :joins => options[:join] || options[:joins],
178 :include => options[:include],
178 :include => options[:include],
179 :select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
179 :select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
180 end
180 end
181
181
182 # Returns a collection of items for the given +model+ and +options[conditions]+,
182 # Returns a collection of items for the given +model+ and +options[conditions]+,
183 # ordered by +options[order]+, for the current page in the given +paginator+.
183 # ordered by +options[order]+, for the current page in the given +paginator+.
184 # Override this method to implement a custom finder.
184 # Override this method to implement a custom finder.
185 def find_collection_for_pagination(model, options, paginator)
185 def find_collection_for_pagination(model, options, paginator)
186 model.find(:all, :conditions => options[:conditions],
186 model.find(:all, :conditions => options[:conditions],
187 :order => options[:order_by] || options[:order],
187 :order => options[:order_by] || options[:order],
188 :joins => options[:join] || options[:joins], :include => options[:include],
188 :joins => options[:join] || options[:joins], :include => options[:include],
189 :select => options[:select], :limit => options[:per_page],
189 :select => options[:select], :limit => options[:per_page],
190 :group => options[:group], :offset => paginator.current.offset)
190 :group => options[:group], :offset => paginator.current.offset)
191 end
191 end
192
192
193 protected :create_paginators_and_retrieve_collections,
193 protected :create_paginators_and_retrieve_collections,
194 :count_collection_for_pagination,
194 :count_collection_for_pagination,
195 :find_collection_for_pagination
195 :find_collection_for_pagination
196
196
197 def paginator_and_collection_for(collection_id, options) #:nodoc:
197 def paginator_and_collection_for(collection_id, options) #:nodoc:
198 klass = options[:class_name].constantize
198 klass = options[:class_name].constantize
199 page = params[options[:parameter]]
199 page = params[options[:parameter]]
200 count = count_collection_for_pagination(klass, options)
200 count = count_collection_for_pagination(klass, options)
201 paginator = Paginator.new(self, count, options[:per_page], page)
201 paginator = Paginator.new(self, count, options[:per_page], page)
202 collection = find_collection_for_pagination(klass, options, paginator)
202 collection = find_collection_for_pagination(klass, options, paginator)
203
203
204 return paginator, collection
204 return paginator, collection
205 end
205 end
206
206
207 private :paginator_and_collection_for
207 private :paginator_and_collection_for
208
208
209 # A class representing a paginator for an Active Record collection.
209 # A class representing a paginator for an Active Record collection.
210 class Paginator
210 class Paginator
211 include Enumerable
211 include Enumerable
212
212
213 # Creates a new Paginator on the given +controller+ for a set of items
213 # Creates a new Paginator on the given +controller+ for a set of items
214 # of size +item_count+ and having +items_per_page+ items per page.
214 # of size +item_count+ and having +items_per_page+ items per page.
215 # Raises ArgumentError if items_per_page is out of bounds (i.e., less
215 # Raises ArgumentError if items_per_page is out of bounds (i.e., less
216 # than or equal to zero). The page CGI parameter for links defaults to
216 # than or equal to zero). The page CGI parameter for links defaults to
217 # "page" and can be overridden with +page_parameter+.
217 # "page" and can be overridden with +page_parameter+.
218 def initialize(controller, item_count, items_per_page, current_page=1)
218 def initialize(controller, item_count, items_per_page, current_page=1)
219 raise ArgumentError, 'must have at least one item per page' if
219 raise ArgumentError, 'must have at least one item per page' if
220 items_per_page <= 0
220 items_per_page <= 0
221
221
222 @controller = controller
222 @controller = controller
223 @item_count = item_count || 0
223 @item_count = item_count || 0
224 @items_per_page = items_per_page
224 @items_per_page = items_per_page
225 @pages = {}
225 @pages = {}
226
226
227 self.current_page = current_page
227 self.current_page = current_page
228 end
228 end
229 attr_reader :controller, :item_count, :items_per_page
229 attr_reader :controller, :item_count, :items_per_page
230
230
231 # Sets the current page number of this paginator. If +page+ is a Page
231 # Sets the current page number of this paginator. If +page+ is a Page
232 # object, its +number+ attribute is used as the value; if the page does
232 # object, its +number+ attribute is used as the value; if the page does
233 # not belong to this Paginator, an ArgumentError is raised.
233 # not belong to this Paginator, an ArgumentError is raised.
234 def current_page=(page)
234 def current_page=(page)
235 if page.is_a? Page
235 if page.is_a? Page
236 raise ArgumentError, 'Page/Paginator mismatch' unless
236 raise ArgumentError, 'Page/Paginator mismatch' unless
237 page.paginator == self
237 page.paginator == self
238 end
238 end
239 page = page.to_i
239 page = page.to_i
240 @current_page_number = has_page_number?(page) ? page : 1
240 @current_page_number = has_page_number?(page) ? page : 1
241 end
241 end
242
242
243 # Returns a Page object representing this paginator's current page.
243 # Returns a Page object representing this paginator's current page.
244 def current_page
244 def current_page
245 @current_page ||= self[@current_page_number]
245 @current_page ||= self[@current_page_number]
246 end
246 end
247 alias current :current_page
247 alias current :current_page
248
248
249 # Returns a new Page representing the first page in this paginator.
249 # Returns a new Page representing the first page in this paginator.
250 def first_page
250 def first_page
251 @first_page ||= self[1]
251 @first_page ||= self[1]
252 end
252 end
253 alias first :first_page
253 alias first :first_page
254
254
255 # Returns a new Page representing the last page in this paginator.
255 # Returns a new Page representing the last page in this paginator.
256 def last_page
256 def last_page
257 @last_page ||= self[page_count]
257 @last_page ||= self[page_count]
258 end
258 end
259 alias last :last_page
259 alias last :last_page
260
260
261 # Returns the number of pages in this paginator.
261 # Returns the number of pages in this paginator.
262 def page_count
262 def page_count
263 @page_count ||= @item_count.zero? ? 1 :
263 @page_count ||= @item_count.zero? ? 1 :
264 (q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
264 (q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
265 end
265 end
266
266
267 alias length :page_count
267 alias length :page_count
268
268
269 # Returns true if this paginator contains the page of index +number+.
269 # Returns true if this paginator contains the page of index +number+.
270 def has_page_number?(number)
270 def has_page_number?(number)
271 number >= 1 and number <= page_count
271 number >= 1 and number <= page_count
272 end
272 end
273
273
274 # Returns a new Page representing the page with the given index
274 # Returns a new Page representing the page with the given index
275 # +number+.
275 # +number+.
276 def [](number)
276 def [](number)
277 @pages[number] ||= Page.new(self, number)
277 @pages[number] ||= Page.new(self, number)
278 end
278 end
279
279
280 # Successively yields all the paginator's pages to the given block.
280 # Successively yields all the paginator's pages to the given block.
281 def each(&block)
281 def each(&block)
282 page_count.times do |n|
282 page_count.times do |n|
283 yield self[n+1]
283 yield self[n+1]
284 end
284 end
285 end
285 end
286
286
287 # A class representing a single page in a paginator.
287 # A class representing a single page in a paginator.
288 class Page
288 class Page
289 include Comparable
289 include Comparable
290
290
291 # Creates a new Page for the given +paginator+ with the index
291 # Creates a new Page for the given +paginator+ with the index
292 # +number+. If +number+ is not in the range of valid page numbers or
292 # +number+. If +number+ is not in the range of valid page numbers or
293 # is not a number at all, it defaults to 1.
293 # is not a number at all, it defaults to 1.
294 def initialize(paginator, number)
294 def initialize(paginator, number)
295 @paginator = paginator
295 @paginator = paginator
296 @number = number.to_i
296 @number = number.to_i
297 @number = 1 unless @paginator.has_page_number? @number
297 @number = 1 unless @paginator.has_page_number? @number
298 end
298 end
299 attr_reader :paginator, :number
299 attr_reader :paginator, :number
300 alias to_i :number
300 alias to_i :number
301
301
302 # Compares two Page objects and returns true when they represent the
302 # Compares two Page objects and returns true when they represent the
303 # same page (i.e., their paginators are the same and they have the
303 # same page (i.e., their paginators are the same and they have the
304 # same page number).
304 # same page number).
305 def ==(page)
305 def ==(page)
306 return false if page.nil?
306 return false if page.nil?
307 @paginator == page.paginator and
307 @paginator == page.paginator and
308 @number == page.number
308 @number == page.number
309 end
309 end
310
310
311 # Compares two Page objects and returns -1 if the left-hand page comes
311 # Compares two Page objects and returns -1 if the left-hand page comes
312 # before the right-hand page, 0 if the pages are equal, and 1 if the
312 # before the right-hand page, 0 if the pages are equal, and 1 if the
313 # left-hand page comes after the right-hand page. Raises ArgumentError
313 # left-hand page comes after the right-hand page. Raises ArgumentError
314 # if the pages do not belong to the same Paginator object.
314 # if the pages do not belong to the same Paginator object.
315 def <=>(page)
315 def <=>(page)
316 raise ArgumentError unless @paginator == page.paginator
316 raise ArgumentError unless @paginator == page.paginator
317 @number <=> page.number
317 @number <=> page.number
318 end
318 end
319
319
320 # Returns the item offset for the first item in this page.
320 # Returns the item offset for the first item in this page.
321 def offset
321 def offset
322 @paginator.items_per_page * (@number - 1)
322 @paginator.items_per_page * (@number - 1)
323 end
323 end
324
324
325 # Returns the number of the first item displayed.
325 # Returns the number of the first item displayed.
326 def first_item
326 def first_item
327 offset + 1
327 offset + 1
328 end
328 end
329
329
330 # Returns the number of the last item displayed.
330 # Returns the number of the last item displayed.
331 def last_item
331 def last_item
332 [@paginator.items_per_page * @number, @paginator.item_count].min
332 [@paginator.items_per_page * @number, @paginator.item_count].min
333 end
333 end
334
334
335 # Returns true if this page is the first page in the paginator.
335 # Returns true if this page is the first page in the paginator.
336 def first?
336 def first?
337 self == @paginator.first
337 self == @paginator.first
338 end
338 end
339
339
340 # Returns true if this page is the last page in the paginator.
340 # Returns true if this page is the last page in the paginator.
341 def last?
341 def last?
342 self == @paginator.last
342 self == @paginator.last
343 end
343 end
344
344
345 # Returns a new Page object representing the page just before this
345 # Returns a new Page object representing the page just before this
346 # page, or nil if this is the first page.
346 # page, or nil if this is the first page.
347 def previous
347 def previous
348 if first? then nil else @paginator[@number - 1] end
348 if first? then nil else @paginator[@number - 1] end
349 end
349 end
350
350
351 # Returns a new Page object representing the page just after this
351 # Returns a new Page object representing the page just after this
352 # page, or nil if this is the last page.
352 # page, or nil if this is the last page.
353 def next
353 def next
354 if last? then nil else @paginator[@number + 1] end
354 if last? then nil else @paginator[@number + 1] end
355 end
355 end
356
356
357 # Returns a new Window object for this page with the specified
357 # Returns a new Window object for this page with the specified
358 # +padding+.
358 # +padding+.
359 def window(padding=2)
359 def window(padding=2)
360 Window.new(self, padding)
360 Window.new(self, padding)
361 end
361 end
362
362
363 # Returns the limit/offset array for this page.
363 # Returns the limit/offset array for this page.
364 def to_sql
364 def to_sql
365 [@paginator.items_per_page, offset]
365 [@paginator.items_per_page, offset]
366 end
366 end
367
367
368 def to_param #:nodoc:
368 def to_param #:nodoc:
369 @number.to_s
369 @number.to_s
370 end
370 end
371 end
371 end
372
372
373 # A class for representing ranges around a given page.
373 # A class for representing ranges around a given page.
374 class Window
374 class Window
375 # Creates a new Window object for the given +page+ with the specified
375 # Creates a new Window object for the given +page+ with the specified
376 # +padding+.
376 # +padding+.
377 def initialize(page, padding=2)
377 def initialize(page, padding=2)
378 @paginator = page.paginator
378 @paginator = page.paginator
379 @page = page
379 @page = page
380 self.padding = padding
380 self.padding = padding
381 end
381 end
382 attr_reader :paginator, :page
382 attr_reader :paginator, :page
383
383
384 # Sets the window's padding (the number of pages on either side of the
384 # Sets the window's padding (the number of pages on either side of the
385 # window page).
385 # window page).
386 def padding=(padding)
386 def padding=(padding)
387 @padding = padding < 0 ? 0 : padding
387 @padding = padding < 0 ? 0 : padding
388 # Find the beginning and end pages of the window
388 # Find the beginning and end pages of the window
389 @first = @paginator.has_page_number?(@page.number - @padding) ?
389 @first = @paginator.has_page_number?(@page.number - @padding) ?
390 @paginator[@page.number - @padding] : @paginator.first
390 @paginator[@page.number - @padding] : @paginator.first
391 @last = @paginator.has_page_number?(@page.number + @padding) ?
391 @last = @paginator.has_page_number?(@page.number + @padding) ?
392 @paginator[@page.number + @padding] : @paginator.last
392 @paginator[@page.number + @padding] : @paginator.last
393 end
393 end
394 attr_reader :padding, :first, :last
394 attr_reader :padding, :first, :last
395
395
396 # Returns an array of Page objects in the current window.
396 # Returns an array of Page objects in the current window.
397 def pages
397 def pages
398 (@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
398 (@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
399 end
399 end
400 alias to_a :pages
400 alias to_a :pages
401 end
401 end
402 end
402 end
403
403
404 end
404 end
405 end
405 end
@@ -1,143 +1,143
1 # One of the magic features that that engines plugin provides is the ability to
1 # One of the magic features that that engines plugin provides is the ability to
2 # override selected methods in controllers and helpers from your application.
2 # override selected methods in controllers and helpers from your application.
3 # This is achieved by trapping requests to load those files, and then mixing in
3 # This is achieved by trapping requests to load those files, and then mixing in
4 # code from plugins (in the order the plugins were loaded) before finally loading
4 # code from plugins (in the order the plugins were loaded) before finally loading
5 # any versions from the main +app+ directory.
5 # any versions from the main +app+ directory.
6 #
6 #
7 # The behaviour of this extension is output to the log file for help when
7 # The behaviour of this extension is output to the log file for help when
8 # debugging.
8 # debugging.
9 #
9 #
10 # == Example
10 # == Example
11 #
11 #
12 # A plugin contains the following controller in <tt>plugin/app/controllers/my_controller.rb</tt>:
12 # A plugin contains the following controller in <tt>plugin/app/controllers/my_controller.rb</tt>:
13 #
13 #
14 # class MyController < ApplicationController
14 # class MyController < ApplicationController
15 # def index
15 # def index
16 # @name = "HAL 9000"
16 # @name = "HAL 9000"
17 # end
17 # end
18 # def list
18 # def list
19 # @robots = Robot.find(:all)
19 # @robots = Robot.find(:all)
20 # end
20 # end
21 # end
21 # end
22 #
22 #
23 # In one application that uses this plugin, we decide that the name used in the
23 # In one application that uses this plugin, we decide that the name used in the
24 # index action should be "Robbie", not "HAL 9000". To override this single method,
24 # index action should be "Robbie", not "HAL 9000". To override this single method,
25 # we create the corresponding controller in our application
25 # we create the corresponding controller in our application
26 # (<tt>RAILS_ROOT/app/controllers/my_controller.rb</tt>), and redefine the method:
26 # (<tt>RAILS_ROOT/app/controllers/my_controller.rb</tt>), and redefine the method:
27 #
27 #
28 # class MyController < ApplicationController
28 # class MyController < ApplicationController
29 # def index
29 # def index
30 # @name = "Robbie"
30 # @name = "Robbie"
31 # end
31 # end
32 # end
32 # end
33 #
33 #
34 # The list method remains as it was defined in the plugin controller.
34 # The list method remains as it was defined in the plugin controller.
35 #
35 #
36 # The same basic principle applies to helpers, and also views and partials (although
36 # The same basic principle applies to helpers, and also views and partials (although
37 # view overriding is performed in Engines::RailsExtensions::Templates; see that
37 # view overriding is performed in Engines::RailsExtensions::Templates; see that
38 # module for more information).
38 # module for more information).
39 #
39 #
40 # === What about models?
40 # === What about models?
41 #
41 #
42 # Unfortunately, it's not possible to provide this kind of magic for models.
42 # Unfortunately, it's not possible to provide this kind of magic for models.
43 # The only reason why it's possible for controllers and helpers is because
43 # The only reason why it's possible for controllers and helpers is because
44 # they can be recognised by their filenames ("whatever_controller", "jazz_helper"),
44 # they can be recognised by their filenames ("whatever_controller", "jazz_helper"),
45 # whereas models appear the same as any other typical Ruby library ("node",
45 # whereas models appear the same as any other typical Ruby library ("node",
46 # "user", "image", etc.).
46 # "user", "image", etc.).
47 #
47 #
48 # If mixing were allowed in models, it would mean code mixing for *every*
48 # If mixing were allowed in models, it would mean code mixing for *every*
49 # file that was loaded via +require_or_load+, and this could result in
49 # file that was loaded via +require_or_load+, and this could result in
50 # problems where, for example, a Node model might start to include
50 # problems where, for example, a Node model might start to include
51 # functionality from another file called "node" somewhere else in the
51 # functionality from another file called "node" somewhere else in the
52 # <tt>$LOAD_PATH</tt>.
52 # <tt>$LOAD_PATH</tt>.
53 #
53 #
54 # One way to overcome this is to provide model functionality as a module in
54 # One way to overcome this is to provide model functionality as a module in
55 # a plugin, which developers can then include into their own model
55 # a plugin, which developers can then include into their own model
56 # implementations.
56 # implementations.
57 #
57 #
58 # Another option is to provide an abstract model (see the ActiveRecord::Base
58 # Another option is to provide an abstract model (see the ActiveRecord::Base
59 # documentation) and have developers subclass this model in their own
59 # documentation) and have developers subclass this model in their own
60 # application if they must.
60 # application if they must.
61 #
61 #
62 # ---
62 # ---
63 #
63 #
64 # The Engines::RailsExtensions::Dependencies module includes a method to
64 # The Engines::RailsExtensions::Dependencies module includes a method to
65 # override Dependencies.require_or_load, which is called to load code needed
65 # override Dependencies.require_or_load, which is called to load code needed
66 # by Rails as it encounters constants that aren't defined.
66 # by Rails as it encounters constants that aren't defined.
67 #
67 #
68 # This method is enhanced with the code-mixing features described above.
68 # This method is enhanced with the code-mixing features described above.
69 #
69 #
70 module Engines::RailsExtensions::Dependencies
70 module Engines::RailsExtensions::Dependencies
71 def self.included(base) #:nodoc:
71 def self.included(base) #:nodoc:
72 base.class_eval { alias_method_chain :require_or_load, :engine_additions }
72 base.class_eval { alias_method_chain :require_or_load, :engine_additions }
73 end
73 end
74
74
75 # Attempt to load the given file from any plugins, as well as the application.
75 # Attempt to load the given file from any plugins, as well as the application.
76 # This performs the 'code mixing' magic, allowing application controllers and
76 # This performs the 'code mixing' magic, allowing application controllers and
77 # helpers to override single methods from those in plugins.
77 # helpers to override single methods from those in plugins.
78 # If the file can be found in any plugins, it will be loaded first from those
78 # If the file can be found in any plugins, it will be loaded first from those
79 # locations. Finally, the application version is loaded, using Ruby's behaviour
79 # locations. Finally, the application version is loaded, using Ruby's behaviour
80 # to replace existing methods with their new definitions.
80 # to replace existing methods with their new definitions.
81 #
81 #
82 # If <tt>Engines.disable_code_mixing == true</tt>, the first controller/helper on the
82 # If <tt>Engines.disable_code_mixing == true</tt>, the first controller/helper on the
83 # <tt>$LOAD_PATH</tt> will be used (plugins' +app+ directories are always lower on the
83 # <tt>$LOAD_PATH</tt> will be used (plugins' +app+ directories are always lower on the
84 # <tt>$LOAD_PATH</tt> than the main +app+ directory).
84 # <tt>$LOAD_PATH</tt> than the main +app+ directory).
85 #
85 #
86 # If <tt>Engines.disable_application_code_loading == true</tt>, controllers will
86 # If <tt>Engines.disable_application_code_loading == true</tt>, controllers will
87 # not be loaded from the main +app+ directory *if* they are present in any
87 # not be loaded from the main +app+ directory *if* they are present in any
88 # plugins.
88 # plugins.
89 #
89 #
90 # Returns true if the file could be loaded (from anywhere); false otherwise -
90 # Returns true if the file could be loaded (from anywhere); false otherwise -
91 # mirroring the behaviour of +require_or_load+ from Rails (which mirrors
91 # mirroring the behaviour of +require_or_load+ from Rails (which mirrors
92 # that of Ruby's own +require+, I believe).
92 # that of Ruby's own +require+, I believe).
93 def require_or_load_with_engine_additions(file_name, const_path=nil)
93 def require_or_load_with_engine_additions(file_name, const_path=nil)
94 return require_or_load_without_engine_additions(file_name, const_path) if Engines.disable_code_mixing
94 return require_or_load_without_engine_additions(file_name, const_path) if Engines.disable_code_mixing
95
95
96 file_loaded = false
96 file_loaded = false
97
97
98 # try and load the plugin code first
98 # try and load the plugin code first
99 # can't use model, as there's nothing in the name to indicate that the file is a 'model' file
99 # can't use model, as there's nothing in the name to indicate that the file is a 'model' file
100 # rather than a library or anything else.
100 # rather than a library or anything else.
101 Engines.code_mixing_file_types.each do |file_type|
101 Engines.code_mixing_file_types.each do |file_type|
102 # if we recognise this type
102 # if we recognise this type
103 # (this regexp splits out the module/filename from any instances of app/#{type}, so that
103 # (this regexp splits out the module/filename from any instances of app/#{type}, so that
104 # modules are still respected.)
104 # modules are still respected.)
105 if file_name =~ /^(.*app\/#{file_type}s\/)?(.*_#{file_type})(\.rb)?$/
105 if file_name =~ /^(.*app\/#{file_type}s\/)?(.*_#{file_type})(\.rb)?$/
106 base_name = $2
106 base_name = $2
107 # ... go through the plugins from first started to last, so that
107 # ... go through the plugins from first started to last, so that
108 # code with a high precedence (started later) will override lower precedence
108 # code with a high precedence (started later) will override lower precedence
109 # implementations
109 # implementations
110 Engines.plugins.each do |plugin|
110 Engines.plugins.each do |plugin|
111 plugin_file_name = File.expand_path(File.join(plugin.directory, 'app', "#{file_type}s", base_name))
111 plugin_file_name = File.expand_path(File.join(plugin.directory, 'app', "#{file_type}s", base_name))
112 Engines.logger.debug("checking plugin '#{plugin.name}' for '#{base_name}'")
112 Engines.logger.debug("checking plugin '#{plugin.name}' for '#{base_name}'")
113 if File.file?("#{plugin_file_name}.rb")
113 if File.file?("#{plugin_file_name}.rb")
114 Engines.logger.debug("==> loading from plugin '#{plugin.name}'")
114 Engines.logger.debug("==> loading from plugin '#{plugin.name}'")
115 file_loaded = true if require_or_load_without_engine_additions(plugin_file_name, const_path)
115 file_loaded = true if require_or_load_without_engine_additions(plugin_file_name, const_path)
116 end
116 end
117 end
117 end
118
118
119 # finally, load any application-specific controller classes using the 'proper'
119 # finally, load any application-specific controller classes using the 'proper'
120 # rails load mechanism, EXCEPT when we're testing engines and could load this file
120 # rails load mechanism, EXCEPT when we're testing engines and could load this file
121 # from an engine
121 # from an engine
122 if Engines.disable_application_code_loading
122 if Engines.disable_application_code_loading
123 Engines.logger.debug("loading from application disabled.")
123 Engines.logger.debug("loading from application disabled.")
124 else
124 else
125 # Ensure we are only loading from the /app directory at this point
125 # Ensure we are only loading from the /app directory at this point
126 app_file_name = File.join(RAILS_ROOT, 'app', "#{file_type}s", "#{base_name}")
126 app_file_name = File.join(RAILS_ROOT, 'app', "#{file_type}s", "#{base_name}")
127 if File.file?("#{app_file_name}.rb")
127 if File.file?("#{app_file_name}.rb")
128 Engines.logger.debug("loading from application: #{base_name}")
128 Engines.logger.debug("loading from application: #{base_name}")
129 file_loaded = true if require_or_load_without_engine_additions(app_file_name, const_path)
129 file_loaded = true if require_or_load_without_engine_additions(app_file_name, const_path)
130 else
130 else
131 Engines.logger.debug("(file not found in application)")
131 Engines.logger.debug("(file not found in application)")
132 end
132 end
133 end
133 end
134 end
134 end
135 end
135 end
136
136
137 # if we managed to load a file, return true. If not, default to the original method.
137 # if we managed to load a file, return true. If not, default to the original method.
138 # Note that this relies on the RHS of a boolean || not to be evaluated if the LHS is true.
138 # Note that this relies on the RHS of a boolean || not to be evaluated if the LHS is true.
139 file_loaded || require_or_load_without_engine_additions(file_name, const_path)
139 file_loaded || require_or_load_without_engine_additions(file_name, const_path)
140 end
140 end
141 end
141 end
142
142
143 Dependencies.send :include, Engines::RailsExtensions::Dependencies
143 ActiveSupport::Dependencies.send :include, Engines::RailsExtensions::Dependencies
General Comments 0
You need to be logged in to leave comments. Login now