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