Commits

Tetsuya Morimoto committed 8f8e3fd

added TicketDeadlineTask components and its tests
refactored some sql syntax and a issue for packaging

Comments (0)

Files changed (7)

 include MANIFEST.in
 include LICENSE
 include README.txt
-recursive-include src *.py *.txt *.rss *.html *.css *.js *.png *.pot *.po *.mo
+recursive-include traccron *.py *.txt *.rss *.html *.css *.js *.png *.pot *.po *.mo
 recursive-include tests *.py

tests/conftest.py

 from traccron.task import HeartBeatTask
 from traccron.task import SleepingTicketReminderTask
 from traccron.task import UnreachableMilestoneTask
+from traccron.task import TicketDeadlineTask
 
 
 def pytest_addoption(parser):
         'heart_beat_task': HeartBeatTask(env),
         'sleeping_ticket_reminder_task': SleepingTicketReminderTask(env),
         'unreachable_milestone_task': UnreachableMilestoneTask(env),
+        'ticket_deadline_task': TicketDeadlineTask(env),
     }
     return env, component
 

tests/test_core.py

     from traccron.task import HeartBeatTask
     from traccron.task import SleepingTicketReminderTask
     from traccron.task import UnreachableMilestoneTask
+    from traccron.task import TicketDeadlineTask
     task_list = core.getTaskList()
     task_classes = [AutoPostponeTask, HeartBeatTask,
-                    SleepingTicketReminderTask, UnreachableMilestoneTask]
+                    SleepingTicketReminderTask, UnreachableMilestoneTask,
+                    TicketDeadlineTask]
     assert _isinstances(task_list, task_classes)
 
 

tests/test_ticket_deadline_task.py

+# -*- coding: utf-8 -*-
+import logging
+from datetime import datetime, timedelta
+
+import pytest
+from utils import has_log_message
+
+now = datetime.now()
+after_1days = now + timedelta(days=1)
+after_3days = now + timedelta(days=3)
+
+
+def pytest_funcarg__ticket_deadline_task(request, component):
+    ticket_deadline_task = component['ticket_deadline_task']
+    return ticket_deadline_task
+
+
+def test_ticket_deadline_task_getId(ticket_deadline_task):
+    assert 'ticket_deadline' == ticket_deadline_task.getId()
+
+
+def _create_tickets(env, date_field_name, date_field):
+    from utils import update_value, create_ticket
+    env.config.set('ticket-custom', date_field_name, 'text')
+
+    @update_value(date_field_name, date_field)
+    @update_value('status', 'new')
+    def create_new_ticket_with_duedate(env, **kwargs):
+        return create_ticket(env, **kwargs)
+
+    @update_value(date_field_name, date_field)
+    @update_value('status', 'closed')
+    def create_closed_ticket_with_duedate(env, **kwargs):
+        return create_ticket(env, **kwargs)
+
+    t1 = create_new_ticket_with_duedate(env)
+    t2 = create_closed_ticket_with_duedate(env)
+    return t1, t2
+
+
+@pytest.mark.parametrize(('args', 'due_date_value', 'is_remind'), [
+    (('my_due_date', '%Y/%m/%d', '3'), after_1days.strftime('%Y/%m/%d'), True),
+    (('deadline', '%Y-%m-%d', '3'), after_1days.strftime('%Y-%m-%d'), True),
+    (('deadline', '%Y-%m-%d', '1'), after_3days.strftime('%Y-%m-%d'), False),
+])
+def test_ticket_deadline_task_wake_up(ticket_deadline_task, caplog,
+                                      args, due_date_value, is_remind):
+    env = ticket_deadline_task.env
+    caplog.setLevel(logging.DEBUG, logger=env.log.name)
+
+    _create_tickets(env, args[0], due_date_value)
+    ticket_deadline_task.wake_up(*args)
+    expected_messages = [
+        'applying config',
+        'stop existing ticker',
+        'ticker is disabled',
+        'action controllers for ticket workflow',
+        'wake_up',
+        'need_notify',
+    ]
+
+    if is_remind:
+        expected_messages.extend([
+            'remind: 1',  # depends on _create_tickets()
+        ])
+    assert has_log_message(caplog, expected_messages)
     :pypi:pytest
     :pypi:pytest-pep8
     :pypi:pytest-capturelog
-commands = py.test -v --pep8 src/traccron tests
+commands = py.test -v --pep8 traccron tests
 
 [testenv:py24]
 deps =
     :pypi:pytest-pep8
     :pypi:pytest-capturelog
     :pypi:pysqlite
-commands = py.test -v --pep8 src/traccron tests
+commands = py.test -v --pep8 traccron tests
 from time import time, localtime
 
 from trac.ticket.model import Ticket
+from trac.config import BoolOption, IntOption, Option
 from trac.core import Component, implements
 from trac.notification import NotifyEmail
+from trac.util.text import wrap
 from trac.util.datefmt import utc, from_utimestamp, to_utimestamp
 from trac.web.chrome import ITemplateProvider
 from traccron.api import ICronTask
     select_next_milestone = """
         SELECT m.name, m.completed, m.due
         FROM milestone m
-        WHERE m.completed is NULL or m.completed = 0
-        AND m.due not NULL and m.due > 0
+        WHERE (m.completed is NULL OR m.completed = 0)
+        AND m.due is not NULL
+        AND m.due > 0
         ORDER BY m.due ASC LIMIT 1
     """
 
         FROM ticket t, milestone m
         WHERE t.status != 'closed'
         AND t.milestone = m.name
-        AND m.completed not NULL and m.completed > 0
+        AND m.completed is not NULL and m.completed > 0
     """
 
     def wake_up(self, *args):
 
     def getDescription(self):
         return self.__doc__
+
+
+class TicketDeadlineNotification(NotifyEmail):
+
+    template_name = 'notify_ticket_near_deadline.txt'
+    column_length = 75
+
+    def __init__(self, env):
+        NotifyEmail.__init__(self, env)
+        ambiguous_char_width = env.config.get('notification',
+                                              'ambiguous_char_width',
+                                              'single')
+        self.ambiwidth = (1, 2)[ambiguous_char_width == 'double']
+
+    def get_recipients(self, ticket):
+        torcpts = ticket.values.get('owner')
+        ccrcpts = set([ticket.values.get('reporter')])
+        cc_users = ticket.values.get('cc')
+        if cc_users is not None:
+            for cc_user in cc_users.split(','):
+                ccrcpts.add(cc_user.strip())
+        self.env.log.debug('get_recipients: %s, %s' % (torcpts, ccrcpts))
+        return [torcpts], list(ccrcpts)
+
+    def set_template_data(self, ticket):
+        ticket_values = ticket.values.copy()
+        ticket_values['id'] = ticket.id
+        ticket_values['link'] = self.env.abs_href.ticket(ticket.id)
+        ticket_values['description'] = wrap(
+            ticket_values.get('description', ''), self.column_length,
+            initial_indent=' ', subsequent_indent=' ', linesep='\n',
+            ambiwidth=self.ambiwidth)
+        self.data.update({
+            'ticket': ticket_values,
+        })
+
+    def make_subject(self, ticket):
+        from genshi.template.text import NewTextTemplate
+        template = self.config.get('notification', 'ticket_subject_template')
+        template = NewTextTemplate(template.encode('utf8'))
+        data = {
+            'prefix': '[Deadline Warning]',
+            'summary': ticket['summary'],
+            'ticket': ticket,
+            'env': self.env,
+        }
+        return template.generate(**data).render('text', encoding=None).strip()
+
+    def remind(self, ticket_id):
+        self.env.log.debug('remind: %s' % ticket_id)
+        ticket = Ticket(self.env, ticket_id)
+        self.set_template_data(ticket)
+        subject = self.make_subject(ticket)
+        NotifyEmail.notify(self, ticket, subject)
+
+    def send(self, torcpts, ccrcpts):
+        self.env.log.debug('send: %s, %s' % (torcpts, ccrcpts))
+        return NotifyEmail.send(self, torcpts, ccrcpts)
+
+
+class TicketDeadlineTask(Component):
+    """
+    Remind relevant users when the ticket is near deadline.
+    """
+    implements(ICronTask, ITemplateProvider)
+
+    days_before = IntOption('traccron', 'days_before', 3,
+        'Notify the days before the ticket deadline.')
+
+    date_field = Option('traccron', 'date_field', None,
+        'Specify the custom field name for deadline.')
+
+    date_format = Option('traccron', 'date_format', '%Y-%m-%d',
+        """Specify the date format stored in table as string
+        so that it can convert string into datetime.""")
+
+    select_ticket_near_deadline = """
+        SELECT t.id, t.status, c.value
+        FROM ticket t, ticket_custom c
+        WHERE t.id = c.ticket
+        AND t.status <> 'closed'
+        AND c.name = %s
+        AND c.value is not NULL AND c.value <> ''
+    """
+
+    def _get_params(self, args):
+        date_field = self.date_field
+        date_format = self.date_format
+        days_before = self.days_before
+        if len(args) == 3:  # for debug/test
+            date_field, date_format, days_before = args
+        _msg = 'wake_up: %s, %s, %s'
+        self.env.log.debug(_msg % (date_field, date_format, days_before))
+        return date_field, date_format, days_before
+
+    def get_tickets(self, date_field):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute(self.select_ticket_near_deadline, (date_field,))
+        for values in cursor:
+            yield values
+
+    def need_notify(self, now, deadline, date_format, days_before):
+        try:
+            due_date = datetime.strptime(deadline, date_format)
+            due_date = due_date - timedelta(days=int(days_before))
+        except:
+            self.env.log.error('cannot convert deadline: %s' % due_date)
+        else:
+            self.env.log.debug('need_notify: %s, %s' % (now, due_date))
+            return now >= due_date
+        return False
+
+    # ICronTask methods
+    def wake_up(self, *args):
+        date_field, date_format, days_before = self._get_params(args)
+        if date_field is None:
+            self.env.log.info('wake_up: date_field is not specified')
+            return
+
+        now = datetime.now()
+        for ticket_id, status, deadline in self.get_tickets(date_field):
+            if self.need_notify(now, deadline, date_format, days_before):
+                TicketDeadlineNotification(self.env).remind(ticket_id)
+
+    def getId(self):
+        return 'ticket_deadline'
+
+    def getDescription(self):
+        return self.__doc__
+
+    # ITemplateProvider methods
+    def get_templates_dirs(self):
+        from pkg_resources import resource_filename
+        return [resource_filename(__name__, 'templates')]
+
+    def get_htdocs_dirs(self):
+        return []

traccron/templates/notify_ticket_near_deadline.txt

+Hi $ticket.owner,
+
+Your ticket is near the deadline.
+
+$ticket.summary
+
+$ticket.description
+
+-- 
+${_('Ticket URL: <%(link)s>', link=ticket.link)}
+$project.name <${project.url or abs_href()}>
+$project.descr