[[[TOC]]]
——-
=Intro=
Below is a patch to add HTML Emails to Trac and below that is a sample template to generate a nice looking email. The completed file can be located [[file:notification.zip|Here]] and should be placed in `trac/ticket/notification.py`. This patch has been tested with [[http://trac.edgewall.org/|Trac 1.0.1dev-r11400]]. Because we will be mucking with Trac’s source, it is highly recommended that you perform a back up of the files we are editing.
——–
=Configure HTML Email=
- In the root of the Trac source, edit `trunk/trac/notification.py`.
- Change `msg = MIMEText(body, ‘plain’)` to `msg = MIMEText(body, ‘html’)`
——–
=Edit Ticket Notifications Module=
* Use this [[http://www.cyberciti.biz/faq/appy-patch-file-using-patch-command/|patch]] to apply the [[file:notification.py_.patch_.zip|below]] from the root of the Trac source
{{{ lang=diff
Index: trac/ticket/notification.py
===================================================================
— trac/ticket/notification.py (revision 11657)
+++ trac/ticket/notification.py (working copy)
@@ -29,6 +29,12 @@
from trac.util.datefmt import to_utimestamp
from trac.util.text import obfuscate_email_address, text_width, wrap
from trac.util.translation import deactivate, reactivate
+from trac.web.api import Request
+from trac.mimeview.api import Context
+from trac.wiki.formatter import format_to_html
+from StringIO import StringIO
+import trac.perm as perm
+import re
class TicketNotificationSystem(Component):
@@ -45,18 +51,18 @@
always_notify_updater = BoolOption(‘notification’, ‘always_notify_updater’,
‘true’,
– “””Always send notifications to the person who causes the ticket
+ “””Always send notifications to the person who causes the ticket
property change and to any previous updater of that ticket.”””)
–
– ticket_subject_template = Option(‘notification’, ‘ticket_subject_template’,
+
+ ticket_subject_template = Option(‘notification’, ‘ticket_subject_template’,
‘$prefix #$ticket.id: $summary’,
“””A Genshi text template snippet used to get the notification subject.
By default, the subject template is `$prefix #$ticket.id: $summary`.
`$prefix` being the value of the `smtp_subject_prefix` option.
”(since 0.11)””””)
–
– batch_subject_template = Option(‘notification’, ‘batch_subject_template’,
+
+ batch_subject_template = Option(‘notification’, ‘batch_subject_template’,
‘$prefix Batch modify: $tickets_descr’,
“””Like ticket_subject_template but for batch modifications.
@@ -85,7 +91,7 @@
for row in db(“SELECT cc, reporter, owner FROM ticket WHERE id=%s”,
(tktid,)):
if row[0]:
– ccrecipients += row[0].replace(‘,’, ‘ ‘).split()
+ ccrecipients += row[0].replace(‘,’, ‘ ‘).split()
reporter = row[1]
owner = row[2]
if notify_reporter:
@@ -121,14 +127,14 @@
if notify_owner and (updater == owner):
filter_out = False
if filter_out:
– torecipients = [r for r in torecipients
+ torecipients = [r for r in torecipients
if r and r != updater]
elif updater:
torecipients.append(updater)
return (torecipients, ccrecipients, reporter, owner)
+
–
class TicketNotifyEmail(NotifyEmail):
“””Notification of ticket changes.”””
@@ -162,7 +168,8 @@
self.ticket = ticket
self.modtime = modtime
self.newticket = newticket
–
+
+ changes_list = []
changes_body = ”
self.reporter = ”
self.owner = ”
@@ -171,52 +178,52 @@
link = self.env.abs_href.ticket(ticket.id)
summary = self.ticket[‘summary’]
author = None
–
+
+ try:
+ self.req
+ except:
+ self.req = None
+
+ if self.req is None:
+ self.env.log.warning(“Req is None – a dummy Req is made so that wiki to html works”)
+ tracurl = self.env.base_url.replace(‘https://’,”)
+ environ = {‘REQUEST_METHOD’:’GET’,’SCRIPT_NAME’:”,’SERVER_PORT’:’443′,’SERVER_NAME’:tracurl, ‘wsgi.url_scheme’: ‘https’, ‘wsgi.input’: StringIO(”)}
+ self.req = Request(environ, None)
+ self.req.perm = perm.PermissionCache(self.env)
+
+ context = Context.from_request(self.req, absurls=True)
+
if not self.newticket and modtime: # Ticket change
from trac.ticket.web_ui import TicketModule
for change in TicketModule(self.env).grouped_changelog_entries(
ticket, when=modtime):
if not change[‘permanent’]: # attachment with same time…
continue
+
author = change[‘author’]
change_data.update({
‘author’: self.obfuscate_email(author),
– ‘comment’: wrap(change[‘comment’], self.COLS, ‘ ‘, ‘ ‘,
– ‘\n’, self.ambiwidth)
+ ‘comment’: format_to_html(self.env, context, change[‘comment’].strip(), escape_newlines=True)
})
+
link += ‘#comment:%s’ % str(change.get(‘cnum’, ”))
for field, values in change[‘fields’].iteritems():
old = values[‘old’]
new = values[‘new’]
newv = ”
if field == ‘description’:
– new_descr = wrap(new, self.COLS, ‘ ‘, ‘ ‘, ‘\n’,
– self.ambiwidth)
– old_descr = wrap(old, self.COLS, ‘> ‘, ‘> ‘, ‘\n’,
– self.ambiwidth)
– old_descr = old_descr.replace(2 * ‘\n’, ‘\n’ + ‘>’ + \
– ‘\n’)
– cdescr = ‘\n’
– cdescr += ‘Old description:’ + 2 * ‘\n’ + old_descr + \
– 2 * ‘\n’
– cdescr += ‘New description:’ + 2 * ‘\n’ + new_descr + \
– ‘\n’
– changes_descr = cdescr
+ changes_descr = new.strip()
elif field == ‘summary’:
summary = “%s (was: %s)” % (new, old)
elif field == ‘cc’:
(addcc, delcc) = self.diff_cc(old, new)
chgcc = ”
if delcc:
– chgcc += wrap(” * cc: %s (removed)” %
– ‘, ‘.join(delcc),
– self.COLS, ‘ ‘, ‘ ‘, ‘\n’,
– self.ambiwidth) + ‘\n’
+ chgcc += “* cc: %s (removed)\n” % ‘, ‘.join(delcc)
+ changes_list.append( {‘name’:’CC’,’oldvalue’:self.parse_cc(old) if old else ”, ‘newvalue’: ‘%s (removed)’ % ‘, ‘.join(delcc)} )
if addcc:
– chgcc += wrap(” * cc: %s (added)” %
– ‘, ‘.join(addcc),
– self.COLS, ‘ ‘, ‘ ‘, ‘\n’,
– self.ambiwidth) + ‘\n’
+ chgcc += “* cc: %s (added)\n” % ‘, ‘.join(addcc)
+ changes_list.append( {‘name’:’CC’,’oldvalue’:self.parse_cc(old) if old else ”, ‘newvalue’:’%s (added)’ % ‘, ‘.join(addcc)} )
if chgcc:
changes_body += chgcc
self.prev_cc += self.parse_cc(old) if old else []
@@ -225,53 +232,43 @@
old = self.obfuscate_email(old)
new = self.obfuscate_email(new)
newv = new
– length = 7 + len(field)
– spacer_old, spacer_new = ‘ ‘, ‘ ‘
– if len(old + new) + length > self.COLS:
– length = 5
– if len(old) + length > self.COLS:
– spacer_old = ‘\n’
– if len(new) + length > self.COLS:
– spacer_new = ‘\n’
– chg = ‘* %s: %s%s%s=>%s%s’ % (field, spacer_old, old,
– spacer_old, spacer_new,
+ chg = ‘* %s: %s%s%s=>%s%s\n’ % (field, 7*’ ‘, old,
+ 7*’ ‘, 7*’ ‘,
new)
– chg = chg.replace(‘\n’, ‘\n’ + length * ‘ ‘)
– chg = wrap(chg, self.COLS, ”, length * ‘ ‘, ‘\n’,
– self.ambiwidth)
– changes_body += ‘ %s%s’ % (chg, ‘\n’)
+ changes_body += chg
+ changes_list.append( {‘name’:field.title(),’oldvalue’: old, ‘newvalue’: new} )
if newv:
change_data[field] = {‘oldvalue’: old, ‘newvalue’: new}
–
+
if newticket:
author = ticket[‘reporter’]
ticket_values = ticket.values.copy()
ticket_values[‘id’] = ticket.id
– ticket_values[‘description’] = wrap(
– ticket_values.get(‘description’, ”), self.COLS,
– initial_indent=’ ‘, subsequent_indent=’ ‘, linesep=’\n’,
– ambiwidth=self.ambiwidth)
+ ticket_values[‘description’] = format_to_html(self.env, context,
+ ticket_values.get(‘description’, ”), escape_newlines=True)
ticket_values[‘new’] = self.newticket
ticket_values[‘link’] = link
–
+
subject = self.format_subj(summary)
if not self.newticket:
– subject = ‘Re: ‘ + subject
+ subject = ‘RE: ‘ + subject
+
self.data.update({
‘ticket_props’: self.format_props(),
‘ticket_body_hdr’: self.format_hdr(),
‘subject’: subject,
‘ticket’: ticket_values,
– ‘changes_body’: changes_body,
– ‘changes_descr’: changes_descr,
– ‘change’: change_data
+ ‘changes_body’: format_to_html(self.env, context, changes_body.strip(), escape_newlines=True),
+ ‘changes_descr’: format_to_html(self.env, context, changes_descr.strip(), escape_newlines=True),
+ ‘change’: change_data,
+ ‘changes_list’: changes_list
})
NotifyEmail.notify(self, ticket.id, subject, author)
def format_props(self):
tkt = self.ticket
– fields = [f for f in tkt.fields
+ fields = [f for f in tkt.fields
if f[‘name’] not in (‘summary’, ‘cc’, ‘time’, ‘changetime’)]
width = [0, 0, 0, 0]
i = 0
@@ -294,7 +291,7 @@
width_r = width[2] + width[3] + 5
half_cols = (self.COLS – 1) / 2
if width_l + width_r + 1 > self.COLS:
– if ((width_l > half_cols and width_r > half_cols) or
+ if ((width_l > half_cols and width_r > half_cols) or
(width[0] > half_cols / 2 or width[2] > half_cols / 2)):
width_l = half_cols
width_r = half_cols
@@ -302,7 +299,7 @@
width_l = min((self.COLS – 1) * 2 / 3, width_l)
width_r = self.COLS – width_l – 1
else:
– width_r = min((self.COLS – 1) * 2 / 3, width_r)
+ width_r = min((self.COLS – 1) * 2 / 3, width_r)
width_l = self.COLS – width_r – 1
sep = width_l * ‘-‘ + ‘+’ + width_r * ‘-‘
txt = sep + ‘\n’
@@ -368,23 +365,23 @@
def format_subj(self, summary):
template = self.config.get(‘notification’,’ticket_subject_template’)
template = NewTextTemplate(template.encode(‘utf8’))
–
+
prefix = self.config.get(‘notification’, ‘smtp_subject_prefix’)
– if prefix == ‘__default__’:
+ if prefix == ‘__default__’:
prefix = ‘[%s]’ % self.env.project_name
–
+
data = {
‘prefix’: prefix,
‘summary’: summary,
‘ticket’: self.ticket,
‘env’: self.env,
}
–
+
return template.generate(**data).render(‘text’, encoding=None).strip()
def get_recipients(self, tktid):
(torecipients, ccrecipients, reporter, owner) = \
– get_ticket_notification_recipients(self.env, self.config,
+ get_ticket_notification_recipients(self.env, self.config,
tktid, self.prev_cc)
self.reporter = reporter
self.owner = owner
@@ -442,6 +439,18 @@
reactivate(t)
def _notify(self, tickets, new_values, comment, action, author):
+ try:
+ self.req
+ except:
+ self.req = None
+ if self.req is None:
+ self.env.log.warning(“Req is None – a dummy Req is made so that wiki to html works”)
+ tracurl = self.env.base_url.replace(‘https://’,”)
+ environ = {‘REQUEST_METHOD’:’GET’,’SCRIPT_NAME’:”,’SERVER_PORT’:’443′,’SERVER_NAME’:tracurl, ‘wsgi.url_scheme’: ‘http’, ‘wsgi.input’: StringIO(”)}
+ self.req = Request(environ, None)
+ self.req.perm = perm.PermissionCache(self.env)
+ context = Context.from_request(self.req, absurls=True)
+
self.tickets = tickets
changes_body = ”
self.reporter = ”
@@ -452,10 +461,10 @@
subject = self.format_subj(tickets_descr)
link = self.env.abs_href.query(id=’,’.join([str(t) for t in tickets]))
self.data.update({
– ‘tickets_descr’: tickets_descr,
– ‘changes_descr’: changes_descr,
– ‘comment’: comment,
– ‘action’: action,
+ ‘tickets_descr’: format_to_html(self.env, context, tickets_descr.strip(), escape_newlines=True),
+ ‘changes_descr’: format_to_html(self.env, context, changes_descr.strip(), escape_newlines=True),
+ ‘comment’: format_to_html(self.env, context, comment.strip(), escape_newlines=True),
+ ‘action’: format_to_html(self.env, context, action.strip(), escape_newlines=True),
‘author’: author,
‘subject’: subject,
‘ticket_query_link’: link,
@@ -465,17 +474,17 @@
def format_subj(self, tickets_descr):
template = self.config.get(‘notification’,’batch_subject_template’)
template = NewTextTemplate(template.encode(‘utf8’))
–
+
prefix = self.config.get(‘notification’, ‘smtp_subject_prefix’)
– if prefix == ‘__default__’:
+ if prefix == ‘__default__’:
prefix = ‘[%s]’ % self.env.project_name
–
+
data = {
‘prefix’: prefix,
‘tickets_descr’: tickets_descr,
‘env’: self.env,
}
–
+
return template.generate(**data).render(‘text’, encoding=None).strip()
def get_recipients(self, tktids):
@@ -483,7 +492,7 @@
allccrecipients = []
for t in tktids:
(torecipients, ccrecipients, reporter, owner) = \
– get_ticket_notification_recipients(self.env, self.config,
+ get_ticket_notification_recipients(self.env, self.config,
t, [])
alltorecipients.extend(torecipients)
allccrecipients.extend(ccrecipients)
}}}
——–
=Template File=
* [[file:ticket_notify_email.txt]] – This can be edited to fit your needs:
{{{
{% choose ticket.new %}
{% when True %}
New ticket by $ticket.reporter: |
$ticket.description |
{% end %}
{% otherwise %}
{% if changes_body %}
Changes by $change.author: | |||||
|
{% end %}
{% if change.comment %}
Comment${not changes_body and ‘ by %s‘ % change.author or ”}: |
$change.comment |
{% end %}
{% if changes_descr %}
Description changed by $change.author: | Description: |
$changes_descr |
{% end %}
{% end %}
{% end %}
Ticket Reference: | ||
|
}}}
——–
=Downloads=
* [[file:notification.py_.patch_.zip]] – Patch to apply from the root of Trac 1.0.1 source
* [[file:notification.zip|notification.py.zip]] – Completed `notification.py` to be located at `Trac_Source/trac/ticket/notification.py`
* [[file:ticket_notify_email.txt]] – Template file to be located at `trac/templates`
=Credits=
——–
* [[http://trac.edgewall.org/wiki/TracNotification]]
* [[http://trac.edgewall.org/ticket/2625]]
Leave a Reply to Jared Bownds Cancel reply