##// END OF EJS Templates
Fixed: auto closing of duplicates doesn't work....
Jean-Philippe Lang -
r1148:652dc1a73af8
parent child
Show More
@@ -1,228 +1,232
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :tracker
21 21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26 26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27 27
28 28 has_many :journals, :as => :journalized, :dependent => :destroy
29 29 has_many :attachments, :as => :container, :dependent => :destroy
30 30 has_many :time_entries, :dependent => :nullify
31 31 has_many :custom_values, :dependent => :delete_all, :as => :customized
32 32 has_many :custom_fields, :through => :custom_values
33 33 has_and_belongs_to_many :changesets, :order => "revision ASC"
34 34
35 35 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
36 36 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
37 37
38 38 acts_as_watchable
39 39 acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
40 40 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
41 41 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
42 42
43 43 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
44 44 validates_length_of :subject, :maximum => 255
45 45 validates_inclusion_of :done_ratio, :in => 0..100
46 46 validates_numericality_of :estimated_hours, :allow_nil => true
47 47 validates_associated :custom_values, :on => :update
48 48
49 49 def after_initialize
50 50 if new_record?
51 51 # set default values for new records only
52 52 self.status ||= IssueStatus.default
53 53 self.priority ||= Enumeration.default('IPRI')
54 54 end
55 55 end
56 56
57 57 def copy_from(arg)
58 58 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
59 59 self.attributes = issue.attributes.dup
60 60 self.custom_values = issue.custom_values.collect {|v| v.clone}
61 61 self
62 62 end
63 63
64 64 # Move an issue to a new project and tracker
65 65 def move_to(new_project, new_tracker = nil)
66 66 transaction do
67 67 if new_project && project_id != new_project.id
68 68 # delete issue relations
69 69 self.relations_from.clear
70 70 self.relations_to.clear
71 71 # issue is moved to another project
72 72 self.category = nil
73 73 self.fixed_version = nil
74 74 self.project = new_project
75 75 end
76 76 if new_tracker
77 77 self.tracker = new_tracker
78 78 end
79 79 if save
80 80 # Manually update project_id on related time entries
81 81 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
82 82 else
83 83 rollback_db_transaction
84 84 return false
85 85 end
86 86 end
87 87 return true
88 88 end
89 89
90 90 def priority_id=(pid)
91 91 self.priority = nil
92 92 write_attribute(:priority_id, pid)
93 93 end
94 94
95 95 def validate
96 96 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
97 97 errors.add :due_date, :activerecord_error_not_a_date
98 98 end
99 99
100 100 if self.due_date and self.start_date and self.due_date < self.start_date
101 101 errors.add :due_date, :activerecord_error_greater_than_start_date
102 102 end
103 103
104 104 if start_date && soonest_start && start_date < soonest_start
105 105 errors.add :start_date, :activerecord_error_invalid
106 106 end
107 107 end
108 108
109 109 def validate_on_create
110 110 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
111 111 end
112 112
113 113 def before_create
114 114 # default assignment based on category
115 115 if assigned_to.nil? && category && category.assigned_to
116 116 self.assigned_to = category.assigned_to
117 117 end
118 118 end
119 119
120 120 def before_save
121 121 if @current_journal
122 122 # attributes changes
123 123 (Issue.column_names - %w(id description)).each {|c|
124 124 @current_journal.details << JournalDetail.new(:property => 'attr',
125 125 :prop_key => c,
126 126 :old_value => @issue_before_change.send(c),
127 127 :value => send(c)) unless send(c)==@issue_before_change.send(c)
128 128 }
129 129 # custom fields changes
130 130 custom_values.each {|c|
131 131 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
132 132 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
133 133 @current_journal.details << JournalDetail.new(:property => 'cf',
134 134 :prop_key => c.custom_field_id,
135 135 :old_value => @custom_values_before_change[c.custom_field_id],
136 136 :value => c.value)
137 137 }
138 138 @current_journal.save
139 139 end
140 140 # Save the issue even if the journal is not saved (because empty)
141 141 true
142 142 end
143 143
144 144 def after_save
145 # Reload is needed in order to get the right status
146 reload
147
145 148 # Update start/due dates of following issues
146 149 relations_from.each(&:set_issue_to_dates)
147 150
148 151 # Close duplicates if the issue was closed
149 152 if @issue_before_change && !@issue_before_change.closed? && self.closed?
150 153 duplicates.each do |duplicate|
151 154 # Don't re-close it if it's already closed
152 155 next if duplicate.closed?
153 156 # Same user and notes
154 157 duplicate.init_journal(@current_journal.user, @current_journal.notes)
155 158 duplicate.update_attribute :status, self.status
156 159 end
157 160 end
158 161 end
159 162
160 163 def custom_value_for(custom_field)
161 164 self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
162 165 return nil
163 166 end
164 167
165 168 def init_journal(user, notes = "")
166 169 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
167 170 @issue_before_change = self.clone
171 @issue_before_change.status = self.status
168 172 @custom_values_before_change = {}
169 173 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
170 174 @current_journal
171 175 end
172 176
173 177 # Return true if the issue is closed, otherwise false
174 178 def closed?
175 179 self.status.is_closed?
176 180 end
177 181
178 182 # Users the issue can be assigned to
179 183 def assignable_users
180 184 project.assignable_users
181 185 end
182 186
183 187 # Returns an array of status that user is able to apply
184 188 def new_statuses_allowed_to(user)
185 189 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
186 190 statuses << status unless statuses.empty?
187 191 statuses.uniq.sort
188 192 end
189 193
190 194 # Returns the mail adresses of users that should be notified for the issue
191 195 def recipients
192 196 recipients = project.recipients
193 197 # Author and assignee are always notified unless they have been locked
194 198 recipients << author.mail if author && author.active?
195 199 recipients << assigned_to.mail if assigned_to && assigned_to.active?
196 200 recipients.compact.uniq
197 201 end
198 202
199 203 def spent_hours
200 204 @spent_hours ||= time_entries.sum(:hours) || 0
201 205 end
202 206
203 207 def relations
204 208 (relations_from + relations_to).sort
205 209 end
206 210
207 211 def all_dependent_issues
208 212 dependencies = []
209 213 relations_from.each do |relation|
210 214 dependencies << relation.issue_to
211 215 dependencies += relation.issue_to.all_dependent_issues
212 216 end
213 217 dependencies
214 218 end
215 219
216 220 # Returns an array of the duplicate issues
217 221 def duplicates
218 222 relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
219 223 end
220 224
221 225 def duration
222 226 (start_date && due_date) ? due_date - start_date : 0
223 227 end
224 228
225 229 def soonest_start
226 230 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
227 231 end
228 232 end
@@ -1,164 +1,163
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Mailer < ActionMailer::Base
19 19 helper ApplicationHelper
20 20 helper IssuesHelper
21 21 helper CustomFieldsHelper
22 22
23 23 include ActionController::UrlWriter
24 24
25 25 def issue_add(issue)
26 26 recipients issue.recipients
27 27 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
28 28 body :issue => issue,
29 29 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
30 30 end
31 31
32 32 def issue_edit(journal)
33 33 issue = journal.journalized
34 issue.reload
35 34 recipients issue.recipients
36 35 # Watchers in cc
37 36 cc(issue.watcher_recipients - @recipients)
38 37 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
39 38 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
40 39 s << issue.subject
41 40 subject s
42 41 body :issue => issue,
43 42 :journal => journal,
44 43 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
45 44 end
46 45
47 46 def document_added(document)
48 47 recipients document.project.recipients
49 48 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
50 49 body :document => document,
51 50 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
52 51 end
53 52
54 53 def attachments_added(attachments)
55 54 container = attachments.first.container
56 55 added_to = ''
57 56 added_to_url = ''
58 57 case container.class.name
59 58 when 'Version'
60 59 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
61 60 added_to = "#{l(:label_version)}: #{container.name}"
62 61 when 'Document'
63 62 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
64 63 added_to = "#{l(:label_document)}: #{container.title}"
65 64 end
66 65 recipients container.project.recipients
67 66 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
68 67 body :attachments => attachments,
69 68 :added_to => added_to,
70 69 :added_to_url => added_to_url
71 70 end
72 71
73 72 def news_added(news)
74 73 recipients news.project.recipients
75 74 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
76 75 body :news => news,
77 76 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
78 77 end
79 78
80 79 def message_posted(message, recipients)
81 80 recipients(recipients)
82 81 subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}"
83 82 body :message => message,
84 83 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
85 84 end
86 85
87 86 def account_information(user, password)
88 87 set_language_if_valid user.language
89 88 recipients user.mail
90 89 subject l(:mail_subject_register)
91 90 body :user => user,
92 91 :password => password,
93 92 :login_url => url_for(:controller => 'account', :action => 'login')
94 93 end
95 94
96 95 def account_activation_request(user)
97 96 # Send the email to all active administrators
98 97 recipients User.find_active(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
99 98 subject l(:mail_subject_account_activation_request)
100 99 body :user => user,
101 100 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
102 101 end
103 102
104 103 def lost_password(token)
105 104 set_language_if_valid(token.user.language)
106 105 recipients token.user.mail
107 106 subject l(:mail_subject_lost_password)
108 107 body :token => token,
109 108 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
110 109 end
111 110
112 111 def register(token)
113 112 set_language_if_valid(token.user.language)
114 113 recipients token.user.mail
115 114 subject l(:mail_subject_register)
116 115 body :token => token,
117 116 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
118 117 end
119 118
120 119 def test(user)
121 120 set_language_if_valid(user.language)
122 121 recipients user.mail
123 122 subject 'Redmine test'
124 123 body :url => url_for(:controller => 'welcome')
125 124 end
126 125
127 126 private
128 127 def initialize_defaults(method_name)
129 128 super
130 129 set_language_if_valid Setting.default_language
131 130 from Setting.mail_from
132 131 default_url_options[:host] = Setting.host_name
133 132 default_url_options[:protocol] = Setting.protocol
134 133 end
135 134
136 135 # Overrides the create_mail method
137 136 def create_mail
138 137 # Removes the current user from the recipients and cc
139 138 # if he doesn't want to receive notifications about what he does
140 139 if User.current.pref[:no_self_notified]
141 140 recipients.delete(User.current.mail) if recipients
142 141 cc.delete(User.current.mail) if cc
143 142 end
144 143 # Blind carbon copy recipients
145 144 if Setting.bcc_recipients?
146 145 bcc([recipients, cc].flatten.compact.uniq)
147 146 recipients []
148 147 cc []
149 148 end
150 149 super
151 150 end
152 151
153 152 # Renders a message with the corresponding layout
154 153 def render_message(method_name, body)
155 154 layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
156 155 body[:content_for_layout] = render(:file => method_name, :body => body)
157 156 ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}")
158 157 end
159 158
160 159 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
161 160 def self.controller_path
162 161 ''
163 162 end unless respond_to?('controller_path')
164 163 end
General Comments 0
You need to be logged in to leave comments. Login now