##// END OF EJS Templates
Moves watchers filtering logic to ActsAsWatchable....
Jean-Philippe Lang -
r3054:6610bb6b6cbb
parent child
Show More
@@ -1,452 +1,445
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_attachable :after_remove => :attachment_removed
35 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_customizable
36 acts_as_customizable
37 acts_as_watchable
37 acts_as_watchable
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 :include => [:project, :journals],
39 :include => [:project, :journals],
40 # sort by id so that limited eager loading doesn't break with postgresql
40 # sort by id so that limited eager loading doesn't break with postgresql
41 :order_column => "#{table_name}.id"
41 :order_column => "#{table_name}.id"
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45
45
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 :author_key => :author_id
47 :author_key => :author_id
48
48
49 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
49 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
50
50
51 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
51 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
52 validates_length_of :subject, :maximum => 255
52 validates_length_of :subject, :maximum => 255
53 validates_inclusion_of :done_ratio, :in => 0..100
53 validates_inclusion_of :done_ratio, :in => 0..100
54 validates_numericality_of :estimated_hours, :allow_nil => true
54 validates_numericality_of :estimated_hours, :allow_nil => true
55
55
56 named_scope :visible, lambda {|*args| { :include => :project,
56 named_scope :visible, lambda {|*args| { :include => :project,
57 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
57 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
58
58
59 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
59 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
60
60
61 before_save :update_done_ratio_from_issue_status
61 before_save :update_done_ratio_from_issue_status
62 after_save :create_journal
62 after_save :create_journal
63
63
64 # Returns true if usr or current user is allowed to view the issue
64 # Returns true if usr or current user is allowed to view the issue
65 def visible?(usr=nil)
65 def visible?(usr=nil)
66 (usr || User.current).allowed_to?(:view_issues, self.project)
66 (usr || User.current).allowed_to?(:view_issues, self.project)
67 end
67 end
68
68
69 def after_initialize
69 def after_initialize
70 if new_record?
70 if new_record?
71 # set default values for new records only
71 # set default values for new records only
72 self.status ||= IssueStatus.default
72 self.status ||= IssueStatus.default
73 self.priority ||= IssuePriority.default
73 self.priority ||= IssuePriority.default
74 end
74 end
75 end
75 end
76
76
77 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
77 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
78 def available_custom_fields
78 def available_custom_fields
79 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
79 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
80 end
80 end
81
81
82 def copy_from(arg)
82 def copy_from(arg)
83 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
83 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
84 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
84 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
85 self.custom_values = issue.custom_values.collect {|v| v.clone}
85 self.custom_values = issue.custom_values.collect {|v| v.clone}
86 self.status = issue.status
86 self.status = issue.status
87 self
87 self
88 end
88 end
89
89
90 # Moves/copies an issue to a new project and tracker
90 # Moves/copies an issue to a new project and tracker
91 # Returns the moved/copied issue on success, false on failure
91 # Returns the moved/copied issue on success, false on failure
92 def move_to(new_project, new_tracker = nil, options = {})
92 def move_to(new_project, new_tracker = nil, options = {})
93 options ||= {}
93 options ||= {}
94 issue = options[:copy] ? self.clone : self
94 issue = options[:copy] ? self.clone : self
95 transaction do
95 transaction do
96 if new_project && issue.project_id != new_project.id
96 if new_project && issue.project_id != new_project.id
97 # delete issue relations
97 # delete issue relations
98 unless Setting.cross_project_issue_relations?
98 unless Setting.cross_project_issue_relations?
99 issue.relations_from.clear
99 issue.relations_from.clear
100 issue.relations_to.clear
100 issue.relations_to.clear
101 end
101 end
102 # issue is moved to another project
102 # issue is moved to another project
103 # reassign to the category with same name if any
103 # reassign to the category with same name if any
104 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
104 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
105 issue.category = new_category
105 issue.category = new_category
106 # Keep the fixed_version if it's still valid in the new_project
106 # Keep the fixed_version if it's still valid in the new_project
107 unless new_project.shared_versions.include?(issue.fixed_version)
107 unless new_project.shared_versions.include?(issue.fixed_version)
108 issue.fixed_version = nil
108 issue.fixed_version = nil
109 end
109 end
110 issue.project = new_project
110 issue.project = new_project
111 end
111 end
112 if new_tracker
112 if new_tracker
113 issue.tracker = new_tracker
113 issue.tracker = new_tracker
114 end
114 end
115 if options[:copy]
115 if options[:copy]
116 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
116 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
117 issue.status = if options[:attributes] && options[:attributes][:status_id]
117 issue.status = if options[:attributes] && options[:attributes][:status_id]
118 IssueStatus.find_by_id(options[:attributes][:status_id])
118 IssueStatus.find_by_id(options[:attributes][:status_id])
119 else
119 else
120 self.status
120 self.status
121 end
121 end
122 end
122 end
123 # Allow bulk setting of attributes on the issue
123 # Allow bulk setting of attributes on the issue
124 if options[:attributes]
124 if options[:attributes]
125 issue.attributes = options[:attributes]
125 issue.attributes = options[:attributes]
126 end
126 end
127 if issue.save
127 if issue.save
128 unless options[:copy]
128 unless options[:copy]
129 # Manually update project_id on related time entries
129 # Manually update project_id on related time entries
130 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
130 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
131 end
131 end
132 else
132 else
133 Issue.connection.rollback_db_transaction
133 Issue.connection.rollback_db_transaction
134 return false
134 return false
135 end
135 end
136 end
136 end
137 return issue
137 return issue
138 end
138 end
139
139
140 def priority_id=(pid)
140 def priority_id=(pid)
141 self.priority = nil
141 self.priority = nil
142 write_attribute(:priority_id, pid)
142 write_attribute(:priority_id, pid)
143 end
143 end
144
144
145 def tracker_id=(tid)
145 def tracker_id=(tid)
146 self.tracker = nil
146 self.tracker = nil
147 write_attribute(:tracker_id, tid)
147 write_attribute(:tracker_id, tid)
148 result = write_attribute(:tracker_id, tid)
148 result = write_attribute(:tracker_id, tid)
149 @custom_field_values = nil
149 @custom_field_values = nil
150 result
150 result
151 end
151 end
152
152
153 # Overrides attributes= so that tracker_id gets assigned first
153 # Overrides attributes= so that tracker_id gets assigned first
154 def attributes_with_tracker_first=(new_attributes, *args)
154 def attributes_with_tracker_first=(new_attributes, *args)
155 return if new_attributes.nil?
155 return if new_attributes.nil?
156 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
156 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
157 if new_tracker_id
157 if new_tracker_id
158 self.tracker_id = new_tracker_id
158 self.tracker_id = new_tracker_id
159 end
159 end
160 self.attributes_without_tracker_first = new_attributes, *args
160 self.attributes_without_tracker_first = new_attributes, *args
161 end
161 end
162 alias_method_chain :attributes=, :tracker_first
162 alias_method_chain :attributes=, :tracker_first
163
163
164 def estimated_hours=(h)
164 def estimated_hours=(h)
165 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
165 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
166 end
166 end
167
167
168 def done_ratio
168 def done_ratio
169 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
169 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
170 status.default_done_ratio
170 status.default_done_ratio
171 else
171 else
172 read_attribute(:done_ratio)
172 read_attribute(:done_ratio)
173 end
173 end
174 end
174 end
175
175
176 def self.use_status_for_done_ratio?
176 def self.use_status_for_done_ratio?
177 Setting.issue_done_ratio == 'issue_status'
177 Setting.issue_done_ratio == 'issue_status'
178 end
178 end
179
179
180 def self.use_field_for_done_ratio?
180 def self.use_field_for_done_ratio?
181 Setting.issue_done_ratio == 'issue_field'
181 Setting.issue_done_ratio == 'issue_field'
182 end
182 end
183
183
184 def validate
184 def validate
185 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
185 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
186 errors.add :due_date, :not_a_date
186 errors.add :due_date, :not_a_date
187 end
187 end
188
188
189 if self.due_date and self.start_date and self.due_date < self.start_date
189 if self.due_date and self.start_date and self.due_date < self.start_date
190 errors.add :due_date, :greater_than_start_date
190 errors.add :due_date, :greater_than_start_date
191 end
191 end
192
192
193 if start_date && soonest_start && start_date < soonest_start
193 if start_date && soonest_start && start_date < soonest_start
194 errors.add :start_date, :invalid
194 errors.add :start_date, :invalid
195 end
195 end
196
196
197 if fixed_version
197 if fixed_version
198 if !assignable_versions.include?(fixed_version)
198 if !assignable_versions.include?(fixed_version)
199 errors.add :fixed_version_id, :inclusion
199 errors.add :fixed_version_id, :inclusion
200 elsif reopened? && fixed_version.closed?
200 elsif reopened? && fixed_version.closed?
201 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
201 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
202 end
202 end
203 end
203 end
204
204
205 # Checks that the issue can not be added/moved to a disabled tracker
205 # Checks that the issue can not be added/moved to a disabled tracker
206 if project && (tracker_id_changed? || project_id_changed?)
206 if project && (tracker_id_changed? || project_id_changed?)
207 unless project.trackers.include?(tracker)
207 unless project.trackers.include?(tracker)
208 errors.add :tracker_id, :inclusion
208 errors.add :tracker_id, :inclusion
209 end
209 end
210 end
210 end
211 end
211 end
212
212
213 def before_create
213 def before_create
214 # default assignment based on category
214 # default assignment based on category
215 if assigned_to.nil? && category && category.assigned_to
215 if assigned_to.nil? && category && category.assigned_to
216 self.assigned_to = category.assigned_to
216 self.assigned_to = category.assigned_to
217 end
217 end
218 end
218 end
219
219
220 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
220 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
221 # even if the user turns off the setting later
221 # even if the user turns off the setting later
222 def update_done_ratio_from_issue_status
222 def update_done_ratio_from_issue_status
223 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
223 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
224 self.done_ratio = status.default_done_ratio
224 self.done_ratio = status.default_done_ratio
225 end
225 end
226 end
226 end
227
227
228 def after_save
228 def after_save
229 # Reload is needed in order to get the right status
229 # Reload is needed in order to get the right status
230 reload
230 reload
231
231
232 # Update start/due dates of following issues
232 # Update start/due dates of following issues
233 relations_from.each(&:set_issue_to_dates)
233 relations_from.each(&:set_issue_to_dates)
234
234
235 # Close duplicates if the issue was closed
235 # Close duplicates if the issue was closed
236 if @issue_before_change && !@issue_before_change.closed? && self.closed?
236 if @issue_before_change && !@issue_before_change.closed? && self.closed?
237 duplicates.each do |duplicate|
237 duplicates.each do |duplicate|
238 # Reload is need in case the duplicate was updated by a previous duplicate
238 # Reload is need in case the duplicate was updated by a previous duplicate
239 duplicate.reload
239 duplicate.reload
240 # Don't re-close it if it's already closed
240 # Don't re-close it if it's already closed
241 next if duplicate.closed?
241 next if duplicate.closed?
242 # Same user and notes
242 # Same user and notes
243 duplicate.init_journal(@current_journal.user, @current_journal.notes)
243 duplicate.init_journal(@current_journal.user, @current_journal.notes)
244 duplicate.update_attribute :status, self.status
244 duplicate.update_attribute :status, self.status
245 end
245 end
246 end
246 end
247 end
247 end
248
248
249 def init_journal(user, notes = "")
249 def init_journal(user, notes = "")
250 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
250 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
251 @issue_before_change = self.clone
251 @issue_before_change = self.clone
252 @issue_before_change.status = self.status
252 @issue_before_change.status = self.status
253 @custom_values_before_change = {}
253 @custom_values_before_change = {}
254 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
254 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
255 # Make sure updated_on is updated when adding a note.
255 # Make sure updated_on is updated when adding a note.
256 updated_on_will_change!
256 updated_on_will_change!
257 @current_journal
257 @current_journal
258 end
258 end
259
259
260 # Return true if the issue is closed, otherwise false
260 # Return true if the issue is closed, otherwise false
261 def closed?
261 def closed?
262 self.status.is_closed?
262 self.status.is_closed?
263 end
263 end
264
264
265 # Return true if the issue is being reopened
265 # Return true if the issue is being reopened
266 def reopened?
266 def reopened?
267 if !new_record? && status_id_changed?
267 if !new_record? && status_id_changed?
268 status_was = IssueStatus.find_by_id(status_id_was)
268 status_was = IssueStatus.find_by_id(status_id_was)
269 status_new = IssueStatus.find_by_id(status_id)
269 status_new = IssueStatus.find_by_id(status_id)
270 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
270 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
271 return true
271 return true
272 end
272 end
273 end
273 end
274 false
274 false
275 end
275 end
276
276
277 # Returns true if the issue is overdue
277 # Returns true if the issue is overdue
278 def overdue?
278 def overdue?
279 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
279 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
280 end
280 end
281
281
282 # Users the issue can be assigned to
282 # Users the issue can be assigned to
283 def assignable_users
283 def assignable_users
284 project.assignable_users
284 project.assignable_users
285 end
285 end
286
286
287 # Versions that the issue can be assigned to
287 # Versions that the issue can be assigned to
288 def assignable_versions
288 def assignable_versions
289 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
289 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
290 end
290 end
291
291
292 # Returns true if this issue is blocked by another issue that is still open
292 # Returns true if this issue is blocked by another issue that is still open
293 def blocked?
293 def blocked?
294 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
294 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
295 end
295 end
296
296
297 # Returns an array of status that user is able to apply
297 # Returns an array of status that user is able to apply
298 def new_statuses_allowed_to(user)
298 def new_statuses_allowed_to(user)
299 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
299 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
300 statuses << status unless statuses.empty?
300 statuses << status unless statuses.empty?
301 statuses = statuses.uniq.sort
301 statuses = statuses.uniq.sort
302 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
302 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
303 end
303 end
304
304
305 # Returns the mail adresses of users that should be notified
305 # Returns the mail adresses of users that should be notified
306 def recipients
306 def recipients
307 notified = project.notified_users
307 notified = project.notified_users
308 # Author and assignee are always notified unless they have been locked
308 # Author and assignee are always notified unless they have been locked
309 notified << author if author && author.active?
309 notified << author if author && author.active?
310 notified << assigned_to if assigned_to && assigned_to.active?
310 notified << assigned_to if assigned_to && assigned_to.active?
311 notified.uniq!
311 notified.uniq!
312 # Remove users that can not view the issue
312 # Remove users that can not view the issue
313 notified.reject! {|user| !visible?(user)}
313 notified.reject! {|user| !visible?(user)}
314 notified.collect(&:mail)
314 notified.collect(&:mail)
315 end
315 end
316
316
317 # Returns the mail adresses of watchers that should be notified
318 def watcher_recipients
319 notified = watcher_users
320 notified.reject! {|user| !user.active? || !visible?(user)}
321 notified.collect(&:mail)
322 end
323
324 # Returns the total number of hours spent on this issue.
317 # Returns the total number of hours spent on this issue.
325 #
318 #
326 # Example:
319 # Example:
327 # spent_hours => 0
320 # spent_hours => 0
328 # spent_hours => 50
321 # spent_hours => 50
329 def spent_hours
322 def spent_hours
330 @spent_hours ||= time_entries.sum(:hours) || 0
323 @spent_hours ||= time_entries.sum(:hours) || 0
331 end
324 end
332
325
333 def relations
326 def relations
334 (relations_from + relations_to).sort
327 (relations_from + relations_to).sort
335 end
328 end
336
329
337 def all_dependent_issues
330 def all_dependent_issues
338 dependencies = []
331 dependencies = []
339 relations_from.each do |relation|
332 relations_from.each do |relation|
340 dependencies << relation.issue_to
333 dependencies << relation.issue_to
341 dependencies += relation.issue_to.all_dependent_issues
334 dependencies += relation.issue_to.all_dependent_issues
342 end
335 end
343 dependencies
336 dependencies
344 end
337 end
345
338
346 # Returns an array of issues that duplicate this one
339 # Returns an array of issues that duplicate this one
347 def duplicates
340 def duplicates
348 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
341 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
349 end
342 end
350
343
351 # Returns the due date or the target due date if any
344 # Returns the due date or the target due date if any
352 # Used on gantt chart
345 # Used on gantt chart
353 def due_before
346 def due_before
354 due_date || (fixed_version ? fixed_version.effective_date : nil)
347 due_date || (fixed_version ? fixed_version.effective_date : nil)
355 end
348 end
356
349
357 # Returns the time scheduled for this issue.
350 # Returns the time scheduled for this issue.
358 #
351 #
359 # Example:
352 # Example:
360 # Start Date: 2/26/09, End Date: 3/04/09
353 # Start Date: 2/26/09, End Date: 3/04/09
361 # duration => 6
354 # duration => 6
362 def duration
355 def duration
363 (start_date && due_date) ? due_date - start_date : 0
356 (start_date && due_date) ? due_date - start_date : 0
364 end
357 end
365
358
366 def soonest_start
359 def soonest_start
367 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
360 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
368 end
361 end
369
362
370 def to_s
363 def to_s
371 "#{tracker} ##{id}: #{subject}"
364 "#{tracker} ##{id}: #{subject}"
372 end
365 end
373
366
374 # Returns a string of css classes that apply to the issue
367 # Returns a string of css classes that apply to the issue
375 def css_classes
368 def css_classes
376 s = "issue status-#{status.position} priority-#{priority.position}"
369 s = "issue status-#{status.position} priority-#{priority.position}"
377 s << ' closed' if closed?
370 s << ' closed' if closed?
378 s << ' overdue' if overdue?
371 s << ' overdue' if overdue?
379 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
372 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
380 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
373 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
381 s
374 s
382 end
375 end
383
376
384 # Unassigns issues from +version+ if it's no longer shared with issue's project
377 # Unassigns issues from +version+ if it's no longer shared with issue's project
385 def self.update_versions_from_sharing_change(version)
378 def self.update_versions_from_sharing_change(version)
386 # Update issues assigned to the version
379 # Update issues assigned to the version
387 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
380 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
388 end
381 end
389
382
390 # Unassigns issues from versions that are no longer shared
383 # Unassigns issues from versions that are no longer shared
391 # after +project+ was moved
384 # after +project+ was moved
392 def self.update_versions_from_hierarchy_change(project)
385 def self.update_versions_from_hierarchy_change(project)
393 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
386 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
394 # Update issues of the moved projects and issues assigned to a version of a moved project
387 # Update issues of the moved projects and issues assigned to a version of a moved project
395 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
388 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
396 end
389 end
397
390
398 private
391 private
399
392
400 # Update issues so their versions are not pointing to a
393 # Update issues so their versions are not pointing to a
401 # fixed_version that is not shared with the issue's project
394 # fixed_version that is not shared with the issue's project
402 def self.update_versions(conditions=nil)
395 def self.update_versions(conditions=nil)
403 # Only need to update issues with a fixed_version from
396 # Only need to update issues with a fixed_version from
404 # a different project and that is not systemwide shared
397 # a different project and that is not systemwide shared
405 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
398 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
406 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
399 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
407 " AND #{Version.table_name}.sharing <> 'system'",
400 " AND #{Version.table_name}.sharing <> 'system'",
408 conditions),
401 conditions),
409 :include => [:project, :fixed_version]
402 :include => [:project, :fixed_version]
410 ).each do |issue|
403 ).each do |issue|
411 next if issue.project.nil? || issue.fixed_version.nil?
404 next if issue.project.nil? || issue.fixed_version.nil?
412 unless issue.project.shared_versions.include?(issue.fixed_version)
405 unless issue.project.shared_versions.include?(issue.fixed_version)
413 issue.init_journal(User.current)
406 issue.init_journal(User.current)
414 issue.fixed_version = nil
407 issue.fixed_version = nil
415 issue.save
408 issue.save
416 end
409 end
417 end
410 end
418 end
411 end
419
412
420 # Callback on attachment deletion
413 # Callback on attachment deletion
421 def attachment_removed(obj)
414 def attachment_removed(obj)
422 journal = init_journal(User.current)
415 journal = init_journal(User.current)
423 journal.details << JournalDetail.new(:property => 'attachment',
416 journal.details << JournalDetail.new(:property => 'attachment',
424 :prop_key => obj.id,
417 :prop_key => obj.id,
425 :old_value => obj.filename)
418 :old_value => obj.filename)
426 journal.save
419 journal.save
427 end
420 end
428
421
429 # Saves the changes in a Journal
422 # Saves the changes in a Journal
430 # Called after_save
423 # Called after_save
431 def create_journal
424 def create_journal
432 if @current_journal
425 if @current_journal
433 # attributes changes
426 # attributes changes
434 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
427 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
435 @current_journal.details << JournalDetail.new(:property => 'attr',
428 @current_journal.details << JournalDetail.new(:property => 'attr',
436 :prop_key => c,
429 :prop_key => c,
437 :old_value => @issue_before_change.send(c),
430 :old_value => @issue_before_change.send(c),
438 :value => send(c)) unless send(c)==@issue_before_change.send(c)
431 :value => send(c)) unless send(c)==@issue_before_change.send(c)
439 }
432 }
440 # custom fields changes
433 # custom fields changes
441 custom_values.each {|c|
434 custom_values.each {|c|
442 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
435 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
443 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
436 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
444 @current_journal.details << JournalDetail.new(:property => 'cf',
437 @current_journal.details << JournalDetail.new(:property => 'cf',
445 :prop_key => c.custom_field_id,
438 :prop_key => c.custom_field_id,
446 :old_value => @custom_values_before_change[c.custom_field_id],
439 :old_value => @custom_values_before_change[c.custom_field_id],
447 :value => c.value)
440 :value => c.value)
448 }
441 }
449 @current_journal.save
442 @current_journal.save
450 end
443 end
451 end
444 end
452 end
445 end
@@ -1,70 +1,74
1 # ActsAsWatchable
1 # ActsAsWatchable
2 module Redmine
2 module Redmine
3 module Acts
3 module Acts
4 module Watchable
4 module Watchable
5 def self.included(base)
5 def self.included(base)
6 base.extend ClassMethods
6 base.extend ClassMethods
7 end
7 end
8
8
9 module ClassMethods
9 module ClassMethods
10 def acts_as_watchable(options = {})
10 def acts_as_watchable(options = {})
11 return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods)
11 return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods)
12 send :include, Redmine::Acts::Watchable::InstanceMethods
12 send :include, Redmine::Acts::Watchable::InstanceMethods
13
13
14 class_eval do
14 class_eval do
15 has_many :watchers, :as => :watchable, :dependent => :delete_all
15 has_many :watchers, :as => :watchable, :dependent => :delete_all
16 has_many :watcher_users, :through => :watchers, :source => :user
16 has_many :watcher_users, :through => :watchers, :source => :user
17
17
18 attr_protected :watcher_ids, :watcher_user_ids
18 attr_protected :watcher_ids, :watcher_user_ids
19 end
19 end
20 end
20 end
21 end
21 end
22
22
23 module InstanceMethods
23 module InstanceMethods
24 def self.included(base)
24 def self.included(base)
25 base.extend ClassMethods
25 base.extend ClassMethods
26 end
26 end
27
27
28 # Returns an array of users that are proposed as watchers
28 # Returns an array of users that are proposed as watchers
29 def addable_watcher_users
29 def addable_watcher_users
30 self.project.users.sort - self.watcher_users
30 self.project.users.sort - self.watcher_users
31 end
31 end
32
32
33 # Adds user as a watcher
33 # Adds user as a watcher
34 def add_watcher(user)
34 def add_watcher(user)
35 self.watchers << Watcher.new(:user => user)
35 self.watchers << Watcher.new(:user => user)
36 end
36 end
37
37
38 # Removes user from the watchers list
38 # Removes user from the watchers list
39 def remove_watcher(user)
39 def remove_watcher(user)
40 return nil unless user && user.is_a?(User)
40 return nil unless user && user.is_a?(User)
41 Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
41 Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
42 end
42 end
43
43
44 # Adds/removes watcher
44 # Adds/removes watcher
45 def set_watcher(user, watching=true)
45 def set_watcher(user, watching=true)
46 watching ? add_watcher(user) : remove_watcher(user)
46 watching ? add_watcher(user) : remove_watcher(user)
47 end
47 end
48
48
49 # Returns true if object is watched by user
49 # Returns true if object is watched by user
50 def watched_by?(user)
50 def watched_by?(user)
51 !!(user && self.watchers.detect {|w| w.user_id == user.id })
51 !!(user && self.watchers.detect {|w| w.user_id == user.id })
52 end
52 end
53
53
54 # Returns an array of watchers' email addresses
54 # Returns an array of watchers' email addresses
55 def watcher_recipients
55 def watcher_recipients
56 self.watchers.collect { |w| w.user.mail if w.user.active? }.compact
56 notified = watchers.collect(&:user).select(&:active?)
57 if respond_to?(:visible?)
58 notified.reject! {|user| !visible?(user)}
59 end
60 notified.collect(&:mail).compact
57 end
61 end
58
62
59 module ClassMethods
63 module ClassMethods
60 # Returns the objects that are watched by user
64 # Returns the objects that are watched by user
61 def watched_by(user)
65 def watched_by(user)
62 find(:all,
66 find(:all,
63 :include => :watchers,
67 :include => :watchers,
64 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id])
68 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id])
65 end
69 end
66 end
70 end
67 end
71 end
68 end
72 end
69 end
73 end
70 end
74 end
General Comments 0
You need to be logged in to leave comments. Login now