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