Adding HTML Emails To Trac 1.0.1
Table of Contents [hide]
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 Here and should be placed in trac/ticket/notification.py
. This patch has been tested with 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')
tomsg = MIMEText(body, 'html')
- Change
Edit Ticket Notifications Module ∞
- Use this patch to apply the below from the root of the Trac source
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 ∞
- ticket_notify_email.txt – This can be edited to fit your needs:
<style type='text/css'> body{ font-family: Helvetic, Verdana, Arial, sans-serif; background-color: #FFF; font-size: 13px; } .greyBg{ background-color: #f8f8f8; } .greyBorder{ border: 1px solid #a0a0a0; } .subHeader{ font-size: 18px; font-weight: bold; text-align: center; } .header{ text-align: center; font-weight: bolder; font-size: 25px; } div{ padding: 5px; margin: 5px; } a:link {color: #000000; text-decoration: underline; } a:active {color: #000000; text-decoration: underline; } a:visited {color: #000000; text-decoration: underline; } a:hover {color: #000DDB; text-decoration: underline; } </style> <div class='greyBg greyBorder' style='border-radius: 15px;text-align: center'> <span class='header'> <a href="${project.url or abs_href()}">Trac Ticket Notification</a> </span> <hr /> <span class='subHeader'> <a href="$ticket.link">$ticket_body_hdr</a> </span> </div> {% choose ticket.new %} {% when True %} <table style='width: 100%;'> <tr> <td class='greyBg' style="font-style: italic;font-weight: bold;"> New ticket by <strong>$ticket.reporter</strong>: </td> </tr> <tr> <td> $ticket.description </td> </tr> </table> {% end %} {% otherwise %} {% if changes_body %} <table style='width: 100%;'> <tr> <td class='greyBg' style="font-style: italic;font-weight: bold;"> Changes by <strong>$change.author</strong>: </td> </tr> <tr> <td> <table> {% for single_change in changes_list %}\ <tr> <td>•</td> <td style='font-weight: bold;'>$single_change.name:</td> <td>$single_change.oldvalue</td> <td style='font-weight: bold;'>⇒</td> <td>$single_change.newvalue</td> </tr> {% end %} </table> </td> </tr> </table> {% end %} {% if change.comment %} <table style='width: 100%;'> <tr> <td class='greyBg' style="font-style: italic;font-weight: bold;"> Comment${not changes_body and ' by <strong>%s</strong>' % change.author or ''}: </td> </tr> <tr> <td> $change.comment </td> </tr> </table> {% end %} {% if changes_descr %} <table style='width: 100%;'> <tr> {% if not changes_body and not change.comment and change.author %} <td class='greyBg' style="font-style: italic;font-weight: bold;"> Description changed by <strong>$change.author</strong>: </td> {% end %} {% if changes_body or change.comment or not change.author %} <td class='greyBg' style="font-style: italic;font-weight: bold;"> Description: </td> {% end %} </tr> <tr> <td> $changes_descr </td> </tr> </table> {% end %} {% end %} <hr/> {% end %} <table> <tr> <td class='greyBg' style="font-style: italic;font-weight: bold;"> Ticket Reference: </td> </tr> <tr> <td> <table cellspacing=2 cellpadding=2> {% 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'] %}\ ${'<tr>'.join('<td style="font-weight: bold;">%s:</td><td>%s</td>' % (format(p[0], ' <12'), p[1]) for p in pv if p[0] in sel)} {% end %}\ </table> </td> </tr> </table>
Downloads ∞
- notification.py.patch – Patch to apply from the root of Trac 1.0.1 source
- notification.py.zip – Completed
notification.py
to be located atTrac_Source/trac/ticket/notification.py
- ticket_notify_email.txt – Template file to be located at
trac/templates
Credits ∞
0
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.
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?
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!
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?
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).