##// END OF EJS Templates
Skip some validations if attribute did not change....
Jean-Philippe Lang -
r13341:e09f48e45994
parent child
Show More
@@ -1,1603 +1,1606
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 include Redmine::I18n
21 include Redmine::I18n
22
22
23 belongs_to :project
23 belongs_to :project
24 belongs_to :tracker
24 belongs_to :tracker
25 belongs_to :status, :class_name => 'IssueStatus'
25 belongs_to :status, :class_name => 'IssueStatus'
26 belongs_to :author, :class_name => 'User'
26 belongs_to :author, :class_name => 'User'
27 belongs_to :assigned_to, :class_name => 'Principal'
27 belongs_to :assigned_to, :class_name => 'Principal'
28 belongs_to :fixed_version, :class_name => 'Version'
28 belongs_to :fixed_version, :class_name => 'Version'
29 belongs_to :priority, :class_name => 'IssuePriority'
29 belongs_to :priority, :class_name => 'IssuePriority'
30 belongs_to :category, :class_name => 'IssueCategory'
30 belongs_to :category, :class_name => 'IssueCategory'
31
31
32 has_many :journals, :as => :journalized, :dependent => :destroy
32 has_many :journals, :as => :journalized, :dependent => :destroy
33 has_many :visible_journals,
33 has_many :visible_journals,
34 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
34 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
35 :class_name => 'Journal',
35 :class_name => 'Journal',
36 :as => :journalized
36 :as => :journalized
37
37
38 has_many :time_entries, :dependent => :destroy
38 has_many :time_entries, :dependent => :destroy
39 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
39 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
40
40
41 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
41 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
42 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
42 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
43
43
44 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
44 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
45 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
45 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
46 acts_as_customizable
46 acts_as_customizable
47 acts_as_watchable
47 acts_as_watchable
48 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
48 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
49 # sort by id so that limited eager loading doesn't break with postgresql
49 # sort by id so that limited eager loading doesn't break with postgresql
50 :order_column => "#{table_name}.id",
50 :order_column => "#{table_name}.id",
51 :scope => lambda { joins(:project).
51 :scope => lambda { joins(:project).
52 joins("LEFT OUTER JOIN #{Journal.table_name} ON #{Journal.table_name}.journalized_type='Issue'" +
52 joins("LEFT OUTER JOIN #{Journal.table_name} ON #{Journal.table_name}.journalized_type='Issue'" +
53 " AND #{Journal.table_name}.journalized_id = #{Issue.table_name}.id" +
53 " AND #{Journal.table_name}.journalized_id = #{Issue.table_name}.id" +
54 " AND (#{Journal.table_name}.private_notes = #{connection.quoted_false}" +
54 " AND (#{Journal.table_name}.private_notes = #{connection.quoted_false}" +
55 " OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))") }
55 " OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))") }
56
56
57 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
57 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
58 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
58 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
59 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
59 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
60
60
61 acts_as_activity_provider :scope => preload(:project, :author, :tracker),
61 acts_as_activity_provider :scope => preload(:project, :author, :tracker),
62 :author_key => :author_id
62 :author_key => :author_id
63
63
64 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
64 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
65
65
66 attr_reader :current_journal
66 attr_reader :current_journal
67 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
67 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
68
68
69 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
69 validates_presence_of :subject, :project, :tracker
70 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
71 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
72 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
70
73
71 validates_length_of :subject, :maximum => 255
74 validates_length_of :subject, :maximum => 255
72 validates_inclusion_of :done_ratio, :in => 0..100
75 validates_inclusion_of :done_ratio, :in => 0..100
73 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
76 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
74 validates :start_date, :date => true
77 validates :start_date, :date => true
75 validates :due_date, :date => true
78 validates :due_date, :date => true
76 validate :validate_issue, :validate_required_fields
79 validate :validate_issue, :validate_required_fields
77 attr_protected :id
80 attr_protected :id
78
81
79 scope :visible, lambda {|*args|
82 scope :visible, lambda {|*args|
80 includes(:project).
83 includes(:project).
81 references(:project).
84 references(:project).
82 where(Issue.visible_condition(args.shift || User.current, *args))
85 where(Issue.visible_condition(args.shift || User.current, *args))
83 }
86 }
84
87
85 scope :open, lambda {|*args|
88 scope :open, lambda {|*args|
86 is_closed = args.size > 0 ? !args.first : false
89 is_closed = args.size > 0 ? !args.first : false
87 joins(:status).
90 joins(:status).
88 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
91 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
89 }
92 }
90
93
91 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
94 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
92 scope :on_active_project, lambda {
95 scope :on_active_project, lambda {
93 joins(:project).
96 joins(:project).
94 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
97 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
95 }
98 }
96 scope :fixed_version, lambda {|versions|
99 scope :fixed_version, lambda {|versions|
97 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
100 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
98 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
101 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
99 }
102 }
100
103
101 before_create :default_assign
104 before_create :default_assign
102 before_save :close_duplicates, :update_done_ratio_from_issue_status,
105 before_save :close_duplicates, :update_done_ratio_from_issue_status,
103 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
106 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
104 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
107 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
105 after_save :reschedule_following_issues, :update_nested_set_attributes,
108 after_save :reschedule_following_issues, :update_nested_set_attributes,
106 :update_parent_attributes, :create_journal
109 :update_parent_attributes, :create_journal
107 # Should be after_create but would be called before previous after_save callbacks
110 # Should be after_create but would be called before previous after_save callbacks
108 after_save :after_create_from_copy
111 after_save :after_create_from_copy
109 after_destroy :update_parent_attributes
112 after_destroy :update_parent_attributes
110 after_create :send_notification
113 after_create :send_notification
111 # Keep it at the end of after_save callbacks
114 # Keep it at the end of after_save callbacks
112 after_save :clear_assigned_to_was
115 after_save :clear_assigned_to_was
113
116
114 # Returns a SQL conditions string used to find all issues visible by the specified user
117 # Returns a SQL conditions string used to find all issues visible by the specified user
115 def self.visible_condition(user, options={})
118 def self.visible_condition(user, options={})
116 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
119 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
117 if user.id && user.logged?
120 if user.id && user.logged?
118 case role.issues_visibility
121 case role.issues_visibility
119 when 'all'
122 when 'all'
120 nil
123 nil
121 when 'default'
124 when 'default'
122 user_ids = [user.id] + user.groups.map(&:id).compact
125 user_ids = [user.id] + user.groups.map(&:id).compact
123 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
126 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
124 when 'own'
127 when 'own'
125 user_ids = [user.id] + user.groups.map(&:id).compact
128 user_ids = [user.id] + user.groups.map(&:id).compact
126 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
129 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
127 else
130 else
128 '1=0'
131 '1=0'
129 end
132 end
130 else
133 else
131 "(#{table_name}.is_private = #{connection.quoted_false})"
134 "(#{table_name}.is_private = #{connection.quoted_false})"
132 end
135 end
133 end
136 end
134 end
137 end
135
138
136 # Returns true if usr or current user is allowed to view the issue
139 # Returns true if usr or current user is allowed to view the issue
137 def visible?(usr=nil)
140 def visible?(usr=nil)
138 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
141 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
139 if user.logged?
142 if user.logged?
140 case role.issues_visibility
143 case role.issues_visibility
141 when 'all'
144 when 'all'
142 true
145 true
143 when 'default'
146 when 'default'
144 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
147 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
145 when 'own'
148 when 'own'
146 self.author == user || user.is_or_belongs_to?(assigned_to)
149 self.author == user || user.is_or_belongs_to?(assigned_to)
147 else
150 else
148 false
151 false
149 end
152 end
150 else
153 else
151 !self.is_private?
154 !self.is_private?
152 end
155 end
153 end
156 end
154 end
157 end
155
158
156 # Returns true if user or current user is allowed to edit or add a note to the issue
159 # Returns true if user or current user is allowed to edit or add a note to the issue
157 def editable?(user=User.current)
160 def editable?(user=User.current)
158 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
161 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
159 end
162 end
160
163
161 def initialize(attributes=nil, *args)
164 def initialize(attributes=nil, *args)
162 super
165 super
163 if new_record?
166 if new_record?
164 # set default values for new records only
167 # set default values for new records only
165 self.priority ||= IssuePriority.default
168 self.priority ||= IssuePriority.default
166 self.watcher_user_ids = []
169 self.watcher_user_ids = []
167 end
170 end
168 end
171 end
169
172
170 def create_or_update
173 def create_or_update
171 super
174 super
172 ensure
175 ensure
173 @status_was = nil
176 @status_was = nil
174 end
177 end
175 private :create_or_update
178 private :create_or_update
176
179
177 # AR#Persistence#destroy would raise and RecordNotFound exception
180 # AR#Persistence#destroy would raise and RecordNotFound exception
178 # if the issue was already deleted or updated (non matching lock_version).
181 # if the issue was already deleted or updated (non matching lock_version).
179 # This is a problem when bulk deleting issues or deleting a project
182 # This is a problem when bulk deleting issues or deleting a project
180 # (because an issue may already be deleted if its parent was deleted
183 # (because an issue may already be deleted if its parent was deleted
181 # first).
184 # first).
182 # The issue is reloaded by the nested_set before being deleted so
185 # The issue is reloaded by the nested_set before being deleted so
183 # the lock_version condition should not be an issue but we handle it.
186 # the lock_version condition should not be an issue but we handle it.
184 def destroy
187 def destroy
185 super
188 super
186 rescue ActiveRecord::RecordNotFound
189 rescue ActiveRecord::RecordNotFound
187 # Stale or already deleted
190 # Stale or already deleted
188 begin
191 begin
189 reload
192 reload
190 rescue ActiveRecord::RecordNotFound
193 rescue ActiveRecord::RecordNotFound
191 # The issue was actually already deleted
194 # The issue was actually already deleted
192 @destroyed = true
195 @destroyed = true
193 return freeze
196 return freeze
194 end
197 end
195 # The issue was stale, retry to destroy
198 # The issue was stale, retry to destroy
196 super
199 super
197 end
200 end
198
201
199 alias :base_reload :reload
202 alias :base_reload :reload
200 def reload(*args)
203 def reload(*args)
201 @workflow_rule_by_attribute = nil
204 @workflow_rule_by_attribute = nil
202 @assignable_versions = nil
205 @assignable_versions = nil
203 @relations = nil
206 @relations = nil
204 @spent_hours = nil
207 @spent_hours = nil
205 base_reload(*args)
208 base_reload(*args)
206 end
209 end
207
210
208 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
211 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
209 def available_custom_fields
212 def available_custom_fields
210 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
213 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
211 end
214 end
212
215
213 def visible_custom_field_values(user=nil)
216 def visible_custom_field_values(user=nil)
214 user_real = user || User.current
217 user_real = user || User.current
215 custom_field_values.select do |value|
218 custom_field_values.select do |value|
216 value.custom_field.visible_by?(project, user_real)
219 value.custom_field.visible_by?(project, user_real)
217 end
220 end
218 end
221 end
219
222
220 # Copies attributes from another issue, arg can be an id or an Issue
223 # Copies attributes from another issue, arg can be an id or an Issue
221 def copy_from(arg, options={})
224 def copy_from(arg, options={})
222 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
225 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
223 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
226 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
224 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
227 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
225 self.status = issue.status
228 self.status = issue.status
226 self.author = User.current
229 self.author = User.current
227 unless options[:attachments] == false
230 unless options[:attachments] == false
228 self.attachments = issue.attachments.map do |attachement|
231 self.attachments = issue.attachments.map do |attachement|
229 attachement.copy(:container => self)
232 attachement.copy(:container => self)
230 end
233 end
231 end
234 end
232 @copied_from = issue
235 @copied_from = issue
233 @copy_options = options
236 @copy_options = options
234 self
237 self
235 end
238 end
236
239
237 # Returns an unsaved copy of the issue
240 # Returns an unsaved copy of the issue
238 def copy(attributes=nil, copy_options={})
241 def copy(attributes=nil, copy_options={})
239 copy = self.class.new.copy_from(self, copy_options)
242 copy = self.class.new.copy_from(self, copy_options)
240 copy.attributes = attributes if attributes
243 copy.attributes = attributes if attributes
241 copy
244 copy
242 end
245 end
243
246
244 # Returns true if the issue is a copy
247 # Returns true if the issue is a copy
245 def copy?
248 def copy?
246 @copied_from.present?
249 @copied_from.present?
247 end
250 end
248
251
249 def status_id=(status_id)
252 def status_id=(status_id)
250 if status_id.to_s != self.status_id.to_s
253 if status_id.to_s != self.status_id.to_s
251 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
254 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
252 end
255 end
253 self.status_id
256 self.status_id
254 end
257 end
255
258
256 # Sets the status.
259 # Sets the status.
257 def status=(status)
260 def status=(status)
258 if status != self.status
261 if status != self.status
259 @workflow_rule_by_attribute = nil
262 @workflow_rule_by_attribute = nil
260 end
263 end
261 association(:status).writer(status)
264 association(:status).writer(status)
262 end
265 end
263
266
264 def priority_id=(pid)
267 def priority_id=(pid)
265 self.priority = nil
268 self.priority = nil
266 write_attribute(:priority_id, pid)
269 write_attribute(:priority_id, pid)
267 end
270 end
268
271
269 def category_id=(cid)
272 def category_id=(cid)
270 self.category = nil
273 self.category = nil
271 write_attribute(:category_id, cid)
274 write_attribute(:category_id, cid)
272 end
275 end
273
276
274 def fixed_version_id=(vid)
277 def fixed_version_id=(vid)
275 self.fixed_version = nil
278 self.fixed_version = nil
276 write_attribute(:fixed_version_id, vid)
279 write_attribute(:fixed_version_id, vid)
277 end
280 end
278
281
279 def tracker_id=(tracker_id)
282 def tracker_id=(tracker_id)
280 if tracker_id.to_s != self.tracker_id.to_s
283 if tracker_id.to_s != self.tracker_id.to_s
281 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
284 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
282 end
285 end
283 self.tracker_id
286 self.tracker_id
284 end
287 end
285
288
286 # Sets the tracker.
289 # Sets the tracker.
287 # This will set the status to the default status of the new tracker if:
290 # This will set the status to the default status of the new tracker if:
288 # * the status was the default for the previous tracker
291 # * the status was the default for the previous tracker
289 # * or if the status was not part of the new tracker statuses
292 # * or if the status was not part of the new tracker statuses
290 # * or the status was nil
293 # * or the status was nil
291 def tracker=(tracker)
294 def tracker=(tracker)
292 if tracker != self.tracker
295 if tracker != self.tracker
293 if status == default_status
296 if status == default_status
294 self.status = nil
297 self.status = nil
295 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
298 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
296 self.status = nil
299 self.status = nil
297 end
300 end
298 @custom_field_values = nil
301 @custom_field_values = nil
299 @workflow_rule_by_attribute = nil
302 @workflow_rule_by_attribute = nil
300 end
303 end
301 association(:tracker).writer(tracker)
304 association(:tracker).writer(tracker)
302 self.status ||= default_status
305 self.status ||= default_status
303 self.tracker
306 self.tracker
304 end
307 end
305
308
306 def project_id=(project_id)
309 def project_id=(project_id)
307 if project_id.to_s != self.project_id.to_s
310 if project_id.to_s != self.project_id.to_s
308 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
311 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
309 end
312 end
310 self.project_id
313 self.project_id
311 end
314 end
312
315
313 # Sets the project.
316 # Sets the project.
314 # Unless keep_tracker argument is set to true, this will change the tracker
317 # Unless keep_tracker argument is set to true, this will change the tracker
315 # to the first tracker of the new project if the previous tracker is not part
318 # to the first tracker of the new project if the previous tracker is not part
316 # of the new project trackers.
319 # of the new project trackers.
317 # This will clear the fixed_version is it's no longer valid for the new project.
320 # This will clear the fixed_version is it's no longer valid for the new project.
318 # This will clear the parent issue if it's no longer valid for the new project.
321 # This will clear the parent issue if it's no longer valid for the new project.
319 # This will set the category to the category with the same name in the new
322 # This will set the category to the category with the same name in the new
320 # project if it exists, or clear it if it doesn't.
323 # project if it exists, or clear it if it doesn't.
321 def project=(project, keep_tracker=false)
324 def project=(project, keep_tracker=false)
322 project_was = self.project
325 project_was = self.project
323 association(:project).writer(project)
326 association(:project).writer(project)
324 if project_was && project && project_was != project
327 if project_was && project && project_was != project
325 @assignable_versions = nil
328 @assignable_versions = nil
326
329
327 unless keep_tracker || project.trackers.include?(tracker)
330 unless keep_tracker || project.trackers.include?(tracker)
328 self.tracker = project.trackers.first
331 self.tracker = project.trackers.first
329 end
332 end
330 # Reassign to the category with same name if any
333 # Reassign to the category with same name if any
331 if category
334 if category
332 self.category = project.issue_categories.find_by_name(category.name)
335 self.category = project.issue_categories.find_by_name(category.name)
333 end
336 end
334 # Keep the fixed_version if it's still valid in the new_project
337 # Keep the fixed_version if it's still valid in the new_project
335 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
338 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
336 self.fixed_version = nil
339 self.fixed_version = nil
337 end
340 end
338 # Clear the parent task if it's no longer valid
341 # Clear the parent task if it's no longer valid
339 unless valid_parent_project?
342 unless valid_parent_project?
340 self.parent_issue_id = nil
343 self.parent_issue_id = nil
341 end
344 end
342 @custom_field_values = nil
345 @custom_field_values = nil
343 @workflow_rule_by_attribute = nil
346 @workflow_rule_by_attribute = nil
344 end
347 end
345 self.project
348 self.project
346 end
349 end
347
350
348 def description=(arg)
351 def description=(arg)
349 if arg.is_a?(String)
352 if arg.is_a?(String)
350 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
353 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
351 end
354 end
352 write_attribute(:description, arg)
355 write_attribute(:description, arg)
353 end
356 end
354
357
355 # Overrides assign_attributes so that project and tracker get assigned first
358 # Overrides assign_attributes so that project and tracker get assigned first
356 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
359 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
357 return if new_attributes.nil?
360 return if new_attributes.nil?
358 attrs = new_attributes.dup
361 attrs = new_attributes.dup
359 attrs.stringify_keys!
362 attrs.stringify_keys!
360
363
361 %w(project project_id tracker tracker_id).each do |attr|
364 %w(project project_id tracker tracker_id).each do |attr|
362 if attrs.has_key?(attr)
365 if attrs.has_key?(attr)
363 send "#{attr}=", attrs.delete(attr)
366 send "#{attr}=", attrs.delete(attr)
364 end
367 end
365 end
368 end
366 send :assign_attributes_without_project_and_tracker_first, attrs, *args
369 send :assign_attributes_without_project_and_tracker_first, attrs, *args
367 end
370 end
368 # Do not redefine alias chain on reload (see #4838)
371 # Do not redefine alias chain on reload (see #4838)
369 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
372 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
370
373
371 def attributes=(new_attributes)
374 def attributes=(new_attributes)
372 assign_attributes new_attributes
375 assign_attributes new_attributes
373 end
376 end
374
377
375 def estimated_hours=(h)
378 def estimated_hours=(h)
376 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
379 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
377 end
380 end
378
381
379 safe_attributes 'project_id',
382 safe_attributes 'project_id',
380 :if => lambda {|issue, user|
383 :if => lambda {|issue, user|
381 if issue.new_record?
384 if issue.new_record?
382 issue.copy?
385 issue.copy?
383 elsif user.allowed_to?(:move_issues, issue.project)
386 elsif user.allowed_to?(:move_issues, issue.project)
384 Issue.allowed_target_projects_on_move.count > 1
387 Issue.allowed_target_projects_on_move.count > 1
385 end
388 end
386 }
389 }
387
390
388 safe_attributes 'tracker_id',
391 safe_attributes 'tracker_id',
389 'status_id',
392 'status_id',
390 'category_id',
393 'category_id',
391 'assigned_to_id',
394 'assigned_to_id',
392 'priority_id',
395 'priority_id',
393 'fixed_version_id',
396 'fixed_version_id',
394 'subject',
397 'subject',
395 'description',
398 'description',
396 'start_date',
399 'start_date',
397 'due_date',
400 'due_date',
398 'done_ratio',
401 'done_ratio',
399 'estimated_hours',
402 'estimated_hours',
400 'custom_field_values',
403 'custom_field_values',
401 'custom_fields',
404 'custom_fields',
402 'lock_version',
405 'lock_version',
403 'notes',
406 'notes',
404 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
407 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
405
408
406 safe_attributes 'status_id',
409 safe_attributes 'status_id',
407 'assigned_to_id',
410 'assigned_to_id',
408 'fixed_version_id',
411 'fixed_version_id',
409 'done_ratio',
412 'done_ratio',
410 'lock_version',
413 'lock_version',
411 'notes',
414 'notes',
412 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
415 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
413
416
414 safe_attributes 'notes',
417 safe_attributes 'notes',
415 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
418 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
416
419
417 safe_attributes 'private_notes',
420 safe_attributes 'private_notes',
418 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
421 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
419
422
420 safe_attributes 'watcher_user_ids',
423 safe_attributes 'watcher_user_ids',
421 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
424 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
422
425
423 safe_attributes 'is_private',
426 safe_attributes 'is_private',
424 :if => lambda {|issue, user|
427 :if => lambda {|issue, user|
425 user.allowed_to?(:set_issues_private, issue.project) ||
428 user.allowed_to?(:set_issues_private, issue.project) ||
426 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
429 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
427 }
430 }
428
431
429 safe_attributes 'parent_issue_id',
432 safe_attributes 'parent_issue_id',
430 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
433 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
431 user.allowed_to?(:manage_subtasks, issue.project)}
434 user.allowed_to?(:manage_subtasks, issue.project)}
432
435
433 def safe_attribute_names(user=nil)
436 def safe_attribute_names(user=nil)
434 names = super
437 names = super
435 names -= disabled_core_fields
438 names -= disabled_core_fields
436 names -= read_only_attribute_names(user)
439 names -= read_only_attribute_names(user)
437 names
440 names
438 end
441 end
439
442
440 # Safely sets attributes
443 # Safely sets attributes
441 # Should be called from controllers instead of #attributes=
444 # Should be called from controllers instead of #attributes=
442 # attr_accessible is too rough because we still want things like
445 # attr_accessible is too rough because we still want things like
443 # Issue.new(:project => foo) to work
446 # Issue.new(:project => foo) to work
444 def safe_attributes=(attrs, user=User.current)
447 def safe_attributes=(attrs, user=User.current)
445 return unless attrs.is_a?(Hash)
448 return unless attrs.is_a?(Hash)
446
449
447 attrs = attrs.deep_dup
450 attrs = attrs.deep_dup
448
451
449 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
452 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
450 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
453 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
451 if allowed_target_projects(user).where(:id => p.to_i).exists?
454 if allowed_target_projects(user).where(:id => p.to_i).exists?
452 self.project_id = p
455 self.project_id = p
453 end
456 end
454 end
457 end
455
458
456 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
459 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
457 self.tracker_id = t
460 self.tracker_id = t
458 end
461 end
459
462
460 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
463 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
461 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
464 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
462 self.status_id = s
465 self.status_id = s
463 end
466 end
464 end
467 end
465
468
466 attrs = delete_unsafe_attributes(attrs, user)
469 attrs = delete_unsafe_attributes(attrs, user)
467 return if attrs.empty?
470 return if attrs.empty?
468
471
469 unless leaf?
472 unless leaf?
470 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
473 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
471 end
474 end
472
475
473 if attrs['parent_issue_id'].present?
476 if attrs['parent_issue_id'].present?
474 s = attrs['parent_issue_id'].to_s
477 s = attrs['parent_issue_id'].to_s
475 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
478 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
476 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
479 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
477 end
480 end
478 end
481 end
479
482
480 if attrs['custom_field_values'].present?
483 if attrs['custom_field_values'].present?
481 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
484 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
482 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
485 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
483 end
486 end
484
487
485 if attrs['custom_fields'].present?
488 if attrs['custom_fields'].present?
486 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
489 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
487 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
490 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
488 end
491 end
489
492
490 # mass-assignment security bypass
493 # mass-assignment security bypass
491 assign_attributes attrs, :without_protection => true
494 assign_attributes attrs, :without_protection => true
492 end
495 end
493
496
494 def disabled_core_fields
497 def disabled_core_fields
495 tracker ? tracker.disabled_core_fields : []
498 tracker ? tracker.disabled_core_fields : []
496 end
499 end
497
500
498 # Returns the custom_field_values that can be edited by the given user
501 # Returns the custom_field_values that can be edited by the given user
499 def editable_custom_field_values(user=nil)
502 def editable_custom_field_values(user=nil)
500 visible_custom_field_values(user).reject do |value|
503 visible_custom_field_values(user).reject do |value|
501 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
504 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
502 end
505 end
503 end
506 end
504
507
505 # Returns the custom fields that can be edited by the given user
508 # Returns the custom fields that can be edited by the given user
506 def editable_custom_fields(user=nil)
509 def editable_custom_fields(user=nil)
507 editable_custom_field_values(user).map(&:custom_field).uniq
510 editable_custom_field_values(user).map(&:custom_field).uniq
508 end
511 end
509
512
510 # Returns the names of attributes that are read-only for user or the current user
513 # Returns the names of attributes that are read-only for user or the current user
511 # For users with multiple roles, the read-only fields are the intersection of
514 # For users with multiple roles, the read-only fields are the intersection of
512 # read-only fields of each role
515 # read-only fields of each role
513 # The result is an array of strings where sustom fields are represented with their ids
516 # The result is an array of strings where sustom fields are represented with their ids
514 #
517 #
515 # Examples:
518 # Examples:
516 # issue.read_only_attribute_names # => ['due_date', '2']
519 # issue.read_only_attribute_names # => ['due_date', '2']
517 # issue.read_only_attribute_names(user) # => []
520 # issue.read_only_attribute_names(user) # => []
518 def read_only_attribute_names(user=nil)
521 def read_only_attribute_names(user=nil)
519 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
522 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
520 end
523 end
521
524
522 # Returns the names of required attributes for user or the current user
525 # Returns the names of required attributes for user or the current user
523 # For users with multiple roles, the required fields are the intersection of
526 # For users with multiple roles, the required fields are the intersection of
524 # required fields of each role
527 # required fields of each role
525 # The result is an array of strings where sustom fields are represented with their ids
528 # The result is an array of strings where sustom fields are represented with their ids
526 #
529 #
527 # Examples:
530 # Examples:
528 # issue.required_attribute_names # => ['due_date', '2']
531 # issue.required_attribute_names # => ['due_date', '2']
529 # issue.required_attribute_names(user) # => []
532 # issue.required_attribute_names(user) # => []
530 def required_attribute_names(user=nil)
533 def required_attribute_names(user=nil)
531 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
534 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
532 end
535 end
533
536
534 # Returns true if the attribute is required for user
537 # Returns true if the attribute is required for user
535 def required_attribute?(name, user=nil)
538 def required_attribute?(name, user=nil)
536 required_attribute_names(user).include?(name.to_s)
539 required_attribute_names(user).include?(name.to_s)
537 end
540 end
538
541
539 # Returns a hash of the workflow rule by attribute for the given user
542 # Returns a hash of the workflow rule by attribute for the given user
540 #
543 #
541 # Examples:
544 # Examples:
542 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
545 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
543 def workflow_rule_by_attribute(user=nil)
546 def workflow_rule_by_attribute(user=nil)
544 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
547 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
545
548
546 user_real = user || User.current
549 user_real = user || User.current
547 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
550 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
548 return {} if roles.empty?
551 return {} if roles.empty?
549
552
550 result = {}
553 result = {}
551 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
554 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
552 if workflow_permissions.any?
555 if workflow_permissions.any?
553 workflow_rules = workflow_permissions.inject({}) do |h, wp|
556 workflow_rules = workflow_permissions.inject({}) do |h, wp|
554 h[wp.field_name] ||= []
557 h[wp.field_name] ||= []
555 h[wp.field_name] << wp.rule
558 h[wp.field_name] << wp.rule
556 h
559 h
557 end
560 end
558 workflow_rules.each do |attr, rules|
561 workflow_rules.each do |attr, rules|
559 next if rules.size < roles.size
562 next if rules.size < roles.size
560 uniq_rules = rules.uniq
563 uniq_rules = rules.uniq
561 if uniq_rules.size == 1
564 if uniq_rules.size == 1
562 result[attr] = uniq_rules.first
565 result[attr] = uniq_rules.first
563 else
566 else
564 result[attr] = 'required'
567 result[attr] = 'required'
565 end
568 end
566 end
569 end
567 end
570 end
568 @workflow_rule_by_attribute = result if user.nil?
571 @workflow_rule_by_attribute = result if user.nil?
569 result
572 result
570 end
573 end
571 private :workflow_rule_by_attribute
574 private :workflow_rule_by_attribute
572
575
573 def done_ratio
576 def done_ratio
574 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
577 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
575 status.default_done_ratio
578 status.default_done_ratio
576 else
579 else
577 read_attribute(:done_ratio)
580 read_attribute(:done_ratio)
578 end
581 end
579 end
582 end
580
583
581 def self.use_status_for_done_ratio?
584 def self.use_status_for_done_ratio?
582 Setting.issue_done_ratio == 'issue_status'
585 Setting.issue_done_ratio == 'issue_status'
583 end
586 end
584
587
585 def self.use_field_for_done_ratio?
588 def self.use_field_for_done_ratio?
586 Setting.issue_done_ratio == 'issue_field'
589 Setting.issue_done_ratio == 'issue_field'
587 end
590 end
588
591
589 def validate_issue
592 def validate_issue
590 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
593 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
591 errors.add :due_date, :greater_than_start_date
594 errors.add :due_date, :greater_than_start_date
592 end
595 end
593
596
594 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
597 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
595 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
598 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
596 end
599 end
597
600
598 if fixed_version
601 if fixed_version
599 if !assignable_versions.include?(fixed_version)
602 if !assignable_versions.include?(fixed_version)
600 errors.add :fixed_version_id, :inclusion
603 errors.add :fixed_version_id, :inclusion
601 elsif reopening? && fixed_version.closed?
604 elsif reopening? && fixed_version.closed?
602 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
605 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
603 end
606 end
604 end
607 end
605
608
606 # Checks that the issue can not be added/moved to a disabled tracker
609 # Checks that the issue can not be added/moved to a disabled tracker
607 if project && (tracker_id_changed? || project_id_changed?)
610 if project && (tracker_id_changed? || project_id_changed?)
608 unless project.trackers.include?(tracker)
611 unless project.trackers.include?(tracker)
609 errors.add :tracker_id, :inclusion
612 errors.add :tracker_id, :inclusion
610 end
613 end
611 end
614 end
612
615
613 # Checks parent issue assignment
616 # Checks parent issue assignment
614 if @invalid_parent_issue_id.present?
617 if @invalid_parent_issue_id.present?
615 errors.add :parent_issue_id, :invalid
618 errors.add :parent_issue_id, :invalid
616 elsif @parent_issue
619 elsif @parent_issue
617 if !valid_parent_project?(@parent_issue)
620 if !valid_parent_project?(@parent_issue)
618 errors.add :parent_issue_id, :invalid
621 errors.add :parent_issue_id, :invalid
619 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
622 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
620 errors.add :parent_issue_id, :invalid
623 errors.add :parent_issue_id, :invalid
621 elsif !new_record?
624 elsif !new_record?
622 # moving an existing issue
625 # moving an existing issue
623 if @parent_issue.root_id != root_id
626 if @parent_issue.root_id != root_id
624 # we can always move to another tree
627 # we can always move to another tree
625 elsif move_possible?(@parent_issue)
628 elsif move_possible?(@parent_issue)
626 # move accepted inside tree
629 # move accepted inside tree
627 else
630 else
628 errors.add :parent_issue_id, :invalid
631 errors.add :parent_issue_id, :invalid
629 end
632 end
630 end
633 end
631 end
634 end
632 end
635 end
633
636
634 # Validates the issue against additional workflow requirements
637 # Validates the issue against additional workflow requirements
635 def validate_required_fields
638 def validate_required_fields
636 user = new_record? ? author : current_journal.try(:user)
639 user = new_record? ? author : current_journal.try(:user)
637
640
638 required_attribute_names(user).each do |attribute|
641 required_attribute_names(user).each do |attribute|
639 if attribute =~ /^\d+$/
642 if attribute =~ /^\d+$/
640 attribute = attribute.to_i
643 attribute = attribute.to_i
641 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
644 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
642 if v && v.value.blank?
645 if v && v.value.blank?
643 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
646 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
644 end
647 end
645 else
648 else
646 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
649 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
647 errors.add attribute, :blank
650 errors.add attribute, :blank
648 end
651 end
649 end
652 end
650 end
653 end
651 end
654 end
652
655
653 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
656 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
654 # even if the user turns off the setting later
657 # even if the user turns off the setting later
655 def update_done_ratio_from_issue_status
658 def update_done_ratio_from_issue_status
656 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
659 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
657 self.done_ratio = status.default_done_ratio
660 self.done_ratio = status.default_done_ratio
658 end
661 end
659 end
662 end
660
663
661 def init_journal(user, notes = "")
664 def init_journal(user, notes = "")
662 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
665 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
663 end
666 end
664
667
665 # Returns the current journal or nil if it's not initialized
668 # Returns the current journal or nil if it's not initialized
666 def current_journal
669 def current_journal
667 @current_journal
670 @current_journal
668 end
671 end
669
672
670 # Returns the names of attributes that are journalized when updating the issue
673 # Returns the names of attributes that are journalized when updating the issue
671 def journalized_attribute_names
674 def journalized_attribute_names
672 Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
675 Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
673 end
676 end
674
677
675 # Returns the id of the last journal or nil
678 # Returns the id of the last journal or nil
676 def last_journal_id
679 def last_journal_id
677 if new_record?
680 if new_record?
678 nil
681 nil
679 else
682 else
680 journals.maximum(:id)
683 journals.maximum(:id)
681 end
684 end
682 end
685 end
683
686
684 # Returns a scope for journals that have an id greater than journal_id
687 # Returns a scope for journals that have an id greater than journal_id
685 def journals_after(journal_id)
688 def journals_after(journal_id)
686 scope = journals.reorder("#{Journal.table_name}.id ASC")
689 scope = journals.reorder("#{Journal.table_name}.id ASC")
687 if journal_id.present?
690 if journal_id.present?
688 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
691 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
689 end
692 end
690 scope
693 scope
691 end
694 end
692
695
693 # Returns the initial status of the issue
696 # Returns the initial status of the issue
694 # Returns nil for a new issue
697 # Returns nil for a new issue
695 def status_was
698 def status_was
696 if status_id_changed?
699 if status_id_changed?
697 if status_id_was.to_i > 0
700 if status_id_was.to_i > 0
698 @status_was ||= IssueStatus.find_by_id(status_id_was)
701 @status_was ||= IssueStatus.find_by_id(status_id_was)
699 end
702 end
700 else
703 else
701 @status_was ||= status
704 @status_was ||= status
702 end
705 end
703 end
706 end
704
707
705 # Return true if the issue is closed, otherwise false
708 # Return true if the issue is closed, otherwise false
706 def closed?
709 def closed?
707 status.present? && status.is_closed?
710 status.present? && status.is_closed?
708 end
711 end
709
712
710 # Returns true if the issue was closed when loaded
713 # Returns true if the issue was closed when loaded
711 def was_closed?
714 def was_closed?
712 status_was.present? && status_was.is_closed?
715 status_was.present? && status_was.is_closed?
713 end
716 end
714
717
715 # Return true if the issue is being reopened
718 # Return true if the issue is being reopened
716 def reopening?
719 def reopening?
717 if new_record?
720 if new_record?
718 false
721 false
719 else
722 else
720 status_id_changed? && !closed? && was_closed?
723 status_id_changed? && !closed? && was_closed?
721 end
724 end
722 end
725 end
723 alias :reopened? :reopening?
726 alias :reopened? :reopening?
724
727
725 # Return true if the issue is being closed
728 # Return true if the issue is being closed
726 def closing?
729 def closing?
727 if new_record?
730 if new_record?
728 closed?
731 closed?
729 else
732 else
730 status_id_changed? && closed? && !was_closed?
733 status_id_changed? && closed? && !was_closed?
731 end
734 end
732 end
735 end
733
736
734 # Returns true if the issue is overdue
737 # Returns true if the issue is overdue
735 def overdue?
738 def overdue?
736 due_date.present? && (due_date < Date.today) && !closed?
739 due_date.present? && (due_date < Date.today) && !closed?
737 end
740 end
738
741
739 # Is the amount of work done less than it should for the due date
742 # Is the amount of work done less than it should for the due date
740 def behind_schedule?
743 def behind_schedule?
741 return false if start_date.nil? || due_date.nil?
744 return false if start_date.nil? || due_date.nil?
742 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
745 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
743 return done_date <= Date.today
746 return done_date <= Date.today
744 end
747 end
745
748
746 # Does this issue have children?
749 # Does this issue have children?
747 def children?
750 def children?
748 !leaf?
751 !leaf?
749 end
752 end
750
753
751 # Users the issue can be assigned to
754 # Users the issue can be assigned to
752 def assignable_users
755 def assignable_users
753 users = project.assignable_users.to_a
756 users = project.assignable_users.to_a
754 users << author if author
757 users << author if author
755 users << assigned_to if assigned_to
758 users << assigned_to if assigned_to
756 users.uniq.sort
759 users.uniq.sort
757 end
760 end
758
761
759 # Versions that the issue can be assigned to
762 # Versions that the issue can be assigned to
760 def assignable_versions
763 def assignable_versions
761 return @assignable_versions if @assignable_versions
764 return @assignable_versions if @assignable_versions
762
765
763 versions = project.shared_versions.open.to_a
766 versions = project.shared_versions.open.to_a
764 if fixed_version
767 if fixed_version
765 if fixed_version_id_changed?
768 if fixed_version_id_changed?
766 # nothing to do
769 # nothing to do
767 elsif project_id_changed?
770 elsif project_id_changed?
768 if project.shared_versions.include?(fixed_version)
771 if project.shared_versions.include?(fixed_version)
769 versions << fixed_version
772 versions << fixed_version
770 end
773 end
771 else
774 else
772 versions << fixed_version
775 versions << fixed_version
773 end
776 end
774 end
777 end
775 @assignable_versions = versions.uniq.sort
778 @assignable_versions = versions.uniq.sort
776 end
779 end
777
780
778 # Returns true if this issue is blocked by another issue that is still open
781 # Returns true if this issue is blocked by another issue that is still open
779 def blocked?
782 def blocked?
780 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
783 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
781 end
784 end
782
785
783 # Returns the default status of the issue based on its tracker
786 # Returns the default status of the issue based on its tracker
784 # Returns nil if tracker is nil
787 # Returns nil if tracker is nil
785 def default_status
788 def default_status
786 tracker.try(:default_status)
789 tracker.try(:default_status)
787 end
790 end
788
791
789 # Returns an array of statuses that user is able to apply
792 # Returns an array of statuses that user is able to apply
790 def new_statuses_allowed_to(user=User.current, include_default=false)
793 def new_statuses_allowed_to(user=User.current, include_default=false)
791 if new_record? && @copied_from
794 if new_record? && @copied_from
792 [default_status, @copied_from.status].compact.uniq.sort
795 [default_status, @copied_from.status].compact.uniq.sort
793 else
796 else
794 initial_status = nil
797 initial_status = nil
795 if new_record?
798 if new_record?
796 initial_status = default_status
799 initial_status = default_status
797 elsif tracker_id_changed?
800 elsif tracker_id_changed?
798 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
801 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
799 initial_status = default_status
802 initial_status = default_status
800 elsif tracker.issue_status_ids.include?(status_id_was)
803 elsif tracker.issue_status_ids.include?(status_id_was)
801 initial_status = IssueStatus.find_by_id(status_id_was)
804 initial_status = IssueStatus.find_by_id(status_id_was)
802 else
805 else
803 initial_status = default_status
806 initial_status = default_status
804 end
807 end
805 else
808 else
806 initial_status = status_was
809 initial_status = status_was
807 end
810 end
808
811
809 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
812 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
810 assignee_transitions_allowed = initial_assigned_to_id.present? &&
813 assignee_transitions_allowed = initial_assigned_to_id.present? &&
811 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
814 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
812
815
813 statuses = []
816 statuses = []
814 if initial_status
817 if initial_status
815 statuses += initial_status.find_new_statuses_allowed_to(
818 statuses += initial_status.find_new_statuses_allowed_to(
816 user.admin ? Role.all.to_a : user.roles_for_project(project),
819 user.admin ? Role.all.to_a : user.roles_for_project(project),
817 tracker,
820 tracker,
818 author == user,
821 author == user,
819 assignee_transitions_allowed
822 assignee_transitions_allowed
820 )
823 )
821 end
824 end
822 statuses << initial_status unless statuses.empty?
825 statuses << initial_status unless statuses.empty?
823 statuses << default_status if include_default
826 statuses << default_status if include_default
824 statuses = statuses.compact.uniq.sort
827 statuses = statuses.compact.uniq.sort
825 if blocked?
828 if blocked?
826 statuses.reject!(&:is_closed?)
829 statuses.reject!(&:is_closed?)
827 end
830 end
828 statuses
831 statuses
829 end
832 end
830 end
833 end
831
834
832 # Returns the previous assignee if changed
835 # Returns the previous assignee if changed
833 def assigned_to_was
836 def assigned_to_was
834 # assigned_to_id_was is reset before after_save callbacks
837 # assigned_to_id_was is reset before after_save callbacks
835 user_id = @previous_assigned_to_id || assigned_to_id_was
838 user_id = @previous_assigned_to_id || assigned_to_id_was
836 if user_id && user_id != assigned_to_id
839 if user_id && user_id != assigned_to_id
837 @assigned_to_was ||= User.find_by_id(user_id)
840 @assigned_to_was ||= User.find_by_id(user_id)
838 end
841 end
839 end
842 end
840
843
841 # Returns the users that should be notified
844 # Returns the users that should be notified
842 def notified_users
845 def notified_users
843 notified = []
846 notified = []
844 # Author and assignee are always notified unless they have been
847 # Author and assignee are always notified unless they have been
845 # locked or don't want to be notified
848 # locked or don't want to be notified
846 notified << author if author
849 notified << author if author
847 if assigned_to
850 if assigned_to
848 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
851 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
849 end
852 end
850 if assigned_to_was
853 if assigned_to_was
851 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
854 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
852 end
855 end
853 notified = notified.select {|u| u.active? && u.notify_about?(self)}
856 notified = notified.select {|u| u.active? && u.notify_about?(self)}
854
857
855 notified += project.notified_users
858 notified += project.notified_users
856 notified.uniq!
859 notified.uniq!
857 # Remove users that can not view the issue
860 # Remove users that can not view the issue
858 notified.reject! {|user| !visible?(user)}
861 notified.reject! {|user| !visible?(user)}
859 notified
862 notified
860 end
863 end
861
864
862 # Returns the email addresses that should be notified
865 # Returns the email addresses that should be notified
863 def recipients
866 def recipients
864 notified_users.collect(&:mail)
867 notified_users.collect(&:mail)
865 end
868 end
866
869
867 def each_notification(users, &block)
870 def each_notification(users, &block)
868 if users.any?
871 if users.any?
869 if custom_field_values.detect {|value| !value.custom_field.visible?}
872 if custom_field_values.detect {|value| !value.custom_field.visible?}
870 users_by_custom_field_visibility = users.group_by do |user|
873 users_by_custom_field_visibility = users.group_by do |user|
871 visible_custom_field_values(user).map(&:custom_field_id).sort
874 visible_custom_field_values(user).map(&:custom_field_id).sort
872 end
875 end
873 users_by_custom_field_visibility.values.each do |users|
876 users_by_custom_field_visibility.values.each do |users|
874 yield(users)
877 yield(users)
875 end
878 end
876 else
879 else
877 yield(users)
880 yield(users)
878 end
881 end
879 end
882 end
880 end
883 end
881
884
882 # Returns the number of hours spent on this issue
885 # Returns the number of hours spent on this issue
883 def spent_hours
886 def spent_hours
884 @spent_hours ||= time_entries.sum(:hours) || 0
887 @spent_hours ||= time_entries.sum(:hours) || 0
885 end
888 end
886
889
887 # Returns the total number of hours spent on this issue and its descendants
890 # Returns the total number of hours spent on this issue and its descendants
888 #
891 #
889 # Example:
892 # Example:
890 # spent_hours => 0.0
893 # spent_hours => 0.0
891 # spent_hours => 50.2
894 # spent_hours => 50.2
892 def total_spent_hours
895 def total_spent_hours
893 @total_spent_hours ||=
896 @total_spent_hours ||=
894 self_and_descendants.
897 self_and_descendants.
895 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
898 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
896 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
899 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
897 end
900 end
898
901
899 def relations
902 def relations
900 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
903 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
901 end
904 end
902
905
903 # Preloads relations for a collection of issues
906 # Preloads relations for a collection of issues
904 def self.load_relations(issues)
907 def self.load_relations(issues)
905 if issues.any?
908 if issues.any?
906 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
909 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
907 issues.each do |issue|
910 issues.each do |issue|
908 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
911 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
909 end
912 end
910 end
913 end
911 end
914 end
912
915
913 # Preloads visible spent time for a collection of issues
916 # Preloads visible spent time for a collection of issues
914 def self.load_visible_spent_hours(issues, user=User.current)
917 def self.load_visible_spent_hours(issues, user=User.current)
915 if issues.any?
918 if issues.any?
916 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
919 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
917 issues.each do |issue|
920 issues.each do |issue|
918 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
921 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
919 end
922 end
920 end
923 end
921 end
924 end
922
925
923 # Preloads visible relations for a collection of issues
926 # Preloads visible relations for a collection of issues
924 def self.load_visible_relations(issues, user=User.current)
927 def self.load_visible_relations(issues, user=User.current)
925 if issues.any?
928 if issues.any?
926 issue_ids = issues.map(&:id)
929 issue_ids = issues.map(&:id)
927 # Relations with issue_from in given issues and visible issue_to
930 # Relations with issue_from in given issues and visible issue_to
928 relations_from = IssueRelation.joins(:issue_to => :project).
931 relations_from = IssueRelation.joins(:issue_to => :project).
929 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
932 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
930 # Relations with issue_to in given issues and visible issue_from
933 # Relations with issue_to in given issues and visible issue_from
931 relations_to = IssueRelation.joins(:issue_from => :project).
934 relations_to = IssueRelation.joins(:issue_from => :project).
932 where(visible_condition(user)).
935 where(visible_condition(user)).
933 where(:issue_to_id => issue_ids).to_a
936 where(:issue_to_id => issue_ids).to_a
934 issues.each do |issue|
937 issues.each do |issue|
935 relations =
938 relations =
936 relations_from.select {|relation| relation.issue_from_id == issue.id} +
939 relations_from.select {|relation| relation.issue_from_id == issue.id} +
937 relations_to.select {|relation| relation.issue_to_id == issue.id}
940 relations_to.select {|relation| relation.issue_to_id == issue.id}
938
941
939 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
942 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
940 end
943 end
941 end
944 end
942 end
945 end
943
946
944 # Finds an issue relation given its id.
947 # Finds an issue relation given its id.
945 def find_relation(relation_id)
948 def find_relation(relation_id)
946 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
949 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
947 end
950 end
948
951
949 # Returns all the other issues that depend on the issue
952 # Returns all the other issues that depend on the issue
950 # The algorithm is a modified breadth first search (bfs)
953 # The algorithm is a modified breadth first search (bfs)
951 def all_dependent_issues(except=[])
954 def all_dependent_issues(except=[])
952 # The found dependencies
955 # The found dependencies
953 dependencies = []
956 dependencies = []
954
957
955 # The visited flag for every node (issue) used by the breadth first search
958 # The visited flag for every node (issue) used by the breadth first search
956 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
959 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
957
960
958 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
961 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
959 # the issue when it is processed.
962 # the issue when it is processed.
960
963
961 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
964 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
962 # but its children will not be added to the queue when it is processed.
965 # but its children will not be added to the queue when it is processed.
963
966
964 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
967 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
965 # the queue, but its children have not been added.
968 # the queue, but its children have not been added.
966
969
967 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
970 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
968 # the children still need to be processed.
971 # the children still need to be processed.
969
972
970 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
973 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
971 # added as dependent issues. It needs no further processing.
974 # added as dependent issues. It needs no further processing.
972
975
973 issue_status = Hash.new(eNOT_DISCOVERED)
976 issue_status = Hash.new(eNOT_DISCOVERED)
974
977
975 # The queue
978 # The queue
976 queue = []
979 queue = []
977
980
978 # Initialize the bfs, add start node (self) to the queue
981 # Initialize the bfs, add start node (self) to the queue
979 queue << self
982 queue << self
980 issue_status[self] = ePROCESS_ALL
983 issue_status[self] = ePROCESS_ALL
981
984
982 while (!queue.empty?) do
985 while (!queue.empty?) do
983 current_issue = queue.shift
986 current_issue = queue.shift
984 current_issue_status = issue_status[current_issue]
987 current_issue_status = issue_status[current_issue]
985 dependencies << current_issue
988 dependencies << current_issue
986
989
987 # Add parent to queue, if not already in it.
990 # Add parent to queue, if not already in it.
988 parent = current_issue.parent
991 parent = current_issue.parent
989 parent_status = issue_status[parent]
992 parent_status = issue_status[parent]
990
993
991 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
994 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
992 queue << parent
995 queue << parent
993 issue_status[parent] = ePROCESS_RELATIONS_ONLY
996 issue_status[parent] = ePROCESS_RELATIONS_ONLY
994 end
997 end
995
998
996 # Add children to queue, but only if they are not already in it and
999 # Add children to queue, but only if they are not already in it and
997 # the children of the current node need to be processed.
1000 # the children of the current node need to be processed.
998 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
1001 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
999 current_issue.children.each do |child|
1002 current_issue.children.each do |child|
1000 next if except.include?(child)
1003 next if except.include?(child)
1001
1004
1002 if (issue_status[child] == eNOT_DISCOVERED)
1005 if (issue_status[child] == eNOT_DISCOVERED)
1003 queue << child
1006 queue << child
1004 issue_status[child] = ePROCESS_ALL
1007 issue_status[child] = ePROCESS_ALL
1005 elsif (issue_status[child] == eRELATIONS_PROCESSED)
1008 elsif (issue_status[child] == eRELATIONS_PROCESSED)
1006 queue << child
1009 queue << child
1007 issue_status[child] = ePROCESS_CHILDREN_ONLY
1010 issue_status[child] = ePROCESS_CHILDREN_ONLY
1008 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
1011 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
1009 queue << child
1012 queue << child
1010 issue_status[child] = ePROCESS_ALL
1013 issue_status[child] = ePROCESS_ALL
1011 end
1014 end
1012 end
1015 end
1013 end
1016 end
1014
1017
1015 # Add related issues to the queue, if they are not already in it.
1018 # Add related issues to the queue, if they are not already in it.
1016 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1019 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1017 next if except.include?(related_issue)
1020 next if except.include?(related_issue)
1018
1021
1019 if (issue_status[related_issue] == eNOT_DISCOVERED)
1022 if (issue_status[related_issue] == eNOT_DISCOVERED)
1020 queue << related_issue
1023 queue << related_issue
1021 issue_status[related_issue] = ePROCESS_ALL
1024 issue_status[related_issue] = ePROCESS_ALL
1022 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1025 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1023 queue << related_issue
1026 queue << related_issue
1024 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1027 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1025 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1028 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1026 queue << related_issue
1029 queue << related_issue
1027 issue_status[related_issue] = ePROCESS_ALL
1030 issue_status[related_issue] = ePROCESS_ALL
1028 end
1031 end
1029 end
1032 end
1030
1033
1031 # Set new status for current issue
1034 # Set new status for current issue
1032 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1035 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1033 issue_status[current_issue] = eALL_PROCESSED
1036 issue_status[current_issue] = eALL_PROCESSED
1034 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1037 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1035 issue_status[current_issue] = eRELATIONS_PROCESSED
1038 issue_status[current_issue] = eRELATIONS_PROCESSED
1036 end
1039 end
1037 end # while
1040 end # while
1038
1041
1039 # Remove the issues from the "except" parameter from the result array
1042 # Remove the issues from the "except" parameter from the result array
1040 dependencies -= except
1043 dependencies -= except
1041 dependencies.delete(self)
1044 dependencies.delete(self)
1042
1045
1043 dependencies
1046 dependencies
1044 end
1047 end
1045
1048
1046 # Returns an array of issues that duplicate this one
1049 # Returns an array of issues that duplicate this one
1047 def duplicates
1050 def duplicates
1048 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1051 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1049 end
1052 end
1050
1053
1051 # Returns the due date or the target due date if any
1054 # Returns the due date or the target due date if any
1052 # Used on gantt chart
1055 # Used on gantt chart
1053 def due_before
1056 def due_before
1054 due_date || (fixed_version ? fixed_version.effective_date : nil)
1057 due_date || (fixed_version ? fixed_version.effective_date : nil)
1055 end
1058 end
1056
1059
1057 # Returns the time scheduled for this issue.
1060 # Returns the time scheduled for this issue.
1058 #
1061 #
1059 # Example:
1062 # Example:
1060 # Start Date: 2/26/09, End Date: 3/04/09
1063 # Start Date: 2/26/09, End Date: 3/04/09
1061 # duration => 6
1064 # duration => 6
1062 def duration
1065 def duration
1063 (start_date && due_date) ? due_date - start_date : 0
1066 (start_date && due_date) ? due_date - start_date : 0
1064 end
1067 end
1065
1068
1066 # Returns the duration in working days
1069 # Returns the duration in working days
1067 def working_duration
1070 def working_duration
1068 (start_date && due_date) ? working_days(start_date, due_date) : 0
1071 (start_date && due_date) ? working_days(start_date, due_date) : 0
1069 end
1072 end
1070
1073
1071 def soonest_start(reload=false)
1074 def soonest_start(reload=false)
1072 @soonest_start = nil if reload
1075 @soonest_start = nil if reload
1073 @soonest_start ||= (
1076 @soonest_start ||= (
1074 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1077 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1075 [(@parent_issue || parent).try(:soonest_start)]
1078 [(@parent_issue || parent).try(:soonest_start)]
1076 ).compact.max
1079 ).compact.max
1077 end
1080 end
1078
1081
1079 # Sets start_date on the given date or the next working day
1082 # Sets start_date on the given date or the next working day
1080 # and changes due_date to keep the same working duration.
1083 # and changes due_date to keep the same working duration.
1081 def reschedule_on(date)
1084 def reschedule_on(date)
1082 wd = working_duration
1085 wd = working_duration
1083 date = next_working_date(date)
1086 date = next_working_date(date)
1084 self.start_date = date
1087 self.start_date = date
1085 self.due_date = add_working_days(date, wd)
1088 self.due_date = add_working_days(date, wd)
1086 end
1089 end
1087
1090
1088 # Reschedules the issue on the given date or the next working day and saves the record.
1091 # Reschedules the issue on the given date or the next working day and saves the record.
1089 # If the issue is a parent task, this is done by rescheduling its subtasks.
1092 # If the issue is a parent task, this is done by rescheduling its subtasks.
1090 def reschedule_on!(date)
1093 def reschedule_on!(date)
1091 return if date.nil?
1094 return if date.nil?
1092 if leaf?
1095 if leaf?
1093 if start_date.nil? || start_date != date
1096 if start_date.nil? || start_date != date
1094 if start_date && start_date > date
1097 if start_date && start_date > date
1095 # Issue can not be moved earlier than its soonest start date
1098 # Issue can not be moved earlier than its soonest start date
1096 date = [soonest_start(true), date].compact.max
1099 date = [soonest_start(true), date].compact.max
1097 end
1100 end
1098 reschedule_on(date)
1101 reschedule_on(date)
1099 begin
1102 begin
1100 save
1103 save
1101 rescue ActiveRecord::StaleObjectError
1104 rescue ActiveRecord::StaleObjectError
1102 reload
1105 reload
1103 reschedule_on(date)
1106 reschedule_on(date)
1104 save
1107 save
1105 end
1108 end
1106 end
1109 end
1107 else
1110 else
1108 leaves.each do |leaf|
1111 leaves.each do |leaf|
1109 if leaf.start_date
1112 if leaf.start_date
1110 # Only move subtask if it starts at the same date as the parent
1113 # Only move subtask if it starts at the same date as the parent
1111 # or if it starts before the given date
1114 # or if it starts before the given date
1112 if start_date == leaf.start_date || date > leaf.start_date
1115 if start_date == leaf.start_date || date > leaf.start_date
1113 leaf.reschedule_on!(date)
1116 leaf.reschedule_on!(date)
1114 end
1117 end
1115 else
1118 else
1116 leaf.reschedule_on!(date)
1119 leaf.reschedule_on!(date)
1117 end
1120 end
1118 end
1121 end
1119 end
1122 end
1120 end
1123 end
1121
1124
1122 def <=>(issue)
1125 def <=>(issue)
1123 if issue.nil?
1126 if issue.nil?
1124 -1
1127 -1
1125 elsif root_id != issue.root_id
1128 elsif root_id != issue.root_id
1126 (root_id || 0) <=> (issue.root_id || 0)
1129 (root_id || 0) <=> (issue.root_id || 0)
1127 else
1130 else
1128 (lft || 0) <=> (issue.lft || 0)
1131 (lft || 0) <=> (issue.lft || 0)
1129 end
1132 end
1130 end
1133 end
1131
1134
1132 def to_s
1135 def to_s
1133 "#{tracker} ##{id}: #{subject}"
1136 "#{tracker} ##{id}: #{subject}"
1134 end
1137 end
1135
1138
1136 # Returns a string of css classes that apply to the issue
1139 # Returns a string of css classes that apply to the issue
1137 def css_classes(user=User.current)
1140 def css_classes(user=User.current)
1138 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1141 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1139 s << ' closed' if closed?
1142 s << ' closed' if closed?
1140 s << ' overdue' if overdue?
1143 s << ' overdue' if overdue?
1141 s << ' child' if child?
1144 s << ' child' if child?
1142 s << ' parent' unless leaf?
1145 s << ' parent' unless leaf?
1143 s << ' private' if is_private?
1146 s << ' private' if is_private?
1144 if user.logged?
1147 if user.logged?
1145 s << ' created-by-me' if author_id == user.id
1148 s << ' created-by-me' if author_id == user.id
1146 s << ' assigned-to-me' if assigned_to_id == user.id
1149 s << ' assigned-to-me' if assigned_to_id == user.id
1147 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1150 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1148 end
1151 end
1149 s
1152 s
1150 end
1153 end
1151
1154
1152 # Unassigns issues from +version+ if it's no longer shared with issue's project
1155 # Unassigns issues from +version+ if it's no longer shared with issue's project
1153 def self.update_versions_from_sharing_change(version)
1156 def self.update_versions_from_sharing_change(version)
1154 # Update issues assigned to the version
1157 # Update issues assigned to the version
1155 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1158 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1156 end
1159 end
1157
1160
1158 # Unassigns issues from versions that are no longer shared
1161 # Unassigns issues from versions that are no longer shared
1159 # after +project+ was moved
1162 # after +project+ was moved
1160 def self.update_versions_from_hierarchy_change(project)
1163 def self.update_versions_from_hierarchy_change(project)
1161 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1164 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1162 # Update issues of the moved projects and issues assigned to a version of a moved project
1165 # Update issues of the moved projects and issues assigned to a version of a moved project
1163 Issue.update_versions(
1166 Issue.update_versions(
1164 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1167 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1165 moved_project_ids, moved_project_ids]
1168 moved_project_ids, moved_project_ids]
1166 )
1169 )
1167 end
1170 end
1168
1171
1169 def parent_issue_id=(arg)
1172 def parent_issue_id=(arg)
1170 s = arg.to_s.strip.presence
1173 s = arg.to_s.strip.presence
1171 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1174 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1172 @invalid_parent_issue_id = nil
1175 @invalid_parent_issue_id = nil
1173 elsif s.blank?
1176 elsif s.blank?
1174 @parent_issue = nil
1177 @parent_issue = nil
1175 @invalid_parent_issue_id = nil
1178 @invalid_parent_issue_id = nil
1176 else
1179 else
1177 @parent_issue = nil
1180 @parent_issue = nil
1178 @invalid_parent_issue_id = arg
1181 @invalid_parent_issue_id = arg
1179 end
1182 end
1180 end
1183 end
1181
1184
1182 def parent_issue_id
1185 def parent_issue_id
1183 if @invalid_parent_issue_id
1186 if @invalid_parent_issue_id
1184 @invalid_parent_issue_id
1187 @invalid_parent_issue_id
1185 elsif instance_variable_defined? :@parent_issue
1188 elsif instance_variable_defined? :@parent_issue
1186 @parent_issue.nil? ? nil : @parent_issue.id
1189 @parent_issue.nil? ? nil : @parent_issue.id
1187 else
1190 else
1188 parent_id
1191 parent_id
1189 end
1192 end
1190 end
1193 end
1191
1194
1192 # Returns true if issue's project is a valid
1195 # Returns true if issue's project is a valid
1193 # parent issue project
1196 # parent issue project
1194 def valid_parent_project?(issue=parent)
1197 def valid_parent_project?(issue=parent)
1195 return true if issue.nil? || issue.project_id == project_id
1198 return true if issue.nil? || issue.project_id == project_id
1196
1199
1197 case Setting.cross_project_subtasks
1200 case Setting.cross_project_subtasks
1198 when 'system'
1201 when 'system'
1199 true
1202 true
1200 when 'tree'
1203 when 'tree'
1201 issue.project.root == project.root
1204 issue.project.root == project.root
1202 when 'hierarchy'
1205 when 'hierarchy'
1203 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1206 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1204 when 'descendants'
1207 when 'descendants'
1205 issue.project.is_or_is_ancestor_of?(project)
1208 issue.project.is_or_is_ancestor_of?(project)
1206 else
1209 else
1207 false
1210 false
1208 end
1211 end
1209 end
1212 end
1210
1213
1211 # Returns an issue scope based on project and scope
1214 # Returns an issue scope based on project and scope
1212 def self.cross_project_scope(project, scope=nil)
1215 def self.cross_project_scope(project, scope=nil)
1213 if project.nil?
1216 if project.nil?
1214 return Issue
1217 return Issue
1215 end
1218 end
1216 case scope
1219 case scope
1217 when 'all', 'system'
1220 when 'all', 'system'
1218 Issue
1221 Issue
1219 when 'tree'
1222 when 'tree'
1220 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1223 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1221 :lft => project.root.lft, :rgt => project.root.rgt)
1224 :lft => project.root.lft, :rgt => project.root.rgt)
1222 when 'hierarchy'
1225 when 'hierarchy'
1223 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1226 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1224 :lft => project.lft, :rgt => project.rgt)
1227 :lft => project.lft, :rgt => project.rgt)
1225 when 'descendants'
1228 when 'descendants'
1226 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1229 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1227 :lft => project.lft, :rgt => project.rgt)
1230 :lft => project.lft, :rgt => project.rgt)
1228 else
1231 else
1229 Issue.where(:project_id => project.id)
1232 Issue.where(:project_id => project.id)
1230 end
1233 end
1231 end
1234 end
1232
1235
1233 def self.by_tracker(project)
1236 def self.by_tracker(project)
1234 count_and_group_by(:project => project, :association => :tracker)
1237 count_and_group_by(:project => project, :association => :tracker)
1235 end
1238 end
1236
1239
1237 def self.by_version(project)
1240 def self.by_version(project)
1238 count_and_group_by(:project => project, :association => :fixed_version)
1241 count_and_group_by(:project => project, :association => :fixed_version)
1239 end
1242 end
1240
1243
1241 def self.by_priority(project)
1244 def self.by_priority(project)
1242 count_and_group_by(:project => project, :association => :priority)
1245 count_and_group_by(:project => project, :association => :priority)
1243 end
1246 end
1244
1247
1245 def self.by_category(project)
1248 def self.by_category(project)
1246 count_and_group_by(:project => project, :association => :category)
1249 count_and_group_by(:project => project, :association => :category)
1247 end
1250 end
1248
1251
1249 def self.by_assigned_to(project)
1252 def self.by_assigned_to(project)
1250 count_and_group_by(:project => project, :association => :assigned_to)
1253 count_and_group_by(:project => project, :association => :assigned_to)
1251 end
1254 end
1252
1255
1253 def self.by_author(project)
1256 def self.by_author(project)
1254 count_and_group_by(:project => project, :association => :author)
1257 count_and_group_by(:project => project, :association => :author)
1255 end
1258 end
1256
1259
1257 def self.by_subproject(project)
1260 def self.by_subproject(project)
1258 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1261 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1259 r.reject {|r| r["project_id"] == project.id.to_s}
1262 r.reject {|r| r["project_id"] == project.id.to_s}
1260 end
1263 end
1261
1264
1262 # Query generator for selecting groups of issue counts for a project
1265 # Query generator for selecting groups of issue counts for a project
1263 # based on specific criteria
1266 # based on specific criteria
1264 #
1267 #
1265 # Options
1268 # Options
1266 # * project - Project to search in.
1269 # * project - Project to search in.
1267 # * with_subprojects - Includes subprojects issues if set to true.
1270 # * with_subprojects - Includes subprojects issues if set to true.
1268 # * association - Symbol. Association for grouping.
1271 # * association - Symbol. Association for grouping.
1269 def self.count_and_group_by(options)
1272 def self.count_and_group_by(options)
1270 assoc = reflect_on_association(options[:association])
1273 assoc = reflect_on_association(options[:association])
1271 select_field = assoc.foreign_key
1274 select_field = assoc.foreign_key
1272
1275
1273 Issue.
1276 Issue.
1274 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1277 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1275 joins(:status, assoc.name).
1278 joins(:status, assoc.name).
1276 group(:status_id, :is_closed, select_field).
1279 group(:status_id, :is_closed, select_field).
1277 count.
1280 count.
1278 map do |columns, total|
1281 map do |columns, total|
1279 status_id, is_closed, field_value = columns
1282 status_id, is_closed, field_value = columns
1280 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1283 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1281 {
1284 {
1282 "status_id" => status_id.to_s,
1285 "status_id" => status_id.to_s,
1283 "closed" => is_closed,
1286 "closed" => is_closed,
1284 select_field => field_value.to_s,
1287 select_field => field_value.to_s,
1285 "total" => total.to_s
1288 "total" => total.to_s
1286 }
1289 }
1287 end
1290 end
1288 end
1291 end
1289
1292
1290 # Returns a scope of projects that user can assign the issue to
1293 # Returns a scope of projects that user can assign the issue to
1291 def allowed_target_projects(user=User.current)
1294 def allowed_target_projects(user=User.current)
1292 if new_record?
1295 if new_record?
1293 Project.where(Project.allowed_to_condition(user, :add_issues))
1296 Project.where(Project.allowed_to_condition(user, :add_issues))
1294 else
1297 else
1295 self.class.allowed_target_projects_on_move(user)
1298 self.class.allowed_target_projects_on_move(user)
1296 end
1299 end
1297 end
1300 end
1298
1301
1299 # Returns a scope of projects that user can move issues to
1302 # Returns a scope of projects that user can move issues to
1300 def self.allowed_target_projects_on_move(user=User.current)
1303 def self.allowed_target_projects_on_move(user=User.current)
1301 Project.where(Project.allowed_to_condition(user, :move_issues))
1304 Project.where(Project.allowed_to_condition(user, :move_issues))
1302 end
1305 end
1303
1306
1304 private
1307 private
1305
1308
1306 def after_project_change
1309 def after_project_change
1307 # Update project_id on related time entries
1310 # Update project_id on related time entries
1308 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1311 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1309
1312
1310 # Delete issue relations
1313 # Delete issue relations
1311 unless Setting.cross_project_issue_relations?
1314 unless Setting.cross_project_issue_relations?
1312 relations_from.clear
1315 relations_from.clear
1313 relations_to.clear
1316 relations_to.clear
1314 end
1317 end
1315
1318
1316 # Move subtasks that were in the same project
1319 # Move subtasks that were in the same project
1317 children.each do |child|
1320 children.each do |child|
1318 next unless child.project_id == project_id_was
1321 next unless child.project_id == project_id_was
1319 # Change project and keep project
1322 # Change project and keep project
1320 child.send :project=, project, true
1323 child.send :project=, project, true
1321 unless child.save
1324 unless child.save
1322 raise ActiveRecord::Rollback
1325 raise ActiveRecord::Rollback
1323 end
1326 end
1324 end
1327 end
1325 end
1328 end
1326
1329
1327 # Callback for after the creation of an issue by copy
1330 # Callback for after the creation of an issue by copy
1328 # * adds a "copied to" relation with the copied issue
1331 # * adds a "copied to" relation with the copied issue
1329 # * copies subtasks from the copied issue
1332 # * copies subtasks from the copied issue
1330 def after_create_from_copy
1333 def after_create_from_copy
1331 return unless copy? && !@after_create_from_copy_handled
1334 return unless copy? && !@after_create_from_copy_handled
1332
1335
1333 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1336 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1334 if @current_journal
1337 if @current_journal
1335 @copied_from.init_journal(@current_journal.user)
1338 @copied_from.init_journal(@current_journal.user)
1336 end
1339 end
1337 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1340 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1338 unless relation.save
1341 unless relation.save
1339 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1342 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1340 end
1343 end
1341 end
1344 end
1342
1345
1343 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1346 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1344 copy_options = (@copy_options || {}).merge(:subtasks => false)
1347 copy_options = (@copy_options || {}).merge(:subtasks => false)
1345 copied_issue_ids = {@copied_from.id => self.id}
1348 copied_issue_ids = {@copied_from.id => self.id}
1346 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1349 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1347 # Do not copy self when copying an issue as a descendant of the copied issue
1350 # Do not copy self when copying an issue as a descendant of the copied issue
1348 next if child == self
1351 next if child == self
1349 # Do not copy subtasks of issues that were not copied
1352 # Do not copy subtasks of issues that were not copied
1350 next unless copied_issue_ids[child.parent_id]
1353 next unless copied_issue_ids[child.parent_id]
1351 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1354 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1352 unless child.visible?
1355 unless child.visible?
1353 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1356 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1354 next
1357 next
1355 end
1358 end
1356 copy = Issue.new.copy_from(child, copy_options)
1359 copy = Issue.new.copy_from(child, copy_options)
1357 if @current_journal
1360 if @current_journal
1358 copy.init_journal(@current_journal.user)
1361 copy.init_journal(@current_journal.user)
1359 end
1362 end
1360 copy.author = author
1363 copy.author = author
1361 copy.project = project
1364 copy.project = project
1362 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1365 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1363 unless copy.save
1366 unless copy.save
1364 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
1367 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
1365 next
1368 next
1366 end
1369 end
1367 copied_issue_ids[child.id] = copy.id
1370 copied_issue_ids[child.id] = copy.id
1368 end
1371 end
1369 end
1372 end
1370 @after_create_from_copy_handled = true
1373 @after_create_from_copy_handled = true
1371 end
1374 end
1372
1375
1373 def update_nested_set_attributes
1376 def update_nested_set_attributes
1374 if root_id.nil?
1377 if root_id.nil?
1375 # issue was just created
1378 # issue was just created
1376 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1379 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1377 Issue.where(["id = ?", id]).update_all(["root_id = ?", root_id])
1380 Issue.where(["id = ?", id]).update_all(["root_id = ?", root_id])
1378 if @parent_issue
1381 if @parent_issue
1379 move_to_child_of(@parent_issue)
1382 move_to_child_of(@parent_issue)
1380 end
1383 end
1381 elsif parent_issue_id != parent_id
1384 elsif parent_issue_id != parent_id
1382 update_nested_set_attributes_on_parent_change
1385 update_nested_set_attributes_on_parent_change
1383 end
1386 end
1384 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1387 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1385 end
1388 end
1386
1389
1387 # Updates the nested set for when an existing issue is moved
1390 # Updates the nested set for when an existing issue is moved
1388 def update_nested_set_attributes_on_parent_change
1391 def update_nested_set_attributes_on_parent_change
1389 former_parent_id = parent_id
1392 former_parent_id = parent_id
1390 # moving an existing issue
1393 # moving an existing issue
1391 if @parent_issue && @parent_issue.root_id == root_id
1394 if @parent_issue && @parent_issue.root_id == root_id
1392 # inside the same tree
1395 # inside the same tree
1393 move_to_child_of(@parent_issue)
1396 move_to_child_of(@parent_issue)
1394 else
1397 else
1395 # to another tree
1398 # to another tree
1396 unless root?
1399 unless root?
1397 move_to_right_of(root)
1400 move_to_right_of(root)
1398 end
1401 end
1399 old_root_id = root_id
1402 old_root_id = root_id
1400 in_tenacious_transaction do
1403 in_tenacious_transaction do
1401 @parent_issue.reload_nested_set if @parent_issue
1404 @parent_issue.reload_nested_set if @parent_issue
1402 self.reload_nested_set
1405 self.reload_nested_set
1403 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1406 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1404 cond = ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]
1407 cond = ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]
1405 self.class.base_class.select('id').lock(true).where(cond)
1408 self.class.base_class.select('id').lock(true).where(cond)
1406 offset = rdm_right_most_bound + 1 - lft
1409 offset = rdm_right_most_bound + 1 - lft
1407 Issue.where(cond).
1410 Issue.where(cond).
1408 update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset])
1411 update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset])
1409 end
1412 end
1410 if @parent_issue
1413 if @parent_issue
1411 move_to_child_of(@parent_issue)
1414 move_to_child_of(@parent_issue)
1412 end
1415 end
1413 end
1416 end
1414 # delete invalid relations of all descendants
1417 # delete invalid relations of all descendants
1415 self_and_descendants.each do |issue|
1418 self_and_descendants.each do |issue|
1416 issue.relations.each do |relation|
1419 issue.relations.each do |relation|
1417 relation.destroy unless relation.valid?
1420 relation.destroy unless relation.valid?
1418 end
1421 end
1419 end
1422 end
1420 # update former parent
1423 # update former parent
1421 recalculate_attributes_for(former_parent_id) if former_parent_id
1424 recalculate_attributes_for(former_parent_id) if former_parent_id
1422 end
1425 end
1423
1426
1424 def rdm_right_most_bound
1427 def rdm_right_most_bound
1425 right_most_node =
1428 right_most_node =
1426 self.class.base_class.unscoped.
1429 self.class.base_class.unscoped.
1427 order("#{quoted_right_column_full_name} desc").limit(1).lock(true).first
1430 order("#{quoted_right_column_full_name} desc").limit(1).lock(true).first
1428 right_most_node ? (right_most_node[right_column_name] || 0 ) : 0
1431 right_most_node ? (right_most_node[right_column_name] || 0 ) : 0
1429 end
1432 end
1430 private :rdm_right_most_bound
1433 private :rdm_right_most_bound
1431
1434
1432 def update_parent_attributes
1435 def update_parent_attributes
1433 recalculate_attributes_for(parent_id) if parent_id
1436 recalculate_attributes_for(parent_id) if parent_id
1434 end
1437 end
1435
1438
1436 def recalculate_attributes_for(issue_id)
1439 def recalculate_attributes_for(issue_id)
1437 if issue_id && p = Issue.find_by_id(issue_id)
1440 if issue_id && p = Issue.find_by_id(issue_id)
1438 # priority = highest priority of children
1441 # priority = highest priority of children
1439 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1442 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1440 p.priority = IssuePriority.find_by_position(priority_position)
1443 p.priority = IssuePriority.find_by_position(priority_position)
1441 end
1444 end
1442
1445
1443 # start/due dates = lowest/highest dates of children
1446 # start/due dates = lowest/highest dates of children
1444 p.start_date = p.children.minimum(:start_date)
1447 p.start_date = p.children.minimum(:start_date)
1445 p.due_date = p.children.maximum(:due_date)
1448 p.due_date = p.children.maximum(:due_date)
1446 if p.start_date && p.due_date && p.due_date < p.start_date
1449 if p.start_date && p.due_date && p.due_date < p.start_date
1447 p.start_date, p.due_date = p.due_date, p.start_date
1450 p.start_date, p.due_date = p.due_date, p.start_date
1448 end
1451 end
1449
1452
1450 # done ratio = weighted average ratio of leaves
1453 # done ratio = weighted average ratio of leaves
1451 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1454 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1452 leaves_count = p.leaves.count
1455 leaves_count = p.leaves.count
1453 if leaves_count > 0
1456 if leaves_count > 0
1454 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1457 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1455 if average == 0
1458 if average == 0
1456 average = 1
1459 average = 1
1457 end
1460 end
1458 done = p.leaves.joins(:status).
1461 done = p.leaves.joins(:status).
1459 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1462 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1460 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1463 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1461 progress = done / (average * leaves_count)
1464 progress = done / (average * leaves_count)
1462 p.done_ratio = progress.round
1465 p.done_ratio = progress.round
1463 end
1466 end
1464 end
1467 end
1465
1468
1466 # estimate = sum of leaves estimates
1469 # estimate = sum of leaves estimates
1467 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1470 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1468 p.estimated_hours = nil if p.estimated_hours == 0.0
1471 p.estimated_hours = nil if p.estimated_hours == 0.0
1469
1472
1470 # ancestors will be recursively updated
1473 # ancestors will be recursively updated
1471 p.save(:validate => false)
1474 p.save(:validate => false)
1472 end
1475 end
1473 end
1476 end
1474
1477
1475 # Update issues so their versions are not pointing to a
1478 # Update issues so their versions are not pointing to a
1476 # fixed_version that is not shared with the issue's project
1479 # fixed_version that is not shared with the issue's project
1477 def self.update_versions(conditions=nil)
1480 def self.update_versions(conditions=nil)
1478 # Only need to update issues with a fixed_version from
1481 # Only need to update issues with a fixed_version from
1479 # a different project and that is not systemwide shared
1482 # a different project and that is not systemwide shared
1480 Issue.joins(:project, :fixed_version).
1483 Issue.joins(:project, :fixed_version).
1481 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1484 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1482 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1485 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1483 " AND #{Version.table_name}.sharing <> 'system'").
1486 " AND #{Version.table_name}.sharing <> 'system'").
1484 where(conditions).each do |issue|
1487 where(conditions).each do |issue|
1485 next if issue.project.nil? || issue.fixed_version.nil?
1488 next if issue.project.nil? || issue.fixed_version.nil?
1486 unless issue.project.shared_versions.include?(issue.fixed_version)
1489 unless issue.project.shared_versions.include?(issue.fixed_version)
1487 issue.init_journal(User.current)
1490 issue.init_journal(User.current)
1488 issue.fixed_version = nil
1491 issue.fixed_version = nil
1489 issue.save
1492 issue.save
1490 end
1493 end
1491 end
1494 end
1492 end
1495 end
1493
1496
1494 # Callback on file attachment
1497 # Callback on file attachment
1495 def attachment_added(attachment)
1498 def attachment_added(attachment)
1496 if current_journal && !attachment.new_record?
1499 if current_journal && !attachment.new_record?
1497 current_journal.journalize_attachment(attachment, :added)
1500 current_journal.journalize_attachment(attachment, :added)
1498 end
1501 end
1499 end
1502 end
1500
1503
1501 # Callback on attachment deletion
1504 # Callback on attachment deletion
1502 def attachment_removed(attachment)
1505 def attachment_removed(attachment)
1503 if current_journal && !attachment.new_record?
1506 if current_journal && !attachment.new_record?
1504 current_journal.journalize_attachment(attachment, :removed)
1507 current_journal.journalize_attachment(attachment, :removed)
1505 current_journal.save
1508 current_journal.save
1506 end
1509 end
1507 end
1510 end
1508
1511
1509 # Called after a relation is added
1512 # Called after a relation is added
1510 def relation_added(relation)
1513 def relation_added(relation)
1511 if current_journal
1514 if current_journal
1512 current_journal.journalize_relation(relation, :added)
1515 current_journal.journalize_relation(relation, :added)
1513 current_journal.save
1516 current_journal.save
1514 end
1517 end
1515 end
1518 end
1516
1519
1517 # Called after a relation is removed
1520 # Called after a relation is removed
1518 def relation_removed(relation)
1521 def relation_removed(relation)
1519 if current_journal
1522 if current_journal
1520 current_journal.journalize_relation(relation, :removed)
1523 current_journal.journalize_relation(relation, :removed)
1521 current_journal.save
1524 current_journal.save
1522 end
1525 end
1523 end
1526 end
1524
1527
1525 # Default assignment based on category
1528 # Default assignment based on category
1526 def default_assign
1529 def default_assign
1527 if assigned_to.nil? && category && category.assigned_to
1530 if assigned_to.nil? && category && category.assigned_to
1528 self.assigned_to = category.assigned_to
1531 self.assigned_to = category.assigned_to
1529 end
1532 end
1530 end
1533 end
1531
1534
1532 # Updates start/due dates of following issues
1535 # Updates start/due dates of following issues
1533 def reschedule_following_issues
1536 def reschedule_following_issues
1534 if start_date_changed? || due_date_changed?
1537 if start_date_changed? || due_date_changed?
1535 relations_from.each do |relation|
1538 relations_from.each do |relation|
1536 relation.set_issue_to_dates
1539 relation.set_issue_to_dates
1537 end
1540 end
1538 end
1541 end
1539 end
1542 end
1540
1543
1541 # Closes duplicates if the issue is being closed
1544 # Closes duplicates if the issue is being closed
1542 def close_duplicates
1545 def close_duplicates
1543 if closing?
1546 if closing?
1544 duplicates.each do |duplicate|
1547 duplicates.each do |duplicate|
1545 # Reload is needed in case the duplicate was updated by a previous duplicate
1548 # Reload is needed in case the duplicate was updated by a previous duplicate
1546 duplicate.reload
1549 duplicate.reload
1547 # Don't re-close it if it's already closed
1550 # Don't re-close it if it's already closed
1548 next if duplicate.closed?
1551 next if duplicate.closed?
1549 # Same user and notes
1552 # Same user and notes
1550 if @current_journal
1553 if @current_journal
1551 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1554 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1552 end
1555 end
1553 duplicate.update_attribute :status, self.status
1556 duplicate.update_attribute :status, self.status
1554 end
1557 end
1555 end
1558 end
1556 end
1559 end
1557
1560
1558 # Make sure updated_on is updated when adding a note and set updated_on now
1561 # Make sure updated_on is updated when adding a note and set updated_on now
1559 # so we can set closed_on with the same value on closing
1562 # so we can set closed_on with the same value on closing
1560 def force_updated_on_change
1563 def force_updated_on_change
1561 if @current_journal || changed?
1564 if @current_journal || changed?
1562 self.updated_on = current_time_from_proper_timezone
1565 self.updated_on = current_time_from_proper_timezone
1563 if new_record?
1566 if new_record?
1564 self.created_on = updated_on
1567 self.created_on = updated_on
1565 end
1568 end
1566 end
1569 end
1567 end
1570 end
1568
1571
1569 # Callback for setting closed_on when the issue is closed.
1572 # Callback for setting closed_on when the issue is closed.
1570 # The closed_on attribute stores the time of the last closing
1573 # The closed_on attribute stores the time of the last closing
1571 # and is preserved when the issue is reopened.
1574 # and is preserved when the issue is reopened.
1572 def update_closed_on
1575 def update_closed_on
1573 if closing?
1576 if closing?
1574 self.closed_on = updated_on
1577 self.closed_on = updated_on
1575 end
1578 end
1576 end
1579 end
1577
1580
1578 # Saves the changes in a Journal
1581 # Saves the changes in a Journal
1579 # Called after_save
1582 # Called after_save
1580 def create_journal
1583 def create_journal
1581 if current_journal
1584 if current_journal
1582 current_journal.save
1585 current_journal.save
1583 end
1586 end
1584 end
1587 end
1585
1588
1586 def send_notification
1589 def send_notification
1587 if Setting.notified_events.include?('issue_added')
1590 if Setting.notified_events.include?('issue_added')
1588 Mailer.deliver_issue_add(self)
1591 Mailer.deliver_issue_add(self)
1589 end
1592 end
1590 end
1593 end
1591
1594
1592 # Stores the previous assignee so we can still have access
1595 # Stores the previous assignee so we can still have access
1593 # to it during after_save callbacks (assigned_to_id_was is reset)
1596 # to it during after_save callbacks (assigned_to_id_was is reset)
1594 def set_assigned_to_was
1597 def set_assigned_to_was
1595 @previous_assigned_to_id = assigned_to_id_was
1598 @previous_assigned_to_id = assigned_to_id_was
1596 end
1599 end
1597
1600
1598 # Clears the previous assignee at the end of after_save callbacks
1601 # Clears the previous assignee at the end of after_save callbacks
1599 def clear_assigned_to_was
1602 def clear_assigned_to_was
1600 @assigned_to_was = nil
1603 @assigned_to_was = nil
1601 @previous_assigned_to_id = nil
1604 @previous_assigned_to_id = nil
1602 end
1605 end
1603 end
1606 end
@@ -1,1077 +1,1076
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 overridden Activities
29 # Specific overridden Activities
30 has_many :time_entry_activities
30 has_many :time_entry_activities
31 has_many :members,
31 has_many :members,
32 lambda { joins(:principal, :roles).
32 lambda { joins(:principal, :roles).
33 where("#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}") }
33 where("#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}") }
34 has_many :memberships, :class_name => 'Member'
34 has_many :memberships, :class_name => 'Member'
35 has_many :member_principals,
35 has_many :member_principals,
36 lambda { joins(:principal).
36 lambda { joins(:principal).
37 where("#{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}")},
37 where("#{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}")},
38 :class_name => 'Member'
38 :class_name => 'Member'
39 has_many :enabled_modules, :dependent => :delete_all
39 has_many :enabled_modules, :dependent => :delete_all
40 has_and_belongs_to_many :trackers, lambda {order(:position)}
40 has_and_belongs_to_many :trackers, lambda {order(:position)}
41 has_many :issues, :dependent => :destroy
41 has_many :issues, :dependent => :destroy
42 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :issue_changes, :through => :issues, :source => :journals
43 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
43 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
44 has_many :time_entries, :dependent => :destroy
44 has_many :time_entries, :dependent => :destroy
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, lambda {includes(:author)}, :dependent => :destroy
47 has_many :news, lambda {includes(:author)}, :dependent => :destroy
48 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
48 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
49 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
49 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
50 has_one :repository, lambda {where(["is_default = ?", true])}
50 has_one :repository, lambda {where(["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 lambda {order("#{CustomField.table_name}.position")},
56 lambda {order("#{CustomField.table_name}.position")},
57 :class_name => 'IssueCustomField',
57 :class_name => 'IssueCustomField',
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 :dependent => :destroy
61 acts_as_nested_set :dependent => :destroy
62 acts_as_attachable :view_permission => :view_files,
62 acts_as_attachable :view_permission => :view_files,
63 :edit_permission => :manage_files,
63 :edit_permission => :manage_files,
64 :delete_permission => :manage_files
64 :delete_permission => :manage_files
65
65
66 acts_as_customizable
66 acts_as_customizable
67 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
67 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
68 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
68 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
69 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
69 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
70 :author => nil
70 :author => nil
71
71
72 attr_protected :status
72 attr_protected :status
73
73
74 validates_presence_of :name, :identifier
74 validates_presence_of :name, :identifier
75 validates_uniqueness_of :identifier
75 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
76 validates_associated :repository, :wiki
77 validates_length_of :name, :maximum => 255
76 validates_length_of :name, :maximum => 255
78 validates_length_of :homepage, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
79 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
80 # downcase letters, digits, dashes but not digits only
79 # downcase letters, digits, dashes but not digits only
81 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? }
82 # reserved words
81 # reserved words
83 validates_exclusion_of :identifier, :in => %w( new )
82 validates_exclusion_of :identifier, :in => %w( new )
84
83
85 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?}
86 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?}
87 before_destroy :delete_all_members
86 before_destroy :delete_all_members
88
87
89 scope :has_module, lambda {|mod|
88 scope :has_module, lambda {|mod|
90 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)
91 }
90 }
92 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 scope :active, lambda { where(:status => STATUS_ACTIVE) }
93 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}) }
94 scope :all_public, lambda { where(:is_public => true) }
93 scope :all_public, lambda { where(:is_public => true) }
95 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)) }
96 scope :allowed_to, lambda {|*args|
95 scope :allowed_to, lambda {|*args|
97 user = User.current
96 user = User.current
98 permission = nil
97 permission = nil
99 if args.first.is_a?(Symbol)
98 if args.first.is_a?(Symbol)
100 permission = args.shift
99 permission = args.shift
101 else
100 else
102 user = args.shift
101 user = args.shift
103 permission = args.shift
102 permission = args.shift
104 end
103 end
105 where(Project.allowed_to_condition(user, permission, *args))
104 where(Project.allowed_to_condition(user, permission, *args))
106 }
105 }
107 scope :like, lambda {|arg|
106 scope :like, lambda {|arg|
108 if arg.blank?
107 if arg.blank?
109 where(nil)
108 where(nil)
110 else
109 else
111 pattern = "%#{arg.to_s.strip.downcase}%"
110 pattern = "%#{arg.to_s.strip.downcase}%"
112 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)
113 end
112 end
114 }
113 }
115 scope :sorted, lambda {order(:lft)}
114 scope :sorted, lambda {order(:lft)}
116
115
117 def initialize(attributes=nil, *args)
116 def initialize(attributes=nil, *args)
118 super
117 super
119
118
120 initialized = (attributes || {}).stringify_keys
119 initialized = (attributes || {}).stringify_keys
121 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
120 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
122 self.identifier = Project.next_identifier
121 self.identifier = Project.next_identifier
123 end
122 end
124 if !initialized.key?('is_public')
123 if !initialized.key?('is_public')
125 self.is_public = Setting.default_projects_public?
124 self.is_public = Setting.default_projects_public?
126 end
125 end
127 if !initialized.key?('enabled_module_names')
126 if !initialized.key?('enabled_module_names')
128 self.enabled_module_names = Setting.default_projects_modules
127 self.enabled_module_names = Setting.default_projects_modules
129 end
128 end
130 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
129 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
131 default = Setting.default_projects_tracker_ids
130 default = Setting.default_projects_tracker_ids
132 if default.is_a?(Array)
131 if default.is_a?(Array)
133 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
132 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
134 else
133 else
135 self.trackers = Tracker.sorted.to_a
134 self.trackers = Tracker.sorted.to_a
136 end
135 end
137 end
136 end
138 end
137 end
139
138
140 def identifier=(identifier)
139 def identifier=(identifier)
141 super unless identifier_frozen?
140 super unless identifier_frozen?
142 end
141 end
143
142
144 def identifier_frozen?
143 def identifier_frozen?
145 errors[:identifier].blank? && !(new_record? || identifier.blank?)
144 errors[:identifier].blank? && !(new_record? || identifier.blank?)
146 end
145 end
147
146
148 # returns latest created projects
147 # returns latest created projects
149 # non public projects will be returned only if user is a member of those
148 # non public projects will be returned only if user is a member of those
150 def self.latest(user=nil, count=5)
149 def self.latest(user=nil, count=5)
151 visible(user).limit(count).order("created_on DESC").to_a
150 visible(user).limit(count).order("created_on DESC").to_a
152 end
151 end
153
152
154 # Returns true if the project is visible to +user+ or to the current user.
153 # Returns true if the project is visible to +user+ or to the current user.
155 def visible?(user=User.current)
154 def visible?(user=User.current)
156 user.allowed_to?(:view_project, self)
155 user.allowed_to?(:view_project, self)
157 end
156 end
158
157
159 # Returns a SQL conditions string used to find all projects visible by the specified user.
158 # Returns a SQL conditions string used to find all projects visible by the specified user.
160 #
159 #
161 # Examples:
160 # Examples:
162 # Project.visible_condition(admin) => "projects.status = 1"
161 # Project.visible_condition(admin) => "projects.status = 1"
163 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
162 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
164 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
163 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
165 def self.visible_condition(user, options={})
164 def self.visible_condition(user, options={})
166 allowed_to_condition(user, :view_project, options)
165 allowed_to_condition(user, :view_project, options)
167 end
166 end
168
167
169 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
168 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
170 #
169 #
171 # Valid options:
170 # Valid options:
172 # * :project => limit the condition to project
171 # * :project => limit the condition to project
173 # * :with_subprojects => limit the condition to project and its subprojects
172 # * :with_subprojects => limit the condition to project and its subprojects
174 # * :member => limit the condition to the user projects
173 # * :member => limit the condition to the user projects
175 def self.allowed_to_condition(user, permission, options={})
174 def self.allowed_to_condition(user, permission, options={})
176 perm = Redmine::AccessControl.permission(permission)
175 perm = Redmine::AccessControl.permission(permission)
177 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
176 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
178 if perm && perm.project_module
177 if perm && perm.project_module
179 # If the permission belongs to a project module, make sure the module is enabled
178 # If the permission belongs to a project module, make sure the module is enabled
180 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
179 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
181 end
180 end
182 if project = options[:project]
181 if project = options[:project]
183 project_statement = project.project_condition(options[:with_subprojects])
182 project_statement = project.project_condition(options[:with_subprojects])
184 base_statement = "(#{project_statement}) AND (#{base_statement})"
183 base_statement = "(#{project_statement}) AND (#{base_statement})"
185 end
184 end
186
185
187 if user.admin?
186 if user.admin?
188 base_statement
187 base_statement
189 else
188 else
190 statement_by_role = {}
189 statement_by_role = {}
191 unless options[:member]
190 unless options[:member]
192 role = user.builtin_role
191 role = user.builtin_role
193 if role.allowed_to?(permission)
192 if role.allowed_to?(permission)
194 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}"
195 end
194 end
196 end
195 end
197 user.projects_by_role.each do |role, projects|
196 user.projects_by_role.each do |role, projects|
198 if role.allowed_to?(permission) && projects.any?
197 if role.allowed_to?(permission) && projects.any?
199 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
198 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
200 end
199 end
201 end
200 end
202 if statement_by_role.empty?
201 if statement_by_role.empty?
203 "1=0"
202 "1=0"
204 else
203 else
205 if block_given?
204 if block_given?
206 statement_by_role.each do |role, statement|
205 statement_by_role.each do |role, statement|
207 if s = yield(role, user)
206 if s = yield(role, user)
208 statement_by_role[role] = "(#{statement} AND (#{s}))"
207 statement_by_role[role] = "(#{statement} AND (#{s}))"
209 end
208 end
210 end
209 end
211 end
210 end
212 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
211 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
213 end
212 end
214 end
213 end
215 end
214 end
216
215
217 def override_roles(role)
216 def override_roles(role)
218 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
217 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
219 member = member_principals.where("#{Principal.table_name}.type = ?", group_class.name).first
218 member = member_principals.where("#{Principal.table_name}.type = ?", group_class.name).first
220 member ? member.roles.to_a : [role]
219 member ? member.roles.to_a : [role]
221 end
220 end
222
221
223 def principals
222 def principals
224 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
223 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
225 end
224 end
226
225
227 def users
226 def users
228 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
227 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
229 end
228 end
230
229
231 # Returns the Systemwide and project specific activities
230 # Returns the Systemwide and project specific activities
232 def activities(include_inactive=false)
231 def activities(include_inactive=false)
233 if include_inactive
232 if include_inactive
234 return all_activities
233 return all_activities
235 else
234 else
236 return active_activities
235 return active_activities
237 end
236 end
238 end
237 end
239
238
240 # Will create a new Project specific Activity or update an existing one
239 # Will create a new Project specific Activity or update an existing one
241 #
240 #
242 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
241 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
243 # does not successfully save.
242 # does not successfully save.
244 def update_or_create_time_entry_activity(id, activity_hash)
243 def update_or_create_time_entry_activity(id, activity_hash)
245 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
244 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
246 self.create_time_entry_activity_if_needed(activity_hash)
245 self.create_time_entry_activity_if_needed(activity_hash)
247 else
246 else
248 activity = project.time_entry_activities.find_by_id(id.to_i)
247 activity = project.time_entry_activities.find_by_id(id.to_i)
249 activity.update_attributes(activity_hash) if activity
248 activity.update_attributes(activity_hash) if activity
250 end
249 end
251 end
250 end
252
251
253 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
252 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
254 #
253 #
255 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
254 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
256 # does not successfully save.
255 # does not successfully save.
257 def create_time_entry_activity_if_needed(activity)
256 def create_time_entry_activity_if_needed(activity)
258 if activity['parent_id']
257 if activity['parent_id']
259 parent_activity = TimeEntryActivity.find(activity['parent_id'])
258 parent_activity = TimeEntryActivity.find(activity['parent_id'])
260 activity['name'] = parent_activity.name
259 activity['name'] = parent_activity.name
261 activity['position'] = parent_activity.position
260 activity['position'] = parent_activity.position
262 if Enumeration.overriding_change?(activity, parent_activity)
261 if Enumeration.overriding_change?(activity, parent_activity)
263 project_activity = self.time_entry_activities.create(activity)
262 project_activity = self.time_entry_activities.create(activity)
264 if project_activity.new_record?
263 if project_activity.new_record?
265 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
264 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
266 else
265 else
267 self.time_entries.
266 self.time_entries.
268 where(["activity_id = ?", parent_activity.id]).
267 where(["activity_id = ?", parent_activity.id]).
269 update_all("activity_id = #{project_activity.id}")
268 update_all("activity_id = #{project_activity.id}")
270 end
269 end
271 end
270 end
272 end
271 end
273 end
272 end
274
273
275 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
274 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
276 #
275 #
277 # Examples:
276 # Examples:
278 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
277 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
279 # project.project_condition(false) => "projects.id = 1"
278 # project.project_condition(false) => "projects.id = 1"
280 def project_condition(with_subprojects)
279 def project_condition(with_subprojects)
281 cond = "#{Project.table_name}.id = #{id}"
280 cond = "#{Project.table_name}.id = #{id}"
282 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
281 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
283 cond
282 cond
284 end
283 end
285
284
286 def self.find(*args)
285 def self.find(*args)
287 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
286 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
288 project = find_by_identifier(*args)
287 project = find_by_identifier(*args)
289 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
288 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
290 project
289 project
291 else
290 else
292 super
291 super
293 end
292 end
294 end
293 end
295
294
296 def self.find_by_param(*args)
295 def self.find_by_param(*args)
297 self.find(*args)
296 self.find(*args)
298 end
297 end
299
298
300 alias :base_reload :reload
299 alias :base_reload :reload
301 def reload(*args)
300 def reload(*args)
302 @principals = nil
301 @principals = nil
303 @users = nil
302 @users = nil
304 @shared_versions = nil
303 @shared_versions = nil
305 @rolled_up_versions = nil
304 @rolled_up_versions = nil
306 @rolled_up_trackers = nil
305 @rolled_up_trackers = nil
307 @all_issue_custom_fields = nil
306 @all_issue_custom_fields = nil
308 @all_time_entry_custom_fields = nil
307 @all_time_entry_custom_fields = nil
309 @to_param = nil
308 @to_param = nil
310 @allowed_parents = nil
309 @allowed_parents = nil
311 @allowed_permissions = nil
310 @allowed_permissions = nil
312 @actions_allowed = nil
311 @actions_allowed = nil
313 @start_date = nil
312 @start_date = nil
314 @due_date = nil
313 @due_date = nil
315 @override_members = nil
314 @override_members = nil
316 @assignable_users = nil
315 @assignable_users = nil
317 base_reload(*args)
316 base_reload(*args)
318 end
317 end
319
318
320 def to_param
319 def to_param
321 # id is used for projects with a numeric identifier (compatibility)
320 # id is used for projects with a numeric identifier (compatibility)
322 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
321 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
323 end
322 end
324
323
325 def active?
324 def active?
326 self.status == STATUS_ACTIVE
325 self.status == STATUS_ACTIVE
327 end
326 end
328
327
329 def archived?
328 def archived?
330 self.status == STATUS_ARCHIVED
329 self.status == STATUS_ARCHIVED
331 end
330 end
332
331
333 # Archives the project and its descendants
332 # Archives the project and its descendants
334 def archive
333 def archive
335 # Check that there is no issue of a non descendant project that is assigned
334 # Check that there is no issue of a non descendant project that is assigned
336 # to one of the project or descendant versions
335 # to one of the project or descendant versions
337 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
336 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
338
337
339 if version_ids.any? &&
338 if version_ids.any? &&
340 Issue.
339 Issue.
341 includes(:project).
340 includes(:project).
342 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
341 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
343 where(:fixed_version_id => version_ids).
342 where(:fixed_version_id => version_ids).
344 exists?
343 exists?
345 return false
344 return false
346 end
345 end
347 Project.transaction do
346 Project.transaction do
348 archive!
347 archive!
349 end
348 end
350 true
349 true
351 end
350 end
352
351
353 # Unarchives the project
352 # Unarchives the project
354 # All its ancestors must be active
353 # All its ancestors must be active
355 def unarchive
354 def unarchive
356 return false if ancestors.detect {|a| !a.active?}
355 return false if ancestors.detect {|a| !a.active?}
357 update_attribute :status, STATUS_ACTIVE
356 update_attribute :status, STATUS_ACTIVE
358 end
357 end
359
358
360 def close
359 def close
361 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
360 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
362 end
361 end
363
362
364 def reopen
363 def reopen
365 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
364 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
366 end
365 end
367
366
368 # Returns an array of projects the project can be moved to
367 # Returns an array of projects the project can be moved to
369 # by the current user
368 # by the current user
370 def allowed_parents
369 def allowed_parents
371 return @allowed_parents if @allowed_parents
370 return @allowed_parents if @allowed_parents
372 @allowed_parents = Project.allowed_to(User.current, :add_subprojects).to_a
371 @allowed_parents = Project.allowed_to(User.current, :add_subprojects).to_a
373 @allowed_parents = @allowed_parents - self_and_descendants
372 @allowed_parents = @allowed_parents - self_and_descendants
374 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
373 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
375 @allowed_parents << nil
374 @allowed_parents << nil
376 end
375 end
377 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
376 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
378 @allowed_parents << parent
377 @allowed_parents << parent
379 end
378 end
380 @allowed_parents
379 @allowed_parents
381 end
380 end
382
381
383 # Sets the parent of the project with authorization check
382 # Sets the parent of the project with authorization check
384 def set_allowed_parent!(p)
383 def set_allowed_parent!(p)
385 unless p.nil? || p.is_a?(Project)
384 unless p.nil? || p.is_a?(Project)
386 if p.to_s.blank?
385 if p.to_s.blank?
387 p = nil
386 p = nil
388 else
387 else
389 p = Project.find_by_id(p)
388 p = Project.find_by_id(p)
390 return false unless p
389 return false unless p
391 end
390 end
392 end
391 end
393 if p.nil?
392 if p.nil?
394 if !new_record? && allowed_parents.empty?
393 if !new_record? && allowed_parents.empty?
395 return false
394 return false
396 end
395 end
397 elsif !allowed_parents.include?(p)
396 elsif !allowed_parents.include?(p)
398 return false
397 return false
399 end
398 end
400 set_parent!(p)
399 set_parent!(p)
401 end
400 end
402
401
403 # Sets the parent of the project
402 # Sets the parent of the project
404 # Argument can be either a Project, a String, a Fixnum or nil
403 # Argument can be either a Project, a String, a Fixnum or nil
405 def set_parent!(p)
404 def set_parent!(p)
406 unless p.nil? || p.is_a?(Project)
405 unless p.nil? || p.is_a?(Project)
407 if p.to_s.blank?
406 if p.to_s.blank?
408 p = nil
407 p = nil
409 else
408 else
410 p = Project.find_by_id(p)
409 p = Project.find_by_id(p)
411 return false unless p
410 return false unless p
412 end
411 end
413 end
412 end
414 if p == parent && !p.nil?
413 if p == parent && !p.nil?
415 # Nothing to do
414 # Nothing to do
416 true
415 true
417 elsif p.nil? || (p.active? && move_possible?(p))
416 elsif p.nil? || (p.active? && move_possible?(p))
418 set_or_update_position_under(p)
417 set_or_update_position_under(p)
419 Issue.update_versions_from_hierarchy_change(self)
418 Issue.update_versions_from_hierarchy_change(self)
420 true
419 true
421 else
420 else
422 # Can not move to the given target
421 # Can not move to the given target
423 false
422 false
424 end
423 end
425 end
424 end
426
425
427 # Recalculates all lft and rgt values based on project names
426 # Recalculates all lft and rgt values based on project names
428 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
427 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
429 # Used in BuildProjectsTree migration
428 # Used in BuildProjectsTree migration
430 def self.rebuild_tree!
429 def self.rebuild_tree!
431 transaction do
430 transaction do
432 update_all "lft = NULL, rgt = NULL"
431 update_all "lft = NULL, rgt = NULL"
433 rebuild!(false)
432 rebuild!(false)
434 all.each { |p| p.set_or_update_position_under(p.parent) }
433 all.each { |p| p.set_or_update_position_under(p.parent) }
435 end
434 end
436 end
435 end
437
436
438 # Returns an array of the trackers used by the project and its active sub projects
437 # Returns an array of the trackers used by the project and its active sub projects
439 def rolled_up_trackers
438 def rolled_up_trackers
440 @rolled_up_trackers ||=
439 @rolled_up_trackers ||=
441 Tracker.
440 Tracker.
442 joins(:projects).
441 joins(:projects).
443 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
442 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
444 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED).
443 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED).
445 uniq.
444 uniq.
446 sorted.
445 sorted.
447 to_a
446 to_a
448 end
447 end
449
448
450 # Closes open and locked project versions that are completed
449 # Closes open and locked project versions that are completed
451 def close_completed_versions
450 def close_completed_versions
452 Version.transaction do
451 Version.transaction do
453 versions.where(:status => %w(open locked)).each do |version|
452 versions.where(:status => %w(open locked)).each do |version|
454 if version.completed?
453 if version.completed?
455 version.update_attribute(:status, 'closed')
454 version.update_attribute(:status, 'closed')
456 end
455 end
457 end
456 end
458 end
457 end
459 end
458 end
460
459
461 # Returns a scope of the Versions on subprojects
460 # Returns a scope of the Versions on subprojects
462 def rolled_up_versions
461 def rolled_up_versions
463 @rolled_up_versions ||=
462 @rolled_up_versions ||=
464 Version.
463 Version.
465 joins(:project).
464 joins(:project).
466 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
465 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
467 end
466 end
468
467
469 # Returns a scope of the Versions used by the project
468 # Returns a scope of the Versions used by the project
470 def shared_versions
469 def shared_versions
471 if new_record?
470 if new_record?
472 Version.
471 Version.
473 joins(:project).
472 joins(:project).
474 preload(:project).
473 preload(:project).
475 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
474 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
476 else
475 else
477 @shared_versions ||= begin
476 @shared_versions ||= begin
478 r = root? ? self : root
477 r = root? ? self : root
479 Version.
478 Version.
480 joins(:project).
479 joins(:project).
481 preload(:project).
480 preload(:project).
482 where("#{Project.table_name}.id = #{id}" +
481 where("#{Project.table_name}.id = #{id}" +
483 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
482 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
484 " #{Version.table_name}.sharing = 'system'" +
483 " #{Version.table_name}.sharing = 'system'" +
485 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
484 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
486 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
485 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
487 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
486 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
488 "))")
487 "))")
489 end
488 end
490 end
489 end
491 end
490 end
492
491
493 # Returns a hash of project users grouped by role
492 # Returns a hash of project users grouped by role
494 def users_by_role
493 def users_by_role
495 members.includes(:user, :roles).inject({}) do |h, m|
494 members.includes(:user, :roles).inject({}) do |h, m|
496 m.roles.each do |r|
495 m.roles.each do |r|
497 h[r] ||= []
496 h[r] ||= []
498 h[r] << m.user
497 h[r] << m.user
499 end
498 end
500 h
499 h
501 end
500 end
502 end
501 end
503
502
504 # Adds user as a project member with the default role
503 # Adds user as a project member with the default role
505 # Used for when a non-admin user creates a project
504 # Used for when a non-admin user creates a project
506 def add_default_member(user)
505 def add_default_member(user)
507 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
506 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
508 member = Member.new(:project => self, :principal => user, :roles => [role])
507 member = Member.new(:project => self, :principal => user, :roles => [role])
509 self.members << member
508 self.members << member
510 member
509 member
511 end
510 end
512
511
513 # Deletes all project's members
512 # Deletes all project's members
514 def delete_all_members
513 def delete_all_members
515 me, mr = Member.table_name, MemberRole.table_name
514 me, mr = Member.table_name, MemberRole.table_name
516 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
515 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
517 Member.delete_all(['project_id = ?', id])
516 Member.delete_all(['project_id = ?', id])
518 end
517 end
519
518
520 # Return a Principal scope of users/groups issues can be assigned to
519 # Return a Principal scope of users/groups issues can be assigned to
521 def assignable_users
520 def assignable_users
522 types = ['User']
521 types = ['User']
523 types << 'Group' if Setting.issue_group_assignment?
522 types << 'Group' if Setting.issue_group_assignment?
524
523
525 @assignable_users ||= Principal.
524 @assignable_users ||= Principal.
526 active.
525 active.
527 joins(:members => :roles).
526 joins(:members => :roles).
528 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
527 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
529 uniq.
528 uniq.
530 sorted
529 sorted
531 end
530 end
532
531
533 # Returns the mail addresses of users that should be always notified on project events
532 # Returns the mail addresses of users that should be always notified on project events
534 def recipients
533 def recipients
535 notified_users.collect {|user| user.mail}
534 notified_users.collect {|user| user.mail}
536 end
535 end
537
536
538 # Returns the users that should be notified on project events
537 # Returns the users that should be notified on project events
539 def notified_users
538 def notified_users
540 # TODO: User part should be extracted to User#notify_about?
539 # TODO: User part should be extracted to User#notify_about?
541 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
540 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
542 end
541 end
543
542
544 # Returns a scope of all custom fields enabled for project issues
543 # Returns a scope of all custom fields enabled for project issues
545 # (explicitly associated custom fields and custom fields enabled for all projects)
544 # (explicitly associated custom fields and custom fields enabled for all projects)
546 def all_issue_custom_fields
545 def all_issue_custom_fields
547 @all_issue_custom_fields ||= IssueCustomField.
546 @all_issue_custom_fields ||= IssueCustomField.
548 sorted.
547 sorted.
549 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
548 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
550 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
549 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
551 " WHERE cfp.project_id = ?)", true, id)
550 " WHERE cfp.project_id = ?)", true, id)
552 end
551 end
553
552
554 def project
553 def project
555 self
554 self
556 end
555 end
557
556
558 def <=>(project)
557 def <=>(project)
559 name.downcase <=> project.name.downcase
558 name.downcase <=> project.name.downcase
560 end
559 end
561
560
562 def to_s
561 def to_s
563 name
562 name
564 end
563 end
565
564
566 # Returns a short description of the projects (first lines)
565 # Returns a short description of the projects (first lines)
567 def short_description(length = 255)
566 def short_description(length = 255)
568 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
567 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
569 end
568 end
570
569
571 def css_classes
570 def css_classes
572 s = 'project'
571 s = 'project'
573 s << ' root' if root?
572 s << ' root' if root?
574 s << ' child' if child?
573 s << ' child' if child?
575 s << (leaf? ? ' leaf' : ' parent')
574 s << (leaf? ? ' leaf' : ' parent')
576 unless active?
575 unless active?
577 if archived?
576 if archived?
578 s << ' archived'
577 s << ' archived'
579 else
578 else
580 s << ' closed'
579 s << ' closed'
581 end
580 end
582 end
581 end
583 s
582 s
584 end
583 end
585
584
586 # The earliest start date of a project, based on it's issues and versions
585 # The earliest start date of a project, based on it's issues and versions
587 def start_date
586 def start_date
588 @start_date ||= [
587 @start_date ||= [
589 issues.minimum('start_date'),
588 issues.minimum('start_date'),
590 shared_versions.minimum('effective_date'),
589 shared_versions.minimum('effective_date'),
591 Issue.fixed_version(shared_versions).minimum('start_date')
590 Issue.fixed_version(shared_versions).minimum('start_date')
592 ].compact.min
591 ].compact.min
593 end
592 end
594
593
595 # The latest due date of an issue or version
594 # The latest due date of an issue or version
596 def due_date
595 def due_date
597 @due_date ||= [
596 @due_date ||= [
598 issues.maximum('due_date'),
597 issues.maximum('due_date'),
599 shared_versions.maximum('effective_date'),
598 shared_versions.maximum('effective_date'),
600 Issue.fixed_version(shared_versions).maximum('due_date')
599 Issue.fixed_version(shared_versions).maximum('due_date')
601 ].compact.max
600 ].compact.max
602 end
601 end
603
602
604 def overdue?
603 def overdue?
605 active? && !due_date.nil? && (due_date < Date.today)
604 active? && !due_date.nil? && (due_date < Date.today)
606 end
605 end
607
606
608 # Returns the percent completed for this project, based on the
607 # Returns the percent completed for this project, based on the
609 # progress on it's versions.
608 # progress on it's versions.
610 def completed_percent(options={:include_subprojects => false})
609 def completed_percent(options={:include_subprojects => false})
611 if options.delete(:include_subprojects)
610 if options.delete(:include_subprojects)
612 total = self_and_descendants.collect(&:completed_percent).sum
611 total = self_and_descendants.collect(&:completed_percent).sum
613
612
614 total / self_and_descendants.count
613 total / self_and_descendants.count
615 else
614 else
616 if versions.count > 0
615 if versions.count > 0
617 total = versions.collect(&:completed_percent).sum
616 total = versions.collect(&:completed_percent).sum
618
617
619 total / versions.count
618 total / versions.count
620 else
619 else
621 100
620 100
622 end
621 end
623 end
622 end
624 end
623 end
625
624
626 # Return true if this project allows to do the specified action.
625 # Return true if this project allows to do the specified action.
627 # action can be:
626 # action can be:
628 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
627 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
629 # * a permission Symbol (eg. :edit_project)
628 # * a permission Symbol (eg. :edit_project)
630 def allows_to?(action)
629 def allows_to?(action)
631 if archived?
630 if archived?
632 # No action allowed on archived projects
631 # No action allowed on archived projects
633 return false
632 return false
634 end
633 end
635 unless active? || Redmine::AccessControl.read_action?(action)
634 unless active? || Redmine::AccessControl.read_action?(action)
636 # No write action allowed on closed projects
635 # No write action allowed on closed projects
637 return false
636 return false
638 end
637 end
639 # No action allowed on disabled modules
638 # No action allowed on disabled modules
640 if action.is_a? Hash
639 if action.is_a? Hash
641 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
640 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
642 else
641 else
643 allowed_permissions.include? action
642 allowed_permissions.include? action
644 end
643 end
645 end
644 end
646
645
647 # Return the enabled module with the given name
646 # Return the enabled module with the given name
648 # or nil if the module is not enabled for the project
647 # or nil if the module is not enabled for the project
649 def enabled_module(name)
648 def enabled_module(name)
650 name = name.to_s
649 name = name.to_s
651 enabled_modules.detect {|m| m.name == name}
650 enabled_modules.detect {|m| m.name == name}
652 end
651 end
653
652
654 # Return true if the module with the given name is enabled
653 # Return true if the module with the given name is enabled
655 def module_enabled?(name)
654 def module_enabled?(name)
656 enabled_module(name).present?
655 enabled_module(name).present?
657 end
656 end
658
657
659 def enabled_module_names=(module_names)
658 def enabled_module_names=(module_names)
660 if module_names && module_names.is_a?(Array)
659 if module_names && module_names.is_a?(Array)
661 module_names = module_names.collect(&:to_s).reject(&:blank?)
660 module_names = module_names.collect(&:to_s).reject(&:blank?)
662 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
661 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
663 else
662 else
664 enabled_modules.clear
663 enabled_modules.clear
665 end
664 end
666 end
665 end
667
666
668 # Returns an array of the enabled modules names
667 # Returns an array of the enabled modules names
669 def enabled_module_names
668 def enabled_module_names
670 enabled_modules.collect(&:name)
669 enabled_modules.collect(&:name)
671 end
670 end
672
671
673 # Enable a specific module
672 # Enable a specific module
674 #
673 #
675 # Examples:
674 # Examples:
676 # project.enable_module!(:issue_tracking)
675 # project.enable_module!(:issue_tracking)
677 # project.enable_module!("issue_tracking")
676 # project.enable_module!("issue_tracking")
678 def enable_module!(name)
677 def enable_module!(name)
679 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
678 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
680 end
679 end
681
680
682 # Disable a module if it exists
681 # Disable a module if it exists
683 #
682 #
684 # Examples:
683 # Examples:
685 # project.disable_module!(:issue_tracking)
684 # project.disable_module!(:issue_tracking)
686 # project.disable_module!("issue_tracking")
685 # project.disable_module!("issue_tracking")
687 # project.disable_module!(project.enabled_modules.first)
686 # project.disable_module!(project.enabled_modules.first)
688 def disable_module!(target)
687 def disable_module!(target)
689 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
688 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
690 target.destroy unless target.blank?
689 target.destroy unless target.blank?
691 end
690 end
692
691
693 safe_attributes 'name',
692 safe_attributes 'name',
694 'description',
693 'description',
695 'homepage',
694 'homepage',
696 'is_public',
695 'is_public',
697 'identifier',
696 'identifier',
698 'custom_field_values',
697 'custom_field_values',
699 'custom_fields',
698 'custom_fields',
700 'tracker_ids',
699 'tracker_ids',
701 'issue_custom_field_ids'
700 'issue_custom_field_ids'
702
701
703 safe_attributes 'enabled_module_names',
702 safe_attributes 'enabled_module_names',
704 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
703 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
705
704
706 safe_attributes 'inherit_members',
705 safe_attributes 'inherit_members',
707 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
706 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
708
707
709 # Returns an array of projects that are in this project's hierarchy
708 # Returns an array of projects that are in this project's hierarchy
710 #
709 #
711 # Example: parents, children, siblings
710 # Example: parents, children, siblings
712 def hierarchy
711 def hierarchy
713 parents = project.self_and_ancestors || []
712 parents = project.self_and_ancestors || []
714 descendants = project.descendants || []
713 descendants = project.descendants || []
715 project_hierarchy = parents | descendants # Set union
714 project_hierarchy = parents | descendants # Set union
716 end
715 end
717
716
718 # Returns an auto-generated project identifier based on the last identifier used
717 # Returns an auto-generated project identifier based on the last identifier used
719 def self.next_identifier
718 def self.next_identifier
720 p = Project.order('id DESC').first
719 p = Project.order('id DESC').first
721 p.nil? ? nil : p.identifier.to_s.succ
720 p.nil? ? nil : p.identifier.to_s.succ
722 end
721 end
723
722
724 # Copies and saves the Project instance based on the +project+.
723 # Copies and saves the Project instance based on the +project+.
725 # Duplicates the source project's:
724 # Duplicates the source project's:
726 # * Wiki
725 # * Wiki
727 # * Versions
726 # * Versions
728 # * Categories
727 # * Categories
729 # * Issues
728 # * Issues
730 # * Members
729 # * Members
731 # * Queries
730 # * Queries
732 #
731 #
733 # Accepts an +options+ argument to specify what to copy
732 # Accepts an +options+ argument to specify what to copy
734 #
733 #
735 # Examples:
734 # Examples:
736 # project.copy(1) # => copies everything
735 # project.copy(1) # => copies everything
737 # project.copy(1, :only => 'members') # => copies members only
736 # project.copy(1, :only => 'members') # => copies members only
738 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
737 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
739 def copy(project, options={})
738 def copy(project, options={})
740 project = project.is_a?(Project) ? project : Project.find(project)
739 project = project.is_a?(Project) ? project : Project.find(project)
741
740
742 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
741 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
743 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
742 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
744
743
745 Project.transaction do
744 Project.transaction do
746 if save
745 if save
747 reload
746 reload
748 to_be_copied.each do |name|
747 to_be_copied.each do |name|
749 send "copy_#{name}", project
748 send "copy_#{name}", project
750 end
749 end
751 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
750 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
752 save
751 save
753 end
752 end
754 end
753 end
755 true
754 true
756 end
755 end
757
756
758 # Returns a new unsaved Project instance with attributes copied from +project+
757 # Returns a new unsaved Project instance with attributes copied from +project+
759 def self.copy_from(project)
758 def self.copy_from(project)
760 project = project.is_a?(Project) ? project : Project.find(project)
759 project = project.is_a?(Project) ? project : Project.find(project)
761 # clear unique attributes
760 # clear unique attributes
762 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
761 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
763 copy = Project.new(attributes)
762 copy = Project.new(attributes)
764 copy.enabled_modules = project.enabled_modules
763 copy.enabled_modules = project.enabled_modules
765 copy.trackers = project.trackers
764 copy.trackers = project.trackers
766 copy.custom_values = project.custom_values.collect {|v| v.clone}
765 copy.custom_values = project.custom_values.collect {|v| v.clone}
767 copy.issue_custom_fields = project.issue_custom_fields
766 copy.issue_custom_fields = project.issue_custom_fields
768 copy
767 copy
769 end
768 end
770
769
771 # Yields the given block for each project with its level in the tree
770 # Yields the given block for each project with its level in the tree
772 def self.project_tree(projects, &block)
771 def self.project_tree(projects, &block)
773 ancestors = []
772 ancestors = []
774 projects.sort_by(&:lft).each do |project|
773 projects.sort_by(&:lft).each do |project|
775 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
774 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
776 ancestors.pop
775 ancestors.pop
777 end
776 end
778 yield project, ancestors.size
777 yield project, ancestors.size
779 ancestors << project
778 ancestors << project
780 end
779 end
781 end
780 end
782
781
783 private
782 private
784
783
785 def after_parent_changed(parent_was)
784 def after_parent_changed(parent_was)
786 remove_inherited_member_roles
785 remove_inherited_member_roles
787 add_inherited_member_roles
786 add_inherited_member_roles
788 end
787 end
789
788
790 def update_inherited_members
789 def update_inherited_members
791 if parent
790 if parent
792 if inherit_members? && !inherit_members_was
791 if inherit_members? && !inherit_members_was
793 remove_inherited_member_roles
792 remove_inherited_member_roles
794 add_inherited_member_roles
793 add_inherited_member_roles
795 elsif !inherit_members? && inherit_members_was
794 elsif !inherit_members? && inherit_members_was
796 remove_inherited_member_roles
795 remove_inherited_member_roles
797 end
796 end
798 end
797 end
799 end
798 end
800
799
801 def remove_inherited_member_roles
800 def remove_inherited_member_roles
802 member_roles = memberships.map(&:member_roles).flatten
801 member_roles = memberships.map(&:member_roles).flatten
803 member_role_ids = member_roles.map(&:id)
802 member_role_ids = member_roles.map(&:id)
804 member_roles.each do |member_role|
803 member_roles.each do |member_role|
805 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
804 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
806 member_role.destroy
805 member_role.destroy
807 end
806 end
808 end
807 end
809 end
808 end
810
809
811 def add_inherited_member_roles
810 def add_inherited_member_roles
812 if inherit_members? && parent
811 if inherit_members? && parent
813 parent.memberships.each do |parent_member|
812 parent.memberships.each do |parent_member|
814 member = Member.find_or_new(self.id, parent_member.user_id)
813 member = Member.find_or_new(self.id, parent_member.user_id)
815 parent_member.member_roles.each do |parent_member_role|
814 parent_member.member_roles.each do |parent_member_role|
816 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
815 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
817 end
816 end
818 member.save!
817 member.save!
819 end
818 end
820 end
819 end
821 end
820 end
822
821
823 # Copies wiki from +project+
822 # Copies wiki from +project+
824 def copy_wiki(project)
823 def copy_wiki(project)
825 # Check that the source project has a wiki first
824 # Check that the source project has a wiki first
826 unless project.wiki.nil?
825 unless project.wiki.nil?
827 wiki = self.wiki || Wiki.new
826 wiki = self.wiki || Wiki.new
828 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
827 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
829 wiki_pages_map = {}
828 wiki_pages_map = {}
830 project.wiki.pages.each do |page|
829 project.wiki.pages.each do |page|
831 # Skip pages without content
830 # Skip pages without content
832 next if page.content.nil?
831 next if page.content.nil?
833 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
832 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
834 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
833 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
835 new_wiki_page.content = new_wiki_content
834 new_wiki_page.content = new_wiki_content
836 wiki.pages << new_wiki_page
835 wiki.pages << new_wiki_page
837 wiki_pages_map[page.id] = new_wiki_page
836 wiki_pages_map[page.id] = new_wiki_page
838 end
837 end
839
838
840 self.wiki = wiki
839 self.wiki = wiki
841 wiki.save
840 wiki.save
842 # Reproduce page hierarchy
841 # Reproduce page hierarchy
843 project.wiki.pages.each do |page|
842 project.wiki.pages.each do |page|
844 if page.parent_id && wiki_pages_map[page.id]
843 if page.parent_id && wiki_pages_map[page.id]
845 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
844 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
846 wiki_pages_map[page.id].save
845 wiki_pages_map[page.id].save
847 end
846 end
848 end
847 end
849 end
848 end
850 end
849 end
851
850
852 # Copies versions from +project+
851 # Copies versions from +project+
853 def copy_versions(project)
852 def copy_versions(project)
854 project.versions.each do |version|
853 project.versions.each do |version|
855 new_version = Version.new
854 new_version = Version.new
856 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
855 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
857 self.versions << new_version
856 self.versions << new_version
858 end
857 end
859 end
858 end
860
859
861 # Copies issue categories from +project+
860 # Copies issue categories from +project+
862 def copy_issue_categories(project)
861 def copy_issue_categories(project)
863 project.issue_categories.each do |issue_category|
862 project.issue_categories.each do |issue_category|
864 new_issue_category = IssueCategory.new
863 new_issue_category = IssueCategory.new
865 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
864 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
866 self.issue_categories << new_issue_category
865 self.issue_categories << new_issue_category
867 end
866 end
868 end
867 end
869
868
870 # Copies issues from +project+
869 # Copies issues from +project+
871 def copy_issues(project)
870 def copy_issues(project)
872 # Stores the source issue id as a key and the copied issues as the
871 # Stores the source issue id as a key and the copied issues as the
873 # value. Used to map the two together for issue relations.
872 # value. Used to map the two together for issue relations.
874 issues_map = {}
873 issues_map = {}
875
874
876 # Store status and reopen locked/closed versions
875 # Store status and reopen locked/closed versions
877 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
876 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
878 version_statuses.each do |version, status|
877 version_statuses.each do |version, status|
879 version.update_attribute :status, 'open'
878 version.update_attribute :status, 'open'
880 end
879 end
881
880
882 # Get issues sorted by root_id, lft so that parent issues
881 # Get issues sorted by root_id, lft so that parent issues
883 # get copied before their children
882 # get copied before their children
884 project.issues.reorder('root_id, lft').each do |issue|
883 project.issues.reorder('root_id, lft').each do |issue|
885 new_issue = Issue.new
884 new_issue = Issue.new
886 new_issue.copy_from(issue, :subtasks => false, :link => false)
885 new_issue.copy_from(issue, :subtasks => false, :link => false)
887 new_issue.project = self
886 new_issue.project = self
888 # Changing project resets the custom field values
887 # Changing project resets the custom field values
889 # TODO: handle this in Issue#project=
888 # TODO: handle this in Issue#project=
890 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
889 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
891 # Reassign fixed_versions by name, since names are unique per project
890 # Reassign fixed_versions by name, since names are unique per project
892 if issue.fixed_version && issue.fixed_version.project == project
891 if issue.fixed_version && issue.fixed_version.project == project
893 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
892 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
894 end
893 end
895 # Reassign the category by name, since names are unique per project
894 # Reassign the category by name, since names are unique per project
896 if issue.category
895 if issue.category
897 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
896 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
898 end
897 end
899 # Parent issue
898 # Parent issue
900 if issue.parent_id
899 if issue.parent_id
901 if copied_parent = issues_map[issue.parent_id]
900 if copied_parent = issues_map[issue.parent_id]
902 new_issue.parent_issue_id = copied_parent.id
901 new_issue.parent_issue_id = copied_parent.id
903 end
902 end
904 end
903 end
905
904
906 self.issues << new_issue
905 self.issues << new_issue
907 if new_issue.new_record?
906 if new_issue.new_record?
908 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
907 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
909 else
908 else
910 issues_map[issue.id] = new_issue unless new_issue.new_record?
909 issues_map[issue.id] = new_issue unless new_issue.new_record?
911 end
910 end
912 end
911 end
913
912
914 # Restore locked/closed version statuses
913 # Restore locked/closed version statuses
915 version_statuses.each do |version, status|
914 version_statuses.each do |version, status|
916 version.update_attribute :status, status
915 version.update_attribute :status, status
917 end
916 end
918
917
919 # Relations after in case issues related each other
918 # Relations after in case issues related each other
920 project.issues.each do |issue|
919 project.issues.each do |issue|
921 new_issue = issues_map[issue.id]
920 new_issue = issues_map[issue.id]
922 unless new_issue
921 unless new_issue
923 # Issue was not copied
922 # Issue was not copied
924 next
923 next
925 end
924 end
926
925
927 # Relations
926 # Relations
928 issue.relations_from.each do |source_relation|
927 issue.relations_from.each do |source_relation|
929 new_issue_relation = IssueRelation.new
928 new_issue_relation = IssueRelation.new
930 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
929 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
931 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
930 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
932 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
931 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
933 new_issue_relation.issue_to = source_relation.issue_to
932 new_issue_relation.issue_to = source_relation.issue_to
934 end
933 end
935 new_issue.relations_from << new_issue_relation
934 new_issue.relations_from << new_issue_relation
936 end
935 end
937
936
938 issue.relations_to.each do |source_relation|
937 issue.relations_to.each do |source_relation|
939 new_issue_relation = IssueRelation.new
938 new_issue_relation = IssueRelation.new
940 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
939 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
941 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
940 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
942 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
941 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
943 new_issue_relation.issue_from = source_relation.issue_from
942 new_issue_relation.issue_from = source_relation.issue_from
944 end
943 end
945 new_issue.relations_to << new_issue_relation
944 new_issue.relations_to << new_issue_relation
946 end
945 end
947 end
946 end
948 end
947 end
949
948
950 # Copies members from +project+
949 # Copies members from +project+
951 def copy_members(project)
950 def copy_members(project)
952 # Copy users first, then groups to handle members with inherited and given roles
951 # Copy users first, then groups to handle members with inherited and given roles
953 members_to_copy = []
952 members_to_copy = []
954 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
953 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
955 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
954 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
956
955
957 members_to_copy.each do |member|
956 members_to_copy.each do |member|
958 new_member = Member.new
957 new_member = Member.new
959 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
958 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
960 # only copy non inherited roles
959 # only copy non inherited roles
961 # inherited roles will be added when copying the group membership
960 # inherited roles will be added when copying the group membership
962 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
961 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
963 next if role_ids.empty?
962 next if role_ids.empty?
964 new_member.role_ids = role_ids
963 new_member.role_ids = role_ids
965 new_member.project = self
964 new_member.project = self
966 self.members << new_member
965 self.members << new_member
967 end
966 end
968 end
967 end
969
968
970 # Copies queries from +project+
969 # Copies queries from +project+
971 def copy_queries(project)
970 def copy_queries(project)
972 project.queries.each do |query|
971 project.queries.each do |query|
973 new_query = IssueQuery.new
972 new_query = IssueQuery.new
974 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
973 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
975 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
974 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
976 new_query.project = self
975 new_query.project = self
977 new_query.user_id = query.user_id
976 new_query.user_id = query.user_id
978 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
977 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
979 self.queries << new_query
978 self.queries << new_query
980 end
979 end
981 end
980 end
982
981
983 # Copies boards from +project+
982 # Copies boards from +project+
984 def copy_boards(project)
983 def copy_boards(project)
985 project.boards.each do |board|
984 project.boards.each do |board|
986 new_board = Board.new
985 new_board = Board.new
987 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
986 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
988 new_board.project = self
987 new_board.project = self
989 self.boards << new_board
988 self.boards << new_board
990 end
989 end
991 end
990 end
992
991
993 def allowed_permissions
992 def allowed_permissions
994 @allowed_permissions ||= begin
993 @allowed_permissions ||= begin
995 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
994 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
996 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
995 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
997 end
996 end
998 end
997 end
999
998
1000 def allowed_actions
999 def allowed_actions
1001 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1000 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1002 end
1001 end
1003
1002
1004 # Returns all the active Systemwide and project specific activities
1003 # Returns all the active Systemwide and project specific activities
1005 def active_activities
1004 def active_activities
1006 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
1005 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
1007
1006
1008 if overridden_activity_ids.empty?
1007 if overridden_activity_ids.empty?
1009 return TimeEntryActivity.shared.active
1008 return TimeEntryActivity.shared.active
1010 else
1009 else
1011 return system_activities_and_project_overrides
1010 return system_activities_and_project_overrides
1012 end
1011 end
1013 end
1012 end
1014
1013
1015 # Returns all the Systemwide and project specific activities
1014 # Returns all the Systemwide and project specific activities
1016 # (inactive and active)
1015 # (inactive and active)
1017 def all_activities
1016 def all_activities
1018 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
1017 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
1019
1018
1020 if overridden_activity_ids.empty?
1019 if overridden_activity_ids.empty?
1021 return TimeEntryActivity.shared
1020 return TimeEntryActivity.shared
1022 else
1021 else
1023 return system_activities_and_project_overrides(true)
1022 return system_activities_and_project_overrides(true)
1024 end
1023 end
1025 end
1024 end
1026
1025
1027 # Returns the systemwide active activities merged with the project specific overrides
1026 # Returns the systemwide active activities merged with the project specific overrides
1028 def system_activities_and_project_overrides(include_inactive=false)
1027 def system_activities_and_project_overrides(include_inactive=false)
1029 t = TimeEntryActivity.table_name
1028 t = TimeEntryActivity.table_name
1030 scope = TimeEntryActivity.where(
1029 scope = TimeEntryActivity.where(
1031 "(#{t}.project_id IS NULL AND #{t}.id NOT IN (?)) OR (#{t}.project_id = ?)",
1030 "(#{t}.project_id IS NULL AND #{t}.id NOT IN (?)) OR (#{t}.project_id = ?)",
1032 time_entry_activities.map(&:parent_id), id
1031 time_entry_activities.map(&:parent_id), id
1033 )
1032 )
1034 unless include_inactive
1033 unless include_inactive
1035 scope = scope.active
1034 scope = scope.active
1036 end
1035 end
1037 scope
1036 scope
1038 end
1037 end
1039
1038
1040 # Archives subprojects recursively
1039 # Archives subprojects recursively
1041 def archive!
1040 def archive!
1042 children.each do |subproject|
1041 children.each do |subproject|
1043 subproject.send :archive!
1042 subproject.send :archive!
1044 end
1043 end
1045 update_attribute :status, STATUS_ARCHIVED
1044 update_attribute :status, STATUS_ARCHIVED
1046 end
1045 end
1047
1046
1048 def update_position_under_parent
1047 def update_position_under_parent
1049 set_or_update_position_under(parent)
1048 set_or_update_position_under(parent)
1050 end
1049 end
1051
1050
1052 public
1051 public
1053
1052
1054 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1053 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1055 def set_or_update_position_under(target_parent)
1054 def set_or_update_position_under(target_parent)
1056 parent_was = parent
1055 parent_was = parent
1057 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1056 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1058 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1057 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1059
1058
1060 if to_be_inserted_before
1059 if to_be_inserted_before
1061 move_to_left_of(to_be_inserted_before)
1060 move_to_left_of(to_be_inserted_before)
1062 elsif target_parent.nil?
1061 elsif target_parent.nil?
1063 if sibs.empty?
1062 if sibs.empty?
1064 # move_to_root adds the project in first (ie. left) position
1063 # move_to_root adds the project in first (ie. left) position
1065 move_to_root
1064 move_to_root
1066 else
1065 else
1067 move_to_right_of(sibs.last) unless self == sibs.last
1066 move_to_right_of(sibs.last) unless self == sibs.last
1068 end
1067 end
1069 else
1068 else
1070 # move_to_child_of adds the project in last (ie.right) position
1069 # move_to_child_of adds the project in last (ie.right) position
1071 move_to_child_of(target_parent)
1070 move_to_child_of(target_parent)
1072 end
1071 end
1073 if parent_was != target_parent
1072 if parent_was != target_parent
1074 after_parent_changed(parent_was)
1073 after_parent_changed(parent_was)
1075 end
1074 end
1076 end
1075 end
1077 end
1076 end
General Comments 0
You need to be logged in to leave comments. Login now