##// END OF EJS Templates
Changed the way the visibility SQL statement is built....
Jean-Philippe Lang -
r5020:5f889932b6ce
parent child
Show More
@@ -1,841 +1,847
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_ARCHIVED = 9
23 STATUS_ARCHIVED = 9
24
24
25 # Maximum length for project identifiers
25 # Maximum length for project identifiers
26 IDENTIFIER_MAX_LENGTH = 100
26 IDENTIFIER_MAX_LENGTH = 100
27
27
28 # Specific overidden Activities
28 # Specific overidden Activities
29 has_many :time_entry_activities
29 has_many :time_entry_activities
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :memberships, :class_name => 'Member'
31 has_many :memberships, :class_name => 'Member'
32 has_many :member_principals, :class_name => 'Member',
32 has_many :member_principals, :class_name => 'Member',
33 :include => :principal,
33 :include => :principal,
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 has_many :users, :through => :members
35 has_many :users, :through => :members
36 has_many :principals, :through => :member_principals, :source => :principal
36 has_many :principals, :through => :member_principals, :source => :principal
37
37
38 has_many :enabled_modules, :dependent => :delete_all
38 has_many :enabled_modules, :dependent => :delete_all
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :time_entries, :dependent => :delete_all
43 has_many :time_entries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
45 has_many :documents, :dependent => :destroy
45 has_many :documents, :dependent => :destroy
46 has_many :news, :dependent => :delete_all, :include => :author
46 has_many :news, :dependent => :delete_all, :include => :author
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_one :repository, :dependent => :destroy
49 has_one :repository, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 :class_name => 'IssueCustomField',
54 :class_name => 'IssueCustomField',
55 :order => "#{CustomField.table_name}.position",
55 :order => "#{CustomField.table_name}.position",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_nested_set :order => 'name'
59 acts_as_nested_set :order => 'name'
60 acts_as_attachable :view_permission => :view_files,
60 acts_as_attachable :view_permission => :view_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier
72 validates_uniqueness_of :identifier
73 validates_associated :repository, :wiki
73 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255
74 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # donwcase letters, digits, dashes but not digits only
77 # donwcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 # reserved words
79 # reserved words
80 validates_exclusion_of :identifier, :in => %w( new )
80 validates_exclusion_of :identifier, :in => %w( new )
81
81
82 before_destroy :delete_all_members, :destroy_children
82 before_destroy :delete_all_members, :destroy_children
83
83
84 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
84 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :all_public, { :conditions => { :is_public => true } }
86 named_scope :all_public, { :conditions => { :is_public => true } }
87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
88
88
89 def initialize(attributes = nil)
89 def initialize(attributes = nil)
90 super
90 super
91
91
92 initialized = (attributes || {}).stringify_keys
92 initialized = (attributes || {}).stringify_keys
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
94 self.identifier = Project.next_identifier
94 self.identifier = Project.next_identifier
95 end
95 end
96 if !initialized.key?('is_public')
96 if !initialized.key?('is_public')
97 self.is_public = Setting.default_projects_public?
97 self.is_public = Setting.default_projects_public?
98 end
98 end
99 if !initialized.key?('enabled_module_names')
99 if !initialized.key?('enabled_module_names')
100 self.enabled_module_names = Setting.default_projects_modules
100 self.enabled_module_names = Setting.default_projects_modules
101 end
101 end
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
103 self.trackers = Tracker.all
103 self.trackers = Tracker.all
104 end
104 end
105 end
105 end
106
106
107 def identifier=(identifier)
107 def identifier=(identifier)
108 super unless identifier_frozen?
108 super unless identifier_frozen?
109 end
109 end
110
110
111 def identifier_frozen?
111 def identifier_frozen?
112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
113 end
113 end
114
114
115 # returns latest created projects
115 # returns latest created projects
116 # non public projects will be returned only if user is a member of those
116 # non public projects will be returned only if user is a member of those
117 def self.latest(user=nil, count=5)
117 def self.latest(user=nil, count=5)
118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
119 end
119 end
120
120
121 # Returns a SQL :conditions string used to find all active projects for the specified user.
121 # Returns a SQL :conditions string used to find all active projects for the specified user.
122 #
122 #
123 # Examples:
123 # Examples:
124 # Projects.visible_by(admin) => "projects.status = 1"
124 # Projects.visible_by(admin) => "projects.status = 1"
125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
126 def self.visible_by(user=nil)
126 def self.visible_by(user=nil)
127 user ||= User.current
127 user ||= User.current
128 if user && user.admin?
128 if user && user.admin?
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
130 elsif user && user.memberships.any?
130 elsif user && user.memberships.any?
131 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
131 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
132 else
132 else
133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
134 end
134 end
135 end
135 end
136
136
137 def self.allowed_to_condition(user, permission, options={})
137 def self.allowed_to_condition(user, permission, options={})
138 statements = []
139 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
138 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
140 if perm = Redmine::AccessControl.permission(permission)
139 if perm = Redmine::AccessControl.permission(permission)
141 unless perm.project_module.nil?
140 unless perm.project_module.nil?
142 # If the permission belongs to a project module, make sure the module is enabled
141 # If the permission belongs to a project module, make sure the module is enabled
143 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
142 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
144 end
143 end
145 end
144 end
146 if options[:project]
145 if options[:project]
147 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
146 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
148 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
147 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
149 base_statement = "(#{project_statement}) AND (#{base_statement})"
148 base_statement = "(#{project_statement}) AND (#{base_statement})"
150 end
149 end
150
151 if user.admin?
151 if user.admin?
152 # no restriction
152 base_statement
153 else
153 else
154 statements << "1=0"
154 statement_by_role = {}
155 if user.logged?
155 if user.logged?
156 if Role.non_member.allowed_to?(permission) && !options[:member]
156 if Role.non_member.allowed_to?(permission) && !options[:member]
157 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
157 statement_by_role[Role.non_member] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
158 end
159 user.projects_by_role.each do |role, projects|
160 if role.allowed_to?(permission)
161 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
162 end
158 end
163 end
159 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
160 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
161 else
164 else
162 if Role.anonymous.allowed_to?(permission) && !options[:member]
165 if Role.anonymous.allowed_to?(permission) && !options[:member]
163 # anonymous user allowed on public project
166 statement_by_role[Role.anonymous] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
164 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
165 end
167 end
166 end
168 end
169 if statement_by_role.empty?
170 "1=0"
171 else
172 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
173 end
167 end
174 end
168 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
169 end
175 end
170
176
171 # Returns the Systemwide and project specific activities
177 # Returns the Systemwide and project specific activities
172 def activities(include_inactive=false)
178 def activities(include_inactive=false)
173 if include_inactive
179 if include_inactive
174 return all_activities
180 return all_activities
175 else
181 else
176 return active_activities
182 return active_activities
177 end
183 end
178 end
184 end
179
185
180 # Will create a new Project specific Activity or update an existing one
186 # Will create a new Project specific Activity or update an existing one
181 #
187 #
182 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
188 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
183 # does not successfully save.
189 # does not successfully save.
184 def update_or_create_time_entry_activity(id, activity_hash)
190 def update_or_create_time_entry_activity(id, activity_hash)
185 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
191 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
186 self.create_time_entry_activity_if_needed(activity_hash)
192 self.create_time_entry_activity_if_needed(activity_hash)
187 else
193 else
188 activity = project.time_entry_activities.find_by_id(id.to_i)
194 activity = project.time_entry_activities.find_by_id(id.to_i)
189 activity.update_attributes(activity_hash) if activity
195 activity.update_attributes(activity_hash) if activity
190 end
196 end
191 end
197 end
192
198
193 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
199 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
194 #
200 #
195 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
201 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
196 # does not successfully save.
202 # does not successfully save.
197 def create_time_entry_activity_if_needed(activity)
203 def create_time_entry_activity_if_needed(activity)
198 if activity['parent_id']
204 if activity['parent_id']
199
205
200 parent_activity = TimeEntryActivity.find(activity['parent_id'])
206 parent_activity = TimeEntryActivity.find(activity['parent_id'])
201 activity['name'] = parent_activity.name
207 activity['name'] = parent_activity.name
202 activity['position'] = parent_activity.position
208 activity['position'] = parent_activity.position
203
209
204 if Enumeration.overridding_change?(activity, parent_activity)
210 if Enumeration.overridding_change?(activity, parent_activity)
205 project_activity = self.time_entry_activities.create(activity)
211 project_activity = self.time_entry_activities.create(activity)
206
212
207 if project_activity.new_record?
213 if project_activity.new_record?
208 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
214 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
209 else
215 else
210 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
216 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
211 end
217 end
212 end
218 end
213 end
219 end
214 end
220 end
215
221
216 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
222 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
217 #
223 #
218 # Examples:
224 # Examples:
219 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
225 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
220 # project.project_condition(false) => "projects.id = 1"
226 # project.project_condition(false) => "projects.id = 1"
221 def project_condition(with_subprojects)
227 def project_condition(with_subprojects)
222 cond = "#{Project.table_name}.id = #{id}"
228 cond = "#{Project.table_name}.id = #{id}"
223 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
229 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
224 cond
230 cond
225 end
231 end
226
232
227 def self.find(*args)
233 def self.find(*args)
228 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
234 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
229 project = find_by_identifier(*args)
235 project = find_by_identifier(*args)
230 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
236 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
231 project
237 project
232 else
238 else
233 super
239 super
234 end
240 end
235 end
241 end
236
242
237 def to_param
243 def to_param
238 # id is used for projects with a numeric identifier (compatibility)
244 # id is used for projects with a numeric identifier (compatibility)
239 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
245 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
240 end
246 end
241
247
242 def active?
248 def active?
243 self.status == STATUS_ACTIVE
249 self.status == STATUS_ACTIVE
244 end
250 end
245
251
246 def archived?
252 def archived?
247 self.status == STATUS_ARCHIVED
253 self.status == STATUS_ARCHIVED
248 end
254 end
249
255
250 # Archives the project and its descendants
256 # Archives the project and its descendants
251 def archive
257 def archive
252 # Check that there is no issue of a non descendant project that is assigned
258 # Check that there is no issue of a non descendant project that is assigned
253 # to one of the project or descendant versions
259 # to one of the project or descendant versions
254 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
260 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
255 if v_ids.any? && Issue.find(:first, :include => :project,
261 if v_ids.any? && Issue.find(:first, :include => :project,
256 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
262 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
257 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
263 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
258 return false
264 return false
259 end
265 end
260 Project.transaction do
266 Project.transaction do
261 archive!
267 archive!
262 end
268 end
263 true
269 true
264 end
270 end
265
271
266 # Unarchives the project
272 # Unarchives the project
267 # All its ancestors must be active
273 # All its ancestors must be active
268 def unarchive
274 def unarchive
269 return false if ancestors.detect {|a| !a.active?}
275 return false if ancestors.detect {|a| !a.active?}
270 update_attribute :status, STATUS_ACTIVE
276 update_attribute :status, STATUS_ACTIVE
271 end
277 end
272
278
273 # Returns an array of projects the project can be moved to
279 # Returns an array of projects the project can be moved to
274 # by the current user
280 # by the current user
275 def allowed_parents
281 def allowed_parents
276 return @allowed_parents if @allowed_parents
282 return @allowed_parents if @allowed_parents
277 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
283 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
278 @allowed_parents = @allowed_parents - self_and_descendants
284 @allowed_parents = @allowed_parents - self_and_descendants
279 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
285 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
280 @allowed_parents << nil
286 @allowed_parents << nil
281 end
287 end
282 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
288 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
283 @allowed_parents << parent
289 @allowed_parents << parent
284 end
290 end
285 @allowed_parents
291 @allowed_parents
286 end
292 end
287
293
288 # Sets the parent of the project with authorization check
294 # Sets the parent of the project with authorization check
289 def set_allowed_parent!(p)
295 def set_allowed_parent!(p)
290 unless p.nil? || p.is_a?(Project)
296 unless p.nil? || p.is_a?(Project)
291 if p.to_s.blank?
297 if p.to_s.blank?
292 p = nil
298 p = nil
293 else
299 else
294 p = Project.find_by_id(p)
300 p = Project.find_by_id(p)
295 return false unless p
301 return false unless p
296 end
302 end
297 end
303 end
298 if p.nil?
304 if p.nil?
299 if !new_record? && allowed_parents.empty?
305 if !new_record? && allowed_parents.empty?
300 return false
306 return false
301 end
307 end
302 elsif !allowed_parents.include?(p)
308 elsif !allowed_parents.include?(p)
303 return false
309 return false
304 end
310 end
305 set_parent!(p)
311 set_parent!(p)
306 end
312 end
307
313
308 # Sets the parent of the project
314 # Sets the parent of the project
309 # Argument can be either a Project, a String, a Fixnum or nil
315 # Argument can be either a Project, a String, a Fixnum or nil
310 def set_parent!(p)
316 def set_parent!(p)
311 unless p.nil? || p.is_a?(Project)
317 unless p.nil? || p.is_a?(Project)
312 if p.to_s.blank?
318 if p.to_s.blank?
313 p = nil
319 p = nil
314 else
320 else
315 p = Project.find_by_id(p)
321 p = Project.find_by_id(p)
316 return false unless p
322 return false unless p
317 end
323 end
318 end
324 end
319 if p == parent && !p.nil?
325 if p == parent && !p.nil?
320 # Nothing to do
326 # Nothing to do
321 true
327 true
322 elsif p.nil? || (p.active? && move_possible?(p))
328 elsif p.nil? || (p.active? && move_possible?(p))
323 # Insert the project so that target's children or root projects stay alphabetically sorted
329 # Insert the project so that target's children or root projects stay alphabetically sorted
324 sibs = (p.nil? ? self.class.roots : p.children)
330 sibs = (p.nil? ? self.class.roots : p.children)
325 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
331 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
326 if to_be_inserted_before
332 if to_be_inserted_before
327 move_to_left_of(to_be_inserted_before)
333 move_to_left_of(to_be_inserted_before)
328 elsif p.nil?
334 elsif p.nil?
329 if sibs.empty?
335 if sibs.empty?
330 # move_to_root adds the project in first (ie. left) position
336 # move_to_root adds the project in first (ie. left) position
331 move_to_root
337 move_to_root
332 else
338 else
333 move_to_right_of(sibs.last) unless self == sibs.last
339 move_to_right_of(sibs.last) unless self == sibs.last
334 end
340 end
335 else
341 else
336 # move_to_child_of adds the project in last (ie.right) position
342 # move_to_child_of adds the project in last (ie.right) position
337 move_to_child_of(p)
343 move_to_child_of(p)
338 end
344 end
339 Issue.update_versions_from_hierarchy_change(self)
345 Issue.update_versions_from_hierarchy_change(self)
340 true
346 true
341 else
347 else
342 # Can not move to the given target
348 # Can not move to the given target
343 false
349 false
344 end
350 end
345 end
351 end
346
352
347 # Returns an array of the trackers used by the project and its active sub projects
353 # Returns an array of the trackers used by the project and its active sub projects
348 def rolled_up_trackers
354 def rolled_up_trackers
349 @rolled_up_trackers ||=
355 @rolled_up_trackers ||=
350 Tracker.find(:all, :include => :projects,
356 Tracker.find(:all, :include => :projects,
351 :select => "DISTINCT #{Tracker.table_name}.*",
357 :select => "DISTINCT #{Tracker.table_name}.*",
352 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
358 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
353 :order => "#{Tracker.table_name}.position")
359 :order => "#{Tracker.table_name}.position")
354 end
360 end
355
361
356 # Closes open and locked project versions that are completed
362 # Closes open and locked project versions that are completed
357 def close_completed_versions
363 def close_completed_versions
358 Version.transaction do
364 Version.transaction do
359 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
365 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
360 if version.completed?
366 if version.completed?
361 version.update_attribute(:status, 'closed')
367 version.update_attribute(:status, 'closed')
362 end
368 end
363 end
369 end
364 end
370 end
365 end
371 end
366
372
367 # Returns a scope of the Versions on subprojects
373 # Returns a scope of the Versions on subprojects
368 def rolled_up_versions
374 def rolled_up_versions
369 @rolled_up_versions ||=
375 @rolled_up_versions ||=
370 Version.scoped(:include => :project,
376 Version.scoped(:include => :project,
371 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
377 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
372 end
378 end
373
379
374 # Returns a scope of the Versions used by the project
380 # Returns a scope of the Versions used by the project
375 def shared_versions
381 def shared_versions
376 @shared_versions ||=
382 @shared_versions ||=
377 Version.scoped(:include => :project,
383 Version.scoped(:include => :project,
378 :conditions => "#{Project.table_name}.id = #{id}" +
384 :conditions => "#{Project.table_name}.id = #{id}" +
379 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
385 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
380 " #{Version.table_name}.sharing = 'system'" +
386 " #{Version.table_name}.sharing = 'system'" +
381 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
387 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
382 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
388 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
383 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
389 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
384 "))")
390 "))")
385 end
391 end
386
392
387 # Returns a hash of project users grouped by role
393 # Returns a hash of project users grouped by role
388 def users_by_role
394 def users_by_role
389 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
395 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
390 m.roles.each do |r|
396 m.roles.each do |r|
391 h[r] ||= []
397 h[r] ||= []
392 h[r] << m.user
398 h[r] << m.user
393 end
399 end
394 h
400 h
395 end
401 end
396 end
402 end
397
403
398 # Deletes all project's members
404 # Deletes all project's members
399 def delete_all_members
405 def delete_all_members
400 me, mr = Member.table_name, MemberRole.table_name
406 me, mr = Member.table_name, MemberRole.table_name
401 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
407 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
402 Member.delete_all(['project_id = ?', id])
408 Member.delete_all(['project_id = ?', id])
403 end
409 end
404
410
405 # Users issues can be assigned to
411 # Users issues can be assigned to
406 def assignable_users
412 def assignable_users
407 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
413 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
408 end
414 end
409
415
410 # Returns the mail adresses of users that should be always notified on project events
416 # Returns the mail adresses of users that should be always notified on project events
411 def recipients
417 def recipients
412 notified_users.collect {|user| user.mail}
418 notified_users.collect {|user| user.mail}
413 end
419 end
414
420
415 # Returns the users that should be notified on project events
421 # Returns the users that should be notified on project events
416 def notified_users
422 def notified_users
417 # TODO: User part should be extracted to User#notify_about?
423 # TODO: User part should be extracted to User#notify_about?
418 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
424 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
419 end
425 end
420
426
421 # Returns an array of all custom fields enabled for project issues
427 # Returns an array of all custom fields enabled for project issues
422 # (explictly associated custom fields and custom fields enabled for all projects)
428 # (explictly associated custom fields and custom fields enabled for all projects)
423 def all_issue_custom_fields
429 def all_issue_custom_fields
424 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
430 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
425 end
431 end
426
432
427 def project
433 def project
428 self
434 self
429 end
435 end
430
436
431 def <=>(project)
437 def <=>(project)
432 name.downcase <=> project.name.downcase
438 name.downcase <=> project.name.downcase
433 end
439 end
434
440
435 def to_s
441 def to_s
436 name
442 name
437 end
443 end
438
444
439 # Returns a short description of the projects (first lines)
445 # Returns a short description of the projects (first lines)
440 def short_description(length = 255)
446 def short_description(length = 255)
441 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
447 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
442 end
448 end
443
449
444 def css_classes
450 def css_classes
445 s = 'project'
451 s = 'project'
446 s << ' root' if root?
452 s << ' root' if root?
447 s << ' child' if child?
453 s << ' child' if child?
448 s << (leaf? ? ' leaf' : ' parent')
454 s << (leaf? ? ' leaf' : ' parent')
449 s
455 s
450 end
456 end
451
457
452 # The earliest start date of a project, based on it's issues and versions
458 # The earliest start date of a project, based on it's issues and versions
453 def start_date
459 def start_date
454 [
460 [
455 issues.minimum('start_date'),
461 issues.minimum('start_date'),
456 shared_versions.collect(&:effective_date),
462 shared_versions.collect(&:effective_date),
457 shared_versions.collect(&:start_date)
463 shared_versions.collect(&:start_date)
458 ].flatten.compact.min
464 ].flatten.compact.min
459 end
465 end
460
466
461 # The latest due date of an issue or version
467 # The latest due date of an issue or version
462 def due_date
468 def due_date
463 [
469 [
464 issues.maximum('due_date'),
470 issues.maximum('due_date'),
465 shared_versions.collect(&:effective_date),
471 shared_versions.collect(&:effective_date),
466 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
472 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
467 ].flatten.compact.max
473 ].flatten.compact.max
468 end
474 end
469
475
470 def overdue?
476 def overdue?
471 active? && !due_date.nil? && (due_date < Date.today)
477 active? && !due_date.nil? && (due_date < Date.today)
472 end
478 end
473
479
474 # Returns the percent completed for this project, based on the
480 # Returns the percent completed for this project, based on the
475 # progress on it's versions.
481 # progress on it's versions.
476 def completed_percent(options={:include_subprojects => false})
482 def completed_percent(options={:include_subprojects => false})
477 if options.delete(:include_subprojects)
483 if options.delete(:include_subprojects)
478 total = self_and_descendants.collect(&:completed_percent).sum
484 total = self_and_descendants.collect(&:completed_percent).sum
479
485
480 total / self_and_descendants.count
486 total / self_and_descendants.count
481 else
487 else
482 if versions.count > 0
488 if versions.count > 0
483 total = versions.collect(&:completed_pourcent).sum
489 total = versions.collect(&:completed_pourcent).sum
484
490
485 total / versions.count
491 total / versions.count
486 else
492 else
487 100
493 100
488 end
494 end
489 end
495 end
490 end
496 end
491
497
492 # Return true if this project is allowed to do the specified action.
498 # Return true if this project is allowed to do the specified action.
493 # action can be:
499 # action can be:
494 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
500 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
495 # * a permission Symbol (eg. :edit_project)
501 # * a permission Symbol (eg. :edit_project)
496 def allows_to?(action)
502 def allows_to?(action)
497 if action.is_a? Hash
503 if action.is_a? Hash
498 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
504 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
499 else
505 else
500 allowed_permissions.include? action
506 allowed_permissions.include? action
501 end
507 end
502 end
508 end
503
509
504 def module_enabled?(module_name)
510 def module_enabled?(module_name)
505 module_name = module_name.to_s
511 module_name = module_name.to_s
506 enabled_modules.detect {|m| m.name == module_name}
512 enabled_modules.detect {|m| m.name == module_name}
507 end
513 end
508
514
509 def enabled_module_names=(module_names)
515 def enabled_module_names=(module_names)
510 if module_names && module_names.is_a?(Array)
516 if module_names && module_names.is_a?(Array)
511 module_names = module_names.collect(&:to_s).reject(&:blank?)
517 module_names = module_names.collect(&:to_s).reject(&:blank?)
512 # remove disabled modules
518 # remove disabled modules
513 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
519 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
514 # add new modules
520 # add new modules
515 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
521 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
516 else
522 else
517 enabled_modules.clear
523 enabled_modules.clear
518 end
524 end
519 end
525 end
520
526
521 # Returns an array of the enabled modules names
527 # Returns an array of the enabled modules names
522 def enabled_module_names
528 def enabled_module_names
523 enabled_modules.collect(&:name)
529 enabled_modules.collect(&:name)
524 end
530 end
525
531
526 safe_attributes 'name',
532 safe_attributes 'name',
527 'description',
533 'description',
528 'homepage',
534 'homepage',
529 'is_public',
535 'is_public',
530 'identifier',
536 'identifier',
531 'custom_field_values',
537 'custom_field_values',
532 'custom_fields',
538 'custom_fields',
533 'tracker_ids',
539 'tracker_ids',
534 'issue_custom_field_ids'
540 'issue_custom_field_ids'
535
541
536 safe_attributes 'enabled_module_names',
542 safe_attributes 'enabled_module_names',
537 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
543 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
538
544
539 # Returns an array of projects that are in this project's hierarchy
545 # Returns an array of projects that are in this project's hierarchy
540 #
546 #
541 # Example: parents, children, siblings
547 # Example: parents, children, siblings
542 def hierarchy
548 def hierarchy
543 parents = project.self_and_ancestors || []
549 parents = project.self_and_ancestors || []
544 descendants = project.descendants || []
550 descendants = project.descendants || []
545 project_hierarchy = parents | descendants # Set union
551 project_hierarchy = parents | descendants # Set union
546 end
552 end
547
553
548 # Returns an auto-generated project identifier based on the last identifier used
554 # Returns an auto-generated project identifier based on the last identifier used
549 def self.next_identifier
555 def self.next_identifier
550 p = Project.find(:first, :order => 'created_on DESC')
556 p = Project.find(:first, :order => 'created_on DESC')
551 p.nil? ? nil : p.identifier.to_s.succ
557 p.nil? ? nil : p.identifier.to_s.succ
552 end
558 end
553
559
554 # Copies and saves the Project instance based on the +project+.
560 # Copies and saves the Project instance based on the +project+.
555 # Duplicates the source project's:
561 # Duplicates the source project's:
556 # * Wiki
562 # * Wiki
557 # * Versions
563 # * Versions
558 # * Categories
564 # * Categories
559 # * Issues
565 # * Issues
560 # * Members
566 # * Members
561 # * Queries
567 # * Queries
562 #
568 #
563 # Accepts an +options+ argument to specify what to copy
569 # Accepts an +options+ argument to specify what to copy
564 #
570 #
565 # Examples:
571 # Examples:
566 # project.copy(1) # => copies everything
572 # project.copy(1) # => copies everything
567 # project.copy(1, :only => 'members') # => copies members only
573 # project.copy(1, :only => 'members') # => copies members only
568 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
574 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
569 def copy(project, options={})
575 def copy(project, options={})
570 project = project.is_a?(Project) ? project : Project.find(project)
576 project = project.is_a?(Project) ? project : Project.find(project)
571
577
572 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
578 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
573 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
579 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
574
580
575 Project.transaction do
581 Project.transaction do
576 if save
582 if save
577 reload
583 reload
578 to_be_copied.each do |name|
584 to_be_copied.each do |name|
579 send "copy_#{name}", project
585 send "copy_#{name}", project
580 end
586 end
581 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
587 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
582 save
588 save
583 end
589 end
584 end
590 end
585 end
591 end
586
592
587
593
588 # Copies +project+ and returns the new instance. This will not save
594 # Copies +project+ and returns the new instance. This will not save
589 # the copy
595 # the copy
590 def self.copy_from(project)
596 def self.copy_from(project)
591 begin
597 begin
592 project = project.is_a?(Project) ? project : Project.find(project)
598 project = project.is_a?(Project) ? project : Project.find(project)
593 if project
599 if project
594 # clear unique attributes
600 # clear unique attributes
595 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
601 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
596 copy = Project.new(attributes)
602 copy = Project.new(attributes)
597 copy.enabled_modules = project.enabled_modules
603 copy.enabled_modules = project.enabled_modules
598 copy.trackers = project.trackers
604 copy.trackers = project.trackers
599 copy.custom_values = project.custom_values.collect {|v| v.clone}
605 copy.custom_values = project.custom_values.collect {|v| v.clone}
600 copy.issue_custom_fields = project.issue_custom_fields
606 copy.issue_custom_fields = project.issue_custom_fields
601 return copy
607 return copy
602 else
608 else
603 return nil
609 return nil
604 end
610 end
605 rescue ActiveRecord::RecordNotFound
611 rescue ActiveRecord::RecordNotFound
606 return nil
612 return nil
607 end
613 end
608 end
614 end
609
615
610 # Yields the given block for each project with its level in the tree
616 # Yields the given block for each project with its level in the tree
611 def self.project_tree(projects, &block)
617 def self.project_tree(projects, &block)
612 ancestors = []
618 ancestors = []
613 projects.sort_by(&:lft).each do |project|
619 projects.sort_by(&:lft).each do |project|
614 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
620 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
615 ancestors.pop
621 ancestors.pop
616 end
622 end
617 yield project, ancestors.size
623 yield project, ancestors.size
618 ancestors << project
624 ancestors << project
619 end
625 end
620 end
626 end
621
627
622 private
628 private
623
629
624 # Destroys children before destroying self
630 # Destroys children before destroying self
625 def destroy_children
631 def destroy_children
626 children.each do |child|
632 children.each do |child|
627 child.destroy
633 child.destroy
628 end
634 end
629 end
635 end
630
636
631 # Copies wiki from +project+
637 # Copies wiki from +project+
632 def copy_wiki(project)
638 def copy_wiki(project)
633 # Check that the source project has a wiki first
639 # Check that the source project has a wiki first
634 unless project.wiki.nil?
640 unless project.wiki.nil?
635 self.wiki ||= Wiki.new
641 self.wiki ||= Wiki.new
636 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
642 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
637 wiki_pages_map = {}
643 wiki_pages_map = {}
638 project.wiki.pages.each do |page|
644 project.wiki.pages.each do |page|
639 # Skip pages without content
645 # Skip pages without content
640 next if page.content.nil?
646 next if page.content.nil?
641 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
647 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
642 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
648 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
643 new_wiki_page.content = new_wiki_content
649 new_wiki_page.content = new_wiki_content
644 wiki.pages << new_wiki_page
650 wiki.pages << new_wiki_page
645 wiki_pages_map[page.id] = new_wiki_page
651 wiki_pages_map[page.id] = new_wiki_page
646 end
652 end
647 wiki.save
653 wiki.save
648 # Reproduce page hierarchy
654 # Reproduce page hierarchy
649 project.wiki.pages.each do |page|
655 project.wiki.pages.each do |page|
650 if page.parent_id && wiki_pages_map[page.id]
656 if page.parent_id && wiki_pages_map[page.id]
651 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
657 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
652 wiki_pages_map[page.id].save
658 wiki_pages_map[page.id].save
653 end
659 end
654 end
660 end
655 end
661 end
656 end
662 end
657
663
658 # Copies versions from +project+
664 # Copies versions from +project+
659 def copy_versions(project)
665 def copy_versions(project)
660 project.versions.each do |version|
666 project.versions.each do |version|
661 new_version = Version.new
667 new_version = Version.new
662 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
668 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
663 self.versions << new_version
669 self.versions << new_version
664 end
670 end
665 end
671 end
666
672
667 # Copies issue categories from +project+
673 # Copies issue categories from +project+
668 def copy_issue_categories(project)
674 def copy_issue_categories(project)
669 project.issue_categories.each do |issue_category|
675 project.issue_categories.each do |issue_category|
670 new_issue_category = IssueCategory.new
676 new_issue_category = IssueCategory.new
671 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
677 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
672 self.issue_categories << new_issue_category
678 self.issue_categories << new_issue_category
673 end
679 end
674 end
680 end
675
681
676 # Copies issues from +project+
682 # Copies issues from +project+
677 def copy_issues(project)
683 def copy_issues(project)
678 # Stores the source issue id as a key and the copied issues as the
684 # Stores the source issue id as a key and the copied issues as the
679 # value. Used to map the two togeather for issue relations.
685 # value. Used to map the two togeather for issue relations.
680 issues_map = {}
686 issues_map = {}
681
687
682 # Get issues sorted by root_id, lft so that parent issues
688 # Get issues sorted by root_id, lft so that parent issues
683 # get copied before their children
689 # get copied before their children
684 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
690 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
685 new_issue = Issue.new
691 new_issue = Issue.new
686 new_issue.copy_from(issue)
692 new_issue.copy_from(issue)
687 new_issue.project = self
693 new_issue.project = self
688 # Reassign fixed_versions by name, since names are unique per
694 # Reassign fixed_versions by name, since names are unique per
689 # project and the versions for self are not yet saved
695 # project and the versions for self are not yet saved
690 if issue.fixed_version
696 if issue.fixed_version
691 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
697 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
692 end
698 end
693 # Reassign the category by name, since names are unique per
699 # Reassign the category by name, since names are unique per
694 # project and the categories for self are not yet saved
700 # project and the categories for self are not yet saved
695 if issue.category
701 if issue.category
696 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
702 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
697 end
703 end
698 # Parent issue
704 # Parent issue
699 if issue.parent_id
705 if issue.parent_id
700 if copied_parent = issues_map[issue.parent_id]
706 if copied_parent = issues_map[issue.parent_id]
701 new_issue.parent_issue_id = copied_parent.id
707 new_issue.parent_issue_id = copied_parent.id
702 end
708 end
703 end
709 end
704
710
705 self.issues << new_issue
711 self.issues << new_issue
706 if new_issue.new_record?
712 if new_issue.new_record?
707 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
713 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
708 else
714 else
709 issues_map[issue.id] = new_issue unless new_issue.new_record?
715 issues_map[issue.id] = new_issue unless new_issue.new_record?
710 end
716 end
711 end
717 end
712
718
713 # Relations after in case issues related each other
719 # Relations after in case issues related each other
714 project.issues.each do |issue|
720 project.issues.each do |issue|
715 new_issue = issues_map[issue.id]
721 new_issue = issues_map[issue.id]
716 unless new_issue
722 unless new_issue
717 # Issue was not copied
723 # Issue was not copied
718 next
724 next
719 end
725 end
720
726
721 # Relations
727 # Relations
722 issue.relations_from.each do |source_relation|
728 issue.relations_from.each do |source_relation|
723 new_issue_relation = IssueRelation.new
729 new_issue_relation = IssueRelation.new
724 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
730 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
725 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
731 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
726 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
732 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
727 new_issue_relation.issue_to = source_relation.issue_to
733 new_issue_relation.issue_to = source_relation.issue_to
728 end
734 end
729 new_issue.relations_from << new_issue_relation
735 new_issue.relations_from << new_issue_relation
730 end
736 end
731
737
732 issue.relations_to.each do |source_relation|
738 issue.relations_to.each do |source_relation|
733 new_issue_relation = IssueRelation.new
739 new_issue_relation = IssueRelation.new
734 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
740 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
735 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
741 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
736 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
742 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
737 new_issue_relation.issue_from = source_relation.issue_from
743 new_issue_relation.issue_from = source_relation.issue_from
738 end
744 end
739 new_issue.relations_to << new_issue_relation
745 new_issue.relations_to << new_issue_relation
740 end
746 end
741 end
747 end
742 end
748 end
743
749
744 # Copies members from +project+
750 # Copies members from +project+
745 def copy_members(project)
751 def copy_members(project)
746 # Copy users first, then groups to handle members with inherited and given roles
752 # Copy users first, then groups to handle members with inherited and given roles
747 members_to_copy = []
753 members_to_copy = []
748 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
754 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
749 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
755 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
750
756
751 members_to_copy.each do |member|
757 members_to_copy.each do |member|
752 new_member = Member.new
758 new_member = Member.new
753 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
759 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
754 # only copy non inherited roles
760 # only copy non inherited roles
755 # inherited roles will be added when copying the group membership
761 # inherited roles will be added when copying the group membership
756 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
762 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
757 next if role_ids.empty?
763 next if role_ids.empty?
758 new_member.role_ids = role_ids
764 new_member.role_ids = role_ids
759 new_member.project = self
765 new_member.project = self
760 self.members << new_member
766 self.members << new_member
761 end
767 end
762 end
768 end
763
769
764 # Copies queries from +project+
770 # Copies queries from +project+
765 def copy_queries(project)
771 def copy_queries(project)
766 project.queries.each do |query|
772 project.queries.each do |query|
767 new_query = Query.new
773 new_query = Query.new
768 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
774 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
769 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
775 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
770 new_query.project = self
776 new_query.project = self
771 self.queries << new_query
777 self.queries << new_query
772 end
778 end
773 end
779 end
774
780
775 # Copies boards from +project+
781 # Copies boards from +project+
776 def copy_boards(project)
782 def copy_boards(project)
777 project.boards.each do |board|
783 project.boards.each do |board|
778 new_board = Board.new
784 new_board = Board.new
779 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
785 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
780 new_board.project = self
786 new_board.project = self
781 self.boards << new_board
787 self.boards << new_board
782 end
788 end
783 end
789 end
784
790
785 def allowed_permissions
791 def allowed_permissions
786 @allowed_permissions ||= begin
792 @allowed_permissions ||= begin
787 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
793 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
788 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
794 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
789 end
795 end
790 end
796 end
791
797
792 def allowed_actions
798 def allowed_actions
793 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
799 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
794 end
800 end
795
801
796 # Returns all the active Systemwide and project specific activities
802 # Returns all the active Systemwide and project specific activities
797 def active_activities
803 def active_activities
798 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
804 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
799
805
800 if overridden_activity_ids.empty?
806 if overridden_activity_ids.empty?
801 return TimeEntryActivity.shared.active
807 return TimeEntryActivity.shared.active
802 else
808 else
803 return system_activities_and_project_overrides
809 return system_activities_and_project_overrides
804 end
810 end
805 end
811 end
806
812
807 # Returns all the Systemwide and project specific activities
813 # Returns all the Systemwide and project specific activities
808 # (inactive and active)
814 # (inactive and active)
809 def all_activities
815 def all_activities
810 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
816 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
811
817
812 if overridden_activity_ids.empty?
818 if overridden_activity_ids.empty?
813 return TimeEntryActivity.shared
819 return TimeEntryActivity.shared
814 else
820 else
815 return system_activities_and_project_overrides(true)
821 return system_activities_and_project_overrides(true)
816 end
822 end
817 end
823 end
818
824
819 # Returns the systemwide active activities merged with the project specific overrides
825 # Returns the systemwide active activities merged with the project specific overrides
820 def system_activities_and_project_overrides(include_inactive=false)
826 def system_activities_and_project_overrides(include_inactive=false)
821 if include_inactive
827 if include_inactive
822 return TimeEntryActivity.shared.
828 return TimeEntryActivity.shared.
823 find(:all,
829 find(:all,
824 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
830 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
825 self.time_entry_activities
831 self.time_entry_activities
826 else
832 else
827 return TimeEntryActivity.shared.active.
833 return TimeEntryActivity.shared.active.
828 find(:all,
834 find(:all,
829 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
835 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
830 self.time_entry_activities.active
836 self.time_entry_activities.active
831 end
837 end
832 end
838 end
833
839
834 # Archives subprojects recursively
840 # Archives subprojects recursively
835 def archive!
841 def archive!
836 children.each do |subproject|
842 children.each do |subproject|
837 subproject.send :archive!
843 subproject.send :archive!
838 end
844 end
839 update_attribute :status, STATUS_ARCHIVED
845 update_attribute :status, STATUS_ARCHIVED
840 end
846 end
841 end
847 end
@@ -1,572 +1,590
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Account statuses
23 # Account statuses
24 STATUS_ANONYMOUS = 0
24 STATUS_ANONYMOUS = 0
25 STATUS_ACTIVE = 1
25 STATUS_ACTIVE = 1
26 STATUS_REGISTERED = 2
26 STATUS_REGISTERED = 2
27 STATUS_LOCKED = 3
27 STATUS_LOCKED = 3
28
28
29 USER_FORMATS = {
29 USER_FORMATS = {
30 :firstname_lastname => '#{firstname} #{lastname}',
30 :firstname_lastname => '#{firstname} #{lastname}',
31 :firstname => '#{firstname}',
31 :firstname => '#{firstname}',
32 :lastname_firstname => '#{lastname} #{firstname}',
32 :lastname_firstname => '#{lastname} #{firstname}',
33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
34 :username => '#{login}'
34 :username => '#{login}'
35 }
35 }
36
36
37 MAIL_NOTIFICATION_OPTIONS = [
37 MAIL_NOTIFICATION_OPTIONS = [
38 ['all', :label_user_mail_option_all],
38 ['all', :label_user_mail_option_all],
39 ['selected', :label_user_mail_option_selected],
39 ['selected', :label_user_mail_option_selected],
40 ['only_my_events', :label_user_mail_option_only_my_events],
40 ['only_my_events', :label_user_mail_option_only_my_events],
41 ['only_assigned', :label_user_mail_option_only_assigned],
41 ['only_assigned', :label_user_mail_option_only_assigned],
42 ['only_owner', :label_user_mail_option_only_owner],
42 ['only_owner', :label_user_mail_option_only_owner],
43 ['none', :label_user_mail_option_none]
43 ['none', :label_user_mail_option_none]
44 ]
44 ]
45
45
46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
47 :after_remove => Proc.new {|user, group| group.user_removed(user)}
47 :after_remove => Proc.new {|user, group| group.user_removed(user)}
48 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
48 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
49 has_many :changesets, :dependent => :nullify
49 has_many :changesets, :dependent => :nullify
50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
53 belongs_to :auth_source
53 belongs_to :auth_source
54
54
55 # Active non-anonymous users scope
55 # Active non-anonymous users scope
56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
57
57
58 acts_as_customizable
58 acts_as_customizable
59
59
60 attr_accessor :password, :password_confirmation
60 attr_accessor :password, :password_confirmation
61 attr_accessor :last_before_login_on
61 attr_accessor :last_before_login_on
62 # Prevents unauthorized assignments
62 # Prevents unauthorized assignments
63 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
63 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
64
64
65 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
65 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
66 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
66 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
67 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
67 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
68 # Login must contain lettres, numbers, underscores only
68 # Login must contain lettres, numbers, underscores only
69 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
69 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
70 validates_length_of :login, :maximum => 30
70 validates_length_of :login, :maximum => 30
71 validates_length_of :firstname, :lastname, :maximum => 30
71 validates_length_of :firstname, :lastname, :maximum => 30
72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
73 validates_length_of :mail, :maximum => 60, :allow_nil => true
73 validates_length_of :mail, :maximum => 60, :allow_nil => true
74 validates_confirmation_of :password, :allow_nil => true
74 validates_confirmation_of :password, :allow_nil => true
75 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
75 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
76
76
77 before_destroy :remove_references_before_destroy
77 before_destroy :remove_references_before_destroy
78
78
79 def before_create
79 def before_create
80 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
80 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
81 true
81 true
82 end
82 end
83
83
84 def before_save
84 def before_save
85 # update hashed_password if password was set
85 # update hashed_password if password was set
86 if self.password && self.auth_source_id.blank?
86 if self.password && self.auth_source_id.blank?
87 salt_password(password)
87 salt_password(password)
88 end
88 end
89 end
89 end
90
90
91 def reload(*args)
91 def reload(*args)
92 @name = nil
92 @name = nil
93 @projects_by_role = nil
93 super
94 super
94 end
95 end
95
96
96 def mail=(arg)
97 def mail=(arg)
97 write_attribute(:mail, arg.to_s.strip)
98 write_attribute(:mail, arg.to_s.strip)
98 end
99 end
99
100
100 def identity_url=(url)
101 def identity_url=(url)
101 if url.blank?
102 if url.blank?
102 write_attribute(:identity_url, '')
103 write_attribute(:identity_url, '')
103 else
104 else
104 begin
105 begin
105 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
106 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
106 rescue OpenIdAuthentication::InvalidOpenId
107 rescue OpenIdAuthentication::InvalidOpenId
107 # Invlaid url, don't save
108 # Invlaid url, don't save
108 end
109 end
109 end
110 end
110 self.read_attribute(:identity_url)
111 self.read_attribute(:identity_url)
111 end
112 end
112
113
113 # Returns the user that matches provided login and password, or nil
114 # Returns the user that matches provided login and password, or nil
114 def self.try_to_login(login, password)
115 def self.try_to_login(login, password)
115 # Make sure no one can sign in with an empty password
116 # Make sure no one can sign in with an empty password
116 return nil if password.to_s.empty?
117 return nil if password.to_s.empty?
117 user = find_by_login(login)
118 user = find_by_login(login)
118 if user
119 if user
119 # user is already in local database
120 # user is already in local database
120 return nil if !user.active?
121 return nil if !user.active?
121 if user.auth_source
122 if user.auth_source
122 # user has an external authentication method
123 # user has an external authentication method
123 return nil unless user.auth_source.authenticate(login, password)
124 return nil unless user.auth_source.authenticate(login, password)
124 else
125 else
125 # authentication with local password
126 # authentication with local password
126 return nil unless user.check_password?(password)
127 return nil unless user.check_password?(password)
127 end
128 end
128 else
129 else
129 # user is not yet registered, try to authenticate with available sources
130 # user is not yet registered, try to authenticate with available sources
130 attrs = AuthSource.authenticate(login, password)
131 attrs = AuthSource.authenticate(login, password)
131 if attrs
132 if attrs
132 user = new(attrs)
133 user = new(attrs)
133 user.login = login
134 user.login = login
134 user.language = Setting.default_language
135 user.language = Setting.default_language
135 if user.save
136 if user.save
136 user.reload
137 user.reload
137 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
138 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
138 end
139 end
139 end
140 end
140 end
141 end
141 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
142 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
142 user
143 user
143 rescue => text
144 rescue => text
144 raise text
145 raise text
145 end
146 end
146
147
147 # Returns the user who matches the given autologin +key+ or nil
148 # Returns the user who matches the given autologin +key+ or nil
148 def self.try_to_autologin(key)
149 def self.try_to_autologin(key)
149 tokens = Token.find_all_by_action_and_value('autologin', key)
150 tokens = Token.find_all_by_action_and_value('autologin', key)
150 # Make sure there's only 1 token that matches the key
151 # Make sure there's only 1 token that matches the key
151 if tokens.size == 1
152 if tokens.size == 1
152 token = tokens.first
153 token = tokens.first
153 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
154 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
154 token.user.update_attribute(:last_login_on, Time.now)
155 token.user.update_attribute(:last_login_on, Time.now)
155 token.user
156 token.user
156 end
157 end
157 end
158 end
158 end
159 end
159
160
160 # Return user's full name for display
161 # Return user's full name for display
161 def name(formatter = nil)
162 def name(formatter = nil)
162 if formatter
163 if formatter
163 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
164 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
164 else
165 else
165 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
166 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
166 end
167 end
167 end
168 end
168
169
169 def active?
170 def active?
170 self.status == STATUS_ACTIVE
171 self.status == STATUS_ACTIVE
171 end
172 end
172
173
173 def registered?
174 def registered?
174 self.status == STATUS_REGISTERED
175 self.status == STATUS_REGISTERED
175 end
176 end
176
177
177 def locked?
178 def locked?
178 self.status == STATUS_LOCKED
179 self.status == STATUS_LOCKED
179 end
180 end
180
181
181 def activate
182 def activate
182 self.status = STATUS_ACTIVE
183 self.status = STATUS_ACTIVE
183 end
184 end
184
185
185 def register
186 def register
186 self.status = STATUS_REGISTERED
187 self.status = STATUS_REGISTERED
187 end
188 end
188
189
189 def lock
190 def lock
190 self.status = STATUS_LOCKED
191 self.status = STATUS_LOCKED
191 end
192 end
192
193
193 def activate!
194 def activate!
194 update_attribute(:status, STATUS_ACTIVE)
195 update_attribute(:status, STATUS_ACTIVE)
195 end
196 end
196
197
197 def register!
198 def register!
198 update_attribute(:status, STATUS_REGISTERED)
199 update_attribute(:status, STATUS_REGISTERED)
199 end
200 end
200
201
201 def lock!
202 def lock!
202 update_attribute(:status, STATUS_LOCKED)
203 update_attribute(:status, STATUS_LOCKED)
203 end
204 end
204
205
205 # Returns true if +clear_password+ is the correct user's password, otherwise false
206 # Returns true if +clear_password+ is the correct user's password, otherwise false
206 def check_password?(clear_password)
207 def check_password?(clear_password)
207 if auth_source_id.present?
208 if auth_source_id.present?
208 auth_source.authenticate(self.login, clear_password)
209 auth_source.authenticate(self.login, clear_password)
209 else
210 else
210 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
211 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
211 end
212 end
212 end
213 end
213
214
214 # Generates a random salt and computes hashed_password for +clear_password+
215 # Generates a random salt and computes hashed_password for +clear_password+
215 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
216 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
216 def salt_password(clear_password)
217 def salt_password(clear_password)
217 self.salt = User.generate_salt
218 self.salt = User.generate_salt
218 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
219 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
219 end
220 end
220
221
221 # Does the backend storage allow this user to change their password?
222 # Does the backend storage allow this user to change their password?
222 def change_password_allowed?
223 def change_password_allowed?
223 return true if auth_source_id.blank?
224 return true if auth_source_id.blank?
224 return auth_source.allow_password_changes?
225 return auth_source.allow_password_changes?
225 end
226 end
226
227
227 # Generate and set a random password. Useful for automated user creation
228 # Generate and set a random password. Useful for automated user creation
228 # Based on Token#generate_token_value
229 # Based on Token#generate_token_value
229 #
230 #
230 def random_password
231 def random_password
231 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
232 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
232 password = ''
233 password = ''
233 40.times { |i| password << chars[rand(chars.size-1)] }
234 40.times { |i| password << chars[rand(chars.size-1)] }
234 self.password = password
235 self.password = password
235 self.password_confirmation = password
236 self.password_confirmation = password
236 self
237 self
237 end
238 end
238
239
239 def pref
240 def pref
240 self.preference ||= UserPreference.new(:user => self)
241 self.preference ||= UserPreference.new(:user => self)
241 end
242 end
242
243
243 def time_zone
244 def time_zone
244 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
245 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
245 end
246 end
246
247
247 def wants_comments_in_reverse_order?
248 def wants_comments_in_reverse_order?
248 self.pref[:comments_sorting] == 'desc'
249 self.pref[:comments_sorting] == 'desc'
249 end
250 end
250
251
251 # Return user's RSS key (a 40 chars long string), used to access feeds
252 # Return user's RSS key (a 40 chars long string), used to access feeds
252 def rss_key
253 def rss_key
253 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
254 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
254 token.value
255 token.value
255 end
256 end
256
257
257 # Return user's API key (a 40 chars long string), used to access the API
258 # Return user's API key (a 40 chars long string), used to access the API
258 def api_key
259 def api_key
259 token = self.api_token || self.create_api_token(:action => 'api')
260 token = self.api_token || self.create_api_token(:action => 'api')
260 token.value
261 token.value
261 end
262 end
262
263
263 # Return an array of project ids for which the user has explicitly turned mail notifications on
264 # Return an array of project ids for which the user has explicitly turned mail notifications on
264 def notified_projects_ids
265 def notified_projects_ids
265 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
266 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
266 end
267 end
267
268
268 def notified_project_ids=(ids)
269 def notified_project_ids=(ids)
269 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
270 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
270 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
271 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
271 @notified_projects_ids = nil
272 @notified_projects_ids = nil
272 notified_projects_ids
273 notified_projects_ids
273 end
274 end
274
275
275 def valid_notification_options
276 def valid_notification_options
276 self.class.valid_notification_options(self)
277 self.class.valid_notification_options(self)
277 end
278 end
278
279
279 # Only users that belong to more than 1 project can select projects for which they are notified
280 # Only users that belong to more than 1 project can select projects for which they are notified
280 def self.valid_notification_options(user=nil)
281 def self.valid_notification_options(user=nil)
281 # Note that @user.membership.size would fail since AR ignores
282 # Note that @user.membership.size would fail since AR ignores
282 # :include association option when doing a count
283 # :include association option when doing a count
283 if user.nil? || user.memberships.length < 1
284 if user.nil? || user.memberships.length < 1
284 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
285 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
285 else
286 else
286 MAIL_NOTIFICATION_OPTIONS
287 MAIL_NOTIFICATION_OPTIONS
287 end
288 end
288 end
289 end
289
290
290 # Find a user account by matching the exact login and then a case-insensitive
291 # Find a user account by matching the exact login and then a case-insensitive
291 # version. Exact matches will be given priority.
292 # version. Exact matches will be given priority.
292 def self.find_by_login(login)
293 def self.find_by_login(login)
293 # force string comparison to be case sensitive on MySQL
294 # force string comparison to be case sensitive on MySQL
294 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
295 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
295
296
296 # First look for an exact match
297 # First look for an exact match
297 user = first(:conditions => ["#{type_cast} login = ?", login])
298 user = first(:conditions => ["#{type_cast} login = ?", login])
298 # Fail over to case-insensitive if none was found
299 # Fail over to case-insensitive if none was found
299 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
300 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
300 end
301 end
301
302
302 def self.find_by_rss_key(key)
303 def self.find_by_rss_key(key)
303 token = Token.find_by_value(key)
304 token = Token.find_by_value(key)
304 token && token.user.active? ? token.user : nil
305 token && token.user.active? ? token.user : nil
305 end
306 end
306
307
307 def self.find_by_api_key(key)
308 def self.find_by_api_key(key)
308 token = Token.find_by_action_and_value('api', key)
309 token = Token.find_by_action_and_value('api', key)
309 token && token.user.active? ? token.user : nil
310 token && token.user.active? ? token.user : nil
310 end
311 end
311
312
312 # Makes find_by_mail case-insensitive
313 # Makes find_by_mail case-insensitive
313 def self.find_by_mail(mail)
314 def self.find_by_mail(mail)
314 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
315 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
315 end
316 end
316
317
317 def to_s
318 def to_s
318 name
319 name
319 end
320 end
320
321
321 # Returns the current day according to user's time zone
322 # Returns the current day according to user's time zone
322 def today
323 def today
323 if time_zone.nil?
324 if time_zone.nil?
324 Date.today
325 Date.today
325 else
326 else
326 Time.now.in_time_zone(time_zone).to_date
327 Time.now.in_time_zone(time_zone).to_date
327 end
328 end
328 end
329 end
329
330
330 def logged?
331 def logged?
331 true
332 true
332 end
333 end
333
334
334 def anonymous?
335 def anonymous?
335 !logged?
336 !logged?
336 end
337 end
337
338
338 # Return user's roles for project
339 # Return user's roles for project
339 def roles_for_project(project)
340 def roles_for_project(project)
340 roles = []
341 roles = []
341 # No role on archived projects
342 # No role on archived projects
342 return roles unless project && project.active?
343 return roles unless project && project.active?
343 if logged?
344 if logged?
344 # Find project membership
345 # Find project membership
345 membership = memberships.detect {|m| m.project_id == project.id}
346 membership = memberships.detect {|m| m.project_id == project.id}
346 if membership
347 if membership
347 roles = membership.roles
348 roles = membership.roles
348 else
349 else
349 @role_non_member ||= Role.non_member
350 @role_non_member ||= Role.non_member
350 roles << @role_non_member
351 roles << @role_non_member
351 end
352 end
352 else
353 else
353 @role_anonymous ||= Role.anonymous
354 @role_anonymous ||= Role.anonymous
354 roles << @role_anonymous
355 roles << @role_anonymous
355 end
356 end
356 roles
357 roles
357 end
358 end
358
359
359 # Return true if the user is a member of project
360 # Return true if the user is a member of project
360 def member_of?(project)
361 def member_of?(project)
361 !roles_for_project(project).detect {|role| role.member?}.nil?
362 !roles_for_project(project).detect {|role| role.member?}.nil?
362 end
363 end
363
364
365 # Returns a hash of user's projects grouped by roles
366 def projects_by_role
367 return @projects_by_role if @projects_by_role
368
369 @projects_by_role = Hash.new {|h,k| h[k]=[]}
370 memberships.each do |membership|
371 membership.roles.each do |role|
372 @projects_by_role[role] << membership.project if membership.project
373 end
374 end
375 @projects_by_role.each do |role, projects|
376 projects.uniq!
377 end
378
379 @projects_by_role
380 end
381
364 # Return true if the user is allowed to do the specified action on a specific context
382 # Return true if the user is allowed to do the specified action on a specific context
365 # Action can be:
383 # Action can be:
366 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
384 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
367 # * a permission Symbol (eg. :edit_project)
385 # * a permission Symbol (eg. :edit_project)
368 # Context can be:
386 # Context can be:
369 # * a project : returns true if user is allowed to do the specified action on this project
387 # * a project : returns true if user is allowed to do the specified action on this project
370 # * a group of projects : returns true if user is allowed on every project
388 # * a group of projects : returns true if user is allowed on every project
371 # * nil with options[:global] set : check if user has at least one role allowed for this action,
389 # * nil with options[:global] set : check if user has at least one role allowed for this action,
372 # or falls back to Non Member / Anonymous permissions depending if the user is logged
390 # or falls back to Non Member / Anonymous permissions depending if the user is logged
373 def allowed_to?(action, context, options={})
391 def allowed_to?(action, context, options={})
374 if context && context.is_a?(Project)
392 if context && context.is_a?(Project)
375 # No action allowed on archived projects
393 # No action allowed on archived projects
376 return false unless context.active?
394 return false unless context.active?
377 # No action allowed on disabled modules
395 # No action allowed on disabled modules
378 return false unless context.allows_to?(action)
396 return false unless context.allows_to?(action)
379 # Admin users are authorized for anything else
397 # Admin users are authorized for anything else
380 return true if admin?
398 return true if admin?
381
399
382 roles = roles_for_project(context)
400 roles = roles_for_project(context)
383 return false unless roles
401 return false unless roles
384 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
402 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
385
403
386 elsif context && context.is_a?(Array)
404 elsif context && context.is_a?(Array)
387 # Authorize if user is authorized on every element of the array
405 # Authorize if user is authorized on every element of the array
388 context.map do |project|
406 context.map do |project|
389 allowed_to?(action,project,options)
407 allowed_to?(action,project,options)
390 end.inject do |memo,allowed|
408 end.inject do |memo,allowed|
391 memo && allowed
409 memo && allowed
392 end
410 end
393 elsif options[:global]
411 elsif options[:global]
394 # Admin users are always authorized
412 # Admin users are always authorized
395 return true if admin?
413 return true if admin?
396
414
397 # authorize if user has at least one role that has this permission
415 # authorize if user has at least one role that has this permission
398 roles = memberships.collect {|m| m.roles}.flatten.uniq
416 roles = memberships.collect {|m| m.roles}.flatten.uniq
399 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
417 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
400 else
418 else
401 false
419 false
402 end
420 end
403 end
421 end
404
422
405 # Is the user allowed to do the specified action on any project?
423 # Is the user allowed to do the specified action on any project?
406 # See allowed_to? for the actions and valid options.
424 # See allowed_to? for the actions and valid options.
407 def allowed_to_globally?(action, options)
425 def allowed_to_globally?(action, options)
408 allowed_to?(action, nil, options.reverse_merge(:global => true))
426 allowed_to?(action, nil, options.reverse_merge(:global => true))
409 end
427 end
410
428
411 safe_attributes 'login',
429 safe_attributes 'login',
412 'firstname',
430 'firstname',
413 'lastname',
431 'lastname',
414 'mail',
432 'mail',
415 'mail_notification',
433 'mail_notification',
416 'language',
434 'language',
417 'custom_field_values',
435 'custom_field_values',
418 'custom_fields',
436 'custom_fields',
419 'identity_url'
437 'identity_url'
420
438
421 safe_attributes 'status',
439 safe_attributes 'status',
422 'auth_source_id',
440 'auth_source_id',
423 :if => lambda {|user, current_user| current_user.admin?}
441 :if => lambda {|user, current_user| current_user.admin?}
424
442
425 safe_attributes 'group_ids',
443 safe_attributes 'group_ids',
426 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
444 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
427
445
428 # Utility method to help check if a user should be notified about an
446 # Utility method to help check if a user should be notified about an
429 # event.
447 # event.
430 #
448 #
431 # TODO: only supports Issue events currently
449 # TODO: only supports Issue events currently
432 def notify_about?(object)
450 def notify_about?(object)
433 case mail_notification
451 case mail_notification
434 when 'all'
452 when 'all'
435 true
453 true
436 when 'selected'
454 when 'selected'
437 # user receives notifications for created/assigned issues on unselected projects
455 # user receives notifications for created/assigned issues on unselected projects
438 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
456 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
439 true
457 true
440 else
458 else
441 false
459 false
442 end
460 end
443 when 'none'
461 when 'none'
444 false
462 false
445 when 'only_my_events'
463 when 'only_my_events'
446 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
464 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
447 true
465 true
448 else
466 else
449 false
467 false
450 end
468 end
451 when 'only_assigned'
469 when 'only_assigned'
452 if object.is_a?(Issue) && object.assigned_to == self
470 if object.is_a?(Issue) && object.assigned_to == self
453 true
471 true
454 else
472 else
455 false
473 false
456 end
474 end
457 when 'only_owner'
475 when 'only_owner'
458 if object.is_a?(Issue) && object.author == self
476 if object.is_a?(Issue) && object.author == self
459 true
477 true
460 else
478 else
461 false
479 false
462 end
480 end
463 else
481 else
464 false
482 false
465 end
483 end
466 end
484 end
467
485
468 def self.current=(user)
486 def self.current=(user)
469 @current_user = user
487 @current_user = user
470 end
488 end
471
489
472 def self.current
490 def self.current
473 @current_user ||= User.anonymous
491 @current_user ||= User.anonymous
474 end
492 end
475
493
476 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
494 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
477 # one anonymous user per database.
495 # one anonymous user per database.
478 def self.anonymous
496 def self.anonymous
479 anonymous_user = AnonymousUser.find(:first)
497 anonymous_user = AnonymousUser.find(:first)
480 if anonymous_user.nil?
498 if anonymous_user.nil?
481 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
499 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
482 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
500 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
483 end
501 end
484 anonymous_user
502 anonymous_user
485 end
503 end
486
504
487 # Salts all existing unsalted passwords
505 # Salts all existing unsalted passwords
488 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
506 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
489 # This method is used in the SaltPasswords migration and is to be kept as is
507 # This method is used in the SaltPasswords migration and is to be kept as is
490 def self.salt_unsalted_passwords!
508 def self.salt_unsalted_passwords!
491 transaction do
509 transaction do
492 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
510 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
493 next if user.hashed_password.blank?
511 next if user.hashed_password.blank?
494 salt = User.generate_salt
512 salt = User.generate_salt
495 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
513 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
496 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
514 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
497 end
515 end
498 end
516 end
499 end
517 end
500
518
501 protected
519 protected
502
520
503 def validate
521 def validate
504 # Password length validation based on setting
522 # Password length validation based on setting
505 if !password.nil? && password.size < Setting.password_min_length.to_i
523 if !password.nil? && password.size < Setting.password_min_length.to_i
506 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
524 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
507 end
525 end
508 end
526 end
509
527
510 private
528 private
511
529
512 # Removes references that are not handled by associations
530 # Removes references that are not handled by associations
513 # Things that are not deleted are reassociated with the anonymous user
531 # Things that are not deleted are reassociated with the anonymous user
514 def remove_references_before_destroy
532 def remove_references_before_destroy
515 return if self.id.nil?
533 return if self.id.nil?
516
534
517 substitute = User.anonymous
535 substitute = User.anonymous
518 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
536 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
519 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
537 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
520 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
538 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
521 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
539 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
522 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
540 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
523 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
541 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
524 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
542 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
525 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
543 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
526 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
544 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
527 # Remove private queries and keep public ones
545 # Remove private queries and keep public ones
528 Query.delete_all ['user_id = ? AND is_public = ?', id, false]
546 Query.delete_all ['user_id = ? AND is_public = ?', id, false]
529 Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
547 Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
530 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
548 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
531 Token.delete_all ['user_id = ?', id]
549 Token.delete_all ['user_id = ?', id]
532 Watcher.delete_all ['user_id = ?', id]
550 Watcher.delete_all ['user_id = ?', id]
533 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
551 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
534 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
552 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
535 end
553 end
536
554
537 # Return password digest
555 # Return password digest
538 def self.hash_password(clear_password)
556 def self.hash_password(clear_password)
539 Digest::SHA1.hexdigest(clear_password || "")
557 Digest::SHA1.hexdigest(clear_password || "")
540 end
558 end
541
559
542 # Returns a 128bits random salt as a hex string (32 chars long)
560 # Returns a 128bits random salt as a hex string (32 chars long)
543 def self.generate_salt
561 def self.generate_salt
544 ActiveSupport::SecureRandom.hex(16)
562 ActiveSupport::SecureRandom.hex(16)
545 end
563 end
546
564
547 end
565 end
548
566
549 class AnonymousUser < User
567 class AnonymousUser < User
550
568
551 def validate_on_create
569 def validate_on_create
552 # There should be only one AnonymousUser in the database
570 # There should be only one AnonymousUser in the database
553 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
571 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
554 end
572 end
555
573
556 def available_custom_fields
574 def available_custom_fields
557 []
575 []
558 end
576 end
559
577
560 # Overrides a few properties
578 # Overrides a few properties
561 def logged?; false end
579 def logged?; false end
562 def admin; false end
580 def admin; false end
563 def name(*args); I18n.t(:label_user_anonymous) end
581 def name(*args); I18n.t(:label_user_anonymous) end
564 def mail; nil end
582 def mail; nil end
565 def time_zone; nil end
583 def time_zone; nil end
566 def rss_key; nil end
584 def rss_key; nil end
567
585
568 # Anonymous user can not be destroyed
586 # Anonymous user can not be destroyed
569 def destroy
587 def destroy
570 false
588 false
571 end
589 end
572 end
590 end
@@ -1,798 +1,815
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class UserTest < ActiveSupport::TestCase
20 class UserTest < ActiveSupport::TestCase
21 fixtures :users, :members, :projects, :roles, :member_roles, :auth_sources
21 fixtures :users, :members, :projects, :roles, :member_roles, :auth_sources
22
22
23 def setup
23 def setup
24 @admin = User.find(1)
24 @admin = User.find(1)
25 @jsmith = User.find(2)
25 @jsmith = User.find(2)
26 @dlopper = User.find(3)
26 @dlopper = User.find(3)
27 end
27 end
28
28
29 test 'object_daddy creation' do
29 test 'object_daddy creation' do
30 User.generate_with_protected!(:firstname => 'Testing connection')
30 User.generate_with_protected!(:firstname => 'Testing connection')
31 User.generate_with_protected!(:firstname => 'Testing connection')
31 User.generate_with_protected!(:firstname => 'Testing connection')
32 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
32 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
33 end
33 end
34
34
35 def test_truth
35 def test_truth
36 assert_kind_of User, @jsmith
36 assert_kind_of User, @jsmith
37 end
37 end
38
38
39 def test_mail_should_be_stripped
39 def test_mail_should_be_stripped
40 u = User.new
40 u = User.new
41 u.mail = " foo@bar.com "
41 u.mail = " foo@bar.com "
42 assert_equal "foo@bar.com", u.mail
42 assert_equal "foo@bar.com", u.mail
43 end
43 end
44
44
45 def test_create
45 def test_create
46 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
46 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
47
47
48 user.login = "jsmith"
48 user.login = "jsmith"
49 user.password, user.password_confirmation = "password", "password"
49 user.password, user.password_confirmation = "password", "password"
50 # login uniqueness
50 # login uniqueness
51 assert !user.save
51 assert !user.save
52 assert_equal 1, user.errors.count
52 assert_equal 1, user.errors.count
53
53
54 user.login = "newuser"
54 user.login = "newuser"
55 user.password, user.password_confirmation = "passwd", "password"
55 user.password, user.password_confirmation = "passwd", "password"
56 # password confirmation
56 # password confirmation
57 assert !user.save
57 assert !user.save
58 assert_equal 1, user.errors.count
58 assert_equal 1, user.errors.count
59
59
60 user.password, user.password_confirmation = "password", "password"
60 user.password, user.password_confirmation = "password", "password"
61 assert user.save
61 assert user.save
62 end
62 end
63
63
64 context "User#before_create" do
64 context "User#before_create" do
65 should "set the mail_notification to the default Setting" do
65 should "set the mail_notification to the default Setting" do
66 @user1 = User.generate_with_protected!
66 @user1 = User.generate_with_protected!
67 assert_equal 'only_my_events', @user1.mail_notification
67 assert_equal 'only_my_events', @user1.mail_notification
68
68
69 with_settings :default_notification_option => 'all' do
69 with_settings :default_notification_option => 'all' do
70 @user2 = User.generate_with_protected!
70 @user2 = User.generate_with_protected!
71 assert_equal 'all', @user2.mail_notification
71 assert_equal 'all', @user2.mail_notification
72 end
72 end
73 end
73 end
74 end
74 end
75
75
76 context "User.login" do
76 context "User.login" do
77 should "be case-insensitive." do
77 should "be case-insensitive." do
78 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
78 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
79 u.login = 'newuser'
79 u.login = 'newuser'
80 u.password, u.password_confirmation = "password", "password"
80 u.password, u.password_confirmation = "password", "password"
81 assert u.save
81 assert u.save
82
82
83 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
83 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
84 u.login = 'NewUser'
84 u.login = 'NewUser'
85 u.password, u.password_confirmation = "password", "password"
85 u.password, u.password_confirmation = "password", "password"
86 assert !u.save
86 assert !u.save
87 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:login)
87 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:login)
88 end
88 end
89 end
89 end
90
90
91 def test_mail_uniqueness_should_not_be_case_sensitive
91 def test_mail_uniqueness_should_not_be_case_sensitive
92 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
92 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
93 u.login = 'newuser1'
93 u.login = 'newuser1'
94 u.password, u.password_confirmation = "password", "password"
94 u.password, u.password_confirmation = "password", "password"
95 assert u.save
95 assert u.save
96
96
97 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
97 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
98 u.login = 'newuser2'
98 u.login = 'newuser2'
99 u.password, u.password_confirmation = "password", "password"
99 u.password, u.password_confirmation = "password", "password"
100 assert !u.save
100 assert !u.save
101 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:mail)
101 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:mail)
102 end
102 end
103
103
104 def test_update
104 def test_update
105 assert_equal "admin", @admin.login
105 assert_equal "admin", @admin.login
106 @admin.login = "john"
106 @admin.login = "john"
107 assert @admin.save, @admin.errors.full_messages.join("; ")
107 assert @admin.save, @admin.errors.full_messages.join("; ")
108 @admin.reload
108 @admin.reload
109 assert_equal "john", @admin.login
109 assert_equal "john", @admin.login
110 end
110 end
111
111
112 def test_destroy_should_delete_members_and_roles
112 def test_destroy_should_delete_members_and_roles
113 members = Member.find_all_by_user_id(2)
113 members = Member.find_all_by_user_id(2)
114 ms = members.size
114 ms = members.size
115 rs = members.collect(&:roles).flatten.size
115 rs = members.collect(&:roles).flatten.size
116
116
117 assert_difference 'Member.count', - ms do
117 assert_difference 'Member.count', - ms do
118 assert_difference 'MemberRole.count', - rs do
118 assert_difference 'MemberRole.count', - rs do
119 User.find(2).destroy
119 User.find(2).destroy
120 end
120 end
121 end
121 end
122
122
123 assert_nil User.find_by_id(2)
123 assert_nil User.find_by_id(2)
124 assert Member.find_all_by_user_id(2).empty?
124 assert Member.find_all_by_user_id(2).empty?
125 end
125 end
126
126
127 def test_destroy_should_update_attachments
127 def test_destroy_should_update_attachments
128 attachment = Attachment.create!(:container => Project.find(1),
128 attachment = Attachment.create!(:container => Project.find(1),
129 :file => uploaded_test_file("testfile.txt", "text/plain"),
129 :file => uploaded_test_file("testfile.txt", "text/plain"),
130 :author_id => 2)
130 :author_id => 2)
131
131
132 User.find(2).destroy
132 User.find(2).destroy
133 assert_nil User.find_by_id(2)
133 assert_nil User.find_by_id(2)
134 assert_equal User.anonymous, attachment.reload.author
134 assert_equal User.anonymous, attachment.reload.author
135 end
135 end
136
136
137 def test_destroy_should_update_comments
137 def test_destroy_should_update_comments
138 comment = Comment.create!(
138 comment = Comment.create!(
139 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
139 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
140 :author => User.find(2),
140 :author => User.find(2),
141 :comments => 'foo'
141 :comments => 'foo'
142 )
142 )
143
143
144 User.find(2).destroy
144 User.find(2).destroy
145 assert_nil User.find_by_id(2)
145 assert_nil User.find_by_id(2)
146 assert_equal User.anonymous, comment.reload.author
146 assert_equal User.anonymous, comment.reload.author
147 end
147 end
148
148
149 def test_destroy_should_update_issues
149 def test_destroy_should_update_issues
150 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
150 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
151
151
152 User.find(2).destroy
152 User.find(2).destroy
153 assert_nil User.find_by_id(2)
153 assert_nil User.find_by_id(2)
154 assert_equal User.anonymous, issue.reload.author
154 assert_equal User.anonymous, issue.reload.author
155 end
155 end
156
156
157 def test_destroy_should_unassign_issues
157 def test_destroy_should_unassign_issues
158 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
158 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
159
159
160 User.find(2).destroy
160 User.find(2).destroy
161 assert_nil User.find_by_id(2)
161 assert_nil User.find_by_id(2)
162 assert_nil issue.reload.assigned_to
162 assert_nil issue.reload.assigned_to
163 end
163 end
164
164
165 def test_destroy_should_update_journals
165 def test_destroy_should_update_journals
166 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
166 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
167 issue.init_journal(User.find(2), "update")
167 issue.init_journal(User.find(2), "update")
168 issue.save!
168 issue.save!
169
169
170 User.find(2).destroy
170 User.find(2).destroy
171 assert_nil User.find_by_id(2)
171 assert_nil User.find_by_id(2)
172 assert_equal User.anonymous, issue.journals.first.reload.user
172 assert_equal User.anonymous, issue.journals.first.reload.user
173 end
173 end
174
174
175 def test_destroy_should_update_journal_details_old_value
175 def test_destroy_should_update_journal_details_old_value
176 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
176 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
177 issue.init_journal(User.find(1), "update")
177 issue.init_journal(User.find(1), "update")
178 issue.assigned_to_id = nil
178 issue.assigned_to_id = nil
179 assert_difference 'JournalDetail.count' do
179 assert_difference 'JournalDetail.count' do
180 issue.save!
180 issue.save!
181 end
181 end
182 journal_detail = JournalDetail.first(:order => 'id DESC')
182 journal_detail = JournalDetail.first(:order => 'id DESC')
183 assert_equal '2', journal_detail.old_value
183 assert_equal '2', journal_detail.old_value
184
184
185 User.find(2).destroy
185 User.find(2).destroy
186 assert_nil User.find_by_id(2)
186 assert_nil User.find_by_id(2)
187 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
187 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
188 end
188 end
189
189
190 def test_destroy_should_update_journal_details_value
190 def test_destroy_should_update_journal_details_value
191 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
191 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
192 issue.init_journal(User.find(1), "update")
192 issue.init_journal(User.find(1), "update")
193 issue.assigned_to_id = 2
193 issue.assigned_to_id = 2
194 assert_difference 'JournalDetail.count' do
194 assert_difference 'JournalDetail.count' do
195 issue.save!
195 issue.save!
196 end
196 end
197 journal_detail = JournalDetail.first(:order => 'id DESC')
197 journal_detail = JournalDetail.first(:order => 'id DESC')
198 assert_equal '2', journal_detail.value
198 assert_equal '2', journal_detail.value
199
199
200 User.find(2).destroy
200 User.find(2).destroy
201 assert_nil User.find_by_id(2)
201 assert_nil User.find_by_id(2)
202 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
202 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
203 end
203 end
204
204
205 def test_destroy_should_update_messages
205 def test_destroy_should_update_messages
206 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
206 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
207 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
207 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
208
208
209 User.find(2).destroy
209 User.find(2).destroy
210 assert_nil User.find_by_id(2)
210 assert_nil User.find_by_id(2)
211 assert_equal User.anonymous, message.reload.author
211 assert_equal User.anonymous, message.reload.author
212 end
212 end
213
213
214 def test_destroy_should_update_news
214 def test_destroy_should_update_news
215 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
215 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
216
216
217 User.find(2).destroy
217 User.find(2).destroy
218 assert_nil User.find_by_id(2)
218 assert_nil User.find_by_id(2)
219 assert_equal User.anonymous, news.reload.author
219 assert_equal User.anonymous, news.reload.author
220 end
220 end
221
221
222 def test_destroy_should_delete_private_queries
222 def test_destroy_should_delete_private_queries
223 query = Query.new(:name => 'foo', :is_public => false)
223 query = Query.new(:name => 'foo', :is_public => false)
224 query.project_id = 1
224 query.project_id = 1
225 query.user_id = 2
225 query.user_id = 2
226 query.save!
226 query.save!
227
227
228 User.find(2).destroy
228 User.find(2).destroy
229 assert_nil User.find_by_id(2)
229 assert_nil User.find_by_id(2)
230 assert_nil Query.find_by_id(query.id)
230 assert_nil Query.find_by_id(query.id)
231 end
231 end
232
232
233 def test_destroy_should_update_public_queries
233 def test_destroy_should_update_public_queries
234 query = Query.new(:name => 'foo', :is_public => true)
234 query = Query.new(:name => 'foo', :is_public => true)
235 query.project_id = 1
235 query.project_id = 1
236 query.user_id = 2
236 query.user_id = 2
237 query.save!
237 query.save!
238
238
239 User.find(2).destroy
239 User.find(2).destroy
240 assert_nil User.find_by_id(2)
240 assert_nil User.find_by_id(2)
241 assert_equal User.anonymous, query.reload.user
241 assert_equal User.anonymous, query.reload.user
242 end
242 end
243
243
244 def test_destroy_should_update_time_entries
244 def test_destroy_should_update_time_entries
245 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
245 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
246 entry.project_id = 1
246 entry.project_id = 1
247 entry.user_id = 2
247 entry.user_id = 2
248 entry.save!
248 entry.save!
249
249
250 User.find(2).destroy
250 User.find(2).destroy
251 assert_nil User.find_by_id(2)
251 assert_nil User.find_by_id(2)
252 assert_equal User.anonymous, entry.reload.user
252 assert_equal User.anonymous, entry.reload.user
253 end
253 end
254
254
255 def test_destroy_should_delete_tokens
255 def test_destroy_should_delete_tokens
256 token = Token.create!(:user_id => 2, :value => 'foo')
256 token = Token.create!(:user_id => 2, :value => 'foo')
257
257
258 User.find(2).destroy
258 User.find(2).destroy
259 assert_nil User.find_by_id(2)
259 assert_nil User.find_by_id(2)
260 assert_nil Token.find_by_id(token.id)
260 assert_nil Token.find_by_id(token.id)
261 end
261 end
262
262
263 def test_destroy_should_delete_watchers
263 def test_destroy_should_delete_watchers
264 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
264 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
265 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
265 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
266
266
267 User.find(2).destroy
267 User.find(2).destroy
268 assert_nil User.find_by_id(2)
268 assert_nil User.find_by_id(2)
269 assert_nil Watcher.find_by_id(watcher.id)
269 assert_nil Watcher.find_by_id(watcher.id)
270 end
270 end
271
271
272 def test_destroy_should_update_wiki_contents
272 def test_destroy_should_update_wiki_contents
273 wiki_content = WikiContent.create!(
273 wiki_content = WikiContent.create!(
274 :text => 'foo',
274 :text => 'foo',
275 :author_id => 2,
275 :author_id => 2,
276 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
276 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
277 )
277 )
278 wiki_content.text = 'bar'
278 wiki_content.text = 'bar'
279 assert_difference 'WikiContent::Version.count' do
279 assert_difference 'WikiContent::Version.count' do
280 wiki_content.save!
280 wiki_content.save!
281 end
281 end
282
282
283 User.find(2).destroy
283 User.find(2).destroy
284 assert_nil User.find_by_id(2)
284 assert_nil User.find_by_id(2)
285 assert_equal User.anonymous, wiki_content.reload.author
285 assert_equal User.anonymous, wiki_content.reload.author
286 wiki_content.versions.each do |version|
286 wiki_content.versions.each do |version|
287 assert_equal User.anonymous, version.reload.author
287 assert_equal User.anonymous, version.reload.author
288 end
288 end
289 end
289 end
290
290
291 def test_destroy_should_nullify_issue_categories
291 def test_destroy_should_nullify_issue_categories
292 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
292 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
293
293
294 User.find(2).destroy
294 User.find(2).destroy
295 assert_nil User.find_by_id(2)
295 assert_nil User.find_by_id(2)
296 assert_nil category.reload.assigned_to_id
296 assert_nil category.reload.assigned_to_id
297 end
297 end
298
298
299 def test_destroy_should_nullify_changesets
299 def test_destroy_should_nullify_changesets
300 changeset = Changeset.create!(
300 changeset = Changeset.create!(
301 :repository => Repository::Subversion.create!(
301 :repository => Repository::Subversion.create!(
302 :project_id => 1,
302 :project_id => 1,
303 :url => 'file:///var/svn'
303 :url => 'file:///var/svn'
304 ),
304 ),
305 :revision => '12',
305 :revision => '12',
306 :committed_on => Time.now,
306 :committed_on => Time.now,
307 :committer => 'jsmith'
307 :committer => 'jsmith'
308 )
308 )
309 assert_equal 2, changeset.user_id
309 assert_equal 2, changeset.user_id
310
310
311 User.find(2).destroy
311 User.find(2).destroy
312 assert_nil User.find_by_id(2)
312 assert_nil User.find_by_id(2)
313 assert_nil changeset.reload.user_id
313 assert_nil changeset.reload.user_id
314 end
314 end
315
315
316 def test_anonymous_user_should_not_be_destroyable
316 def test_anonymous_user_should_not_be_destroyable
317 assert_no_difference 'User.count' do
317 assert_no_difference 'User.count' do
318 assert_equal false, User.anonymous.destroy
318 assert_equal false, User.anonymous.destroy
319 end
319 end
320 end
320 end
321
321
322 def test_validate_login_presence
322 def test_validate_login_presence
323 @admin.login = ""
323 @admin.login = ""
324 assert !@admin.save
324 assert !@admin.save
325 assert_equal 1, @admin.errors.count
325 assert_equal 1, @admin.errors.count
326 end
326 end
327
327
328 def test_validate_mail_notification_inclusion
328 def test_validate_mail_notification_inclusion
329 u = User.new
329 u = User.new
330 u.mail_notification = 'foo'
330 u.mail_notification = 'foo'
331 u.save
331 u.save
332 assert_not_nil u.errors.on(:mail_notification)
332 assert_not_nil u.errors.on(:mail_notification)
333 end
333 end
334
334
335 context "User#try_to_login" do
335 context "User#try_to_login" do
336 should "fall-back to case-insensitive if user login is not found as-typed." do
336 should "fall-back to case-insensitive if user login is not found as-typed." do
337 user = User.try_to_login("AdMin", "admin")
337 user = User.try_to_login("AdMin", "admin")
338 assert_kind_of User, user
338 assert_kind_of User, user
339 assert_equal "admin", user.login
339 assert_equal "admin", user.login
340 end
340 end
341
341
342 should "select the exact matching user first" do
342 should "select the exact matching user first" do
343 case_sensitive_user = User.generate_with_protected!(:login => 'changed', :password => 'admin', :password_confirmation => 'admin')
343 case_sensitive_user = User.generate_with_protected!(:login => 'changed', :password => 'admin', :password_confirmation => 'admin')
344 # bypass validations to make it appear like existing data
344 # bypass validations to make it appear like existing data
345 case_sensitive_user.update_attribute(:login, 'ADMIN')
345 case_sensitive_user.update_attribute(:login, 'ADMIN')
346
346
347 user = User.try_to_login("ADMIN", "admin")
347 user = User.try_to_login("ADMIN", "admin")
348 assert_kind_of User, user
348 assert_kind_of User, user
349 assert_equal "ADMIN", user.login
349 assert_equal "ADMIN", user.login
350
350
351 end
351 end
352 end
352 end
353
353
354 def test_password
354 def test_password
355 user = User.try_to_login("admin", "admin")
355 user = User.try_to_login("admin", "admin")
356 assert_kind_of User, user
356 assert_kind_of User, user
357 assert_equal "admin", user.login
357 assert_equal "admin", user.login
358 user.password = "hello"
358 user.password = "hello"
359 assert user.save
359 assert user.save
360
360
361 user = User.try_to_login("admin", "hello")
361 user = User.try_to_login("admin", "hello")
362 assert_kind_of User, user
362 assert_kind_of User, user
363 assert_equal "admin", user.login
363 assert_equal "admin", user.login
364 end
364 end
365
365
366 def test_name_format
366 def test_name_format
367 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
367 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
368 Setting.user_format = :firstname_lastname
368 Setting.user_format = :firstname_lastname
369 assert_equal 'John Smith', @jsmith.reload.name
369 assert_equal 'John Smith', @jsmith.reload.name
370 Setting.user_format = :username
370 Setting.user_format = :username
371 assert_equal 'jsmith', @jsmith.reload.name
371 assert_equal 'jsmith', @jsmith.reload.name
372 end
372 end
373
373
374 def test_lock
374 def test_lock
375 user = User.try_to_login("jsmith", "jsmith")
375 user = User.try_to_login("jsmith", "jsmith")
376 assert_equal @jsmith, user
376 assert_equal @jsmith, user
377
377
378 @jsmith.status = User::STATUS_LOCKED
378 @jsmith.status = User::STATUS_LOCKED
379 assert @jsmith.save
379 assert @jsmith.save
380
380
381 user = User.try_to_login("jsmith", "jsmith")
381 user = User.try_to_login("jsmith", "jsmith")
382 assert_equal nil, user
382 assert_equal nil, user
383 end
383 end
384
384
385 context ".try_to_login" do
385 context ".try_to_login" do
386 context "with good credentials" do
386 context "with good credentials" do
387 should "return the user" do
387 should "return the user" do
388 user = User.try_to_login("admin", "admin")
388 user = User.try_to_login("admin", "admin")
389 assert_kind_of User, user
389 assert_kind_of User, user
390 assert_equal "admin", user.login
390 assert_equal "admin", user.login
391 end
391 end
392 end
392 end
393
393
394 context "with wrong credentials" do
394 context "with wrong credentials" do
395 should "return nil" do
395 should "return nil" do
396 assert_nil User.try_to_login("admin", "foo")
396 assert_nil User.try_to_login("admin", "foo")
397 end
397 end
398 end
398 end
399 end
399 end
400
400
401 if ldap_configured?
401 if ldap_configured?
402 context "#try_to_login using LDAP" do
402 context "#try_to_login using LDAP" do
403 context "with failed connection to the LDAP server" do
403 context "with failed connection to the LDAP server" do
404 should "return nil" do
404 should "return nil" do
405 @auth_source = AuthSourceLdap.find(1)
405 @auth_source = AuthSourceLdap.find(1)
406 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
406 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
407
407
408 assert_equal nil, User.try_to_login('edavis', 'wrong')
408 assert_equal nil, User.try_to_login('edavis', 'wrong')
409 end
409 end
410 end
410 end
411
411
412 context "with an unsuccessful authentication" do
412 context "with an unsuccessful authentication" do
413 should "return nil" do
413 should "return nil" do
414 assert_equal nil, User.try_to_login('edavis', 'wrong')
414 assert_equal nil, User.try_to_login('edavis', 'wrong')
415 end
415 end
416 end
416 end
417
417
418 context "on the fly registration" do
418 context "on the fly registration" do
419 setup do
419 setup do
420 @auth_source = AuthSourceLdap.find(1)
420 @auth_source = AuthSourceLdap.find(1)
421 end
421 end
422
422
423 context "with a successful authentication" do
423 context "with a successful authentication" do
424 should "create a new user account if it doesn't exist" do
424 should "create a new user account if it doesn't exist" do
425 assert_difference('User.count') do
425 assert_difference('User.count') do
426 user = User.try_to_login('edavis', '123456')
426 user = User.try_to_login('edavis', '123456')
427 assert !user.admin?
427 assert !user.admin?
428 end
428 end
429 end
429 end
430
430
431 should "retrieve existing user" do
431 should "retrieve existing user" do
432 user = User.try_to_login('edavis', '123456')
432 user = User.try_to_login('edavis', '123456')
433 user.admin = true
433 user.admin = true
434 user.save!
434 user.save!
435
435
436 assert_no_difference('User.count') do
436 assert_no_difference('User.count') do
437 user = User.try_to_login('edavis', '123456')
437 user = User.try_to_login('edavis', '123456')
438 assert user.admin?
438 assert user.admin?
439 end
439 end
440 end
440 end
441 end
441 end
442 end
442 end
443 end
443 end
444
444
445 else
445 else
446 puts "Skipping LDAP tests."
446 puts "Skipping LDAP tests."
447 end
447 end
448
448
449 def test_create_anonymous
449 def test_create_anonymous
450 AnonymousUser.delete_all
450 AnonymousUser.delete_all
451 anon = User.anonymous
451 anon = User.anonymous
452 assert !anon.new_record?
452 assert !anon.new_record?
453 assert_kind_of AnonymousUser, anon
453 assert_kind_of AnonymousUser, anon
454 end
454 end
455
455
456 should_have_one :rss_token
456 should_have_one :rss_token
457
457
458 def test_rss_key
458 def test_rss_key
459 assert_nil @jsmith.rss_token
459 assert_nil @jsmith.rss_token
460 key = @jsmith.rss_key
460 key = @jsmith.rss_key
461 assert_equal 40, key.length
461 assert_equal 40, key.length
462
462
463 @jsmith.reload
463 @jsmith.reload
464 assert_equal key, @jsmith.rss_key
464 assert_equal key, @jsmith.rss_key
465 end
465 end
466
466
467
467
468 should_have_one :api_token
468 should_have_one :api_token
469
469
470 context "User#api_key" do
470 context "User#api_key" do
471 should "generate a new one if the user doesn't have one" do
471 should "generate a new one if the user doesn't have one" do
472 user = User.generate_with_protected!(:api_token => nil)
472 user = User.generate_with_protected!(:api_token => nil)
473 assert_nil user.api_token
473 assert_nil user.api_token
474
474
475 key = user.api_key
475 key = user.api_key
476 assert_equal 40, key.length
476 assert_equal 40, key.length
477 user.reload
477 user.reload
478 assert_equal key, user.api_key
478 assert_equal key, user.api_key
479 end
479 end
480
480
481 should "return the existing api token value" do
481 should "return the existing api token value" do
482 user = User.generate_with_protected!
482 user = User.generate_with_protected!
483 token = Token.generate!(:action => 'api')
483 token = Token.generate!(:action => 'api')
484 user.api_token = token
484 user.api_token = token
485 assert user.save
485 assert user.save
486
486
487 assert_equal token.value, user.api_key
487 assert_equal token.value, user.api_key
488 end
488 end
489 end
489 end
490
490
491 context "User#find_by_api_key" do
491 context "User#find_by_api_key" do
492 should "return nil if no matching key is found" do
492 should "return nil if no matching key is found" do
493 assert_nil User.find_by_api_key('zzzzzzzzz')
493 assert_nil User.find_by_api_key('zzzzzzzzz')
494 end
494 end
495
495
496 should "return nil if the key is found for an inactive user" do
496 should "return nil if the key is found for an inactive user" do
497 user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
497 user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
498 token = Token.generate!(:action => 'api')
498 token = Token.generate!(:action => 'api')
499 user.api_token = token
499 user.api_token = token
500 user.save
500 user.save
501
501
502 assert_nil User.find_by_api_key(token.value)
502 assert_nil User.find_by_api_key(token.value)
503 end
503 end
504
504
505 should "return the user if the key is found for an active user" do
505 should "return the user if the key is found for an active user" do
506 user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
506 user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
507 token = Token.generate!(:action => 'api')
507 token = Token.generate!(:action => 'api')
508 user.api_token = token
508 user.api_token = token
509 user.save
509 user.save
510
510
511 assert_equal user, User.find_by_api_key(token.value)
511 assert_equal user, User.find_by_api_key(token.value)
512 end
512 end
513 end
513 end
514
514
515 def test_roles_for_project
515 def test_roles_for_project
516 # user with a role
516 # user with a role
517 roles = @jsmith.roles_for_project(Project.find(1))
517 roles = @jsmith.roles_for_project(Project.find(1))
518 assert_kind_of Role, roles.first
518 assert_kind_of Role, roles.first
519 assert_equal "Manager", roles.first.name
519 assert_equal "Manager", roles.first.name
520
520
521 # user with no role
521 # user with no role
522 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
522 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
523 end
523 end
524
524
525 def test_projects_by_role_for_user_with_role
526 user = User.find(2)
527 assert_kind_of Hash, user.projects_by_role
528 assert_equal 2, user.projects_by_role.size
529 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
530 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
531 end
532
533 def test_projects_by_role_for_user_with_no_role
534 user = User.generate!
535 assert_equal({}, user.projects_by_role)
536 end
537
538 def test_projects_by_role_for_anonymous
539 assert_equal({}, User.anonymous.projects_by_role)
540 end
541
525 def test_valid_notification_options
542 def test_valid_notification_options
526 # without memberships
543 # without memberships
527 assert_equal 5, User.find(7).valid_notification_options.size
544 assert_equal 5, User.find(7).valid_notification_options.size
528 # with memberships
545 # with memberships
529 assert_equal 6, User.find(2).valid_notification_options.size
546 assert_equal 6, User.find(2).valid_notification_options.size
530 end
547 end
531
548
532 def test_valid_notification_options_class_method
549 def test_valid_notification_options_class_method
533 assert_equal 5, User.valid_notification_options.size
550 assert_equal 5, User.valid_notification_options.size
534 assert_equal 5, User.valid_notification_options(User.find(7)).size
551 assert_equal 5, User.valid_notification_options(User.find(7)).size
535 assert_equal 6, User.valid_notification_options(User.find(2)).size
552 assert_equal 6, User.valid_notification_options(User.find(2)).size
536 end
553 end
537
554
538 def test_mail_notification_all
555 def test_mail_notification_all
539 @jsmith.mail_notification = 'all'
556 @jsmith.mail_notification = 'all'
540 @jsmith.notified_project_ids = []
557 @jsmith.notified_project_ids = []
541 @jsmith.save
558 @jsmith.save
542 @jsmith.reload
559 @jsmith.reload
543 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
560 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
544 end
561 end
545
562
546 def test_mail_notification_selected
563 def test_mail_notification_selected
547 @jsmith.mail_notification = 'selected'
564 @jsmith.mail_notification = 'selected'
548 @jsmith.notified_project_ids = [1]
565 @jsmith.notified_project_ids = [1]
549 @jsmith.save
566 @jsmith.save
550 @jsmith.reload
567 @jsmith.reload
551 assert Project.find(1).recipients.include?(@jsmith.mail)
568 assert Project.find(1).recipients.include?(@jsmith.mail)
552 end
569 end
553
570
554 def test_mail_notification_only_my_events
571 def test_mail_notification_only_my_events
555 @jsmith.mail_notification = 'only_my_events'
572 @jsmith.mail_notification = 'only_my_events'
556 @jsmith.notified_project_ids = []
573 @jsmith.notified_project_ids = []
557 @jsmith.save
574 @jsmith.save
558 @jsmith.reload
575 @jsmith.reload
559 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
576 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
560 end
577 end
561
578
562 def test_comments_sorting_preference
579 def test_comments_sorting_preference
563 assert !@jsmith.wants_comments_in_reverse_order?
580 assert !@jsmith.wants_comments_in_reverse_order?
564 @jsmith.pref.comments_sorting = 'asc'
581 @jsmith.pref.comments_sorting = 'asc'
565 assert !@jsmith.wants_comments_in_reverse_order?
582 assert !@jsmith.wants_comments_in_reverse_order?
566 @jsmith.pref.comments_sorting = 'desc'
583 @jsmith.pref.comments_sorting = 'desc'
567 assert @jsmith.wants_comments_in_reverse_order?
584 assert @jsmith.wants_comments_in_reverse_order?
568 end
585 end
569
586
570 def test_find_by_mail_should_be_case_insensitive
587 def test_find_by_mail_should_be_case_insensitive
571 u = User.find_by_mail('JSmith@somenet.foo')
588 u = User.find_by_mail('JSmith@somenet.foo')
572 assert_not_nil u
589 assert_not_nil u
573 assert_equal 'jsmith@somenet.foo', u.mail
590 assert_equal 'jsmith@somenet.foo', u.mail
574 end
591 end
575
592
576 def test_random_password
593 def test_random_password
577 u = User.new
594 u = User.new
578 u.random_password
595 u.random_password
579 assert !u.password.blank?
596 assert !u.password.blank?
580 assert !u.password_confirmation.blank?
597 assert !u.password_confirmation.blank?
581 end
598 end
582
599
583 context "#change_password_allowed?" do
600 context "#change_password_allowed?" do
584 should "be allowed if no auth source is set" do
601 should "be allowed if no auth source is set" do
585 user = User.generate_with_protected!
602 user = User.generate_with_protected!
586 assert user.change_password_allowed?
603 assert user.change_password_allowed?
587 end
604 end
588
605
589 should "delegate to the auth source" do
606 should "delegate to the auth source" do
590 user = User.generate_with_protected!
607 user = User.generate_with_protected!
591
608
592 allowed_auth_source = AuthSource.generate!
609 allowed_auth_source = AuthSource.generate!
593 def allowed_auth_source.allow_password_changes?; true; end
610 def allowed_auth_source.allow_password_changes?; true; end
594
611
595 denied_auth_source = AuthSource.generate!
612 denied_auth_source = AuthSource.generate!
596 def denied_auth_source.allow_password_changes?; false; end
613 def denied_auth_source.allow_password_changes?; false; end
597
614
598 assert user.change_password_allowed?
615 assert user.change_password_allowed?
599
616
600 user.auth_source = allowed_auth_source
617 user.auth_source = allowed_auth_source
601 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
618 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
602
619
603 user.auth_source = denied_auth_source
620 user.auth_source = denied_auth_source
604 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
621 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
605 end
622 end
606
623
607 end
624 end
608
625
609 context "#allowed_to?" do
626 context "#allowed_to?" do
610 context "with a unique project" do
627 context "with a unique project" do
611 should "return false if project is archived" do
628 should "return false if project is archived" do
612 project = Project.find(1)
629 project = Project.find(1)
613 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
630 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
614 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
631 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
615 end
632 end
616
633
617 should "return false if related module is disabled" do
634 should "return false if related module is disabled" do
618 project = Project.find(1)
635 project = Project.find(1)
619 project.enabled_module_names = ["issue_tracking"]
636 project.enabled_module_names = ["issue_tracking"]
620 assert @admin.allowed_to?(:add_issues, project)
637 assert @admin.allowed_to?(:add_issues, project)
621 assert ! @admin.allowed_to?(:view_wiki_pages, project)
638 assert ! @admin.allowed_to?(:view_wiki_pages, project)
622 end
639 end
623
640
624 should "authorize nearly everything for admin users" do
641 should "authorize nearly everything for admin users" do
625 project = Project.find(1)
642 project = Project.find(1)
626 assert ! @admin.member_of?(project)
643 assert ! @admin.member_of?(project)
627 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
644 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
628 assert @admin.allowed_to?(p.to_sym, project)
645 assert @admin.allowed_to?(p.to_sym, project)
629 end
646 end
630 end
647 end
631
648
632 should "authorize normal users depending on their roles" do
649 should "authorize normal users depending on their roles" do
633 project = Project.find(1)
650 project = Project.find(1)
634 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
651 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
635 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
652 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
636 end
653 end
637 end
654 end
638
655
639 context "with multiple projects" do
656 context "with multiple projects" do
640 should "return false if array is empty" do
657 should "return false if array is empty" do
641 assert ! @admin.allowed_to?(:view_project, [])
658 assert ! @admin.allowed_to?(:view_project, [])
642 end
659 end
643
660
644 should "return true only if user has permission on all these projects" do
661 should "return true only if user has permission on all these projects" do
645 assert @admin.allowed_to?(:view_project, Project.all)
662 assert @admin.allowed_to?(:view_project, Project.all)
646 assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
663 assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
647 assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
664 assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
648 assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
665 assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
649 end
666 end
650
667
651 should "behave correctly with arrays of 1 project" do
668 should "behave correctly with arrays of 1 project" do
652 assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first])
669 assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first])
653 end
670 end
654 end
671 end
655
672
656 context "with options[:global]" do
673 context "with options[:global]" do
657 should "authorize if user has at least one role that has this permission" do
674 should "authorize if user has at least one role that has this permission" do
658 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
675 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
659 @anonymous = User.find(6)
676 @anonymous = User.find(6)
660 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
677 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
661 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
678 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
662 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
679 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
663 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
680 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
664 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
681 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
665 end
682 end
666 end
683 end
667 end
684 end
668
685
669 context "User#notify_about?" do
686 context "User#notify_about?" do
670 context "Issues" do
687 context "Issues" do
671 setup do
688 setup do
672 @project = Project.find(1)
689 @project = Project.find(1)
673 @author = User.generate_with_protected!
690 @author = User.generate_with_protected!
674 @assignee = User.generate_with_protected!
691 @assignee = User.generate_with_protected!
675 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
692 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
676 end
693 end
677
694
678 should "be true for a user with :all" do
695 should "be true for a user with :all" do
679 @author.update_attribute(:mail_notification, 'all')
696 @author.update_attribute(:mail_notification, 'all')
680 assert @author.notify_about?(@issue)
697 assert @author.notify_about?(@issue)
681 end
698 end
682
699
683 should "be false for a user with :none" do
700 should "be false for a user with :none" do
684 @author.update_attribute(:mail_notification, 'none')
701 @author.update_attribute(:mail_notification, 'none')
685 assert ! @author.notify_about?(@issue)
702 assert ! @author.notify_about?(@issue)
686 end
703 end
687
704
688 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
705 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
689 @user = User.generate_with_protected!(:mail_notification => 'only_my_events')
706 @user = User.generate_with_protected!(:mail_notification => 'only_my_events')
690 Member.create!(:user => @user, :project => @project, :role_ids => [1])
707 Member.create!(:user => @user, :project => @project, :role_ids => [1])
691 assert ! @user.notify_about?(@issue)
708 assert ! @user.notify_about?(@issue)
692 end
709 end
693
710
694 should "be true for a user with :only_my_events and is the author" do
711 should "be true for a user with :only_my_events and is the author" do
695 @author.update_attribute(:mail_notification, 'only_my_events')
712 @author.update_attribute(:mail_notification, 'only_my_events')
696 assert @author.notify_about?(@issue)
713 assert @author.notify_about?(@issue)
697 end
714 end
698
715
699 should "be true for a user with :only_my_events and is the assignee" do
716 should "be true for a user with :only_my_events and is the assignee" do
700 @assignee.update_attribute(:mail_notification, 'only_my_events')
717 @assignee.update_attribute(:mail_notification, 'only_my_events')
701 assert @assignee.notify_about?(@issue)
718 assert @assignee.notify_about?(@issue)
702 end
719 end
703
720
704 should "be true for a user with :only_assigned and is the assignee" do
721 should "be true for a user with :only_assigned and is the assignee" do
705 @assignee.update_attribute(:mail_notification, 'only_assigned')
722 @assignee.update_attribute(:mail_notification, 'only_assigned')
706 assert @assignee.notify_about?(@issue)
723 assert @assignee.notify_about?(@issue)
707 end
724 end
708
725
709 should "be false for a user with :only_assigned and is not the assignee" do
726 should "be false for a user with :only_assigned and is not the assignee" do
710 @author.update_attribute(:mail_notification, 'only_assigned')
727 @author.update_attribute(:mail_notification, 'only_assigned')
711 assert ! @author.notify_about?(@issue)
728 assert ! @author.notify_about?(@issue)
712 end
729 end
713
730
714 should "be true for a user with :only_owner and is the author" do
731 should "be true for a user with :only_owner and is the author" do
715 @author.update_attribute(:mail_notification, 'only_owner')
732 @author.update_attribute(:mail_notification, 'only_owner')
716 assert @author.notify_about?(@issue)
733 assert @author.notify_about?(@issue)
717 end
734 end
718
735
719 should "be false for a user with :only_owner and is not the author" do
736 should "be false for a user with :only_owner and is not the author" do
720 @assignee.update_attribute(:mail_notification, 'only_owner')
737 @assignee.update_attribute(:mail_notification, 'only_owner')
721 assert ! @assignee.notify_about?(@issue)
738 assert ! @assignee.notify_about?(@issue)
722 end
739 end
723
740
724 should "be true for a user with :selected and is the author" do
741 should "be true for a user with :selected and is the author" do
725 @author.update_attribute(:mail_notification, 'selected')
742 @author.update_attribute(:mail_notification, 'selected')
726 assert @author.notify_about?(@issue)
743 assert @author.notify_about?(@issue)
727 end
744 end
728
745
729 should "be true for a user with :selected and is the assignee" do
746 should "be true for a user with :selected and is the assignee" do
730 @assignee.update_attribute(:mail_notification, 'selected')
747 @assignee.update_attribute(:mail_notification, 'selected')
731 assert @assignee.notify_about?(@issue)
748 assert @assignee.notify_about?(@issue)
732 end
749 end
733
750
734 should "be false for a user with :selected and is not the author or assignee" do
751 should "be false for a user with :selected and is not the author or assignee" do
735 @user = User.generate_with_protected!(:mail_notification => 'selected')
752 @user = User.generate_with_protected!(:mail_notification => 'selected')
736 Member.create!(:user => @user, :project => @project, :role_ids => [1])
753 Member.create!(:user => @user, :project => @project, :role_ids => [1])
737 assert ! @user.notify_about?(@issue)
754 assert ! @user.notify_about?(@issue)
738 end
755 end
739 end
756 end
740
757
741 context "other events" do
758 context "other events" do
742 should 'be added and tested'
759 should 'be added and tested'
743 end
760 end
744 end
761 end
745
762
746 def test_salt_unsalted_passwords
763 def test_salt_unsalted_passwords
747 # Restore a user with an unsalted password
764 # Restore a user with an unsalted password
748 user = User.find(1)
765 user = User.find(1)
749 user.salt = nil
766 user.salt = nil
750 user.hashed_password = User.hash_password("unsalted")
767 user.hashed_password = User.hash_password("unsalted")
751 user.save!
768 user.save!
752
769
753 User.salt_unsalted_passwords!
770 User.salt_unsalted_passwords!
754
771
755 user.reload
772 user.reload
756 # Salt added
773 # Salt added
757 assert !user.salt.blank?
774 assert !user.salt.blank?
758 # Password still valid
775 # Password still valid
759 assert user.check_password?("unsalted")
776 assert user.check_password?("unsalted")
760 assert_equal user, User.try_to_login(user.login, "unsalted")
777 assert_equal user, User.try_to_login(user.login, "unsalted")
761 end
778 end
762
779
763 if Object.const_defined?(:OpenID)
780 if Object.const_defined?(:OpenID)
764
781
765 def test_setting_identity_url
782 def test_setting_identity_url
766 normalized_open_id_url = 'http://example.com/'
783 normalized_open_id_url = 'http://example.com/'
767 u = User.new( :identity_url => 'http://example.com/' )
784 u = User.new( :identity_url => 'http://example.com/' )
768 assert_equal normalized_open_id_url, u.identity_url
785 assert_equal normalized_open_id_url, u.identity_url
769 end
786 end
770
787
771 def test_setting_identity_url_without_trailing_slash
788 def test_setting_identity_url_without_trailing_slash
772 normalized_open_id_url = 'http://example.com/'
789 normalized_open_id_url = 'http://example.com/'
773 u = User.new( :identity_url => 'http://example.com' )
790 u = User.new( :identity_url => 'http://example.com' )
774 assert_equal normalized_open_id_url, u.identity_url
791 assert_equal normalized_open_id_url, u.identity_url
775 end
792 end
776
793
777 def test_setting_identity_url_without_protocol
794 def test_setting_identity_url_without_protocol
778 normalized_open_id_url = 'http://example.com/'
795 normalized_open_id_url = 'http://example.com/'
779 u = User.new( :identity_url => 'example.com' )
796 u = User.new( :identity_url => 'example.com' )
780 assert_equal normalized_open_id_url, u.identity_url
797 assert_equal normalized_open_id_url, u.identity_url
781 end
798 end
782
799
783 def test_setting_blank_identity_url
800 def test_setting_blank_identity_url
784 u = User.new( :identity_url => 'example.com' )
801 u = User.new( :identity_url => 'example.com' )
785 u.identity_url = ''
802 u.identity_url = ''
786 assert u.identity_url.blank?
803 assert u.identity_url.blank?
787 end
804 end
788
805
789 def test_setting_invalid_identity_url
806 def test_setting_invalid_identity_url
790 u = User.new( :identity_url => 'this is not an openid url' )
807 u = User.new( :identity_url => 'this is not an openid url' )
791 assert u.identity_url.blank?
808 assert u.identity_url.blank?
792 end
809 end
793
810
794 else
811 else
795 puts "Skipping openid tests."
812 puts "Skipping openid tests."
796 end
813 end
797
814
798 end
815 end
General Comments 0
You need to be logged in to leave comments. Login now