Changeset 17594


Ignore:
Timestamp:
Nov 25, 2019, 9:37:28 AM (7 months ago)
Author:
Jun Omae
Message:

TracHtmlNotificationPlugin: make compatible with Trac 1.4 (closes #13663)

Location:
trachtmlnotificationplugin/0.12
Files:
4 added
4 edited
1 moved

Legend:

Unmodified
Added
Removed
  • trachtmlnotificationplugin/0.12

    • Property svn:ignore
      •  

        old new  
        22dist
        33*.egg-info
         4.tox
  • trachtmlnotificationplugin/0.12/setup.py

    r13660 r17594  
    88setup(
    99    name = 'TracHtmlNotificationPlugin',
    10     version = '0.12.0.1',
     10    version = '0.12.0.2',
    1111    description = 'Send ticket notification with HTML part (t:#2625)',
    1212    license = 'BSD',  # the same as Trac
     
    1616    packages = find_packages(exclude=['*.tests*']),
    1717    package_data = {
    18         'trachtmlnotification': ['templates/*.html'],
     18        'trachtmlnotification': [
     19            'templates/genshi/*.html',
     20            'templates/jinja2/*.html',
     21        ],
    1922    },
    2023    entry_points = {
  • trachtmlnotificationplugin/0.12/trachtmlnotification/notification.py

    r16119 r17594  
    66from email.MIMEText import MIMEText
    77from email.MIMEMultipart import MIMEMultipart
    8 from genshi.builder import tag
    9 Locale = None
     8from pkg_resources import parse_version, resource_filename
     9
     10try:
     11    from trac.util.html import tag
     12except ImportError:
     13    from genshi.builder import tag
     14
    1015try:
    1116    from babel.core import Locale
    1217except ImportError:
    13     pass
    14 
     18    Locale = None
     19
     20from trac import __version__
    1521from trac.core import Component, implements
    1622from trac.attachment import AttachmentModule
    1723from trac.env import Environment
    18 from trac.mimeview.api import Context
    1924from trac.notification import SmtpEmailSender, SendmailEmailSender
    2025from trac.resource import ResourceNotFound
     
    3136
    3237try:
     38    from trac.web.chrome import web_context
     39except ImportError:
     40    from trac.mimeview.api import Context
     41    web_context = Context.from_request
     42
     43try:
    3344    from trac.notification.api import INotificationFormatter
    3445except ImportError:
    3546    INotificationFormatter = None
     47
     48
     49_parsed_version = parse_version(__version__)
     50if _parsed_version >= parse_version('1.4'):
     51    _use_jinja2 = True
     52elif _parsed_version >= parse_version('1.3'):
     53    _use_jinja2 = hasattr(Chrome, 'jenv')
     54else:
     55    _use_jinja2 = False
     56
     57
     58if _use_jinja2:
     59    _template_dir = resource_filename(__name__, 'templates/jinja2')
     60else:
     61    _template_dir = resource_filename(__name__, 'templates/genshi')
    3662
    3763
     
    4773            return Locale('en', 'US')
    4874else:
    49     def _parse_locale(lang):
    50         return None
    51 
    52 
    53 if hasattr(Environment, 'get_read_db'):
    54     def _get_db(env):
    55         return env.get_read_db()
    56 else:
    57     def _get_db(env):
    58         return env.get_db_cnx()
     75    _parse_locale = lambda lang: None
    5976
    6077
     
    7996        cnum = None
    8097        if event.time:
    81             db = self.env.get_read_db()
    82             cursor = db.cursor()
    83             cursor.execute("""\
     98            rows = self._db_query("""\
    8499                SELECT field, oldvalue FROM ticket_change
    85100                WHERE ticket=%s AND time=%s AND field='comment'
    86101                """, (ticket.id, to_utimestamp(event.time)))
    87             for field, oldvalue in cursor:
     102            for field, oldvalue in rows:
    88103                if oldvalue:
    89104                    cnum = int(oldvalue.rsplit('.', 1)[-1])
     
    116131
    117132    def get_templates_dirs(self):
    118         from pkg_resources import resource_filename
    119         return [resource_filename(__name__, 'templates')]
     133        return [_template_dir]
    120134
    121135    # public methods
     
    140154    # private methods
    141155
     156    if hasattr(Environment, 'db_query'):
     157        def _db_query(self, query, args=()):
     158            return self.env.db_query(query, args)
     159    else:
     160        def _db_query(self, query, args=()):
     161            db = self.env.get_read_db()
     162            cursor = db.cursor()
     163            cursor.execute(query, args)
     164            return list(cursor)
     165
    142166    def _create_request(self):
    143167        languages = filter(None, [self.config.get('trac', 'default_language')])
     
    148172        tzname = self.config.get('trac', 'default_timezone')
    149173        tz = get_timezone(tzname) or localtz
     174        base_url = self.env.abs_href()
     175        if ':' in base_url:
     176            url_scheme = base_url.split(':', 1)[0]
     177        else:
     178            url_scheme = 'http'
    150179        environ = {'REQUEST_METHOD': 'POST', 'REMOTE_ADDR': '127.0.0.1',
    151180                   'SERVER_NAME': 'localhost', 'SERVER_PORT': '80',
    152                    'wsgi.url_scheme': 'http',
    153                    'trac.base_url': self.env.abs_href()}
     181                   'wsgi.url_scheme': url_scheme, 'trac.base_url': base_url}
    154182        if languages:
    155183            environ['HTTP_ACCEPT_LANGUAGE'] = ','.join(languages)
     
    160188        req.args = {}
    161189        req.authname = 'anonymous'
     190        req.form_token = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    162191        req.session = session
    163192        req.perm = MockPerm()
     
    182211            cnum = int(cnum)
    183212
    184         db = _get_db(self.env)
    185213        try:
    186214            ticket = Ticket(self.env, tktid)
     
    218246                              if change.get('cnum') == cnum]
    219247        data['changes'] = changes
    220         context = Context.from_request(req, ticket.resource, absurls=True)
     248        context = web_context(req, ticket.resource, absurls=True)
    221249        alist = attmod.attachment_data(context)
    222250        alist['can_create'] = False
    223         data.update({
    224                 'can_append': False,
    225                 'show_editor': False,
    226                 'start_time': ticket['changetime'],
    227                 'context': context,
    228                 'alist': alist,
    229                 'styles': self._get_styles(chrome),
    230                 'link': tag.a(link, href=link),
    231                 'tag_': tag_,
    232                })
     251        data.update({'can_append': False,
     252                     'show_editor': False,
     253                     'start_time': ticket['changetime'],
     254                     'context': context,
     255                     'alist': alist,
     256                     'styles': self._get_styles(chrome),
     257                     'link': tag.a(link, href=link),
     258                     'tag_': tag_})
    233259        template = 'htmlnotification_ticket.html'
    234260        # use pretty_dateinfo in TimelineModule
    235261        TimelineModule(self.env).post_process_request(req, template, data,
    236262                                                      None)
    237         rendered = chrome.render_template(req, template, data, fragment=True)
    238         return unicode(rendered)
     263        if _use_jinja2:
     264            return chrome.render_template(req, template, data,
     265                                          {'iterable': False})
     266        else:
     267            return unicode(chrome.render_template(req, template, data,
     268                           fragment=True))
    239269
    240270    def _get_styles(self, chrome):
     
    282312
    283313
    284 class HtmlNotificationSmtpEmailSender(SmtpEmailSender):
    285 
    286     def send(self, from_addr, recipients, message):
    287         if not INotificationFormatter:
     314if not INotificationFormatter:
     315
     316    class HtmlNotificationSmtpEmailSender(SmtpEmailSender):
     317
     318        def send(self, from_addr, recipients, message):
    288319            mod = HtmlNotificationModule(self.env)
    289320            message = mod.substitute_message(message)
    290         SmtpEmailSender.send(self, from_addr, recipients, message)
    291 
    292 
    293 class HtmlNotificationSendmailEmailSender(SendmailEmailSender):
    294 
    295     def send(self, from_addr, recipients, message):
    296         if not INotificationFormatter:
     321            SmtpEmailSender.send(self, from_addr, recipients, message)
     322
     323
     324    class HtmlNotificationSendmailEmailSender(SendmailEmailSender):
     325
     326        def send(self, from_addr, recipients, message):
    297327            mod = HtmlNotificationModule(self.env)
    298328            message = mod.substitute_message(message)
    299         SendmailEmailSender.send(self, from_addr, recipients, message)
     329            SendmailEmailSender.send(self, from_addr, recipients, message)
  • trachtmlnotificationplugin/0.12/trachtmlnotification/templates/genshi/htmlnotification_ticket.html

    r17593 r17594  
    1717        <h1 id="trac-ticket-title">
    1818          <a href="${abs_href.ticket(ticket.id)}">Ticket #${ticket.id}</a>
    19           <span class="status">(${ticket.status}<py:if
    20               test="ticket.type"> ${ticket.type}</py:if><py:if
    21               test="ticket.resolution">: ${ticket.resolution}</py:if>)</span>
     19          <span class="trac-status">(${ticket.status}</span>
     20          <span class="trac-type" py:if="ticket.type"> ${ticket.type}</span>
     21          <span class="trac-resolution" py:if="ticket.resolution"> ${ticket.resolution}</span>
    2222        </h1>
    2323        <hr />
  • trachtmlnotificationplugin/0.12/trachtmlnotification/tests/notification.py

    r13660 r17594  
    33import unittest
    44
    5 from genshi.builder import tag
     5from trac.core import Component, implements
     6from trac.test import EnvironmentStub, MockRequest
     7from trac.ticket.model import Ticket
     8from trac.ticket.web_ui import TicketModule
     9from trac.util.datefmt import to_utimestamp
     10from trac.web.api import ITemplateStreamFilter, RequestDone
     11try:
     12    from trac.notification.api import IEmailSender, NotificationSystem
     13except ImportError:
     14    from trac.notification import IEmailSender, NotificationSystem
    615
    7 from trac.core import Component, implements
    8 from trac.notification import IEmailSender
    9 from trac.test import EnvironmentStub
    10 from trac.ticket.model import Ticket
    11 from trac.web.api import ITemplateStreamFilter
    12 
    13 from trachtmlnotification.notification import HtmlNotificationModule
    14 
    15 message = """\
    16 From: <trac@localhost>
    17 To: <trac@localhost>
    18 X-Trac-Ticket-URL: %(url)s
    19 Content-Type: text/plain; charset=utf-8
    20 
    21 email body
    22 """
     16from trachtmlnotification.notification import (
     17    HtmlNotificationModule, INotificationFormatter, tag)
    2318
    2419
     
    3934
    4035
     36class EmailSenderStub(Component):
     37
     38    implements(IEmailSender)
     39
     40    history = None
     41
     42    def __init__(self):
     43        self.history = []
     44
     45    def send(self, from_addr, recipients, message):
     46        if not INotificationFormatter:
     47            mod = HtmlNotificationModule(self.env)
     48            message = mod.substitute_message(message)
     49        self.history.append((from_addr, recipients, message))
     50
     51
    4152class NormalTestCase(unittest.TestCase):
     53
     54    users = [('joe', 'Joe User', 'joe@example.org')]
    4255
    4356    def setUp(self):
    4457        self.env = EnvironmentStub(default_data=True,
    45                                    enable=['trac.*', 'trachtmlnotification.*'])
    46         self.env.config.set('notification', 'mime_encoding', 'none')
     58                                   enable=['trac.*', 'trachtmlnotification.*',
     59                                           EmailSenderStub])
     60        self.config = self.env.config
     61        section = self.config['notification']
     62        section.set('smtp_enabled', 'enabled')
     63        section.set('mime_encoding', 'none')
     64        section.set('email_sender', 'EmailSenderStub')
     65        if hasattr(NotificationSystem, 'subscribers'):
     66            section = self.config['notification-subscriber']
     67            section.set('s', 'TicketReporterSubscriber')
     68            section.set('s.priority', '1')
     69            section.set('s.adverb', 'always')
     70            section.set('s.format', 'text/html')
     71        else:
     72            section.set('always_notify_reporter', 'enabled')
     73        users = [('joe', 'Joe User', 'joe@example.org')]
     74        if hasattr(self.env, 'insert_users'):
     75            self.env.insert_users(self.users)
     76        else:
     77            self.env.known_users = users
    4778        self.mod = HtmlNotificationModule(self.env)
     79        self.tktmod = TicketModule(self.env)
     80        self.sender = EmailSenderStub(self.env)
    4881
    4982    def tearDown(self):
    5083        self.env.reset_db()
    5184
    52     def test_newticket(self):
    53         ticket = Ticket(self.env)
    54         ticket['status'] = 'new'
    55         ticket['summary'] = 'Blah blah blah'
    56         ticket['reporter'] = 'joe'
    57         ticket['description'] = '<<<Description>>>'
    58         ticket.insert()
    59         orig = message % {'url': 'http://localhost/ticket/%d' % ticket.id}
    60         result = self.mod.substitute_message(orig, ignore_exc=False)
    61         self.assertNotEqual(result, orig)
    62         self.assertTrue('\nContent-Type: text/html;' in result)
    63         self.assertTrue('&lt;&lt;&lt;Description&gt;&gt;&gt;' in result)
     85    def _create_ticket(self, **kwargs):
     86        args = dict(('field_' + name, value)
     87                     for name, value in kwargs.iteritems())
     88        req = MockRequest(self.env, method='POST', authname=kwargs['reporter'],
     89                          path_info='/newticket', args=args)
     90        self.assertEqual(True, self.tktmod.match_request(req))
     91        try:
     92            self.tktmod.process_request(req)
     93            self.fail('RequestDone not raised')
     94        except RequestDone:
     95            pass
    6496
    65     def test_ticket_comment(self):
    66         ticket = Ticket(self.env)
    67         ticket['status'] = 'new'
    68         ticket['summary'] = 'Blah blah blah'
    69         ticket['reporter'] = 'joe'
    70         ticket['description'] = '`<<<Description>>>`'
    71         ticket.insert()
    72         ticket.save_changes(author='anonymous', comment='`>>>first comment<<<`')
    73         ticket.save_changes(author='anonymous', comment='`>>>second comment<<<`')
    74         url = 'http://localhost/ticket/%d#comment:2' % ticket.id
    75         orig = message % {'url': url}
    76         result = self.mod.substitute_message(orig, ignore_exc=False)
    77         self.assertNotEqual(result, orig)
    78         self.assertTrue('\nContent-Type: text/html;' in result)
    79         self.assertTrue('&lt;&lt;&lt;Description&gt;&gt;&gt;' in result)
    80         self.assertFalse('&gt;&gt;&gt;first comment&lt;&lt;&lt;' in result)
    81         self.assertTrue('&gt;&gt;&gt;second comment&lt;&lt;&lt;' in result)
     97    def _comment_ticket(self, id_, author, comment):
     98        ticket = Ticket(self.env, id_)
     99        changetime = str(to_utimestamp(ticket['changetime']))
     100        req = MockRequest(self.env, method='POST', authname=author,
     101                          path_info='/ticket/%d' % id_,
     102                          args={'action': 'leave', 'submit': '*',
     103                                'comment': comment, 'start_time': changetime,
     104                                'view_time': changetime})
     105        self.assertEqual(True, self.tktmod.match_request(req))
     106        try:
     107            self.tktmod.process_request(req)
     108            self.fail('RequestDone not raised')
     109        except RequestDone:
     110            pass
    82111
    83 
    84 class RequestAttributeTestCase(unittest.TestCase):
    85 
    86     def setUp(self):
    87         self.env = EnvironmentStub(
    88             default_data=True,
    89             enable=['trac.*', 'trachtmlnotification.*',
    90                     'trachtmlnotification.tests.*'])
    91         self.env.config.set('notification', 'mime_encoding', 'none')
    92         self.mod = HtmlNotificationModule(self.env)
    93 
    94     def tearDown(self):
    95         self.env.reset_db()
    96 
    97     def test_read_attribute_in_filter_stream(self):
    98         ticket = Ticket(self.env)
    99         ticket['status'] = 'new'
    100         ticket['summary'] = 'Blah blah blah'
    101         ticket['reporter'] = 'joe'
    102         ticket['description'] = '<<<Description>>>'
    103         ticket.insert()
    104         orig = message % {'url': 'http://localhost/ticket/%d' % ticket.id}
    105         result = self.mod.substitute_message(orig, ignore_exc=False)
    106         self.assertNotEqual(result, orig)
    107         self.assertTrue('\nContent-Type: text/html;' in result)
    108         self.assertTrue('<script>// BLAH-BLAH-BLAH</script>' in result)
     112    def test_ticket_notification(self):
     113        self._create_ticket(status='new', summary='Blah blah blah',
     114                            reporter='joe',
     115                            description='<<<Description>>>\r\n')
     116        from_addr, recipients, message = self.sender.history[-1]
     117        self.assertIn('\nContent-Type: text/html;', message)
     118        self.assertIn('&lt;&lt;&lt;Description&gt;&gt;&gt;', message)
     119        self._comment_ticket(1, 'joe', '`>>>first comment<<<`')
     120        from_addr, recipients, message = self.sender.history[-1]
     121        self.assertIn('\nContent-Type: text/html;', message)
     122        self.assertIn('&lt;&lt;&lt;Description&gt;&gt;&gt;', message)
     123        self.assertIn('&gt;&gt;&gt;first comment&lt;&lt;&lt;', message)
     124        self._comment_ticket(1, 'joe', '`>>>second comment<<<`')
     125        from_addr, recipients, message = self.sender.history[-1]
     126        self.assertIn('\nContent-Type: text/html;', message)
     127        self.assertIn('&lt;&lt;&lt;Description&gt;&gt;&gt;', message)
     128        self.assertIn('&gt;&gt;&gt;second comment&lt;&lt;&lt;', message)
    109129
    110130
     
    112132    suite = unittest.TestSuite()
    113133    suite.addTest(unittest.makeSuite(NormalTestCase))
    114     suite.addTest(unittest.makeSuite(RequestAttributeTestCase))
    115134    return suite
    116135
Note: See TracChangeset for help on using the changeset viewer.