Source

trac.plugins.traccron / src / traccron / task.py

Full commit
# -*- encoding: UTF-8 -*-
"""
Created on 28 oct. 2010

@author: thierry
"""
###############################################################################
##
##             O U T    O F    T H E    B O X    T A S K
##
###############################################################################
from datetime import datetime, timedelta
from time import time, localtime

from trac.ticket.model import Ticket
from trac.core import Component, implements
from trac.notification import NotifyEmail
from trac.util.datefmt import utc, from_utimestamp, to_utimestamp
from trac.web.chrome import ITemplateProvider
from traccron.api import ICronTask
from traccron.core import CronConfig


class HeartBeatTask(Component, ICronTask):
    """
    This is a simple task for testing purpose.
    It only write a trace in log at debug level
    """
    implements(ICronTask)

    def wake_up(self, *args):
        if len(args) > 0:
            for arg in args:
                self.env.log.debug('Heart beat: %s' % arg)
        else:
            self.env.log.debug('Heart beat: boom boom !!!')

    def getId(self):
        return 'heart_beat'

    def getDescription(self):
        return self.__doc__


class SleepingTicketNotification(NotifyEmail):

    template_name = 'sleeping_ticket_template.txt'

    def __init__(self, env):
        NotifyEmail.__init__(self, env)

    def get_recipients(self, owner):
        return ([owner], [])

    def remind(self, tiketsByOwner, delay):
        """
        Send a digest mail to ticket owner to remind him of those
        sleeping tickets
        """
        for owner in tiketsByOwner:
            # prepare the data for the email content generation
            self.data.update({
                'ticket_count': len(tiketsByOwner[owner]),
                'delay': delay
            })
            NotifyEmail.notify(self, owner, 'Sleeping ticket notification')

    def send(self, torcpts, ccrcpts):
        return NotifyEmail.send(self, torcpts, ccrcpts)


class OrphanedTicketNotification(NotifyEmail):

    template_name = 'orphaned_ticket_template.txt'

    def __init__(self, env):
        NotifyEmail.__init__(self, env)

    def get_recipients(self, reporter):
        return ([reporter], [])

    def remind(self, tiketsByReporter, delay):
        """
        Send a digest mail to the reporter to remind them
        of those orphaned tickets
        """
        for reporter in tiketsByReporter:
            # prepare the data for the email content generation
            self.data.update({
                'ticket_count': len(tiketsByReporter[reporter]),
                'delay': delay
            })
            NotifyEmail.notify(self, reporter, 'orphaned ticket notification')

    def send(self, torcpts, ccrcpts):
        return NotifyEmail.send(self, torcpts, ccrcpts)


class SleepingTicketReminderTask(Component, ICronTask, ITemplateProvider):
    """
    Remind user about sleeping ticket they are assigned to.
    """
    implements(ICronTask, ITemplateProvider)

    delay_days = 3

    select_assigned_ticket = """
        SELECT t.id , t.owner
        FROM ticket t, ticket_change tc
        WHERE t.id = tc.ticket
        AND t.status in ('new','assigned','accepted')
        AND (SELECT MAX(tc2.time)
             FROM ticket_change tc2
             WHERE tc2.ticket=tc.ticket) < %s
        GROUP BY t.id
    """

    select_orphaned_ticket = """
       SELECT t.id, t.reporter
       FROM ticket t
       WHERE t.id not in (SELECT tc.ticket
                          FROM ticket_change tc
                          WHERE tc.ticket=t.id)
       AND t.time < %s
       AND t.status = 'new'
    """

    def get_htdocs_dirs(self):
        return []

    def get_templates_dirs(self):
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]

    def _find_sleeping_ticket(self, sql, delay, dico, msg):
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        delay_time = datetime.now(utc) - timedelta(days=delay)
        cursor.execute(sql, (to_utimestamp(delay_time),))
        for ticket, recipient in cursor:
            self.env.log.info(msg % (ticket, recipient, delay))
            if recipient in dico:
                dico[recipient].append(ticket)
            else:
                dico[recipient] = [ticket]

    def remind_assigned_ticket(self, delay):
        dico = {}
        msg = 'warning sleeping ticket %d assigned to %s ' \
              'but is inactive since more than %d day'
        self._find_sleeping_ticket(self.select_assigned_ticket,
                                   delay, dico, msg)
        SleepingTicketNotification(self.env).remind(dico, delay)

    def remind_orphaned_ticket(self, delay):
        dico = {}
        msg = 'warning ticket %d is new but orphaned: %s, %d'
        self._find_sleeping_ticket(self.select_orphaned_ticket,
                                   delay, dico, msg)
        OrphanedTicketNotification(self.env).remind(dico, delay)

    def wake_up(self, *args):
        delay = SleepingTicketReminderTask.delay_days
        if len(args) > 0:
            delay = int(args[0])

        # look for ticket assigned but not touched since more that the delay
        self.remind_assigned_ticket(delay)
        self.remind_orphaned_ticket(delay)

    def getId(self):
        return 'sleeping_ticket'

    def getDescription(self):
        return self.__doc__


class BaseTicketNotification(NotifyEmail):

    def __init__(self, env, milestone):
        NotifyEmail.__init__(self, env)
        self.milestone = milestone

    def get_recipients(self, recipient):
        return ([recipient], [])

    def send(self, torcpts, ccrcpts):
        return NotifyEmail.send(self, torcpts, ccrcpts)

    def populate_unreachable_tickets_data(self, tickets):
        # we are not called if there is no tickets
        self.data['milestone'] = tickets[0]['milestone']
        due_date = from_utimestamp(tickets[0]['due'])
        self.data['due_date'] = due_date.strftime('%Y-%m-%d')

        tickets_list = ''
        for ticket in tickets:
            tickets_list += ticket['summary'] + '\n'
            tickets_list += self.env.abs_href.ticket(ticket['ticket']) + '\n'
            tickets_list += '\n'

        self.data['tickets_list'] = tickets_list

    def notify_opened_ticket(self, recipient, tickets):
        """
        Send a digest mail to recipients (e.g. ticket owner and reporter)
        about ticket still opened
        """
        self.populate_unreachable_tickets_data(tickets)
        subject = 'Milestone %s with still opened ticket' % self.milestone
        NotifyEmail.notify(self, recipient, subject)
        self.env.log.debug('notify opened ticket: %s, %s, %s' % (
                           recipient, self.milestone, self.__class__.__name__))


class ReporterOpenedTicketNotification(BaseTicketNotification):
    """
    Notify reporter about an opened ticket in a near milestone
    """
    template_name = "opened_ticket_for_reporter_template.txt"

    def __init__(self, env, milestone):
        BaseTicketNotification.__init__(self, env, milestone)


class OwnerOpenedTicketNotification(BaseTicketNotification):
    """
    Notify owner about an opened ticket in a near milestone
    """
    template_name = "opened_ticket_for_owner_template.txt"

    def __init__(self, env, milestone):
        BaseTicketNotification.__init__(self, env, milestone)


class UnreachableMilestoneNotification(BaseTicketNotification):
    """
    Notify the specified person (ex: admin, release manager) that a milestone
    is about to closed but there still are opened ticket
    """
    template_name = 'unreachable_milestone_template.txt'

    def __init__(self, env, milestone):
        BaseTicketNotification.__init__(self, env, milestone)
        self.cronconf = CronConfig(self.env)

    def get_recipients(self, milestone):
        reclist = self.cronconf.get_unreachable_milestone_task_recipient_list()
        return (reclist, [])

    def notify_unreachable_milestone(self, tickets):
        """
        Send a digest mail listing all tickets still opened in the milestone
        """
        self.populate_unreachable_tickets_data(tickets)
        subject = "Milestone %s still has opened ticket" % self.milestone
        NotifyEmail.notify(self, self.milestone, subject)
        self.env.log.debug('notify unreachable milestone: %s, %s' % (
                           self.milestone, self.__class__.__name__))


class UnreachableMilestoneTask(Component, ICronTask, ITemplateProvider):
    """
    Send notification about near milestone with opened ticked
    """
    implements(ICronTask, ITemplateProvider)

    select_near_milestone_ticket = """
        SELECT t.id , t.owner, t.reporter, t.milestone, t.summary, m.due
        FROM ticket t, milestone m
        WHERE t.milestone = m.name
        AND m.due < %s
    """

    def __init__(self):
        self.cronconf = CronConfig(self.env)

    def get_htdocs_dirs(self):
        return []

    def get_templates_dirs(self):
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]

    def _set_recipient_data(self, dico, recipient, milestone, ticket_data):
        dico.setdefault(recipient, {}).__setitem__(milestone, [ticket_data])
        dico[recipient].setdefault(milestone, []).append(ticket_data)

    def remind_unreachable_recipients(self, dico):
        """
        Send notification for each milestone
        """
        for milestone in dico:
            notifier = UnreachableMilestoneNotification(self.env, milestone)
            notifier.notify_unreachable_milestone(dico[milestone])

    def remind_recipient(self, dico, klass):
        """
        Send notification for owner or reporter
        """
        for recipient in dico:
            _dico = dico[recipient]
            for milestone in _dico:
                notifier = klass(self.env, milestone)
                notifier.notify_opened_ticket(recipient, _dico[milestone])

    def wake_up(self, *args):
        delay = 3
        if len(args) > 0:
            delay = int(args[0])

        # look opened ticket in near milestone
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        # select ticket whom milestone are due in less than specified delay
        delay_time = datetime.now(utc) + timedelta(days=delay)
        cursor.execute(self.select_near_milestone_ticket,
                       (to_utimestamp(delay_time),))

        dico, dico_owner, dico_reporter = {}, {}, {}
        for ticket, owner, reporter, milestone, summary, due in cursor:
            msg = 'warning ticket %d will probably miss its milestone %s' % (
                  ticket, milestone)
            self.env.log.info(msg)
            ticket_data = {
                'ticket': ticket,
                'owner': owner,
                'reporter': reporter,
                'milestone': milestone,
                'summary': summary,
                'due': due
            }
            dico.setdefault(milestone, []).append(ticket_data)
            self._set_recipient_data(dico_owner, owner, milestone, ticket_data)
            self._set_recipient_data(dico_reporter, reporter, milestone,
                                     ticket_data)

        self.remind_unreachable_recipients(dico)
        self.remind_recipient(dico_owner, OwnerOpenedTicketNotification)
        self.remind_recipient(dico_reporter, ReporterOpenedTicketNotification)

    def getId(self):
        return self.cronconf.UNREACHABLE_MILESTONE_TASK_BASEKEY

    def getDescription(self):
        return self.__doc__


class AutoPostponeTask(Component, ICronTask):
    """
    Scan closed milestone for still opened ticket then posptone those tickets
    to the next milestone
    """

    implements(ICronTask)

    def wake_up(self, *args):
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        # find still opened more recent milestone
        # select ticket whom milestone are due in less than specified delay
        cursor.execute("""
            SELECT m.name  FROM milestone m
            WHERE  m.completed is NULL or m.completed = 0
            AND m.due not NULL and m.due > 0
            ORDER BY m.due ASC LIMIT 1
        """)
        next_milestone = None
        for name, in cursor:
            next_milestone = name

        # select ticket whom milestone are due in less than specified delay
        cursor.execute("""
            SELECT t.id , t.milestone  FROM ticket t, milestone m
            WHERE t.status != 'closed'
            AND    t.milestone = m.name
            AND    m.completed not NULL and m.completed > 0
        """)
        if next_milestone:
            for id, milestone in cursor:
                mess = "ticket %s is opened in closed milestone %s. Should postpone this ticket to %s" % (id, milestone, next_milestone)
                self.env.log.debug(mess)
                ticket = Ticket(self.env, id)
                ticket.populate({'milestone': next_milestone})
                ticket.save_changes(self.getId(), mess)
        else:
            self.env.log.debug("No opened milestone found. Cannot postpone tickets")

    def getId(self):
        return "auto_postpone"

    def getDescription(self):
        return self.__doc__