Adding HTML Emails To Trac 1.0.1

[[[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:
{% for single_change in changes_list %}\

{% end %}

$single_change.name: $single_change.oldvalue $single_change.newvalue

{% end %}
{% if change.comment %}

Comment${not changes_body and ‘ by %s‘ % change.author or ”}:
$change.comment

{% end %}
{% if changes_descr %}

{% if not changes_body and not change.comment and change.author %}

{% end %}
{% if changes_body or change.comment or not change.author %}

{% end %}

Description changed by $change.author: Description:
$changes_descr

{% end %}
{% end %}


{% end %}

Ticket Reference:

{% with
pv = [(a[0].strip(), a[1].strip()) for a in [b.split(‘:’) for b in
[c.strip() for c in
ticket_props.replace(‘|’, ‘\n’).splitlines()[1:-1]] if ‘:’ in b]];
sel = [‘Reporter’, ‘Owner’, ‘Type’, ‘Status’, ‘Priority’, ‘Component’, ‘Severity’, ‘Resolution’, ‘Keywords’, ‘Last Author’, ‘Relationships’,’Summary’] %}\
${‘

‘.join(‘

‘ % (format(p[0], ‘ <12'), p[1]) for p in pv if p[0] in sel)} {% end %}\

%s: %s

}}}
——–
=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]]


Posted

in

, ,

by

Tags:

Comments

5 responses to “Adding HTML Emails To Trac 1.0.1”

  1. Jared Bownds Avatar
    Jared Bownds

    This doesn’t seem to function with Trac 1.0.1 – Returns error “No handler matched request to /newticket”.
    I would love to begin implementing the patch, please advise.

    1. Jared Bownds Avatar
      Jared Bownds

      After further review, the appropriate question should be what is the best strategy on implementing this patch? I see the notification.py script, however I am new to programming and am having a hard time interpreting the usage of “-” and “+” and @@. How are these used and what do you propose is the best way to implement this patch?

      1. dlasley Avatar

        The best way to apply this patch is by using the `patch` application. If you start in the root directory of the Trac 1.01 source tree, you can apply the patch by entering `patch < /path/to/notification/patch/file`. I also went ahead and added the completed `notification.py` as a download on this article, it will replace the `notification.py` in `Trac_Source/trac/ticket/`. Let me know if I can be of further assistance!

  2. Jared Bownds Avatar
    Jared Bownds

    So I learned how to implement a diff, however now I receive the following error.

    Warning: The ticket has been created, but an error occurred while sending notifications: “ticket_values” not defined

    Could this be due to using custom fields?

    1. dlasley Avatar

      That error probably indicates that the patch was not applied correctly. I had to create a new variable (`ticket_values` – defined in the `@@ -225,53 +232,43 @@` section) to pass through ticket values to the template file. I have attached the completed `notification.py` to this post; place it in `Trac_Source/trac/ticket/notification.py`. Also make sure to restart `tracd` (or `apache` depending on your setup).

Leave a Reply to dlasley Cancel reply

Your email address will not be published. Required fields are marked *