##// END OF EJS Templates
Fixed: locked users should not receive email notifications....
Jean-Philippe Lang -
r1061:3e031b4243b3
parent child
Show More
@@ -1,228 +1,228
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 => 'Enumeration', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'Enumeration', :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 :attachments, :as => :container, :dependent => :destroy
29 has_many :attachments, :as => :container, :dependent => :destroy
30 has_many :time_entries, :dependent => :nullify
30 has_many :time_entries, :dependent => :nullify
31 has_many :custom_values, :dependent => :delete_all, :as => :customized
31 has_many :custom_values, :dependent => :delete_all, :as => :customized
32 has_many :custom_fields, :through => :custom_values
32 has_many :custom_fields, :through => :custom_values
33 has_and_belongs_to_many :changesets, :order => "revision ASC"
33 has_and_belongs_to_many :changesets, :order => "revision ASC"
34
34
35 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
36 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
37
37
38 acts_as_watchable
38 acts_as_watchable
39 acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
39 acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
40 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
40 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
41 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
41 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
42
42
43 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
43 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
44 validates_length_of :subject, :maximum => 255
44 validates_length_of :subject, :maximum => 255
45 validates_inclusion_of :done_ratio, :in => 0..100
45 validates_inclusion_of :done_ratio, :in => 0..100
46 validates_numericality_of :estimated_hours, :allow_nil => true
46 validates_numericality_of :estimated_hours, :allow_nil => true
47 validates_associated :custom_values, :on => :update
47 validates_associated :custom_values, :on => :update
48
48
49 def after_initialize
49 def after_initialize
50 if new_record?
50 if new_record?
51 # set default values for new records only
51 # set default values for new records only
52 self.status ||= IssueStatus.default
52 self.status ||= IssueStatus.default
53 self.priority ||= Enumeration.default('IPRI')
53 self.priority ||= Enumeration.default('IPRI')
54 end
54 end
55 end
55 end
56
56
57 def copy_from(arg)
57 def copy_from(arg)
58 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
58 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
59 self.attributes = issue.attributes.dup
59 self.attributes = issue.attributes.dup
60 self.custom_values = issue.custom_values.collect {|v| v.clone}
60 self.custom_values = issue.custom_values.collect {|v| v.clone}
61 self
61 self
62 end
62 end
63
63
64 # Move an issue to a new project and tracker
64 # Move an issue to a new project and tracker
65 def move_to(new_project, new_tracker = nil)
65 def move_to(new_project, new_tracker = nil)
66 transaction do
66 transaction do
67 if new_project && project_id != new_project.id
67 if new_project && project_id != new_project.id
68 # delete issue relations
68 # delete issue relations
69 self.relations_from.clear
69 self.relations_from.clear
70 self.relations_to.clear
70 self.relations_to.clear
71 # issue is moved to another project
71 # issue is moved to another project
72 self.category = nil
72 self.category = nil
73 self.fixed_version = nil
73 self.fixed_version = nil
74 self.project = new_project
74 self.project = new_project
75 end
75 end
76 if new_tracker
76 if new_tracker
77 self.tracker = new_tracker
77 self.tracker = new_tracker
78 end
78 end
79 if save
79 if save
80 # Manually update project_id on related time entries
80 # Manually update project_id on related time entries
81 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
81 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
82 else
82 else
83 rollback_db_transaction
83 rollback_db_transaction
84 return false
84 return false
85 end
85 end
86 end
86 end
87 return true
87 return true
88 end
88 end
89
89
90 def priority_id=(pid)
90 def priority_id=(pid)
91 self.priority = nil
91 self.priority = nil
92 write_attribute(:priority_id, pid)
92 write_attribute(:priority_id, pid)
93 end
93 end
94
94
95 def validate
95 def validate
96 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
96 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
97 errors.add :due_date, :activerecord_error_not_a_date
97 errors.add :due_date, :activerecord_error_not_a_date
98 end
98 end
99
99
100 if self.due_date and self.start_date and self.due_date < self.start_date
100 if self.due_date and self.start_date and self.due_date < self.start_date
101 errors.add :due_date, :activerecord_error_greater_than_start_date
101 errors.add :due_date, :activerecord_error_greater_than_start_date
102 end
102 end
103
103
104 if start_date && soonest_start && start_date < soonest_start
104 if start_date && soonest_start && start_date < soonest_start
105 errors.add :start_date, :activerecord_error_invalid
105 errors.add :start_date, :activerecord_error_invalid
106 end
106 end
107 end
107 end
108
108
109 def validate_on_create
109 def validate_on_create
110 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
110 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
111 end
111 end
112
112
113 def before_create
113 def before_create
114 # default assignment based on category
114 # default assignment based on category
115 if assigned_to.nil? && category && category.assigned_to
115 if assigned_to.nil? && category && category.assigned_to
116 self.assigned_to = category.assigned_to
116 self.assigned_to = category.assigned_to
117 end
117 end
118 end
118 end
119
119
120 def before_save
120 def before_save
121 if @current_journal
121 if @current_journal
122 # attributes changes
122 # attributes changes
123 (Issue.column_names - %w(id description)).each {|c|
123 (Issue.column_names - %w(id description)).each {|c|
124 @current_journal.details << JournalDetail.new(:property => 'attr',
124 @current_journal.details << JournalDetail.new(:property => 'attr',
125 :prop_key => c,
125 :prop_key => c,
126 :old_value => @issue_before_change.send(c),
126 :old_value => @issue_before_change.send(c),
127 :value => send(c)) unless send(c)==@issue_before_change.send(c)
127 :value => send(c)) unless send(c)==@issue_before_change.send(c)
128 }
128 }
129 # custom fields changes
129 # custom fields changes
130 custom_values.each {|c|
130 custom_values.each {|c|
131 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
131 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
132 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
132 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
133 @current_journal.details << JournalDetail.new(:property => 'cf',
133 @current_journal.details << JournalDetail.new(:property => 'cf',
134 :prop_key => c.custom_field_id,
134 :prop_key => c.custom_field_id,
135 :old_value => @custom_values_before_change[c.custom_field_id],
135 :old_value => @custom_values_before_change[c.custom_field_id],
136 :value => c.value)
136 :value => c.value)
137 }
137 }
138 @current_journal.save
138 @current_journal.save
139 end
139 end
140 # Save the issue even if the journal is not saved (because empty)
140 # Save the issue even if the journal is not saved (because empty)
141 true
141 true
142 end
142 end
143
143
144 def after_save
144 def after_save
145 # Update start/due dates of following issues
145 # Update start/due dates of following issues
146 relations_from.each(&:set_issue_to_dates)
146 relations_from.each(&:set_issue_to_dates)
147
147
148 # Close duplicates if the issue was closed
148 # Close duplicates if the issue was closed
149 if @issue_before_change && !@issue_before_change.closed? && self.closed?
149 if @issue_before_change && !@issue_before_change.closed? && self.closed?
150 duplicates.each do |duplicate|
150 duplicates.each do |duplicate|
151 # Don't re-close it if it's already closed
151 # Don't re-close it if it's already closed
152 next if duplicate.closed?
152 next if duplicate.closed?
153 # Same user and notes
153 # Same user and notes
154 duplicate.init_journal(@current_journal.user, @current_journal.notes)
154 duplicate.init_journal(@current_journal.user, @current_journal.notes)
155 duplicate.update_attribute :status, self.status
155 duplicate.update_attribute :status, self.status
156 end
156 end
157 end
157 end
158 end
158 end
159
159
160 def custom_value_for(custom_field)
160 def custom_value_for(custom_field)
161 self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
161 self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
162 return nil
162 return nil
163 end
163 end
164
164
165 def init_journal(user, notes = "")
165 def init_journal(user, notes = "")
166 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
166 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
167 @issue_before_change = self.clone
167 @issue_before_change = self.clone
168 @custom_values_before_change = {}
168 @custom_values_before_change = {}
169 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
169 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
170 @current_journal
170 @current_journal
171 end
171 end
172
172
173 # Return true if the issue is closed, otherwise false
173 # Return true if the issue is closed, otherwise false
174 def closed?
174 def closed?
175 self.status.is_closed?
175 self.status.is_closed?
176 end
176 end
177
177
178 # Users the issue can be assigned to
178 # Users the issue can be assigned to
179 def assignable_users
179 def assignable_users
180 project.assignable_users
180 project.assignable_users
181 end
181 end
182
182
183 # Returns an array of status that user is able to apply
183 # Returns an array of status that user is able to apply
184 def new_statuses_allowed_to(user)
184 def new_statuses_allowed_to(user)
185 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
185 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
186 statuses << status unless statuses.empty?
186 statuses << status unless statuses.empty?
187 statuses.uniq.sort
187 statuses.uniq.sort
188 end
188 end
189
189
190 # Returns the mail adresses of users that should be notified for the issue
190 # Returns the mail adresses of users that should be notified for the issue
191 def recipients
191 def recipients
192 recipients = project.recipients
192 recipients = project.recipients
193 # Author and assignee are always notified
193 # Author and assignee are always notified unless they have been locked
194 recipients << author.mail if author
194 recipients << author.mail if author && author.active?
195 recipients << assigned_to.mail if assigned_to
195 recipients << assigned_to.mail if assigned_to && assigned_to.active?
196 recipients.compact.uniq
196 recipients.compact.uniq
197 end
197 end
198
198
199 def spent_hours
199 def spent_hours
200 @spent_hours ||= time_entries.sum(:hours) || 0
200 @spent_hours ||= time_entries.sum(:hours) || 0
201 end
201 end
202
202
203 def relations
203 def relations
204 (relations_from + relations_to).sort
204 (relations_from + relations_to).sort
205 end
205 end
206
206
207 def all_dependent_issues
207 def all_dependent_issues
208 dependencies = []
208 dependencies = []
209 relations_from.each do |relation|
209 relations_from.each do |relation|
210 dependencies << relation.issue_to
210 dependencies << relation.issue_to
211 dependencies += relation.issue_to.all_dependent_issues
211 dependencies += relation.issue_to.all_dependent_issues
212 end
212 end
213 dependencies
213 dependencies
214 end
214 end
215
215
216 # Returns an array of the duplicate issues
216 # Returns an array of the duplicate issues
217 def duplicates
217 def duplicates
218 relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
218 relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
219 end
219 end
220
220
221 def duration
221 def duration
222 (start_date && due_date) ? due_date - start_date : 0
222 (start_date && due_date) ? due_date - start_date : 0
223 end
223 end
224
224
225 def soonest_start
225 def soonest_start
226 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
226 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
227 end
227 end
228 end
228 end
@@ -1,27 +1,27
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 MessageObserver < ActiveRecord::Observer
18 class MessageObserver < ActiveRecord::Observer
19 def after_create(message)
19 def after_create(message)
20 # send notification to the authors of the thread
20 # send notification to the authors of the thread
21 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author}
21 recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author && m.author.active?}
22 # send notification to the board watchers
22 # send notification to the board watchers
23 recipients += message.board.watcher_recipients
23 recipients += message.board.watcher_recipients
24 recipients = recipients.compact.uniq
24 recipients = recipients.compact.uniq
25 Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted')
25 Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted')
26 end
26 end
27 end
27 end
@@ -1,53 +1,53
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 end
16 end
17 end
17 end
18 end
18 end
19
19
20 module InstanceMethods
20 module InstanceMethods
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 def add_watcher(user)
25 def add_watcher(user)
26 self.watchers << Watcher.new(:user => user)
26 self.watchers << Watcher.new(:user => user)
27 end
27 end
28
28
29 def remove_watcher(user)
29 def remove_watcher(user)
30 return nil unless user && user.is_a?(User)
30 return nil unless user && user.is_a?(User)
31 Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
31 Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
32 end
32 end
33
33
34 def watched_by?(user)
34 def watched_by?(user)
35 !self.watchers.find(:first,
35 !self.watchers.find(:first,
36 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id]).nil?
36 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id]).nil?
37 end
37 end
38
38
39 def watcher_recipients
39 def watcher_recipients
40 self.watchers.collect { |w| w.user.mail }
40 self.watchers.collect { |w| w.user.mail if w.user.active? }.compact
41 end
41 end
42
42
43 module ClassMethods
43 module ClassMethods
44 def watched_by(user)
44 def watched_by(user)
45 find(:all,
45 find(:all,
46 :include => :watchers,
46 :include => :watchers,
47 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id])
47 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id])
48 end
48 end
49 end
49 end
50 end
50 end
51 end
51 end
52 end
52 end
53 end No newline at end of file
53 end
General Comments 0
You need to be logged in to leave comments. Login now