##// END OF EJS Templates
Fixing Plugin and Mailer default_url_options....
Eric Davis -
r2458:4baf32b166a6
parent child
Show More
@@ -1,307 +1,307
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 Mailer < ActionMailer::Base
18 class Mailer < ActionMailer::Base
19 helper :application
19 helper :application
20 helper :issues
20 helper :issues
21 helper :custom_fields
21 helper :custom_fields
22
22
23 include ActionController::UrlWriter
23 include ActionController::UrlWriter
24 include Redmine::I18n
24 include Redmine::I18n
25
25
26 def self.default_url_options
27 h = Setting.host_name
28 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
29 { :host => h, :protocol => Setting.protocol }
30 end
31
26 def issue_add(issue)
32 def issue_add(issue)
27 redmine_headers 'Project' => issue.project.identifier,
33 redmine_headers 'Project' => issue.project.identifier,
28 'Issue-Id' => issue.id,
34 'Issue-Id' => issue.id,
29 'Issue-Author' => issue.author.login
35 'Issue-Author' => issue.author.login
30 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
36 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
31 message_id issue
37 message_id issue
32 recipients issue.recipients
38 recipients issue.recipients
33 cc(issue.watcher_recipients - @recipients)
39 cc(issue.watcher_recipients - @recipients)
34 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
40 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
35 body :issue => issue,
41 body :issue => issue,
36 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
42 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
37 end
43 end
38
44
39 def issue_edit(journal)
45 def issue_edit(journal)
40 issue = journal.journalized
46 issue = journal.journalized
41 redmine_headers 'Project' => issue.project.identifier,
47 redmine_headers 'Project' => issue.project.identifier,
42 'Issue-Id' => issue.id,
48 'Issue-Id' => issue.id,
43 'Issue-Author' => issue.author.login
49 'Issue-Author' => issue.author.login
44 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
50 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
45 message_id journal
51 message_id journal
46 references issue
52 references issue
47 @author = journal.user
53 @author = journal.user
48 recipients issue.recipients
54 recipients issue.recipients
49 # Watchers in cc
55 # Watchers in cc
50 cc(issue.watcher_recipients - @recipients)
56 cc(issue.watcher_recipients - @recipients)
51 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
57 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
52 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
58 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
53 s << issue.subject
59 s << issue.subject
54 subject s
60 subject s
55 body :issue => issue,
61 body :issue => issue,
56 :journal => journal,
62 :journal => journal,
57 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
63 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
58 end
64 end
59
65
60 def reminder(user, issues, days)
66 def reminder(user, issues, days)
61 set_language_if_valid user.language
67 set_language_if_valid user.language
62 recipients user.mail
68 recipients user.mail
63 subject l(:mail_subject_reminder, issues.size)
69 subject l(:mail_subject_reminder, issues.size)
64 body :issues => issues,
70 body :issues => issues,
65 :days => days,
71 :days => days,
66 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
72 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
67 end
73 end
68
74
69 def document_added(document)
75 def document_added(document)
70 redmine_headers 'Project' => document.project.identifier
76 redmine_headers 'Project' => document.project.identifier
71 recipients document.project.recipients
77 recipients document.project.recipients
72 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
78 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
73 body :document => document,
79 body :document => document,
74 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
80 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
75 end
81 end
76
82
77 def attachments_added(attachments)
83 def attachments_added(attachments)
78 container = attachments.first.container
84 container = attachments.first.container
79 added_to = ''
85 added_to = ''
80 added_to_url = ''
86 added_to_url = ''
81 case container.class.name
87 case container.class.name
82 when 'Project'
88 when 'Project'
83 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
89 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
84 added_to = "#{l(:label_project)}: #{container}"
90 added_to = "#{l(:label_project)}: #{container}"
85 when 'Version'
91 when 'Version'
86 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
92 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
87 added_to = "#{l(:label_version)}: #{container.name}"
93 added_to = "#{l(:label_version)}: #{container.name}"
88 when 'Document'
94 when 'Document'
89 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
95 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
90 added_to = "#{l(:label_document)}: #{container.title}"
96 added_to = "#{l(:label_document)}: #{container.title}"
91 end
97 end
92 redmine_headers 'Project' => container.project.identifier
98 redmine_headers 'Project' => container.project.identifier
93 recipients container.project.recipients
99 recipients container.project.recipients
94 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
100 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
95 body :attachments => attachments,
101 body :attachments => attachments,
96 :added_to => added_to,
102 :added_to => added_to,
97 :added_to_url => added_to_url
103 :added_to_url => added_to_url
98 end
104 end
99
105
100 def news_added(news)
106 def news_added(news)
101 redmine_headers 'Project' => news.project.identifier
107 redmine_headers 'Project' => news.project.identifier
102 message_id news
108 message_id news
103 recipients news.project.recipients
109 recipients news.project.recipients
104 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
110 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
105 body :news => news,
111 body :news => news,
106 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
112 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
107 end
113 end
108
114
109 def message_posted(message, recipients)
115 def message_posted(message, recipients)
110 redmine_headers 'Project' => message.project.identifier,
116 redmine_headers 'Project' => message.project.identifier,
111 'Topic-Id' => (message.parent_id || message.id)
117 'Topic-Id' => (message.parent_id || message.id)
112 message_id message
118 message_id message
113 references message.parent unless message.parent.nil?
119 references message.parent unless message.parent.nil?
114 recipients(recipients)
120 recipients(recipients)
115 subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
121 subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
116 body :message => message,
122 body :message => message,
117 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
123 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
118 end
124 end
119
125
120 def account_information(user, password)
126 def account_information(user, password)
121 set_language_if_valid user.language
127 set_language_if_valid user.language
122 recipients user.mail
128 recipients user.mail
123 subject l(:mail_subject_register, Setting.app_title)
129 subject l(:mail_subject_register, Setting.app_title)
124 body :user => user,
130 body :user => user,
125 :password => password,
131 :password => password,
126 :login_url => url_for(:controller => 'account', :action => 'login')
132 :login_url => url_for(:controller => 'account', :action => 'login')
127 end
133 end
128
134
129 def account_activation_request(user)
135 def account_activation_request(user)
130 # Send the email to all active administrators
136 # Send the email to all active administrators
131 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
137 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
132 subject l(:mail_subject_account_activation_request, Setting.app_title)
138 subject l(:mail_subject_account_activation_request, Setting.app_title)
133 body :user => user,
139 body :user => user,
134 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
140 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
135 end
141 end
136
142
137 # A registered user's account was activated by an administrator
143 # A registered user's account was activated by an administrator
138 def account_activated(user)
144 def account_activated(user)
139 set_language_if_valid user.language
145 set_language_if_valid user.language
140 recipients user.mail
146 recipients user.mail
141 subject l(:mail_subject_register, Setting.app_title)
147 subject l(:mail_subject_register, Setting.app_title)
142 body :user => user,
148 body :user => user,
143 :login_url => url_for(:controller => 'account', :action => 'login')
149 :login_url => url_for(:controller => 'account', :action => 'login')
144 end
150 end
145
151
146 def lost_password(token)
152 def lost_password(token)
147 set_language_if_valid(token.user.language)
153 set_language_if_valid(token.user.language)
148 recipients token.user.mail
154 recipients token.user.mail
149 subject l(:mail_subject_lost_password, Setting.app_title)
155 subject l(:mail_subject_lost_password, Setting.app_title)
150 body :token => token,
156 body :token => token,
151 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
157 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
152 end
158 end
153
159
154 def register(token)
160 def register(token)
155 set_language_if_valid(token.user.language)
161 set_language_if_valid(token.user.language)
156 recipients token.user.mail
162 recipients token.user.mail
157 subject l(:mail_subject_register, Setting.app_title)
163 subject l(:mail_subject_register, Setting.app_title)
158 body :token => token,
164 body :token => token,
159 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
165 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
160 end
166 end
161
167
162 def test(user)
168 def test(user)
163 set_language_if_valid(user.language)
169 set_language_if_valid(user.language)
164 recipients user.mail
170 recipients user.mail
165 subject 'Redmine test'
171 subject 'Redmine test'
166 body :url => url_for(:controller => 'welcome')
172 body :url => url_for(:controller => 'welcome')
167 end
173 end
168
174
169 # Overrides default deliver! method to prevent from sending an email
175 # Overrides default deliver! method to prevent from sending an email
170 # with no recipient, cc or bcc
176 # with no recipient, cc or bcc
171 def deliver!(mail = @mail)
177 def deliver!(mail = @mail)
172 return false if (recipients.nil? || recipients.empty?) &&
178 return false if (recipients.nil? || recipients.empty?) &&
173 (cc.nil? || cc.empty?) &&
179 (cc.nil? || cc.empty?) &&
174 (bcc.nil? || bcc.empty?)
180 (bcc.nil? || bcc.empty?)
175
181
176 # Set Message-Id and References
182 # Set Message-Id and References
177 if @message_id_object
183 if @message_id_object
178 mail.message_id = self.class.message_id_for(@message_id_object)
184 mail.message_id = self.class.message_id_for(@message_id_object)
179 end
185 end
180 if @references_objects
186 if @references_objects
181 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
187 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
182 end
188 end
183 super(mail)
189 super(mail)
184 end
190 end
185
191
186 # Sends reminders to issue assignees
192 # Sends reminders to issue assignees
187 # Available options:
193 # Available options:
188 # * :days => how many days in the future to remind about (defaults to 7)
194 # * :days => how many days in the future to remind about (defaults to 7)
189 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
195 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
190 # * :project => id or identifier of project to process (defaults to all projects)
196 # * :project => id or identifier of project to process (defaults to all projects)
191 def self.reminders(options={})
197 def self.reminders(options={})
192 days = options[:days] || 7
198 days = options[:days] || 7
193 project = options[:project] ? Project.find(options[:project]) : nil
199 project = options[:project] ? Project.find(options[:project]) : nil
194 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
200 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
195
201
196 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
202 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
197 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
203 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
198 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
204 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
199 s << "#{Issue.table_name}.project_id = #{project.id}" if project
205 s << "#{Issue.table_name}.project_id = #{project.id}" if project
200 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
206 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
201
207
202 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
208 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
203 :conditions => s.conditions
209 :conditions => s.conditions
204 ).group_by(&:assigned_to)
210 ).group_by(&:assigned_to)
205 issues_by_assignee.each do |assignee, issues|
211 issues_by_assignee.each do |assignee, issues|
206 deliver_reminder(assignee, issues, days) unless assignee.nil?
212 deliver_reminder(assignee, issues, days) unless assignee.nil?
207 end
213 end
208 end
214 end
209
215
210 private
216 private
211 def initialize_defaults(method_name)
217 def initialize_defaults(method_name)
212 super
218 super
213 set_language_if_valid Setting.default_language
219 set_language_if_valid Setting.default_language
214 from Setting.mail_from
220 from Setting.mail_from
215
221
216 # URL options
217 h = Setting.host_name
218 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
219 default_url_options[:host] = h
220 default_url_options[:protocol] = Setting.protocol
221
222 # Common headers
222 # Common headers
223 headers 'X-Mailer' => 'Redmine',
223 headers 'X-Mailer' => 'Redmine',
224 'X-Redmine-Host' => Setting.host_name,
224 'X-Redmine-Host' => Setting.host_name,
225 'X-Redmine-Site' => Setting.app_title
225 'X-Redmine-Site' => Setting.app_title
226 end
226 end
227
227
228 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
228 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
229 def redmine_headers(h)
229 def redmine_headers(h)
230 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
230 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
231 end
231 end
232
232
233 # Overrides the create_mail method
233 # Overrides the create_mail method
234 def create_mail
234 def create_mail
235 # Removes the current user from the recipients and cc
235 # Removes the current user from the recipients and cc
236 # if he doesn't want to receive notifications about what he does
236 # if he doesn't want to receive notifications about what he does
237 @author ||= User.current
237 @author ||= User.current
238 if @author.pref[:no_self_notified]
238 if @author.pref[:no_self_notified]
239 recipients.delete(@author.mail) if recipients
239 recipients.delete(@author.mail) if recipients
240 cc.delete(@author.mail) if cc
240 cc.delete(@author.mail) if cc
241 end
241 end
242 # Blind carbon copy recipients
242 # Blind carbon copy recipients
243 if Setting.bcc_recipients?
243 if Setting.bcc_recipients?
244 bcc([recipients, cc].flatten.compact.uniq)
244 bcc([recipients, cc].flatten.compact.uniq)
245 recipients []
245 recipients []
246 cc []
246 cc []
247 end
247 end
248 super
248 super
249 end
249 end
250
250
251 # Renders a message with the corresponding layout
251 # Renders a message with the corresponding layout
252 def render_message(method_name, body)
252 def render_message(method_name, body)
253 layout = method_name.to_s.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
253 layout = method_name.to_s.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
254 body[:content_for_layout] = render(:file => method_name, :body => body)
254 body[:content_for_layout] = render(:file => method_name, :body => body)
255 ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
255 ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
256 end
256 end
257
257
258 # for the case of plain text only
258 # for the case of plain text only
259 def body(*params)
259 def body(*params)
260 value = super(*params)
260 value = super(*params)
261 if Setting.plain_text_mail?
261 if Setting.plain_text_mail?
262 templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
262 templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
263 unless String === @body or templates.empty?
263 unless String === @body or templates.empty?
264 template = File.basename(templates.first)
264 template = File.basename(templates.first)
265 @body[:content_for_layout] = render(:file => template, :body => @body)
265 @body[:content_for_layout] = render(:file => template, :body => @body)
266 @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
266 @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
267 return @body
267 return @body
268 end
268 end
269 end
269 end
270 return value
270 return value
271 end
271 end
272
272
273 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
273 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
274 def self.controller_path
274 def self.controller_path
275 ''
275 ''
276 end unless respond_to?('controller_path')
276 end unless respond_to?('controller_path')
277
277
278 # Returns a predictable Message-Id for the given object
278 # Returns a predictable Message-Id for the given object
279 def self.message_id_for(object)
279 def self.message_id_for(object)
280 # id + timestamp should reduce the odds of a collision
280 # id + timestamp should reduce the odds of a collision
281 # as far as we don't send multiple emails for the same object
281 # as far as we don't send multiple emails for the same object
282 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{object.created_on.strftime("%Y%m%d%H%M%S")}"
282 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{object.created_on.strftime("%Y%m%d%H%M%S")}"
283 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
283 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
284 host = "#{::Socket.gethostname}.redmine" if host.empty?
284 host = "#{::Socket.gethostname}.redmine" if host.empty?
285 "<#{hash}@#{host}>"
285 "<#{hash}@#{host}>"
286 end
286 end
287
287
288 private
288 private
289
289
290 def message_id(object)
290 def message_id(object)
291 @message_id_object = object
291 @message_id_object = object
292 end
292 end
293
293
294 def references(object)
294 def references(object)
295 @references_objects ||= []
295 @references_objects ||= []
296 @references_objects << object
296 @references_objects << object
297 end
297 end
298 end
298 end
299
299
300 # Patch TMail so that message_id is not overwritten
300 # Patch TMail so that message_id is not overwritten
301 module TMail
301 module TMail
302 class Mail
302 class Mail
303 def add_message_id( fqdn = nil )
303 def add_message_id( fqdn = nil )
304 self.message_id ||= ::TMail::new_message_id(fqdn)
304 self.message_id ||= ::TMail::new_message_id(fqdn)
305 end
305 end
306 end
306 end
307 end
307 end
@@ -1,146 +1,152
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 module Redmine
18 module Redmine
19 module Hook
19 module Hook
20 include ActionController::UrlWriter
20 include ActionController::UrlWriter
21
21
22 @@listener_classes = []
22 @@listener_classes = []
23 @@listeners = nil
23 @@listeners = nil
24 @@hook_listeners = {}
24 @@hook_listeners = {}
25
25
26 class << self
26 class << self
27 # Adds a listener class.
27 # Adds a listener class.
28 # Automatically called when a class inherits from Redmine::Hook::Listener.
28 # Automatically called when a class inherits from Redmine::Hook::Listener.
29 def add_listener(klass)
29 def add_listener(klass)
30 raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton)
30 raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton)
31 @@listener_classes << klass
31 @@listener_classes << klass
32 clear_listeners_instances
32 clear_listeners_instances
33 end
33 end
34
34
35 # Returns all the listerners instances.
35 # Returns all the listerners instances.
36 def listeners
36 def listeners
37 @@listeners ||= @@listener_classes.collect {|listener| listener.instance}
37 @@listeners ||= @@listener_classes.collect {|listener| listener.instance}
38 end
38 end
39
39
40 # Returns the listeners instances for the given hook.
40 # Returns the listeners instances for the given hook.
41 def hook_listeners(hook)
41 def hook_listeners(hook)
42 @@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
42 @@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
43 end
43 end
44
44
45 # Clears all the listeners.
45 # Clears all the listeners.
46 def clear_listeners
46 def clear_listeners
47 @@listener_classes = []
47 @@listener_classes = []
48 clear_listeners_instances
48 clear_listeners_instances
49 end
49 end
50
50
51 # Clears all the listeners instances.
51 # Clears all the listeners instances.
52 def clear_listeners_instances
52 def clear_listeners_instances
53 @@listeners = nil
53 @@listeners = nil
54 @@hook_listeners = {}
54 @@hook_listeners = {}
55 end
55 end
56
56
57 # Calls a hook.
57 # Calls a hook.
58 # Returns the listeners response.
58 # Returns the listeners response.
59 def call_hook(hook, context={})
59 def call_hook(hook, context={})
60 returning [] do |response|
60 returning [] do |response|
61 hls = hook_listeners(hook)
61 hls = hook_listeners(hook)
62 if hls.any?
62 if hls.any?
63 default_url_options[:only_path] ||= true
64 hls.each {|listener| response << listener.send(hook, context)}
63 hls.each {|listener| response << listener.send(hook, context)}
65 end
64 end
66 end
65 end
67 end
66 end
68 end
67 end
69
68
70 # Base class for hook listeners.
69 # Base class for hook listeners.
71 class Listener
70 class Listener
72 include Singleton
71 include Singleton
73 include Redmine::I18n
72 include Redmine::I18n
74
73
75 # Registers the listener
74 # Registers the listener
76 def self.inherited(child)
75 def self.inherited(child)
77 Redmine::Hook.add_listener(child)
76 Redmine::Hook.add_listener(child)
78 super
77 super
79 end
78 end
79
80 end
80 end
81
81
82 # Listener class used for views hooks.
82 # Listener class used for views hooks.
83 # Listeners that inherit this class will include various helpers by default.
83 # Listeners that inherit this class will include various helpers by default.
84 class ViewListener < Listener
84 class ViewListener < Listener
85 include ERB::Util
85 include ERB::Util
86 include ActionView::Helpers::TagHelper
86 include ActionView::Helpers::TagHelper
87 include ActionView::Helpers::FormHelper
87 include ActionView::Helpers::FormHelper
88 include ActionView::Helpers::FormTagHelper
88 include ActionView::Helpers::FormTagHelper
89 include ActionView::Helpers::FormOptionsHelper
89 include ActionView::Helpers::FormOptionsHelper
90 include ActionView::Helpers::JavaScriptHelper
90 include ActionView::Helpers::JavaScriptHelper
91 include ActionView::Helpers::PrototypeHelper
91 include ActionView::Helpers::PrototypeHelper
92 include ActionView::Helpers::NumberHelper
92 include ActionView::Helpers::NumberHelper
93 include ActionView::Helpers::UrlHelper
93 include ActionView::Helpers::UrlHelper
94 include ActionView::Helpers::AssetTagHelper
94 include ActionView::Helpers::AssetTagHelper
95 include ActionView::Helpers::TextHelper
95 include ActionView::Helpers::TextHelper
96 include ActionController::UrlWriter
96 include ActionController::UrlWriter
97 include ApplicationHelper
97 include ApplicationHelper
98
98
99 # Default to creating links using only the path. Subclasses can
100 # change this default as needed
101 def self.default_url_options
102 {:only_path => true }
103 end
104
99 # Helper method to directly render a partial using the context:
105 # Helper method to directly render a partial using the context:
100 #
106 #
101 # class MyHook < Redmine::Hook::ViewListener
107 # class MyHook < Redmine::Hook::ViewListener
102 # render_on :view_issues_show_details_bottom, :partial => "show_more_data"
108 # render_on :view_issues_show_details_bottom, :partial => "show_more_data"
103 # end
109 # end
104 #
110 #
105 def self.render_on(hook, options={})
111 def self.render_on(hook, options={})
106 define_method hook do |context|
112 define_method hook do |context|
107 context[:controller].send(:render_to_string, {:locals => context}.merge(options))
113 context[:controller].send(:render_to_string, {:locals => context}.merge(options))
108 end
114 end
109 end
115 end
110 end
116 end
111
117
112 # Helper module included in ApplicationHelper and ActionControllerso that
118 # Helper module included in ApplicationHelper and ActionControllerso that
113 # hooks can be called in views like this:
119 # hooks can be called in views like this:
114 #
120 #
115 # <%= call_hook(:some_hook) %>
121 # <%= call_hook(:some_hook) %>
116 # <%= call_hook(:another_hook, :foo => 'bar' %>
122 # <%= call_hook(:another_hook, :foo => 'bar' %>
117 #
123 #
118 # Or in controllers like:
124 # Or in controllers like:
119 # call_hook(:some_hook)
125 # call_hook(:some_hook)
120 # call_hook(:another_hook, :foo => 'bar'
126 # call_hook(:another_hook, :foo => 'bar'
121 #
127 #
122 # Hooks added to views will be concatenated into a string. Hooks added to
128 # Hooks added to views will be concatenated into a string. Hooks added to
123 # controllers will return an array of results.
129 # controllers will return an array of results.
124 #
130 #
125 # Several objects are automatically added to the call context:
131 # Several objects are automatically added to the call context:
126 #
132 #
127 # * project => current project
133 # * project => current project
128 # * request => Request instance
134 # * request => Request instance
129 # * controller => current Controller instance
135 # * controller => current Controller instance
130 #
136 #
131 module Helper
137 module Helper
132 def call_hook(hook, context={})
138 def call_hook(hook, context={})
133 if is_a?(ActionController::Base)
139 if is_a?(ActionController::Base)
134 default_context = {:controller => self, :project => @project, :request => request}
140 default_context = {:controller => self, :project => @project, :request => request}
135 Redmine::Hook.call_hook(hook, default_context.merge(context))
141 Redmine::Hook.call_hook(hook, default_context.merge(context))
136 else
142 else
137 default_context = {:controller => controller, :project => @project, :request => request}
143 default_context = {:controller => controller, :project => @project, :request => request}
138 Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ')
144 Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ')
139 end
145 end
140 end
146 end
141 end
147 end
142 end
148 end
143 end
149 end
144
150
145 ApplicationHelper.send(:include, Redmine::Hook::Helper)
151 ApplicationHelper.send(:include, Redmine::Hook::Helper)
146 ActionController::Base.send(:include, Redmine::Hook::Helper)
152 ActionController::Base.send(:include, Redmine::Hook::Helper)
@@ -1,148 +1,166
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 require File.dirname(__FILE__) + '/../../../test_helper'
18 require File.dirname(__FILE__) + '/../../../test_helper'
19
19
20 class Redmine::Hook::ManagerTest < Test::Unit::TestCase
20 class Redmine::Hook::ManagerTest < Test::Unit::TestCase
21
21
22 fixtures :issues
23
22 # Some hooks that are manually registered in these tests
24 # Some hooks that are manually registered in these tests
23 class TestHook < Redmine::Hook::ViewListener; end
25 class TestHook < Redmine::Hook::ViewListener; end
24
26
25 class TestHook1 < TestHook
27 class TestHook1 < TestHook
26 def view_layouts_base_html_head(context)
28 def view_layouts_base_html_head(context)
27 'Test hook 1 listener.'
29 'Test hook 1 listener.'
28 end
30 end
29 end
31 end
30
32
31 class TestHook2 < TestHook
33 class TestHook2 < TestHook
32 def view_layouts_base_html_head(context)
34 def view_layouts_base_html_head(context)
33 'Test hook 2 listener.'
35 'Test hook 2 listener.'
34 end
36 end
35 end
37 end
36
38
37 class TestHook3 < TestHook
39 class TestHook3 < TestHook
38 def view_layouts_base_html_head(context)
40 def view_layouts_base_html_head(context)
39 "Context keys: #{context.keys.collect(&:to_s).sort.join(', ')}."
41 "Context keys: #{context.keys.collect(&:to_s).sort.join(', ')}."
40 end
42 end
41 end
43 end
42
44
43 class TestLinkToHook < TestHook
45 class TestLinkToHook < TestHook
44 def view_layouts_base_html_head(context)
46 def view_layouts_base_html_head(context)
45 link_to('Issues', :controller => 'issues')
47 link_to('Issues', :controller => 'issues')
46 end
48 end
47 end
49 end
48
50
49 class TestHookHelperController < ActionController::Base
51 class TestHookHelperController < ActionController::Base
50 include Redmine::Hook::Helper
52 include Redmine::Hook::Helper
51 end
53 end
52
54
53 class TestHookHelperView < ActionView::Base
55 class TestHookHelperView < ActionView::Base
54 include Redmine::Hook::Helper
56 include Redmine::Hook::Helper
55 end
57 end
56
58
57 Redmine::Hook.clear_listeners
59 Redmine::Hook.clear_listeners
58
60
59 def setup
61 def setup
60 @hook_module = Redmine::Hook
62 @hook_module = Redmine::Hook
61 @hook_helper = TestHookHelperController.new
63 @hook_helper = TestHookHelperController.new
62 @view_hook_helper = TestHookHelperView.new(RAILS_ROOT + '/app/views')
64 @view_hook_helper = TestHookHelperView.new(RAILS_ROOT + '/app/views')
63 end
65 end
64
66
65 def teardown
67 def teardown
66 @hook_module.clear_listeners
68 @hook_module.clear_listeners
67 @hook_module.default_url_options = { }
68 end
69 end
69
70
70 def test_clear_listeners
71 def test_clear_listeners
71 assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size
72 assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size
72 @hook_module.add_listener(TestHook1)
73 @hook_module.add_listener(TestHook1)
73 @hook_module.add_listener(TestHook2)
74 @hook_module.add_listener(TestHook2)
74 assert_equal 2, @hook_module.hook_listeners(:view_layouts_base_html_head).size
75 assert_equal 2, @hook_module.hook_listeners(:view_layouts_base_html_head).size
75
76
76 @hook_module.clear_listeners
77 @hook_module.clear_listeners
77 assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size
78 assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size
78 end
79 end
79
80
80 def test_add_listener
81 def test_add_listener
81 assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size
82 assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size
82 @hook_module.add_listener(TestHook1)
83 @hook_module.add_listener(TestHook1)
83 assert_equal 1, @hook_module.hook_listeners(:view_layouts_base_html_head).size
84 assert_equal 1, @hook_module.hook_listeners(:view_layouts_base_html_head).size
84 end
85 end
85
86
86 def test_call_hook
87 def test_call_hook
87 @hook_module.add_listener(TestHook1)
88 @hook_module.add_listener(TestHook1)
88 assert_equal ['Test hook 1 listener.'], @hook_helper.call_hook(:view_layouts_base_html_head)
89 assert_equal ['Test hook 1 listener.'], @hook_helper.call_hook(:view_layouts_base_html_head)
89 end
90 end
90
91
91 def test_call_hook_with_context
92 def test_call_hook_with_context
92 @hook_module.add_listener(TestHook3)
93 @hook_module.add_listener(TestHook3)
93 assert_equal ['Context keys: bar, controller, foo, project, request.'],
94 assert_equal ['Context keys: bar, controller, foo, project, request.'],
94 @hook_helper.call_hook(:view_layouts_base_html_head, :foo => 1, :bar => 'a')
95 @hook_helper.call_hook(:view_layouts_base_html_head, :foo => 1, :bar => 'a')
95 end
96 end
96
97
97 def test_call_hook_with_multiple_listeners
98 def test_call_hook_with_multiple_listeners
98 @hook_module.add_listener(TestHook1)
99 @hook_module.add_listener(TestHook1)
99 @hook_module.add_listener(TestHook2)
100 @hook_module.add_listener(TestHook2)
100 assert_equal ['Test hook 1 listener.', 'Test hook 2 listener.'], @hook_helper.call_hook(:view_layouts_base_html_head)
101 assert_equal ['Test hook 1 listener.', 'Test hook 2 listener.'], @hook_helper.call_hook(:view_layouts_base_html_head)
101 end
102 end
102
103
103 # Context: Redmine::Hook::Helper.call_hook default_url
104 # Context: Redmine::Hook::Helper.call_hook default_url
104 def test_call_hook_default_url_options
105 def test_call_hook_default_url_options
105 @hook_module.add_listener(TestLinkToHook)
106 @hook_module.add_listener(TestLinkToHook)
106
107
107 assert_equal ['<a href="/issues">Issues</a>'], @hook_helper.call_hook(:view_layouts_base_html_head)
108 assert_equal ['<a href="/issues">Issues</a>'], @hook_helper.call_hook(:view_layouts_base_html_head)
108 end
109 end
109
110
110 # Context: Redmine::Hook::Helper.call_hook
111 # Context: Redmine::Hook::Helper.call_hook
111 def test_call_hook_with_project_added_to_context
112 def test_call_hook_with_project_added_to_context
112 @hook_module.add_listener(TestHook3)
113 @hook_module.add_listener(TestHook3)
113 assert_match /project/i, @hook_helper.call_hook(:view_layouts_base_html_head)[0]
114 assert_match /project/i, @hook_helper.call_hook(:view_layouts_base_html_head)[0]
114 end
115 end
115
116
116 def test_call_hook_from_controller_with_controller_added_to_context
117 def test_call_hook_from_controller_with_controller_added_to_context
117 @hook_module.add_listener(TestHook3)
118 @hook_module.add_listener(TestHook3)
118 assert_match /controller/i, @hook_helper.call_hook(:view_layouts_base_html_head)[0]
119 assert_match /controller/i, @hook_helper.call_hook(:view_layouts_base_html_head)[0]
119 end
120 end
120
121
121 def test_call_hook_from_controller_with_request_added_to_context
122 def test_call_hook_from_controller_with_request_added_to_context
122 @hook_module.add_listener(TestHook3)
123 @hook_module.add_listener(TestHook3)
123 assert_match /request/i, @hook_helper.call_hook(:view_layouts_base_html_head)[0]
124 assert_match /request/i, @hook_helper.call_hook(:view_layouts_base_html_head)[0]
124 end
125 end
125
126
126 def test_call_hook_from_view_with_project_added_to_context
127 def test_call_hook_from_view_with_project_added_to_context
127 @hook_module.add_listener(TestHook3)
128 @hook_module.add_listener(TestHook3)
128 assert_match /project/i, @view_hook_helper.call_hook(:view_layouts_base_html_head)
129 assert_match /project/i, @view_hook_helper.call_hook(:view_layouts_base_html_head)
129 end
130 end
130
131
131 def test_call_hook_from_view_with_controller_added_to_context
132 def test_call_hook_from_view_with_controller_added_to_context
132 @hook_module.add_listener(TestHook3)
133 @hook_module.add_listener(TestHook3)
133 assert_match /controller/i, @view_hook_helper.call_hook(:view_layouts_base_html_head)
134 assert_match /controller/i, @view_hook_helper.call_hook(:view_layouts_base_html_head)
134 end
135 end
135
136
136 def test_call_hook_from_view_with_request_added_to_context
137 def test_call_hook_from_view_with_request_added_to_context
137 @hook_module.add_listener(TestHook3)
138 @hook_module.add_listener(TestHook3)
138 assert_match /request/i, @view_hook_helper.call_hook(:view_layouts_base_html_head)
139 assert_match /request/i, @view_hook_helper.call_hook(:view_layouts_base_html_head)
139 end
140 end
140
141
141 def test_call_hook_from_view_should_join_responses_with_a_space
142 def test_call_hook_from_view_should_join_responses_with_a_space
142 @hook_module.add_listener(TestHook1)
143 @hook_module.add_listener(TestHook1)
143 @hook_module.add_listener(TestHook2)
144 @hook_module.add_listener(TestHook2)
144 assert_equal 'Test hook 1 listener. Test hook 2 listener.',
145 assert_equal 'Test hook 1 listener. Test hook 2 listener.',
145 @view_hook_helper.call_hook(:view_layouts_base_html_head)
146 @view_hook_helper.call_hook(:view_layouts_base_html_head)
146 end
147 end
148
149 def test_call_hook_should_not_change_the_default_url_for_email_notifications
150 issue = Issue.find(1)
151
152 ActionMailer::Base.deliveries.clear
153 Mailer.deliver_issue_add(issue)
154 mail = ActionMailer::Base.deliveries.last
155
156 @hook_module.add_listener(TestLinkToHook)
157 @hook_helper.call_hook(:view_layouts_base_html_head)
158
159 ActionMailer::Base.deliveries.clear
160 Mailer.deliver_issue_add(issue)
161 mail2 = ActionMailer::Base.deliveries.last
162
163 assert_equal mail.body, mail2.body
164 end
147 end
165 end
148
166
General Comments 0
You need to be logged in to leave comments. Login now