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