##// END OF EJS Templates
Fixed: list of users for adding to a group may be empty if 100 first users have been added (#8029)....
Jean-Philippe Lang -
r5164:b972b5a647ca
parent child
Show More
@@ -1,170 +1,170
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 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 class GroupsController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin
22 22
23 23 helper :custom_fields
24 24
25 25 # GET /groups
26 26 # GET /groups.xml
27 27 def index
28 28 @groups = Group.find(:all, :order => 'lastname')
29 29
30 30 respond_to do |format|
31 31 format.html # index.html.erb
32 32 format.xml { render :xml => @groups }
33 33 end
34 34 end
35 35
36 36 # GET /groups/1
37 37 # GET /groups/1.xml
38 38 def show
39 39 @group = Group.find(params[:id])
40 40
41 41 respond_to do |format|
42 42 format.html # show.html.erb
43 43 format.xml { render :xml => @group }
44 44 end
45 45 end
46 46
47 47 # GET /groups/new
48 48 # GET /groups/new.xml
49 49 def new
50 50 @group = Group.new
51 51
52 52 respond_to do |format|
53 53 format.html # new.html.erb
54 54 format.xml { render :xml => @group }
55 55 end
56 56 end
57 57
58 58 # GET /groups/1/edit
59 59 def edit
60 60 @group = Group.find(params[:id], :include => :projects)
61 61 end
62 62
63 63 # POST /groups
64 64 # POST /groups.xml
65 65 def create
66 66 @group = Group.new(params[:group])
67 67
68 68 respond_to do |format|
69 69 if @group.save
70 70 flash[:notice] = l(:notice_successful_create)
71 71 format.html { redirect_to(groups_path) }
72 72 format.xml { render :xml => @group, :status => :created, :location => @group }
73 73 else
74 74 format.html { render :action => "new" }
75 75 format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
76 76 end
77 77 end
78 78 end
79 79
80 80 # PUT /groups/1
81 81 # PUT /groups/1.xml
82 82 def update
83 83 @group = Group.find(params[:id])
84 84
85 85 respond_to do |format|
86 86 if @group.update_attributes(params[:group])
87 87 flash[:notice] = l(:notice_successful_update)
88 88 format.html { redirect_to(groups_path) }
89 89 format.xml { head :ok }
90 90 else
91 91 format.html { render :action => "edit" }
92 92 format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
93 93 end
94 94 end
95 95 end
96 96
97 97 # DELETE /groups/1
98 98 # DELETE /groups/1.xml
99 99 def destroy
100 100 @group = Group.find(params[:id])
101 101 @group.destroy
102 102
103 103 respond_to do |format|
104 104 format.html { redirect_to(groups_url) }
105 105 format.xml { head :ok }
106 106 end
107 107 end
108 108
109 109 def add_users
110 110 @group = Group.find(params[:id])
111 111 users = User.find_all_by_id(params[:user_ids])
112 112 @group.users << users if request.post?
113 113 respond_to do |format|
114 114 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
115 115 format.js {
116 116 render(:update) {|page|
117 117 page.replace_html "tab-content-users", :partial => 'groups/users'
118 118 users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") }
119 119 }
120 120 }
121 121 end
122 122 end
123 123
124 124 def remove_user
125 125 @group = Group.find(params[:id])
126 126 @group.users.delete(User.find(params[:user_id])) if request.post?
127 127 respond_to do |format|
128 128 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
129 129 format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} }
130 130 end
131 131 end
132 132
133 133 def autocomplete_for_user
134 134 @group = Group.find(params[:id])
135 @users = User.active.like(params[:q]).find(:all, :limit => 100) - @group.users
135 @users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100)
136 136 render :layout => false
137 137 end
138 138
139 139 def edit_membership
140 140 @group = Group.find(params[:id])
141 141 @membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
142 142 @membership.save if request.post?
143 143 respond_to do |format|
144 144 if @membership.valid?
145 145 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
146 146 format.js {
147 147 render(:update) {|page|
148 148 page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
149 149 page.visual_effect(:highlight, "member-#{@membership.id}")
150 150 }
151 151 }
152 152 else
153 153 format.js {
154 154 render(:update) {|page|
155 155 page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
156 156 }
157 157 }
158 158 end
159 159 end
160 160 end
161 161
162 162 def destroy_membership
163 163 @group = Group.find(params[:id])
164 164 Member.find(params[:membership_id]).destroy if request.post?
165 165 respond_to do |format|
166 166 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
167 167 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} }
168 168 end
169 169 end
170 170 end
@@ -1,595 +1,599
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 < Principal
21 21 include Redmine::SafeAttributes
22 22
23 23 # Account statuses
24 24 STATUS_ANONYMOUS = 0
25 25 STATUS_ACTIVE = 1
26 26 STATUS_REGISTERED = 2
27 27 STATUS_LOCKED = 3
28 28
29 29 USER_FORMATS = {
30 30 :firstname_lastname => '#{firstname} #{lastname}',
31 31 :firstname => '#{firstname}',
32 32 :lastname_firstname => '#{lastname} #{firstname}',
33 33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
34 34 :username => '#{login}'
35 35 }
36 36
37 37 MAIL_NOTIFICATION_OPTIONS = [
38 38 ['all', :label_user_mail_option_all],
39 39 ['selected', :label_user_mail_option_selected],
40 40 ['only_my_events', :label_user_mail_option_only_my_events],
41 41 ['only_assigned', :label_user_mail_option_only_assigned],
42 42 ['only_owner', :label_user_mail_option_only_owner],
43 43 ['none', :label_user_mail_option_none]
44 44 ]
45 45
46 46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
47 47 :after_remove => Proc.new {|user, group| group.user_removed(user)}
48 48 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
49 49 has_many :changesets, :dependent => :nullify
50 50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
51 51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
52 52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
53 53 belongs_to :auth_source
54 54
55 55 # Active non-anonymous users scope
56 56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
57 57
58 58 acts_as_customizable
59 59
60 60 attr_accessor :password, :password_confirmation
61 61 attr_accessor :last_before_login_on
62 62 # Prevents unauthorized assignments
63 63 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
64 64
65 65 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
66 66 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
67 67 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
68 68 # Login must contain lettres, numbers, underscores only
69 69 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
70 70 validates_length_of :login, :maximum => 30
71 71 validates_length_of :firstname, :lastname, :maximum => 30
72 72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
73 73 validates_length_of :mail, :maximum => 60, :allow_nil => true
74 74 validates_confirmation_of :password, :allow_nil => true
75 75 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
76 76
77 77 before_destroy :remove_references_before_destroy
78 78
79 79 named_scope :in_group, lambda {|group|
80 80 group_id = group.is_a?(Group) ? group.id : group.to_i
81 81 { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
82 82 }
83 named_scope :not_in_group, lambda {|group|
84 group_id = group.is_a?(Group) ? group.id : group.to_i
85 { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
86 }
83 87
84 88 def before_create
85 89 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
86 90 true
87 91 end
88 92
89 93 def before_save
90 94 # update hashed_password if password was set
91 95 if self.password && self.auth_source_id.blank?
92 96 salt_password(password)
93 97 end
94 98 end
95 99
96 100 def reload(*args)
97 101 @name = nil
98 102 @projects_by_role = nil
99 103 super
100 104 end
101 105
102 106 def mail=(arg)
103 107 write_attribute(:mail, arg.to_s.strip)
104 108 end
105 109
106 110 def identity_url=(url)
107 111 if url.blank?
108 112 write_attribute(:identity_url, '')
109 113 else
110 114 begin
111 115 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
112 116 rescue OpenIdAuthentication::InvalidOpenId
113 117 # Invlaid url, don't save
114 118 end
115 119 end
116 120 self.read_attribute(:identity_url)
117 121 end
118 122
119 123 # Returns the user that matches provided login and password, or nil
120 124 def self.try_to_login(login, password)
121 125 # Make sure no one can sign in with an empty password
122 126 return nil if password.to_s.empty?
123 127 user = find_by_login(login)
124 128 if user
125 129 # user is already in local database
126 130 return nil if !user.active?
127 131 if user.auth_source
128 132 # user has an external authentication method
129 133 return nil unless user.auth_source.authenticate(login, password)
130 134 else
131 135 # authentication with local password
132 136 return nil unless user.check_password?(password)
133 137 end
134 138 else
135 139 # user is not yet registered, try to authenticate with available sources
136 140 attrs = AuthSource.authenticate(login, password)
137 141 if attrs
138 142 user = new(attrs)
139 143 user.login = login
140 144 user.language = Setting.default_language
141 145 if user.save
142 146 user.reload
143 147 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
144 148 end
145 149 end
146 150 end
147 151 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
148 152 user
149 153 rescue => text
150 154 raise text
151 155 end
152 156
153 157 # Returns the user who matches the given autologin +key+ or nil
154 158 def self.try_to_autologin(key)
155 159 tokens = Token.find_all_by_action_and_value('autologin', key)
156 160 # Make sure there's only 1 token that matches the key
157 161 if tokens.size == 1
158 162 token = tokens.first
159 163 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
160 164 token.user.update_attribute(:last_login_on, Time.now)
161 165 token.user
162 166 end
163 167 end
164 168 end
165 169
166 170 # Return user's full name for display
167 171 def name(formatter = nil)
168 172 if formatter
169 173 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
170 174 else
171 175 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
172 176 end
173 177 end
174 178
175 179 def active?
176 180 self.status == STATUS_ACTIVE
177 181 end
178 182
179 183 def registered?
180 184 self.status == STATUS_REGISTERED
181 185 end
182 186
183 187 def locked?
184 188 self.status == STATUS_LOCKED
185 189 end
186 190
187 191 def activate
188 192 self.status = STATUS_ACTIVE
189 193 end
190 194
191 195 def register
192 196 self.status = STATUS_REGISTERED
193 197 end
194 198
195 199 def lock
196 200 self.status = STATUS_LOCKED
197 201 end
198 202
199 203 def activate!
200 204 update_attribute(:status, STATUS_ACTIVE)
201 205 end
202 206
203 207 def register!
204 208 update_attribute(:status, STATUS_REGISTERED)
205 209 end
206 210
207 211 def lock!
208 212 update_attribute(:status, STATUS_LOCKED)
209 213 end
210 214
211 215 # Returns true if +clear_password+ is the correct user's password, otherwise false
212 216 def check_password?(clear_password)
213 217 if auth_source_id.present?
214 218 auth_source.authenticate(self.login, clear_password)
215 219 else
216 220 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
217 221 end
218 222 end
219 223
220 224 # Generates a random salt and computes hashed_password for +clear_password+
221 225 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
222 226 def salt_password(clear_password)
223 227 self.salt = User.generate_salt
224 228 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
225 229 end
226 230
227 231 # Does the backend storage allow this user to change their password?
228 232 def change_password_allowed?
229 233 return true if auth_source_id.blank?
230 234 return auth_source.allow_password_changes?
231 235 end
232 236
233 237 # Generate and set a random password. Useful for automated user creation
234 238 # Based on Token#generate_token_value
235 239 #
236 240 def random_password
237 241 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
238 242 password = ''
239 243 40.times { |i| password << chars[rand(chars.size-1)] }
240 244 self.password = password
241 245 self.password_confirmation = password
242 246 self
243 247 end
244 248
245 249 def pref
246 250 self.preference ||= UserPreference.new(:user => self)
247 251 end
248 252
249 253 def time_zone
250 254 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
251 255 end
252 256
253 257 def wants_comments_in_reverse_order?
254 258 self.pref[:comments_sorting] == 'desc'
255 259 end
256 260
257 261 # Return user's RSS key (a 40 chars long string), used to access feeds
258 262 def rss_key
259 263 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
260 264 token.value
261 265 end
262 266
263 267 # Return user's API key (a 40 chars long string), used to access the API
264 268 def api_key
265 269 token = self.api_token || self.create_api_token(:action => 'api')
266 270 token.value
267 271 end
268 272
269 273 # Return an array of project ids for which the user has explicitly turned mail notifications on
270 274 def notified_projects_ids
271 275 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
272 276 end
273 277
274 278 def notified_project_ids=(ids)
275 279 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
276 280 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
277 281 @notified_projects_ids = nil
278 282 notified_projects_ids
279 283 end
280 284
281 285 def valid_notification_options
282 286 self.class.valid_notification_options(self)
283 287 end
284 288
285 289 # Only users that belong to more than 1 project can select projects for which they are notified
286 290 def self.valid_notification_options(user=nil)
287 291 # Note that @user.membership.size would fail since AR ignores
288 292 # :include association option when doing a count
289 293 if user.nil? || user.memberships.length < 1
290 294 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
291 295 else
292 296 MAIL_NOTIFICATION_OPTIONS
293 297 end
294 298 end
295 299
296 300 # Find a user account by matching the exact login and then a case-insensitive
297 301 # version. Exact matches will be given priority.
298 302 def self.find_by_login(login)
299 303 # force string comparison to be case sensitive on MySQL
300 304 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
301 305
302 306 # First look for an exact match
303 307 user = first(:conditions => ["#{type_cast} login = ?", login])
304 308 # Fail over to case-insensitive if none was found
305 309 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
306 310 end
307 311
308 312 def self.find_by_rss_key(key)
309 313 token = Token.find_by_value(key)
310 314 token && token.user.active? ? token.user : nil
311 315 end
312 316
313 317 def self.find_by_api_key(key)
314 318 token = Token.find_by_action_and_value('api', key)
315 319 token && token.user.active? ? token.user : nil
316 320 end
317 321
318 322 # Makes find_by_mail case-insensitive
319 323 def self.find_by_mail(mail)
320 324 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
321 325 end
322 326
323 327 def to_s
324 328 name
325 329 end
326 330
327 331 # Returns the current day according to user's time zone
328 332 def today
329 333 if time_zone.nil?
330 334 Date.today
331 335 else
332 336 Time.now.in_time_zone(time_zone).to_date
333 337 end
334 338 end
335 339
336 340 def logged?
337 341 true
338 342 end
339 343
340 344 def anonymous?
341 345 !logged?
342 346 end
343 347
344 348 # Return user's roles for project
345 349 def roles_for_project(project)
346 350 roles = []
347 351 # No role on archived projects
348 352 return roles unless project && project.active?
349 353 if logged?
350 354 # Find project membership
351 355 membership = memberships.detect {|m| m.project_id == project.id}
352 356 if membership
353 357 roles = membership.roles
354 358 else
355 359 @role_non_member ||= Role.non_member
356 360 roles << @role_non_member
357 361 end
358 362 else
359 363 @role_anonymous ||= Role.anonymous
360 364 roles << @role_anonymous
361 365 end
362 366 roles
363 367 end
364 368
365 369 # Return true if the user is a member of project
366 370 def member_of?(project)
367 371 !roles_for_project(project).detect {|role| role.member?}.nil?
368 372 end
369 373
370 374 # Returns a hash of user's projects grouped by roles
371 375 def projects_by_role
372 376 return @projects_by_role if @projects_by_role
373 377
374 378 @projects_by_role = Hash.new {|h,k| h[k]=[]}
375 379 memberships.each do |membership|
376 380 membership.roles.each do |role|
377 381 @projects_by_role[role] << membership.project if membership.project
378 382 end
379 383 end
380 384 @projects_by_role.each do |role, projects|
381 385 projects.uniq!
382 386 end
383 387
384 388 @projects_by_role
385 389 end
386 390
387 391 # Return true if the user is allowed to do the specified action on a specific context
388 392 # Action can be:
389 393 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
390 394 # * a permission Symbol (eg. :edit_project)
391 395 # Context can be:
392 396 # * a project : returns true if user is allowed to do the specified action on this project
393 397 # * a group of projects : returns true if user is allowed on every project
394 398 # * nil with options[:global] set : check if user has at least one role allowed for this action,
395 399 # or falls back to Non Member / Anonymous permissions depending if the user is logged
396 400 def allowed_to?(action, context, options={})
397 401 if context && context.is_a?(Project)
398 402 # No action allowed on archived projects
399 403 return false unless context.active?
400 404 # No action allowed on disabled modules
401 405 return false unless context.allows_to?(action)
402 406 # Admin users are authorized for anything else
403 407 return true if admin?
404 408
405 409 roles = roles_for_project(context)
406 410 return false unless roles
407 411 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
408 412
409 413 elsif context && context.is_a?(Array)
410 414 # Authorize if user is authorized on every element of the array
411 415 context.map do |project|
412 416 allowed_to?(action,project,options)
413 417 end.inject do |memo,allowed|
414 418 memo && allowed
415 419 end
416 420 elsif options[:global]
417 421 # Admin users are always authorized
418 422 return true if admin?
419 423
420 424 # authorize if user has at least one role that has this permission
421 425 roles = memberships.collect {|m| m.roles}.flatten.uniq
422 426 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
423 427 else
424 428 false
425 429 end
426 430 end
427 431
428 432 # Is the user allowed to do the specified action on any project?
429 433 # See allowed_to? for the actions and valid options.
430 434 def allowed_to_globally?(action, options)
431 435 allowed_to?(action, nil, options.reverse_merge(:global => true))
432 436 end
433 437
434 438 safe_attributes 'login',
435 439 'firstname',
436 440 'lastname',
437 441 'mail',
438 442 'mail_notification',
439 443 'language',
440 444 'custom_field_values',
441 445 'custom_fields',
442 446 'identity_url'
443 447
444 448 safe_attributes 'status',
445 449 'auth_source_id',
446 450 :if => lambda {|user, current_user| current_user.admin?}
447 451
448 452 safe_attributes 'group_ids',
449 453 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
450 454
451 455 # Utility method to help check if a user should be notified about an
452 456 # event.
453 457 #
454 458 # TODO: only supports Issue events currently
455 459 def notify_about?(object)
456 460 case mail_notification
457 461 when 'all'
458 462 true
459 463 when 'selected'
460 464 # user receives notifications for created/assigned issues on unselected projects
461 465 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
462 466 true
463 467 else
464 468 false
465 469 end
466 470 when 'none'
467 471 false
468 472 when 'only_my_events'
469 473 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
470 474 true
471 475 else
472 476 false
473 477 end
474 478 when 'only_assigned'
475 479 if object.is_a?(Issue) && object.assigned_to == self
476 480 true
477 481 else
478 482 false
479 483 end
480 484 when 'only_owner'
481 485 if object.is_a?(Issue) && object.author == self
482 486 true
483 487 else
484 488 false
485 489 end
486 490 else
487 491 false
488 492 end
489 493 end
490 494
491 495 def self.current=(user)
492 496 @current_user = user
493 497 end
494 498
495 499 def self.current
496 500 @current_user ||= User.anonymous
497 501 end
498 502
499 503 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
500 504 # one anonymous user per database.
501 505 def self.anonymous
502 506 anonymous_user = AnonymousUser.find(:first)
503 507 if anonymous_user.nil?
504 508 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
505 509 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
506 510 end
507 511 anonymous_user
508 512 end
509 513
510 514 # Salts all existing unsalted passwords
511 515 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
512 516 # This method is used in the SaltPasswords migration and is to be kept as is
513 517 def self.salt_unsalted_passwords!
514 518 transaction do
515 519 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
516 520 next if user.hashed_password.blank?
517 521 salt = User.generate_salt
518 522 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
519 523 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
520 524 end
521 525 end
522 526 end
523 527
524 528 protected
525 529
526 530 def validate
527 531 # Password length validation based on setting
528 532 if !password.nil? && password.size < Setting.password_min_length.to_i
529 533 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
530 534 end
531 535 end
532 536
533 537 private
534 538
535 539 # Removes references that are not handled by associations
536 540 # Things that are not deleted are reassociated with the anonymous user
537 541 def remove_references_before_destroy
538 542 return if self.id.nil?
539 543
540 544 substitute = User.anonymous
541 545 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
542 546 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
543 547 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
544 548 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
545 549 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
546 550 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
547 551 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
548 552 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
549 553 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
550 554 # Remove private queries and keep public ones
551 555 Query.delete_all ['user_id = ? AND is_public = ?', id, false]
552 556 Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
553 557 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
554 558 Token.delete_all ['user_id = ?', id]
555 559 Watcher.delete_all ['user_id = ?', id]
556 560 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
557 561 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
558 562 end
559 563
560 564 # Return password digest
561 565 def self.hash_password(clear_password)
562 566 Digest::SHA1.hexdigest(clear_password || "")
563 567 end
564 568
565 569 # Returns a 128bits random salt as a hex string (32 chars long)
566 570 def self.generate_salt
567 571 ActiveSupport::SecureRandom.hex(16)
568 572 end
569 573
570 574 end
571 575
572 576 class AnonymousUser < User
573 577
574 578 def validate_on_create
575 579 # There should be only one AnonymousUser in the database
576 580 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
577 581 end
578 582
579 583 def available_custom_fields
580 584 []
581 585 end
582 586
583 587 # Overrides a few properties
584 588 def logged?; false end
585 589 def admin; false end
586 590 def name(*args); I18n.t(:label_user_anonymous) end
587 591 def mail; nil end
588 592 def time_zone; nil end
589 593 def rss_key; nil end
590 594
591 595 # Anonymous user can not be destroyed
592 596 def destroy
593 597 false
594 598 end
595 599 end
@@ -1,49 +1,49
1 1 <div class="splitcontentleft">
2 2 <% if @group.users.any? %>
3 3 <table class="list users">
4 4 <thead><tr>
5 5 <th><%= l(:label_user) %></th>
6 6 <th style="width:15%"></th>
7 7 </tr></thead>
8 8 <tbody>
9 9 <% @group.users.sort.each do |user| %>
10 10 <tr id="user-<%= user.id %>" class="<%= cycle 'odd', 'even' %>">
11 11 <td class="user"><%= link_to_user user %></td>
12 12 <td class="buttons">
13 13 <%= link_to_remote l(:button_delete), { :url => { :controller => 'groups', :action => 'remove_user', :id => @group, :user_id => user },
14 14 :method => :post },
15 15 :class => 'icon icon-del' %>
16 16 </td>
17 17 </tr>
18 18 <% end %>
19 19 </tbody>
20 20 </table>
21 21 <% else %>
22 22 <p class="nodata"><%= l(:label_no_data) %></p>
23 23 <% end %>
24 24 </div>
25 25
26 26 <div class="splitcontentright">
27 <% users = User.active.find(:all, :limit => 100) - @group.users %>
27 <% users = User.active.not_in_group(@group).all(:limit => 100) %>
28 28 <% if users.any? %>
29 29 <% remote_form_for(:group, @group, :url => {:controller => 'groups', :action => 'add_users', :id => @group}, :method => :post) do |f| %>
30 30 <fieldset><legend><%=l(:label_user_new)%></legend>
31 31
32 32 <p><%= label_tag "user_search", l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
33 33 <%= observe_field(:user_search,
34 34 :frequency => 0.5,
35 35 :update => :users,
36 36 :url => { :controller => 'groups', :action => 'autocomplete_for_user', :id => @group },
37 37 :with => 'q')
38 38 %>
39 39
40 40 <div id="users">
41 41 <%= principals_check_box_tags 'user_ids[]', users %>
42 42 </div>
43 43
44 44 <p><%= submit_tag l(:button_add) %></p>
45 45 </fieldset>
46 46 <% end %>
47 47 <% end %>
48 48
49 49 </div>
@@ -1,107 +1,116
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 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 File.expand_path('../../test_helper', __FILE__)
19 19 require 'groups_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class GroupsController; def rescue_action(e) raise e end; end
23 23
24 24 class GroupsControllerTest < ActionController::TestCase
25 25 fixtures :projects, :users, :members, :member_roles, :groups_users
26 26
27 27 def setup
28 28 @controller = GroupsController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 @request.session[:user_id] = 1
33 33 end
34 34
35 35 def test_index
36 36 get :index
37 37 assert_response :success
38 38 assert_template 'index'
39 39 end
40 40
41 41 def test_show
42 42 get :show, :id => 10
43 43 assert_response :success
44 44 assert_template 'show'
45 45 end
46 46
47 47 def test_new
48 48 get :new
49 49 assert_response :success
50 50 assert_template 'new'
51 51 end
52 52
53 53 def test_create
54 54 assert_difference 'Group.count' do
55 55 post :create, :group => {:lastname => 'New group'}
56 56 end
57 57 assert_redirected_to '/groups'
58 58 end
59 59
60 60 def test_edit
61 61 get :edit, :id => 10
62 62 assert_response :success
63 63 assert_template 'edit'
64 64 end
65 65
66 66 def test_update
67 67 post :update, :id => 10
68 68 assert_redirected_to '/groups'
69 69 end
70 70
71 71 def test_destroy
72 72 assert_difference 'Group.count', -1 do
73 73 post :destroy, :id => 10
74 74 end
75 75 assert_redirected_to '/groups'
76 76 end
77 77
78 78 def test_add_users
79 79 assert_difference 'Group.find(10).users.count', 2 do
80 80 post :add_users, :id => 10, :user_ids => ['2', '3']
81 81 end
82 82 end
83 83
84 84 def test_remove_user
85 85 assert_difference 'Group.find(10).users.count', -1 do
86 86 post :remove_user, :id => 10, :user_id => '8'
87 87 end
88 88 end
89 89
90 90 def test_new_membership
91 91 assert_difference 'Group.find(10).members.count' do
92 92 post :edit_membership, :id => 10, :membership => { :project_id => 2, :role_ids => ['1', '2']}
93 93 end
94 94 end
95 95
96 96 def test_edit_membership
97 97 assert_no_difference 'Group.find(10).members.count' do
98 98 post :edit_membership, :id => 10, :membership_id => 6, :membership => { :role_ids => ['1', '3']}
99 99 end
100 100 end
101 101
102 102 def test_destroy_membership
103 103 assert_difference 'Group.find(10).members.count', -1 do
104 104 post :destroy_membership, :id => 10, :membership_id => 6
105 105 end
106 106 end
107
108 def test_autocomplete_for_user
109 get :autocomplete_for_user, :id => 10, :q => 'mis'
110 assert_response :success
111 users = assigns(:users)
112 assert_not_nil users
113 assert users.any?
114 assert !users.include?(Group.find(10).users.first)
115 end
107 116 end
General Comments 0
You need to be logged in to leave comments. Login now