##// END OF EJS Templates
Adds a scope for issue auto complete....
Jean-Philippe Lang -
r15863:c283212f9fa6
parent child
Show More
@@ -1,53 +1,53
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class AutoCompletesController < ApplicationController
18 class AutoCompletesController < ApplicationController
19 before_action :find_project
19 before_action :find_project
20
20
21 def issues
21 def issues
22 @issues = []
22 @issues = []
23 q = (params[:q] || params[:term]).to_s.strip
23 q = (params[:q] || params[:term]).to_s.strip
24 status = params[:status].to_s
24 status = params[:status].to_s
25 issue_id = params[:issue_id].to_s
25 issue_id = params[:issue_id].to_s
26 if q.present?
26 if q.present?
27 scope = Issue.cross_project_scope(@project, params[:scope]).visible
27 scope = Issue.cross_project_scope(@project, params[:scope]).visible
28 if status.present?
28 if status.present?
29 scope = scope.open(status == 'o')
29 scope = scope.open(status == 'o')
30 end
30 end
31 if issue_id.present?
31 if issue_id.present?
32 scope = scope.where.not(:id => issue_id.to_i)
32 scope = scope.where.not(:id => issue_id.to_i)
33 end
33 end
34 if q.match(/\A#?(\d+)\z/)
34 if q.match(/\A#?(\d+)\z/)
35 @issues << scope.find_by_id($1.to_i)
35 @issues << scope.find_by_id($1.to_i)
36 end
36 end
37
37
38 @issues += scope.where("LOWER(#{Issue.table_name}.subject) LIKE LOWER(?)", "%#{q}%").order(:id => :desc).limit(10).to_a
38 @issues += scope.like(q).order(:id => :desc).limit(10).to_a
39 @issues.compact!
39 @issues.compact!
40 end
40 end
41 render :layout => false
41 render :layout => false
42 end
42 end
43
43
44 private
44 private
45
45
46 def find_project
46 def find_project
47 if params[:project_id].present?
47 if params[:project_id].present?
48 @project = Project.find(params[:project_id])
48 @project = Project.find(params[:project_id])
49 end
49 end
50 rescue ActiveRecord::RecordNotFound
50 rescue ActiveRecord::RecordNotFound
51 render_404
51 render_404
52 end
52 end
53 end
53 end
@@ -1,1779 +1,1785
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class 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 before_save :set_parent_id
22 before_save :set_parent_id
23 include Redmine::NestedSet::IssueNestedSet
23 include Redmine::NestedSet::IssueNestedSet
24
24
25 belongs_to :project
25 belongs_to :project
26 belongs_to :tracker
26 belongs_to :tracker
27 belongs_to :status, :class_name => 'IssueStatus'
27 belongs_to :status, :class_name => 'IssueStatus'
28 belongs_to :author, :class_name => 'User'
28 belongs_to :author, :class_name => 'User'
29 belongs_to :assigned_to, :class_name => 'Principal'
29 belongs_to :assigned_to, :class_name => 'Principal'
30 belongs_to :fixed_version, :class_name => 'Version'
30 belongs_to :fixed_version, :class_name => 'Version'
31 belongs_to :priority, :class_name => 'IssuePriority'
31 belongs_to :priority, :class_name => 'IssuePriority'
32 belongs_to :category, :class_name => 'IssueCategory'
32 belongs_to :category, :class_name => 'IssueCategory'
33
33
34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
35 has_many :time_entries, :dependent => :destroy
35 has_many :time_entries, :dependent => :destroy
36 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
36 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
37
37
38 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
38 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
39 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
39 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
40
40
41 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
41 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
42 acts_as_customizable
42 acts_as_customizable
43 acts_as_watchable
43 acts_as_watchable
44 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
44 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
45 :preload => [:project, :status, :tracker],
45 :preload => [:project, :status, :tracker],
46 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
46 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
47
47
48 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
48 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
49 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
49 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
50 :type => Proc.new {|o| 'issue' + (o.closed? ? '-closed' : '') }
50 :type => Proc.new {|o| 'issue' + (o.closed? ? '-closed' : '') }
51
51
52 acts_as_activity_provider :scope => preload(:project, :author, :tracker, :status),
52 acts_as_activity_provider :scope => preload(:project, :author, :tracker, :status),
53 :author_key => :author_id
53 :author_key => :author_id
54
54
55 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
55 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
56
56
57 attr_accessor :deleted_attachment_ids
57 attr_accessor :deleted_attachment_ids
58 attr_reader :current_journal
58 attr_reader :current_journal
59 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
59 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
60
60
61 validates_presence_of :subject, :project, :tracker
61 validates_presence_of :subject, :project, :tracker
62 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
62 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
63 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
63 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
64 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
64 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
65
65
66 validates_length_of :subject, :maximum => 255
66 validates_length_of :subject, :maximum => 255
67 validates_inclusion_of :done_ratio, :in => 0..100
67 validates_inclusion_of :done_ratio, :in => 0..100
68 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
68 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
69 validates :start_date, :date => true
69 validates :start_date, :date => true
70 validates :due_date, :date => true
70 validates :due_date, :date => true
71 validate :validate_issue, :validate_required_fields
71 validate :validate_issue, :validate_required_fields
72 attr_protected :id
72 attr_protected :id
73
73
74 scope :visible, lambda {|*args|
74 scope :visible, lambda {|*args|
75 joins(:project).
75 joins(:project).
76 where(Issue.visible_condition(args.shift || User.current, *args))
76 where(Issue.visible_condition(args.shift || User.current, *args))
77 }
77 }
78
78
79 scope :open, lambda {|*args|
79 scope :open, lambda {|*args|
80 is_closed = args.size > 0 ? !args.first : false
80 is_closed = args.size > 0 ? !args.first : false
81 joins(:status).
81 joins(:status).
82 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
82 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
83 }
83 }
84
84
85 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
85 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
86 scope :on_active_project, lambda {
86 scope :on_active_project, lambda {
87 joins(:project).
87 joins(:project).
88 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
88 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
89 }
89 }
90 scope :fixed_version, lambda {|versions|
90 scope :fixed_version, lambda {|versions|
91 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
91 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
92 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
92 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
93 }
93 }
94 scope :assigned_to, lambda {|arg|
94 scope :assigned_to, lambda {|arg|
95 arg = Array(arg).uniq
95 arg = Array(arg).uniq
96 ids = arg.map {|p| p.is_a?(Principal) ? p.id : p}
96 ids = arg.map {|p| p.is_a?(Principal) ? p.id : p}
97 ids += arg.select {|p| p.is_a?(User)}.map(&:group_ids).flatten.uniq
97 ids += arg.select {|p| p.is_a?(User)}.map(&:group_ids).flatten.uniq
98 ids.compact!
98 ids.compact!
99 ids.any? ? where(:assigned_to_id => ids) : none
99 ids.any? ? where(:assigned_to_id => ids) : none
100 }
100 }
101 scope :like, lambda {|q|
102 q = q.to_s
103 if q.present?
104 where("LOWER(#{table_name}.subject) LIKE LOWER(?)", "%#{q}%")
105 end
106 }
101
107
102 before_validation :clear_disabled_fields
108 before_validation :clear_disabled_fields
103 before_create :default_assign
109 before_create :default_assign
104 before_save :close_duplicates, :update_done_ratio_from_issue_status,
110 before_save :close_duplicates, :update_done_ratio_from_issue_status,
105 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
111 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
106 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
112 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
107 after_save :reschedule_following_issues, :update_nested_set_attributes,
113 after_save :reschedule_following_issues, :update_nested_set_attributes,
108 :update_parent_attributes, :delete_selected_attachments, :create_journal
114 :update_parent_attributes, :delete_selected_attachments, :create_journal
109 # Should be after_create but would be called before previous after_save callbacks
115 # Should be after_create but would be called before previous after_save callbacks
110 after_save :after_create_from_copy
116 after_save :after_create_from_copy
111 after_destroy :update_parent_attributes
117 after_destroy :update_parent_attributes
112 after_create :send_notification
118 after_create :send_notification
113 # Keep it at the end of after_save callbacks
119 # Keep it at the end of after_save callbacks
114 after_save :clear_assigned_to_was
120 after_save :clear_assigned_to_was
115
121
116 # Returns a SQL conditions string used to find all issues visible by the specified user
122 # Returns a SQL conditions string used to find all issues visible by the specified user
117 def self.visible_condition(user, options={})
123 def self.visible_condition(user, options={})
118 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
124 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
119 sql = if user.id && user.logged?
125 sql = if user.id && user.logged?
120 case role.issues_visibility
126 case role.issues_visibility
121 when 'all'
127 when 'all'
122 '1=1'
128 '1=1'
123 when 'default'
129 when 'default'
124 user_ids = [user.id] + user.groups.map(&:id).compact
130 user_ids = [user.id] + user.groups.map(&:id).compact
125 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
131 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
126 when 'own'
132 when 'own'
127 user_ids = [user.id] + user.groups.map(&:id).compact
133 user_ids = [user.id] + user.groups.map(&:id).compact
128 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
134 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
129 else
135 else
130 '1=0'
136 '1=0'
131 end
137 end
132 else
138 else
133 "(#{table_name}.is_private = #{connection.quoted_false})"
139 "(#{table_name}.is_private = #{connection.quoted_false})"
134 end
140 end
135 unless role.permissions_all_trackers?(:view_issues)
141 unless role.permissions_all_trackers?(:view_issues)
136 tracker_ids = role.permissions_tracker_ids(:view_issues)
142 tracker_ids = role.permissions_tracker_ids(:view_issues)
137 if tracker_ids.any?
143 if tracker_ids.any?
138 sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
144 sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
139 else
145 else
140 sql = '1=0'
146 sql = '1=0'
141 end
147 end
142 end
148 end
143 sql
149 sql
144 end
150 end
145 end
151 end
146
152
147 # Returns true if usr or current user is allowed to view the issue
153 # Returns true if usr or current user is allowed to view the issue
148 def visible?(usr=nil)
154 def visible?(usr=nil)
149 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
155 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
150 visible = if user.logged?
156 visible = if user.logged?
151 case role.issues_visibility
157 case role.issues_visibility
152 when 'all'
158 when 'all'
153 true
159 true
154 when 'default'
160 when 'default'
155 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
161 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
156 when 'own'
162 when 'own'
157 self.author == user || user.is_or_belongs_to?(assigned_to)
163 self.author == user || user.is_or_belongs_to?(assigned_to)
158 else
164 else
159 false
165 false
160 end
166 end
161 else
167 else
162 !self.is_private?
168 !self.is_private?
163 end
169 end
164 unless role.permissions_all_trackers?(:view_issues)
170 unless role.permissions_all_trackers?(:view_issues)
165 visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
171 visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
166 end
172 end
167 visible
173 visible
168 end
174 end
169 end
175 end
170
176
171 # Returns true if user or current user is allowed to edit or add notes to the issue
177 # Returns true if user or current user is allowed to edit or add notes to the issue
172 def editable?(user=User.current)
178 def editable?(user=User.current)
173 attributes_editable?(user) || notes_addable?(user)
179 attributes_editable?(user) || notes_addable?(user)
174 end
180 end
175
181
176 # Returns true if user or current user is allowed to edit the issue
182 # Returns true if user or current user is allowed to edit the issue
177 def attributes_editable?(user=User.current)
183 def attributes_editable?(user=User.current)
178 user_tracker_permission?(user, :edit_issues)
184 user_tracker_permission?(user, :edit_issues)
179 end
185 end
180
186
181 # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
187 # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
182 def attachments_editable?(user=User.current)
188 def attachments_editable?(user=User.current)
183 attributes_editable?(user)
189 attributes_editable?(user)
184 end
190 end
185
191
186 # Returns true if user or current user is allowed to add notes to the issue
192 # Returns true if user or current user is allowed to add notes to the issue
187 def notes_addable?(user=User.current)
193 def notes_addable?(user=User.current)
188 user_tracker_permission?(user, :add_issue_notes)
194 user_tracker_permission?(user, :add_issue_notes)
189 end
195 end
190
196
191 # Returns true if user or current user is allowed to delete the issue
197 # Returns true if user or current user is allowed to delete the issue
192 def deletable?(user=User.current)
198 def deletable?(user=User.current)
193 user_tracker_permission?(user, :delete_issues)
199 user_tracker_permission?(user, :delete_issues)
194 end
200 end
195
201
196 def initialize(attributes=nil, *args)
202 def initialize(attributes=nil, *args)
197 super
203 super
198 if new_record?
204 if new_record?
199 # set default values for new records only
205 # set default values for new records only
200 self.priority ||= IssuePriority.default
206 self.priority ||= IssuePriority.default
201 self.watcher_user_ids = []
207 self.watcher_user_ids = []
202 end
208 end
203 end
209 end
204
210
205 def create_or_update
211 def create_or_update
206 super
212 super
207 ensure
213 ensure
208 @status_was = nil
214 @status_was = nil
209 end
215 end
210 private :create_or_update
216 private :create_or_update
211
217
212 # AR#Persistence#destroy would raise and RecordNotFound exception
218 # AR#Persistence#destroy would raise and RecordNotFound exception
213 # if the issue was already deleted or updated (non matching lock_version).
219 # if the issue was already deleted or updated (non matching lock_version).
214 # This is a problem when bulk deleting issues or deleting a project
220 # This is a problem when bulk deleting issues or deleting a project
215 # (because an issue may already be deleted if its parent was deleted
221 # (because an issue may already be deleted if its parent was deleted
216 # first).
222 # first).
217 # The issue is reloaded by the nested_set before being deleted so
223 # The issue is reloaded by the nested_set before being deleted so
218 # the lock_version condition should not be an issue but we handle it.
224 # the lock_version condition should not be an issue but we handle it.
219 def destroy
225 def destroy
220 super
226 super
221 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
227 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
222 # Stale or already deleted
228 # Stale or already deleted
223 begin
229 begin
224 reload
230 reload
225 rescue ActiveRecord::RecordNotFound
231 rescue ActiveRecord::RecordNotFound
226 # The issue was actually already deleted
232 # The issue was actually already deleted
227 @destroyed = true
233 @destroyed = true
228 return freeze
234 return freeze
229 end
235 end
230 # The issue was stale, retry to destroy
236 # The issue was stale, retry to destroy
231 super
237 super
232 end
238 end
233
239
234 alias :base_reload :reload
240 alias :base_reload :reload
235 def reload(*args)
241 def reload(*args)
236 @workflow_rule_by_attribute = nil
242 @workflow_rule_by_attribute = nil
237 @assignable_versions = nil
243 @assignable_versions = nil
238 @relations = nil
244 @relations = nil
239 @spent_hours = nil
245 @spent_hours = nil
240 @total_spent_hours = nil
246 @total_spent_hours = nil
241 @total_estimated_hours = nil
247 @total_estimated_hours = nil
242 base_reload(*args)
248 base_reload(*args)
243 end
249 end
244
250
245 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
251 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
246 def available_custom_fields
252 def available_custom_fields
247 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
253 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
248 end
254 end
249
255
250 def visible_custom_field_values(user=nil)
256 def visible_custom_field_values(user=nil)
251 user_real = user || User.current
257 user_real = user || User.current
252 custom_field_values.select do |value|
258 custom_field_values.select do |value|
253 value.custom_field.visible_by?(project, user_real)
259 value.custom_field.visible_by?(project, user_real)
254 end
260 end
255 end
261 end
256
262
257 # Copies attributes from another issue, arg can be an id or an Issue
263 # Copies attributes from another issue, arg can be an id or an Issue
258 def copy_from(arg, options={})
264 def copy_from(arg, options={})
259 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
265 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
260 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on", "closed_on")
266 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on", "closed_on")
261 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
267 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
262 self.status = issue.status
268 self.status = issue.status
263 self.author = User.current
269 self.author = User.current
264 unless options[:attachments] == false
270 unless options[:attachments] == false
265 self.attachments = issue.attachments.map do |attachement|
271 self.attachments = issue.attachments.map do |attachement|
266 attachement.copy(:container => self)
272 attachement.copy(:container => self)
267 end
273 end
268 end
274 end
269 @copied_from = issue
275 @copied_from = issue
270 @copy_options = options
276 @copy_options = options
271 self
277 self
272 end
278 end
273
279
274 # Returns an unsaved copy of the issue
280 # Returns an unsaved copy of the issue
275 def copy(attributes=nil, copy_options={})
281 def copy(attributes=nil, copy_options={})
276 copy = self.class.new.copy_from(self, copy_options)
282 copy = self.class.new.copy_from(self, copy_options)
277 copy.attributes = attributes if attributes
283 copy.attributes = attributes if attributes
278 copy
284 copy
279 end
285 end
280
286
281 # Returns true if the issue is a copy
287 # Returns true if the issue is a copy
282 def copy?
288 def copy?
283 @copied_from.present?
289 @copied_from.present?
284 end
290 end
285
291
286 def status_id=(status_id)
292 def status_id=(status_id)
287 if status_id.to_s != self.status_id.to_s
293 if status_id.to_s != self.status_id.to_s
288 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
294 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
289 end
295 end
290 self.status_id
296 self.status_id
291 end
297 end
292
298
293 # Sets the status.
299 # Sets the status.
294 def status=(status)
300 def status=(status)
295 if status != self.status
301 if status != self.status
296 @workflow_rule_by_attribute = nil
302 @workflow_rule_by_attribute = nil
297 end
303 end
298 association(:status).writer(status)
304 association(:status).writer(status)
299 end
305 end
300
306
301 def priority_id=(pid)
307 def priority_id=(pid)
302 self.priority = nil
308 self.priority = nil
303 write_attribute(:priority_id, pid)
309 write_attribute(:priority_id, pid)
304 end
310 end
305
311
306 def category_id=(cid)
312 def category_id=(cid)
307 self.category = nil
313 self.category = nil
308 write_attribute(:category_id, cid)
314 write_attribute(:category_id, cid)
309 end
315 end
310
316
311 def fixed_version_id=(vid)
317 def fixed_version_id=(vid)
312 self.fixed_version = nil
318 self.fixed_version = nil
313 write_attribute(:fixed_version_id, vid)
319 write_attribute(:fixed_version_id, vid)
314 end
320 end
315
321
316 def tracker_id=(tracker_id)
322 def tracker_id=(tracker_id)
317 if tracker_id.to_s != self.tracker_id.to_s
323 if tracker_id.to_s != self.tracker_id.to_s
318 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
324 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
319 end
325 end
320 self.tracker_id
326 self.tracker_id
321 end
327 end
322
328
323 # Sets the tracker.
329 # Sets the tracker.
324 # This will set the status to the default status of the new tracker if:
330 # This will set the status to the default status of the new tracker if:
325 # * the status was the default for the previous tracker
331 # * the status was the default for the previous tracker
326 # * or if the status was not part of the new tracker statuses
332 # * or if the status was not part of the new tracker statuses
327 # * or the status was nil
333 # * or the status was nil
328 def tracker=(tracker)
334 def tracker=(tracker)
329 tracker_was = self.tracker
335 tracker_was = self.tracker
330 association(:tracker).writer(tracker)
336 association(:tracker).writer(tracker)
331 if tracker != tracker_was
337 if tracker != tracker_was
332 if status == tracker_was.try(:default_status)
338 if status == tracker_was.try(:default_status)
333 self.status = nil
339 self.status = nil
334 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
340 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
335 self.status = nil
341 self.status = nil
336 end
342 end
337 reassign_custom_field_values
343 reassign_custom_field_values
338 @workflow_rule_by_attribute = nil
344 @workflow_rule_by_attribute = nil
339 end
345 end
340 self.status ||= default_status
346 self.status ||= default_status
341 self.tracker
347 self.tracker
342 end
348 end
343
349
344 def project_id=(project_id)
350 def project_id=(project_id)
345 if project_id.to_s != self.project_id.to_s
351 if project_id.to_s != self.project_id.to_s
346 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
352 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
347 end
353 end
348 self.project_id
354 self.project_id
349 end
355 end
350
356
351 # Sets the project.
357 # Sets the project.
352 # Unless keep_tracker argument is set to true, this will change the tracker
358 # Unless keep_tracker argument is set to true, this will change the tracker
353 # to the first tracker of the new project if the previous tracker is not part
359 # to the first tracker of the new project if the previous tracker is not part
354 # of the new project trackers.
360 # of the new project trackers.
355 # This will:
361 # This will:
356 # * clear the fixed_version is it's no longer valid for the new project.
362 # * clear the fixed_version is it's no longer valid for the new project.
357 # * clear the parent issue if it's no longer valid for the new project.
363 # * clear the parent issue if it's no longer valid for the new project.
358 # * set the category to the category with the same name in the new
364 # * set the category to the category with the same name in the new
359 # project if it exists, or clear it if it doesn't.
365 # project if it exists, or clear it if it doesn't.
360 # * for new issue, set the fixed_version to the project default version
366 # * for new issue, set the fixed_version to the project default version
361 # if it's a valid fixed_version.
367 # if it's a valid fixed_version.
362 def project=(project, keep_tracker=false)
368 def project=(project, keep_tracker=false)
363 project_was = self.project
369 project_was = self.project
364 association(:project).writer(project)
370 association(:project).writer(project)
365 if project_was && project && project_was != project
371 if project_was && project && project_was != project
366 @assignable_versions = nil
372 @assignable_versions = nil
367
373
368 unless keep_tracker || project.trackers.include?(tracker)
374 unless keep_tracker || project.trackers.include?(tracker)
369 self.tracker = project.trackers.first
375 self.tracker = project.trackers.first
370 end
376 end
371 # Reassign to the category with same name if any
377 # Reassign to the category with same name if any
372 if category
378 if category
373 self.category = project.issue_categories.find_by_name(category.name)
379 self.category = project.issue_categories.find_by_name(category.name)
374 end
380 end
375 # Clear the assignee if not available in the new project for new issues (eg. copy)
381 # Clear the assignee if not available in the new project for new issues (eg. copy)
376 # For existing issue, the previous assignee is still valid, so we keep it
382 # For existing issue, the previous assignee is still valid, so we keep it
377 if new_record? && assigned_to && !assignable_users.include?(assigned_to)
383 if new_record? && assigned_to && !assignable_users.include?(assigned_to)
378 self.assigned_to_id = nil
384 self.assigned_to_id = nil
379 end
385 end
380 # Keep the fixed_version if it's still valid in the new_project
386 # Keep the fixed_version if it's still valid in the new_project
381 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
387 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
382 self.fixed_version = nil
388 self.fixed_version = nil
383 end
389 end
384 # Clear the parent task if it's no longer valid
390 # Clear the parent task if it's no longer valid
385 unless valid_parent_project?
391 unless valid_parent_project?
386 self.parent_issue_id = nil
392 self.parent_issue_id = nil
387 end
393 end
388 reassign_custom_field_values
394 reassign_custom_field_values
389 @workflow_rule_by_attribute = nil
395 @workflow_rule_by_attribute = nil
390 end
396 end
391 # Set fixed_version to the project default version if it's valid
397 # Set fixed_version to the project default version if it's valid
392 if new_record? && fixed_version.nil? && project && project.default_version_id?
398 if new_record? && fixed_version.nil? && project && project.default_version_id?
393 if project.shared_versions.open.exists?(project.default_version_id)
399 if project.shared_versions.open.exists?(project.default_version_id)
394 self.fixed_version_id = project.default_version_id
400 self.fixed_version_id = project.default_version_id
395 end
401 end
396 end
402 end
397 self.project
403 self.project
398 end
404 end
399
405
400 def description=(arg)
406 def description=(arg)
401 if arg.is_a?(String)
407 if arg.is_a?(String)
402 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
408 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
403 end
409 end
404 write_attribute(:description, arg)
410 write_attribute(:description, arg)
405 end
411 end
406
412
407 def deleted_attachment_ids
413 def deleted_attachment_ids
408 Array(@deleted_attachment_ids).map(&:to_i)
414 Array(@deleted_attachment_ids).map(&:to_i)
409 end
415 end
410
416
411 # Overrides assign_attributes so that project and tracker get assigned first
417 # Overrides assign_attributes so that project and tracker get assigned first
412 def assign_attributes(new_attributes, *args)
418 def assign_attributes(new_attributes, *args)
413 return if new_attributes.nil?
419 return if new_attributes.nil?
414 attrs = new_attributes.dup
420 attrs = new_attributes.dup
415 attrs.stringify_keys!
421 attrs.stringify_keys!
416
422
417 %w(project project_id tracker tracker_id).each do |attr|
423 %w(project project_id tracker tracker_id).each do |attr|
418 if attrs.has_key?(attr)
424 if attrs.has_key?(attr)
419 send "#{attr}=", attrs.delete(attr)
425 send "#{attr}=", attrs.delete(attr)
420 end
426 end
421 end
427 end
422 super attrs, *args
428 super attrs, *args
423 end
429 end
424
430
425 def attributes=(new_attributes)
431 def attributes=(new_attributes)
426 assign_attributes new_attributes
432 assign_attributes new_attributes
427 end
433 end
428
434
429 def estimated_hours=(h)
435 def estimated_hours=(h)
430 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
436 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
431 end
437 end
432
438
433 safe_attributes 'project_id',
439 safe_attributes 'project_id',
434 'tracker_id',
440 'tracker_id',
435 'status_id',
441 'status_id',
436 'category_id',
442 'category_id',
437 'assigned_to_id',
443 'assigned_to_id',
438 'priority_id',
444 'priority_id',
439 'fixed_version_id',
445 'fixed_version_id',
440 'subject',
446 'subject',
441 'description',
447 'description',
442 'start_date',
448 'start_date',
443 'due_date',
449 'due_date',
444 'done_ratio',
450 'done_ratio',
445 'estimated_hours',
451 'estimated_hours',
446 'custom_field_values',
452 'custom_field_values',
447 'custom_fields',
453 'custom_fields',
448 'lock_version',
454 'lock_version',
449 'notes',
455 'notes',
450 :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
456 :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
451
457
452 safe_attributes 'notes',
458 safe_attributes 'notes',
453 :if => lambda {|issue, user| issue.notes_addable?(user)}
459 :if => lambda {|issue, user| issue.notes_addable?(user)}
454
460
455 safe_attributes 'private_notes',
461 safe_attributes 'private_notes',
456 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
462 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
457
463
458 safe_attributes 'watcher_user_ids',
464 safe_attributes 'watcher_user_ids',
459 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
465 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
460
466
461 safe_attributes 'is_private',
467 safe_attributes 'is_private',
462 :if => lambda {|issue, user|
468 :if => lambda {|issue, user|
463 user.allowed_to?(:set_issues_private, issue.project) ||
469 user.allowed_to?(:set_issues_private, issue.project) ||
464 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
470 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
465 }
471 }
466
472
467 safe_attributes 'parent_issue_id',
473 safe_attributes 'parent_issue_id',
468 :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
474 :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
469 user.allowed_to?(:manage_subtasks, issue.project)}
475 user.allowed_to?(:manage_subtasks, issue.project)}
470
476
471 safe_attributes 'deleted_attachment_ids',
477 safe_attributes 'deleted_attachment_ids',
472 :if => lambda {|issue, user| issue.attachments_deletable?(user)}
478 :if => lambda {|issue, user| issue.attachments_deletable?(user)}
473
479
474 def safe_attribute_names(user=nil)
480 def safe_attribute_names(user=nil)
475 names = super
481 names = super
476 names -= disabled_core_fields
482 names -= disabled_core_fields
477 names -= read_only_attribute_names(user)
483 names -= read_only_attribute_names(user)
478 if new_record?
484 if new_record?
479 # Make sure that project_id can always be set for new issues
485 # Make sure that project_id can always be set for new issues
480 names |= %w(project_id)
486 names |= %w(project_id)
481 end
487 end
482 if dates_derived?
488 if dates_derived?
483 names -= %w(start_date due_date)
489 names -= %w(start_date due_date)
484 end
490 end
485 if priority_derived?
491 if priority_derived?
486 names -= %w(priority_id)
492 names -= %w(priority_id)
487 end
493 end
488 if done_ratio_derived?
494 if done_ratio_derived?
489 names -= %w(done_ratio)
495 names -= %w(done_ratio)
490 end
496 end
491 names
497 names
492 end
498 end
493
499
494 # Safely sets attributes
500 # Safely sets attributes
495 # Should be called from controllers instead of #attributes=
501 # Should be called from controllers instead of #attributes=
496 # attr_accessible is too rough because we still want things like
502 # attr_accessible is too rough because we still want things like
497 # Issue.new(:project => foo) to work
503 # Issue.new(:project => foo) to work
498 def safe_attributes=(attrs, user=User.current)
504 def safe_attributes=(attrs, user=User.current)
499 return unless attrs.is_a?(Hash)
505 return unless attrs.is_a?(Hash)
500
506
501 attrs = attrs.deep_dup
507 attrs = attrs.deep_dup
502
508
503 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
509 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
504 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
510 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
505 if p.is_a?(String) && !p.match(/^\d*$/)
511 if p.is_a?(String) && !p.match(/^\d*$/)
506 p_id = Project.find_by_identifier(p).try(:id)
512 p_id = Project.find_by_identifier(p).try(:id)
507 else
513 else
508 p_id = p.to_i
514 p_id = p.to_i
509 end
515 end
510 if allowed_target_projects(user).where(:id => p_id).exists?
516 if allowed_target_projects(user).where(:id => p_id).exists?
511 self.project_id = p_id
517 self.project_id = p_id
512 end
518 end
513
519
514 if project_id_changed? && attrs['category_id'].to_s == category_id_was.to_s
520 if project_id_changed? && attrs['category_id'].to_s == category_id_was.to_s
515 # Discard submitted category on previous project
521 # Discard submitted category on previous project
516 attrs.delete('category_id')
522 attrs.delete('category_id')
517 end
523 end
518 end
524 end
519
525
520 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
526 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
521 if allowed_target_trackers(user).where(:id => t.to_i).exists?
527 if allowed_target_trackers(user).where(:id => t.to_i).exists?
522 self.tracker_id = t
528 self.tracker_id = t
523 end
529 end
524 end
530 end
525 if project
531 if project
526 # Set a default tracker to accept custom field values
532 # Set a default tracker to accept custom field values
527 # even if tracker is not specified
533 # even if tracker is not specified
528 self.tracker ||= allowed_target_trackers(user).first
534 self.tracker ||= allowed_target_trackers(user).first
529 end
535 end
530
536
531 statuses_allowed = new_statuses_allowed_to(user)
537 statuses_allowed = new_statuses_allowed_to(user)
532 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
538 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
533 if statuses_allowed.collect(&:id).include?(s.to_i)
539 if statuses_allowed.collect(&:id).include?(s.to_i)
534 self.status_id = s
540 self.status_id = s
535 end
541 end
536 end
542 end
537 if new_record? && !statuses_allowed.include?(status)
543 if new_record? && !statuses_allowed.include?(status)
538 self.status = statuses_allowed.first || default_status
544 self.status = statuses_allowed.first || default_status
539 end
545 end
540 if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
546 if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
541 self.assigned_to_id = u
547 self.assigned_to_id = u
542 end
548 end
543
549
544
550
545 attrs = delete_unsafe_attributes(attrs, user)
551 attrs = delete_unsafe_attributes(attrs, user)
546 return if attrs.empty?
552 return if attrs.empty?
547
553
548 if attrs['parent_issue_id'].present?
554 if attrs['parent_issue_id'].present?
549 s = attrs['parent_issue_id'].to_s
555 s = attrs['parent_issue_id'].to_s
550 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
556 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
551 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
557 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
552 end
558 end
553 end
559 end
554
560
555 if attrs['custom_field_values'].present?
561 if attrs['custom_field_values'].present?
556 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
562 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
557 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
563 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
558 end
564 end
559
565
560 if attrs['custom_fields'].present?
566 if attrs['custom_fields'].present?
561 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
567 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
562 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
568 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
563 end
569 end
564
570
565 # mass-assignment security bypass
571 # mass-assignment security bypass
566 assign_attributes attrs, :without_protection => true
572 assign_attributes attrs, :without_protection => true
567 end
573 end
568
574
569 def disabled_core_fields
575 def disabled_core_fields
570 tracker ? tracker.disabled_core_fields : []
576 tracker ? tracker.disabled_core_fields : []
571 end
577 end
572
578
573 # Returns the custom_field_values that can be edited by the given user
579 # Returns the custom_field_values that can be edited by the given user
574 def editable_custom_field_values(user=nil)
580 def editable_custom_field_values(user=nil)
575 read_only = read_only_attribute_names(user)
581 read_only = read_only_attribute_names(user)
576 visible_custom_field_values(user).reject do |value|
582 visible_custom_field_values(user).reject do |value|
577 read_only.include?(value.custom_field_id.to_s)
583 read_only.include?(value.custom_field_id.to_s)
578 end
584 end
579 end
585 end
580
586
581 # Returns the custom fields that can be edited by the given user
587 # Returns the custom fields that can be edited by the given user
582 def editable_custom_fields(user=nil)
588 def editable_custom_fields(user=nil)
583 editable_custom_field_values(user).map(&:custom_field).uniq
589 editable_custom_field_values(user).map(&:custom_field).uniq
584 end
590 end
585
591
586 # Returns the names of attributes that are read-only for user or the current user
592 # Returns the names of attributes that are read-only for user or the current user
587 # For users with multiple roles, the read-only fields are the intersection of
593 # For users with multiple roles, the read-only fields are the intersection of
588 # read-only fields of each role
594 # read-only fields of each role
589 # The result is an array of strings where sustom fields are represented with their ids
595 # The result is an array of strings where sustom fields are represented with their ids
590 #
596 #
591 # Examples:
597 # Examples:
592 # issue.read_only_attribute_names # => ['due_date', '2']
598 # issue.read_only_attribute_names # => ['due_date', '2']
593 # issue.read_only_attribute_names(user) # => []
599 # issue.read_only_attribute_names(user) # => []
594 def read_only_attribute_names(user=nil)
600 def read_only_attribute_names(user=nil)
595 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
601 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
596 end
602 end
597
603
598 # Returns the names of required attributes for user or the current user
604 # Returns the names of required attributes for user or the current user
599 # For users with multiple roles, the required fields are the intersection of
605 # For users with multiple roles, the required fields are the intersection of
600 # required fields of each role
606 # required fields of each role
601 # The result is an array of strings where sustom fields are represented with their ids
607 # The result is an array of strings where sustom fields are represented with their ids
602 #
608 #
603 # Examples:
609 # Examples:
604 # issue.required_attribute_names # => ['due_date', '2']
610 # issue.required_attribute_names # => ['due_date', '2']
605 # issue.required_attribute_names(user) # => []
611 # issue.required_attribute_names(user) # => []
606 def required_attribute_names(user=nil)
612 def required_attribute_names(user=nil)
607 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
613 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
608 end
614 end
609
615
610 # Returns true if the attribute is required for user
616 # Returns true if the attribute is required for user
611 def required_attribute?(name, user=nil)
617 def required_attribute?(name, user=nil)
612 required_attribute_names(user).include?(name.to_s)
618 required_attribute_names(user).include?(name.to_s)
613 end
619 end
614
620
615 # Returns a hash of the workflow rule by attribute for the given user
621 # Returns a hash of the workflow rule by attribute for the given user
616 #
622 #
617 # Examples:
623 # Examples:
618 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
624 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
619 def workflow_rule_by_attribute(user=nil)
625 def workflow_rule_by_attribute(user=nil)
620 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
626 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
621
627
622 user_real = user || User.current
628 user_real = user || User.current
623 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
629 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
624 roles = roles.select(&:consider_workflow?)
630 roles = roles.select(&:consider_workflow?)
625 return {} if roles.empty?
631 return {} if roles.empty?
626
632
627 result = {}
633 result = {}
628 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
634 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
629 if workflow_permissions.any?
635 if workflow_permissions.any?
630 workflow_rules = workflow_permissions.inject({}) do |h, wp|
636 workflow_rules = workflow_permissions.inject({}) do |h, wp|
631 h[wp.field_name] ||= {}
637 h[wp.field_name] ||= {}
632 h[wp.field_name][wp.role_id] = wp.rule
638 h[wp.field_name][wp.role_id] = wp.rule
633 h
639 h
634 end
640 end
635 fields_with_roles = {}
641 fields_with_roles = {}
636 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
642 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
637 fields_with_roles[field_id] ||= []
643 fields_with_roles[field_id] ||= []
638 fields_with_roles[field_id] << role_id
644 fields_with_roles[field_id] << role_id
639 end
645 end
640 roles.each do |role|
646 roles.each do |role|
641 fields_with_roles.each do |field_id, role_ids|
647 fields_with_roles.each do |field_id, role_ids|
642 unless role_ids.include?(role.id)
648 unless role_ids.include?(role.id)
643 field_name = field_id.to_s
649 field_name = field_id.to_s
644 workflow_rules[field_name] ||= {}
650 workflow_rules[field_name] ||= {}
645 workflow_rules[field_name][role.id] = 'readonly'
651 workflow_rules[field_name][role.id] = 'readonly'
646 end
652 end
647 end
653 end
648 end
654 end
649 workflow_rules.each do |attr, rules|
655 workflow_rules.each do |attr, rules|
650 next if rules.size < roles.size
656 next if rules.size < roles.size
651 uniq_rules = rules.values.uniq
657 uniq_rules = rules.values.uniq
652 if uniq_rules.size == 1
658 if uniq_rules.size == 1
653 result[attr] = uniq_rules.first
659 result[attr] = uniq_rules.first
654 else
660 else
655 result[attr] = 'required'
661 result[attr] = 'required'
656 end
662 end
657 end
663 end
658 end
664 end
659 @workflow_rule_by_attribute = result if user.nil?
665 @workflow_rule_by_attribute = result if user.nil?
660 result
666 result
661 end
667 end
662 private :workflow_rule_by_attribute
668 private :workflow_rule_by_attribute
663
669
664 def done_ratio
670 def done_ratio
665 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
671 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
666 status.default_done_ratio
672 status.default_done_ratio
667 else
673 else
668 read_attribute(:done_ratio)
674 read_attribute(:done_ratio)
669 end
675 end
670 end
676 end
671
677
672 def self.use_status_for_done_ratio?
678 def self.use_status_for_done_ratio?
673 Setting.issue_done_ratio == 'issue_status'
679 Setting.issue_done_ratio == 'issue_status'
674 end
680 end
675
681
676 def self.use_field_for_done_ratio?
682 def self.use_field_for_done_ratio?
677 Setting.issue_done_ratio == 'issue_field'
683 Setting.issue_done_ratio == 'issue_field'
678 end
684 end
679
685
680 def validate_issue
686 def validate_issue
681 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
687 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
682 errors.add :due_date, :greater_than_start_date
688 errors.add :due_date, :greater_than_start_date
683 end
689 end
684
690
685 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
691 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
686 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
692 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
687 end
693 end
688
694
689 if fixed_version
695 if fixed_version
690 if !assignable_versions.include?(fixed_version)
696 if !assignable_versions.include?(fixed_version)
691 errors.add :fixed_version_id, :inclusion
697 errors.add :fixed_version_id, :inclusion
692 elsif reopening? && fixed_version.closed?
698 elsif reopening? && fixed_version.closed?
693 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
699 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
694 end
700 end
695 end
701 end
696
702
697 # Checks that the issue can not be added/moved to a disabled tracker
703 # Checks that the issue can not be added/moved to a disabled tracker
698 if project && (tracker_id_changed? || project_id_changed?)
704 if project && (tracker_id_changed? || project_id_changed?)
699 if tracker && !project.trackers.include?(tracker)
705 if tracker && !project.trackers.include?(tracker)
700 errors.add :tracker_id, :inclusion
706 errors.add :tracker_id, :inclusion
701 end
707 end
702 end
708 end
703
709
704 if assigned_to_id_changed? && assigned_to_id.present?
710 if assigned_to_id_changed? && assigned_to_id.present?
705 unless assignable_users.include?(assigned_to)
711 unless assignable_users.include?(assigned_to)
706 errors.add :assigned_to_id, :invalid
712 errors.add :assigned_to_id, :invalid
707 end
713 end
708 end
714 end
709
715
710 # Checks parent issue assignment
716 # Checks parent issue assignment
711 if @invalid_parent_issue_id.present?
717 if @invalid_parent_issue_id.present?
712 errors.add :parent_issue_id, :invalid
718 errors.add :parent_issue_id, :invalid
713 elsif @parent_issue
719 elsif @parent_issue
714 if !valid_parent_project?(@parent_issue)
720 if !valid_parent_project?(@parent_issue)
715 errors.add :parent_issue_id, :invalid
721 errors.add :parent_issue_id, :invalid
716 elsif (@parent_issue != parent) && (
722 elsif (@parent_issue != parent) && (
717 self.would_reschedule?(@parent_issue) ||
723 self.would_reschedule?(@parent_issue) ||
718 @parent_issue.self_and_ancestors.any? {|a| a.relations_from.any? {|r| r.relation_type == IssueRelation::TYPE_PRECEDES && r.issue_to.would_reschedule?(self)}}
724 @parent_issue.self_and_ancestors.any? {|a| a.relations_from.any? {|r| r.relation_type == IssueRelation::TYPE_PRECEDES && r.issue_to.would_reschedule?(self)}}
719 )
725 )
720 errors.add :parent_issue_id, :invalid
726 errors.add :parent_issue_id, :invalid
721 elsif !closed? && @parent_issue.closed?
727 elsif !closed? && @parent_issue.closed?
722 # cannot attach an open issue to a closed parent
728 # cannot attach an open issue to a closed parent
723 errors.add :base, :open_issue_with_closed_parent
729 errors.add :base, :open_issue_with_closed_parent
724 elsif !new_record?
730 elsif !new_record?
725 # moving an existing issue
731 # moving an existing issue
726 if move_possible?(@parent_issue)
732 if move_possible?(@parent_issue)
727 # move accepted
733 # move accepted
728 else
734 else
729 errors.add :parent_issue_id, :invalid
735 errors.add :parent_issue_id, :invalid
730 end
736 end
731 end
737 end
732 end
738 end
733 end
739 end
734
740
735 # Validates the issue against additional workflow requirements
741 # Validates the issue against additional workflow requirements
736 def validate_required_fields
742 def validate_required_fields
737 user = new_record? ? author : current_journal.try(:user)
743 user = new_record? ? author : current_journal.try(:user)
738
744
739 required_attribute_names(user).each do |attribute|
745 required_attribute_names(user).each do |attribute|
740 if attribute =~ /^\d+$/
746 if attribute =~ /^\d+$/
741 attribute = attribute.to_i
747 attribute = attribute.to_i
742 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
748 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
743 if v && Array(v.value).detect(&:present?).nil?
749 if v && Array(v.value).detect(&:present?).nil?
744 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
750 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
745 end
751 end
746 else
752 else
747 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
753 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
748 next if attribute == 'category_id' && project.try(:issue_categories).blank?
754 next if attribute == 'category_id' && project.try(:issue_categories).blank?
749 next if attribute == 'fixed_version_id' && assignable_versions.blank?
755 next if attribute == 'fixed_version_id' && assignable_versions.blank?
750 errors.add attribute, :blank
756 errors.add attribute, :blank
751 end
757 end
752 end
758 end
753 end
759 end
754 end
760 end
755
761
756 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
762 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
757 # so that custom values that are not editable are not validated (eg. a custom field that
763 # so that custom values that are not editable are not validated (eg. a custom field that
758 # is marked as required should not trigger a validation error if the user is not allowed
764 # is marked as required should not trigger a validation error if the user is not allowed
759 # to edit this field).
765 # to edit this field).
760 def validate_custom_field_values
766 def validate_custom_field_values
761 user = new_record? ? author : current_journal.try(:user)
767 user = new_record? ? author : current_journal.try(:user)
762 if new_record? || custom_field_values_changed?
768 if new_record? || custom_field_values_changed?
763 editable_custom_field_values(user).each(&:validate_value)
769 editable_custom_field_values(user).each(&:validate_value)
764 end
770 end
765 end
771 end
766
772
767 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
773 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
768 # even if the user turns off the setting later
774 # even if the user turns off the setting later
769 def update_done_ratio_from_issue_status
775 def update_done_ratio_from_issue_status
770 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
776 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
771 self.done_ratio = status.default_done_ratio
777 self.done_ratio = status.default_done_ratio
772 end
778 end
773 end
779 end
774
780
775 def init_journal(user, notes = "")
781 def init_journal(user, notes = "")
776 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
782 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
777 end
783 end
778
784
779 # Returns the current journal or nil if it's not initialized
785 # Returns the current journal or nil if it's not initialized
780 def current_journal
786 def current_journal
781 @current_journal
787 @current_journal
782 end
788 end
783
789
784 # Returns the names of attributes that are journalized when updating the issue
790 # Returns the names of attributes that are journalized when updating the issue
785 def journalized_attribute_names
791 def journalized_attribute_names
786 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
792 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
787 if tracker
793 if tracker
788 names -= tracker.disabled_core_fields
794 names -= tracker.disabled_core_fields
789 end
795 end
790 names
796 names
791 end
797 end
792
798
793 # Returns the id of the last journal or nil
799 # Returns the id of the last journal or nil
794 def last_journal_id
800 def last_journal_id
795 if new_record?
801 if new_record?
796 nil
802 nil
797 else
803 else
798 journals.maximum(:id)
804 journals.maximum(:id)
799 end
805 end
800 end
806 end
801
807
802 # Returns a scope for journals that have an id greater than journal_id
808 # Returns a scope for journals that have an id greater than journal_id
803 def journals_after(journal_id)
809 def journals_after(journal_id)
804 scope = journals.reorder("#{Journal.table_name}.id ASC")
810 scope = journals.reorder("#{Journal.table_name}.id ASC")
805 if journal_id.present?
811 if journal_id.present?
806 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
812 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
807 end
813 end
808 scope
814 scope
809 end
815 end
810
816
811 # Returns the journals that are visible to user with their index
817 # Returns the journals that are visible to user with their index
812 # Used to display the issue history
818 # Used to display the issue history
813 def visible_journals_with_index(user=User.current)
819 def visible_journals_with_index(user=User.current)
814 result = journals.
820 result = journals.
815 preload(:details).
821 preload(:details).
816 preload(:user => :email_address).
822 preload(:user => :email_address).
817 reorder(:created_on, :id).to_a
823 reorder(:created_on, :id).to_a
818
824
819 result.each_with_index {|j,i| j.indice = i+1}
825 result.each_with_index {|j,i| j.indice = i+1}
820
826
821 unless user.allowed_to?(:view_private_notes, project)
827 unless user.allowed_to?(:view_private_notes, project)
822 result.select! do |journal|
828 result.select! do |journal|
823 !journal.private_notes? || journal.user == user
829 !journal.private_notes? || journal.user == user
824 end
830 end
825 end
831 end
826 Journal.preload_journals_details_custom_fields(result)
832 Journal.preload_journals_details_custom_fields(result)
827 result.select! {|journal| journal.notes? || journal.visible_details.any?}
833 result.select! {|journal| journal.notes? || journal.visible_details.any?}
828 result
834 result
829 end
835 end
830
836
831 # Returns the initial status of the issue
837 # Returns the initial status of the issue
832 # Returns nil for a new issue
838 # Returns nil for a new issue
833 def status_was
839 def status_was
834 if status_id_changed?
840 if status_id_changed?
835 if status_id_was.to_i > 0
841 if status_id_was.to_i > 0
836 @status_was ||= IssueStatus.find_by_id(status_id_was)
842 @status_was ||= IssueStatus.find_by_id(status_id_was)
837 end
843 end
838 else
844 else
839 @status_was ||= status
845 @status_was ||= status
840 end
846 end
841 end
847 end
842
848
843 # Return true if the issue is closed, otherwise false
849 # Return true if the issue is closed, otherwise false
844 def closed?
850 def closed?
845 status.present? && status.is_closed?
851 status.present? && status.is_closed?
846 end
852 end
847
853
848 # Returns true if the issue was closed when loaded
854 # Returns true if the issue was closed when loaded
849 def was_closed?
855 def was_closed?
850 status_was.present? && status_was.is_closed?
856 status_was.present? && status_was.is_closed?
851 end
857 end
852
858
853 # Return true if the issue is being reopened
859 # Return true if the issue is being reopened
854 def reopening?
860 def reopening?
855 if new_record?
861 if new_record?
856 false
862 false
857 else
863 else
858 status_id_changed? && !closed? && was_closed?
864 status_id_changed? && !closed? && was_closed?
859 end
865 end
860 end
866 end
861 alias :reopened? :reopening?
867 alias :reopened? :reopening?
862
868
863 # Return true if the issue is being closed
869 # Return true if the issue is being closed
864 def closing?
870 def closing?
865 if new_record?
871 if new_record?
866 closed?
872 closed?
867 else
873 else
868 status_id_changed? && closed? && !was_closed?
874 status_id_changed? && closed? && !was_closed?
869 end
875 end
870 end
876 end
871
877
872 # Returns true if the issue is overdue
878 # Returns true if the issue is overdue
873 def overdue?
879 def overdue?
874 due_date.present? && (due_date < User.current.today) && !closed?
880 due_date.present? && (due_date < User.current.today) && !closed?
875 end
881 end
876
882
877 # Is the amount of work done less than it should for the due date
883 # Is the amount of work done less than it should for the due date
878 def behind_schedule?
884 def behind_schedule?
879 return false if start_date.nil? || due_date.nil?
885 return false if start_date.nil? || due_date.nil?
880 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
886 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
881 return done_date <= User.current.today
887 return done_date <= User.current.today
882 end
888 end
883
889
884 # Does this issue have children?
890 # Does this issue have children?
885 def children?
891 def children?
886 !leaf?
892 !leaf?
887 end
893 end
888
894
889 # Users the issue can be assigned to
895 # Users the issue can be assigned to
890 def assignable_users
896 def assignable_users
891 users = project.assignable_users(tracker).to_a
897 users = project.assignable_users(tracker).to_a
892 users << author if author && author.active?
898 users << author if author && author.active?
893 if assigned_to_id_was.present? && assignee = Principal.find_by_id(assigned_to_id_was)
899 if assigned_to_id_was.present? && assignee = Principal.find_by_id(assigned_to_id_was)
894 users << assignee
900 users << assignee
895 end
901 end
896 users.uniq.sort
902 users.uniq.sort
897 end
903 end
898
904
899 # Versions that the issue can be assigned to
905 # Versions that the issue can be assigned to
900 def assignable_versions
906 def assignable_versions
901 return @assignable_versions if @assignable_versions
907 return @assignable_versions if @assignable_versions
902
908
903 versions = project.shared_versions.open.to_a
909 versions = project.shared_versions.open.to_a
904 if fixed_version
910 if fixed_version
905 if fixed_version_id_changed?
911 if fixed_version_id_changed?
906 # nothing to do
912 # nothing to do
907 elsif project_id_changed?
913 elsif project_id_changed?
908 if project.shared_versions.include?(fixed_version)
914 if project.shared_versions.include?(fixed_version)
909 versions << fixed_version
915 versions << fixed_version
910 end
916 end
911 else
917 else
912 versions << fixed_version
918 versions << fixed_version
913 end
919 end
914 end
920 end
915 @assignable_versions = versions.uniq.sort
921 @assignable_versions = versions.uniq.sort
916 end
922 end
917
923
918 # Returns true if this issue is blocked by another issue that is still open
924 # Returns true if this issue is blocked by another issue that is still open
919 def blocked?
925 def blocked?
920 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
926 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
921 end
927 end
922
928
923 # Returns the default status of the issue based on its tracker
929 # Returns the default status of the issue based on its tracker
924 # Returns nil if tracker is nil
930 # Returns nil if tracker is nil
925 def default_status
931 def default_status
926 tracker.try(:default_status)
932 tracker.try(:default_status)
927 end
933 end
928
934
929 # Returns an array of statuses that user is able to apply
935 # Returns an array of statuses that user is able to apply
930 def new_statuses_allowed_to(user=User.current, include_default=false)
936 def new_statuses_allowed_to(user=User.current, include_default=false)
931 initial_status = nil
937 initial_status = nil
932 if new_record?
938 if new_record?
933 # nop
939 # nop
934 elsif tracker_id_changed?
940 elsif tracker_id_changed?
935 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
941 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
936 initial_status = default_status
942 initial_status = default_status
937 elsif tracker.issue_status_ids.include?(status_id_was)
943 elsif tracker.issue_status_ids.include?(status_id_was)
938 initial_status = IssueStatus.find_by_id(status_id_was)
944 initial_status = IssueStatus.find_by_id(status_id_was)
939 else
945 else
940 initial_status = default_status
946 initial_status = default_status
941 end
947 end
942 else
948 else
943 initial_status = status_was
949 initial_status = status_was
944 end
950 end
945
951
946 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
952 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
947 assignee_transitions_allowed = initial_assigned_to_id.present? &&
953 assignee_transitions_allowed = initial_assigned_to_id.present? &&
948 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
954 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
949
955
950 statuses = []
956 statuses = []
951 statuses += IssueStatus.new_statuses_allowed(
957 statuses += IssueStatus.new_statuses_allowed(
952 initial_status,
958 initial_status,
953 user.admin ? Role.all.to_a : user.roles_for_project(project),
959 user.admin ? Role.all.to_a : user.roles_for_project(project),
954 tracker,
960 tracker,
955 author == user,
961 author == user,
956 assignee_transitions_allowed
962 assignee_transitions_allowed
957 )
963 )
958 statuses << initial_status unless statuses.empty?
964 statuses << initial_status unless statuses.empty?
959 statuses << default_status if include_default || (new_record? && statuses.empty?)
965 statuses << default_status if include_default || (new_record? && statuses.empty?)
960
966
961 if new_record? && @copied_from
967 if new_record? && @copied_from
962 statuses << @copied_from.status
968 statuses << @copied_from.status
963 end
969 end
964
970
965 statuses = statuses.compact.uniq.sort
971 statuses = statuses.compact.uniq.sort
966 if blocked? || descendants.open.any?
972 if blocked? || descendants.open.any?
967 # cannot close a blocked issue or a parent with open subtasks
973 # cannot close a blocked issue or a parent with open subtasks
968 statuses.reject!(&:is_closed?)
974 statuses.reject!(&:is_closed?)
969 end
975 end
970 if ancestors.open(false).any?
976 if ancestors.open(false).any?
971 # cannot reopen a subtask of a closed parent
977 # cannot reopen a subtask of a closed parent
972 statuses.select!(&:is_closed?)
978 statuses.select!(&:is_closed?)
973 end
979 end
974 statuses
980 statuses
975 end
981 end
976
982
977 # Returns the previous assignee (user or group) if changed
983 # Returns the previous assignee (user or group) if changed
978 def assigned_to_was
984 def assigned_to_was
979 # assigned_to_id_was is reset before after_save callbacks
985 # assigned_to_id_was is reset before after_save callbacks
980 user_id = @previous_assigned_to_id || assigned_to_id_was
986 user_id = @previous_assigned_to_id || assigned_to_id_was
981 if user_id && user_id != assigned_to_id
987 if user_id && user_id != assigned_to_id
982 @assigned_to_was ||= Principal.find_by_id(user_id)
988 @assigned_to_was ||= Principal.find_by_id(user_id)
983 end
989 end
984 end
990 end
985
991
986 # Returns the original tracker
992 # Returns the original tracker
987 def tracker_was
993 def tracker_was
988 Tracker.find_by_id(tracker_id_was)
994 Tracker.find_by_id(tracker_id_was)
989 end
995 end
990
996
991 # Returns the users that should be notified
997 # Returns the users that should be notified
992 def notified_users
998 def notified_users
993 notified = []
999 notified = []
994 # Author and assignee are always notified unless they have been
1000 # Author and assignee are always notified unless they have been
995 # locked or don't want to be notified
1001 # locked or don't want to be notified
996 notified << author if author
1002 notified << author if author
997 if assigned_to
1003 if assigned_to
998 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
1004 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
999 end
1005 end
1000 if assigned_to_was
1006 if assigned_to_was
1001 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
1007 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
1002 end
1008 end
1003 notified = notified.select {|u| u.active? && u.notify_about?(self)}
1009 notified = notified.select {|u| u.active? && u.notify_about?(self)}
1004
1010
1005 notified += project.notified_users
1011 notified += project.notified_users
1006 notified.uniq!
1012 notified.uniq!
1007 # Remove users that can not view the issue
1013 # Remove users that can not view the issue
1008 notified.reject! {|user| !visible?(user)}
1014 notified.reject! {|user| !visible?(user)}
1009 notified
1015 notified
1010 end
1016 end
1011
1017
1012 # Returns the email addresses that should be notified
1018 # Returns the email addresses that should be notified
1013 def recipients
1019 def recipients
1014 notified_users.collect(&:mail)
1020 notified_users.collect(&:mail)
1015 end
1021 end
1016
1022
1017 def each_notification(users, &block)
1023 def each_notification(users, &block)
1018 if users.any?
1024 if users.any?
1019 if custom_field_values.detect {|value| !value.custom_field.visible?}
1025 if custom_field_values.detect {|value| !value.custom_field.visible?}
1020 users_by_custom_field_visibility = users.group_by do |user|
1026 users_by_custom_field_visibility = users.group_by do |user|
1021 visible_custom_field_values(user).map(&:custom_field_id).sort
1027 visible_custom_field_values(user).map(&:custom_field_id).sort
1022 end
1028 end
1023 users_by_custom_field_visibility.values.each do |users|
1029 users_by_custom_field_visibility.values.each do |users|
1024 yield(users)
1030 yield(users)
1025 end
1031 end
1026 else
1032 else
1027 yield(users)
1033 yield(users)
1028 end
1034 end
1029 end
1035 end
1030 end
1036 end
1031
1037
1032 def notify?
1038 def notify?
1033 @notify != false
1039 @notify != false
1034 end
1040 end
1035
1041
1036 def notify=(arg)
1042 def notify=(arg)
1037 @notify = arg
1043 @notify = arg
1038 end
1044 end
1039
1045
1040 # Returns the number of hours spent on this issue
1046 # Returns the number of hours spent on this issue
1041 def spent_hours
1047 def spent_hours
1042 @spent_hours ||= time_entries.sum(:hours) || 0
1048 @spent_hours ||= time_entries.sum(:hours) || 0
1043 end
1049 end
1044
1050
1045 # Returns the total number of hours spent on this issue and its descendants
1051 # Returns the total number of hours spent on this issue and its descendants
1046 def total_spent_hours
1052 def total_spent_hours
1047 @total_spent_hours ||= if leaf?
1053 @total_spent_hours ||= if leaf?
1048 spent_hours
1054 spent_hours
1049 else
1055 else
1050 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1056 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1051 end
1057 end
1052 end
1058 end
1053
1059
1054 def total_estimated_hours
1060 def total_estimated_hours
1055 if leaf?
1061 if leaf?
1056 estimated_hours
1062 estimated_hours
1057 else
1063 else
1058 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
1064 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
1059 end
1065 end
1060 end
1066 end
1061
1067
1062 def relations
1068 def relations
1063 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1069 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1064 end
1070 end
1065
1071
1066 # Preloads relations for a collection of issues
1072 # Preloads relations for a collection of issues
1067 def self.load_relations(issues)
1073 def self.load_relations(issues)
1068 if issues.any?
1074 if issues.any?
1069 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
1075 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
1070 issues.each do |issue|
1076 issues.each do |issue|
1071 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1077 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1072 end
1078 end
1073 end
1079 end
1074 end
1080 end
1075
1081
1076 # Preloads visible spent time for a collection of issues
1082 # Preloads visible spent time for a collection of issues
1077 def self.load_visible_spent_hours(issues, user=User.current)
1083 def self.load_visible_spent_hours(issues, user=User.current)
1078 if issues.any?
1084 if issues.any?
1079 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1085 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1080 issues.each do |issue|
1086 issues.each do |issue|
1081 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
1087 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
1082 end
1088 end
1083 end
1089 end
1084 end
1090 end
1085
1091
1086 # Preloads visible total spent time for a collection of issues
1092 # Preloads visible total spent time for a collection of issues
1087 def self.load_visible_total_spent_hours(issues, user=User.current)
1093 def self.load_visible_total_spent_hours(issues, user=User.current)
1088 if issues.any?
1094 if issues.any?
1089 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1095 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1090 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1096 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1091 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1097 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1092 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1098 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1093 issues.each do |issue|
1099 issues.each do |issue|
1094 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
1100 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
1095 end
1101 end
1096 end
1102 end
1097 end
1103 end
1098
1104
1099 # Preloads visible relations for a collection of issues
1105 # Preloads visible relations for a collection of issues
1100 def self.load_visible_relations(issues, user=User.current)
1106 def self.load_visible_relations(issues, user=User.current)
1101 if issues.any?
1107 if issues.any?
1102 issue_ids = issues.map(&:id)
1108 issue_ids = issues.map(&:id)
1103 # Relations with issue_from in given issues and visible issue_to
1109 # Relations with issue_from in given issues and visible issue_to
1104 relations_from = IssueRelation.joins(:issue_to => :project).
1110 relations_from = IssueRelation.joins(:issue_to => :project).
1105 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1111 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1106 # Relations with issue_to in given issues and visible issue_from
1112 # Relations with issue_to in given issues and visible issue_from
1107 relations_to = IssueRelation.joins(:issue_from => :project).
1113 relations_to = IssueRelation.joins(:issue_from => :project).
1108 where(visible_condition(user)).
1114 where(visible_condition(user)).
1109 where(:issue_to_id => issue_ids).to_a
1115 where(:issue_to_id => issue_ids).to_a
1110 issues.each do |issue|
1116 issues.each do |issue|
1111 relations =
1117 relations =
1112 relations_from.select {|relation| relation.issue_from_id == issue.id} +
1118 relations_from.select {|relation| relation.issue_from_id == issue.id} +
1113 relations_to.select {|relation| relation.issue_to_id == issue.id}
1119 relations_to.select {|relation| relation.issue_to_id == issue.id}
1114
1120
1115 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
1121 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
1116 end
1122 end
1117 end
1123 end
1118 end
1124 end
1119
1125
1120 # Returns a scope of the given issues and their descendants
1126 # Returns a scope of the given issues and their descendants
1121 def self.self_and_descendants(issues)
1127 def self.self_and_descendants(issues)
1122 Issue.joins("JOIN #{Issue.table_name} ancestors" +
1128 Issue.joins("JOIN #{Issue.table_name} ancestors" +
1123 " ON ancestors.root_id = #{Issue.table_name}.root_id" +
1129 " ON ancestors.root_id = #{Issue.table_name}.root_id" +
1124 " AND ancestors.lft <= #{Issue.table_name}.lft AND ancestors.rgt >= #{Issue.table_name}.rgt"
1130 " AND ancestors.lft <= #{Issue.table_name}.lft AND ancestors.rgt >= #{Issue.table_name}.rgt"
1125 ).
1131 ).
1126 where(:ancestors => {:id => issues.map(&:id)})
1132 where(:ancestors => {:id => issues.map(&:id)})
1127 end
1133 end
1128
1134
1129 # Finds an issue relation given its id.
1135 # Finds an issue relation given its id.
1130 def find_relation(relation_id)
1136 def find_relation(relation_id)
1131 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1137 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1132 end
1138 end
1133
1139
1134 # Returns true if this issue blocks the other issue, otherwise returns false
1140 # Returns true if this issue blocks the other issue, otherwise returns false
1135 def blocks?(other)
1141 def blocks?(other)
1136 all = [self]
1142 all = [self]
1137 last = [self]
1143 last = [self]
1138 while last.any?
1144 while last.any?
1139 current = last.map {|i| i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)}.flatten.uniq
1145 current = last.map {|i| i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)}.flatten.uniq
1140 current -= last
1146 current -= last
1141 current -= all
1147 current -= all
1142 return true if current.include?(other)
1148 return true if current.include?(other)
1143 last = current
1149 last = current
1144 all += last
1150 all += last
1145 end
1151 end
1146 false
1152 false
1147 end
1153 end
1148
1154
1149 # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1155 # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1150 def would_reschedule?(other)
1156 def would_reschedule?(other)
1151 all = [self]
1157 all = [self]
1152 last = [self]
1158 last = [self]
1153 while last.any?
1159 while last.any?
1154 current = last.map {|i|
1160 current = last.map {|i|
1155 i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1161 i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1156 i.leaves.to_a +
1162 i.leaves.to_a +
1157 i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1163 i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1158 }.flatten.uniq
1164 }.flatten.uniq
1159 current -= last
1165 current -= last
1160 current -= all
1166 current -= all
1161 return true if current.include?(other)
1167 return true if current.include?(other)
1162 last = current
1168 last = current
1163 all += last
1169 all += last
1164 end
1170 end
1165 false
1171 false
1166 end
1172 end
1167
1173
1168 # Returns an array of issues that duplicate this one
1174 # Returns an array of issues that duplicate this one
1169 def duplicates
1175 def duplicates
1170 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1176 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1171 end
1177 end
1172
1178
1173 # Returns the due date or the target due date if any
1179 # Returns the due date or the target due date if any
1174 # Used on gantt chart
1180 # Used on gantt chart
1175 def due_before
1181 def due_before
1176 due_date || (fixed_version ? fixed_version.effective_date : nil)
1182 due_date || (fixed_version ? fixed_version.effective_date : nil)
1177 end
1183 end
1178
1184
1179 # Returns the time scheduled for this issue.
1185 # Returns the time scheduled for this issue.
1180 #
1186 #
1181 # Example:
1187 # Example:
1182 # Start Date: 2/26/09, End Date: 3/04/09
1188 # Start Date: 2/26/09, End Date: 3/04/09
1183 # duration => 6
1189 # duration => 6
1184 def duration
1190 def duration
1185 (start_date && due_date) ? due_date - start_date : 0
1191 (start_date && due_date) ? due_date - start_date : 0
1186 end
1192 end
1187
1193
1188 # Returns the duration in working days
1194 # Returns the duration in working days
1189 def working_duration
1195 def working_duration
1190 (start_date && due_date) ? working_days(start_date, due_date) : 0
1196 (start_date && due_date) ? working_days(start_date, due_date) : 0
1191 end
1197 end
1192
1198
1193 def soonest_start(reload=false)
1199 def soonest_start(reload=false)
1194 if @soonest_start.nil? || reload
1200 if @soonest_start.nil? || reload
1195 relations_to.reload if reload
1201 relations_to.reload if reload
1196 dates = relations_to.collect{|relation| relation.successor_soonest_start}
1202 dates = relations_to.collect{|relation| relation.successor_soonest_start}
1197 p = @parent_issue || parent
1203 p = @parent_issue || parent
1198 if p && Setting.parent_issue_dates == 'derived'
1204 if p && Setting.parent_issue_dates == 'derived'
1199 dates << p.soonest_start
1205 dates << p.soonest_start
1200 end
1206 end
1201 @soonest_start = dates.compact.max
1207 @soonest_start = dates.compact.max
1202 end
1208 end
1203 @soonest_start
1209 @soonest_start
1204 end
1210 end
1205
1211
1206 # Sets start_date on the given date or the next working day
1212 # Sets start_date on the given date or the next working day
1207 # and changes due_date to keep the same working duration.
1213 # and changes due_date to keep the same working duration.
1208 def reschedule_on(date)
1214 def reschedule_on(date)
1209 wd = working_duration
1215 wd = working_duration
1210 date = next_working_date(date)
1216 date = next_working_date(date)
1211 self.start_date = date
1217 self.start_date = date
1212 self.due_date = add_working_days(date, wd)
1218 self.due_date = add_working_days(date, wd)
1213 end
1219 end
1214
1220
1215 # Reschedules the issue on the given date or the next working day and saves the record.
1221 # Reschedules the issue on the given date or the next working day and saves the record.
1216 # If the issue is a parent task, this is done by rescheduling its subtasks.
1222 # If the issue is a parent task, this is done by rescheduling its subtasks.
1217 def reschedule_on!(date)
1223 def reschedule_on!(date)
1218 return if date.nil?
1224 return if date.nil?
1219 if leaf? || !dates_derived?
1225 if leaf? || !dates_derived?
1220 if start_date.nil? || start_date != date
1226 if start_date.nil? || start_date != date
1221 if start_date && start_date > date
1227 if start_date && start_date > date
1222 # Issue can not be moved earlier than its soonest start date
1228 # Issue can not be moved earlier than its soonest start date
1223 date = [soonest_start(true), date].compact.max
1229 date = [soonest_start(true), date].compact.max
1224 end
1230 end
1225 reschedule_on(date)
1231 reschedule_on(date)
1226 begin
1232 begin
1227 save
1233 save
1228 rescue ActiveRecord::StaleObjectError
1234 rescue ActiveRecord::StaleObjectError
1229 reload
1235 reload
1230 reschedule_on(date)
1236 reschedule_on(date)
1231 save
1237 save
1232 end
1238 end
1233 end
1239 end
1234 else
1240 else
1235 leaves.each do |leaf|
1241 leaves.each do |leaf|
1236 if leaf.start_date
1242 if leaf.start_date
1237 # Only move subtask if it starts at the same date as the parent
1243 # Only move subtask if it starts at the same date as the parent
1238 # or if it starts before the given date
1244 # or if it starts before the given date
1239 if start_date == leaf.start_date || date > leaf.start_date
1245 if start_date == leaf.start_date || date > leaf.start_date
1240 leaf.reschedule_on!(date)
1246 leaf.reschedule_on!(date)
1241 end
1247 end
1242 else
1248 else
1243 leaf.reschedule_on!(date)
1249 leaf.reschedule_on!(date)
1244 end
1250 end
1245 end
1251 end
1246 end
1252 end
1247 end
1253 end
1248
1254
1249 def dates_derived?
1255 def dates_derived?
1250 !leaf? && Setting.parent_issue_dates == 'derived'
1256 !leaf? && Setting.parent_issue_dates == 'derived'
1251 end
1257 end
1252
1258
1253 def priority_derived?
1259 def priority_derived?
1254 !leaf? && Setting.parent_issue_priority == 'derived'
1260 !leaf? && Setting.parent_issue_priority == 'derived'
1255 end
1261 end
1256
1262
1257 def done_ratio_derived?
1263 def done_ratio_derived?
1258 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1264 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1259 end
1265 end
1260
1266
1261 def <=>(issue)
1267 def <=>(issue)
1262 if issue.nil?
1268 if issue.nil?
1263 -1
1269 -1
1264 elsif root_id != issue.root_id
1270 elsif root_id != issue.root_id
1265 (root_id || 0) <=> (issue.root_id || 0)
1271 (root_id || 0) <=> (issue.root_id || 0)
1266 else
1272 else
1267 (lft || 0) <=> (issue.lft || 0)
1273 (lft || 0) <=> (issue.lft || 0)
1268 end
1274 end
1269 end
1275 end
1270
1276
1271 def to_s
1277 def to_s
1272 "#{tracker} ##{id}: #{subject}"
1278 "#{tracker} ##{id}: #{subject}"
1273 end
1279 end
1274
1280
1275 # Returns a string of css classes that apply to the issue
1281 # Returns a string of css classes that apply to the issue
1276 def css_classes(user=User.current)
1282 def css_classes(user=User.current)
1277 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1283 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1278 s << ' closed' if closed?
1284 s << ' closed' if closed?
1279 s << ' overdue' if overdue?
1285 s << ' overdue' if overdue?
1280 s << ' child' if child?
1286 s << ' child' if child?
1281 s << ' parent' unless leaf?
1287 s << ' parent' unless leaf?
1282 s << ' private' if is_private?
1288 s << ' private' if is_private?
1283 if user.logged?
1289 if user.logged?
1284 s << ' created-by-me' if author_id == user.id
1290 s << ' created-by-me' if author_id == user.id
1285 s << ' assigned-to-me' if assigned_to_id == user.id
1291 s << ' assigned-to-me' if assigned_to_id == user.id
1286 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1292 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1287 end
1293 end
1288 s
1294 s
1289 end
1295 end
1290
1296
1291 # Unassigns issues from +version+ if it's no longer shared with issue's project
1297 # Unassigns issues from +version+ if it's no longer shared with issue's project
1292 def self.update_versions_from_sharing_change(version)
1298 def self.update_versions_from_sharing_change(version)
1293 # Update issues assigned to the version
1299 # Update issues assigned to the version
1294 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1300 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1295 end
1301 end
1296
1302
1297 # Unassigns issues from versions that are no longer shared
1303 # Unassigns issues from versions that are no longer shared
1298 # after +project+ was moved
1304 # after +project+ was moved
1299 def self.update_versions_from_hierarchy_change(project)
1305 def self.update_versions_from_hierarchy_change(project)
1300 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1306 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1301 # Update issues of the moved projects and issues assigned to a version of a moved project
1307 # Update issues of the moved projects and issues assigned to a version of a moved project
1302 Issue.update_versions(
1308 Issue.update_versions(
1303 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1309 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1304 moved_project_ids, moved_project_ids]
1310 moved_project_ids, moved_project_ids]
1305 )
1311 )
1306 end
1312 end
1307
1313
1308 def parent_issue_id=(arg)
1314 def parent_issue_id=(arg)
1309 s = arg.to_s.strip.presence
1315 s = arg.to_s.strip.presence
1310 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1316 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1311 @invalid_parent_issue_id = nil
1317 @invalid_parent_issue_id = nil
1312 elsif s.blank?
1318 elsif s.blank?
1313 @parent_issue = nil
1319 @parent_issue = nil
1314 @invalid_parent_issue_id = nil
1320 @invalid_parent_issue_id = nil
1315 else
1321 else
1316 @parent_issue = nil
1322 @parent_issue = nil
1317 @invalid_parent_issue_id = arg
1323 @invalid_parent_issue_id = arg
1318 end
1324 end
1319 end
1325 end
1320
1326
1321 def parent_issue_id
1327 def parent_issue_id
1322 if @invalid_parent_issue_id
1328 if @invalid_parent_issue_id
1323 @invalid_parent_issue_id
1329 @invalid_parent_issue_id
1324 elsif instance_variable_defined? :@parent_issue
1330 elsif instance_variable_defined? :@parent_issue
1325 @parent_issue.nil? ? nil : @parent_issue.id
1331 @parent_issue.nil? ? nil : @parent_issue.id
1326 else
1332 else
1327 parent_id
1333 parent_id
1328 end
1334 end
1329 end
1335 end
1330
1336
1331 def set_parent_id
1337 def set_parent_id
1332 self.parent_id = parent_issue_id
1338 self.parent_id = parent_issue_id
1333 end
1339 end
1334
1340
1335 # Returns true if issue's project is a valid
1341 # Returns true if issue's project is a valid
1336 # parent issue project
1342 # parent issue project
1337 def valid_parent_project?(issue=parent)
1343 def valid_parent_project?(issue=parent)
1338 return true if issue.nil? || issue.project_id == project_id
1344 return true if issue.nil? || issue.project_id == project_id
1339
1345
1340 case Setting.cross_project_subtasks
1346 case Setting.cross_project_subtasks
1341 when 'system'
1347 when 'system'
1342 true
1348 true
1343 when 'tree'
1349 when 'tree'
1344 issue.project.root == project.root
1350 issue.project.root == project.root
1345 when 'hierarchy'
1351 when 'hierarchy'
1346 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1352 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1347 when 'descendants'
1353 when 'descendants'
1348 issue.project.is_or_is_ancestor_of?(project)
1354 issue.project.is_or_is_ancestor_of?(project)
1349 else
1355 else
1350 false
1356 false
1351 end
1357 end
1352 end
1358 end
1353
1359
1354 # Returns an issue scope based on project and scope
1360 # Returns an issue scope based on project and scope
1355 def self.cross_project_scope(project, scope=nil)
1361 def self.cross_project_scope(project, scope=nil)
1356 if project.nil?
1362 if project.nil?
1357 return Issue
1363 return Issue
1358 end
1364 end
1359 case scope
1365 case scope
1360 when 'all', 'system'
1366 when 'all', 'system'
1361 Issue
1367 Issue
1362 when 'tree'
1368 when 'tree'
1363 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1369 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1364 :lft => project.root.lft, :rgt => project.root.rgt)
1370 :lft => project.root.lft, :rgt => project.root.rgt)
1365 when 'hierarchy'
1371 when 'hierarchy'
1366 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)",
1372 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)",
1367 :lft => project.lft, :rgt => project.rgt)
1373 :lft => project.lft, :rgt => project.rgt)
1368 when 'descendants'
1374 when 'descendants'
1369 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1375 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1370 :lft => project.lft, :rgt => project.rgt)
1376 :lft => project.lft, :rgt => project.rgt)
1371 else
1377 else
1372 Issue.where(:project_id => project.id)
1378 Issue.where(:project_id => project.id)
1373 end
1379 end
1374 end
1380 end
1375
1381
1376 def self.by_tracker(project)
1382 def self.by_tracker(project)
1377 count_and_group_by(:project => project, :association => :tracker)
1383 count_and_group_by(:project => project, :association => :tracker)
1378 end
1384 end
1379
1385
1380 def self.by_version(project)
1386 def self.by_version(project)
1381 count_and_group_by(:project => project, :association => :fixed_version)
1387 count_and_group_by(:project => project, :association => :fixed_version)
1382 end
1388 end
1383
1389
1384 def self.by_priority(project)
1390 def self.by_priority(project)
1385 count_and_group_by(:project => project, :association => :priority)
1391 count_and_group_by(:project => project, :association => :priority)
1386 end
1392 end
1387
1393
1388 def self.by_category(project)
1394 def self.by_category(project)
1389 count_and_group_by(:project => project, :association => :category)
1395 count_and_group_by(:project => project, :association => :category)
1390 end
1396 end
1391
1397
1392 def self.by_assigned_to(project)
1398 def self.by_assigned_to(project)
1393 count_and_group_by(:project => project, :association => :assigned_to)
1399 count_and_group_by(:project => project, :association => :assigned_to)
1394 end
1400 end
1395
1401
1396 def self.by_author(project)
1402 def self.by_author(project)
1397 count_and_group_by(:project => project, :association => :author)
1403 count_and_group_by(:project => project, :association => :author)
1398 end
1404 end
1399
1405
1400 def self.by_subproject(project)
1406 def self.by_subproject(project)
1401 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1407 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1402 r.reject {|r| r["project_id"] == project.id.to_s}
1408 r.reject {|r| r["project_id"] == project.id.to_s}
1403 end
1409 end
1404
1410
1405 # Query generator for selecting groups of issue counts for a project
1411 # Query generator for selecting groups of issue counts for a project
1406 # based on specific criteria
1412 # based on specific criteria
1407 #
1413 #
1408 # Options
1414 # Options
1409 # * project - Project to search in.
1415 # * project - Project to search in.
1410 # * with_subprojects - Includes subprojects issues if set to true.
1416 # * with_subprojects - Includes subprojects issues if set to true.
1411 # * association - Symbol. Association for grouping.
1417 # * association - Symbol. Association for grouping.
1412 def self.count_and_group_by(options)
1418 def self.count_and_group_by(options)
1413 assoc = reflect_on_association(options[:association])
1419 assoc = reflect_on_association(options[:association])
1414 select_field = assoc.foreign_key
1420 select_field = assoc.foreign_key
1415
1421
1416 Issue.
1422 Issue.
1417 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1423 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1418 joins(:status, assoc.name).
1424 joins(:status, assoc.name).
1419 group(:status_id, :is_closed, select_field).
1425 group(:status_id, :is_closed, select_field).
1420 count.
1426 count.
1421 map do |columns, total|
1427 map do |columns, total|
1422 status_id, is_closed, field_value = columns
1428 status_id, is_closed, field_value = columns
1423 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1429 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1424 {
1430 {
1425 "status_id" => status_id.to_s,
1431 "status_id" => status_id.to_s,
1426 "closed" => is_closed,
1432 "closed" => is_closed,
1427 select_field => field_value.to_s,
1433 select_field => field_value.to_s,
1428 "total" => total.to_s
1434 "total" => total.to_s
1429 }
1435 }
1430 end
1436 end
1431 end
1437 end
1432
1438
1433 # Returns a scope of projects that user can assign the issue to
1439 # Returns a scope of projects that user can assign the issue to
1434 def allowed_target_projects(user=User.current)
1440 def allowed_target_projects(user=User.current)
1435 current_project = new_record? ? nil : project
1441 current_project = new_record? ? nil : project
1436 self.class.allowed_target_projects(user, current_project)
1442 self.class.allowed_target_projects(user, current_project)
1437 end
1443 end
1438
1444
1439 # Returns a scope of projects that user can assign issues to
1445 # Returns a scope of projects that user can assign issues to
1440 # If current_project is given, it will be included in the scope
1446 # If current_project is given, it will be included in the scope
1441 def self.allowed_target_projects(user=User.current, current_project=nil)
1447 def self.allowed_target_projects(user=User.current, current_project=nil)
1442 condition = Project.allowed_to_condition(user, :add_issues)
1448 condition = Project.allowed_to_condition(user, :add_issues)
1443 if current_project
1449 if current_project
1444 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1450 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1445 end
1451 end
1446 Project.where(condition).having_trackers
1452 Project.where(condition).having_trackers
1447 end
1453 end
1448
1454
1449 # Returns a scope of trackers that user can assign the issue to
1455 # Returns a scope of trackers that user can assign the issue to
1450 def allowed_target_trackers(user=User.current)
1456 def allowed_target_trackers(user=User.current)
1451 self.class.allowed_target_trackers(project, user, tracker_id_was)
1457 self.class.allowed_target_trackers(project, user, tracker_id_was)
1452 end
1458 end
1453
1459
1454 # Returns a scope of trackers that user can assign project issues to
1460 # Returns a scope of trackers that user can assign project issues to
1455 def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1461 def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1456 if project
1462 if project
1457 scope = project.trackers.sorted
1463 scope = project.trackers.sorted
1458 unless user.admin?
1464 unless user.admin?
1459 roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1465 roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1460 unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1466 unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1461 tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1467 tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1462 if current_tracker
1468 if current_tracker
1463 tracker_ids << current_tracker
1469 tracker_ids << current_tracker
1464 end
1470 end
1465 scope = scope.where(:id => tracker_ids)
1471 scope = scope.where(:id => tracker_ids)
1466 end
1472 end
1467 end
1473 end
1468 scope
1474 scope
1469 else
1475 else
1470 Tracker.none
1476 Tracker.none
1471 end
1477 end
1472 end
1478 end
1473
1479
1474 private
1480 private
1475
1481
1476 def user_tracker_permission?(user, permission)
1482 def user_tracker_permission?(user, permission)
1477 if project && !project.active?
1483 if project && !project.active?
1478 perm = Redmine::AccessControl.permission(permission)
1484 perm = Redmine::AccessControl.permission(permission)
1479 return false unless perm && perm.read?
1485 return false unless perm && perm.read?
1480 end
1486 end
1481
1487
1482 if user.admin?
1488 if user.admin?
1483 true
1489 true
1484 else
1490 else
1485 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1491 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1486 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1492 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1487 end
1493 end
1488 end
1494 end
1489
1495
1490 def after_project_change
1496 def after_project_change
1491 # Update project_id on related time entries
1497 # Update project_id on related time entries
1492 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1498 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1493
1499
1494 # Delete issue relations
1500 # Delete issue relations
1495 unless Setting.cross_project_issue_relations?
1501 unless Setting.cross_project_issue_relations?
1496 relations_from.clear
1502 relations_from.clear
1497 relations_to.clear
1503 relations_to.clear
1498 end
1504 end
1499
1505
1500 # Move subtasks that were in the same project
1506 # Move subtasks that were in the same project
1501 children.each do |child|
1507 children.each do |child|
1502 next unless child.project_id == project_id_was
1508 next unless child.project_id == project_id_was
1503 # Change project and keep project
1509 # Change project and keep project
1504 child.send :project=, project, true
1510 child.send :project=, project, true
1505 unless child.save
1511 unless child.save
1506 errors.add :base, l(:error_move_of_child_not_possible, :child => "##{child.id}", :errors => child.errors.full_messages.join(", "))
1512 errors.add :base, l(:error_move_of_child_not_possible, :child => "##{child.id}", :errors => child.errors.full_messages.join(", "))
1507 raise ActiveRecord::Rollback
1513 raise ActiveRecord::Rollback
1508 end
1514 end
1509 end
1515 end
1510 end
1516 end
1511
1517
1512 # Callback for after the creation of an issue by copy
1518 # Callback for after the creation of an issue by copy
1513 # * adds a "copied to" relation with the copied issue
1519 # * adds a "copied to" relation with the copied issue
1514 # * copies subtasks from the copied issue
1520 # * copies subtasks from the copied issue
1515 def after_create_from_copy
1521 def after_create_from_copy
1516 return unless copy? && !@after_create_from_copy_handled
1522 return unless copy? && !@after_create_from_copy_handled
1517
1523
1518 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1524 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1519 if @current_journal
1525 if @current_journal
1520 @copied_from.init_journal(@current_journal.user)
1526 @copied_from.init_journal(@current_journal.user)
1521 end
1527 end
1522 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1528 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1523 unless relation.save
1529 unless relation.save
1524 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1530 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1525 end
1531 end
1526 end
1532 end
1527
1533
1528 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1534 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1529 copy_options = (@copy_options || {}).merge(:subtasks => false)
1535 copy_options = (@copy_options || {}).merge(:subtasks => false)
1530 copied_issue_ids = {@copied_from.id => self.id}
1536 copied_issue_ids = {@copied_from.id => self.id}
1531 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1537 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1532 # Do not copy self when copying an issue as a descendant of the copied issue
1538 # Do not copy self when copying an issue as a descendant of the copied issue
1533 next if child == self
1539 next if child == self
1534 # Do not copy subtasks of issues that were not copied
1540 # Do not copy subtasks of issues that were not copied
1535 next unless copied_issue_ids[child.parent_id]
1541 next unless copied_issue_ids[child.parent_id]
1536 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1542 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1537 unless child.visible?
1543 unless child.visible?
1538 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1544 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1539 next
1545 next
1540 end
1546 end
1541 copy = Issue.new.copy_from(child, copy_options)
1547 copy = Issue.new.copy_from(child, copy_options)
1542 if @current_journal
1548 if @current_journal
1543 copy.init_journal(@current_journal.user)
1549 copy.init_journal(@current_journal.user)
1544 end
1550 end
1545 copy.author = author
1551 copy.author = author
1546 copy.project = project
1552 copy.project = project
1547 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1553 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1548 unless copy.save
1554 unless copy.save
1549 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
1555 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
1550 next
1556 next
1551 end
1557 end
1552 copied_issue_ids[child.id] = copy.id
1558 copied_issue_ids[child.id] = copy.id
1553 end
1559 end
1554 end
1560 end
1555 @after_create_from_copy_handled = true
1561 @after_create_from_copy_handled = true
1556 end
1562 end
1557
1563
1558 def update_nested_set_attributes
1564 def update_nested_set_attributes
1559 if parent_id_changed?
1565 if parent_id_changed?
1560 update_nested_set_attributes_on_parent_change
1566 update_nested_set_attributes_on_parent_change
1561 end
1567 end
1562 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1568 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1563 end
1569 end
1564
1570
1565 # Updates the nested set for when an existing issue is moved
1571 # Updates the nested set for when an existing issue is moved
1566 def update_nested_set_attributes_on_parent_change
1572 def update_nested_set_attributes_on_parent_change
1567 former_parent_id = parent_id_was
1573 former_parent_id = parent_id_was
1568 # delete invalid relations of all descendants
1574 # delete invalid relations of all descendants
1569 self_and_descendants.each do |issue|
1575 self_and_descendants.each do |issue|
1570 issue.relations.each do |relation|
1576 issue.relations.each do |relation|
1571 relation.destroy unless relation.valid?
1577 relation.destroy unless relation.valid?
1572 end
1578 end
1573 end
1579 end
1574 # update former parent
1580 # update former parent
1575 recalculate_attributes_for(former_parent_id) if former_parent_id
1581 recalculate_attributes_for(former_parent_id) if former_parent_id
1576 end
1582 end
1577
1583
1578 def update_parent_attributes
1584 def update_parent_attributes
1579 if parent_id
1585 if parent_id
1580 recalculate_attributes_for(parent_id)
1586 recalculate_attributes_for(parent_id)
1581 association(:parent).reset
1587 association(:parent).reset
1582 end
1588 end
1583 end
1589 end
1584
1590
1585 def recalculate_attributes_for(issue_id)
1591 def recalculate_attributes_for(issue_id)
1586 if issue_id && p = Issue.find_by_id(issue_id)
1592 if issue_id && p = Issue.find_by_id(issue_id)
1587 if p.priority_derived?
1593 if p.priority_derived?
1588 # priority = highest priority of open children
1594 # priority = highest priority of open children
1589 # priority is left unchanged if all children are closed and there's no default priority defined
1595 # priority is left unchanged if all children are closed and there's no default priority defined
1590 if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1596 if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1591 p.priority = IssuePriority.find_by_position(priority_position)
1597 p.priority = IssuePriority.find_by_position(priority_position)
1592 elsif default_priority = IssuePriority.default
1598 elsif default_priority = IssuePriority.default
1593 p.priority = default_priority
1599 p.priority = default_priority
1594 end
1600 end
1595 end
1601 end
1596
1602
1597 if p.dates_derived?
1603 if p.dates_derived?
1598 # start/due dates = lowest/highest dates of children
1604 # start/due dates = lowest/highest dates of children
1599 p.start_date = p.children.minimum(:start_date)
1605 p.start_date = p.children.minimum(:start_date)
1600 p.due_date = p.children.maximum(:due_date)
1606 p.due_date = p.children.maximum(:due_date)
1601 if p.start_date && p.due_date && p.due_date < p.start_date
1607 if p.start_date && p.due_date && p.due_date < p.start_date
1602 p.start_date, p.due_date = p.due_date, p.start_date
1608 p.start_date, p.due_date = p.due_date, p.start_date
1603 end
1609 end
1604 end
1610 end
1605
1611
1606 if p.done_ratio_derived?
1612 if p.done_ratio_derived?
1607 # done ratio = average ratio of children weighted with their total estimated hours
1613 # done ratio = average ratio of children weighted with their total estimated hours
1608 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1614 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1609 children = p.children.to_a
1615 children = p.children.to_a
1610 if children.any?
1616 if children.any?
1611 child_with_total_estimated_hours = children.select {|c| c.total_estimated_hours.to_f > 0.0}
1617 child_with_total_estimated_hours = children.select {|c| c.total_estimated_hours.to_f > 0.0}
1612 if child_with_total_estimated_hours.any?
1618 if child_with_total_estimated_hours.any?
1613 average = child_with_total_estimated_hours.map(&:total_estimated_hours).sum.to_f / child_with_total_estimated_hours.count
1619 average = child_with_total_estimated_hours.map(&:total_estimated_hours).sum.to_f / child_with_total_estimated_hours.count
1614 else
1620 else
1615 average = 1.0
1621 average = 1.0
1616 end
1622 end
1617 done = children.map {|c|
1623 done = children.map {|c|
1618 estimated = c.total_estimated_hours.to_f
1624 estimated = c.total_estimated_hours.to_f
1619 estimated = average unless estimated > 0.0
1625 estimated = average unless estimated > 0.0
1620 ratio = c.closed? ? 100 : (c.done_ratio || 0)
1626 ratio = c.closed? ? 100 : (c.done_ratio || 0)
1621 estimated * ratio
1627 estimated * ratio
1622 }.sum
1628 }.sum
1623 progress = done / (average * children.count)
1629 progress = done / (average * children.count)
1624 p.done_ratio = progress.round
1630 p.done_ratio = progress.round
1625 end
1631 end
1626 end
1632 end
1627 end
1633 end
1628
1634
1629 # ancestors will be recursively updated
1635 # ancestors will be recursively updated
1630 p.save(:validate => false)
1636 p.save(:validate => false)
1631 end
1637 end
1632 end
1638 end
1633
1639
1634 # Update issues so their versions are not pointing to a
1640 # Update issues so their versions are not pointing to a
1635 # fixed_version that is not shared with the issue's project
1641 # fixed_version that is not shared with the issue's project
1636 def self.update_versions(conditions=nil)
1642 def self.update_versions(conditions=nil)
1637 # Only need to update issues with a fixed_version from
1643 # Only need to update issues with a fixed_version from
1638 # a different project and that is not systemwide shared
1644 # a different project and that is not systemwide shared
1639 Issue.joins(:project, :fixed_version).
1645 Issue.joins(:project, :fixed_version).
1640 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1646 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1641 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1647 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1642 " AND #{Version.table_name}.sharing <> 'system'").
1648 " AND #{Version.table_name}.sharing <> 'system'").
1643 where(conditions).each do |issue|
1649 where(conditions).each do |issue|
1644 next if issue.project.nil? || issue.fixed_version.nil?
1650 next if issue.project.nil? || issue.fixed_version.nil?
1645 unless issue.project.shared_versions.include?(issue.fixed_version)
1651 unless issue.project.shared_versions.include?(issue.fixed_version)
1646 issue.init_journal(User.current)
1652 issue.init_journal(User.current)
1647 issue.fixed_version = nil
1653 issue.fixed_version = nil
1648 issue.save
1654 issue.save
1649 end
1655 end
1650 end
1656 end
1651 end
1657 end
1652
1658
1653 def delete_selected_attachments
1659 def delete_selected_attachments
1654 if deleted_attachment_ids.present?
1660 if deleted_attachment_ids.present?
1655 objects = attachments.where(:id => deleted_attachment_ids.map(&:to_i))
1661 objects = attachments.where(:id => deleted_attachment_ids.map(&:to_i))
1656 attachments.delete(objects)
1662 attachments.delete(objects)
1657 end
1663 end
1658 end
1664 end
1659
1665
1660 # Callback on file attachment
1666 # Callback on file attachment
1661 def attachment_added(attachment)
1667 def attachment_added(attachment)
1662 if current_journal && !attachment.new_record?
1668 if current_journal && !attachment.new_record?
1663 current_journal.journalize_attachment(attachment, :added)
1669 current_journal.journalize_attachment(attachment, :added)
1664 end
1670 end
1665 end
1671 end
1666
1672
1667 # Callback on attachment deletion
1673 # Callback on attachment deletion
1668 def attachment_removed(attachment)
1674 def attachment_removed(attachment)
1669 if current_journal && !attachment.new_record?
1675 if current_journal && !attachment.new_record?
1670 current_journal.journalize_attachment(attachment, :removed)
1676 current_journal.journalize_attachment(attachment, :removed)
1671 current_journal.save
1677 current_journal.save
1672 end
1678 end
1673 end
1679 end
1674
1680
1675 # Called after a relation is added
1681 # Called after a relation is added
1676 def relation_added(relation)
1682 def relation_added(relation)
1677 if current_journal
1683 if current_journal
1678 current_journal.journalize_relation(relation, :added)
1684 current_journal.journalize_relation(relation, :added)
1679 current_journal.save
1685 current_journal.save
1680 end
1686 end
1681 end
1687 end
1682
1688
1683 # Called after a relation is removed
1689 # Called after a relation is removed
1684 def relation_removed(relation)
1690 def relation_removed(relation)
1685 if current_journal
1691 if current_journal
1686 current_journal.journalize_relation(relation, :removed)
1692 current_journal.journalize_relation(relation, :removed)
1687 current_journal.save
1693 current_journal.save
1688 end
1694 end
1689 end
1695 end
1690
1696
1691 # Default assignment based on category
1697 # Default assignment based on category
1692 def default_assign
1698 def default_assign
1693 if assigned_to.nil? && category && category.assigned_to
1699 if assigned_to.nil? && category && category.assigned_to
1694 self.assigned_to = category.assigned_to
1700 self.assigned_to = category.assigned_to
1695 end
1701 end
1696 end
1702 end
1697
1703
1698 # Updates start/due dates of following issues
1704 # Updates start/due dates of following issues
1699 def reschedule_following_issues
1705 def reschedule_following_issues
1700 if start_date_changed? || due_date_changed?
1706 if start_date_changed? || due_date_changed?
1701 relations_from.each do |relation|
1707 relations_from.each do |relation|
1702 relation.set_issue_to_dates
1708 relation.set_issue_to_dates
1703 end
1709 end
1704 end
1710 end
1705 end
1711 end
1706
1712
1707 # Closes duplicates if the issue is being closed
1713 # Closes duplicates if the issue is being closed
1708 def close_duplicates
1714 def close_duplicates
1709 if closing?
1715 if closing?
1710 duplicates.each do |duplicate|
1716 duplicates.each do |duplicate|
1711 # Reload is needed in case the duplicate was updated by a previous duplicate
1717 # Reload is needed in case the duplicate was updated by a previous duplicate
1712 duplicate.reload
1718 duplicate.reload
1713 # Don't re-close it if it's already closed
1719 # Don't re-close it if it's already closed
1714 next if duplicate.closed?
1720 next if duplicate.closed?
1715 # Same user and notes
1721 # Same user and notes
1716 if @current_journal
1722 if @current_journal
1717 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1723 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1718 duplicate.private_notes = @current_journal.private_notes
1724 duplicate.private_notes = @current_journal.private_notes
1719 end
1725 end
1720 duplicate.update_attribute :status, self.status
1726 duplicate.update_attribute :status, self.status
1721 end
1727 end
1722 end
1728 end
1723 end
1729 end
1724
1730
1725 # Make sure updated_on is updated when adding a note and set updated_on now
1731 # Make sure updated_on is updated when adding a note and set updated_on now
1726 # so we can set closed_on with the same value on closing
1732 # so we can set closed_on with the same value on closing
1727 def force_updated_on_change
1733 def force_updated_on_change
1728 if @current_journal || changed?
1734 if @current_journal || changed?
1729 self.updated_on = current_time_from_proper_timezone
1735 self.updated_on = current_time_from_proper_timezone
1730 if new_record?
1736 if new_record?
1731 self.created_on = updated_on
1737 self.created_on = updated_on
1732 end
1738 end
1733 end
1739 end
1734 end
1740 end
1735
1741
1736 # Callback for setting closed_on when the issue is closed.
1742 # Callback for setting closed_on when the issue is closed.
1737 # The closed_on attribute stores the time of the last closing
1743 # The closed_on attribute stores the time of the last closing
1738 # and is preserved when the issue is reopened.
1744 # and is preserved when the issue is reopened.
1739 def update_closed_on
1745 def update_closed_on
1740 if closing?
1746 if closing?
1741 self.closed_on = updated_on
1747 self.closed_on = updated_on
1742 end
1748 end
1743 end
1749 end
1744
1750
1745 # Saves the changes in a Journal
1751 # Saves the changes in a Journal
1746 # Called after_save
1752 # Called after_save
1747 def create_journal
1753 def create_journal
1748 if current_journal
1754 if current_journal
1749 current_journal.save
1755 current_journal.save
1750 end
1756 end
1751 end
1757 end
1752
1758
1753 def send_notification
1759 def send_notification
1754 if notify? && Setting.notified_events.include?('issue_added')
1760 if notify? && Setting.notified_events.include?('issue_added')
1755 Mailer.deliver_issue_add(self)
1761 Mailer.deliver_issue_add(self)
1756 end
1762 end
1757 end
1763 end
1758
1764
1759 # Stores the previous assignee so we can still have access
1765 # Stores the previous assignee so we can still have access
1760 # to it during after_save callbacks (assigned_to_id_was is reset)
1766 # to it during after_save callbacks (assigned_to_id_was is reset)
1761 def set_assigned_to_was
1767 def set_assigned_to_was
1762 @previous_assigned_to_id = assigned_to_id_was
1768 @previous_assigned_to_id = assigned_to_id_was
1763 end
1769 end
1764
1770
1765 # Clears the previous assignee at the end of after_save callbacks
1771 # Clears the previous assignee at the end of after_save callbacks
1766 def clear_assigned_to_was
1772 def clear_assigned_to_was
1767 @assigned_to_was = nil
1773 @assigned_to_was = nil
1768 @previous_assigned_to_id = nil
1774 @previous_assigned_to_id = nil
1769 end
1775 end
1770
1776
1771 def clear_disabled_fields
1777 def clear_disabled_fields
1772 if tracker
1778 if tracker
1773 tracker.disabled_core_fields.each do |attribute|
1779 tracker.disabled_core_fields.each do |attribute|
1774 send "#{attribute}=", nil
1780 send "#{attribute}=", nil
1775 end
1781 end
1776 self.done_ratio ||= 0
1782 self.done_ratio ||= 0
1777 end
1783 end
1778 end
1784 end
1779 end
1785 end
General Comments 0
You need to be logged in to leave comments. Login now