Wednesday, February 15, 2012

trac and HTML notifications

Hacking Trac

Of course I used a modified trac 0.10 before, so there were some features I wanted to keep. The most important one are HTML notifications on ticket changes. Today, in trac 0.12.2 this feature still hasn't made it into their trunk. I find it sad really, but that's another story.

There are a few places that offer patches, but none is complete, and worst of all, none seems to work with trac 0.12.2 right away.

So, for any poor souls in the same situation, here's what I did to make it work. First of all download the sources for trac (v0.12.2 in my case), grabbed the best parts of the patches mentioned above, and set to work patching trac. Then I created an egg file, installed it and restarted apache. That's all!



Here's the diff to trac/notification.py:
278d277
<         self.from_email = self.env.config.get('notification', 'smtp_from')
312,315c311
<         #DNK I prefer to have "Author`s name" prefix in the From e-mail field instead of static label defined by 'smtp_from_name' in Trac config
<         #self.from_name = self.config['notification'].get('smtp_from_name')
<         #self.from_name =  self.data['change']['author'] + ' ' + self.config['notification'].get('smtp_from_name')
<         #DNK end
---
>         self.from_email = self.config['notification'].get('smtp_from')
464c460
<         msg = MIMEText(body, 'html')
---
>         msg = MIMEText(body, 'plain')
Here's the diff in  trac/ticket/web_ui.py:
 1197c1197
<             tn = TicketNotifyEmail(self.env, req) #rlrj60:4/10/09
---
>             tn = TicketNotifyEmail(self.env)
1241c1241
<                 tn = TicketNotifyEmail(self.env, req) #rlrj60:4/10/09
---
>                 tn = TicketNotifyEmail(self.env)
Here's the diff to trac/ticket/notification.py:
22c22
< from trac.wiki.formatter import *
---
>
78c78
<     def __init__(self, env, req):
---
>     def __init__(self, env):
81d80
<         self.req = req
108d106
<         BRCRLF = '<br />' + CRLF
119,120c117,119
<                     'author': change['author'],
<                     'comment': wiki_to_html(change['comment'], env=self.env, req=self.req, absurls=True)
---
>                     'author': obfuscate_email_address(change['author']),
>                     'comment': wrap(change['comment'], self.COLS, ' ', ' ',
>                                     CRLF)
128,131c127,130
<                         new_descr = wrap(new, self.COLS, ' ', ' ', BRCRLF)
<                         old_descr = wrap(old, self.COLS, '&gt;', '&gt;', BRCRLF)
<                         old_descr = old_descr.replace(2*CRLF, CRLF + '&gt;' + BRCRLF)
<
---
>                         new_descr = wrap(new, self.COLS, ' ', ' ', CRLF)
>                         old_descr = wrap(old, self.COLS, '> ', '> ', CRLF)
>                         old_descr = old_descr.replace(2 * CRLF, CRLF + '>' + \
>                                                       CRLF)
133,134c132,135
<                         cdescr += 'Old description:' + 2*CRLF + old_descr + CRLF + BRCRLF
<                         cdescr += 'New description:' + 2*CRLF + new_descr + CRLF + BRCRLF
---
>                         cdescr += 'Old description:' + 2 * CRLF + old_descr + \
>                                   2 * CRLF
>                         cdescr += 'New description:' + 2 * CRLF + new_descr + \
>                                   CRLF
142,144c143,145
<                             chgcc += '<li>' + wrap("cc: %s (removed)" % ', '.join(delcc),
<                                            self.COLS, ' ', ' ', BRCRLF) + '</li>'
<                             chgcc += CRLF
---
>                             chgcc += wrap(" * cc: %s (removed)" %
>                                           ', '.join(delcc),
>                                           self.COLS, ' ', ' ', CRLF) + CRLF
146,148c147,149
<                             chgcc += '<li>' + wrap("cc: %s (added)" % ', '.join(addcc),
<                                            self.COLS, ' ', ' ', BRCRLF) + '</li>'
<                             chgcc += CRLF
---
>                             chgcc += wrap(" * cc: %s (added)" %
>                                           ', '.join(addcc),
>                                           self.COLS, ' ', ' ', CRLF) + CRLF
152a154,156
>                         if field in ['owner', 'reporter']:
>                             old = obfuscate_email_address(old)
>                             new = obfuscate_email_address(new)
162,164c166,171
<                         chg = wrap('%s &rarr; %s' % (old, new), self.COLS , '',
<                                     ' ', CRLF)
<                         changes_body += '<li>' + '%s:  %s%s' % (field, chg, CRLF) + '</li>'
---
>                         chg = '* %s: %s%s%s=>%s%s' % (field, spacer_old, old,
>                                                       spacer_old, spacer_new,
>                                                       new)
>                         chg = chg.replace(CRLF, CRLF + length * ' ')
>                         chg = wrap(chg, self.COLS, '', length * ' ', CRLF)
>                         changes_body += ' %s%s' % (chg, CRLF)
167,169d173
<
<         if changes_body:
<             changes_body = '<ul>' + changes_body + '</ul>'
173,174c177,179
<         # convert wiki syntax to html
<         ticket_values['description'] = wiki_to_html(ticket_values.get('description', ''), env=self.env, req=self.req, absurls=True)
---
>         ticket_values['description'] = wrap(
>             ticket_values.get('description', ''), self.COLS,
>             initial_indent=' ', subsequent_indent=' ', linesep=CRLF)
194d198
<         BRCRLF = '<br />' + CRLF
199,200c203,207
<         for f in [f['name'] for f in fields if f['type'] != 'textarea']:
<             if not tkt.values.has_key(f):
---
>         for f in fields:
>             if f['type'] == 'textarea':
>                 continue
>             fname = f['name']
>             if not fname in tkt.values:
202c209
<             fval = tkt[f] or ''
---
>             fval = tkt[fname] or ''
204a212,213
>             if fname in ['owner', 'reporter']:
>                 fval = obfuscate_email_address(fval)
206,209c215,216
<             if len(f) > width[idx]:
<                 width[idx] = len(f)
<             if len(fval) > width[idx + 1]:
<                 width[idx + 1] = len(fval)
---
>             width[idx] = max(self.get_text_width(f['label']), width[idx])
>             width[idx + 1] = max(self.get_text_width(fval), width[idx + 1])
211,214c218,232
<         format = ('<tr valign="top"><td align="right"><b>%s:</b></td><td align="left">%s</td>','<td align="right"><b>%s:</b></td><td align="left">%s</td></tr>'+CRLF)
<         #l = (width[0] + width[1] + 5)
<         #sep = l * '-' + '+' + (self.COLS - l) * '-'
<         sep = CRLF       
---
>         width_l = width[0] + width[1] + 5
>         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
>                     (width[0] > half_cols / 2 or width[2] > half_cols / 2)):
>                 width_l = half_cols
>                 width_r = half_cols
>             elif width_l > width_r:
>                 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_l = self.COLS - width_r - 1
>         sep = width_l * '-' + '+' + width_r * '-'
216c234
<         txt = '<table border="0" cellpadding="2" cellspacing="0">' + CRLF
---
>         cell_tmp = [u'', u'']
218a237
>         width_lr = [width_l, width_r]
231c250,257
<                 txt += format[i % 2] % (f['label'], unicode(fval))
---
>                 str_tmp = u'%s:  %s' % (f['label'], unicode(fval))
>                 idx = i % 2
>                 cell_tmp[idx] += wrap(str_tmp, width_lr[idx] - 2 + 2 * idx,
>                                       (width[2 * idx]
>                                        - self.get_text_width(f['label'])
>                                        + 2 * idx) * ' ',
>                                       2 * ' ', CRLF)
>                 cell_tmp[idx] += CRLF
233,235c259,268
<         if i % 2:
<             txt += '<td colspan="2">&nbsp;</td></tr>' + CRLF
<             txt += CRLF
---
>         cell_l = cell_tmp[0].splitlines()
>         cell_r = cell_tmp[1].splitlines()
>         for i in range(max(len(cell_l), len(cell_r))):
>             if i >= len(cell_l):
>                 cell_l.append(width_l * ' ')
>             elif i >= len(cell_r):
>                 cell_r.append('')
>             fmt_width = width_l - self.get_text_width(cell_l[i]) \
>                         + len(cell_l[i])
>             txt += u'%-*s|%s%s' % (fmt_width, cell_l[i], cell_r[i], CRLF)
239,240c272
<                 txt += CRLF.join(['<tr align="left"><td colspan="2"><b>' + name + ':' + '</b></td></tr>', '<tr align="left"><td colspan="2">' + value + '</td></tr>'])
<                 """txt += CRLF.join(['', name + ':', value, '', ''])"""
---
>                 txt += CRLF.join(['', name + ':', value, '', ''])
242d273
<         txt += '</table>' + CRLF       
251,252c282,285
<         added = [x for x in newcc if x and x not in oldcc]
<         removed = [x for x in oldcc if x and x not in newcc]
---
>         added = [obfuscate_email_address(x) \
>                                 for x in newcc if x and x not in oldcc]
>         removed = [obfuscate_email_address(x) \
>                                 for x in oldcc if x and x not in newcc]
348d380
<         hdrs['Content-Type'] = 'text/html; charset=utf-8'
 And finally, the diff of trac/ticket/templates/ticket_notify_email.txt
 1,19c1,11
< <div>  
<   <div style="font-family: Verdana, Arial, Helvetica, sans-serif; background-color:#f8f8f8">
<     <hr>
<     <a style="text-decoration:none;color:#069; font-size: 19px" href="${project.url or abs_href()}"><strong>$project.name</strong></a>
<     <hr>
<     <a style="text-decoration:none;color:#666666; font-size: 17px" href="$ticket.link">$ticket_body_hdr</a>
<     <hr>
<   </div>    
<  {% choose ticket.new %}\
<  {%   when True %}\
<     <div style="color:#069; font-size: 15px"><em>New ticket</em> (by <strong>$ticket.reporter</strong>)</div>
<     <br/>
<      <div style="padding:1.5em;">$ticket.description</div>
<     <br/>
<  {%   end %}\
<  {%   otherwise %}\
<  {%     if changes_body %}\
<       <div style="color:#069; font-size: 15px"><em>Changes</em> (by <strong>$change.author</strong>)</div>
<       $changes_body
---
> $ticket_body_hdr
> $ticket_props
> {% choose ticket.new %}\
> {%   when True %}\
> $ticket.description
> {%   end %}\
> {%   otherwise %}\
> {%     if changes_body %}\
> ${_('Changes (by %(author)s):', author=change.author)}
>
> $changes_body
23c15
<         <div style="color:#069; font-size: 15px"><em>Description changed by</em> <strong>$change.author</strong></div>
---
> ${_('Description changed by %(author)s:', author=change.author)}
25,26c17,18
< {%      if changes_body or change.comment or not change.author %}\
<         <div style="color:#069; font-size: 15px"><em>Description</em></div>
---
> $changes_descr
> --
28,38c20,22
<       <div style="padding:1.5em; font-size: 14px">$changes_descr</div>
<       <br/>
< {%    end %}\
< {%    if change.comment %}\
<       <div style="color:#069;"><em>Comment</em>  ${not changes_body and '(by <strong>%s</strong>)' % change.author or ''}</div>
<       <div style="padding:1.5em;">$change.comment</div>
<       <br/>
< {%    end %}\
< {%  end %}\
<   <hr/>
< {% end %}\
---
> {%     if change.comment %}\
>
> ${changes_body and _('Comment:') or _('Comment (by %(author)s):', author=change.author)}
40,50c24,27
< <style type="text/css">
<   th { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 12px }
<   td { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 12px }
< </style>
<   
<     $ticket_props
<     
<     <hr/>
<     <div style="font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 13px;background-color:#f0f0f0;color:#999">$project.descr</div>
<     <cite style="display:block;padding:4px;background-color:#f0f0f0;color:#999;font-size:95%;border-top:1px dotted #ccc;">$project.descr</cite>    
< </div>
---
> $change.comment
> {%     end %}\
> {%   end %}\
> {% end %}\
51a29,32
> --
> ${_('Ticket URL: <%(link)s>', link=ticket.link)}
> $project.name <${project.url or abs_href()}>
> $project.descr

Obviously it's up to anyone to modify the template to their likes. I used winmerge to create those diffs.

1 comment:

FReNeTiC said...

Hey there!

First of all, I'd like to say thank you.
I'm trying to make trac send html emails and I found you post.
Got dazzled.

Now, I'm trying to implement your modifications to try it out.
Im having a little problem.
Your diff isnt helpful =/

Do you have a better visual way so people can check your diff, ou could you share the modified source files?

Thank you!