##// END OF EJS Templates
Merged r11497 from trunk (#13329)....
Jean-Philippe Lang -
r11344:53680edb2d80
parent child
Show More
@@ -1,1433 +1,1434
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 include Redmine::Utils::DateCalculation
20 include Redmine::Utils::DateCalculation
21
21
22 belongs_to :project
22 belongs_to :project
23 belongs_to :tracker
23 belongs_to :tracker
24 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
25 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
26 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
27 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
28 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
29 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
30
30
31 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :journals, :as => :journalized, :dependent => :destroy
32 has_many :visible_journals,
32 has_many :visible_journals,
33 :class_name => 'Journal',
33 :class_name => 'Journal',
34 :as => :journalized,
34 :as => :journalized,
35 :conditions => Proc.new {
35 :conditions => Proc.new {
36 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
36 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
37 },
37 },
38 :readonly => true
38 :readonly => true
39
39
40 has_many :time_entries, :dependent => :delete_all
40 has_many :time_entries, :dependent => :delete_all
41 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
41 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
42
42
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45
45
46 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
46 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
47 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
48 acts_as_customizable
48 acts_as_customizable
49 acts_as_watchable
49 acts_as_watchable
50 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
50 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
51 :include => [:project, :visible_journals],
51 :include => [:project, :visible_journals],
52 # sort by id so that limited eager loading doesn't break with postgresql
52 # sort by id so that limited eager loading doesn't break with postgresql
53 :order_column => "#{table_name}.id"
53 :order_column => "#{table_name}.id"
54 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
55 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
56 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
57
57
58 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
58 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
59 :author_key => :author_id
59 :author_key => :author_id
60
60
61 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
62
62
63 attr_reader :current_journal
63 attr_reader :current_journal
64 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
65
65
66 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
66 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
67
67
68 validates_length_of :subject, :maximum => 255
68 validates_length_of :subject, :maximum => 255
69 validates_inclusion_of :done_ratio, :in => 0..100
69 validates_inclusion_of :done_ratio, :in => 0..100
70 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
70 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
71 validates :start_date, :date => true
71 validates :start_date, :date => true
72 validates :due_date, :date => true
72 validates :due_date, :date => true
73 validate :validate_issue, :validate_required_fields
73 validate :validate_issue, :validate_required_fields
74
74
75 scope :visible, lambda {|*args|
75 scope :visible, lambda {|*args|
76 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
76 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
77 }
77 }
78
78
79 scope :open, lambda {|*args|
79 scope :open, lambda {|*args|
80 is_closed = args.size > 0 ? !args.first : false
80 is_closed = args.size > 0 ? !args.first : false
81 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
81 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
82 }
82 }
83
83
84 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
84 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
85 scope :on_active_project, lambda {
85 scope :on_active_project, lambda {
86 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
86 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
87 }
87 }
88 scope :fixed_version, lambda {|versions|
88 scope :fixed_version, lambda {|versions|
89 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
89 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
90 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
90 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
91 }
91 }
92
92
93 before_create :default_assign
93 before_create :default_assign
94 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on
94 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on
95 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
95 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
96 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
96 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
97 # Should be after_create but would be called before previous after_save callbacks
97 # Should be after_create but would be called before previous after_save callbacks
98 after_save :after_create_from_copy
98 after_save :after_create_from_copy
99 after_destroy :update_parent_attributes
99 after_destroy :update_parent_attributes
100
100
101 # Returns a SQL conditions string used to find all issues visible by the specified user
101 # Returns a SQL conditions string used to find all issues visible by the specified user
102 def self.visible_condition(user, options={})
102 def self.visible_condition(user, options={})
103 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
103 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
104 if user.logged?
104 if user.logged?
105 case role.issues_visibility
105 case role.issues_visibility
106 when 'all'
106 when 'all'
107 nil
107 nil
108 when 'default'
108 when 'default'
109 user_ids = [user.id] + user.groups.map(&:id)
109 user_ids = [user.id] + user.groups.map(&:id)
110 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
110 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
111 when 'own'
111 when 'own'
112 user_ids = [user.id] + user.groups.map(&:id)
112 user_ids = [user.id] + user.groups.map(&:id)
113 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
113 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
114 else
114 else
115 '1=0'
115 '1=0'
116 end
116 end
117 else
117 else
118 "(#{table_name}.is_private = #{connection.quoted_false})"
118 "(#{table_name}.is_private = #{connection.quoted_false})"
119 end
119 end
120 end
120 end
121 end
121 end
122
122
123 # Returns true if usr or current user is allowed to view the issue
123 # Returns true if usr or current user is allowed to view the issue
124 def visible?(usr=nil)
124 def visible?(usr=nil)
125 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
125 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
126 if user.logged?
126 if user.logged?
127 case role.issues_visibility
127 case role.issues_visibility
128 when 'all'
128 when 'all'
129 true
129 true
130 when 'default'
130 when 'default'
131 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
131 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
132 when 'own'
132 when 'own'
133 self.author == user || user.is_or_belongs_to?(assigned_to)
133 self.author == user || user.is_or_belongs_to?(assigned_to)
134 else
134 else
135 false
135 false
136 end
136 end
137 else
137 else
138 !self.is_private?
138 !self.is_private?
139 end
139 end
140 end
140 end
141 end
141 end
142
142
143 # Returns true if user or current user is allowed to edit or add a note to the issue
143 # Returns true if user or current user is allowed to edit or add a note to the issue
144 def editable?(user=User.current)
144 def editable?(user=User.current)
145 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
145 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
146 end
146 end
147
147
148 def initialize(attributes=nil, *args)
148 def initialize(attributes=nil, *args)
149 super
149 super
150 if new_record?
150 if new_record?
151 # set default values for new records only
151 # set default values for new records only
152 self.status ||= IssueStatus.default
152 self.status ||= IssueStatus.default
153 self.priority ||= IssuePriority.default
153 self.priority ||= IssuePriority.default
154 self.watcher_user_ids = []
154 self.watcher_user_ids = []
155 end
155 end
156 end
156 end
157
157
158 def create_or_update
158 def create_or_update
159 super
159 super
160 ensure
160 ensure
161 @status_was = nil
161 @status_was = nil
162 end
162 end
163 private :create_or_update
163 private :create_or_update
164
164
165 # AR#Persistence#destroy would raise and RecordNotFound exception
165 # AR#Persistence#destroy would raise and RecordNotFound exception
166 # if the issue was already deleted or updated (non matching lock_version).
166 # if the issue was already deleted or updated (non matching lock_version).
167 # This is a problem when bulk deleting issues or deleting a project
167 # This is a problem when bulk deleting issues or deleting a project
168 # (because an issue may already be deleted if its parent was deleted
168 # (because an issue may already be deleted if its parent was deleted
169 # first).
169 # first).
170 # The issue is reloaded by the nested_set before being deleted so
170 # The issue is reloaded by the nested_set before being deleted so
171 # the lock_version condition should not be an issue but we handle it.
171 # the lock_version condition should not be an issue but we handle it.
172 def destroy
172 def destroy
173 super
173 super
174 rescue ActiveRecord::RecordNotFound
174 rescue ActiveRecord::RecordNotFound
175 # Stale or already deleted
175 # Stale or already deleted
176 begin
176 begin
177 reload
177 reload
178 rescue ActiveRecord::RecordNotFound
178 rescue ActiveRecord::RecordNotFound
179 # The issue was actually already deleted
179 # The issue was actually already deleted
180 @destroyed = true
180 @destroyed = true
181 return freeze
181 return freeze
182 end
182 end
183 # The issue was stale, retry to destroy
183 # The issue was stale, retry to destroy
184 super
184 super
185 end
185 end
186
186
187 alias :base_reload :reload
187 def reload(*args)
188 def reload(*args)
188 @workflow_rule_by_attribute = nil
189 @workflow_rule_by_attribute = nil
189 @assignable_versions = nil
190 @assignable_versions = nil
190 @relations = nil
191 @relations = nil
191 super
192 base_reload(*args)
192 end
193 end
193
194
194 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
195 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
195 def available_custom_fields
196 def available_custom_fields
196 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
197 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
197 end
198 end
198
199
199 # Copies attributes from another issue, arg can be an id or an Issue
200 # Copies attributes from another issue, arg can be an id or an Issue
200 def copy_from(arg, options={})
201 def copy_from(arg, options={})
201 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
202 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
202 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
203 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
203 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
204 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
204 self.status = issue.status
205 self.status = issue.status
205 self.author = User.current
206 self.author = User.current
206 unless options[:attachments] == false
207 unless options[:attachments] == false
207 self.attachments = issue.attachments.map do |attachement|
208 self.attachments = issue.attachments.map do |attachement|
208 attachement.copy(:container => self)
209 attachement.copy(:container => self)
209 end
210 end
210 end
211 end
211 @copied_from = issue
212 @copied_from = issue
212 @copy_options = options
213 @copy_options = options
213 self
214 self
214 end
215 end
215
216
216 # Returns an unsaved copy of the issue
217 # Returns an unsaved copy of the issue
217 def copy(attributes=nil, copy_options={})
218 def copy(attributes=nil, copy_options={})
218 copy = self.class.new.copy_from(self, copy_options)
219 copy = self.class.new.copy_from(self, copy_options)
219 copy.attributes = attributes if attributes
220 copy.attributes = attributes if attributes
220 copy
221 copy
221 end
222 end
222
223
223 # Returns true if the issue is a copy
224 # Returns true if the issue is a copy
224 def copy?
225 def copy?
225 @copied_from.present?
226 @copied_from.present?
226 end
227 end
227
228
228 # Moves/copies an issue to a new project and tracker
229 # Moves/copies an issue to a new project and tracker
229 # Returns the moved/copied issue on success, false on failure
230 # Returns the moved/copied issue on success, false on failure
230 def move_to_project(new_project, new_tracker=nil, options={})
231 def move_to_project(new_project, new_tracker=nil, options={})
231 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
232 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
232
233
233 if options[:copy]
234 if options[:copy]
234 issue = self.copy
235 issue = self.copy
235 else
236 else
236 issue = self
237 issue = self
237 end
238 end
238
239
239 issue.init_journal(User.current, options[:notes])
240 issue.init_journal(User.current, options[:notes])
240
241
241 # Preserve previous behaviour
242 # Preserve previous behaviour
242 # #move_to_project doesn't change tracker automatically
243 # #move_to_project doesn't change tracker automatically
243 issue.send :project=, new_project, true
244 issue.send :project=, new_project, true
244 if new_tracker
245 if new_tracker
245 issue.tracker = new_tracker
246 issue.tracker = new_tracker
246 end
247 end
247 # Allow bulk setting of attributes on the issue
248 # Allow bulk setting of attributes on the issue
248 if options[:attributes]
249 if options[:attributes]
249 issue.attributes = options[:attributes]
250 issue.attributes = options[:attributes]
250 end
251 end
251
252
252 issue.save ? issue : false
253 issue.save ? issue : false
253 end
254 end
254
255
255 def status_id=(sid)
256 def status_id=(sid)
256 self.status = nil
257 self.status = nil
257 result = write_attribute(:status_id, sid)
258 result = write_attribute(:status_id, sid)
258 @workflow_rule_by_attribute = nil
259 @workflow_rule_by_attribute = nil
259 result
260 result
260 end
261 end
261
262
262 def priority_id=(pid)
263 def priority_id=(pid)
263 self.priority = nil
264 self.priority = nil
264 write_attribute(:priority_id, pid)
265 write_attribute(:priority_id, pid)
265 end
266 end
266
267
267 def category_id=(cid)
268 def category_id=(cid)
268 self.category = nil
269 self.category = nil
269 write_attribute(:category_id, cid)
270 write_attribute(:category_id, cid)
270 end
271 end
271
272
272 def fixed_version_id=(vid)
273 def fixed_version_id=(vid)
273 self.fixed_version = nil
274 self.fixed_version = nil
274 write_attribute(:fixed_version_id, vid)
275 write_attribute(:fixed_version_id, vid)
275 end
276 end
276
277
277 def tracker_id=(tid)
278 def tracker_id=(tid)
278 self.tracker = nil
279 self.tracker = nil
279 result = write_attribute(:tracker_id, tid)
280 result = write_attribute(:tracker_id, tid)
280 @custom_field_values = nil
281 @custom_field_values = nil
281 @workflow_rule_by_attribute = nil
282 @workflow_rule_by_attribute = nil
282 result
283 result
283 end
284 end
284
285
285 def project_id=(project_id)
286 def project_id=(project_id)
286 if project_id.to_s != self.project_id.to_s
287 if project_id.to_s != self.project_id.to_s
287 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
288 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
288 end
289 end
289 end
290 end
290
291
291 def project=(project, keep_tracker=false)
292 def project=(project, keep_tracker=false)
292 project_was = self.project
293 project_was = self.project
293 write_attribute(:project_id, project ? project.id : nil)
294 write_attribute(:project_id, project ? project.id : nil)
294 association_instance_set('project', project)
295 association_instance_set('project', project)
295 if project_was && project && project_was != project
296 if project_was && project && project_was != project
296 @assignable_versions = nil
297 @assignable_versions = nil
297
298
298 unless keep_tracker || project.trackers.include?(tracker)
299 unless keep_tracker || project.trackers.include?(tracker)
299 self.tracker = project.trackers.first
300 self.tracker = project.trackers.first
300 end
301 end
301 # Reassign to the category with same name if any
302 # Reassign to the category with same name if any
302 if category
303 if category
303 self.category = project.issue_categories.find_by_name(category.name)
304 self.category = project.issue_categories.find_by_name(category.name)
304 end
305 end
305 # Keep the fixed_version if it's still valid in the new_project
306 # Keep the fixed_version if it's still valid in the new_project
306 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
307 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
307 self.fixed_version = nil
308 self.fixed_version = nil
308 end
309 end
309 # Clear the parent task if it's no longer valid
310 # Clear the parent task if it's no longer valid
310 unless valid_parent_project?
311 unless valid_parent_project?
311 self.parent_issue_id = nil
312 self.parent_issue_id = nil
312 end
313 end
313 @custom_field_values = nil
314 @custom_field_values = nil
314 end
315 end
315 end
316 end
316
317
317 def description=(arg)
318 def description=(arg)
318 if arg.is_a?(String)
319 if arg.is_a?(String)
319 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
320 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
320 end
321 end
321 write_attribute(:description, arg)
322 write_attribute(:description, arg)
322 end
323 end
323
324
324 # Overrides assign_attributes so that project and tracker get assigned first
325 # Overrides assign_attributes so that project and tracker get assigned first
325 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
326 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
326 return if new_attributes.nil?
327 return if new_attributes.nil?
327 attrs = new_attributes.dup
328 attrs = new_attributes.dup
328 attrs.stringify_keys!
329 attrs.stringify_keys!
329
330
330 %w(project project_id tracker tracker_id).each do |attr|
331 %w(project project_id tracker tracker_id).each do |attr|
331 if attrs.has_key?(attr)
332 if attrs.has_key?(attr)
332 send "#{attr}=", attrs.delete(attr)
333 send "#{attr}=", attrs.delete(attr)
333 end
334 end
334 end
335 end
335 send :assign_attributes_without_project_and_tracker_first, attrs, *args
336 send :assign_attributes_without_project_and_tracker_first, attrs, *args
336 end
337 end
337 # Do not redefine alias chain on reload (see #4838)
338 # Do not redefine alias chain on reload (see #4838)
338 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
339 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
339
340
340 def estimated_hours=(h)
341 def estimated_hours=(h)
341 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
342 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
342 end
343 end
343
344
344 safe_attributes 'project_id',
345 safe_attributes 'project_id',
345 :if => lambda {|issue, user|
346 :if => lambda {|issue, user|
346 if issue.new_record?
347 if issue.new_record?
347 issue.copy?
348 issue.copy?
348 elsif user.allowed_to?(:move_issues, issue.project)
349 elsif user.allowed_to?(:move_issues, issue.project)
349 projects = Issue.allowed_target_projects_on_move(user)
350 projects = Issue.allowed_target_projects_on_move(user)
350 projects.include?(issue.project) && projects.size > 1
351 projects.include?(issue.project) && projects.size > 1
351 end
352 end
352 }
353 }
353
354
354 safe_attributes 'tracker_id',
355 safe_attributes 'tracker_id',
355 'status_id',
356 'status_id',
356 'category_id',
357 'category_id',
357 'assigned_to_id',
358 'assigned_to_id',
358 'priority_id',
359 'priority_id',
359 'fixed_version_id',
360 'fixed_version_id',
360 'subject',
361 'subject',
361 'description',
362 'description',
362 'start_date',
363 'start_date',
363 'due_date',
364 'due_date',
364 'done_ratio',
365 'done_ratio',
365 'estimated_hours',
366 'estimated_hours',
366 'custom_field_values',
367 'custom_field_values',
367 'custom_fields',
368 'custom_fields',
368 'lock_version',
369 'lock_version',
369 'notes',
370 'notes',
370 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
371 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
371
372
372 safe_attributes 'status_id',
373 safe_attributes 'status_id',
373 'assigned_to_id',
374 'assigned_to_id',
374 'fixed_version_id',
375 'fixed_version_id',
375 'done_ratio',
376 'done_ratio',
376 'lock_version',
377 'lock_version',
377 'notes',
378 'notes',
378 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
379 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
379
380
380 safe_attributes 'notes',
381 safe_attributes 'notes',
381 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
382 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
382
383
383 safe_attributes 'private_notes',
384 safe_attributes 'private_notes',
384 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
385 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
385
386
386 safe_attributes 'watcher_user_ids',
387 safe_attributes 'watcher_user_ids',
387 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
388 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
388
389
389 safe_attributes 'is_private',
390 safe_attributes 'is_private',
390 :if => lambda {|issue, user|
391 :if => lambda {|issue, user|
391 user.allowed_to?(:set_issues_private, issue.project) ||
392 user.allowed_to?(:set_issues_private, issue.project) ||
392 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
393 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
393 }
394 }
394
395
395 safe_attributes 'parent_issue_id',
396 safe_attributes 'parent_issue_id',
396 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
397 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
397 user.allowed_to?(:manage_subtasks, issue.project)}
398 user.allowed_to?(:manage_subtasks, issue.project)}
398
399
399 def safe_attribute_names(user=nil)
400 def safe_attribute_names(user=nil)
400 names = super
401 names = super
401 names -= disabled_core_fields
402 names -= disabled_core_fields
402 names -= read_only_attribute_names(user)
403 names -= read_only_attribute_names(user)
403 names
404 names
404 end
405 end
405
406
406 # Safely sets attributes
407 # Safely sets attributes
407 # Should be called from controllers instead of #attributes=
408 # Should be called from controllers instead of #attributes=
408 # attr_accessible is too rough because we still want things like
409 # attr_accessible is too rough because we still want things like
409 # Issue.new(:project => foo) to work
410 # Issue.new(:project => foo) to work
410 def safe_attributes=(attrs, user=User.current)
411 def safe_attributes=(attrs, user=User.current)
411 return unless attrs.is_a?(Hash)
412 return unless attrs.is_a?(Hash)
412
413
413 attrs = attrs.dup
414 attrs = attrs.dup
414
415
415 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
416 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
416 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
417 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
417 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
418 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
418 self.project_id = p
419 self.project_id = p
419 end
420 end
420 end
421 end
421
422
422 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
423 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
423 self.tracker_id = t
424 self.tracker_id = t
424 end
425 end
425
426
426 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
427 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
427 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
428 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
428 self.status_id = s
429 self.status_id = s
429 end
430 end
430 end
431 end
431
432
432 attrs = delete_unsafe_attributes(attrs, user)
433 attrs = delete_unsafe_attributes(attrs, user)
433 return if attrs.empty?
434 return if attrs.empty?
434
435
435 unless leaf?
436 unless leaf?
436 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
437 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
437 end
438 end
438
439
439 if attrs['parent_issue_id'].present?
440 if attrs['parent_issue_id'].present?
440 s = attrs['parent_issue_id'].to_s
441 s = attrs['parent_issue_id'].to_s
441 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
442 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
442 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
443 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
443 end
444 end
444 end
445 end
445
446
446 if attrs['custom_field_values'].present?
447 if attrs['custom_field_values'].present?
447 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
448 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
448 end
449 end
449
450
450 if attrs['custom_fields'].present?
451 if attrs['custom_fields'].present?
451 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
452 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
452 end
453 end
453
454
454 # mass-assignment security bypass
455 # mass-assignment security bypass
455 assign_attributes attrs, :without_protection => true
456 assign_attributes attrs, :without_protection => true
456 end
457 end
457
458
458 def disabled_core_fields
459 def disabled_core_fields
459 tracker ? tracker.disabled_core_fields : []
460 tracker ? tracker.disabled_core_fields : []
460 end
461 end
461
462
462 # Returns the custom_field_values that can be edited by the given user
463 # Returns the custom_field_values that can be edited by the given user
463 def editable_custom_field_values(user=nil)
464 def editable_custom_field_values(user=nil)
464 custom_field_values.reject do |value|
465 custom_field_values.reject do |value|
465 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
466 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
466 end
467 end
467 end
468 end
468
469
469 # Returns the names of attributes that are read-only for user or the current user
470 # Returns the names of attributes that are read-only for user or the current user
470 # For users with multiple roles, the read-only fields are the intersection of
471 # For users with multiple roles, the read-only fields are the intersection of
471 # read-only fields of each role
472 # read-only fields of each role
472 # The result is an array of strings where sustom fields are represented with their ids
473 # The result is an array of strings where sustom fields are represented with their ids
473 #
474 #
474 # Examples:
475 # Examples:
475 # issue.read_only_attribute_names # => ['due_date', '2']
476 # issue.read_only_attribute_names # => ['due_date', '2']
476 # issue.read_only_attribute_names(user) # => []
477 # issue.read_only_attribute_names(user) # => []
477 def read_only_attribute_names(user=nil)
478 def read_only_attribute_names(user=nil)
478 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
479 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
479 end
480 end
480
481
481 # Returns the names of required attributes for user or the current user
482 # Returns the names of required attributes for user or the current user
482 # For users with multiple roles, the required fields are the intersection of
483 # For users with multiple roles, the required fields are the intersection of
483 # required fields of each role
484 # required fields of each role
484 # The result is an array of strings where sustom fields are represented with their ids
485 # The result is an array of strings where sustom fields are represented with their ids
485 #
486 #
486 # Examples:
487 # Examples:
487 # issue.required_attribute_names # => ['due_date', '2']
488 # issue.required_attribute_names # => ['due_date', '2']
488 # issue.required_attribute_names(user) # => []
489 # issue.required_attribute_names(user) # => []
489 def required_attribute_names(user=nil)
490 def required_attribute_names(user=nil)
490 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
491 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
491 end
492 end
492
493
493 # Returns true if the attribute is required for user
494 # Returns true if the attribute is required for user
494 def required_attribute?(name, user=nil)
495 def required_attribute?(name, user=nil)
495 required_attribute_names(user).include?(name.to_s)
496 required_attribute_names(user).include?(name.to_s)
496 end
497 end
497
498
498 # Returns a hash of the workflow rule by attribute for the given user
499 # Returns a hash of the workflow rule by attribute for the given user
499 #
500 #
500 # Examples:
501 # Examples:
501 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
502 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
502 def workflow_rule_by_attribute(user=nil)
503 def workflow_rule_by_attribute(user=nil)
503 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
504 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
504
505
505 user_real = user || User.current
506 user_real = user || User.current
506 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
507 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
507 return {} if roles.empty?
508 return {} if roles.empty?
508
509
509 result = {}
510 result = {}
510 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
511 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
511 if workflow_permissions.any?
512 if workflow_permissions.any?
512 workflow_rules = workflow_permissions.inject({}) do |h, wp|
513 workflow_rules = workflow_permissions.inject({}) do |h, wp|
513 h[wp.field_name] ||= []
514 h[wp.field_name] ||= []
514 h[wp.field_name] << wp.rule
515 h[wp.field_name] << wp.rule
515 h
516 h
516 end
517 end
517 workflow_rules.each do |attr, rules|
518 workflow_rules.each do |attr, rules|
518 next if rules.size < roles.size
519 next if rules.size < roles.size
519 uniq_rules = rules.uniq
520 uniq_rules = rules.uniq
520 if uniq_rules.size == 1
521 if uniq_rules.size == 1
521 result[attr] = uniq_rules.first
522 result[attr] = uniq_rules.first
522 else
523 else
523 result[attr] = 'required'
524 result[attr] = 'required'
524 end
525 end
525 end
526 end
526 end
527 end
527 @workflow_rule_by_attribute = result if user.nil?
528 @workflow_rule_by_attribute = result if user.nil?
528 result
529 result
529 end
530 end
530 private :workflow_rule_by_attribute
531 private :workflow_rule_by_attribute
531
532
532 def done_ratio
533 def done_ratio
533 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
534 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
534 status.default_done_ratio
535 status.default_done_ratio
535 else
536 else
536 read_attribute(:done_ratio)
537 read_attribute(:done_ratio)
537 end
538 end
538 end
539 end
539
540
540 def self.use_status_for_done_ratio?
541 def self.use_status_for_done_ratio?
541 Setting.issue_done_ratio == 'issue_status'
542 Setting.issue_done_ratio == 'issue_status'
542 end
543 end
543
544
544 def self.use_field_for_done_ratio?
545 def self.use_field_for_done_ratio?
545 Setting.issue_done_ratio == 'issue_field'
546 Setting.issue_done_ratio == 'issue_field'
546 end
547 end
547
548
548 def validate_issue
549 def validate_issue
549 if due_date && start_date && due_date < start_date
550 if due_date && start_date && due_date < start_date
550 errors.add :due_date, :greater_than_start_date
551 errors.add :due_date, :greater_than_start_date
551 end
552 end
552
553
553 if start_date && soonest_start && start_date < soonest_start
554 if start_date && soonest_start && start_date < soonest_start
554 errors.add :start_date, :invalid
555 errors.add :start_date, :invalid
555 end
556 end
556
557
557 if fixed_version
558 if fixed_version
558 if !assignable_versions.include?(fixed_version)
559 if !assignable_versions.include?(fixed_version)
559 errors.add :fixed_version_id, :inclusion
560 errors.add :fixed_version_id, :inclusion
560 elsif reopened? && fixed_version.closed?
561 elsif reopened? && fixed_version.closed?
561 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
562 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
562 end
563 end
563 end
564 end
564
565
565 # Checks that the issue can not be added/moved to a disabled tracker
566 # Checks that the issue can not be added/moved to a disabled tracker
566 if project && (tracker_id_changed? || project_id_changed?)
567 if project && (tracker_id_changed? || project_id_changed?)
567 unless project.trackers.include?(tracker)
568 unless project.trackers.include?(tracker)
568 errors.add :tracker_id, :inclusion
569 errors.add :tracker_id, :inclusion
569 end
570 end
570 end
571 end
571
572
572 # Checks parent issue assignment
573 # Checks parent issue assignment
573 if @invalid_parent_issue_id.present?
574 if @invalid_parent_issue_id.present?
574 errors.add :parent_issue_id, :invalid
575 errors.add :parent_issue_id, :invalid
575 elsif @parent_issue
576 elsif @parent_issue
576 if !valid_parent_project?(@parent_issue)
577 if !valid_parent_project?(@parent_issue)
577 errors.add :parent_issue_id, :invalid
578 errors.add :parent_issue_id, :invalid
578 elsif !new_record?
579 elsif !new_record?
579 # moving an existing issue
580 # moving an existing issue
580 if @parent_issue.root_id != root_id
581 if @parent_issue.root_id != root_id
581 # we can always move to another tree
582 # we can always move to another tree
582 elsif move_possible?(@parent_issue)
583 elsif move_possible?(@parent_issue)
583 # move accepted inside tree
584 # move accepted inside tree
584 else
585 else
585 errors.add :parent_issue_id, :invalid
586 errors.add :parent_issue_id, :invalid
586 end
587 end
587 end
588 end
588 end
589 end
589 end
590 end
590
591
591 # Validates the issue against additional workflow requirements
592 # Validates the issue against additional workflow requirements
592 def validate_required_fields
593 def validate_required_fields
593 user = new_record? ? author : current_journal.try(:user)
594 user = new_record? ? author : current_journal.try(:user)
594
595
595 required_attribute_names(user).each do |attribute|
596 required_attribute_names(user).each do |attribute|
596 if attribute =~ /^\d+$/
597 if attribute =~ /^\d+$/
597 attribute = attribute.to_i
598 attribute = attribute.to_i
598 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
599 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
599 if v && v.value.blank?
600 if v && v.value.blank?
600 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
601 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
601 end
602 end
602 else
603 else
603 if respond_to?(attribute) && send(attribute).blank?
604 if respond_to?(attribute) && send(attribute).blank?
604 errors.add attribute, :blank
605 errors.add attribute, :blank
605 end
606 end
606 end
607 end
607 end
608 end
608 end
609 end
609
610
610 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
611 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
611 # even if the user turns off the setting later
612 # even if the user turns off the setting later
612 def update_done_ratio_from_issue_status
613 def update_done_ratio_from_issue_status
613 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
614 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
614 self.done_ratio = status.default_done_ratio
615 self.done_ratio = status.default_done_ratio
615 end
616 end
616 end
617 end
617
618
618 def init_journal(user, notes = "")
619 def init_journal(user, notes = "")
619 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
620 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
620 if new_record?
621 if new_record?
621 @current_journal.notify = false
622 @current_journal.notify = false
622 else
623 else
623 @attributes_before_change = attributes.dup
624 @attributes_before_change = attributes.dup
624 @custom_values_before_change = {}
625 @custom_values_before_change = {}
625 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
626 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
626 end
627 end
627 @current_journal
628 @current_journal
628 end
629 end
629
630
630 # Returns the id of the last journal or nil
631 # Returns the id of the last journal or nil
631 def last_journal_id
632 def last_journal_id
632 if new_record?
633 if new_record?
633 nil
634 nil
634 else
635 else
635 journals.maximum(:id)
636 journals.maximum(:id)
636 end
637 end
637 end
638 end
638
639
639 # Returns a scope for journals that have an id greater than journal_id
640 # Returns a scope for journals that have an id greater than journal_id
640 def journals_after(journal_id)
641 def journals_after(journal_id)
641 scope = journals.reorder("#{Journal.table_name}.id ASC")
642 scope = journals.reorder("#{Journal.table_name}.id ASC")
642 if journal_id.present?
643 if journal_id.present?
643 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
644 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
644 end
645 end
645 scope
646 scope
646 end
647 end
647
648
648 # Returns the initial status of the issue
649 # Returns the initial status of the issue
649 # Returns nil for a new issue
650 # Returns nil for a new issue
650 def status_was
651 def status_was
651 if status_id_was && status_id_was.to_i > 0
652 if status_id_was && status_id_was.to_i > 0
652 @status_was ||= IssueStatus.find_by_id(status_id_was)
653 @status_was ||= IssueStatus.find_by_id(status_id_was)
653 end
654 end
654 end
655 end
655
656
656 # Return true if the issue is closed, otherwise false
657 # Return true if the issue is closed, otherwise false
657 def closed?
658 def closed?
658 self.status.is_closed?
659 self.status.is_closed?
659 end
660 end
660
661
661 # Return true if the issue is being reopened
662 # Return true if the issue is being reopened
662 def reopened?
663 def reopened?
663 if !new_record? && status_id_changed?
664 if !new_record? && status_id_changed?
664 status_was = IssueStatus.find_by_id(status_id_was)
665 status_was = IssueStatus.find_by_id(status_id_was)
665 status_new = IssueStatus.find_by_id(status_id)
666 status_new = IssueStatus.find_by_id(status_id)
666 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
667 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
667 return true
668 return true
668 end
669 end
669 end
670 end
670 false
671 false
671 end
672 end
672
673
673 # Return true if the issue is being closed
674 # Return true if the issue is being closed
674 def closing?
675 def closing?
675 if !new_record? && status_id_changed?
676 if !new_record? && status_id_changed?
676 if status_was && status && !status_was.is_closed? && status.is_closed?
677 if status_was && status && !status_was.is_closed? && status.is_closed?
677 return true
678 return true
678 end
679 end
679 end
680 end
680 false
681 false
681 end
682 end
682
683
683 # Returns true if the issue is overdue
684 # Returns true if the issue is overdue
684 def overdue?
685 def overdue?
685 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
686 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
686 end
687 end
687
688
688 # Is the amount of work done less than it should for the due date
689 # Is the amount of work done less than it should for the due date
689 def behind_schedule?
690 def behind_schedule?
690 return false if start_date.nil? || due_date.nil?
691 return false if start_date.nil? || due_date.nil?
691 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
692 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
692 return done_date <= Date.today
693 return done_date <= Date.today
693 end
694 end
694
695
695 # Does this issue have children?
696 # Does this issue have children?
696 def children?
697 def children?
697 !leaf?
698 !leaf?
698 end
699 end
699
700
700 # Users the issue can be assigned to
701 # Users the issue can be assigned to
701 def assignable_users
702 def assignable_users
702 users = project.assignable_users
703 users = project.assignable_users
703 users << author if author
704 users << author if author
704 users << assigned_to if assigned_to
705 users << assigned_to if assigned_to
705 users.uniq.sort
706 users.uniq.sort
706 end
707 end
707
708
708 # Versions that the issue can be assigned to
709 # Versions that the issue can be assigned to
709 def assignable_versions
710 def assignable_versions
710 return @assignable_versions if @assignable_versions
711 return @assignable_versions if @assignable_versions
711
712
712 versions = project.shared_versions.open.all
713 versions = project.shared_versions.open.all
713 if fixed_version
714 if fixed_version
714 if fixed_version_id_changed?
715 if fixed_version_id_changed?
715 # nothing to do
716 # nothing to do
716 elsif project_id_changed?
717 elsif project_id_changed?
717 if project.shared_versions.include?(fixed_version)
718 if project.shared_versions.include?(fixed_version)
718 versions << fixed_version
719 versions << fixed_version
719 end
720 end
720 else
721 else
721 versions << fixed_version
722 versions << fixed_version
722 end
723 end
723 end
724 end
724 @assignable_versions = versions.uniq.sort
725 @assignable_versions = versions.uniq.sort
725 end
726 end
726
727
727 # Returns true if this issue is blocked by another issue that is still open
728 # Returns true if this issue is blocked by another issue that is still open
728 def blocked?
729 def blocked?
729 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
730 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
730 end
731 end
731
732
732 # Returns an array of statuses that user is able to apply
733 # Returns an array of statuses that user is able to apply
733 def new_statuses_allowed_to(user=User.current, include_default=false)
734 def new_statuses_allowed_to(user=User.current, include_default=false)
734 if new_record? && @copied_from
735 if new_record? && @copied_from
735 [IssueStatus.default, @copied_from.status].compact.uniq.sort
736 [IssueStatus.default, @copied_from.status].compact.uniq.sort
736 else
737 else
737 initial_status = nil
738 initial_status = nil
738 if new_record?
739 if new_record?
739 initial_status = IssueStatus.default
740 initial_status = IssueStatus.default
740 elsif status_id_was
741 elsif status_id_was
741 initial_status = IssueStatus.find_by_id(status_id_was)
742 initial_status = IssueStatus.find_by_id(status_id_was)
742 end
743 end
743 initial_status ||= status
744 initial_status ||= status
744
745
745 statuses = initial_status.find_new_statuses_allowed_to(
746 statuses = initial_status.find_new_statuses_allowed_to(
746 user.admin ? Role.all : user.roles_for_project(project),
747 user.admin ? Role.all : user.roles_for_project(project),
747 tracker,
748 tracker,
748 author == user,
749 author == user,
749 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
750 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
750 )
751 )
751 statuses << initial_status unless statuses.empty?
752 statuses << initial_status unless statuses.empty?
752 statuses << IssueStatus.default if include_default
753 statuses << IssueStatus.default if include_default
753 statuses = statuses.compact.uniq.sort
754 statuses = statuses.compact.uniq.sort
754 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
755 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
755 end
756 end
756 end
757 end
757
758
758 def assigned_to_was
759 def assigned_to_was
759 if assigned_to_id_changed? && assigned_to_id_was.present?
760 if assigned_to_id_changed? && assigned_to_id_was.present?
760 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
761 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
761 end
762 end
762 end
763 end
763
764
764 # Returns the users that should be notified
765 # Returns the users that should be notified
765 def notified_users
766 def notified_users
766 notified = []
767 notified = []
767 # Author and assignee are always notified unless they have been
768 # Author and assignee are always notified unless they have been
768 # locked or don't want to be notified
769 # locked or don't want to be notified
769 notified << author if author
770 notified << author if author
770 if assigned_to
771 if assigned_to
771 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
772 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
772 end
773 end
773 if assigned_to_was
774 if assigned_to_was
774 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
775 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
775 end
776 end
776 notified = notified.select {|u| u.active? && u.notify_about?(self)}
777 notified = notified.select {|u| u.active? && u.notify_about?(self)}
777
778
778 notified += project.notified_users
779 notified += project.notified_users
779 notified.uniq!
780 notified.uniq!
780 # Remove users that can not view the issue
781 # Remove users that can not view the issue
781 notified.reject! {|user| !visible?(user)}
782 notified.reject! {|user| !visible?(user)}
782 notified
783 notified
783 end
784 end
784
785
785 # Returns the email addresses that should be notified
786 # Returns the email addresses that should be notified
786 def recipients
787 def recipients
787 notified_users.collect(&:mail)
788 notified_users.collect(&:mail)
788 end
789 end
789
790
790 # Returns the number of hours spent on this issue
791 # Returns the number of hours spent on this issue
791 def spent_hours
792 def spent_hours
792 @spent_hours ||= time_entries.sum(:hours) || 0
793 @spent_hours ||= time_entries.sum(:hours) || 0
793 end
794 end
794
795
795 # Returns the total number of hours spent on this issue and its descendants
796 # Returns the total number of hours spent on this issue and its descendants
796 #
797 #
797 # Example:
798 # Example:
798 # spent_hours => 0.0
799 # spent_hours => 0.0
799 # spent_hours => 50.2
800 # spent_hours => 50.2
800 def total_spent_hours
801 def total_spent_hours
801 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
802 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
802 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
803 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
803 end
804 end
804
805
805 def relations
806 def relations
806 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
807 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
807 end
808 end
808
809
809 # Preloads relations for a collection of issues
810 # Preloads relations for a collection of issues
810 def self.load_relations(issues)
811 def self.load_relations(issues)
811 if issues.any?
812 if issues.any?
812 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
813 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
813 issues.each do |issue|
814 issues.each do |issue|
814 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
815 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
815 end
816 end
816 end
817 end
817 end
818 end
818
819
819 # Preloads visible spent time for a collection of issues
820 # Preloads visible spent time for a collection of issues
820 def self.load_visible_spent_hours(issues, user=User.current)
821 def self.load_visible_spent_hours(issues, user=User.current)
821 if issues.any?
822 if issues.any?
822 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
823 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
823 issues.each do |issue|
824 issues.each do |issue|
824 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
825 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
825 end
826 end
826 end
827 end
827 end
828 end
828
829
829 # Preloads visible relations for a collection of issues
830 # Preloads visible relations for a collection of issues
830 def self.load_visible_relations(issues, user=User.current)
831 def self.load_visible_relations(issues, user=User.current)
831 if issues.any?
832 if issues.any?
832 issue_ids = issues.map(&:id)
833 issue_ids = issues.map(&:id)
833 # Relations with issue_from in given issues and visible issue_to
834 # Relations with issue_from in given issues and visible issue_to
834 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
835 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
835 # Relations with issue_to in given issues and visible issue_from
836 # Relations with issue_to in given issues and visible issue_from
836 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
837 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
837
838
838 issues.each do |issue|
839 issues.each do |issue|
839 relations =
840 relations =
840 relations_from.select {|relation| relation.issue_from_id == issue.id} +
841 relations_from.select {|relation| relation.issue_from_id == issue.id} +
841 relations_to.select {|relation| relation.issue_to_id == issue.id}
842 relations_to.select {|relation| relation.issue_to_id == issue.id}
842
843
843 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
844 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
844 end
845 end
845 end
846 end
846 end
847 end
847
848
848 # Finds an issue relation given its id.
849 # Finds an issue relation given its id.
849 def find_relation(relation_id)
850 def find_relation(relation_id)
850 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
851 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
851 end
852 end
852
853
853 def all_dependent_issues(except=[])
854 def all_dependent_issues(except=[])
854 except << self
855 except << self
855 dependencies = []
856 dependencies = []
856 relations_from.each do |relation|
857 relations_from.each do |relation|
857 if relation.issue_to && !except.include?(relation.issue_to)
858 if relation.issue_to && !except.include?(relation.issue_to)
858 dependencies << relation.issue_to
859 dependencies << relation.issue_to
859 dependencies += relation.issue_to.all_dependent_issues(except)
860 dependencies += relation.issue_to.all_dependent_issues(except)
860 end
861 end
861 end
862 end
862 dependencies
863 dependencies
863 end
864 end
864
865
865 # Returns an array of issues that duplicate this one
866 # Returns an array of issues that duplicate this one
866 def duplicates
867 def duplicates
867 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
868 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
868 end
869 end
869
870
870 # Returns the due date or the target due date if any
871 # Returns the due date or the target due date if any
871 # Used on gantt chart
872 # Used on gantt chart
872 def due_before
873 def due_before
873 due_date || (fixed_version ? fixed_version.effective_date : nil)
874 due_date || (fixed_version ? fixed_version.effective_date : nil)
874 end
875 end
875
876
876 # Returns the time scheduled for this issue.
877 # Returns the time scheduled for this issue.
877 #
878 #
878 # Example:
879 # Example:
879 # Start Date: 2/26/09, End Date: 3/04/09
880 # Start Date: 2/26/09, End Date: 3/04/09
880 # duration => 6
881 # duration => 6
881 def duration
882 def duration
882 (start_date && due_date) ? due_date - start_date : 0
883 (start_date && due_date) ? due_date - start_date : 0
883 end
884 end
884
885
885 # Returns the duration in working days
886 # Returns the duration in working days
886 def working_duration
887 def working_duration
887 (start_date && due_date) ? working_days(start_date, due_date) : 0
888 (start_date && due_date) ? working_days(start_date, due_date) : 0
888 end
889 end
889
890
890 def soonest_start(reload=false)
891 def soonest_start(reload=false)
891 @soonest_start = nil if reload
892 @soonest_start = nil if reload
892 @soonest_start ||= (
893 @soonest_start ||= (
893 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
894 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
894 ancestors.collect(&:soonest_start)
895 ancestors.collect(&:soonest_start)
895 ).compact.max
896 ).compact.max
896 end
897 end
897
898
898 # Sets start_date on the given date or the next working day
899 # Sets start_date on the given date or the next working day
899 # and changes due_date to keep the same working duration.
900 # and changes due_date to keep the same working duration.
900 def reschedule_on(date)
901 def reschedule_on(date)
901 wd = working_duration
902 wd = working_duration
902 date = next_working_date(date)
903 date = next_working_date(date)
903 self.start_date = date
904 self.start_date = date
904 self.due_date = add_working_days(date, wd)
905 self.due_date = add_working_days(date, wd)
905 end
906 end
906
907
907 # Reschedules the issue on the given date or the next working day and saves the record.
908 # Reschedules the issue on the given date or the next working day and saves the record.
908 # If the issue is a parent task, this is done by rescheduling its subtasks.
909 # If the issue is a parent task, this is done by rescheduling its subtasks.
909 def reschedule_on!(date)
910 def reschedule_on!(date)
910 return if date.nil?
911 return if date.nil?
911 if leaf?
912 if leaf?
912 if start_date.nil? || start_date != date
913 if start_date.nil? || start_date != date
913 if start_date && start_date > date
914 if start_date && start_date > date
914 # Issue can not be moved earlier than its soonest start date
915 # Issue can not be moved earlier than its soonest start date
915 date = [soonest_start(true), date].compact.max
916 date = [soonest_start(true), date].compact.max
916 end
917 end
917 reschedule_on(date)
918 reschedule_on(date)
918 begin
919 begin
919 save
920 save
920 rescue ActiveRecord::StaleObjectError
921 rescue ActiveRecord::StaleObjectError
921 reload
922 reload
922 reschedule_on(date)
923 reschedule_on(date)
923 save
924 save
924 end
925 end
925 end
926 end
926 else
927 else
927 leaves.each do |leaf|
928 leaves.each do |leaf|
928 if leaf.start_date
929 if leaf.start_date
929 # Only move subtask if it starts at the same date as the parent
930 # Only move subtask if it starts at the same date as the parent
930 # or if it starts before the given date
931 # or if it starts before the given date
931 if start_date == leaf.start_date || date > leaf.start_date
932 if start_date == leaf.start_date || date > leaf.start_date
932 leaf.reschedule_on!(date)
933 leaf.reschedule_on!(date)
933 end
934 end
934 else
935 else
935 leaf.reschedule_on!(date)
936 leaf.reschedule_on!(date)
936 end
937 end
937 end
938 end
938 end
939 end
939 end
940 end
940
941
941 def <=>(issue)
942 def <=>(issue)
942 if issue.nil?
943 if issue.nil?
943 -1
944 -1
944 elsif root_id != issue.root_id
945 elsif root_id != issue.root_id
945 (root_id || 0) <=> (issue.root_id || 0)
946 (root_id || 0) <=> (issue.root_id || 0)
946 else
947 else
947 (lft || 0) <=> (issue.lft || 0)
948 (lft || 0) <=> (issue.lft || 0)
948 end
949 end
949 end
950 end
950
951
951 def to_s
952 def to_s
952 "#{tracker} ##{id}: #{subject}"
953 "#{tracker} ##{id}: #{subject}"
953 end
954 end
954
955
955 # Returns a string of css classes that apply to the issue
956 # Returns a string of css classes that apply to the issue
956 def css_classes
957 def css_classes
957 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
958 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
958 s << ' closed' if closed?
959 s << ' closed' if closed?
959 s << ' overdue' if overdue?
960 s << ' overdue' if overdue?
960 s << ' child' if child?
961 s << ' child' if child?
961 s << ' parent' unless leaf?
962 s << ' parent' unless leaf?
962 s << ' private' if is_private?
963 s << ' private' if is_private?
963 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
964 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
964 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
965 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
965 s
966 s
966 end
967 end
967
968
968 # Saves an issue and a time_entry from the parameters
969 # Saves an issue and a time_entry from the parameters
969 def save_issue_with_child_records(params, existing_time_entry=nil)
970 def save_issue_with_child_records(params, existing_time_entry=nil)
970 Issue.transaction do
971 Issue.transaction do
971 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
972 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
972 @time_entry = existing_time_entry || TimeEntry.new
973 @time_entry = existing_time_entry || TimeEntry.new
973 @time_entry.project = project
974 @time_entry.project = project
974 @time_entry.issue = self
975 @time_entry.issue = self
975 @time_entry.user = User.current
976 @time_entry.user = User.current
976 @time_entry.spent_on = User.current.today
977 @time_entry.spent_on = User.current.today
977 @time_entry.attributes = params[:time_entry]
978 @time_entry.attributes = params[:time_entry]
978 self.time_entries << @time_entry
979 self.time_entries << @time_entry
979 end
980 end
980
981
981 # TODO: Rename hook
982 # TODO: Rename hook
982 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
983 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
983 if save
984 if save
984 # TODO: Rename hook
985 # TODO: Rename hook
985 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
986 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
986 else
987 else
987 raise ActiveRecord::Rollback
988 raise ActiveRecord::Rollback
988 end
989 end
989 end
990 end
990 end
991 end
991
992
992 # Unassigns issues from +version+ if it's no longer shared with issue's project
993 # Unassigns issues from +version+ if it's no longer shared with issue's project
993 def self.update_versions_from_sharing_change(version)
994 def self.update_versions_from_sharing_change(version)
994 # Update issues assigned to the version
995 # Update issues assigned to the version
995 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
996 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
996 end
997 end
997
998
998 # Unassigns issues from versions that are no longer shared
999 # Unassigns issues from versions that are no longer shared
999 # after +project+ was moved
1000 # after +project+ was moved
1000 def self.update_versions_from_hierarchy_change(project)
1001 def self.update_versions_from_hierarchy_change(project)
1001 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1002 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1002 # Update issues of the moved projects and issues assigned to a version of a moved project
1003 # Update issues of the moved projects and issues assigned to a version of a moved project
1003 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1004 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1004 end
1005 end
1005
1006
1006 def parent_issue_id=(arg)
1007 def parent_issue_id=(arg)
1007 s = arg.to_s.strip.presence
1008 s = arg.to_s.strip.presence
1008 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1009 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1009 @parent_issue.id
1010 @parent_issue.id
1010 else
1011 else
1011 @parent_issue = nil
1012 @parent_issue = nil
1012 @invalid_parent_issue_id = arg
1013 @invalid_parent_issue_id = arg
1013 end
1014 end
1014 end
1015 end
1015
1016
1016 def parent_issue_id
1017 def parent_issue_id
1017 if @invalid_parent_issue_id
1018 if @invalid_parent_issue_id
1018 @invalid_parent_issue_id
1019 @invalid_parent_issue_id
1019 elsif instance_variable_defined? :@parent_issue
1020 elsif instance_variable_defined? :@parent_issue
1020 @parent_issue.nil? ? nil : @parent_issue.id
1021 @parent_issue.nil? ? nil : @parent_issue.id
1021 else
1022 else
1022 parent_id
1023 parent_id
1023 end
1024 end
1024 end
1025 end
1025
1026
1026 # Returns true if issue's project is a valid
1027 # Returns true if issue's project is a valid
1027 # parent issue project
1028 # parent issue project
1028 def valid_parent_project?(issue=parent)
1029 def valid_parent_project?(issue=parent)
1029 return true if issue.nil? || issue.project_id == project_id
1030 return true if issue.nil? || issue.project_id == project_id
1030
1031
1031 case Setting.cross_project_subtasks
1032 case Setting.cross_project_subtasks
1032 when 'system'
1033 when 'system'
1033 true
1034 true
1034 when 'tree'
1035 when 'tree'
1035 issue.project.root == project.root
1036 issue.project.root == project.root
1036 when 'hierarchy'
1037 when 'hierarchy'
1037 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1038 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1038 when 'descendants'
1039 when 'descendants'
1039 issue.project.is_or_is_ancestor_of?(project)
1040 issue.project.is_or_is_ancestor_of?(project)
1040 else
1041 else
1041 false
1042 false
1042 end
1043 end
1043 end
1044 end
1044
1045
1045 # Extracted from the ReportsController.
1046 # Extracted from the ReportsController.
1046 def self.by_tracker(project)
1047 def self.by_tracker(project)
1047 count_and_group_by(:project => project,
1048 count_and_group_by(:project => project,
1048 :field => 'tracker_id',
1049 :field => 'tracker_id',
1049 :joins => Tracker.table_name)
1050 :joins => Tracker.table_name)
1050 end
1051 end
1051
1052
1052 def self.by_version(project)
1053 def self.by_version(project)
1053 count_and_group_by(:project => project,
1054 count_and_group_by(:project => project,
1054 :field => 'fixed_version_id',
1055 :field => 'fixed_version_id',
1055 :joins => Version.table_name)
1056 :joins => Version.table_name)
1056 end
1057 end
1057
1058
1058 def self.by_priority(project)
1059 def self.by_priority(project)
1059 count_and_group_by(:project => project,
1060 count_and_group_by(:project => project,
1060 :field => 'priority_id',
1061 :field => 'priority_id',
1061 :joins => IssuePriority.table_name)
1062 :joins => IssuePriority.table_name)
1062 end
1063 end
1063
1064
1064 def self.by_category(project)
1065 def self.by_category(project)
1065 count_and_group_by(:project => project,
1066 count_and_group_by(:project => project,
1066 :field => 'category_id',
1067 :field => 'category_id',
1067 :joins => IssueCategory.table_name)
1068 :joins => IssueCategory.table_name)
1068 end
1069 end
1069
1070
1070 def self.by_assigned_to(project)
1071 def self.by_assigned_to(project)
1071 count_and_group_by(:project => project,
1072 count_and_group_by(:project => project,
1072 :field => 'assigned_to_id',
1073 :field => 'assigned_to_id',
1073 :joins => User.table_name)
1074 :joins => User.table_name)
1074 end
1075 end
1075
1076
1076 def self.by_author(project)
1077 def self.by_author(project)
1077 count_and_group_by(:project => project,
1078 count_and_group_by(:project => project,
1078 :field => 'author_id',
1079 :field => 'author_id',
1079 :joins => User.table_name)
1080 :joins => User.table_name)
1080 end
1081 end
1081
1082
1082 def self.by_subproject(project)
1083 def self.by_subproject(project)
1083 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1084 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1084 s.is_closed as closed,
1085 s.is_closed as closed,
1085 #{Issue.table_name}.project_id as project_id,
1086 #{Issue.table_name}.project_id as project_id,
1086 count(#{Issue.table_name}.id) as total
1087 count(#{Issue.table_name}.id) as total
1087 from
1088 from
1088 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1089 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1089 where
1090 where
1090 #{Issue.table_name}.status_id=s.id
1091 #{Issue.table_name}.status_id=s.id
1091 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1092 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1092 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1093 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1093 and #{Issue.table_name}.project_id <> #{project.id}
1094 and #{Issue.table_name}.project_id <> #{project.id}
1094 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1095 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1095 end
1096 end
1096 # End ReportsController extraction
1097 # End ReportsController extraction
1097
1098
1098 # Returns an array of projects that user can assign the issue to
1099 # Returns an array of projects that user can assign the issue to
1099 def allowed_target_projects(user=User.current)
1100 def allowed_target_projects(user=User.current)
1100 if new_record?
1101 if new_record?
1101 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
1102 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
1102 else
1103 else
1103 self.class.allowed_target_projects_on_move(user)
1104 self.class.allowed_target_projects_on_move(user)
1104 end
1105 end
1105 end
1106 end
1106
1107
1107 # Returns an array of projects that user can move issues to
1108 # Returns an array of projects that user can move issues to
1108 def self.allowed_target_projects_on_move(user=User.current)
1109 def self.allowed_target_projects_on_move(user=User.current)
1109 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
1110 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
1110 end
1111 end
1111
1112
1112 private
1113 private
1113
1114
1114 def after_project_change
1115 def after_project_change
1115 # Update project_id on related time entries
1116 # Update project_id on related time entries
1116 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1117 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1117
1118
1118 # Delete issue relations
1119 # Delete issue relations
1119 unless Setting.cross_project_issue_relations?
1120 unless Setting.cross_project_issue_relations?
1120 relations_from.clear
1121 relations_from.clear
1121 relations_to.clear
1122 relations_to.clear
1122 end
1123 end
1123
1124
1124 # Move subtasks that were in the same project
1125 # Move subtasks that were in the same project
1125 children.each do |child|
1126 children.each do |child|
1126 next unless child.project_id == project_id_was
1127 next unless child.project_id == project_id_was
1127 # Change project and keep project
1128 # Change project and keep project
1128 child.send :project=, project, true
1129 child.send :project=, project, true
1129 unless child.save
1130 unless child.save
1130 raise ActiveRecord::Rollback
1131 raise ActiveRecord::Rollback
1131 end
1132 end
1132 end
1133 end
1133 end
1134 end
1134
1135
1135 # Callback for after the creation of an issue by copy
1136 # Callback for after the creation of an issue by copy
1136 # * adds a "copied to" relation with the copied issue
1137 # * adds a "copied to" relation with the copied issue
1137 # * copies subtasks from the copied issue
1138 # * copies subtasks from the copied issue
1138 def after_create_from_copy
1139 def after_create_from_copy
1139 return unless copy? && !@after_create_from_copy_handled
1140 return unless copy? && !@after_create_from_copy_handled
1140
1141
1141 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1142 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1142 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1143 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1143 unless relation.save
1144 unless relation.save
1144 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1145 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1145 end
1146 end
1146 end
1147 end
1147
1148
1148 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1149 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1149 copy_options = (@copy_options || {}).merge(:subtasks => false)
1150 copy_options = (@copy_options || {}).merge(:subtasks => false)
1150 copied_issue_ids = {@copied_from.id => self.id}
1151 copied_issue_ids = {@copied_from.id => self.id}
1151 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1152 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1152 # Do not copy self when copying an issue as a descendant of the copied issue
1153 # Do not copy self when copying an issue as a descendant of the copied issue
1153 next if child == self
1154 next if child == self
1154 # Do not copy subtasks of issues that were not copied
1155 # Do not copy subtasks of issues that were not copied
1155 next unless copied_issue_ids[child.parent_id]
1156 next unless copied_issue_ids[child.parent_id]
1156 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1157 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1157 unless child.visible?
1158 unless child.visible?
1158 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1159 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1159 next
1160 next
1160 end
1161 end
1161 copy = Issue.new.copy_from(child, copy_options)
1162 copy = Issue.new.copy_from(child, copy_options)
1162 copy.author = author
1163 copy.author = author
1163 copy.project = project
1164 copy.project = project
1164 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1165 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1165 unless copy.save
1166 unless copy.save
1166 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1167 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1167 next
1168 next
1168 end
1169 end
1169 copied_issue_ids[child.id] = copy.id
1170 copied_issue_ids[child.id] = copy.id
1170 end
1171 end
1171 end
1172 end
1172 @after_create_from_copy_handled = true
1173 @after_create_from_copy_handled = true
1173 end
1174 end
1174
1175
1175 def update_nested_set_attributes
1176 def update_nested_set_attributes
1176 if root_id.nil?
1177 if root_id.nil?
1177 # issue was just created
1178 # issue was just created
1178 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1179 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1179 set_default_left_and_right
1180 set_default_left_and_right
1180 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1181 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1181 if @parent_issue
1182 if @parent_issue
1182 move_to_child_of(@parent_issue)
1183 move_to_child_of(@parent_issue)
1183 end
1184 end
1184 reload
1185 reload
1185 elsif parent_issue_id != parent_id
1186 elsif parent_issue_id != parent_id
1186 former_parent_id = parent_id
1187 former_parent_id = parent_id
1187 # moving an existing issue
1188 # moving an existing issue
1188 if @parent_issue && @parent_issue.root_id == root_id
1189 if @parent_issue && @parent_issue.root_id == root_id
1189 # inside the same tree
1190 # inside the same tree
1190 move_to_child_of(@parent_issue)
1191 move_to_child_of(@parent_issue)
1191 else
1192 else
1192 # to another tree
1193 # to another tree
1193 unless root?
1194 unless root?
1194 move_to_right_of(root)
1195 move_to_right_of(root)
1195 reload
1196 reload
1196 end
1197 end
1197 old_root_id = root_id
1198 old_root_id = root_id
1198 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1199 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1199 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1200 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1200 offset = target_maxright + 1 - lft
1201 offset = target_maxright + 1 - lft
1201 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1202 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1202 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1203 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1203 self[left_column_name] = lft + offset
1204 self[left_column_name] = lft + offset
1204 self[right_column_name] = rgt + offset
1205 self[right_column_name] = rgt + offset
1205 if @parent_issue
1206 if @parent_issue
1206 move_to_child_of(@parent_issue)
1207 move_to_child_of(@parent_issue)
1207 end
1208 end
1208 end
1209 end
1209 reload
1210 reload
1210 # delete invalid relations of all descendants
1211 # delete invalid relations of all descendants
1211 self_and_descendants.each do |issue|
1212 self_and_descendants.each do |issue|
1212 issue.relations.each do |relation|
1213 issue.relations.each do |relation|
1213 relation.destroy unless relation.valid?
1214 relation.destroy unless relation.valid?
1214 end
1215 end
1215 end
1216 end
1216 # update former parent
1217 # update former parent
1217 recalculate_attributes_for(former_parent_id) if former_parent_id
1218 recalculate_attributes_for(former_parent_id) if former_parent_id
1218 end
1219 end
1219 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1220 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1220 end
1221 end
1221
1222
1222 def update_parent_attributes
1223 def update_parent_attributes
1223 recalculate_attributes_for(parent_id) if parent_id
1224 recalculate_attributes_for(parent_id) if parent_id
1224 end
1225 end
1225
1226
1226 def recalculate_attributes_for(issue_id)
1227 def recalculate_attributes_for(issue_id)
1227 if issue_id && p = Issue.find_by_id(issue_id)
1228 if issue_id && p = Issue.find_by_id(issue_id)
1228 # priority = highest priority of children
1229 # priority = highest priority of children
1229 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1230 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1230 p.priority = IssuePriority.find_by_position(priority_position)
1231 p.priority = IssuePriority.find_by_position(priority_position)
1231 end
1232 end
1232
1233
1233 # start/due dates = lowest/highest dates of children
1234 # start/due dates = lowest/highest dates of children
1234 p.start_date = p.children.minimum(:start_date)
1235 p.start_date = p.children.minimum(:start_date)
1235 p.due_date = p.children.maximum(:due_date)
1236 p.due_date = p.children.maximum(:due_date)
1236 if p.start_date && p.due_date && p.due_date < p.start_date
1237 if p.start_date && p.due_date && p.due_date < p.start_date
1237 p.start_date, p.due_date = p.due_date, p.start_date
1238 p.start_date, p.due_date = p.due_date, p.start_date
1238 end
1239 end
1239
1240
1240 # done ratio = weighted average ratio of leaves
1241 # done ratio = weighted average ratio of leaves
1241 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1242 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1242 leaves_count = p.leaves.count
1243 leaves_count = p.leaves.count
1243 if leaves_count > 0
1244 if leaves_count > 0
1244 average = p.leaves.average(:estimated_hours).to_f
1245 average = p.leaves.average(:estimated_hours).to_f
1245 if average == 0
1246 if average == 0
1246 average = 1
1247 average = 1
1247 end
1248 end
1248 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1249 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1249 progress = done / (average * leaves_count)
1250 progress = done / (average * leaves_count)
1250 p.done_ratio = progress.round
1251 p.done_ratio = progress.round
1251 end
1252 end
1252 end
1253 end
1253
1254
1254 # estimate = sum of leaves estimates
1255 # estimate = sum of leaves estimates
1255 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1256 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1256 p.estimated_hours = nil if p.estimated_hours == 0.0
1257 p.estimated_hours = nil if p.estimated_hours == 0.0
1257
1258
1258 # ancestors will be recursively updated
1259 # ancestors will be recursively updated
1259 p.save(:validate => false)
1260 p.save(:validate => false)
1260 end
1261 end
1261 end
1262 end
1262
1263
1263 # Update issues so their versions are not pointing to a
1264 # Update issues so their versions are not pointing to a
1264 # fixed_version that is not shared with the issue's project
1265 # fixed_version that is not shared with the issue's project
1265 def self.update_versions(conditions=nil)
1266 def self.update_versions(conditions=nil)
1266 # Only need to update issues with a fixed_version from
1267 # Only need to update issues with a fixed_version from
1267 # a different project and that is not systemwide shared
1268 # a different project and that is not systemwide shared
1268 Issue.scoped(:conditions => conditions).all(
1269 Issue.scoped(:conditions => conditions).all(
1269 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1270 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1270 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1271 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1271 " AND #{Version.table_name}.sharing <> 'system'",
1272 " AND #{Version.table_name}.sharing <> 'system'",
1272 :include => [:project, :fixed_version]
1273 :include => [:project, :fixed_version]
1273 ).each do |issue|
1274 ).each do |issue|
1274 next if issue.project.nil? || issue.fixed_version.nil?
1275 next if issue.project.nil? || issue.fixed_version.nil?
1275 unless issue.project.shared_versions.include?(issue.fixed_version)
1276 unless issue.project.shared_versions.include?(issue.fixed_version)
1276 issue.init_journal(User.current)
1277 issue.init_journal(User.current)
1277 issue.fixed_version = nil
1278 issue.fixed_version = nil
1278 issue.save
1279 issue.save
1279 end
1280 end
1280 end
1281 end
1281 end
1282 end
1282
1283
1283 # Callback on file attachment
1284 # Callback on file attachment
1284 def attachment_added(obj)
1285 def attachment_added(obj)
1285 if @current_journal && !obj.new_record?
1286 if @current_journal && !obj.new_record?
1286 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1287 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1287 end
1288 end
1288 end
1289 end
1289
1290
1290 # Callback on attachment deletion
1291 # Callback on attachment deletion
1291 def attachment_removed(obj)
1292 def attachment_removed(obj)
1292 if @current_journal && !obj.new_record?
1293 if @current_journal && !obj.new_record?
1293 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1294 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1294 @current_journal.save
1295 @current_journal.save
1295 end
1296 end
1296 end
1297 end
1297
1298
1298 # Default assignment based on category
1299 # Default assignment based on category
1299 def default_assign
1300 def default_assign
1300 if assigned_to.nil? && category && category.assigned_to
1301 if assigned_to.nil? && category && category.assigned_to
1301 self.assigned_to = category.assigned_to
1302 self.assigned_to = category.assigned_to
1302 end
1303 end
1303 end
1304 end
1304
1305
1305 # Updates start/due dates of following issues
1306 # Updates start/due dates of following issues
1306 def reschedule_following_issues
1307 def reschedule_following_issues
1307 if start_date_changed? || due_date_changed?
1308 if start_date_changed? || due_date_changed?
1308 relations_from.each do |relation|
1309 relations_from.each do |relation|
1309 relation.set_issue_to_dates
1310 relation.set_issue_to_dates
1310 end
1311 end
1311 end
1312 end
1312 end
1313 end
1313
1314
1314 # Closes duplicates if the issue is being closed
1315 # Closes duplicates if the issue is being closed
1315 def close_duplicates
1316 def close_duplicates
1316 if closing?
1317 if closing?
1317 duplicates.each do |duplicate|
1318 duplicates.each do |duplicate|
1318 # Reload is need in case the duplicate was updated by a previous duplicate
1319 # Reload is need in case the duplicate was updated by a previous duplicate
1319 duplicate.reload
1320 duplicate.reload
1320 # Don't re-close it if it's already closed
1321 # Don't re-close it if it's already closed
1321 next if duplicate.closed?
1322 next if duplicate.closed?
1322 # Same user and notes
1323 # Same user and notes
1323 if @current_journal
1324 if @current_journal
1324 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1325 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1325 end
1326 end
1326 duplicate.update_attribute :status, self.status
1327 duplicate.update_attribute :status, self.status
1327 end
1328 end
1328 end
1329 end
1329 end
1330 end
1330
1331
1331 # Make sure updated_on is updated when adding a note and set updated_on now
1332 # Make sure updated_on is updated when adding a note and set updated_on now
1332 # so we can set closed_on with the same value on closing
1333 # so we can set closed_on with the same value on closing
1333 def force_updated_on_change
1334 def force_updated_on_change
1334 if @current_journal || changed?
1335 if @current_journal || changed?
1335 self.updated_on = current_time_from_proper_timezone
1336 self.updated_on = current_time_from_proper_timezone
1336 if new_record?
1337 if new_record?
1337 self.created_on = updated_on
1338 self.created_on = updated_on
1338 end
1339 end
1339 end
1340 end
1340 end
1341 end
1341
1342
1342 # Callback for setting closed_on when the issue is closed.
1343 # Callback for setting closed_on when the issue is closed.
1343 # The closed_on attribute stores the time of the last closing
1344 # The closed_on attribute stores the time of the last closing
1344 # and is preserved when the issue is reopened.
1345 # and is preserved when the issue is reopened.
1345 def update_closed_on
1346 def update_closed_on
1346 if closing? || (new_record? && closed?)
1347 if closing? || (new_record? && closed?)
1347 self.closed_on = updated_on
1348 self.closed_on = updated_on
1348 end
1349 end
1349 end
1350 end
1350
1351
1351 # Saves the changes in a Journal
1352 # Saves the changes in a Journal
1352 # Called after_save
1353 # Called after_save
1353 def create_journal
1354 def create_journal
1354 if @current_journal
1355 if @current_journal
1355 # attributes changes
1356 # attributes changes
1356 if @attributes_before_change
1357 if @attributes_before_change
1357 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1358 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1358 before = @attributes_before_change[c]
1359 before = @attributes_before_change[c]
1359 after = send(c)
1360 after = send(c)
1360 next if before == after || (before.blank? && after.blank?)
1361 next if before == after || (before.blank? && after.blank?)
1361 @current_journal.details << JournalDetail.new(:property => 'attr',
1362 @current_journal.details << JournalDetail.new(:property => 'attr',
1362 :prop_key => c,
1363 :prop_key => c,
1363 :old_value => before,
1364 :old_value => before,
1364 :value => after)
1365 :value => after)
1365 }
1366 }
1366 end
1367 end
1367 if @custom_values_before_change
1368 if @custom_values_before_change
1368 # custom fields changes
1369 # custom fields changes
1369 custom_field_values.each {|c|
1370 custom_field_values.each {|c|
1370 before = @custom_values_before_change[c.custom_field_id]
1371 before = @custom_values_before_change[c.custom_field_id]
1371 after = c.value
1372 after = c.value
1372 next if before == after || (before.blank? && after.blank?)
1373 next if before == after || (before.blank? && after.blank?)
1373
1374
1374 if before.is_a?(Array) || after.is_a?(Array)
1375 if before.is_a?(Array) || after.is_a?(Array)
1375 before = [before] unless before.is_a?(Array)
1376 before = [before] unless before.is_a?(Array)
1376 after = [after] unless after.is_a?(Array)
1377 after = [after] unless after.is_a?(Array)
1377
1378
1378 # values removed
1379 # values removed
1379 (before - after).reject(&:blank?).each do |value|
1380 (before - after).reject(&:blank?).each do |value|
1380 @current_journal.details << JournalDetail.new(:property => 'cf',
1381 @current_journal.details << JournalDetail.new(:property => 'cf',
1381 :prop_key => c.custom_field_id,
1382 :prop_key => c.custom_field_id,
1382 :old_value => value,
1383 :old_value => value,
1383 :value => nil)
1384 :value => nil)
1384 end
1385 end
1385 # values added
1386 # values added
1386 (after - before).reject(&:blank?).each do |value|
1387 (after - before).reject(&:blank?).each do |value|
1387 @current_journal.details << JournalDetail.new(:property => 'cf',
1388 @current_journal.details << JournalDetail.new(:property => 'cf',
1388 :prop_key => c.custom_field_id,
1389 :prop_key => c.custom_field_id,
1389 :old_value => nil,
1390 :old_value => nil,
1390 :value => value)
1391 :value => value)
1391 end
1392 end
1392 else
1393 else
1393 @current_journal.details << JournalDetail.new(:property => 'cf',
1394 @current_journal.details << JournalDetail.new(:property => 'cf',
1394 :prop_key => c.custom_field_id,
1395 :prop_key => c.custom_field_id,
1395 :old_value => before,
1396 :old_value => before,
1396 :value => after)
1397 :value => after)
1397 end
1398 end
1398 }
1399 }
1399 end
1400 end
1400 @current_journal.save
1401 @current_journal.save
1401 # reset current journal
1402 # reset current journal
1402 init_journal @current_journal.user, @current_journal.notes
1403 init_journal @current_journal.user, @current_journal.notes
1403 end
1404 end
1404 end
1405 end
1405
1406
1406 # Query generator for selecting groups of issue counts for a project
1407 # Query generator for selecting groups of issue counts for a project
1407 # based on specific criteria
1408 # based on specific criteria
1408 #
1409 #
1409 # Options
1410 # Options
1410 # * project - Project to search in.
1411 # * project - Project to search in.
1411 # * field - String. Issue field to key off of in the grouping.
1412 # * field - String. Issue field to key off of in the grouping.
1412 # * joins - String. The table name to join against.
1413 # * joins - String. The table name to join against.
1413 def self.count_and_group_by(options)
1414 def self.count_and_group_by(options)
1414 project = options.delete(:project)
1415 project = options.delete(:project)
1415 select_field = options.delete(:field)
1416 select_field = options.delete(:field)
1416 joins = options.delete(:joins)
1417 joins = options.delete(:joins)
1417
1418
1418 where = "#{Issue.table_name}.#{select_field}=j.id"
1419 where = "#{Issue.table_name}.#{select_field}=j.id"
1419
1420
1420 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1421 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1421 s.is_closed as closed,
1422 s.is_closed as closed,
1422 j.id as #{select_field},
1423 j.id as #{select_field},
1423 count(#{Issue.table_name}.id) as total
1424 count(#{Issue.table_name}.id) as total
1424 from
1425 from
1425 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1426 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1426 where
1427 where
1427 #{Issue.table_name}.status_id=s.id
1428 #{Issue.table_name}.status_id=s.id
1428 and #{where}
1429 and #{where}
1429 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1430 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1430 and #{visible_condition(User.current, :project => project)}
1431 and #{visible_condition(User.current, :project => project)}
1431 group by s.id, s.is_closed, j.id")
1432 group by s.id, s.is_closed, j.id")
1432 end
1433 end
1433 end
1434 end
@@ -1,1025 +1,1026
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_CLOSED = 5
23 STATUS_CLOSED = 5
24 STATUS_ARCHIVED = 9
24 STATUS_ARCHIVED = 9
25
25
26 # Maximum length for project identifiers
26 # Maximum length for project identifiers
27 IDENTIFIER_MAX_LENGTH = 100
27 IDENTIFIER_MAX_LENGTH = 100
28
28
29 # Specific overidden Activities
29 # Specific overidden Activities
30 has_many :time_entry_activities
30 has_many :time_entry_activities
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 has_many :memberships, :class_name => 'Member'
32 has_many :memberships, :class_name => 'Member'
33 has_many :member_principals, :class_name => 'Member',
33 has_many :member_principals, :class_name => 'Member',
34 :include => :principal,
34 :include => :principal,
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36 has_many :users, :through => :members
36 has_many :users, :through => :members
37 has_many :principals, :through => :member_principals, :source => :principal
37 has_many :principals, :through => :member_principals, :source => :principal
38
38
39 has_many :enabled_modules, :dependent => :delete_all
39 has_many :enabled_modules, :dependent => :delete_all
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
42 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :issue_changes, :through => :issues, :source => :journals
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
44 has_many :time_entries, :dependent => :delete_all
44 has_many :time_entries, :dependent => :delete_all
45 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
45 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
46 has_many :documents, :dependent => :destroy
46 has_many :documents, :dependent => :destroy
47 has_many :news, :dependent => :destroy, :include => :author
47 has_many :news, :dependent => :destroy, :include => :author
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
50 has_one :repository, :conditions => ["is_default = ?", true]
50 has_one :repository, :conditions => ["is_default = ?", true]
51 has_many :repositories, :dependent => :destroy
51 has_many :repositories, :dependent => :destroy
52 has_many :changesets, :through => :repository
52 has_many :changesets, :through => :repository
53 has_one :wiki, :dependent => :destroy
53 has_one :wiki, :dependent => :destroy
54 # Custom field for the project issues
54 # Custom field for the project issues
55 has_and_belongs_to_many :issue_custom_fields,
55 has_and_belongs_to_many :issue_custom_fields,
56 :class_name => 'IssueCustomField',
56 :class_name => 'IssueCustomField',
57 :order => "#{CustomField.table_name}.position",
57 :order => "#{CustomField.table_name}.position",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
59 :association_foreign_key => 'custom_field_id'
59 :association_foreign_key => 'custom_field_id'
60
60
61 acts_as_nested_set :order => 'name', :dependent => :destroy
61 acts_as_nested_set :order => 'name', :dependent => :destroy
62 acts_as_attachable :view_permission => :view_files,
62 acts_as_attachable :view_permission => :view_files,
63 :delete_permission => :manage_files
63 :delete_permission => :manage_files
64
64
65 acts_as_customizable
65 acts_as_customizable
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
69 :author => nil
69 :author => nil
70
70
71 attr_protected :status
71 attr_protected :status
72
72
73 validates_presence_of :name, :identifier
73 validates_presence_of :name, :identifier
74 validates_uniqueness_of :identifier
74 validates_uniqueness_of :identifier
75 validates_associated :repository, :wiki
75 validates_associated :repository, :wiki
76 validates_length_of :name, :maximum => 255
76 validates_length_of :name, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
79 # donwcase letters, digits, dashes but not digits only
79 # donwcase letters, digits, dashes but not digits only
80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
81 # reserved words
81 # reserved words
82 validates_exclusion_of :identifier, :in => %w( new )
82 validates_exclusion_of :identifier, :in => %w( new )
83
83
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
85 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
85 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
86 before_destroy :delete_all_members
86 before_destroy :delete_all_members
87
87
88 scope :has_module, lambda {|mod|
88 scope :has_module, lambda {|mod|
89 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
90 }
90 }
91 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 scope :active, lambda { where(:status => STATUS_ACTIVE) }
92 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
93 scope :all_public, lambda { where(:is_public => true) }
93 scope :all_public, lambda { where(:is_public => true) }
94 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
95 scope :allowed_to, lambda {|*args|
95 scope :allowed_to, lambda {|*args|
96 user = User.current
96 user = User.current
97 permission = nil
97 permission = nil
98 if args.first.is_a?(Symbol)
98 if args.first.is_a?(Symbol)
99 permission = args.shift
99 permission = args.shift
100 else
100 else
101 user = args.shift
101 user = args.shift
102 permission = args.shift
102 permission = args.shift
103 end
103 end
104 where(Project.allowed_to_condition(user, permission, *args))
104 where(Project.allowed_to_condition(user, permission, *args))
105 }
105 }
106 scope :like, lambda {|arg|
106 scope :like, lambda {|arg|
107 if arg.blank?
107 if arg.blank?
108 where(nil)
108 where(nil)
109 else
109 else
110 pattern = "%#{arg.to_s.strip.downcase}%"
110 pattern = "%#{arg.to_s.strip.downcase}%"
111 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
112 end
112 end
113 }
113 }
114
114
115 def initialize(attributes=nil, *args)
115 def initialize(attributes=nil, *args)
116 super
116 super
117
117
118 initialized = (attributes || {}).stringify_keys
118 initialized = (attributes || {}).stringify_keys
119 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
119 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
120 self.identifier = Project.next_identifier
120 self.identifier = Project.next_identifier
121 end
121 end
122 if !initialized.key?('is_public')
122 if !initialized.key?('is_public')
123 self.is_public = Setting.default_projects_public?
123 self.is_public = Setting.default_projects_public?
124 end
124 end
125 if !initialized.key?('enabled_module_names')
125 if !initialized.key?('enabled_module_names')
126 self.enabled_module_names = Setting.default_projects_modules
126 self.enabled_module_names = Setting.default_projects_modules
127 end
127 end
128 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
128 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
129 default = Setting.default_projects_tracker_ids
129 default = Setting.default_projects_tracker_ids
130 if default.is_a?(Array)
130 if default.is_a?(Array)
131 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
131 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
132 else
132 else
133 self.trackers = Tracker.sorted.all
133 self.trackers = Tracker.sorted.all
134 end
134 end
135 end
135 end
136 end
136 end
137
137
138 def identifier=(identifier)
138 def identifier=(identifier)
139 super unless identifier_frozen?
139 super unless identifier_frozen?
140 end
140 end
141
141
142 def identifier_frozen?
142 def identifier_frozen?
143 errors[:identifier].blank? && !(new_record? || identifier.blank?)
143 errors[:identifier].blank? && !(new_record? || identifier.blank?)
144 end
144 end
145
145
146 # returns latest created projects
146 # returns latest created projects
147 # non public projects will be returned only if user is a member of those
147 # non public projects will be returned only if user is a member of those
148 def self.latest(user=nil, count=5)
148 def self.latest(user=nil, count=5)
149 visible(user).limit(count).order("created_on DESC").all
149 visible(user).limit(count).order("created_on DESC").all
150 end
150 end
151
151
152 # Returns true if the project is visible to +user+ or to the current user.
152 # Returns true if the project is visible to +user+ or to the current user.
153 def visible?(user=User.current)
153 def visible?(user=User.current)
154 user.allowed_to?(:view_project, self)
154 user.allowed_to?(:view_project, self)
155 end
155 end
156
156
157 # Returns a SQL conditions string used to find all projects visible by the specified user.
157 # Returns a SQL conditions string used to find all projects visible by the specified user.
158 #
158 #
159 # Examples:
159 # Examples:
160 # Project.visible_condition(admin) => "projects.status = 1"
160 # Project.visible_condition(admin) => "projects.status = 1"
161 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
161 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
162 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
162 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
163 def self.visible_condition(user, options={})
163 def self.visible_condition(user, options={})
164 allowed_to_condition(user, :view_project, options)
164 allowed_to_condition(user, :view_project, options)
165 end
165 end
166
166
167 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
167 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
168 #
168 #
169 # Valid options:
169 # Valid options:
170 # * :project => limit the condition to project
170 # * :project => limit the condition to project
171 # * :with_subprojects => limit the condition to project and its subprojects
171 # * :with_subprojects => limit the condition to project and its subprojects
172 # * :member => limit the condition to the user projects
172 # * :member => limit the condition to the user projects
173 def self.allowed_to_condition(user, permission, options={})
173 def self.allowed_to_condition(user, permission, options={})
174 perm = Redmine::AccessControl.permission(permission)
174 perm = Redmine::AccessControl.permission(permission)
175 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
175 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
176 if perm && perm.project_module
176 if perm && perm.project_module
177 # If the permission belongs to a project module, make sure the module is enabled
177 # If the permission belongs to a project module, make sure the module is enabled
178 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
178 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
179 end
179 end
180 if options[:project]
180 if options[:project]
181 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
181 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
182 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
182 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
183 base_statement = "(#{project_statement}) AND (#{base_statement})"
183 base_statement = "(#{project_statement}) AND (#{base_statement})"
184 end
184 end
185
185
186 if user.admin?
186 if user.admin?
187 base_statement
187 base_statement
188 else
188 else
189 statement_by_role = {}
189 statement_by_role = {}
190 unless options[:member]
190 unless options[:member]
191 role = user.logged? ? Role.non_member : Role.anonymous
191 role = user.logged? ? Role.non_member : Role.anonymous
192 if role.allowed_to?(permission)
192 if role.allowed_to?(permission)
193 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
193 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
194 end
194 end
195 end
195 end
196 if user.logged?
196 if user.logged?
197 user.projects_by_role.each do |role, projects|
197 user.projects_by_role.each do |role, projects|
198 if role.allowed_to?(permission) && projects.any?
198 if role.allowed_to?(permission) && projects.any?
199 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
199 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
200 end
200 end
201 end
201 end
202 end
202 end
203 if statement_by_role.empty?
203 if statement_by_role.empty?
204 "1=0"
204 "1=0"
205 else
205 else
206 if block_given?
206 if block_given?
207 statement_by_role.each do |role, statement|
207 statement_by_role.each do |role, statement|
208 if s = yield(role, user)
208 if s = yield(role, user)
209 statement_by_role[role] = "(#{statement} AND (#{s}))"
209 statement_by_role[role] = "(#{statement} AND (#{s}))"
210 end
210 end
211 end
211 end
212 end
212 end
213 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
213 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
214 end
214 end
215 end
215 end
216 end
216 end
217
217
218 # Returns the Systemwide and project specific activities
218 # Returns the Systemwide and project specific activities
219 def activities(include_inactive=false)
219 def activities(include_inactive=false)
220 if include_inactive
220 if include_inactive
221 return all_activities
221 return all_activities
222 else
222 else
223 return active_activities
223 return active_activities
224 end
224 end
225 end
225 end
226
226
227 # Will create a new Project specific Activity or update an existing one
227 # Will create a new Project specific Activity or update an existing one
228 #
228 #
229 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
229 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
230 # does not successfully save.
230 # does not successfully save.
231 def update_or_create_time_entry_activity(id, activity_hash)
231 def update_or_create_time_entry_activity(id, activity_hash)
232 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
232 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
233 self.create_time_entry_activity_if_needed(activity_hash)
233 self.create_time_entry_activity_if_needed(activity_hash)
234 else
234 else
235 activity = project.time_entry_activities.find_by_id(id.to_i)
235 activity = project.time_entry_activities.find_by_id(id.to_i)
236 activity.update_attributes(activity_hash) if activity
236 activity.update_attributes(activity_hash) if activity
237 end
237 end
238 end
238 end
239
239
240 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
240 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
241 #
241 #
242 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
242 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
243 # does not successfully save.
243 # does not successfully save.
244 def create_time_entry_activity_if_needed(activity)
244 def create_time_entry_activity_if_needed(activity)
245 if activity['parent_id']
245 if activity['parent_id']
246
246
247 parent_activity = TimeEntryActivity.find(activity['parent_id'])
247 parent_activity = TimeEntryActivity.find(activity['parent_id'])
248 activity['name'] = parent_activity.name
248 activity['name'] = parent_activity.name
249 activity['position'] = parent_activity.position
249 activity['position'] = parent_activity.position
250
250
251 if Enumeration.overridding_change?(activity, parent_activity)
251 if Enumeration.overridding_change?(activity, parent_activity)
252 project_activity = self.time_entry_activities.create(activity)
252 project_activity = self.time_entry_activities.create(activity)
253
253
254 if project_activity.new_record?
254 if project_activity.new_record?
255 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
255 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
256 else
256 else
257 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
257 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
258 end
258 end
259 end
259 end
260 end
260 end
261 end
261 end
262
262
263 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
263 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
264 #
264 #
265 # Examples:
265 # Examples:
266 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
266 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
267 # project.project_condition(false) => "projects.id = 1"
267 # project.project_condition(false) => "projects.id = 1"
268 def project_condition(with_subprojects)
268 def project_condition(with_subprojects)
269 cond = "#{Project.table_name}.id = #{id}"
269 cond = "#{Project.table_name}.id = #{id}"
270 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
270 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
271 cond
271 cond
272 end
272 end
273
273
274 def self.find(*args)
274 def self.find(*args)
275 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
275 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
276 project = find_by_identifier(*args)
276 project = find_by_identifier(*args)
277 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
277 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
278 project
278 project
279 else
279 else
280 super
280 super
281 end
281 end
282 end
282 end
283
283
284 def self.find_by_param(*args)
284 def self.find_by_param(*args)
285 self.find(*args)
285 self.find(*args)
286 end
286 end
287
287
288 alias :base_reload :reload
288 def reload(*args)
289 def reload(*args)
289 @shared_versions = nil
290 @shared_versions = nil
290 @rolled_up_versions = nil
291 @rolled_up_versions = nil
291 @rolled_up_trackers = nil
292 @rolled_up_trackers = nil
292 @all_issue_custom_fields = nil
293 @all_issue_custom_fields = nil
293 @all_time_entry_custom_fields = nil
294 @all_time_entry_custom_fields = nil
294 @to_param = nil
295 @to_param = nil
295 @allowed_parents = nil
296 @allowed_parents = nil
296 @allowed_permissions = nil
297 @allowed_permissions = nil
297 @actions_allowed = nil
298 @actions_allowed = nil
298 @start_date = nil
299 @start_date = nil
299 @due_date = nil
300 @due_date = nil
300 super
301 base_reload(*args)
301 end
302 end
302
303
303 def to_param
304 def to_param
304 # id is used for projects with a numeric identifier (compatibility)
305 # id is used for projects with a numeric identifier (compatibility)
305 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
306 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
306 end
307 end
307
308
308 def active?
309 def active?
309 self.status == STATUS_ACTIVE
310 self.status == STATUS_ACTIVE
310 end
311 end
311
312
312 def archived?
313 def archived?
313 self.status == STATUS_ARCHIVED
314 self.status == STATUS_ARCHIVED
314 end
315 end
315
316
316 # Archives the project and its descendants
317 # Archives the project and its descendants
317 def archive
318 def archive
318 # Check that there is no issue of a non descendant project that is assigned
319 # Check that there is no issue of a non descendant project that is assigned
319 # to one of the project or descendant versions
320 # to one of the project or descendant versions
320 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
321 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
321 if v_ids.any? &&
322 if v_ids.any? &&
322 Issue.
323 Issue.
323 includes(:project).
324 includes(:project).
324 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
325 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
325 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
326 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
326 exists?
327 exists?
327 return false
328 return false
328 end
329 end
329 Project.transaction do
330 Project.transaction do
330 archive!
331 archive!
331 end
332 end
332 true
333 true
333 end
334 end
334
335
335 # Unarchives the project
336 # Unarchives the project
336 # All its ancestors must be active
337 # All its ancestors must be active
337 def unarchive
338 def unarchive
338 return false if ancestors.detect {|a| !a.active?}
339 return false if ancestors.detect {|a| !a.active?}
339 update_attribute :status, STATUS_ACTIVE
340 update_attribute :status, STATUS_ACTIVE
340 end
341 end
341
342
342 def close
343 def close
343 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
344 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
344 end
345 end
345
346
346 def reopen
347 def reopen
347 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
348 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
348 end
349 end
349
350
350 # Returns an array of projects the project can be moved to
351 # Returns an array of projects the project can be moved to
351 # by the current user
352 # by the current user
352 def allowed_parents
353 def allowed_parents
353 return @allowed_parents if @allowed_parents
354 return @allowed_parents if @allowed_parents
354 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
355 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
355 @allowed_parents = @allowed_parents - self_and_descendants
356 @allowed_parents = @allowed_parents - self_and_descendants
356 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
357 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
357 @allowed_parents << nil
358 @allowed_parents << nil
358 end
359 end
359 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
360 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
360 @allowed_parents << parent
361 @allowed_parents << parent
361 end
362 end
362 @allowed_parents
363 @allowed_parents
363 end
364 end
364
365
365 # Sets the parent of the project with authorization check
366 # Sets the parent of the project with authorization check
366 def set_allowed_parent!(p)
367 def set_allowed_parent!(p)
367 unless p.nil? || p.is_a?(Project)
368 unless p.nil? || p.is_a?(Project)
368 if p.to_s.blank?
369 if p.to_s.blank?
369 p = nil
370 p = nil
370 else
371 else
371 p = Project.find_by_id(p)
372 p = Project.find_by_id(p)
372 return false unless p
373 return false unless p
373 end
374 end
374 end
375 end
375 if p.nil?
376 if p.nil?
376 if !new_record? && allowed_parents.empty?
377 if !new_record? && allowed_parents.empty?
377 return false
378 return false
378 end
379 end
379 elsif !allowed_parents.include?(p)
380 elsif !allowed_parents.include?(p)
380 return false
381 return false
381 end
382 end
382 set_parent!(p)
383 set_parent!(p)
383 end
384 end
384
385
385 # Sets the parent of the project
386 # Sets the parent of the project
386 # Argument can be either a Project, a String, a Fixnum or nil
387 # Argument can be either a Project, a String, a Fixnum or nil
387 def set_parent!(p)
388 def set_parent!(p)
388 unless p.nil? || p.is_a?(Project)
389 unless p.nil? || p.is_a?(Project)
389 if p.to_s.blank?
390 if p.to_s.blank?
390 p = nil
391 p = nil
391 else
392 else
392 p = Project.find_by_id(p)
393 p = Project.find_by_id(p)
393 return false unless p
394 return false unless p
394 end
395 end
395 end
396 end
396 if p == parent && !p.nil?
397 if p == parent && !p.nil?
397 # Nothing to do
398 # Nothing to do
398 true
399 true
399 elsif p.nil? || (p.active? && move_possible?(p))
400 elsif p.nil? || (p.active? && move_possible?(p))
400 set_or_update_position_under(p)
401 set_or_update_position_under(p)
401 Issue.update_versions_from_hierarchy_change(self)
402 Issue.update_versions_from_hierarchy_change(self)
402 true
403 true
403 else
404 else
404 # Can not move to the given target
405 # Can not move to the given target
405 false
406 false
406 end
407 end
407 end
408 end
408
409
409 # Recalculates all lft and rgt values based on project names
410 # Recalculates all lft and rgt values based on project names
410 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
411 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
411 # Used in BuildProjectsTree migration
412 # Used in BuildProjectsTree migration
412 def self.rebuild_tree!
413 def self.rebuild_tree!
413 transaction do
414 transaction do
414 update_all "lft = NULL, rgt = NULL"
415 update_all "lft = NULL, rgt = NULL"
415 rebuild!(false)
416 rebuild!(false)
416 end
417 end
417 end
418 end
418
419
419 # Returns an array of the trackers used by the project and its active sub projects
420 # Returns an array of the trackers used by the project and its active sub projects
420 def rolled_up_trackers
421 def rolled_up_trackers
421 @rolled_up_trackers ||=
422 @rolled_up_trackers ||=
422 Tracker.
423 Tracker.
423 joins(:projects).
424 joins(:projects).
424 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
425 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
425 select("DISTINCT #{Tracker.table_name}.*").
426 select("DISTINCT #{Tracker.table_name}.*").
426 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
427 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
427 sorted.
428 sorted.
428 all
429 all
429 end
430 end
430
431
431 # Closes open and locked project versions that are completed
432 # Closes open and locked project versions that are completed
432 def close_completed_versions
433 def close_completed_versions
433 Version.transaction do
434 Version.transaction do
434 versions.where(:status => %w(open locked)).all.each do |version|
435 versions.where(:status => %w(open locked)).all.each do |version|
435 if version.completed?
436 if version.completed?
436 version.update_attribute(:status, 'closed')
437 version.update_attribute(:status, 'closed')
437 end
438 end
438 end
439 end
439 end
440 end
440 end
441 end
441
442
442 # Returns a scope of the Versions on subprojects
443 # Returns a scope of the Versions on subprojects
443 def rolled_up_versions
444 def rolled_up_versions
444 @rolled_up_versions ||=
445 @rolled_up_versions ||=
445 Version.scoped(:include => :project,
446 Version.scoped(:include => :project,
446 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
447 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
447 end
448 end
448
449
449 # Returns a scope of the Versions used by the project
450 # Returns a scope of the Versions used by the project
450 def shared_versions
451 def shared_versions
451 if new_record?
452 if new_record?
452 Version.scoped(:include => :project,
453 Version.scoped(:include => :project,
453 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
454 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
454 else
455 else
455 @shared_versions ||= begin
456 @shared_versions ||= begin
456 r = root? ? self : root
457 r = root? ? self : root
457 Version.scoped(:include => :project,
458 Version.scoped(:include => :project,
458 :conditions => "#{Project.table_name}.id = #{id}" +
459 :conditions => "#{Project.table_name}.id = #{id}" +
459 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
460 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
460 " #{Version.table_name}.sharing = 'system'" +
461 " #{Version.table_name}.sharing = 'system'" +
461 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
462 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
462 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
463 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
463 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
464 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
464 "))")
465 "))")
465 end
466 end
466 end
467 end
467 end
468 end
468
469
469 # Returns a hash of project users grouped by role
470 # Returns a hash of project users grouped by role
470 def users_by_role
471 def users_by_role
471 members.includes(:user, :roles).all.inject({}) do |h, m|
472 members.includes(:user, :roles).all.inject({}) do |h, m|
472 m.roles.each do |r|
473 m.roles.each do |r|
473 h[r] ||= []
474 h[r] ||= []
474 h[r] << m.user
475 h[r] << m.user
475 end
476 end
476 h
477 h
477 end
478 end
478 end
479 end
479
480
480 # Deletes all project's members
481 # Deletes all project's members
481 def delete_all_members
482 def delete_all_members
482 me, mr = Member.table_name, MemberRole.table_name
483 me, mr = Member.table_name, MemberRole.table_name
483 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
484 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
484 Member.delete_all(['project_id = ?', id])
485 Member.delete_all(['project_id = ?', id])
485 end
486 end
486
487
487 # Users/groups issues can be assigned to
488 # Users/groups issues can be assigned to
488 def assignable_users
489 def assignable_users
489 assignable = Setting.issue_group_assignment? ? member_principals : members
490 assignable = Setting.issue_group_assignment? ? member_principals : members
490 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
491 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
491 end
492 end
492
493
493 # Returns the mail adresses of users that should be always notified on project events
494 # Returns the mail adresses of users that should be always notified on project events
494 def recipients
495 def recipients
495 notified_users.collect {|user| user.mail}
496 notified_users.collect {|user| user.mail}
496 end
497 end
497
498
498 # Returns the users that should be notified on project events
499 # Returns the users that should be notified on project events
499 def notified_users
500 def notified_users
500 # TODO: User part should be extracted to User#notify_about?
501 # TODO: User part should be extracted to User#notify_about?
501 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
502 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
502 end
503 end
503
504
504 # Returns an array of all custom fields enabled for project issues
505 # Returns an array of all custom fields enabled for project issues
505 # (explictly associated custom fields and custom fields enabled for all projects)
506 # (explictly associated custom fields and custom fields enabled for all projects)
506 def all_issue_custom_fields
507 def all_issue_custom_fields
507 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
508 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
508 end
509 end
509
510
510 # Returns an array of all custom fields enabled for project time entries
511 # Returns an array of all custom fields enabled for project time entries
511 # (explictly associated custom fields and custom fields enabled for all projects)
512 # (explictly associated custom fields and custom fields enabled for all projects)
512 def all_time_entry_custom_fields
513 def all_time_entry_custom_fields
513 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
514 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
514 end
515 end
515
516
516 def project
517 def project
517 self
518 self
518 end
519 end
519
520
520 def <=>(project)
521 def <=>(project)
521 name.downcase <=> project.name.downcase
522 name.downcase <=> project.name.downcase
522 end
523 end
523
524
524 def to_s
525 def to_s
525 name
526 name
526 end
527 end
527
528
528 # Returns a short description of the projects (first lines)
529 # Returns a short description of the projects (first lines)
529 def short_description(length = 255)
530 def short_description(length = 255)
530 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
531 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
531 end
532 end
532
533
533 def css_classes
534 def css_classes
534 s = 'project'
535 s = 'project'
535 s << ' root' if root?
536 s << ' root' if root?
536 s << ' child' if child?
537 s << ' child' if child?
537 s << (leaf? ? ' leaf' : ' parent')
538 s << (leaf? ? ' leaf' : ' parent')
538 unless active?
539 unless active?
539 if archived?
540 if archived?
540 s << ' archived'
541 s << ' archived'
541 else
542 else
542 s << ' closed'
543 s << ' closed'
543 end
544 end
544 end
545 end
545 s
546 s
546 end
547 end
547
548
548 # The earliest start date of a project, based on it's issues and versions
549 # The earliest start date of a project, based on it's issues and versions
549 def start_date
550 def start_date
550 @start_date ||= [
551 @start_date ||= [
551 issues.minimum('start_date'),
552 issues.minimum('start_date'),
552 shared_versions.minimum('effective_date'),
553 shared_versions.minimum('effective_date'),
553 Issue.fixed_version(shared_versions).minimum('start_date')
554 Issue.fixed_version(shared_versions).minimum('start_date')
554 ].compact.min
555 ].compact.min
555 end
556 end
556
557
557 # The latest due date of an issue or version
558 # The latest due date of an issue or version
558 def due_date
559 def due_date
559 @due_date ||= [
560 @due_date ||= [
560 issues.maximum('due_date'),
561 issues.maximum('due_date'),
561 shared_versions.maximum('effective_date'),
562 shared_versions.maximum('effective_date'),
562 Issue.fixed_version(shared_versions).maximum('due_date')
563 Issue.fixed_version(shared_versions).maximum('due_date')
563 ].compact.max
564 ].compact.max
564 end
565 end
565
566
566 def overdue?
567 def overdue?
567 active? && !due_date.nil? && (due_date < Date.today)
568 active? && !due_date.nil? && (due_date < Date.today)
568 end
569 end
569
570
570 # Returns the percent completed for this project, based on the
571 # Returns the percent completed for this project, based on the
571 # progress on it's versions.
572 # progress on it's versions.
572 def completed_percent(options={:include_subprojects => false})
573 def completed_percent(options={:include_subprojects => false})
573 if options.delete(:include_subprojects)
574 if options.delete(:include_subprojects)
574 total = self_and_descendants.collect(&:completed_percent).sum
575 total = self_and_descendants.collect(&:completed_percent).sum
575
576
576 total / self_and_descendants.count
577 total / self_and_descendants.count
577 else
578 else
578 if versions.count > 0
579 if versions.count > 0
579 total = versions.collect(&:completed_percent).sum
580 total = versions.collect(&:completed_percent).sum
580
581
581 total / versions.count
582 total / versions.count
582 else
583 else
583 100
584 100
584 end
585 end
585 end
586 end
586 end
587 end
587
588
588 # Return true if this project allows to do the specified action.
589 # Return true if this project allows to do the specified action.
589 # action can be:
590 # action can be:
590 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
591 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
591 # * a permission Symbol (eg. :edit_project)
592 # * a permission Symbol (eg. :edit_project)
592 def allows_to?(action)
593 def allows_to?(action)
593 if archived?
594 if archived?
594 # No action allowed on archived projects
595 # No action allowed on archived projects
595 return false
596 return false
596 end
597 end
597 unless active? || Redmine::AccessControl.read_action?(action)
598 unless active? || Redmine::AccessControl.read_action?(action)
598 # No write action allowed on closed projects
599 # No write action allowed on closed projects
599 return false
600 return false
600 end
601 end
601 # No action allowed on disabled modules
602 # No action allowed on disabled modules
602 if action.is_a? Hash
603 if action.is_a? Hash
603 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
604 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
604 else
605 else
605 allowed_permissions.include? action
606 allowed_permissions.include? action
606 end
607 end
607 end
608 end
608
609
609 def module_enabled?(module_name)
610 def module_enabled?(module_name)
610 module_name = module_name.to_s
611 module_name = module_name.to_s
611 enabled_modules.detect {|m| m.name == module_name}
612 enabled_modules.detect {|m| m.name == module_name}
612 end
613 end
613
614
614 def enabled_module_names=(module_names)
615 def enabled_module_names=(module_names)
615 if module_names && module_names.is_a?(Array)
616 if module_names && module_names.is_a?(Array)
616 module_names = module_names.collect(&:to_s).reject(&:blank?)
617 module_names = module_names.collect(&:to_s).reject(&:blank?)
617 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
618 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
618 else
619 else
619 enabled_modules.clear
620 enabled_modules.clear
620 end
621 end
621 end
622 end
622
623
623 # Returns an array of the enabled modules names
624 # Returns an array of the enabled modules names
624 def enabled_module_names
625 def enabled_module_names
625 enabled_modules.collect(&:name)
626 enabled_modules.collect(&:name)
626 end
627 end
627
628
628 # Enable a specific module
629 # Enable a specific module
629 #
630 #
630 # Examples:
631 # Examples:
631 # project.enable_module!(:issue_tracking)
632 # project.enable_module!(:issue_tracking)
632 # project.enable_module!("issue_tracking")
633 # project.enable_module!("issue_tracking")
633 def enable_module!(name)
634 def enable_module!(name)
634 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
635 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
635 end
636 end
636
637
637 # Disable a module if it exists
638 # Disable a module if it exists
638 #
639 #
639 # Examples:
640 # Examples:
640 # project.disable_module!(:issue_tracking)
641 # project.disable_module!(:issue_tracking)
641 # project.disable_module!("issue_tracking")
642 # project.disable_module!("issue_tracking")
642 # project.disable_module!(project.enabled_modules.first)
643 # project.disable_module!(project.enabled_modules.first)
643 def disable_module!(target)
644 def disable_module!(target)
644 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
645 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
645 target.destroy unless target.blank?
646 target.destroy unless target.blank?
646 end
647 end
647
648
648 safe_attributes 'name',
649 safe_attributes 'name',
649 'description',
650 'description',
650 'homepage',
651 'homepage',
651 'is_public',
652 'is_public',
652 'identifier',
653 'identifier',
653 'custom_field_values',
654 'custom_field_values',
654 'custom_fields',
655 'custom_fields',
655 'tracker_ids',
656 'tracker_ids',
656 'issue_custom_field_ids'
657 'issue_custom_field_ids'
657
658
658 safe_attributes 'enabled_module_names',
659 safe_attributes 'enabled_module_names',
659 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
660 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
660
661
661 safe_attributes 'inherit_members',
662 safe_attributes 'inherit_members',
662 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
663 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
663
664
664 # Returns an array of projects that are in this project's hierarchy
665 # Returns an array of projects that are in this project's hierarchy
665 #
666 #
666 # Example: parents, children, siblings
667 # Example: parents, children, siblings
667 def hierarchy
668 def hierarchy
668 parents = project.self_and_ancestors || []
669 parents = project.self_and_ancestors || []
669 descendants = project.descendants || []
670 descendants = project.descendants || []
670 project_hierarchy = parents | descendants # Set union
671 project_hierarchy = parents | descendants # Set union
671 end
672 end
672
673
673 # Returns an auto-generated project identifier based on the last identifier used
674 # Returns an auto-generated project identifier based on the last identifier used
674 def self.next_identifier
675 def self.next_identifier
675 p = Project.order('created_on DESC').first
676 p = Project.order('created_on DESC').first
676 p.nil? ? nil : p.identifier.to_s.succ
677 p.nil? ? nil : p.identifier.to_s.succ
677 end
678 end
678
679
679 # Copies and saves the Project instance based on the +project+.
680 # Copies and saves the Project instance based on the +project+.
680 # Duplicates the source project's:
681 # Duplicates the source project's:
681 # * Wiki
682 # * Wiki
682 # * Versions
683 # * Versions
683 # * Categories
684 # * Categories
684 # * Issues
685 # * Issues
685 # * Members
686 # * Members
686 # * Queries
687 # * Queries
687 #
688 #
688 # Accepts an +options+ argument to specify what to copy
689 # Accepts an +options+ argument to specify what to copy
689 #
690 #
690 # Examples:
691 # Examples:
691 # project.copy(1) # => copies everything
692 # project.copy(1) # => copies everything
692 # project.copy(1, :only => 'members') # => copies members only
693 # project.copy(1, :only => 'members') # => copies members only
693 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
694 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
694 def copy(project, options={})
695 def copy(project, options={})
695 project = project.is_a?(Project) ? project : Project.find(project)
696 project = project.is_a?(Project) ? project : Project.find(project)
696
697
697 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
698 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
698 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
699 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
699
700
700 Project.transaction do
701 Project.transaction do
701 if save
702 if save
702 reload
703 reload
703 to_be_copied.each do |name|
704 to_be_copied.each do |name|
704 send "copy_#{name}", project
705 send "copy_#{name}", project
705 end
706 end
706 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
707 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
707 save
708 save
708 end
709 end
709 end
710 end
710 end
711 end
711
712
712 # Returns a new unsaved Project instance with attributes copied from +project+
713 # Returns a new unsaved Project instance with attributes copied from +project+
713 def self.copy_from(project)
714 def self.copy_from(project)
714 project = project.is_a?(Project) ? project : Project.find(project)
715 project = project.is_a?(Project) ? project : Project.find(project)
715 # clear unique attributes
716 # clear unique attributes
716 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
717 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
717 copy = Project.new(attributes)
718 copy = Project.new(attributes)
718 copy.enabled_modules = project.enabled_modules
719 copy.enabled_modules = project.enabled_modules
719 copy.trackers = project.trackers
720 copy.trackers = project.trackers
720 copy.custom_values = project.custom_values.collect {|v| v.clone}
721 copy.custom_values = project.custom_values.collect {|v| v.clone}
721 copy.issue_custom_fields = project.issue_custom_fields
722 copy.issue_custom_fields = project.issue_custom_fields
722 copy
723 copy
723 end
724 end
724
725
725 # Yields the given block for each project with its level in the tree
726 # Yields the given block for each project with its level in the tree
726 def self.project_tree(projects, &block)
727 def self.project_tree(projects, &block)
727 ancestors = []
728 ancestors = []
728 projects.sort_by(&:lft).each do |project|
729 projects.sort_by(&:lft).each do |project|
729 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
730 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
730 ancestors.pop
731 ancestors.pop
731 end
732 end
732 yield project, ancestors.size
733 yield project, ancestors.size
733 ancestors << project
734 ancestors << project
734 end
735 end
735 end
736 end
736
737
737 private
738 private
738
739
739 def after_parent_changed(parent_was)
740 def after_parent_changed(parent_was)
740 remove_inherited_member_roles
741 remove_inherited_member_roles
741 add_inherited_member_roles
742 add_inherited_member_roles
742 end
743 end
743
744
744 def update_inherited_members
745 def update_inherited_members
745 if parent
746 if parent
746 if inherit_members? && !inherit_members_was
747 if inherit_members? && !inherit_members_was
747 remove_inherited_member_roles
748 remove_inherited_member_roles
748 add_inherited_member_roles
749 add_inherited_member_roles
749 elsif !inherit_members? && inherit_members_was
750 elsif !inherit_members? && inherit_members_was
750 remove_inherited_member_roles
751 remove_inherited_member_roles
751 end
752 end
752 end
753 end
753 end
754 end
754
755
755 def remove_inherited_member_roles
756 def remove_inherited_member_roles
756 member_roles = memberships.map(&:member_roles).flatten
757 member_roles = memberships.map(&:member_roles).flatten
757 member_role_ids = member_roles.map(&:id)
758 member_role_ids = member_roles.map(&:id)
758 member_roles.each do |member_role|
759 member_roles.each do |member_role|
759 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
760 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
760 member_role.destroy
761 member_role.destroy
761 end
762 end
762 end
763 end
763 end
764 end
764
765
765 def add_inherited_member_roles
766 def add_inherited_member_roles
766 if inherit_members? && parent
767 if inherit_members? && parent
767 parent.memberships.each do |parent_member|
768 parent.memberships.each do |parent_member|
768 member = Member.find_or_new(self.id, parent_member.user_id)
769 member = Member.find_or_new(self.id, parent_member.user_id)
769 parent_member.member_roles.each do |parent_member_role|
770 parent_member.member_roles.each do |parent_member_role|
770 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
771 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
771 end
772 end
772 member.save!
773 member.save!
773 end
774 end
774 end
775 end
775 end
776 end
776
777
777 # Copies wiki from +project+
778 # Copies wiki from +project+
778 def copy_wiki(project)
779 def copy_wiki(project)
779 # Check that the source project has a wiki first
780 # Check that the source project has a wiki first
780 unless project.wiki.nil?
781 unless project.wiki.nil?
781 wiki = self.wiki || Wiki.new
782 wiki = self.wiki || Wiki.new
782 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
783 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
783 wiki_pages_map = {}
784 wiki_pages_map = {}
784 project.wiki.pages.each do |page|
785 project.wiki.pages.each do |page|
785 # Skip pages without content
786 # Skip pages without content
786 next if page.content.nil?
787 next if page.content.nil?
787 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
788 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
788 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
789 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
789 new_wiki_page.content = new_wiki_content
790 new_wiki_page.content = new_wiki_content
790 wiki.pages << new_wiki_page
791 wiki.pages << new_wiki_page
791 wiki_pages_map[page.id] = new_wiki_page
792 wiki_pages_map[page.id] = new_wiki_page
792 end
793 end
793
794
794 self.wiki = wiki
795 self.wiki = wiki
795 wiki.save
796 wiki.save
796 # Reproduce page hierarchy
797 # Reproduce page hierarchy
797 project.wiki.pages.each do |page|
798 project.wiki.pages.each do |page|
798 if page.parent_id && wiki_pages_map[page.id]
799 if page.parent_id && wiki_pages_map[page.id]
799 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
800 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
800 wiki_pages_map[page.id].save
801 wiki_pages_map[page.id].save
801 end
802 end
802 end
803 end
803 end
804 end
804 end
805 end
805
806
806 # Copies versions from +project+
807 # Copies versions from +project+
807 def copy_versions(project)
808 def copy_versions(project)
808 project.versions.each do |version|
809 project.versions.each do |version|
809 new_version = Version.new
810 new_version = Version.new
810 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
811 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
811 self.versions << new_version
812 self.versions << new_version
812 end
813 end
813 end
814 end
814
815
815 # Copies issue categories from +project+
816 # Copies issue categories from +project+
816 def copy_issue_categories(project)
817 def copy_issue_categories(project)
817 project.issue_categories.each do |issue_category|
818 project.issue_categories.each do |issue_category|
818 new_issue_category = IssueCategory.new
819 new_issue_category = IssueCategory.new
819 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
820 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
820 self.issue_categories << new_issue_category
821 self.issue_categories << new_issue_category
821 end
822 end
822 end
823 end
823
824
824 # Copies issues from +project+
825 # Copies issues from +project+
825 def copy_issues(project)
826 def copy_issues(project)
826 # Stores the source issue id as a key and the copied issues as the
827 # Stores the source issue id as a key and the copied issues as the
827 # value. Used to map the two togeather for issue relations.
828 # value. Used to map the two togeather for issue relations.
828 issues_map = {}
829 issues_map = {}
829
830
830 # Store status and reopen locked/closed versions
831 # Store status and reopen locked/closed versions
831 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
832 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
832 version_statuses.each do |version, status|
833 version_statuses.each do |version, status|
833 version.update_attribute :status, 'open'
834 version.update_attribute :status, 'open'
834 end
835 end
835
836
836 # Get issues sorted by root_id, lft so that parent issues
837 # Get issues sorted by root_id, lft so that parent issues
837 # get copied before their children
838 # get copied before their children
838 project.issues.reorder('root_id, lft').all.each do |issue|
839 project.issues.reorder('root_id, lft').all.each do |issue|
839 new_issue = Issue.new
840 new_issue = Issue.new
840 new_issue.copy_from(issue, :subtasks => false, :link => false)
841 new_issue.copy_from(issue, :subtasks => false, :link => false)
841 new_issue.project = self
842 new_issue.project = self
842 # Reassign fixed_versions by name, since names are unique per project
843 # Reassign fixed_versions by name, since names are unique per project
843 if issue.fixed_version && issue.fixed_version.project == project
844 if issue.fixed_version && issue.fixed_version.project == project
844 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
845 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
845 end
846 end
846 # Reassign the category by name, since names are unique per project
847 # Reassign the category by name, since names are unique per project
847 if issue.category
848 if issue.category
848 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
849 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
849 end
850 end
850 # Parent issue
851 # Parent issue
851 if issue.parent_id
852 if issue.parent_id
852 if copied_parent = issues_map[issue.parent_id]
853 if copied_parent = issues_map[issue.parent_id]
853 new_issue.parent_issue_id = copied_parent.id
854 new_issue.parent_issue_id = copied_parent.id
854 end
855 end
855 end
856 end
856
857
857 self.issues << new_issue
858 self.issues << new_issue
858 if new_issue.new_record?
859 if new_issue.new_record?
859 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
860 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
860 else
861 else
861 issues_map[issue.id] = new_issue unless new_issue.new_record?
862 issues_map[issue.id] = new_issue unless new_issue.new_record?
862 end
863 end
863 end
864 end
864
865
865 # Restore locked/closed version statuses
866 # Restore locked/closed version statuses
866 version_statuses.each do |version, status|
867 version_statuses.each do |version, status|
867 version.update_attribute :status, status
868 version.update_attribute :status, status
868 end
869 end
869
870
870 # Relations after in case issues related each other
871 # Relations after in case issues related each other
871 project.issues.each do |issue|
872 project.issues.each do |issue|
872 new_issue = issues_map[issue.id]
873 new_issue = issues_map[issue.id]
873 unless new_issue
874 unless new_issue
874 # Issue was not copied
875 # Issue was not copied
875 next
876 next
876 end
877 end
877
878
878 # Relations
879 # Relations
879 issue.relations_from.each do |source_relation|
880 issue.relations_from.each do |source_relation|
880 new_issue_relation = IssueRelation.new
881 new_issue_relation = IssueRelation.new
881 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
882 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
882 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
883 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
883 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
884 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
884 new_issue_relation.issue_to = source_relation.issue_to
885 new_issue_relation.issue_to = source_relation.issue_to
885 end
886 end
886 new_issue.relations_from << new_issue_relation
887 new_issue.relations_from << new_issue_relation
887 end
888 end
888
889
889 issue.relations_to.each do |source_relation|
890 issue.relations_to.each do |source_relation|
890 new_issue_relation = IssueRelation.new
891 new_issue_relation = IssueRelation.new
891 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
892 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
892 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
893 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
893 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
894 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
894 new_issue_relation.issue_from = source_relation.issue_from
895 new_issue_relation.issue_from = source_relation.issue_from
895 end
896 end
896 new_issue.relations_to << new_issue_relation
897 new_issue.relations_to << new_issue_relation
897 end
898 end
898 end
899 end
899 end
900 end
900
901
901 # Copies members from +project+
902 # Copies members from +project+
902 def copy_members(project)
903 def copy_members(project)
903 # Copy users first, then groups to handle members with inherited and given roles
904 # Copy users first, then groups to handle members with inherited and given roles
904 members_to_copy = []
905 members_to_copy = []
905 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
906 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
906 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
907 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
907
908
908 members_to_copy.each do |member|
909 members_to_copy.each do |member|
909 new_member = Member.new
910 new_member = Member.new
910 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
911 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
911 # only copy non inherited roles
912 # only copy non inherited roles
912 # inherited roles will be added when copying the group membership
913 # inherited roles will be added when copying the group membership
913 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
914 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
914 next if role_ids.empty?
915 next if role_ids.empty?
915 new_member.role_ids = role_ids
916 new_member.role_ids = role_ids
916 new_member.project = self
917 new_member.project = self
917 self.members << new_member
918 self.members << new_member
918 end
919 end
919 end
920 end
920
921
921 # Copies queries from +project+
922 # Copies queries from +project+
922 def copy_queries(project)
923 def copy_queries(project)
923 project.queries.each do |query|
924 project.queries.each do |query|
924 new_query = IssueQuery.new
925 new_query = IssueQuery.new
925 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
926 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
926 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
927 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
927 new_query.project = self
928 new_query.project = self
928 new_query.user_id = query.user_id
929 new_query.user_id = query.user_id
929 self.queries << new_query
930 self.queries << new_query
930 end
931 end
931 end
932 end
932
933
933 # Copies boards from +project+
934 # Copies boards from +project+
934 def copy_boards(project)
935 def copy_boards(project)
935 project.boards.each do |board|
936 project.boards.each do |board|
936 new_board = Board.new
937 new_board = Board.new
937 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
938 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
938 new_board.project = self
939 new_board.project = self
939 self.boards << new_board
940 self.boards << new_board
940 end
941 end
941 end
942 end
942
943
943 def allowed_permissions
944 def allowed_permissions
944 @allowed_permissions ||= begin
945 @allowed_permissions ||= begin
945 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
946 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
946 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
947 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
947 end
948 end
948 end
949 end
949
950
950 def allowed_actions
951 def allowed_actions
951 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
952 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
952 end
953 end
953
954
954 # Returns all the active Systemwide and project specific activities
955 # Returns all the active Systemwide and project specific activities
955 def active_activities
956 def active_activities
956 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
957 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
957
958
958 if overridden_activity_ids.empty?
959 if overridden_activity_ids.empty?
959 return TimeEntryActivity.shared.active
960 return TimeEntryActivity.shared.active
960 else
961 else
961 return system_activities_and_project_overrides
962 return system_activities_and_project_overrides
962 end
963 end
963 end
964 end
964
965
965 # Returns all the Systemwide and project specific activities
966 # Returns all the Systemwide and project specific activities
966 # (inactive and active)
967 # (inactive and active)
967 def all_activities
968 def all_activities
968 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
969 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
969
970
970 if overridden_activity_ids.empty?
971 if overridden_activity_ids.empty?
971 return TimeEntryActivity.shared
972 return TimeEntryActivity.shared
972 else
973 else
973 return system_activities_and_project_overrides(true)
974 return system_activities_and_project_overrides(true)
974 end
975 end
975 end
976 end
976
977
977 # Returns the systemwide active activities merged with the project specific overrides
978 # Returns the systemwide active activities merged with the project specific overrides
978 def system_activities_and_project_overrides(include_inactive=false)
979 def system_activities_and_project_overrides(include_inactive=false)
979 if include_inactive
980 if include_inactive
980 return TimeEntryActivity.shared.
981 return TimeEntryActivity.shared.
981 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
982 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
982 self.time_entry_activities
983 self.time_entry_activities
983 else
984 else
984 return TimeEntryActivity.shared.active.
985 return TimeEntryActivity.shared.active.
985 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
986 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
986 self.time_entry_activities.active
987 self.time_entry_activities.active
987 end
988 end
988 end
989 end
989
990
990 # Archives subprojects recursively
991 # Archives subprojects recursively
991 def archive!
992 def archive!
992 children.each do |subproject|
993 children.each do |subproject|
993 subproject.send :archive!
994 subproject.send :archive!
994 end
995 end
995 update_attribute :status, STATUS_ARCHIVED
996 update_attribute :status, STATUS_ARCHIVED
996 end
997 end
997
998
998 def update_position_under_parent
999 def update_position_under_parent
999 set_or_update_position_under(parent)
1000 set_or_update_position_under(parent)
1000 end
1001 end
1001
1002
1002 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1003 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1003 def set_or_update_position_under(target_parent)
1004 def set_or_update_position_under(target_parent)
1004 parent_was = parent
1005 parent_was = parent
1005 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1006 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1006 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1007 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1007
1008
1008 if to_be_inserted_before
1009 if to_be_inserted_before
1009 move_to_left_of(to_be_inserted_before)
1010 move_to_left_of(to_be_inserted_before)
1010 elsif target_parent.nil?
1011 elsif target_parent.nil?
1011 if sibs.empty?
1012 if sibs.empty?
1012 # move_to_root adds the project in first (ie. left) position
1013 # move_to_root adds the project in first (ie. left) position
1013 move_to_root
1014 move_to_root
1014 else
1015 else
1015 move_to_right_of(sibs.last) unless self == sibs.last
1016 move_to_right_of(sibs.last) unless self == sibs.last
1016 end
1017 end
1017 else
1018 else
1018 # move_to_child_of adds the project in last (ie.right) position
1019 # move_to_child_of adds the project in last (ie.right) position
1019 move_to_child_of(target_parent)
1020 move_to_child_of(target_parent)
1020 end
1021 end
1021 if parent_was != target_parent
1022 if parent_was != target_parent
1022 after_parent_changed(parent_was)
1023 after_parent_changed(parent_was)
1023 end
1024 end
1024 end
1025 end
1025 end
1026 end
@@ -1,698 +1,699
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Different ways of displaying/sorting users
23 # Different ways of displaying/sorting users
24 USER_FORMATS = {
24 USER_FORMATS = {
25 :firstname_lastname => {
25 :firstname_lastname => {
26 :string => '#{firstname} #{lastname}',
26 :string => '#{firstname} #{lastname}',
27 :order => %w(firstname lastname id),
27 :order => %w(firstname lastname id),
28 :setting_order => 1
28 :setting_order => 1
29 },
29 },
30 :firstname_lastinitial => {
30 :firstname_lastinitial => {
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 :order => %w(firstname lastname id),
32 :order => %w(firstname lastname id),
33 :setting_order => 2
33 :setting_order => 2
34 },
34 },
35 :firstname => {
35 :firstname => {
36 :string => '#{firstname}',
36 :string => '#{firstname}',
37 :order => %w(firstname id),
37 :order => %w(firstname id),
38 :setting_order => 3
38 :setting_order => 3
39 },
39 },
40 :lastname_firstname => {
40 :lastname_firstname => {
41 :string => '#{lastname} #{firstname}',
41 :string => '#{lastname} #{firstname}',
42 :order => %w(lastname firstname id),
42 :order => %w(lastname firstname id),
43 :setting_order => 4
43 :setting_order => 4
44 },
44 },
45 :lastname_coma_firstname => {
45 :lastname_coma_firstname => {
46 :string => '#{lastname}, #{firstname}',
46 :string => '#{lastname}, #{firstname}',
47 :order => %w(lastname firstname id),
47 :order => %w(lastname firstname id),
48 :setting_order => 5
48 :setting_order => 5
49 },
49 },
50 :lastname => {
50 :lastname => {
51 :string => '#{lastname}',
51 :string => '#{lastname}',
52 :order => %w(lastname id),
52 :order => %w(lastname id),
53 :setting_order => 6
53 :setting_order => 6
54 },
54 },
55 :username => {
55 :username => {
56 :string => '#{login}',
56 :string => '#{login}',
57 :order => %w(login id),
57 :order => %w(login id),
58 :setting_order => 7
58 :setting_order => 7
59 },
59 },
60 }
60 }
61
61
62 MAIL_NOTIFICATION_OPTIONS = [
62 MAIL_NOTIFICATION_OPTIONS = [
63 ['all', :label_user_mail_option_all],
63 ['all', :label_user_mail_option_all],
64 ['selected', :label_user_mail_option_selected],
64 ['selected', :label_user_mail_option_selected],
65 ['only_my_events', :label_user_mail_option_only_my_events],
65 ['only_my_events', :label_user_mail_option_only_my_events],
66 ['only_assigned', :label_user_mail_option_only_assigned],
66 ['only_assigned', :label_user_mail_option_only_assigned],
67 ['only_owner', :label_user_mail_option_only_owner],
67 ['only_owner', :label_user_mail_option_only_owner],
68 ['none', :label_user_mail_option_none]
68 ['none', :label_user_mail_option_none]
69 ]
69 ]
70
70
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
73 has_many :changesets, :dependent => :nullify
73 has_many :changesets, :dependent => :nullify
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
77 belongs_to :auth_source
77 belongs_to :auth_source
78
78
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
81
81
82 acts_as_customizable
82 acts_as_customizable
83
83
84 attr_accessor :password, :password_confirmation
84 attr_accessor :password, :password_confirmation
85 attr_accessor :last_before_login_on
85 attr_accessor :last_before_login_on
86 # Prevents unauthorized assignments
86 # Prevents unauthorized assignments
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
88
88
89 LOGIN_LENGTH_LIMIT = 60
89 LOGIN_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
91
91
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
95 # Login must contain letters, numbers, underscores only
95 # Login must contain letters, numbers, underscores only
96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
98 validates_length_of :firstname, :lastname, :maximum => 30
98 validates_length_of :firstname, :lastname, :maximum => 30
99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
103 validate :validate_password_length
103 validate :validate_password_length
104
104
105 before_create :set_mail_notification
105 before_create :set_mail_notification
106 before_save :update_hashed_password
106 before_save :update_hashed_password
107 before_destroy :remove_references_before_destroy
107 before_destroy :remove_references_before_destroy
108
108
109 scope :in_group, lambda {|group|
109 scope :in_group, lambda {|group|
110 group_id = group.is_a?(Group) ? group.id : group.to_i
110 group_id = group.is_a?(Group) ? group.id : group.to_i
111 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)
111 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
112 }
112 }
113 scope :not_in_group, lambda {|group|
113 scope :not_in_group, lambda {|group|
114 group_id = group.is_a?(Group) ? group.id : group.to_i
114 group_id = group.is_a?(Group) ? group.id : group.to_i
115 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)
115 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
116 }
116 }
117 scope :sorted, lambda { order(*User.fields_for_order_statement)}
117 scope :sorted, lambda { order(*User.fields_for_order_statement)}
118
118
119 def set_mail_notification
119 def set_mail_notification
120 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
120 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
121 true
121 true
122 end
122 end
123
123
124 def update_hashed_password
124 def update_hashed_password
125 # update hashed_password if password was set
125 # update hashed_password if password was set
126 if self.password && self.auth_source_id.blank?
126 if self.password && self.auth_source_id.blank?
127 salt_password(password)
127 salt_password(password)
128 end
128 end
129 end
129 end
130
130
131 alias :base_reload :reload
131 def reload(*args)
132 def reload(*args)
132 @name = nil
133 @name = nil
133 @projects_by_role = nil
134 @projects_by_role = nil
134 @membership_by_project_id = nil
135 @membership_by_project_id = nil
135 super
136 base_reload(*args)
136 end
137 end
137
138
138 def mail=(arg)
139 def mail=(arg)
139 write_attribute(:mail, arg.to_s.strip)
140 write_attribute(:mail, arg.to_s.strip)
140 end
141 end
141
142
142 def identity_url=(url)
143 def identity_url=(url)
143 if url.blank?
144 if url.blank?
144 write_attribute(:identity_url, '')
145 write_attribute(:identity_url, '')
145 else
146 else
146 begin
147 begin
147 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
148 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
148 rescue OpenIdAuthentication::InvalidOpenId
149 rescue OpenIdAuthentication::InvalidOpenId
149 # Invalid url, don't save
150 # Invalid url, don't save
150 end
151 end
151 end
152 end
152 self.read_attribute(:identity_url)
153 self.read_attribute(:identity_url)
153 end
154 end
154
155
155 # Returns the user that matches provided login and password, or nil
156 # Returns the user that matches provided login and password, or nil
156 def self.try_to_login(login, password)
157 def self.try_to_login(login, password)
157 login = login.to_s
158 login = login.to_s
158 password = password.to_s
159 password = password.to_s
159
160
160 # Make sure no one can sign in with an empty login or password
161 # Make sure no one can sign in with an empty login or password
161 return nil if login.empty? || password.empty?
162 return nil if login.empty? || password.empty?
162 user = find_by_login(login)
163 user = find_by_login(login)
163 if user
164 if user
164 # user is already in local database
165 # user is already in local database
165 return nil unless user.active?
166 return nil unless user.active?
166 return nil unless user.check_password?(password)
167 return nil unless user.check_password?(password)
167 else
168 else
168 # user is not yet registered, try to authenticate with available sources
169 # user is not yet registered, try to authenticate with available sources
169 attrs = AuthSource.authenticate(login, password)
170 attrs = AuthSource.authenticate(login, password)
170 if attrs
171 if attrs
171 user = new(attrs)
172 user = new(attrs)
172 user.login = login
173 user.login = login
173 user.language = Setting.default_language
174 user.language = Setting.default_language
174 if user.save
175 if user.save
175 user.reload
176 user.reload
176 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
177 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
177 end
178 end
178 end
179 end
179 end
180 end
180 user.update_column(:last_login_on, Time.now) if user && !user.new_record?
181 user.update_column(:last_login_on, Time.now) if user && !user.new_record?
181 user
182 user
182 rescue => text
183 rescue => text
183 raise text
184 raise text
184 end
185 end
185
186
186 # Returns the user who matches the given autologin +key+ or nil
187 # Returns the user who matches the given autologin +key+ or nil
187 def self.try_to_autologin(key)
188 def self.try_to_autologin(key)
188 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
189 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
189 if user
190 if user
190 user.update_column(:last_login_on, Time.now)
191 user.update_column(:last_login_on, Time.now)
191 user
192 user
192 end
193 end
193 end
194 end
194
195
195 def self.name_formatter(formatter = nil)
196 def self.name_formatter(formatter = nil)
196 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
197 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
197 end
198 end
198
199
199 # Returns an array of fields names than can be used to make an order statement for users
200 # Returns an array of fields names than can be used to make an order statement for users
200 # according to how user names are displayed
201 # according to how user names are displayed
201 # Examples:
202 # Examples:
202 #
203 #
203 # User.fields_for_order_statement => ['users.login', 'users.id']
204 # User.fields_for_order_statement => ['users.login', 'users.id']
204 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
205 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
205 def self.fields_for_order_statement(table=nil)
206 def self.fields_for_order_statement(table=nil)
206 table ||= table_name
207 table ||= table_name
207 name_formatter[:order].map {|field| "#{table}.#{field}"}
208 name_formatter[:order].map {|field| "#{table}.#{field}"}
208 end
209 end
209
210
210 # Return user's full name for display
211 # Return user's full name for display
211 def name(formatter = nil)
212 def name(formatter = nil)
212 f = self.class.name_formatter(formatter)
213 f = self.class.name_formatter(formatter)
213 if formatter
214 if formatter
214 eval('"' + f[:string] + '"')
215 eval('"' + f[:string] + '"')
215 else
216 else
216 @name ||= eval('"' + f[:string] + '"')
217 @name ||= eval('"' + f[:string] + '"')
217 end
218 end
218 end
219 end
219
220
220 def active?
221 def active?
221 self.status == STATUS_ACTIVE
222 self.status == STATUS_ACTIVE
222 end
223 end
223
224
224 def registered?
225 def registered?
225 self.status == STATUS_REGISTERED
226 self.status == STATUS_REGISTERED
226 end
227 end
227
228
228 def locked?
229 def locked?
229 self.status == STATUS_LOCKED
230 self.status == STATUS_LOCKED
230 end
231 end
231
232
232 def activate
233 def activate
233 self.status = STATUS_ACTIVE
234 self.status = STATUS_ACTIVE
234 end
235 end
235
236
236 def register
237 def register
237 self.status = STATUS_REGISTERED
238 self.status = STATUS_REGISTERED
238 end
239 end
239
240
240 def lock
241 def lock
241 self.status = STATUS_LOCKED
242 self.status = STATUS_LOCKED
242 end
243 end
243
244
244 def activate!
245 def activate!
245 update_attribute(:status, STATUS_ACTIVE)
246 update_attribute(:status, STATUS_ACTIVE)
246 end
247 end
247
248
248 def register!
249 def register!
249 update_attribute(:status, STATUS_REGISTERED)
250 update_attribute(:status, STATUS_REGISTERED)
250 end
251 end
251
252
252 def lock!
253 def lock!
253 update_attribute(:status, STATUS_LOCKED)
254 update_attribute(:status, STATUS_LOCKED)
254 end
255 end
255
256
256 # Returns true if +clear_password+ is the correct user's password, otherwise false
257 # Returns true if +clear_password+ is the correct user's password, otherwise false
257 def check_password?(clear_password)
258 def check_password?(clear_password)
258 if auth_source_id.present?
259 if auth_source_id.present?
259 auth_source.authenticate(self.login, clear_password)
260 auth_source.authenticate(self.login, clear_password)
260 else
261 else
261 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
262 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
262 end
263 end
263 end
264 end
264
265
265 # Generates a random salt and computes hashed_password for +clear_password+
266 # Generates a random salt and computes hashed_password for +clear_password+
266 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
267 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
267 def salt_password(clear_password)
268 def salt_password(clear_password)
268 self.salt = User.generate_salt
269 self.salt = User.generate_salt
269 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
270 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
270 end
271 end
271
272
272 # Does the backend storage allow this user to change their password?
273 # Does the backend storage allow this user to change their password?
273 def change_password_allowed?
274 def change_password_allowed?
274 return true if auth_source.nil?
275 return true if auth_source.nil?
275 return auth_source.allow_password_changes?
276 return auth_source.allow_password_changes?
276 end
277 end
277
278
278 # Generate and set a random password. Useful for automated user creation
279 # Generate and set a random password. Useful for automated user creation
279 # Based on Token#generate_token_value
280 # Based on Token#generate_token_value
280 #
281 #
281 def random_password
282 def random_password
282 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
283 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
283 password = ''
284 password = ''
284 40.times { |i| password << chars[rand(chars.size-1)] }
285 40.times { |i| password << chars[rand(chars.size-1)] }
285 self.password = password
286 self.password = password
286 self.password_confirmation = password
287 self.password_confirmation = password
287 self
288 self
288 end
289 end
289
290
290 def pref
291 def pref
291 self.preference ||= UserPreference.new(:user => self)
292 self.preference ||= UserPreference.new(:user => self)
292 end
293 end
293
294
294 def time_zone
295 def time_zone
295 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
296 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
296 end
297 end
297
298
298 def wants_comments_in_reverse_order?
299 def wants_comments_in_reverse_order?
299 self.pref[:comments_sorting] == 'desc'
300 self.pref[:comments_sorting] == 'desc'
300 end
301 end
301
302
302 # Return user's RSS key (a 40 chars long string), used to access feeds
303 # Return user's RSS key (a 40 chars long string), used to access feeds
303 def rss_key
304 def rss_key
304 if rss_token.nil?
305 if rss_token.nil?
305 create_rss_token(:action => 'feeds')
306 create_rss_token(:action => 'feeds')
306 end
307 end
307 rss_token.value
308 rss_token.value
308 end
309 end
309
310
310 # Return user's API key (a 40 chars long string), used to access the API
311 # Return user's API key (a 40 chars long string), used to access the API
311 def api_key
312 def api_key
312 if api_token.nil?
313 if api_token.nil?
313 create_api_token(:action => 'api')
314 create_api_token(:action => 'api')
314 end
315 end
315 api_token.value
316 api_token.value
316 end
317 end
317
318
318 # Return an array of project ids for which the user has explicitly turned mail notifications on
319 # Return an array of project ids for which the user has explicitly turned mail notifications on
319 def notified_projects_ids
320 def notified_projects_ids
320 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
321 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
321 end
322 end
322
323
323 def notified_project_ids=(ids)
324 def notified_project_ids=(ids)
324 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
325 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
325 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
326 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
326 @notified_projects_ids = nil
327 @notified_projects_ids = nil
327 notified_projects_ids
328 notified_projects_ids
328 end
329 end
329
330
330 def valid_notification_options
331 def valid_notification_options
331 self.class.valid_notification_options(self)
332 self.class.valid_notification_options(self)
332 end
333 end
333
334
334 # Only users that belong to more than 1 project can select projects for which they are notified
335 # Only users that belong to more than 1 project can select projects for which they are notified
335 def self.valid_notification_options(user=nil)
336 def self.valid_notification_options(user=nil)
336 # Note that @user.membership.size would fail since AR ignores
337 # Note that @user.membership.size would fail since AR ignores
337 # :include association option when doing a count
338 # :include association option when doing a count
338 if user.nil? || user.memberships.length < 1
339 if user.nil? || user.memberships.length < 1
339 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
340 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
340 else
341 else
341 MAIL_NOTIFICATION_OPTIONS
342 MAIL_NOTIFICATION_OPTIONS
342 end
343 end
343 end
344 end
344
345
345 # Find a user account by matching the exact login and then a case-insensitive
346 # Find a user account by matching the exact login and then a case-insensitive
346 # version. Exact matches will be given priority.
347 # version. Exact matches will be given priority.
347 def self.find_by_login(login)
348 def self.find_by_login(login)
348 if login.present?
349 if login.present?
349 login = login.to_s
350 login = login.to_s
350 # First look for an exact match
351 # First look for an exact match
351 user = where(:login => login).all.detect {|u| u.login == login}
352 user = where(:login => login).all.detect {|u| u.login == login}
352 unless user
353 unless user
353 # Fail over to case-insensitive if none was found
354 # Fail over to case-insensitive if none was found
354 user = where("LOWER(login) = ?", login.downcase).first
355 user = where("LOWER(login) = ?", login.downcase).first
355 end
356 end
356 user
357 user
357 end
358 end
358 end
359 end
359
360
360 def self.find_by_rss_key(key)
361 def self.find_by_rss_key(key)
361 Token.find_active_user('feeds', key)
362 Token.find_active_user('feeds', key)
362 end
363 end
363
364
364 def self.find_by_api_key(key)
365 def self.find_by_api_key(key)
365 Token.find_active_user('api', key)
366 Token.find_active_user('api', key)
366 end
367 end
367
368
368 # Makes find_by_mail case-insensitive
369 # Makes find_by_mail case-insensitive
369 def self.find_by_mail(mail)
370 def self.find_by_mail(mail)
370 where("LOWER(mail) = ?", mail.to_s.downcase).first
371 where("LOWER(mail) = ?", mail.to_s.downcase).first
371 end
372 end
372
373
373 # Returns true if the default admin account can no longer be used
374 # Returns true if the default admin account can no longer be used
374 def self.default_admin_account_changed?
375 def self.default_admin_account_changed?
375 !User.active.find_by_login("admin").try(:check_password?, "admin")
376 !User.active.find_by_login("admin").try(:check_password?, "admin")
376 end
377 end
377
378
378 def to_s
379 def to_s
379 name
380 name
380 end
381 end
381
382
382 CSS_CLASS_BY_STATUS = {
383 CSS_CLASS_BY_STATUS = {
383 STATUS_ANONYMOUS => 'anon',
384 STATUS_ANONYMOUS => 'anon',
384 STATUS_ACTIVE => 'active',
385 STATUS_ACTIVE => 'active',
385 STATUS_REGISTERED => 'registered',
386 STATUS_REGISTERED => 'registered',
386 STATUS_LOCKED => 'locked'
387 STATUS_LOCKED => 'locked'
387 }
388 }
388
389
389 def css_classes
390 def css_classes
390 "user #{CSS_CLASS_BY_STATUS[status]}"
391 "user #{CSS_CLASS_BY_STATUS[status]}"
391 end
392 end
392
393
393 # Returns the current day according to user's time zone
394 # Returns the current day according to user's time zone
394 def today
395 def today
395 if time_zone.nil?
396 if time_zone.nil?
396 Date.today
397 Date.today
397 else
398 else
398 Time.now.in_time_zone(time_zone).to_date
399 Time.now.in_time_zone(time_zone).to_date
399 end
400 end
400 end
401 end
401
402
402 # Returns the day of +time+ according to user's time zone
403 # Returns the day of +time+ according to user's time zone
403 def time_to_date(time)
404 def time_to_date(time)
404 if time_zone.nil?
405 if time_zone.nil?
405 time.to_date
406 time.to_date
406 else
407 else
407 time.in_time_zone(time_zone).to_date
408 time.in_time_zone(time_zone).to_date
408 end
409 end
409 end
410 end
410
411
411 def logged?
412 def logged?
412 true
413 true
413 end
414 end
414
415
415 def anonymous?
416 def anonymous?
416 !logged?
417 !logged?
417 end
418 end
418
419
419 # Returns user's membership for the given project
420 # Returns user's membership for the given project
420 # or nil if the user is not a member of project
421 # or nil if the user is not a member of project
421 def membership(project)
422 def membership(project)
422 project_id = project.is_a?(Project) ? project.id : project
423 project_id = project.is_a?(Project) ? project.id : project
423
424
424 @membership_by_project_id ||= Hash.new {|h, project_id|
425 @membership_by_project_id ||= Hash.new {|h, project_id|
425 h[project_id] = memberships.where(:project_id => project_id).first
426 h[project_id] = memberships.where(:project_id => project_id).first
426 }
427 }
427 @membership_by_project_id[project_id]
428 @membership_by_project_id[project_id]
428 end
429 end
429
430
430 # Return user's roles for project
431 # Return user's roles for project
431 def roles_for_project(project)
432 def roles_for_project(project)
432 roles = []
433 roles = []
433 # No role on archived projects
434 # No role on archived projects
434 return roles if project.nil? || project.archived?
435 return roles if project.nil? || project.archived?
435 if logged?
436 if logged?
436 # Find project membership
437 # Find project membership
437 membership = membership(project)
438 membership = membership(project)
438 if membership
439 if membership
439 roles = membership.roles
440 roles = membership.roles
440 else
441 else
441 @role_non_member ||= Role.non_member
442 @role_non_member ||= Role.non_member
442 roles << @role_non_member
443 roles << @role_non_member
443 end
444 end
444 else
445 else
445 @role_anonymous ||= Role.anonymous
446 @role_anonymous ||= Role.anonymous
446 roles << @role_anonymous
447 roles << @role_anonymous
447 end
448 end
448 roles
449 roles
449 end
450 end
450
451
451 # Return true if the user is a member of project
452 # Return true if the user is a member of project
452 def member_of?(project)
453 def member_of?(project)
453 roles_for_project(project).any? {|role| role.member?}
454 roles_for_project(project).any? {|role| role.member?}
454 end
455 end
455
456
456 # Returns a hash of user's projects grouped by roles
457 # Returns a hash of user's projects grouped by roles
457 def projects_by_role
458 def projects_by_role
458 return @projects_by_role if @projects_by_role
459 return @projects_by_role if @projects_by_role
459
460
460 @projects_by_role = Hash.new([])
461 @projects_by_role = Hash.new([])
461 memberships.each do |membership|
462 memberships.each do |membership|
462 if membership.project
463 if membership.project
463 membership.roles.each do |role|
464 membership.roles.each do |role|
464 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
465 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
465 @projects_by_role[role] << membership.project
466 @projects_by_role[role] << membership.project
466 end
467 end
467 end
468 end
468 end
469 end
469 @projects_by_role.each do |role, projects|
470 @projects_by_role.each do |role, projects|
470 projects.uniq!
471 projects.uniq!
471 end
472 end
472
473
473 @projects_by_role
474 @projects_by_role
474 end
475 end
475
476
476 # Returns true if user is arg or belongs to arg
477 # Returns true if user is arg or belongs to arg
477 def is_or_belongs_to?(arg)
478 def is_or_belongs_to?(arg)
478 if arg.is_a?(User)
479 if arg.is_a?(User)
479 self == arg
480 self == arg
480 elsif arg.is_a?(Group)
481 elsif arg.is_a?(Group)
481 arg.users.include?(self)
482 arg.users.include?(self)
482 else
483 else
483 false
484 false
484 end
485 end
485 end
486 end
486
487
487 # Return true if the user is allowed to do the specified action on a specific context
488 # Return true if the user is allowed to do the specified action on a specific context
488 # Action can be:
489 # Action can be:
489 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
490 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
490 # * a permission Symbol (eg. :edit_project)
491 # * a permission Symbol (eg. :edit_project)
491 # Context can be:
492 # Context can be:
492 # * a project : returns true if user is allowed to do the specified action on this project
493 # * a project : returns true if user is allowed to do the specified action on this project
493 # * an array of projects : returns true if user is allowed on every project
494 # * an array of projects : returns true if user is allowed on every project
494 # * nil with options[:global] set : check if user has at least one role allowed for this action,
495 # * nil with options[:global] set : check if user has at least one role allowed for this action,
495 # or falls back to Non Member / Anonymous permissions depending if the user is logged
496 # or falls back to Non Member / Anonymous permissions depending if the user is logged
496 def allowed_to?(action, context, options={}, &block)
497 def allowed_to?(action, context, options={}, &block)
497 if context && context.is_a?(Project)
498 if context && context.is_a?(Project)
498 return false unless context.allows_to?(action)
499 return false unless context.allows_to?(action)
499 # Admin users are authorized for anything else
500 # Admin users are authorized for anything else
500 return true if admin?
501 return true if admin?
501
502
502 roles = roles_for_project(context)
503 roles = roles_for_project(context)
503 return false unless roles
504 return false unless roles
504 roles.any? {|role|
505 roles.any? {|role|
505 (context.is_public? || role.member?) &&
506 (context.is_public? || role.member?) &&
506 role.allowed_to?(action) &&
507 role.allowed_to?(action) &&
507 (block_given? ? yield(role, self) : true)
508 (block_given? ? yield(role, self) : true)
508 }
509 }
509 elsif context && context.is_a?(Array)
510 elsif context && context.is_a?(Array)
510 if context.empty?
511 if context.empty?
511 false
512 false
512 else
513 else
513 # Authorize if user is authorized on every element of the array
514 # Authorize if user is authorized on every element of the array
514 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
515 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
515 end
516 end
516 elsif options[:global]
517 elsif options[:global]
517 # Admin users are always authorized
518 # Admin users are always authorized
518 return true if admin?
519 return true if admin?
519
520
520 # authorize if user has at least one role that has this permission
521 # authorize if user has at least one role that has this permission
521 roles = memberships.collect {|m| m.roles}.flatten.uniq
522 roles = memberships.collect {|m| m.roles}.flatten.uniq
522 roles << (self.logged? ? Role.non_member : Role.anonymous)
523 roles << (self.logged? ? Role.non_member : Role.anonymous)
523 roles.any? {|role|
524 roles.any? {|role|
524 role.allowed_to?(action) &&
525 role.allowed_to?(action) &&
525 (block_given? ? yield(role, self) : true)
526 (block_given? ? yield(role, self) : true)
526 }
527 }
527 else
528 else
528 false
529 false
529 end
530 end
530 end
531 end
531
532
532 # Is the user allowed to do the specified action on any project?
533 # Is the user allowed to do the specified action on any project?
533 # See allowed_to? for the actions and valid options.
534 # See allowed_to? for the actions and valid options.
534 def allowed_to_globally?(action, options, &block)
535 def allowed_to_globally?(action, options, &block)
535 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
536 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
536 end
537 end
537
538
538 # Returns true if the user is allowed to delete his own account
539 # Returns true if the user is allowed to delete his own account
539 def own_account_deletable?
540 def own_account_deletable?
540 Setting.unsubscribe? &&
541 Setting.unsubscribe? &&
541 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
542 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
542 end
543 end
543
544
544 safe_attributes 'login',
545 safe_attributes 'login',
545 'firstname',
546 'firstname',
546 'lastname',
547 'lastname',
547 'mail',
548 'mail',
548 'mail_notification',
549 'mail_notification',
549 'language',
550 'language',
550 'custom_field_values',
551 'custom_field_values',
551 'custom_fields',
552 'custom_fields',
552 'identity_url'
553 'identity_url'
553
554
554 safe_attributes 'status',
555 safe_attributes 'status',
555 'auth_source_id',
556 'auth_source_id',
556 :if => lambda {|user, current_user| current_user.admin?}
557 :if => lambda {|user, current_user| current_user.admin?}
557
558
558 safe_attributes 'group_ids',
559 safe_attributes 'group_ids',
559 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
560 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
560
561
561 # Utility method to help check if a user should be notified about an
562 # Utility method to help check if a user should be notified about an
562 # event.
563 # event.
563 #
564 #
564 # TODO: only supports Issue events currently
565 # TODO: only supports Issue events currently
565 def notify_about?(object)
566 def notify_about?(object)
566 if mail_notification == 'all'
567 if mail_notification == 'all'
567 true
568 true
568 elsif mail_notification.blank? || mail_notification == 'none'
569 elsif mail_notification.blank? || mail_notification == 'none'
569 false
570 false
570 else
571 else
571 case object
572 case object
572 when Issue
573 when Issue
573 case mail_notification
574 case mail_notification
574 when 'selected', 'only_my_events'
575 when 'selected', 'only_my_events'
575 # user receives notifications for created/assigned issues on unselected projects
576 # user receives notifications for created/assigned issues on unselected projects
576 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
577 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
577 when 'only_assigned'
578 when 'only_assigned'
578 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
579 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
579 when 'only_owner'
580 when 'only_owner'
580 object.author == self
581 object.author == self
581 end
582 end
582 when News
583 when News
583 # always send to project members except when mail_notification is set to 'none'
584 # always send to project members except when mail_notification is set to 'none'
584 true
585 true
585 end
586 end
586 end
587 end
587 end
588 end
588
589
589 def self.current=(user)
590 def self.current=(user)
590 Thread.current[:current_user] = user
591 Thread.current[:current_user] = user
591 end
592 end
592
593
593 def self.current
594 def self.current
594 Thread.current[:current_user] ||= User.anonymous
595 Thread.current[:current_user] ||= User.anonymous
595 end
596 end
596
597
597 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
598 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
598 # one anonymous user per database.
599 # one anonymous user per database.
599 def self.anonymous
600 def self.anonymous
600 anonymous_user = AnonymousUser.first
601 anonymous_user = AnonymousUser.first
601 if anonymous_user.nil?
602 if anonymous_user.nil?
602 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
603 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
603 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
604 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
604 end
605 end
605 anonymous_user
606 anonymous_user
606 end
607 end
607
608
608 # Salts all existing unsalted passwords
609 # Salts all existing unsalted passwords
609 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
610 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
610 # This method is used in the SaltPasswords migration and is to be kept as is
611 # This method is used in the SaltPasswords migration and is to be kept as is
611 def self.salt_unsalted_passwords!
612 def self.salt_unsalted_passwords!
612 transaction do
613 transaction do
613 User.where("salt IS NULL OR salt = ''").find_each do |user|
614 User.where("salt IS NULL OR salt = ''").find_each do |user|
614 next if user.hashed_password.blank?
615 next if user.hashed_password.blank?
615 salt = User.generate_salt
616 salt = User.generate_salt
616 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
617 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
617 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
618 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
618 end
619 end
619 end
620 end
620 end
621 end
621
622
622 protected
623 protected
623
624
624 def validate_password_length
625 def validate_password_length
625 # Password length validation based on setting
626 # Password length validation based on setting
626 if !password.nil? && password.size < Setting.password_min_length.to_i
627 if !password.nil? && password.size < Setting.password_min_length.to_i
627 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
628 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
628 end
629 end
629 end
630 end
630
631
631 private
632 private
632
633
633 # Removes references that are not handled by associations
634 # Removes references that are not handled by associations
634 # Things that are not deleted are reassociated with the anonymous user
635 # Things that are not deleted are reassociated with the anonymous user
635 def remove_references_before_destroy
636 def remove_references_before_destroy
636 return if self.id.nil?
637 return if self.id.nil?
637
638
638 substitute = User.anonymous
639 substitute = User.anonymous
639 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
640 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
640 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
641 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
641 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
642 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
642 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
643 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
643 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
644 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
644 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
645 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
645 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
646 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
646 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
647 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
647 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
648 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
648 # Remove private queries and keep public ones
649 # Remove private queries and keep public ones
649 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
650 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
650 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
651 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
651 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
652 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
652 Token.delete_all ['user_id = ?', id]
653 Token.delete_all ['user_id = ?', id]
653 Watcher.delete_all ['user_id = ?', id]
654 Watcher.delete_all ['user_id = ?', id]
654 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
655 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
655 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
656 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
656 end
657 end
657
658
658 # Return password digest
659 # Return password digest
659 def self.hash_password(clear_password)
660 def self.hash_password(clear_password)
660 Digest::SHA1.hexdigest(clear_password || "")
661 Digest::SHA1.hexdigest(clear_password || "")
661 end
662 end
662
663
663 # Returns a 128bits random salt as a hex string (32 chars long)
664 # Returns a 128bits random salt as a hex string (32 chars long)
664 def self.generate_salt
665 def self.generate_salt
665 Redmine::Utils.random_hex(16)
666 Redmine::Utils.random_hex(16)
666 end
667 end
667
668
668 end
669 end
669
670
670 class AnonymousUser < User
671 class AnonymousUser < User
671 validate :validate_anonymous_uniqueness, :on => :create
672 validate :validate_anonymous_uniqueness, :on => :create
672
673
673 def validate_anonymous_uniqueness
674 def validate_anonymous_uniqueness
674 # There should be only one AnonymousUser in the database
675 # There should be only one AnonymousUser in the database
675 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
676 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
676 end
677 end
677
678
678 def available_custom_fields
679 def available_custom_fields
679 []
680 []
680 end
681 end
681
682
682 # Overrides a few properties
683 # Overrides a few properties
683 def logged?; false end
684 def logged?; false end
684 def admin; false end
685 def admin; false end
685 def name(*args); I18n.t(:label_user_anonymous) end
686 def name(*args); I18n.t(:label_user_anonymous) end
686 def mail; nil end
687 def mail; nil end
687 def time_zone; nil end
688 def time_zone; nil end
688 def rss_key; nil end
689 def rss_key; nil end
689
690
690 def pref
691 def pref
691 UserPreference.new(:user => self)
692 UserPreference.new(:user => self)
692 end
693 end
693
694
694 # Anonymous user can not be destroyed
695 # Anonymous user can not be destroyed
695 def destroy
696 def destroy
696 false
697 false
697 end
698 end
698 end
699 end
@@ -1,168 +1,168
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Customizable
20 module Customizable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 def acts_as_customizable(options = {})
26 def acts_as_customizable(options = {})
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
28 cattr_accessor :customizable_options
28 cattr_accessor :customizable_options
29 self.customizable_options = options
29 self.customizable_options = options
30 has_many :custom_values, :as => :customized,
30 has_many :custom_values, :as => :customized,
31 :include => :custom_field,
31 :include => :custom_field,
32 :order => "#{CustomField.table_name}.position",
32 :order => "#{CustomField.table_name}.position",
33 :dependent => :delete_all,
33 :dependent => :delete_all,
34 :validate => false
34 :validate => false
35
35
36 send :alias_method, :reload_without_custom_fields, :reload
37 send :include, Redmine::Acts::Customizable::InstanceMethods
36 send :include, Redmine::Acts::Customizable::InstanceMethods
38 validate :validate_custom_field_values
37 validate :validate_custom_field_values
39 after_save :save_custom_field_values
38 after_save :save_custom_field_values
40 end
39 end
41 end
40 end
42
41
43 module InstanceMethods
42 module InstanceMethods
44 def self.included(base)
43 def self.included(base)
45 base.extend ClassMethods
44 base.extend ClassMethods
45 base.send :alias_method_chain, :reload, :custom_fields
46 end
46 end
47
47
48 def available_custom_fields
48 def available_custom_fields
49 CustomField.where("type = '#{self.class.name}CustomField'").sorted.all
49 CustomField.where("type = '#{self.class.name}CustomField'").sorted.all
50 end
50 end
51
51
52 # Sets the values of the object's custom fields
52 # Sets the values of the object's custom fields
53 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
53 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
54 def custom_fields=(values)
54 def custom_fields=(values)
55 values_to_hash = values.inject({}) do |hash, v|
55 values_to_hash = values.inject({}) do |hash, v|
56 v = v.stringify_keys
56 v = v.stringify_keys
57 if v['id'] && v.has_key?('value')
57 if v['id'] && v.has_key?('value')
58 hash[v['id']] = v['value']
58 hash[v['id']] = v['value']
59 end
59 end
60 hash
60 hash
61 end
61 end
62 self.custom_field_values = values_to_hash
62 self.custom_field_values = values_to_hash
63 end
63 end
64
64
65 # Sets the values of the object's custom fields
65 # Sets the values of the object's custom fields
66 # values is a hash like {'1' => 'foo', 2 => 'bar'}
66 # values is a hash like {'1' => 'foo', 2 => 'bar'}
67 def custom_field_values=(values)
67 def custom_field_values=(values)
68 values = values.stringify_keys
68 values = values.stringify_keys
69
69
70 custom_field_values.each do |custom_field_value|
70 custom_field_values.each do |custom_field_value|
71 key = custom_field_value.custom_field_id.to_s
71 key = custom_field_value.custom_field_id.to_s
72 if values.has_key?(key)
72 if values.has_key?(key)
73 value = values[key]
73 value = values[key]
74 if value.is_a?(Array)
74 if value.is_a?(Array)
75 value = value.reject(&:blank?).uniq
75 value = value.reject(&:blank?).uniq
76 if value.empty?
76 if value.empty?
77 value << ''
77 value << ''
78 end
78 end
79 end
79 end
80 custom_field_value.value = value
80 custom_field_value.value = value
81 end
81 end
82 end
82 end
83 @custom_field_values_changed = true
83 @custom_field_values_changed = true
84 end
84 end
85
85
86 def custom_field_values
86 def custom_field_values
87 @custom_field_values ||= available_custom_fields.collect do |field|
87 @custom_field_values ||= available_custom_fields.collect do |field|
88 x = CustomFieldValue.new
88 x = CustomFieldValue.new
89 x.custom_field = field
89 x.custom_field = field
90 x.customized = self
90 x.customized = self
91 if field.multiple?
91 if field.multiple?
92 values = custom_values.select { |v| v.custom_field == field }
92 values = custom_values.select { |v| v.custom_field == field }
93 if values.empty?
93 if values.empty?
94 values << custom_values.build(:customized => self, :custom_field => field, :value => nil)
94 values << custom_values.build(:customized => self, :custom_field => field, :value => nil)
95 end
95 end
96 x.value = values.map(&:value)
96 x.value = values.map(&:value)
97 else
97 else
98 cv = custom_values.detect { |v| v.custom_field == field }
98 cv = custom_values.detect { |v| v.custom_field == field }
99 cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
99 cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
100 x.value = cv.value
100 x.value = cv.value
101 end
101 end
102 x
102 x
103 end
103 end
104 end
104 end
105
105
106 def visible_custom_field_values
106 def visible_custom_field_values
107 custom_field_values.select(&:visible?)
107 custom_field_values.select(&:visible?)
108 end
108 end
109
109
110 def custom_field_values_changed?
110 def custom_field_values_changed?
111 @custom_field_values_changed == true
111 @custom_field_values_changed == true
112 end
112 end
113
113
114 def custom_value_for(c)
114 def custom_value_for(c)
115 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
115 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
116 custom_values.detect {|v| v.custom_field_id == field_id }
116 custom_values.detect {|v| v.custom_field_id == field_id }
117 end
117 end
118
118
119 def custom_field_value(c)
119 def custom_field_value(c)
120 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
120 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
121 custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
121 custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
122 end
122 end
123
123
124 def validate_custom_field_values
124 def validate_custom_field_values
125 if new_record? || custom_field_values_changed?
125 if new_record? || custom_field_values_changed?
126 custom_field_values.each(&:validate_value)
126 custom_field_values.each(&:validate_value)
127 end
127 end
128 end
128 end
129
129
130 def save_custom_field_values
130 def save_custom_field_values
131 target_custom_values = []
131 target_custom_values = []
132 custom_field_values.each do |custom_field_value|
132 custom_field_values.each do |custom_field_value|
133 if custom_field_value.value.is_a?(Array)
133 if custom_field_value.value.is_a?(Array)
134 custom_field_value.value.each do |v|
134 custom_field_value.value.each do |v|
135 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
135 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
136 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
136 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
137 target_custom_values << target
137 target_custom_values << target
138 end
138 end
139 else
139 else
140 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
140 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
141 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
141 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
142 target.value = custom_field_value.value
142 target.value = custom_field_value.value
143 target_custom_values << target
143 target_custom_values << target
144 end
144 end
145 end
145 end
146 self.custom_values = target_custom_values
146 self.custom_values = target_custom_values
147 custom_values.each(&:save)
147 custom_values.each(&:save)
148 @custom_field_values_changed = false
148 @custom_field_values_changed = false
149 true
149 true
150 end
150 end
151
151
152 def reset_custom_values!
152 def reset_custom_values!
153 @custom_field_values = nil
153 @custom_field_values = nil
154 @custom_field_values_changed = true
154 @custom_field_values_changed = true
155 end
155 end
156
156
157 def reload(*args)
157 def reload_with_custom_fields(*args)
158 @custom_field_values = nil
158 @custom_field_values = nil
159 @custom_field_values_changed = false
159 @custom_field_values_changed = false
160 reload_without_custom_fields(*args)
160 reload_without_custom_fields(*args)
161 end
161 end
162
162
163 module ClassMethods
163 module ClassMethods
164 end
164 end
165 end
165 end
166 end
166 end
167 end
167 end
168 end
168 end
General Comments 0
You need to be logged in to leave comments. Login now