--- trac/ticket/notification.py 2011-04-04 15:03:25.000000000 +0200 +++ trac/ticket/notification.py 2011-04-04 15:16:17.000000000 +0200 @@ -29,6 +29,21 @@ from trac.util.text import CRLF, wrap, obfuscate_email_address, to_unicode from trac.util.translation import deactivate, reactivate +from trac.versioncontrol.diff import diff_blocks +from genshi import HTML +import difflib + +def diff_cleanup(gen): + for value in gen: + if value.startswith('---'): + continue + if value.startswith('+++'): + continue + if value.startswith('@@'): + yield '\n' + else: + yield value + class TicketNotificationSystem(Component): always_notify_owner = BoolOption('notification', 'always_notify_owner', @@ -106,6 +120,7 @@ change_data = {} link = self.env.abs_href.ticket(ticket.id) summary = self.ticket['summary'] + BRCRLF = '
' + CRLF if not self.newticket and modtime: # Ticket change from trac.ticket.web_ui import TicketModule @@ -124,29 +139,32 @@ new = values['new'] newv = '' if field == 'description': - new_descr = wrap(new, self.COLS, ' ', ' ', CRLF) - old_descr = wrap(old, self.COLS, '> ', '> ', CRLF) - old_descr = old_descr.replace(2 * CRLF, CRLF + '>' + \ - CRLF) - cdescr = CRLF - cdescr += 'Old description:' + 2 * CRLF + old_descr + \ - 2 * CRLF - cdescr += 'New description:' + 2 * CRLF + new_descr + \ - CRLF - changes_descr = cdescr + changes_descr = HTML( + "%s" % ( + '\n'.join( + diff_cleanup( + difflib.unified_diff( + wrap(old, cols=60).split('\n'), + wrap(new, cols=60).split('\n'), + lineterm='', n=3 + ) + ) + ).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, ' ', ' ', CRLF) + CRLF + chgcc += '
  • ' + wrap("cc: %s (removed)" % ', '.join(delcc), + self.COLS, ' ', ' ', BRCRLF) + '
  • ' + chgcc += CRLF if addcc: - chgcc += wrap(" * cc: %s (added)" % - ', '.join(addcc), - self.COLS, ' ', ' ', CRLF) + CRLF + chgcc += '
  • ' + wrap("cc: %s (added)" % ', '.join(addcc), + self.COLS, ' ', ' ', BRCRLF) + '
  • ' + chgcc += CRLF if chgcc: changes_body += chgcc self.prev_cc += old and self.parse_cc(old) or [] @@ -163,15 +181,18 @@ spacer_old = CRLF if len(new) + length > self.COLS: spacer_new = CRLF - chg = '* %s: %s%s%s=>%s%s' % (field, spacer_old, old, + 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) + changes_body += '
  • ' + ' %s%s' % (chg, CRLF) + '
  • ' if newv: change_data[field] = {'oldvalue': old, 'newvalue': new} + if changes_body: + changes_body = '' + ticket_values = ticket.values.copy() ticket_values['id'] = ticket.id ticket_values['description'] = wrap( @@ -198,8 +219,8 @@ tkt = self.ticket fields = [f for f in tkt.fields if f['name'] not in ('summary', 'cc', 'time', 'changetime')] - width = [0, 0, 0, 0] i = 0 + BRCRLF = '
    ' + CRLF for f in fields: if f['type'] == 'textarea': continue @@ -211,30 +232,12 @@ continue if fname in ['owner', 'reporter']: fval = obfuscate_email_address(fval) - idx = 2 * (i % 2) - width[idx] = max(self.get_text_width(f['label']), width[idx]) - width[idx + 1] = max(self.get_text_width(fval), width[idx + 1]) i += 1 - 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 * '-' - txt = sep + CRLF - cell_tmp = [u'', u''] big = [] + sep = CRLF + txt = '' + CRLF + format = ('',''+CRLF) i = 0 - width_lr = [width_l, width_r] for f in [f for f in fields if f['name'] != 'description']: fname = f['name'] if not tkt.values.has_key(fname): @@ -244,33 +247,16 @@ fval = obfuscate_email_address(fval) if f['type'] == 'textarea' or '\n' in unicode(fval): big.append((f['label'], CRLF.join(fval.splitlines()))) + txt += '' + CRLF else: - # Note: f['label'] is a Babel's LazyObject, make sure its - # __str__ method won't be called. - 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 + txt += format[i % 2] % (f['label'].capitalize(), unicode(fval)) i += 1 - 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) if big: txt += sep for name, value in big: - txt += CRLF.join(['', name + ':', value, '', '']) + txt += CRLF.join(['', '']) txt += sep + txt += '
    %s: %s%s: %s
     
    ' + name + ':' + '
    ' + value + '
    ' + CRLF return txt def parse_cc(self, txt): @@ -376,6 +362,7 @@ def send(self, torcpts, ccrcpts): dest = self.reporter or 'anonymous' hdrs = {} + hdrs['Content-Type'] = 'text/html; charset=utf-8' hdrs['Message-ID'] = self.get_message_id(dest, self.modtime) hdrs['X-Trac-Ticket-ID'] = str(self.ticket.id) hdrs['X-Trac-Ticket-URL'] = self.data['ticket']['link'] --- trac/ticket/templates/ticket_notify_email.txt 2011-04-04 21:43:48.000000000 +0200 +++ trac/ticket/templates/ticket_notify_email.txt 2011-04-04 21:53:00.000000000 +0200 @@ -1,32 +1,53 @@ -$ticket_body_hdr -$ticket_props -{% choose ticket.new %}\ -{% when True %}\ -$ticket.description -{% end %}\ -{% otherwise %}\ -{% if changes_body %}\ -${_('Changes (by %(author)s):', author=change.author)} + + + +
    + $ticket_body_hdr +
    -$changes_body -{% end %}\ -{% if changes_descr %}\ -{% if not changes_body and not change.comment and change.author %}\ -${_('Description changed by %(author)s:', author=change.author)} -{% end %}\ -$changes_descr --- -{% end %}\ -{% if change.comment %}\ - -${changes_body and _('Comment:') or _('Comment (by %(author)s):', author=change.author)} - -$change.comment -{% end %}\ -{% end %}\ -{% end %}\ - --- -${_('Ticket URL: <%(link)s>', link=ticket.link)} -$project.name <${project.url or abs_href()}> -$project.descr +{% choose ticket.new %}\ +{% when True %}\ +
    $ticket.description
    +{% end %}\ +{% otherwise %}\ +{% if changes_body %}\ +
    + Changes (by $change.author) + $changes_body +
    +{% end %}\ +{% if change.comment %}\ +
    + Comment ${not changes_body and '(by %s)' % change.author or ''} +
    $change.comment
    +
    +{% end %}\ +{% if changes_descr %}\ +
    +{% choose not changes_body and not change.comment and change.author %}\ +{% when True %}\ + Description changed by $change.author +{% end %}\ +{% otherwise %}\ + Description +{% end %}\ +{% end %}\ +
    $changes_descr
    +
    +{% end %}\ +{% end %}\ +{% end %}\ + +
     
    +
    +
    $ticket_props
    +
    +
    + --- trac/notification.py 2011-04-04 22:47:14.000000000 +0200 +++ trac/notification.py 2011-04-04 22:47:23.000000000 +0200 @@ -457,7 +457,7 @@ if pcc: headers['Cc'] = ', '.join(pcc) headers['Date'] = formatdate() - msg = MIMEText(body, 'plain') + msg = MIMEText(body, 'html') # Message class computes the wrong type from MIMEText constructor, # which does not take a Charset object as initializer. Reset the # encoding type to force a new, valid evaluation