##// END OF EJS Templates
Adds User#builtin_role....
Jean-Philippe Lang -
r11735:e978b3ace0e8
parent child
Show More
@@ -1,1044 +1,1044
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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_CLOSED = 5
23 STATUS_CLOSED = 5
24 STATUS_ARCHIVED = 9
24 STATUS_ARCHIVED = 9
25
25
26 # Maximum length for project identifiers
26 # Maximum length for project identifiers
27 IDENTIFIER_MAX_LENGTH = 100
27 IDENTIFIER_MAX_LENGTH = 100
28
28
29 # Specific overidden Activities
29 # Specific overidden Activities
30 has_many :time_entry_activities
30 has_many :time_entry_activities
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 has_many :memberships, :class_name => 'Member'
32 has_many :memberships, :class_name => 'Member'
33 has_many :member_principals, :class_name => 'Member',
33 has_many :member_principals, :class_name => 'Member',
34 :include => :principal,
34 :include => :principal,
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36
36
37 has_many :enabled_modules, :dependent => :delete_all
37 has_many :enabled_modules, :dependent => :delete_all
38 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
39 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
40 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :time_entries, :dependent => :delete_all
42 has_many :time_entries, :dependent => :delete_all
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 has_many :documents, :dependent => :destroy
44 has_many :documents, :dependent => :destroy
45 has_many :news, :dependent => :destroy, :include => :author
45 has_many :news, :dependent => :destroy, :include => :author
46 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_one :repository, :conditions => ["is_default = ?", true]
48 has_one :repository, :conditions => ["is_default = ?", true]
49 has_many :repositories, :dependent => :destroy
49 has_many :repositories, :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', :dependent => :destroy
59 acts_as_nested_set :order => 'name', :dependent => :destroy
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 => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
78 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :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 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
82 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
83 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
83 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
84 before_destroy :delete_all_members
84 before_destroy :delete_all_members
85
85
86 scope :has_module, lambda {|mod|
86 scope :has_module, lambda {|mod|
87 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
87 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 }
88 }
89 scope :active, lambda { where(:status => STATUS_ACTIVE) }
89 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
90 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 scope :all_public, lambda { where(:is_public => true) }
91 scope :all_public, lambda { where(:is_public => true) }
92 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
92 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 scope :allowed_to, lambda {|*args|
93 scope :allowed_to, lambda {|*args|
94 user = User.current
94 user = User.current
95 permission = nil
95 permission = nil
96 if args.first.is_a?(Symbol)
96 if args.first.is_a?(Symbol)
97 permission = args.shift
97 permission = args.shift
98 else
98 else
99 user = args.shift
99 user = args.shift
100 permission = args.shift
100 permission = args.shift
101 end
101 end
102 where(Project.allowed_to_condition(user, permission, *args))
102 where(Project.allowed_to_condition(user, permission, *args))
103 }
103 }
104 scope :like, lambda {|arg|
104 scope :like, lambda {|arg|
105 if arg.blank?
105 if arg.blank?
106 where(nil)
106 where(nil)
107 else
107 else
108 pattern = "%#{arg.to_s.strip.downcase}%"
108 pattern = "%#{arg.to_s.strip.downcase}%"
109 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
109 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 end
110 end
111 }
111 }
112
112
113 def initialize(attributes=nil, *args)
113 def initialize(attributes=nil, *args)
114 super
114 super
115
115
116 initialized = (attributes || {}).stringify_keys
116 initialized = (attributes || {}).stringify_keys
117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
118 self.identifier = Project.next_identifier
118 self.identifier = Project.next_identifier
119 end
119 end
120 if !initialized.key?('is_public')
120 if !initialized.key?('is_public')
121 self.is_public = Setting.default_projects_public?
121 self.is_public = Setting.default_projects_public?
122 end
122 end
123 if !initialized.key?('enabled_module_names')
123 if !initialized.key?('enabled_module_names')
124 self.enabled_module_names = Setting.default_projects_modules
124 self.enabled_module_names = Setting.default_projects_modules
125 end
125 end
126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
127 default = Setting.default_projects_tracker_ids
127 default = Setting.default_projects_tracker_ids
128 if default.is_a?(Array)
128 if default.is_a?(Array)
129 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
129 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
130 else
130 else
131 self.trackers = Tracker.sorted.all
131 self.trackers = Tracker.sorted.all
132 end
132 end
133 end
133 end
134 end
134 end
135
135
136 def identifier=(identifier)
136 def identifier=(identifier)
137 super unless identifier_frozen?
137 super unless identifier_frozen?
138 end
138 end
139
139
140 def identifier_frozen?
140 def identifier_frozen?
141 errors[:identifier].blank? && !(new_record? || identifier.blank?)
141 errors[:identifier].blank? && !(new_record? || identifier.blank?)
142 end
142 end
143
143
144 # returns latest created projects
144 # returns latest created projects
145 # non public projects will be returned only if user is a member of those
145 # non public projects will be returned only if user is a member of those
146 def self.latest(user=nil, count=5)
146 def self.latest(user=nil, count=5)
147 visible(user).limit(count).order("created_on DESC").all
147 visible(user).limit(count).order("created_on DESC").all
148 end
148 end
149
149
150 # Returns true if the project is visible to +user+ or to the current user.
150 # Returns true if the project is visible to +user+ or to the current user.
151 def visible?(user=User.current)
151 def visible?(user=User.current)
152 user.allowed_to?(:view_project, self)
152 user.allowed_to?(:view_project, self)
153 end
153 end
154
154
155 # Returns a SQL conditions string used to find all projects visible by the specified user.
155 # Returns a SQL conditions string used to find all projects visible by the specified user.
156 #
156 #
157 # Examples:
157 # Examples:
158 # Project.visible_condition(admin) => "projects.status = 1"
158 # Project.visible_condition(admin) => "projects.status = 1"
159 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
159 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
160 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
160 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
161 def self.visible_condition(user, options={})
161 def self.visible_condition(user, options={})
162 allowed_to_condition(user, :view_project, options)
162 allowed_to_condition(user, :view_project, options)
163 end
163 end
164
164
165 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
165 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
166 #
166 #
167 # Valid options:
167 # Valid options:
168 # * :project => limit the condition to project
168 # * :project => limit the condition to project
169 # * :with_subprojects => limit the condition to project and its subprojects
169 # * :with_subprojects => limit the condition to project and its subprojects
170 # * :member => limit the condition to the user projects
170 # * :member => limit the condition to the user projects
171 def self.allowed_to_condition(user, permission, options={})
171 def self.allowed_to_condition(user, permission, options={})
172 perm = Redmine::AccessControl.permission(permission)
172 perm = Redmine::AccessControl.permission(permission)
173 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
173 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
174 if perm && perm.project_module
174 if perm && perm.project_module
175 # If the permission belongs to a project module, make sure the module is enabled
175 # If the permission belongs to a project module, make sure the module is enabled
176 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
176 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
177 end
177 end
178 if options[:project]
178 if options[:project]
179 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
179 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
180 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
180 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
181 base_statement = "(#{project_statement}) AND (#{base_statement})"
181 base_statement = "(#{project_statement}) AND (#{base_statement})"
182 end
182 end
183
183
184 if user.admin?
184 if user.admin?
185 base_statement
185 base_statement
186 else
186 else
187 statement_by_role = {}
187 statement_by_role = {}
188 unless options[:member]
188 unless options[:member]
189 role = user.logged? ? Role.non_member : Role.anonymous
189 role = user.builtin_role
190 if role.allowed_to?(permission)
190 if role.allowed_to?(permission)
191 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
191 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
192 end
192 end
193 end
193 end
194 if user.logged?
194 if user.logged?
195 user.projects_by_role.each do |role, projects|
195 user.projects_by_role.each do |role, projects|
196 if role.allowed_to?(permission) && projects.any?
196 if role.allowed_to?(permission) && projects.any?
197 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
197 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
198 end
198 end
199 end
199 end
200 end
200 end
201 if statement_by_role.empty?
201 if statement_by_role.empty?
202 "1=0"
202 "1=0"
203 else
203 else
204 if block_given?
204 if block_given?
205 statement_by_role.each do |role, statement|
205 statement_by_role.each do |role, statement|
206 if s = yield(role, user)
206 if s = yield(role, user)
207 statement_by_role[role] = "(#{statement} AND (#{s}))"
207 statement_by_role[role] = "(#{statement} AND (#{s}))"
208 end
208 end
209 end
209 end
210 end
210 end
211 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
211 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
212 end
212 end
213 end
213 end
214 end
214 end
215
215
216 def principals
216 def principals
217 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
217 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
218 end
218 end
219
219
220 def users
220 def users
221 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
221 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
222 end
222 end
223
223
224 # Returns the Systemwide and project specific activities
224 # Returns the Systemwide and project specific activities
225 def activities(include_inactive=false)
225 def activities(include_inactive=false)
226 if include_inactive
226 if include_inactive
227 return all_activities
227 return all_activities
228 else
228 else
229 return active_activities
229 return active_activities
230 end
230 end
231 end
231 end
232
232
233 # Will create a new Project specific Activity or update an existing one
233 # Will create a new Project specific Activity or update an existing one
234 #
234 #
235 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
235 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
236 # does not successfully save.
236 # does not successfully save.
237 def update_or_create_time_entry_activity(id, activity_hash)
237 def update_or_create_time_entry_activity(id, activity_hash)
238 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
238 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
239 self.create_time_entry_activity_if_needed(activity_hash)
239 self.create_time_entry_activity_if_needed(activity_hash)
240 else
240 else
241 activity = project.time_entry_activities.find_by_id(id.to_i)
241 activity = project.time_entry_activities.find_by_id(id.to_i)
242 activity.update_attributes(activity_hash) if activity
242 activity.update_attributes(activity_hash) if activity
243 end
243 end
244 end
244 end
245
245
246 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
246 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
247 #
247 #
248 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
248 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
249 # does not successfully save.
249 # does not successfully save.
250 def create_time_entry_activity_if_needed(activity)
250 def create_time_entry_activity_if_needed(activity)
251 if activity['parent_id']
251 if activity['parent_id']
252
252
253 parent_activity = TimeEntryActivity.find(activity['parent_id'])
253 parent_activity = TimeEntryActivity.find(activity['parent_id'])
254 activity['name'] = parent_activity.name
254 activity['name'] = parent_activity.name
255 activity['position'] = parent_activity.position
255 activity['position'] = parent_activity.position
256
256
257 if Enumeration.overridding_change?(activity, parent_activity)
257 if Enumeration.overridding_change?(activity, parent_activity)
258 project_activity = self.time_entry_activities.create(activity)
258 project_activity = self.time_entry_activities.create(activity)
259
259
260 if project_activity.new_record?
260 if project_activity.new_record?
261 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
261 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
262 else
262 else
263 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
263 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
264 end
264 end
265 end
265 end
266 end
266 end
267 end
267 end
268
268
269 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
269 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
270 #
270 #
271 # Examples:
271 # Examples:
272 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
272 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
273 # project.project_condition(false) => "projects.id = 1"
273 # project.project_condition(false) => "projects.id = 1"
274 def project_condition(with_subprojects)
274 def project_condition(with_subprojects)
275 cond = "#{Project.table_name}.id = #{id}"
275 cond = "#{Project.table_name}.id = #{id}"
276 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
276 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
277 cond
277 cond
278 end
278 end
279
279
280 def self.find(*args)
280 def self.find(*args)
281 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
281 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
282 project = find_by_identifier(*args)
282 project = find_by_identifier(*args)
283 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
283 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
284 project
284 project
285 else
285 else
286 super
286 super
287 end
287 end
288 end
288 end
289
289
290 def self.find_by_param(*args)
290 def self.find_by_param(*args)
291 self.find(*args)
291 self.find(*args)
292 end
292 end
293
293
294 alias :base_reload :reload
294 alias :base_reload :reload
295 def reload(*args)
295 def reload(*args)
296 @principals = nil
296 @principals = nil
297 @users = nil
297 @users = nil
298 @shared_versions = nil
298 @shared_versions = nil
299 @rolled_up_versions = nil
299 @rolled_up_versions = nil
300 @rolled_up_trackers = nil
300 @rolled_up_trackers = nil
301 @all_issue_custom_fields = nil
301 @all_issue_custom_fields = nil
302 @all_time_entry_custom_fields = nil
302 @all_time_entry_custom_fields = nil
303 @to_param = nil
303 @to_param = nil
304 @allowed_parents = nil
304 @allowed_parents = nil
305 @allowed_permissions = nil
305 @allowed_permissions = nil
306 @actions_allowed = nil
306 @actions_allowed = nil
307 @start_date = nil
307 @start_date = nil
308 @due_date = nil
308 @due_date = nil
309 base_reload(*args)
309 base_reload(*args)
310 end
310 end
311
311
312 def to_param
312 def to_param
313 # id is used for projects with a numeric identifier (compatibility)
313 # id is used for projects with a numeric identifier (compatibility)
314 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
314 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
315 end
315 end
316
316
317 def active?
317 def active?
318 self.status == STATUS_ACTIVE
318 self.status == STATUS_ACTIVE
319 end
319 end
320
320
321 def archived?
321 def archived?
322 self.status == STATUS_ARCHIVED
322 self.status == STATUS_ARCHIVED
323 end
323 end
324
324
325 # Archives the project and its descendants
325 # Archives the project and its descendants
326 def archive
326 def archive
327 # Check that there is no issue of a non descendant project that is assigned
327 # Check that there is no issue of a non descendant project that is assigned
328 # to one of the project or descendant versions
328 # to one of the project or descendant versions
329 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
329 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
330 if v_ids.any? &&
330 if v_ids.any? &&
331 Issue.
331 Issue.
332 includes(:project).
332 includes(:project).
333 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
333 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
334 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
334 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
335 exists?
335 exists?
336 return false
336 return false
337 end
337 end
338 Project.transaction do
338 Project.transaction do
339 archive!
339 archive!
340 end
340 end
341 true
341 true
342 end
342 end
343
343
344 # Unarchives the project
344 # Unarchives the project
345 # All its ancestors must be active
345 # All its ancestors must be active
346 def unarchive
346 def unarchive
347 return false if ancestors.detect {|a| !a.active?}
347 return false if ancestors.detect {|a| !a.active?}
348 update_attribute :status, STATUS_ACTIVE
348 update_attribute :status, STATUS_ACTIVE
349 end
349 end
350
350
351 def close
351 def close
352 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
352 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
353 end
353 end
354
354
355 def reopen
355 def reopen
356 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
356 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
357 end
357 end
358
358
359 # Returns an array of projects the project can be moved to
359 # Returns an array of projects the project can be moved to
360 # by the current user
360 # by the current user
361 def allowed_parents
361 def allowed_parents
362 return @allowed_parents if @allowed_parents
362 return @allowed_parents if @allowed_parents
363 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
363 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
364 @allowed_parents = @allowed_parents - self_and_descendants
364 @allowed_parents = @allowed_parents - self_and_descendants
365 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
365 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
366 @allowed_parents << nil
366 @allowed_parents << nil
367 end
367 end
368 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
368 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
369 @allowed_parents << parent
369 @allowed_parents << parent
370 end
370 end
371 @allowed_parents
371 @allowed_parents
372 end
372 end
373
373
374 # Sets the parent of the project with authorization check
374 # Sets the parent of the project with authorization check
375 def set_allowed_parent!(p)
375 def set_allowed_parent!(p)
376 unless p.nil? || p.is_a?(Project)
376 unless p.nil? || p.is_a?(Project)
377 if p.to_s.blank?
377 if p.to_s.blank?
378 p = nil
378 p = nil
379 else
379 else
380 p = Project.find_by_id(p)
380 p = Project.find_by_id(p)
381 return false unless p
381 return false unless p
382 end
382 end
383 end
383 end
384 if p.nil?
384 if p.nil?
385 if !new_record? && allowed_parents.empty?
385 if !new_record? && allowed_parents.empty?
386 return false
386 return false
387 end
387 end
388 elsif !allowed_parents.include?(p)
388 elsif !allowed_parents.include?(p)
389 return false
389 return false
390 end
390 end
391 set_parent!(p)
391 set_parent!(p)
392 end
392 end
393
393
394 # Sets the parent of the project
394 # Sets the parent of the project
395 # Argument can be either a Project, a String, a Fixnum or nil
395 # Argument can be either a Project, a String, a Fixnum or nil
396 def set_parent!(p)
396 def set_parent!(p)
397 unless p.nil? || p.is_a?(Project)
397 unless p.nil? || p.is_a?(Project)
398 if p.to_s.blank?
398 if p.to_s.blank?
399 p = nil
399 p = nil
400 else
400 else
401 p = Project.find_by_id(p)
401 p = Project.find_by_id(p)
402 return false unless p
402 return false unless p
403 end
403 end
404 end
404 end
405 if p == parent && !p.nil?
405 if p == parent && !p.nil?
406 # Nothing to do
406 # Nothing to do
407 true
407 true
408 elsif p.nil? || (p.active? && move_possible?(p))
408 elsif p.nil? || (p.active? && move_possible?(p))
409 set_or_update_position_under(p)
409 set_or_update_position_under(p)
410 Issue.update_versions_from_hierarchy_change(self)
410 Issue.update_versions_from_hierarchy_change(self)
411 true
411 true
412 else
412 else
413 # Can not move to the given target
413 # Can not move to the given target
414 false
414 false
415 end
415 end
416 end
416 end
417
417
418 # Recalculates all lft and rgt values based on project names
418 # Recalculates all lft and rgt values based on project names
419 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
419 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
420 # Used in BuildProjectsTree migration
420 # Used in BuildProjectsTree migration
421 def self.rebuild_tree!
421 def self.rebuild_tree!
422 transaction do
422 transaction do
423 update_all "lft = NULL, rgt = NULL"
423 update_all "lft = NULL, rgt = NULL"
424 rebuild!(false)
424 rebuild!(false)
425 end
425 end
426 end
426 end
427
427
428 # Returns an array of the trackers used by the project and its active sub projects
428 # Returns an array of the trackers used by the project and its active sub projects
429 def rolled_up_trackers
429 def rolled_up_trackers
430 @rolled_up_trackers ||=
430 @rolled_up_trackers ||=
431 Tracker.
431 Tracker.
432 joins(:projects).
432 joins(:projects).
433 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
433 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
434 select("DISTINCT #{Tracker.table_name}.*").
434 select("DISTINCT #{Tracker.table_name}.*").
435 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
435 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
436 sorted.
436 sorted.
437 all
437 all
438 end
438 end
439
439
440 # Closes open and locked project versions that are completed
440 # Closes open and locked project versions that are completed
441 def close_completed_versions
441 def close_completed_versions
442 Version.transaction do
442 Version.transaction do
443 versions.where(:status => %w(open locked)).all.each do |version|
443 versions.where(:status => %w(open locked)).all.each do |version|
444 if version.completed?
444 if version.completed?
445 version.update_attribute(:status, 'closed')
445 version.update_attribute(:status, 'closed')
446 end
446 end
447 end
447 end
448 end
448 end
449 end
449 end
450
450
451 # Returns a scope of the Versions on subprojects
451 # Returns a scope of the Versions on subprojects
452 def rolled_up_versions
452 def rolled_up_versions
453 @rolled_up_versions ||=
453 @rolled_up_versions ||=
454 Version.
454 Version.
455 includes(:project).
455 includes(:project).
456 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
456 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
457 end
457 end
458
458
459 # Returns a scope of the Versions used by the project
459 # Returns a scope of the Versions used by the project
460 def shared_versions
460 def shared_versions
461 if new_record?
461 if new_record?
462 Version.
462 Version.
463 includes(:project).
463 includes(:project).
464 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
464 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
465 else
465 else
466 @shared_versions ||= begin
466 @shared_versions ||= begin
467 r = root? ? self : root
467 r = root? ? self : root
468 Version.
468 Version.
469 includes(:project).
469 includes(:project).
470 where("#{Project.table_name}.id = #{id}" +
470 where("#{Project.table_name}.id = #{id}" +
471 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
471 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
472 " #{Version.table_name}.sharing = 'system'" +
472 " #{Version.table_name}.sharing = 'system'" +
473 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
473 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
474 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
474 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
475 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
475 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
476 "))")
476 "))")
477 end
477 end
478 end
478 end
479 end
479 end
480
480
481 # Returns a hash of project users grouped by role
481 # Returns a hash of project users grouped by role
482 def users_by_role
482 def users_by_role
483 members.includes(:user, :roles).all.inject({}) do |h, m|
483 members.includes(:user, :roles).all.inject({}) do |h, m|
484 m.roles.each do |r|
484 m.roles.each do |r|
485 h[r] ||= []
485 h[r] ||= []
486 h[r] << m.user
486 h[r] << m.user
487 end
487 end
488 h
488 h
489 end
489 end
490 end
490 end
491
491
492 # Deletes all project's members
492 # Deletes all project's members
493 def delete_all_members
493 def delete_all_members
494 me, mr = Member.table_name, MemberRole.table_name
494 me, mr = Member.table_name, MemberRole.table_name
495 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
495 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
496 Member.delete_all(['project_id = ?', id])
496 Member.delete_all(['project_id = ?', id])
497 end
497 end
498
498
499 # Users/groups issues can be assigned to
499 # Users/groups issues can be assigned to
500 def assignable_users
500 def assignable_users
501 assignable = Setting.issue_group_assignment? ? member_principals : members
501 assignable = Setting.issue_group_assignment? ? member_principals : members
502 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
502 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
503 end
503 end
504
504
505 # Returns the mail adresses of users that should be always notified on project events
505 # Returns the mail adresses of users that should be always notified on project events
506 def recipients
506 def recipients
507 notified_users.collect {|user| user.mail}
507 notified_users.collect {|user| user.mail}
508 end
508 end
509
509
510 # Returns the users that should be notified on project events
510 # Returns the users that should be notified on project events
511 def notified_users
511 def notified_users
512 # TODO: User part should be extracted to User#notify_about?
512 # TODO: User part should be extracted to User#notify_about?
513 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
513 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
514 end
514 end
515
515
516 # Returns a scope of all custom fields enabled for project issues
516 # Returns a scope of all custom fields enabled for project issues
517 # (explictly associated custom fields and custom fields enabled for all projects)
517 # (explictly associated custom fields and custom fields enabled for all projects)
518 def all_issue_custom_fields
518 def all_issue_custom_fields
519 @all_issue_custom_fields ||= IssueCustomField.
519 @all_issue_custom_fields ||= IssueCustomField.
520 sorted.
520 sorted.
521 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
521 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
522 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
522 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
523 " WHERE cfp.project_id = ?)", true, id)
523 " WHERE cfp.project_id = ?)", true, id)
524 end
524 end
525
525
526 # Returns an array of all custom fields enabled for project time entries
526 # Returns an array of all custom fields enabled for project time entries
527 # (explictly associated custom fields and custom fields enabled for all projects)
527 # (explictly associated custom fields and custom fields enabled for all projects)
528 def all_time_entry_custom_fields
528 def all_time_entry_custom_fields
529 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
529 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
530 end
530 end
531
531
532 def project
532 def project
533 self
533 self
534 end
534 end
535
535
536 def <=>(project)
536 def <=>(project)
537 name.downcase <=> project.name.downcase
537 name.downcase <=> project.name.downcase
538 end
538 end
539
539
540 def to_s
540 def to_s
541 name
541 name
542 end
542 end
543
543
544 # Returns a short description of the projects (first lines)
544 # Returns a short description of the projects (first lines)
545 def short_description(length = 255)
545 def short_description(length = 255)
546 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
546 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
547 end
547 end
548
548
549 def css_classes
549 def css_classes
550 s = 'project'
550 s = 'project'
551 s << ' root' if root?
551 s << ' root' if root?
552 s << ' child' if child?
552 s << ' child' if child?
553 s << (leaf? ? ' leaf' : ' parent')
553 s << (leaf? ? ' leaf' : ' parent')
554 unless active?
554 unless active?
555 if archived?
555 if archived?
556 s << ' archived'
556 s << ' archived'
557 else
557 else
558 s << ' closed'
558 s << ' closed'
559 end
559 end
560 end
560 end
561 s
561 s
562 end
562 end
563
563
564 # The earliest start date of a project, based on it's issues and versions
564 # The earliest start date of a project, based on it's issues and versions
565 def start_date
565 def start_date
566 @start_date ||= [
566 @start_date ||= [
567 issues.minimum('start_date'),
567 issues.minimum('start_date'),
568 shared_versions.minimum('effective_date'),
568 shared_versions.minimum('effective_date'),
569 Issue.fixed_version(shared_versions).minimum('start_date')
569 Issue.fixed_version(shared_versions).minimum('start_date')
570 ].compact.min
570 ].compact.min
571 end
571 end
572
572
573 # The latest due date of an issue or version
573 # The latest due date of an issue or version
574 def due_date
574 def due_date
575 @due_date ||= [
575 @due_date ||= [
576 issues.maximum('due_date'),
576 issues.maximum('due_date'),
577 shared_versions.maximum('effective_date'),
577 shared_versions.maximum('effective_date'),
578 Issue.fixed_version(shared_versions).maximum('due_date')
578 Issue.fixed_version(shared_versions).maximum('due_date')
579 ].compact.max
579 ].compact.max
580 end
580 end
581
581
582 def overdue?
582 def overdue?
583 active? && !due_date.nil? && (due_date < Date.today)
583 active? && !due_date.nil? && (due_date < Date.today)
584 end
584 end
585
585
586 # Returns the percent completed for this project, based on the
586 # Returns the percent completed for this project, based on the
587 # progress on it's versions.
587 # progress on it's versions.
588 def completed_percent(options={:include_subprojects => false})
588 def completed_percent(options={:include_subprojects => false})
589 if options.delete(:include_subprojects)
589 if options.delete(:include_subprojects)
590 total = self_and_descendants.collect(&:completed_percent).sum
590 total = self_and_descendants.collect(&:completed_percent).sum
591
591
592 total / self_and_descendants.count
592 total / self_and_descendants.count
593 else
593 else
594 if versions.count > 0
594 if versions.count > 0
595 total = versions.collect(&:completed_percent).sum
595 total = versions.collect(&:completed_percent).sum
596
596
597 total / versions.count
597 total / versions.count
598 else
598 else
599 100
599 100
600 end
600 end
601 end
601 end
602 end
602 end
603
603
604 # Return true if this project allows to do the specified action.
604 # Return true if this project allows to do the specified action.
605 # action can be:
605 # action can be:
606 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
606 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
607 # * a permission Symbol (eg. :edit_project)
607 # * a permission Symbol (eg. :edit_project)
608 def allows_to?(action)
608 def allows_to?(action)
609 if archived?
609 if archived?
610 # No action allowed on archived projects
610 # No action allowed on archived projects
611 return false
611 return false
612 end
612 end
613 unless active? || Redmine::AccessControl.read_action?(action)
613 unless active? || Redmine::AccessControl.read_action?(action)
614 # No write action allowed on closed projects
614 # No write action allowed on closed projects
615 return false
615 return false
616 end
616 end
617 # No action allowed on disabled modules
617 # No action allowed on disabled modules
618 if action.is_a? Hash
618 if action.is_a? Hash
619 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
619 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
620 else
620 else
621 allowed_permissions.include? action
621 allowed_permissions.include? action
622 end
622 end
623 end
623 end
624
624
625 def module_enabled?(module_name)
625 def module_enabled?(module_name)
626 module_name = module_name.to_s
626 module_name = module_name.to_s
627 enabled_modules.detect {|m| m.name == module_name}
627 enabled_modules.detect {|m| m.name == module_name}
628 end
628 end
629
629
630 def enabled_module_names=(module_names)
630 def enabled_module_names=(module_names)
631 if module_names && module_names.is_a?(Array)
631 if module_names && module_names.is_a?(Array)
632 module_names = module_names.collect(&:to_s).reject(&:blank?)
632 module_names = module_names.collect(&:to_s).reject(&:blank?)
633 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
633 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
634 else
634 else
635 enabled_modules.clear
635 enabled_modules.clear
636 end
636 end
637 end
637 end
638
638
639 # Returns an array of the enabled modules names
639 # Returns an array of the enabled modules names
640 def enabled_module_names
640 def enabled_module_names
641 enabled_modules.collect(&:name)
641 enabled_modules.collect(&:name)
642 end
642 end
643
643
644 # Enable a specific module
644 # Enable a specific module
645 #
645 #
646 # Examples:
646 # Examples:
647 # project.enable_module!(:issue_tracking)
647 # project.enable_module!(:issue_tracking)
648 # project.enable_module!("issue_tracking")
648 # project.enable_module!("issue_tracking")
649 def enable_module!(name)
649 def enable_module!(name)
650 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
650 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
651 end
651 end
652
652
653 # Disable a module if it exists
653 # Disable a module if it exists
654 #
654 #
655 # Examples:
655 # Examples:
656 # project.disable_module!(:issue_tracking)
656 # project.disable_module!(:issue_tracking)
657 # project.disable_module!("issue_tracking")
657 # project.disable_module!("issue_tracking")
658 # project.disable_module!(project.enabled_modules.first)
658 # project.disable_module!(project.enabled_modules.first)
659 def disable_module!(target)
659 def disable_module!(target)
660 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
660 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
661 target.destroy unless target.blank?
661 target.destroy unless target.blank?
662 end
662 end
663
663
664 safe_attributes 'name',
664 safe_attributes 'name',
665 'description',
665 'description',
666 'homepage',
666 'homepage',
667 'is_public',
667 'is_public',
668 'identifier',
668 'identifier',
669 'custom_field_values',
669 'custom_field_values',
670 'custom_fields',
670 'custom_fields',
671 'tracker_ids',
671 'tracker_ids',
672 'issue_custom_field_ids'
672 'issue_custom_field_ids'
673
673
674 safe_attributes 'enabled_module_names',
674 safe_attributes 'enabled_module_names',
675 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
675 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
676
676
677 safe_attributes 'inherit_members',
677 safe_attributes 'inherit_members',
678 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
678 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
679
679
680 # Returns an array of projects that are in this project's hierarchy
680 # Returns an array of projects that are in this project's hierarchy
681 #
681 #
682 # Example: parents, children, siblings
682 # Example: parents, children, siblings
683 def hierarchy
683 def hierarchy
684 parents = project.self_and_ancestors || []
684 parents = project.self_and_ancestors || []
685 descendants = project.descendants || []
685 descendants = project.descendants || []
686 project_hierarchy = parents | descendants # Set union
686 project_hierarchy = parents | descendants # Set union
687 end
687 end
688
688
689 # Returns an auto-generated project identifier based on the last identifier used
689 # Returns an auto-generated project identifier based on the last identifier used
690 def self.next_identifier
690 def self.next_identifier
691 p = Project.order('id DESC').first
691 p = Project.order('id DESC').first
692 p.nil? ? nil : p.identifier.to_s.succ
692 p.nil? ? nil : p.identifier.to_s.succ
693 end
693 end
694
694
695 # Copies and saves the Project instance based on the +project+.
695 # Copies and saves the Project instance based on the +project+.
696 # Duplicates the source project's:
696 # Duplicates the source project's:
697 # * Wiki
697 # * Wiki
698 # * Versions
698 # * Versions
699 # * Categories
699 # * Categories
700 # * Issues
700 # * Issues
701 # * Members
701 # * Members
702 # * Queries
702 # * Queries
703 #
703 #
704 # Accepts an +options+ argument to specify what to copy
704 # Accepts an +options+ argument to specify what to copy
705 #
705 #
706 # Examples:
706 # Examples:
707 # project.copy(1) # => copies everything
707 # project.copy(1) # => copies everything
708 # project.copy(1, :only => 'members') # => copies members only
708 # project.copy(1, :only => 'members') # => copies members only
709 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
709 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
710 def copy(project, options={})
710 def copy(project, options={})
711 project = project.is_a?(Project) ? project : Project.find(project)
711 project = project.is_a?(Project) ? project : Project.find(project)
712
712
713 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
713 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
714 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
714 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
715
715
716 Project.transaction do
716 Project.transaction do
717 if save
717 if save
718 reload
718 reload
719 to_be_copied.each do |name|
719 to_be_copied.each do |name|
720 send "copy_#{name}", project
720 send "copy_#{name}", project
721 end
721 end
722 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
722 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
723 save
723 save
724 end
724 end
725 end
725 end
726 end
726 end
727
727
728 # Returns a new unsaved Project instance with attributes copied from +project+
728 # Returns a new unsaved Project instance with attributes copied from +project+
729 def self.copy_from(project)
729 def self.copy_from(project)
730 project = project.is_a?(Project) ? project : Project.find(project)
730 project = project.is_a?(Project) ? project : Project.find(project)
731 # clear unique attributes
731 # clear unique attributes
732 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
732 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
733 copy = Project.new(attributes)
733 copy = Project.new(attributes)
734 copy.enabled_modules = project.enabled_modules
734 copy.enabled_modules = project.enabled_modules
735 copy.trackers = project.trackers
735 copy.trackers = project.trackers
736 copy.custom_values = project.custom_values.collect {|v| v.clone}
736 copy.custom_values = project.custom_values.collect {|v| v.clone}
737 copy.issue_custom_fields = project.issue_custom_fields
737 copy.issue_custom_fields = project.issue_custom_fields
738 copy
738 copy
739 end
739 end
740
740
741 # Yields the given block for each project with its level in the tree
741 # Yields the given block for each project with its level in the tree
742 def self.project_tree(projects, &block)
742 def self.project_tree(projects, &block)
743 ancestors = []
743 ancestors = []
744 projects.sort_by(&:lft).each do |project|
744 projects.sort_by(&:lft).each do |project|
745 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
745 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
746 ancestors.pop
746 ancestors.pop
747 end
747 end
748 yield project, ancestors.size
748 yield project, ancestors.size
749 ancestors << project
749 ancestors << project
750 end
750 end
751 end
751 end
752
752
753 private
753 private
754
754
755 def after_parent_changed(parent_was)
755 def after_parent_changed(parent_was)
756 remove_inherited_member_roles
756 remove_inherited_member_roles
757 add_inherited_member_roles
757 add_inherited_member_roles
758 end
758 end
759
759
760 def update_inherited_members
760 def update_inherited_members
761 if parent
761 if parent
762 if inherit_members? && !inherit_members_was
762 if inherit_members? && !inherit_members_was
763 remove_inherited_member_roles
763 remove_inherited_member_roles
764 add_inherited_member_roles
764 add_inherited_member_roles
765 elsif !inherit_members? && inherit_members_was
765 elsif !inherit_members? && inherit_members_was
766 remove_inherited_member_roles
766 remove_inherited_member_roles
767 end
767 end
768 end
768 end
769 end
769 end
770
770
771 def remove_inherited_member_roles
771 def remove_inherited_member_roles
772 member_roles = memberships.map(&:member_roles).flatten
772 member_roles = memberships.map(&:member_roles).flatten
773 member_role_ids = member_roles.map(&:id)
773 member_role_ids = member_roles.map(&:id)
774 member_roles.each do |member_role|
774 member_roles.each do |member_role|
775 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
775 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
776 member_role.destroy
776 member_role.destroy
777 end
777 end
778 end
778 end
779 end
779 end
780
780
781 def add_inherited_member_roles
781 def add_inherited_member_roles
782 if inherit_members? && parent
782 if inherit_members? && parent
783 parent.memberships.each do |parent_member|
783 parent.memberships.each do |parent_member|
784 member = Member.find_or_new(self.id, parent_member.user_id)
784 member = Member.find_or_new(self.id, parent_member.user_id)
785 parent_member.member_roles.each do |parent_member_role|
785 parent_member.member_roles.each do |parent_member_role|
786 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
786 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
787 end
787 end
788 member.save!
788 member.save!
789 end
789 end
790 end
790 end
791 end
791 end
792
792
793 # Copies wiki from +project+
793 # Copies wiki from +project+
794 def copy_wiki(project)
794 def copy_wiki(project)
795 # Check that the source project has a wiki first
795 # Check that the source project has a wiki first
796 unless project.wiki.nil?
796 unless project.wiki.nil?
797 wiki = self.wiki || Wiki.new
797 wiki = self.wiki || Wiki.new
798 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
798 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
799 wiki_pages_map = {}
799 wiki_pages_map = {}
800 project.wiki.pages.each do |page|
800 project.wiki.pages.each do |page|
801 # Skip pages without content
801 # Skip pages without content
802 next if page.content.nil?
802 next if page.content.nil?
803 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
803 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
804 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
804 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
805 new_wiki_page.content = new_wiki_content
805 new_wiki_page.content = new_wiki_content
806 wiki.pages << new_wiki_page
806 wiki.pages << new_wiki_page
807 wiki_pages_map[page.id] = new_wiki_page
807 wiki_pages_map[page.id] = new_wiki_page
808 end
808 end
809
809
810 self.wiki = wiki
810 self.wiki = wiki
811 wiki.save
811 wiki.save
812 # Reproduce page hierarchy
812 # Reproduce page hierarchy
813 project.wiki.pages.each do |page|
813 project.wiki.pages.each do |page|
814 if page.parent_id && wiki_pages_map[page.id]
814 if page.parent_id && wiki_pages_map[page.id]
815 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
815 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
816 wiki_pages_map[page.id].save
816 wiki_pages_map[page.id].save
817 end
817 end
818 end
818 end
819 end
819 end
820 end
820 end
821
821
822 # Copies versions from +project+
822 # Copies versions from +project+
823 def copy_versions(project)
823 def copy_versions(project)
824 project.versions.each do |version|
824 project.versions.each do |version|
825 new_version = Version.new
825 new_version = Version.new
826 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
826 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
827 self.versions << new_version
827 self.versions << new_version
828 end
828 end
829 end
829 end
830
830
831 # Copies issue categories from +project+
831 # Copies issue categories from +project+
832 def copy_issue_categories(project)
832 def copy_issue_categories(project)
833 project.issue_categories.each do |issue_category|
833 project.issue_categories.each do |issue_category|
834 new_issue_category = IssueCategory.new
834 new_issue_category = IssueCategory.new
835 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
835 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
836 self.issue_categories << new_issue_category
836 self.issue_categories << new_issue_category
837 end
837 end
838 end
838 end
839
839
840 # Copies issues from +project+
840 # Copies issues from +project+
841 def copy_issues(project)
841 def copy_issues(project)
842 # Stores the source issue id as a key and the copied issues as the
842 # Stores the source issue id as a key and the copied issues as the
843 # value. Used to map the two togeather for issue relations.
843 # value. Used to map the two togeather for issue relations.
844 issues_map = {}
844 issues_map = {}
845
845
846 # Store status and reopen locked/closed versions
846 # Store status and reopen locked/closed versions
847 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
847 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
848 version_statuses.each do |version, status|
848 version_statuses.each do |version, status|
849 version.update_attribute :status, 'open'
849 version.update_attribute :status, 'open'
850 end
850 end
851
851
852 # Get issues sorted by root_id, lft so that parent issues
852 # Get issues sorted by root_id, lft so that parent issues
853 # get copied before their children
853 # get copied before their children
854 project.issues.reorder('root_id, lft').all.each do |issue|
854 project.issues.reorder('root_id, lft').all.each do |issue|
855 new_issue = Issue.new
855 new_issue = Issue.new
856 new_issue.copy_from(issue, :subtasks => false, :link => false)
856 new_issue.copy_from(issue, :subtasks => false, :link => false)
857 new_issue.project = self
857 new_issue.project = self
858 # Changing project resets the custom field values
858 # Changing project resets the custom field values
859 # TODO: handle this in Issue#project=
859 # TODO: handle this in Issue#project=
860 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
860 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
861 # Reassign fixed_versions by name, since names are unique per project
861 # Reassign fixed_versions by name, since names are unique per project
862 if issue.fixed_version && issue.fixed_version.project == project
862 if issue.fixed_version && issue.fixed_version.project == project
863 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
863 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
864 end
864 end
865 # Reassign the category by name, since names are unique per project
865 # Reassign the category by name, since names are unique per project
866 if issue.category
866 if issue.category
867 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
867 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
868 end
868 end
869 # Parent issue
869 # Parent issue
870 if issue.parent_id
870 if issue.parent_id
871 if copied_parent = issues_map[issue.parent_id]
871 if copied_parent = issues_map[issue.parent_id]
872 new_issue.parent_issue_id = copied_parent.id
872 new_issue.parent_issue_id = copied_parent.id
873 end
873 end
874 end
874 end
875
875
876 self.issues << new_issue
876 self.issues << new_issue
877 if new_issue.new_record?
877 if new_issue.new_record?
878 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
878 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
879 else
879 else
880 issues_map[issue.id] = new_issue unless new_issue.new_record?
880 issues_map[issue.id] = new_issue unless new_issue.new_record?
881 end
881 end
882 end
882 end
883
883
884 # Restore locked/closed version statuses
884 # Restore locked/closed version statuses
885 version_statuses.each do |version, status|
885 version_statuses.each do |version, status|
886 version.update_attribute :status, status
886 version.update_attribute :status, status
887 end
887 end
888
888
889 # Relations after in case issues related each other
889 # Relations after in case issues related each other
890 project.issues.each do |issue|
890 project.issues.each do |issue|
891 new_issue = issues_map[issue.id]
891 new_issue = issues_map[issue.id]
892 unless new_issue
892 unless new_issue
893 # Issue was not copied
893 # Issue was not copied
894 next
894 next
895 end
895 end
896
896
897 # Relations
897 # Relations
898 issue.relations_from.each do |source_relation|
898 issue.relations_from.each do |source_relation|
899 new_issue_relation = IssueRelation.new
899 new_issue_relation = IssueRelation.new
900 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
900 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
901 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
901 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
902 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
902 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
903 new_issue_relation.issue_to = source_relation.issue_to
903 new_issue_relation.issue_to = source_relation.issue_to
904 end
904 end
905 new_issue.relations_from << new_issue_relation
905 new_issue.relations_from << new_issue_relation
906 end
906 end
907
907
908 issue.relations_to.each do |source_relation|
908 issue.relations_to.each do |source_relation|
909 new_issue_relation = IssueRelation.new
909 new_issue_relation = IssueRelation.new
910 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
910 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
911 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
911 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
912 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
912 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
913 new_issue_relation.issue_from = source_relation.issue_from
913 new_issue_relation.issue_from = source_relation.issue_from
914 end
914 end
915 new_issue.relations_to << new_issue_relation
915 new_issue.relations_to << new_issue_relation
916 end
916 end
917 end
917 end
918 end
918 end
919
919
920 # Copies members from +project+
920 # Copies members from +project+
921 def copy_members(project)
921 def copy_members(project)
922 # Copy users first, then groups to handle members with inherited and given roles
922 # Copy users first, then groups to handle members with inherited and given roles
923 members_to_copy = []
923 members_to_copy = []
924 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
924 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
925 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
925 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
926
926
927 members_to_copy.each do |member|
927 members_to_copy.each do |member|
928 new_member = Member.new
928 new_member = Member.new
929 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
929 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
930 # only copy non inherited roles
930 # only copy non inherited roles
931 # inherited roles will be added when copying the group membership
931 # inherited roles will be added when copying the group membership
932 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
932 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
933 next if role_ids.empty?
933 next if role_ids.empty?
934 new_member.role_ids = role_ids
934 new_member.role_ids = role_ids
935 new_member.project = self
935 new_member.project = self
936 self.members << new_member
936 self.members << new_member
937 end
937 end
938 end
938 end
939
939
940 # Copies queries from +project+
940 # Copies queries from +project+
941 def copy_queries(project)
941 def copy_queries(project)
942 project.queries.each do |query|
942 project.queries.each do |query|
943 new_query = IssueQuery.new
943 new_query = IssueQuery.new
944 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
944 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
945 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
945 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
946 new_query.project = self
946 new_query.project = self
947 new_query.user_id = query.user_id
947 new_query.user_id = query.user_id
948 self.queries << new_query
948 self.queries << new_query
949 end
949 end
950 end
950 end
951
951
952 # Copies boards from +project+
952 # Copies boards from +project+
953 def copy_boards(project)
953 def copy_boards(project)
954 project.boards.each do |board|
954 project.boards.each do |board|
955 new_board = Board.new
955 new_board = Board.new
956 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
956 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
957 new_board.project = self
957 new_board.project = self
958 self.boards << new_board
958 self.boards << new_board
959 end
959 end
960 end
960 end
961
961
962 def allowed_permissions
962 def allowed_permissions
963 @allowed_permissions ||= begin
963 @allowed_permissions ||= begin
964 module_names = enabled_modules.pluck(:name)
964 module_names = enabled_modules.pluck(:name)
965 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
965 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
966 end
966 end
967 end
967 end
968
968
969 def allowed_actions
969 def allowed_actions
970 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
970 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
971 end
971 end
972
972
973 # Returns all the active Systemwide and project specific activities
973 # Returns all the active Systemwide and project specific activities
974 def active_activities
974 def active_activities
975 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
975 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
976
976
977 if overridden_activity_ids.empty?
977 if overridden_activity_ids.empty?
978 return TimeEntryActivity.shared.active
978 return TimeEntryActivity.shared.active
979 else
979 else
980 return system_activities_and_project_overrides
980 return system_activities_and_project_overrides
981 end
981 end
982 end
982 end
983
983
984 # Returns all the Systemwide and project specific activities
984 # Returns all the Systemwide and project specific activities
985 # (inactive and active)
985 # (inactive and active)
986 def all_activities
986 def all_activities
987 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
987 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
988
988
989 if overridden_activity_ids.empty?
989 if overridden_activity_ids.empty?
990 return TimeEntryActivity.shared
990 return TimeEntryActivity.shared
991 else
991 else
992 return system_activities_and_project_overrides(true)
992 return system_activities_and_project_overrides(true)
993 end
993 end
994 end
994 end
995
995
996 # Returns the systemwide active activities merged with the project specific overrides
996 # Returns the systemwide active activities merged with the project specific overrides
997 def system_activities_and_project_overrides(include_inactive=false)
997 def system_activities_and_project_overrides(include_inactive=false)
998 if include_inactive
998 if include_inactive
999 return TimeEntryActivity.shared.
999 return TimeEntryActivity.shared.
1000 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
1000 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
1001 self.time_entry_activities
1001 self.time_entry_activities
1002 else
1002 else
1003 return TimeEntryActivity.shared.active.
1003 return TimeEntryActivity.shared.active.
1004 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
1004 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
1005 self.time_entry_activities.active
1005 self.time_entry_activities.active
1006 end
1006 end
1007 end
1007 end
1008
1008
1009 # Archives subprojects recursively
1009 # Archives subprojects recursively
1010 def archive!
1010 def archive!
1011 children.each do |subproject|
1011 children.each do |subproject|
1012 subproject.send :archive!
1012 subproject.send :archive!
1013 end
1013 end
1014 update_attribute :status, STATUS_ARCHIVED
1014 update_attribute :status, STATUS_ARCHIVED
1015 end
1015 end
1016
1016
1017 def update_position_under_parent
1017 def update_position_under_parent
1018 set_or_update_position_under(parent)
1018 set_or_update_position_under(parent)
1019 end
1019 end
1020
1020
1021 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1021 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1022 def set_or_update_position_under(target_parent)
1022 def set_or_update_position_under(target_parent)
1023 parent_was = parent
1023 parent_was = parent
1024 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1024 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1025 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1025 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1026
1026
1027 if to_be_inserted_before
1027 if to_be_inserted_before
1028 move_to_left_of(to_be_inserted_before)
1028 move_to_left_of(to_be_inserted_before)
1029 elsif target_parent.nil?
1029 elsif target_parent.nil?
1030 if sibs.empty?
1030 if sibs.empty?
1031 # move_to_root adds the project in first (ie. left) position
1031 # move_to_root adds the project in first (ie. left) position
1032 move_to_root
1032 move_to_root
1033 else
1033 else
1034 move_to_right_of(sibs.last) unless self == sibs.last
1034 move_to_right_of(sibs.last) unless self == sibs.last
1035 end
1035 end
1036 else
1036 else
1037 # move_to_child_of adds the project in last (ie.right) position
1037 # move_to_child_of adds the project in last (ie.right) position
1038 move_to_child_of(target_parent)
1038 move_to_child_of(target_parent)
1039 end
1039 end
1040 if parent_was != target_parent
1040 if parent_was != target_parent
1041 after_parent_changed(parent_was)
1041 after_parent_changed(parent_was)
1042 end
1042 end
1043 end
1043 end
1044 end
1044 end
@@ -1,727 +1,734
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 # Different ways of displaying/sorting users
23 # Different ways of displaying/sorting users
24 USER_FORMATS = {
24 USER_FORMATS = {
25 :firstname_lastname => {
25 :firstname_lastname => {
26 :string => '#{firstname} #{lastname}',
26 :string => '#{firstname} #{lastname}',
27 :order => %w(firstname lastname id),
27 :order => %w(firstname lastname id),
28 :setting_order => 1
28 :setting_order => 1
29 },
29 },
30 :firstname_lastinitial => {
30 :firstname_lastinitial => {
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 :order => %w(firstname lastname id),
32 :order => %w(firstname lastname id),
33 :setting_order => 2
33 :setting_order => 2
34 },
34 },
35 :firstname => {
35 :firstname => {
36 :string => '#{firstname}',
36 :string => '#{firstname}',
37 :order => %w(firstname id),
37 :order => %w(firstname id),
38 :setting_order => 3
38 :setting_order => 3
39 },
39 },
40 :lastname_firstname => {
40 :lastname_firstname => {
41 :string => '#{lastname} #{firstname}',
41 :string => '#{lastname} #{firstname}',
42 :order => %w(lastname firstname id),
42 :order => %w(lastname firstname id),
43 :setting_order => 4
43 :setting_order => 4
44 },
44 },
45 :lastname_coma_firstname => {
45 :lastname_coma_firstname => {
46 :string => '#{lastname}, #{firstname}',
46 :string => '#{lastname}, #{firstname}',
47 :order => %w(lastname firstname id),
47 :order => %w(lastname firstname id),
48 :setting_order => 5
48 :setting_order => 5
49 },
49 },
50 :lastname => {
50 :lastname => {
51 :string => '#{lastname}',
51 :string => '#{lastname}',
52 :order => %w(lastname id),
52 :order => %w(lastname id),
53 :setting_order => 6
53 :setting_order => 6
54 },
54 },
55 :username => {
55 :username => {
56 :string => '#{login}',
56 :string => '#{login}',
57 :order => %w(login id),
57 :order => %w(login id),
58 :setting_order => 7
58 :setting_order => 7
59 },
59 },
60 }
60 }
61
61
62 MAIL_NOTIFICATION_OPTIONS = [
62 MAIL_NOTIFICATION_OPTIONS = [
63 ['all', :label_user_mail_option_all],
63 ['all', :label_user_mail_option_all],
64 ['selected', :label_user_mail_option_selected],
64 ['selected', :label_user_mail_option_selected],
65 ['only_my_events', :label_user_mail_option_only_my_events],
65 ['only_my_events', :label_user_mail_option_only_my_events],
66 ['only_assigned', :label_user_mail_option_only_assigned],
66 ['only_assigned', :label_user_mail_option_only_assigned],
67 ['only_owner', :label_user_mail_option_only_owner],
67 ['only_owner', :label_user_mail_option_only_owner],
68 ['none', :label_user_mail_option_none]
68 ['none', :label_user_mail_option_none]
69 ]
69 ]
70
70
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
73 has_many :changesets, :dependent => :nullify
73 has_many :changesets, :dependent => :nullify
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
77 belongs_to :auth_source
77 belongs_to :auth_source
78
78
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
81
81
82 acts_as_customizable
82 acts_as_customizable
83
83
84 attr_accessor :password, :password_confirmation, :generate_password
84 attr_accessor :password, :password_confirmation, :generate_password
85 attr_accessor :last_before_login_on
85 attr_accessor :last_before_login_on
86 # Prevents unauthorized assignments
86 # Prevents unauthorized assignments
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
88
88
89 LOGIN_LENGTH_LIMIT = 60
89 LOGIN_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
91
91
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
95 # Login must contain letters, numbers, underscores only
95 # Login must contain letters, numbers, underscores only
96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
98 validates_length_of :firstname, :lastname, :maximum => 30
98 validates_length_of :firstname, :lastname, :maximum => 30
99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
103 validate :validate_password_length
103 validate :validate_password_length
104
104
105 before_create :set_mail_notification
105 before_create :set_mail_notification
106 before_save :generate_password_if_needed, :update_hashed_password
106 before_save :generate_password_if_needed, :update_hashed_password
107 before_destroy :remove_references_before_destroy
107 before_destroy :remove_references_before_destroy
108 after_save :update_notified_project_ids
108 after_save :update_notified_project_ids
109
109
110 scope :in_group, lambda {|group|
110 scope :in_group, lambda {|group|
111 group_id = group.is_a?(Group) ? group.id : group.to_i
111 group_id = group.is_a?(Group) ? group.id : group.to_i
112 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
112 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
113 }
113 }
114 scope :not_in_group, lambda {|group|
114 scope :not_in_group, lambda {|group|
115 group_id = group.is_a?(Group) ? group.id : group.to_i
115 group_id = group.is_a?(Group) ? group.id : group.to_i
116 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
116 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
117 }
117 }
118 scope :sorted, lambda { order(*User.fields_for_order_statement)}
118 scope :sorted, lambda { order(*User.fields_for_order_statement)}
119
119
120 def set_mail_notification
120 def set_mail_notification
121 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
121 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
122 true
122 true
123 end
123 end
124
124
125 def update_hashed_password
125 def update_hashed_password
126 # update hashed_password if password was set
126 # update hashed_password if password was set
127 if self.password && self.auth_source_id.blank?
127 if self.password && self.auth_source_id.blank?
128 salt_password(password)
128 salt_password(password)
129 end
129 end
130 end
130 end
131
131
132 alias :base_reload :reload
132 alias :base_reload :reload
133 def reload(*args)
133 def reload(*args)
134 @name = nil
134 @name = nil
135 @projects_by_role = nil
135 @projects_by_role = nil
136 @membership_by_project_id = nil
136 @membership_by_project_id = nil
137 @notified_projects_ids = nil
137 @notified_projects_ids = nil
138 @notified_projects_ids_changed = false
138 @notified_projects_ids_changed = false
139 base_reload(*args)
139 base_reload(*args)
140 end
140 end
141
141
142 def mail=(arg)
142 def mail=(arg)
143 write_attribute(:mail, arg.to_s.strip)
143 write_attribute(:mail, arg.to_s.strip)
144 end
144 end
145
145
146 def identity_url=(url)
146 def identity_url=(url)
147 if url.blank?
147 if url.blank?
148 write_attribute(:identity_url, '')
148 write_attribute(:identity_url, '')
149 else
149 else
150 begin
150 begin
151 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
151 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
152 rescue OpenIdAuthentication::InvalidOpenId
152 rescue OpenIdAuthentication::InvalidOpenId
153 # Invalid url, don't save
153 # Invalid url, don't save
154 end
154 end
155 end
155 end
156 self.read_attribute(:identity_url)
156 self.read_attribute(:identity_url)
157 end
157 end
158
158
159 # Returns the user that matches provided login and password, or nil
159 # Returns the user that matches provided login and password, or nil
160 def self.try_to_login(login, password, active_only=true)
160 def self.try_to_login(login, password, active_only=true)
161 login = login.to_s
161 login = login.to_s
162 password = password.to_s
162 password = password.to_s
163
163
164 # Make sure no one can sign in with an empty login or password
164 # Make sure no one can sign in with an empty login or password
165 return nil if login.empty? || password.empty?
165 return nil if login.empty? || password.empty?
166 user = find_by_login(login)
166 user = find_by_login(login)
167 if user
167 if user
168 # user is already in local database
168 # user is already in local database
169 return nil unless user.check_password?(password)
169 return nil unless user.check_password?(password)
170 return nil if !user.active? && active_only
170 return nil if !user.active? && active_only
171 else
171 else
172 # user is not yet registered, try to authenticate with available sources
172 # user is not yet registered, try to authenticate with available sources
173 attrs = AuthSource.authenticate(login, password)
173 attrs = AuthSource.authenticate(login, password)
174 if attrs
174 if attrs
175 user = new(attrs)
175 user = new(attrs)
176 user.login = login
176 user.login = login
177 user.language = Setting.default_language
177 user.language = Setting.default_language
178 if user.save
178 if user.save
179 user.reload
179 user.reload
180 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
180 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
181 end
181 end
182 end
182 end
183 end
183 end
184 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
184 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
185 user
185 user
186 rescue => text
186 rescue => text
187 raise text
187 raise text
188 end
188 end
189
189
190 # Returns the user who matches the given autologin +key+ or nil
190 # Returns the user who matches the given autologin +key+ or nil
191 def self.try_to_autologin(key)
191 def self.try_to_autologin(key)
192 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
192 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
193 if user
193 if user
194 user.update_column(:last_login_on, Time.now)
194 user.update_column(:last_login_on, Time.now)
195 user
195 user
196 end
196 end
197 end
197 end
198
198
199 def self.name_formatter(formatter = nil)
199 def self.name_formatter(formatter = nil)
200 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
200 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
201 end
201 end
202
202
203 # Returns an array of fields names than can be used to make an order statement for users
203 # Returns an array of fields names than can be used to make an order statement for users
204 # according to how user names are displayed
204 # according to how user names are displayed
205 # Examples:
205 # Examples:
206 #
206 #
207 # User.fields_for_order_statement => ['users.login', 'users.id']
207 # User.fields_for_order_statement => ['users.login', 'users.id']
208 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
208 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
209 def self.fields_for_order_statement(table=nil)
209 def self.fields_for_order_statement(table=nil)
210 table ||= table_name
210 table ||= table_name
211 name_formatter[:order].map {|field| "#{table}.#{field}"}
211 name_formatter[:order].map {|field| "#{table}.#{field}"}
212 end
212 end
213
213
214 # Return user's full name for display
214 # Return user's full name for display
215 def name(formatter = nil)
215 def name(formatter = nil)
216 f = self.class.name_formatter(formatter)
216 f = self.class.name_formatter(formatter)
217 if formatter
217 if formatter
218 eval('"' + f[:string] + '"')
218 eval('"' + f[:string] + '"')
219 else
219 else
220 @name ||= eval('"' + f[:string] + '"')
220 @name ||= eval('"' + f[:string] + '"')
221 end
221 end
222 end
222 end
223
223
224 def active?
224 def active?
225 self.status == STATUS_ACTIVE
225 self.status == STATUS_ACTIVE
226 end
226 end
227
227
228 def registered?
228 def registered?
229 self.status == STATUS_REGISTERED
229 self.status == STATUS_REGISTERED
230 end
230 end
231
231
232 def locked?
232 def locked?
233 self.status == STATUS_LOCKED
233 self.status == STATUS_LOCKED
234 end
234 end
235
235
236 def activate
236 def activate
237 self.status = STATUS_ACTIVE
237 self.status = STATUS_ACTIVE
238 end
238 end
239
239
240 def register
240 def register
241 self.status = STATUS_REGISTERED
241 self.status = STATUS_REGISTERED
242 end
242 end
243
243
244 def lock
244 def lock
245 self.status = STATUS_LOCKED
245 self.status = STATUS_LOCKED
246 end
246 end
247
247
248 def activate!
248 def activate!
249 update_attribute(:status, STATUS_ACTIVE)
249 update_attribute(:status, STATUS_ACTIVE)
250 end
250 end
251
251
252 def register!
252 def register!
253 update_attribute(:status, STATUS_REGISTERED)
253 update_attribute(:status, STATUS_REGISTERED)
254 end
254 end
255
255
256 def lock!
256 def lock!
257 update_attribute(:status, STATUS_LOCKED)
257 update_attribute(:status, STATUS_LOCKED)
258 end
258 end
259
259
260 # Returns true if +clear_password+ is the correct user's password, otherwise false
260 # Returns true if +clear_password+ is the correct user's password, otherwise false
261 def check_password?(clear_password)
261 def check_password?(clear_password)
262 if auth_source_id.present?
262 if auth_source_id.present?
263 auth_source.authenticate(self.login, clear_password)
263 auth_source.authenticate(self.login, clear_password)
264 else
264 else
265 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
265 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
266 end
266 end
267 end
267 end
268
268
269 # Generates a random salt and computes hashed_password for +clear_password+
269 # Generates a random salt and computes hashed_password for +clear_password+
270 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
270 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
271 def salt_password(clear_password)
271 def salt_password(clear_password)
272 self.salt = User.generate_salt
272 self.salt = User.generate_salt
273 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
273 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
274 end
274 end
275
275
276 # Does the backend storage allow this user to change their password?
276 # Does the backend storage allow this user to change their password?
277 def change_password_allowed?
277 def change_password_allowed?
278 return true if auth_source.nil?
278 return true if auth_source.nil?
279 return auth_source.allow_password_changes?
279 return auth_source.allow_password_changes?
280 end
280 end
281
281
282 def generate_password?
282 def generate_password?
283 generate_password == '1' || generate_password == true
283 generate_password == '1' || generate_password == true
284 end
284 end
285
285
286 # Generate and set a random password on given length
286 # Generate and set a random password on given length
287 def random_password(length=40)
287 def random_password(length=40)
288 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
288 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
289 chars -= %w(0 O 1 l)
289 chars -= %w(0 O 1 l)
290 password = ''
290 password = ''
291 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
291 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
292 self.password = password
292 self.password = password
293 self.password_confirmation = password
293 self.password_confirmation = password
294 self
294 self
295 end
295 end
296
296
297 def pref
297 def pref
298 self.preference ||= UserPreference.new(:user => self)
298 self.preference ||= UserPreference.new(:user => self)
299 end
299 end
300
300
301 def time_zone
301 def time_zone
302 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
302 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
303 end
303 end
304
304
305 def wants_comments_in_reverse_order?
305 def wants_comments_in_reverse_order?
306 self.pref[:comments_sorting] == 'desc'
306 self.pref[:comments_sorting] == 'desc'
307 end
307 end
308
308
309 # Return user's RSS key (a 40 chars long string), used to access feeds
309 # Return user's RSS key (a 40 chars long string), used to access feeds
310 def rss_key
310 def rss_key
311 if rss_token.nil?
311 if rss_token.nil?
312 create_rss_token(:action => 'feeds')
312 create_rss_token(:action => 'feeds')
313 end
313 end
314 rss_token.value
314 rss_token.value
315 end
315 end
316
316
317 # Return user's API key (a 40 chars long string), used to access the API
317 # Return user's API key (a 40 chars long string), used to access the API
318 def api_key
318 def api_key
319 if api_token.nil?
319 if api_token.nil?
320 create_api_token(:action => 'api')
320 create_api_token(:action => 'api')
321 end
321 end
322 api_token.value
322 api_token.value
323 end
323 end
324
324
325 # Return an array of project ids for which the user has explicitly turned mail notifications on
325 # Return an array of project ids for which the user has explicitly turned mail notifications on
326 def notified_projects_ids
326 def notified_projects_ids
327 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
327 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
328 end
328 end
329
329
330 def notified_project_ids=(ids)
330 def notified_project_ids=(ids)
331 @notified_projects_ids_changed = true
331 @notified_projects_ids_changed = true
332 @notified_projects_ids = ids
332 @notified_projects_ids = ids
333 end
333 end
334
334
335 # Updates per project notifications (after_save callback)
335 # Updates per project notifications (after_save callback)
336 def update_notified_project_ids
336 def update_notified_project_ids
337 if @notified_projects_ids_changed
337 if @notified_projects_ids_changed
338 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
338 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
339 members.update_all(:mail_notification => false)
339 members.update_all(:mail_notification => false)
340 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
340 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
341 end
341 end
342 end
342 end
343 private :update_notified_project_ids
343 private :update_notified_project_ids
344
344
345 def valid_notification_options
345 def valid_notification_options
346 self.class.valid_notification_options(self)
346 self.class.valid_notification_options(self)
347 end
347 end
348
348
349 # Only users that belong to more than 1 project can select projects for which they are notified
349 # Only users that belong to more than 1 project can select projects for which they are notified
350 def self.valid_notification_options(user=nil)
350 def self.valid_notification_options(user=nil)
351 # Note that @user.membership.size would fail since AR ignores
351 # Note that @user.membership.size would fail since AR ignores
352 # :include association option when doing a count
352 # :include association option when doing a count
353 if user.nil? || user.memberships.length < 1
353 if user.nil? || user.memberships.length < 1
354 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
354 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
355 else
355 else
356 MAIL_NOTIFICATION_OPTIONS
356 MAIL_NOTIFICATION_OPTIONS
357 end
357 end
358 end
358 end
359
359
360 # Find a user account by matching the exact login and then a case-insensitive
360 # Find a user account by matching the exact login and then a case-insensitive
361 # version. Exact matches will be given priority.
361 # version. Exact matches will be given priority.
362 def self.find_by_login(login)
362 def self.find_by_login(login)
363 if login.present?
363 if login.present?
364 login = login.to_s
364 login = login.to_s
365 # First look for an exact match
365 # First look for an exact match
366 user = where(:login => login).all.detect {|u| u.login == login}
366 user = where(:login => login).all.detect {|u| u.login == login}
367 unless user
367 unless user
368 # Fail over to case-insensitive if none was found
368 # Fail over to case-insensitive if none was found
369 user = where("LOWER(login) = ?", login.downcase).first
369 user = where("LOWER(login) = ?", login.downcase).first
370 end
370 end
371 user
371 user
372 end
372 end
373 end
373 end
374
374
375 def self.find_by_rss_key(key)
375 def self.find_by_rss_key(key)
376 Token.find_active_user('feeds', key)
376 Token.find_active_user('feeds', key)
377 end
377 end
378
378
379 def self.find_by_api_key(key)
379 def self.find_by_api_key(key)
380 Token.find_active_user('api', key)
380 Token.find_active_user('api', key)
381 end
381 end
382
382
383 # Makes find_by_mail case-insensitive
383 # Makes find_by_mail case-insensitive
384 def self.find_by_mail(mail)
384 def self.find_by_mail(mail)
385 where("LOWER(mail) = ?", mail.to_s.downcase).first
385 where("LOWER(mail) = ?", mail.to_s.downcase).first
386 end
386 end
387
387
388 # Returns true if the default admin account can no longer be used
388 # Returns true if the default admin account can no longer be used
389 def self.default_admin_account_changed?
389 def self.default_admin_account_changed?
390 !User.active.find_by_login("admin").try(:check_password?, "admin")
390 !User.active.find_by_login("admin").try(:check_password?, "admin")
391 end
391 end
392
392
393 def to_s
393 def to_s
394 name
394 name
395 end
395 end
396
396
397 CSS_CLASS_BY_STATUS = {
397 CSS_CLASS_BY_STATUS = {
398 STATUS_ANONYMOUS => 'anon',
398 STATUS_ANONYMOUS => 'anon',
399 STATUS_ACTIVE => 'active',
399 STATUS_ACTIVE => 'active',
400 STATUS_REGISTERED => 'registered',
400 STATUS_REGISTERED => 'registered',
401 STATUS_LOCKED => 'locked'
401 STATUS_LOCKED => 'locked'
402 }
402 }
403
403
404 def css_classes
404 def css_classes
405 "user #{CSS_CLASS_BY_STATUS[status]}"
405 "user #{CSS_CLASS_BY_STATUS[status]}"
406 end
406 end
407
407
408 # Returns the current day according to user's time zone
408 # Returns the current day according to user's time zone
409 def today
409 def today
410 if time_zone.nil?
410 if time_zone.nil?
411 Date.today
411 Date.today
412 else
412 else
413 Time.now.in_time_zone(time_zone).to_date
413 Time.now.in_time_zone(time_zone).to_date
414 end
414 end
415 end
415 end
416
416
417 # Returns the day of +time+ according to user's time zone
417 # Returns the day of +time+ according to user's time zone
418 def time_to_date(time)
418 def time_to_date(time)
419 if time_zone.nil?
419 if time_zone.nil?
420 time.to_date
420 time.to_date
421 else
421 else
422 time.in_time_zone(time_zone).to_date
422 time.in_time_zone(time_zone).to_date
423 end
423 end
424 end
424 end
425
425
426 def logged?
426 def logged?
427 true
427 true
428 end
428 end
429
429
430 def anonymous?
430 def anonymous?
431 !logged?
431 !logged?
432 end
432 end
433
433
434 # Returns user's membership for the given project
434 # Returns user's membership for the given project
435 # or nil if the user is not a member of project
435 # or nil if the user is not a member of project
436 def membership(project)
436 def membership(project)
437 project_id = project.is_a?(Project) ? project.id : project
437 project_id = project.is_a?(Project) ? project.id : project
438
438
439 @membership_by_project_id ||= Hash.new {|h, project_id|
439 @membership_by_project_id ||= Hash.new {|h, project_id|
440 h[project_id] = memberships.where(:project_id => project_id).first
440 h[project_id] = memberships.where(:project_id => project_id).first
441 }
441 }
442 @membership_by_project_id[project_id]
442 @membership_by_project_id[project_id]
443 end
443 end
444
444
445 # Returns the user's bult-in role
446 def builtin_role
447 if logged?
448 @role_non_member ||= Role.non_member
449 else
450 @role_anonymous ||= Role.anonymous
451 end
452 end
453
445 # Return user's roles for project
454 # Return user's roles for project
446 def roles_for_project(project)
455 def roles_for_project(project)
447 roles = []
456 roles = []
448 # No role on archived projects
457 # No role on archived projects
449 return roles if project.nil? || project.archived?
458 return roles if project.nil? || project.archived?
450 if logged?
459 if logged?
451 # Find project membership
460 # Find project membership
452 membership = membership(project)
461 membership = membership(project)
453 if membership
462 if membership
454 roles = membership.roles
463 roles = membership.roles
455 else
464 else
456 @role_non_member ||= Role.non_member
465 roles << builtin_role
457 roles << @role_non_member
458 end
466 end
459 else
467 else
460 @role_anonymous ||= Role.anonymous
468 roles << builtin_role
461 roles << @role_anonymous
462 end
469 end
463 roles
470 roles
464 end
471 end
465
472
466 # Return true if the user is a member of project
473 # Return true if the user is a member of project
467 def member_of?(project)
474 def member_of?(project)
468 projects.to_a.include?(project)
475 projects.to_a.include?(project)
469 end
476 end
470
477
471 # Returns a hash of user's projects grouped by roles
478 # Returns a hash of user's projects grouped by roles
472 def projects_by_role
479 def projects_by_role
473 return @projects_by_role if @projects_by_role
480 return @projects_by_role if @projects_by_role
474
481
475 @projects_by_role = Hash.new([])
482 @projects_by_role = Hash.new([])
476 memberships.each do |membership|
483 memberships.each do |membership|
477 if membership.project
484 if membership.project
478 membership.roles.each do |role|
485 membership.roles.each do |role|
479 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
486 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
480 @projects_by_role[role] << membership.project
487 @projects_by_role[role] << membership.project
481 end
488 end
482 end
489 end
483 end
490 end
484 @projects_by_role.each do |role, projects|
491 @projects_by_role.each do |role, projects|
485 projects.uniq!
492 projects.uniq!
486 end
493 end
487
494
488 @projects_by_role
495 @projects_by_role
489 end
496 end
490
497
491 # Returns true if user is arg or belongs to arg
498 # Returns true if user is arg or belongs to arg
492 def is_or_belongs_to?(arg)
499 def is_or_belongs_to?(arg)
493 if arg.is_a?(User)
500 if arg.is_a?(User)
494 self == arg
501 self == arg
495 elsif arg.is_a?(Group)
502 elsif arg.is_a?(Group)
496 arg.users.include?(self)
503 arg.users.include?(self)
497 else
504 else
498 false
505 false
499 end
506 end
500 end
507 end
501
508
502 # Return true if the user is allowed to do the specified action on a specific context
509 # Return true if the user is allowed to do the specified action on a specific context
503 # Action can be:
510 # Action can be:
504 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
511 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
505 # * a permission Symbol (eg. :edit_project)
512 # * a permission Symbol (eg. :edit_project)
506 # Context can be:
513 # Context can be:
507 # * a project : returns true if user is allowed to do the specified action on this project
514 # * a project : returns true if user is allowed to do the specified action on this project
508 # * an array of projects : returns true if user is allowed on every project
515 # * an array of projects : returns true if user is allowed on every project
509 # * nil with options[:global] set : check if user has at least one role allowed for this action,
516 # * nil with options[:global] set : check if user has at least one role allowed for this action,
510 # or falls back to Non Member / Anonymous permissions depending if the user is logged
517 # or falls back to Non Member / Anonymous permissions depending if the user is logged
511 def allowed_to?(action, context, options={}, &block)
518 def allowed_to?(action, context, options={}, &block)
512 if context && context.is_a?(Project)
519 if context && context.is_a?(Project)
513 return false unless context.allows_to?(action)
520 return false unless context.allows_to?(action)
514 # Admin users are authorized for anything else
521 # Admin users are authorized for anything else
515 return true if admin?
522 return true if admin?
516
523
517 roles = roles_for_project(context)
524 roles = roles_for_project(context)
518 return false unless roles
525 return false unless roles
519 roles.any? {|role|
526 roles.any? {|role|
520 (context.is_public? || role.member?) &&
527 (context.is_public? || role.member?) &&
521 role.allowed_to?(action) &&
528 role.allowed_to?(action) &&
522 (block_given? ? yield(role, self) : true)
529 (block_given? ? yield(role, self) : true)
523 }
530 }
524 elsif context && context.is_a?(Array)
531 elsif context && context.is_a?(Array)
525 if context.empty?
532 if context.empty?
526 false
533 false
527 else
534 else
528 # Authorize if user is authorized on every element of the array
535 # Authorize if user is authorized on every element of the array
529 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
536 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
530 end
537 end
531 elsif options[:global]
538 elsif options[:global]
532 # Admin users are always authorized
539 # Admin users are always authorized
533 return true if admin?
540 return true if admin?
534
541
535 # authorize if user has at least one role that has this permission
542 # authorize if user has at least one role that has this permission
536 roles = memberships.collect {|m| m.roles}.flatten.uniq
543 roles = memberships.collect {|m| m.roles}.flatten.uniq
537 roles << (self.logged? ? Role.non_member : Role.anonymous)
544 roles << (self.logged? ? Role.non_member : Role.anonymous)
538 roles.any? {|role|
545 roles.any? {|role|
539 role.allowed_to?(action) &&
546 role.allowed_to?(action) &&
540 (block_given? ? yield(role, self) : true)
547 (block_given? ? yield(role, self) : true)
541 }
548 }
542 else
549 else
543 false
550 false
544 end
551 end
545 end
552 end
546
553
547 # Is the user allowed to do the specified action on any project?
554 # Is the user allowed to do the specified action on any project?
548 # See allowed_to? for the actions and valid options.
555 # See allowed_to? for the actions and valid options.
549 def allowed_to_globally?(action, options, &block)
556 def allowed_to_globally?(action, options, &block)
550 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
557 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
551 end
558 end
552
559
553 # Returns true if the user is allowed to delete his own account
560 # Returns true if the user is allowed to delete his own account
554 def own_account_deletable?
561 def own_account_deletable?
555 Setting.unsubscribe? &&
562 Setting.unsubscribe? &&
556 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
563 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
557 end
564 end
558
565
559 safe_attributes 'login',
566 safe_attributes 'login',
560 'firstname',
567 'firstname',
561 'lastname',
568 'lastname',
562 'mail',
569 'mail',
563 'mail_notification',
570 'mail_notification',
564 'notified_project_ids',
571 'notified_project_ids',
565 'language',
572 'language',
566 'custom_field_values',
573 'custom_field_values',
567 'custom_fields',
574 'custom_fields',
568 'identity_url'
575 'identity_url'
569
576
570 safe_attributes 'status',
577 safe_attributes 'status',
571 'auth_source_id',
578 'auth_source_id',
572 'generate_password',
579 'generate_password',
573 :if => lambda {|user, current_user| current_user.admin?}
580 :if => lambda {|user, current_user| current_user.admin?}
574
581
575 safe_attributes 'group_ids',
582 safe_attributes 'group_ids',
576 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
583 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
577
584
578 # Utility method to help check if a user should be notified about an
585 # Utility method to help check if a user should be notified about an
579 # event.
586 # event.
580 #
587 #
581 # TODO: only supports Issue events currently
588 # TODO: only supports Issue events currently
582 def notify_about?(object)
589 def notify_about?(object)
583 if mail_notification == 'all'
590 if mail_notification == 'all'
584 true
591 true
585 elsif mail_notification.blank? || mail_notification == 'none'
592 elsif mail_notification.blank? || mail_notification == 'none'
586 false
593 false
587 else
594 else
588 case object
595 case object
589 when Issue
596 when Issue
590 case mail_notification
597 case mail_notification
591 when 'selected', 'only_my_events'
598 when 'selected', 'only_my_events'
592 # user receives notifications for created/assigned issues on unselected projects
599 # user receives notifications for created/assigned issues on unselected projects
593 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
600 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
594 when 'only_assigned'
601 when 'only_assigned'
595 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
602 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
596 when 'only_owner'
603 when 'only_owner'
597 object.author == self
604 object.author == self
598 end
605 end
599 when News
606 when News
600 # always send to project members except when mail_notification is set to 'none'
607 # always send to project members except when mail_notification is set to 'none'
601 true
608 true
602 end
609 end
603 end
610 end
604 end
611 end
605
612
606 def self.current=(user)
613 def self.current=(user)
607 Thread.current[:current_user] = user
614 Thread.current[:current_user] = user
608 end
615 end
609
616
610 def self.current
617 def self.current
611 Thread.current[:current_user] ||= User.anonymous
618 Thread.current[:current_user] ||= User.anonymous
612 end
619 end
613
620
614 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
621 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
615 # one anonymous user per database.
622 # one anonymous user per database.
616 def self.anonymous
623 def self.anonymous
617 anonymous_user = AnonymousUser.first
624 anonymous_user = AnonymousUser.first
618 if anonymous_user.nil?
625 if anonymous_user.nil?
619 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
626 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
620 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
627 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
621 end
628 end
622 anonymous_user
629 anonymous_user
623 end
630 end
624
631
625 # Salts all existing unsalted passwords
632 # Salts all existing unsalted passwords
626 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
633 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
627 # This method is used in the SaltPasswords migration and is to be kept as is
634 # This method is used in the SaltPasswords migration and is to be kept as is
628 def self.salt_unsalted_passwords!
635 def self.salt_unsalted_passwords!
629 transaction do
636 transaction do
630 User.where("salt IS NULL OR salt = ''").find_each do |user|
637 User.where("salt IS NULL OR salt = ''").find_each do |user|
631 next if user.hashed_password.blank?
638 next if user.hashed_password.blank?
632 salt = User.generate_salt
639 salt = User.generate_salt
633 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
640 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
634 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
641 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
635 end
642 end
636 end
643 end
637 end
644 end
638
645
639 protected
646 protected
640
647
641 def validate_password_length
648 def validate_password_length
642 return if password.blank? && generate_password?
649 return if password.blank? && generate_password?
643 # Password length validation based on setting
650 # Password length validation based on setting
644 if !password.nil? && password.size < Setting.password_min_length.to_i
651 if !password.nil? && password.size < Setting.password_min_length.to_i
645 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
652 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
646 end
653 end
647 end
654 end
648
655
649 private
656 private
650
657
651 def generate_password_if_needed
658 def generate_password_if_needed
652 if generate_password? && auth_source.nil?
659 if generate_password? && auth_source.nil?
653 length = [Setting.password_min_length.to_i + 2, 10].max
660 length = [Setting.password_min_length.to_i + 2, 10].max
654 random_password(length)
661 random_password(length)
655 end
662 end
656 end
663 end
657
664
658 # Removes references that are not handled by associations
665 # Removes references that are not handled by associations
659 # Things that are not deleted are reassociated with the anonymous user
666 # Things that are not deleted are reassociated with the anonymous user
660 def remove_references_before_destroy
667 def remove_references_before_destroy
661 return if self.id.nil?
668 return if self.id.nil?
662
669
663 substitute = User.anonymous
670 substitute = User.anonymous
664 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
671 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
665 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
672 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
666 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
673 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
667 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
674 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
668 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
675 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
669 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
676 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
670 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
677 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
671 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
678 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
672 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
679 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
673 # Remove private queries and keep public ones
680 # Remove private queries and keep public ones
674 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
681 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
675 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
682 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
676 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
683 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
677 Token.delete_all ['user_id = ?', id]
684 Token.delete_all ['user_id = ?', id]
678 Watcher.delete_all ['user_id = ?', id]
685 Watcher.delete_all ['user_id = ?', id]
679 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
686 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
680 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
687 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
681 end
688 end
682
689
683 # Return password digest
690 # Return password digest
684 def self.hash_password(clear_password)
691 def self.hash_password(clear_password)
685 Digest::SHA1.hexdigest(clear_password || "")
692 Digest::SHA1.hexdigest(clear_password || "")
686 end
693 end
687
694
688 # Returns a 128bits random salt as a hex string (32 chars long)
695 # Returns a 128bits random salt as a hex string (32 chars long)
689 def self.generate_salt
696 def self.generate_salt
690 Redmine::Utils.random_hex(16)
697 Redmine::Utils.random_hex(16)
691 end
698 end
692
699
693 end
700 end
694
701
695 class AnonymousUser < User
702 class AnonymousUser < User
696 validate :validate_anonymous_uniqueness, :on => :create
703 validate :validate_anonymous_uniqueness, :on => :create
697
704
698 def validate_anonymous_uniqueness
705 def validate_anonymous_uniqueness
699 # There should be only one AnonymousUser in the database
706 # There should be only one AnonymousUser in the database
700 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
707 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
701 end
708 end
702
709
703 def available_custom_fields
710 def available_custom_fields
704 []
711 []
705 end
712 end
706
713
707 # Overrides a few properties
714 # Overrides a few properties
708 def logged?; false end
715 def logged?; false end
709 def admin; false end
716 def admin; false end
710 def name(*args); I18n.t(:label_user_anonymous) end
717 def name(*args); I18n.t(:label_user_anonymous) end
711 def mail; nil end
718 def mail; nil end
712 def time_zone; nil end
719 def time_zone; nil end
713 def rss_key; nil end
720 def rss_key; nil end
714
721
715 def pref
722 def pref
716 UserPreference.new(:user => self)
723 UserPreference.new(:user => self)
717 end
724 end
718
725
719 def member_of?(project)
726 def member_of?(project)
720 false
727 false
721 end
728 end
722
729
723 # Anonymous user can not be destroyed
730 # Anonymous user can not be destroyed
724 def destroy
731 def destroy
725 false
732 false
726 end
733 end
727 end
734 end
General Comments 0
You need to be logged in to leave comments. Login now