<commit_before># -*- coding: utf-8 -*-
##
##
## This file is part of Indico.
## Copyright (C) 2002 - 2012 European Organization for Nuclear Research (CERN).
##
## Indico is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License as
## published by the Free Software Foundation; either version 3 of the
## License, or (at your option) any later version.
##
## Indico is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
## General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Indico;if not, see <http://www.gnu.org/licenses/>.

import copy, logging, os, time
from dateutil import rrule
from datetime import timedelta
import urllib
import zope.interface

# Required by specific tasks
from MaKaC.user import Avatar
from MaKaC.common import Config, info
# end required

from persistent import Persistent
from BTrees.IOBTree import IOBTree

from indico.util.fossilize import fossilizes, Fossilizable
from indico.util.date_time import int_timestamp, format_date, format_time, format_datetime
from indico.modules.scheduler.fossils import ITaskFossil, ITaskOccurrenceFossil
from indico.modules.scheduler import base, TaskDelayed
from indico.core.index import IUniqueIdProvider, IIndexableByArbitraryDateTime
from MaKaC.common.utils import getEmailList
from MaKaC.plugins.base import Observable
from MaKaC.accessControl import AccessWrapper
from MaKaC import schedule
from MaKaC.common.utils import getLocationInfo
from MaKaC.common.TemplateExec import render

"""
Defines base classes for tasks, and some specific tasks as well
"""

class TimedEvent(Persistent, Fossilizable):

    zope.interface.implements(IUniqueIdProvider,
                              IIndexableByArbitraryDateTime)

    def getIndexingDateTime(self):
        return int_timestamp(self._getCurrentDateTime())

    def _getCurrentDateTime(self):
        # just get current date/time
        return base.TimeSource.get().getCurrentTime()

    def __conform__(self, proto):

        if proto == IIndexableByArbitraryDateTime:
            return self.getIndexingDateTime()
        else:
            return None


class BaseTask(TimedEvent):
    """
    A base class for tasks.
    `expiryDate` is the last point in time when the task can run. A task will
    refuse to run if current time is past `expiryDate`
    """

    fossilizes(ITaskFossil)

    # seconds to consider a task AWOL
    _AWOLThresold = 6000

    def __cmp__(self, obj):
        if obj == None:
            return 1
        elif (not isinstance(obj, BaseTask) or \
              self.id == obj.id and self.id == None):
            return cmp(hash(self), hash(obj))
        else:
            return cmp(self.id, obj.id)

    def __init__(self, expiryDate=None):
        self.createdOn = self._getCurrentDateTime()
        self.expiryDate = expiryDate
        self.typeId = self.__class__.__name__
        self.id = None
        self.reset()
        self.status = 0

        self.startedOn = None
        self.endedOn = None

    def reset(self):
        '''Resets a task to its state before being run'''

        self.running = False
        self.onRunningListSince = None

    # Time methods

    def getCreatedOn(self):
        return self.createdOn

    def getEndedOn(self):
        return self.endedOn

    def setEndedOn(self, dateTime):
        self.endedOn = dateTime

    def getStartedOn(self):
        return self.startedOn

    def setStartedOn(self, dateTime):
        self.startedOn = dateTime

    def setOnRunningListSince(self, sometime):
        self.onRunningListSince = sometime
        self._p_changed = 1

    def getOnRunningListSince(self):
        return self.onRunningListSince

    def setStatus(self, newstatus):
        self.getLogger().info("%s set status %s" % (self, base.status(newstatus)))
        self.status = newstatus

    def getStatus(self):
        return self.status

    def getId(self):
        return self.id

    def getUniqueId(self):
        return "task%s" % self.id

    def getTypeId(self):
        return self.typeId

    def initialize(self, newid, newstatus):
        self.id = newid
        self.setStatus(newstatus)

    def plugLogger(self, logger):
        self._v_logger = logger

    def getLogger(self):
        if not hasattr(self, '_v_logger') or not self._v_logger:
            self._v_logger = logging.getLogger('task/%s' % self.typeId)
        return self._v_logger

    def prepare(self):
        """
        This information will be saved regardless of the task being repeated or not
        """

        curTime = self._getCurrentDateTime()
        tsDiff = int_timestamp(self.getStartOn()) - int_timestamp(curTime)

        if tsDiff > 0:
            self.getLogger().debug('Task %s will wait for some time. (%s) > (%s)' % \
                                   (self.id, self.getStartOn(), curTime))
            base.TimeSource.get().sleep(tsDiff)

        if self.expiryDate and curTime > self.expiryDate:
            self.getLogger().warning(
                'Task %s will not be executed, expiryDate (%s) < current time (%s)' % \
                (self.id, self.expiryDate, curTime))
            return False

        self.startedOn = curTime
        self.running = True
        self.setStatus(base.TASK_STATUS_RUNNING)

    def start(self, delay):
        self._executionDelay = delay
        try:
            self.run()
            self.endedOn = self._getCurrentDateTime()
        finally:
            self.running = False

    def tearDown(self):
        """
        If a task needs to do something once it has run and been removed
        from runningList, overload this method
        """
        pass

    def __str__(self):
        return "[%s:%s|%s]" % (self.typeId, self.id,
                               base.status(self.status))


class OneShotTask(BaseTask):
    """
    Tasks that are executed only once
    """

    def __init__(self, startDateTime, expiryDate = None):
        super(OneShotTask, self).__init__(expiryDate = expiryDate)
        self.startDateTime = startDateTime

    def getStartOn(self):
        return self.startDateTime

    def setStartOn(self, newtime):
        self.startDateTime = newtime

    def suicide(self):
        self.setStatus(base.TASK_STATUS_TERMINATED)
        self.setEndedOn(self._getCurrentDateTime())


class PeriodicTask(BaseTask):
    """
    Tasks that should be executed at regular intervals
    """

    def __init__(self, frequency, **kwargs):
        """
        :param frequency: a valid dateutil frequency specifier (DAILY, HOURLY, etc...)
        """
        super(PeriodicTask, self).__init__()

        self._nextOccurrence = None
        self._lastFinishedOn = None
        self._occurrences = IOBTree()
        self._occurrenceCount = 0
        self._repeat = True

        if 'dtstart' not in kwargs:
            kwargs['dtstart'] = self._getCurrentDateTime()

        self._rule = rrule.rrule(
            frequency,
            **kwargs
            )

        self._nextOccurrence = self._rule.after(
            kwargs['dtstart'] - timedelta(seconds=1),
            inc = True)



    def start(self, delay):
        super(PeriodicTask, self).start(delay)

    def tearDown(self):
        super(PeriodicTask, self).tearDown()

    def setNextOccurrence(self, dateAfter = None):

        if not self._nextOccurrence:
            # if it's None already, it means there's no "future"
            return

        if not dateAfter:
            dateAfter = self._getCurrentDateTime()

        # find next date after
        nextOcc = self._rule.after(max(self._nextOccurrence, dateAfter),
                                   inc = False)

        # repeat process till a satisfactory date is found
        # or there is nothing left to check
        #while nextOcc and nextOcc < dateAfter:
        #    nextOcc = self._rule.after(nextOcc,
        #                               inc = False)

        self._nextOccurrence = nextOcc
        return nextOcc

    def getStartOn(self):
        return self._nextOccurrence

    def getLastFinishedOn(self):
        return self._lastFinishedOn

    def addOccurrence(self, occurrence):
        occurrence.setId(self._occurrenceCount)
        self._occurrences[self._occurrenceCount] = occurrence
        self._occurrenceCount += 1

    def dontComeBack(self):
        self._repeat = False

    def shouldComeBack(self):
        return self._repeat


class PeriodicUniqueTask(PeriodicTask):
    """
    Singleton periodic tasks: no two or more PeriodicUniqueTask of this
    class will be queued or running at the same time (TODO)
    """
    # TODO: implement this


class TaskOccurrence(TimedEvent):
    """
    Wraps around a PeriodicTask object, and represents an occurrence of this task
    """

    fossilizes(ITaskOccurrenceFossil)


    def __init__(self, task):
        self._task = task
        self._startedOn = task.getStartedOn()
        self._endedOn = task.getEndedOn()
        self._id = None

    def getId(self):
        return self._id

    def getUniqueId(self):
        return "%s:%s" % (self._task.getUniqueId(), self._id)

    def setId(self, occId):
        self._id = occId

    def getStartedOn(self):
        return self._startedOn

    def getEndedOn(self):
        return self._endedOn

    def getTask(self):
        return self._task


class CategoryStatisticsUpdaterTask(PeriodicUniqueTask):
    '''Updates statistics associated with categories
    '''

    # seconds to consider a task AWOL
    _AWOLThresold = 15000

    def __init__(self, cat, frequency, **kwargs):
        super(CategoryStatisticsUpdaterTask, self).__init__(frequency,
                                                            **kwargs)
        self._cat = cat

    def run(self):
        from MaKaC.statistics import CategoryStatistics
        CategoryStatistics.updateStatistics(self._cat,
                                            self.getLogger())


# TODO: Isolate CERN Specific
class FoundationSyncTask(PeriodicUniqueTask):
    """
    Synchronizes room data (along with associated room managers
    and equipment) with Foundation database.

    Also, updates list of CERN Official Holidays

    (This is object for a task class)
    """
    def __init__(self, frequency, **kwargs):
        super(FoundationSyncTask, self).__init__(frequency, **kwargs)

    def run(self):
        from MaKaC.common.FoundationSync.foundationSync import FoundationSync
        FoundationSync(self.getLogger()).doAll(None)


class SendMailTask(OneShotTask):
    """
    """
    def __init__(self, startDateTime):
        super(SendMailTask, self).__init__(startDateTime)
        self.fromAddr = ""
        self.toAddr = []
        self.toUser = []
        self.ccAddr = []
        self.subject = ""
        self.text = ""
        self.smtpServer = Config.getInstance().getSmtpServer()

    def _prepare(self, check):
        """
        Overloaded by descendants
        """

    def run(self, check=True):
        import smtplib
        from MaKaC.webinterface.mail import GenericMailer, GenericNotification

        # prepare the mail
        send = self._prepare(check=check)

        # _prepare decided we shouldn't send the mail?
        if not send:
            return

        # just in case some ill-behaved code generates empty addresses
        addrs = list(smtplib.quoteaddr(x) for x in self.toAddr if x)
        ccaddrs = list(smtplib.quoteaddr(x) for x in self.ccAddr if x)

        if len(addrs) + len(ccaddrs) == 0:
            self.getLogger().warning("Attention: no recipients, mail won't be sent")
        else:
            self.getLogger().info("Sending mail To: %s, CC: %s" % (addrs, ccaddrs))

        for user in self.toUser:
            addrs.append(smtplib.quoteaddr(user.getEmail()))

        if addrs or ccaddrs:
            GenericMailer.send(GenericNotification({"fromAddr": self.fromAddr,
                                                    "toList": addrs,
                                                    "ccList": ccaddrs,
                                                    "subject": self.subject,
                                                    "body": self.text }))

    def setFromAddr(self, addr):
        self.fromAddr = addr
        self._p_changed = 1

    def getFromAddr(self):
        return self.fromAddr

    def addToAddr(self, addr):
        if not addr in self.toAddr:
            self.toAddr.append(addr)
            self._p_changed = 1

    def addCcAddr(self, addr):
        if not addr in self.ccAddr:
            self.ccAddr.append(addr)
            self._p_changed = 1

    def removeToAddr(self, addr):
        if addr in self.toAddr:
            self.toAddr.remove(addr)
            self._p_changed = 1

    def setToAddrList(self, addrList):
        """Params: addrList -- addresses of type : list of str."""
        self.toAddr = addrList
        self._p_changed = 1

    def getToAddrList(self):
        return self.toAddr

    def setCcAddrList(self, addrList):
        """Params: addrList -- addresses of type : list of str."""
        self.ccAddr = addrList
        self._p_changed = 1

    def getCcAddrList(self):
        return self.ccAddr

    def addToUser(self, user):
        if not user in self.toUser:
            self.toUser.append(user)
            self._p_changed = 1

    def removeToUser(self, user):
        if user in self.toUser:
            self.toUser.remove(user)
            self._p_changed = 1

    def getToUserList(self):
        return self.toUser

    def setSubject(self, subject):
        self.subject = subject

    def getSubject(self):
        return self.subject

    def setText(self, text):
        self.text = text

    def getText(self):
        return self.text


class AlarmTask(SendMailTask):
    """
    implement an alarm componment
    """
    def __init__(self, conf, confRelId, startDateTime=None, relative=None):

        self.conf = conf
        super(AlarmTask, self).__init__(startDateTime)

        if not startDateTime:
            startDateTime = self.conf.getStartDate() - relative

        self.setRelative(relative)
        self.setStartOn(startDateTime)

        self.text = ""
        self.note = ""
        self.confSumary = False
        self.toAllParticipants = False
        self._confRelId = confRelId
        self._relative = relative

    def getConference(self):
        return self.conf

    def getConfRelativeId(self):
        return self._confRelId

    def getToAllParticipants(self):
        try:
            return self.toAllParticipants
        except:
            self.toAllParticipants = False
            return self.toAllParticipants

    def setRelative(self, relative):
        self._relative = relative

    def setToAllParticipants(self, toAllParticipants):
        self.toAllParticipants = toAllParticipants

    def clone(self, conference):
        """
        Clone the alarm, changing only the conference
        """
        alarm = conference.newAlarm(self.getTimeBefore() or self.getStartOn())
        for addr in self.getToAddrList():
            alarm.addToAddr(addr)
        alarm.setFromAddr(self.getFromAddr())
        alarm.setSubject(self.getSubject())
        alarm.setConfSummary(self.getConfSummary())
        alarm.setNote(self.getNote())
        alarm.setText(self.getText())
        alarm.setToAllParticipants(self.getToAllParticipants())
        return alarm

    def getTimeBefore(self):
        # TODO: remove this
        if not hasattr(self, '_relative'):
            self._relative = None
        return self._relative

    def addToUser(self, user):
        super(AlarmTask, self).addToUser(user)
        if isinstance(user, Avatar):
            user.linkTo(self, "to")

    def removeToUser(self, user):
        super(AlarmTask, self).removeToUser(user)
        if isinstance(user, Avatar):
            user.unlinkTo(self, "to")

    def getText(self):
        return self.text

    def getLocator(self):
        d = self.conf.getLocator()
        d["alarmId"] = self.getConfRelativeId()
        return d

    def canAccess(self, aw):
        return self.conf.canAccess(aw)

    def canModify(self, aw):
        return self.conf.canModify(aw)

    def __getVarsTextTpl(self, conf):
        tvars = {}
        tvars['entries'] = []
        confSchedule = conf.getSchedule()
        entrylist = confSchedule.getEntries()
        for entry in entrylist:
            if type(entry) is schedule.BreakTimeSchEntry:
                newItem = entry
            else:
                newItem = entry.getOwner()
            tvars['entries'].append(newItem)
        tvars["conf"] = conf
        styleMgr = info.HelperMaKaCInfo.getMaKaCInfoInstance().getStyleManager()
        tvars['INCLUDE'] = os.path.join(styleMgr.getBaseTPLPath(), 'include')
        tvars['getTime'] = lambda date : format_time(date.time())
        tvars['isTime0H0M'] = lambda date : (date.hour, date.minute) == (0,0)
        tvars['getDate'] = lambda date : format_date(date, format='yyyy-MM-dd')
        tvars['prettyDate'] = lambda date : format_date(date, format='full')
        tvars['getLocationInfo'] = lambda item: getLocationInfo(item, False)
        from MaKaC.conference import SessionSlot, AcceptedContribution
        tvars['getItemType'] = lambda item: "Break" if isinstance(item, schedule.BreakTimeSchEntry) \
                                            else ("Session" if isinstance(item, SessionSlot) \
                                                  else ("Contribution" if isinstance(item, AcceptedContribution) \
                                                                                else item.__class__.__name__))
        return tvars

    def _setMailText(self):
        text = self.text
        if self.note:
            text = text + "Note: %s" % self.note
        if self.confSumary:
            tplDir = Config.getInstance().getTPLDir()
            text += render(os.path.join(tplDir, "events", "Text.tpl"), self.__getVarsTextTpl(self.conf))

        super(AlarmTask, self).setText(text)

    def setNote(self, note):
        self.note = note
        self._setMailText()
        self._p_changed=1

    def getNote(self):
        return self.note

    def setConfSummary(self, val):
        self.confSumary = val
        self._setMailText()
        self._p_changed=1

    def getConfSummary(self):
        return self.confSumary

    def _prepare(self, check = True):

        # Date checks...
        if check:
            from MaKaC.conference import ConferenceHolder
            if not ConferenceHolder().hasKey(self.conf.getId()):
                self.getLogger().warning("Conference %s no longer exists! "
                                     "Deleting alarm." % self.conf.getId())
                self.conf.removeAlarm(self)
                self.suicide()
            elif self.conf.getStartDate() <= self._getCurrentDateTime():
                self.getLogger().warning("Conference %s already started. "
                                     "Deleting alarm." % self.conf.getId())
                self.conf.removeAlarm(self)
                self.suicide()
                return False

        # Email
        startDateTime = format_datetime(self.conf.getAdjustedStartDate(), format="EEEE d MMMM yyyy 'at' H:mm")
        self.setSubject( _("Event reminder: %s (%s (%s))") % (self.conf.getTitle(), startDateTime, self.conf.getTimezone()))
        try:
            locationText = self.conf.getLocation().getName()<commit_msg>[FIX] Use default date/time format<commit_after># -*- coding: utf-8 -*-
##
##
## This file is part of Indico.
## Copyright (C) 2002 - 2012 European Organization for Nuclear Research (CERN).
##
## Indico is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License as
## published by the Free Software Foundation; either version 3 of the
## License, or (at your option) any later version.
##
## Indico is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
## General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Indico;if not, see <http://www.gnu.org/licenses/>.

import copy, logging, os, time
from dateutil import rrule
from datetime import timedelta
import urllib
import zope.interface

# Required by specific tasks
from MaKaC.user import Avatar
from MaKaC.common import Config, info
# end required

from persistent import Persistent
from BTrees.IOBTree import IOBTree

from indico.util.fossilize import fossilizes, Fossilizable
from indico.util.date_time import int_timestamp, format_date, format_time, format_datetime
from indico.modules.scheduler.fossils import ITaskFossil, ITaskOccurrenceFossil
from indico.modules.scheduler import base, TaskDelayed
from indico.core.index import IUniqueIdProvider, IIndexableByArbitraryDateTime
from MaKaC.common.utils import getEmailList
from MaKaC.plugins.base import Observable
from MaKaC.accessControl import AccessWrapper
from MaKaC import schedule
from MaKaC.common.utils import getLocationInfo
from MaKaC.common.TemplateExec import render

"""
Defines base classes for tasks, and some specific tasks as well
"""

class TimedEvent(Persistent, Fossilizable):

    zope.interface.implements(IUniqueIdProvider,
                              IIndexableByArbitraryDateTime)

    def getIndexingDateTime(self):
        return int_timestamp(self._getCurrentDateTime())

    def _getCurrentDateTime(self):
        # just get current date/time
        return base.TimeSource.get().getCurrentTime()

    def __conform__(self, proto):

        if proto == IIndexableByArbitraryDateTime:
            return self.getIndexingDateTime()
        else:
            return None


class BaseTask(TimedEvent):
    """
    A base class for tasks.
    `expiryDate` is the last point in time when the task can run. A task will
    refuse to run if current time is past `expiryDate`
    """

    fossilizes(ITaskFossil)

    # seconds to consider a task AWOL
    _AWOLThresold = 6000

    def __cmp__(self, obj):
        if obj == None:
            return 1
        elif (not isinstance(obj, BaseTask) or \
              self.id == obj.id and self.id == None):
            return cmp(hash(self), hash(obj))
        else:
            return cmp(self.id, obj.id)

    def __init__(self, expiryDate=None):
        self.createdOn = self._getCurrentDateTime()
        self.expiryDate = expiryDate
        self.typeId = self.__class__.__name__
        self.id = None
        self.reset()
        self.status = 0

        self.startedOn = None
        self.endedOn = None

    def reset(self):
        '''Resets a task to its state before being run'''

        self.running = False
        self.onRunningListSince = None

    # Time methods

    def getCreatedOn(self):
        return self.createdOn

    def getEndedOn(self):
        return self.endedOn

    def setEndedOn(self, dateTime):
        self.endedOn = dateTime

    def getStartedOn(self):
        return self.startedOn

    def setStartedOn(self, dateTime):
        self.startedOn = dateTime

    def setOnRunningListSince(self, sometime):
        self.onRunningListSince = sometime
        self._p_changed = 1

    def getOnRunningListSince(self):
        return self.onRunningListSince

    def setStatus(self, newstatus):
        self.getLogger().info("%s set status %s" % (self, base.status(newstatus)))
        self.status = newstatus

    def getStatus(self):
        return self.status

    def getId(self):
        return self.id

    def getUniqueId(self):
        return "task%s" % self.id

    def getTypeId(self):
        return self.typeId

    def initialize(self, newid, newstatus):
        self.id = newid
        self.setStatus(newstatus)

    def plugLogger(self, logger):
        self._v_logger = logger

    def getLogger(self):
        if not hasattr(self, '_v_logger') or not self._v_logger:
            self._v_logger = logging.getLogger('task/%s' % self.typeId)
        return self._v_logger

    def prepare(self):
        """
        This information will be saved regardless of the task being repeated or not
        """

        curTime = self._getCurrentDateTime()
        tsDiff = int_timestamp(self.getStartOn()) - int_timestamp(curTime)

        if tsDiff > 0:
            self.getLogger().debug('Task %s will wait for some time. (%s) > (%s)' % \
                                   (self.id, self.getStartOn(), curTime))
            base.TimeSource.get().sleep(tsDiff)

        if self.expiryDate and curTime > self.expiryDate:
            self.getLogger().warning(
                'Task %s will not be executed, expiryDate (%s) < current time (%s)' % \
                (self.id, self.expiryDate, curTime))
            return False

        self.startedOn = curTime
        self.running = True
        self.setStatus(base.TASK_STATUS_RUNNING)

    def start(self, delay):
        self._executionDelay = delay
        try:
            self.run()
            self.endedOn = self._getCurrentDateTime()
        finally:
            self.running = False

    def tearDown(self):
        """
        If a task needs to do something once it has run and been removed
        from runningList, overload this method
        """
        pass

    def __str__(self):
        return "[%s:%s|%s]" % (self.typeId, self.id,
                               base.status(self.status))


class OneShotTask(BaseTask):
    """
    Tasks that are executed only once
    """

    def __init__(self, startDateTime, expiryDate = None):
        super(OneShotTask, self).__init__(expiryDate = expiryDate)
        self.startDateTime = startDateTime

    def getStartOn(self):
        return self.startDateTime

    def setStartOn(self, newtime):
        self.startDateTime = newtime

    def suicide(self):
        self.setStatus(base.TASK_STATUS_TERMINATED)
        self.setEndedOn(self._getCurrentDateTime())


class PeriodicTask(BaseTask):
    """
    Tasks that should be executed at regular intervals
    """

    def __init__(self, frequency, **kwargs):
        """
        :param frequency: a valid dateutil frequency specifier (DAILY, HOURLY, etc...)
        """
        super(PeriodicTask, self).__init__()

        self._nextOccurrence = None
        self._lastFinishedOn = None
        self._occurrences = IOBTree()
        self._occurrenceCount = 0
        self._repeat = True

        if 'dtstart' not in kwargs:
            kwargs['dtstart'] = self._getCurrentDateTime()

        self._rule = rrule.rrule(
            frequency,
            **kwargs
            )

        self._nextOccurrence = self._rule.after(
            kwargs['dtstart'] - timedelta(seconds=1),
            inc = True)



    def start(self, delay):
        super(PeriodicTask, self).start(delay)

    def tearDown(self):
        super(PeriodicTask, self).tearDown()

    def setNextOccurrence(self, dateAfter = None):

        if not self._nextOccurrence:
            # if it's None already, it means there's no "future"
            return

        if not dateAfter:
            dateAfter = self._getCurrentDateTime()

        # find next date after
        nextOcc = self._rule.after(max(self._nextOccurrence, dateAfter),
                                   inc = False)

        # repeat process till a satisfactory date is found
        # or there is nothing left to check
        #while nextOcc and nextOcc < dateAfter:
        #    nextOcc = self._rule.after(nextOcc,
        #                               inc = False)

        self._nextOccurrence = nextOcc
        return nextOcc

    def getStartOn(self):
        return self._nextOccurrence

    def getLastFinishedOn(self):
        return self._lastFinishedOn

    def addOccurrence(self, occurrence):
        occurrence.setId(self._occurrenceCount)
        self._occurrences[self._occurrenceCount] = occurrence
        self._occurrenceCount += 1

    def dontComeBack(self):
        self._repeat = False

    def shouldComeBack(self):
        return self._repeat


class PeriodicUniqueTask(PeriodicTask):
    """
    Singleton periodic tasks: no two or more PeriodicUniqueTask of this
    class will be queued or running at the same time (TODO)
    """
    # TODO: implement this


class TaskOccurrence(TimedEvent):
    """
    Wraps around a PeriodicTask object, and represents an occurrence of this task
    """

    fossilizes(ITaskOccurrenceFossil)


    def __init__(self, task):
        self._task = task
        self._startedOn = task.getStartedOn()
        self._endedOn = task.getEndedOn()
        self._id = None

    def getId(self):
        return self._id

    def getUniqueId(self):
        return "%s:%s" % (self._task.getUniqueId(), self._id)

    def setId(self, occId):
        self._id = occId

    def getStartedOn(self):
        return self._startedOn

    def getEndedOn(self):
        return self._endedOn

    def getTask(self):
        return self._task


class CategoryStatisticsUpdaterTask(PeriodicUniqueTask):
    '''Updates statistics associated with categories
    '''

    # seconds to consider a task AWOL
    _AWOLThresold = 15000

    def __init__(self, cat, frequency, **kwargs):
        super(CategoryStatisticsUpdaterTask, self).__init__(frequency,
                                                            **kwargs)
        self._cat = cat

    def run(self):
        from MaKaC.statistics import CategoryStatistics
        CategoryStatistics.updateStatistics(self._cat,
                                            self.getLogger())


# TODO: Isolate CERN Specific
class FoundationSyncTask(PeriodicUniqueTask):
    """
    Synchronizes room data (along with associated room managers
    and equipment) with Foundation database.

    Also, updates list of CERN Official Holidays

    (This is object for a task class)
    """
    def __init__(self, frequency, **kwargs):
        super(FoundationSyncTask, self).__init__(frequency, **kwargs)

    def run(self):
        from MaKaC.common.FoundationSync.foundationSync import FoundationSync
        FoundationSync(self.getLogger()).doAll(None)


class SendMailTask(OneShotTask):
    """
    """
    def __init__(self, startDateTime):
        super(SendMailTask, self).__init__(startDateTime)
        self.fromAddr = ""
        self.toAddr = []
        self.toUser = []
        self.ccAddr = []
        self.subject = ""
        self.text = ""
        self.smtpServer = Config.getInstance().getSmtpServer()

    def _prepare(self, check):
        """
        Overloaded by descendants
        """

    def run(self, check=True):
        import smtplib
        from MaKaC.webinterface.mail import GenericMailer, GenericNotification

        # prepare the mail
        send = self._prepare(check=check)

        # _prepare decided we shouldn't send the mail?
        if not send:
            return

        # just in case some ill-behaved code generates empty addresses
        addrs = list(smtplib.quoteaddr(x) for x in self.toAddr if x)
        ccaddrs = list(smtplib.quoteaddr(x) for x in self.ccAddr if x)

        if len(addrs) + len(ccaddrs) == 0:
            self.getLogger().warning("Attention: no recipients, mail won't be sent")
        else:
            self.getLogger().info("Sending mail To: %s, CC: %s" % (addrs, ccaddrs))

        for user in self.toUser:
            addrs.append(smtplib.quoteaddr(user.getEmail()))

        if addrs or ccaddrs:
            GenericMailer.send(GenericNotification({"fromAddr": self.fromAddr,
                                                    "toList": addrs,
                                                    "ccList": ccaddrs,
                                                    "subject": self.subject,
                                                    "body": self.text }))

    def setFromAddr(self, addr):
        self.fromAddr = addr
        self._p_changed = 1

    def getFromAddr(self):
        return self.fromAddr

    def addToAddr(self, addr):
        if not addr in self.toAddr:
            self.toAddr.append(addr)
            self._p_changed = 1

    def addCcAddr(self, addr):
        if not addr in self.ccAddr:
            self.ccAddr.append(addr)
            self._p_changed = 1

    def removeToAddr(self, addr):
        if addr in self.toAddr:
            self.toAddr.remove(addr)
            self._p_changed = 1

    def setToAddrList(self, addrList):
        """Params: addrList -- addresses of type : list of str."""
        self.toAddr = addrList
        self._p_changed = 1

    def getToAddrList(self):
        return self.toAddr

    def setCcAddrList(self, addrList):
        """Params: addrList -- addresses of type : list of str."""
        self.ccAddr = addrList
        self._p_changed = 1

    def getCcAddrList(self):
        return self.ccAddr

    def addToUser(self, user):
        if not user in self.toUser:
            self.toUser.append(user)
            self._p_changed = 1

    def removeToUser(self, user):
        if user in self.toUser:
            self.toUser.remove(user)
            self._p_changed = 1

    def getToUserList(self):
        return self.toUser

    def setSubject(self, subject):
        self.subject = subject

    def getSubject(self):
        return self.subject

    def setText(self, text):
        self.text = text

    def getText(self):
        return self.text


class AlarmTask(SendMailTask):
    """
    implement an alarm componment
    """
    def __init__(self, conf, confRelId, startDateTime=None, relative=None):

        self.conf = conf
        super(AlarmTask, self).__init__(startDateTime)

        if not startDateTime:
            startDateTime = self.conf.getStartDate() - relative

        self.setRelative(relative)
        self.setStartOn(startDateTime)

        self.text = ""
        self.note = ""
        self.confSumary = False
        self.toAllParticipants = False
        self._confRelId = confRelId
        self._relative = relative

    def getConference(self):
        return self.conf

    def getConfRelativeId(self):
        return self._confRelId

    def getToAllParticipants(self):
        try:
            return self.toAllParticipants
        except:
            self.toAllParticipants = False
            return self.toAllParticipants

    def setRelative(self, relative):
        self._relative = relative

    def setToAllParticipants(self, toAllParticipants):
        self.toAllParticipants = toAllParticipants

    def clone(self, conference):
        """
        Clone the alarm, changing only the conference
        """
        alarm = conference.newAlarm(self.getTimeBefore() or self.getStartOn())
        for addr in self.getToAddrList():
            alarm.addToAddr(addr)
        alarm.setFromAddr(self.getFromAddr())
        alarm.setSubject(self.getSubject())
        alarm.setConfSummary(self.getConfSummary())
        alarm.setNote(self.getNote())
        alarm.setText(self.getText())
        alarm.setToAllParticipants(self.getToAllParticipants())
        return alarm

    def getTimeBefore(self):
        # TODO: remove this
        if not hasattr(self, '_relative'):
            self._relative = None
        return self._relative

    def addToUser(self, user):
        super(AlarmTask, self).addToUser(user)
        if isinstance(user, Avatar):
            user.linkTo(self, "to")

    def removeToUser(self, user):
        super(AlarmTask, self).removeToUser(user)
        if isinstance(user, Avatar):
            user.unlinkTo(self, "to")

    def getText(self):
        return self.text

    def getLocator(self):
        d = self.conf.getLocator()
        d["alarmId"] = self.getConfRelativeId()
        return d

    def canAccess(self, aw):
        return self.conf.canAccess(aw)

    def canModify(self, aw):
        return self.conf.canModify(aw)

    def __getVarsTextTpl(self, conf):
        tvars = {}
        tvars['entries'] = []
        confSchedule = conf.getSchedule()
        entrylist = confSchedule.getEntries()
        for entry in entrylist:
            if type(entry) is schedule.BreakTimeSchEntry:
                newItem = entry
            else:
                newItem = entry.getOwner()
            tvars['entries'].append(newItem)
        tvars["conf"] = conf
        styleMgr = info.HelperMaKaCInfo.getMaKaCInfoInstance().getStyleManager()
        tvars['INCLUDE'] = os.path.join(styleMgr.getBaseTPLPath(), 'include')
        tvars['getTime'] = lambda date : format_time(date.time())
        tvars['isTime0H0M'] = lambda date : (date.hour, date.minute) == (0,0)
        tvars['getDate'] = lambda date : format_date(date, format='yyyy-MM-dd')
        tvars['prettyDate'] = lambda date : format_date(date, format='full')
        tvars['getLocationInfo'] = lambda item: getLocationInfo(item, False)
        from MaKaC.conference import SessionSlot, AcceptedContribution
        tvars['getItemType'] = lambda item: "Break" if isinstance(item, schedule.BreakTimeSchEntry) \
                                            else ("Session" if isinstance(item, SessionSlot) \
                                                  else ("Contribution" if isinstance(item, AcceptedContribution) \
                                                                                else item.__class__.__name__))
        return tvars

    def _setMailText(self):
        text = self.text
        if self.note:
            text = text + "Note: %s" % self.note
        if self.confSumary:
            tplDir = Config.getInstance().getTPLDir()
            text += render(os.path.join(tplDir, "events", "Text.tpl"), self.__getVarsTextTpl(self.conf))

        super(AlarmTask, self).setText(text)

    def setNote(self, note):
        self.note = note
        self._setMailText()
        self._p_changed=1

    def getNote(self):
        return self.note

    def setConfSummary(self, val):
        self.confSumary = val
        self._setMailText()
        self._p_changed=1

    def getConfSummary(self):
        return self.confSumary

    def _prepare(self, check = True):

        # Date checks...
        if check:
            from MaKaC.conference import ConferenceHolder
            if not ConferenceHolder().hasKey(self.conf.getId()):
                self.getLogger().warning("Conference %s no longer exists! "
                                     "Deleting alarm." % self.conf.getId())
                self.conf.removeAlarm(self)
                self.suicide()
            elif self.conf.getStartDate() <= self._getCurrentDateTime():
                self.getLogger().warning("Conference %s already started. "
                                     "Deleting alarm." % self.conf.getId())
                self.conf.removeAlarm(self)
                self.suicide()
                return False

        # Email
        startDateTime = format_datetime(self.conf.getAdjustedStartDate(), format="short")
        self.setSubject( _("Event reminder: %s (%s %s)") % (self.conf.getTitle(), startDateTime, self.conf.getTimezone()))
        try:
            locationText = self.conf.getLocation().getName()
{'fraction_non_alphanumeric': 0.09387236206458174, 'fraction_numerical': 0.002084922451055174, 'mean_word_length': 2.1813475693601876, 'pattern_counts': {'":': 10, '<': 11, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 48, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '226122', 'n_tokens_mistral': 11253, 'n_tokens_neox': 10776, 'n_words': 2866}