blob: a51153a7784fcf9c6d094f33feb0267cf774a1d1 [file] [log] [blame]
# Copyright 2014 the Melange authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Logic for task reminders."""
import datetime
import enum
from google.appengine.api import taskqueue
from google.appengine.ext import db
from google.appengine.ext import ndb
from codein.request import links
from melange.utils import time as time_utils
from soc.tasks import mailer
from soc.modules.gci.logic.helper import notifications
from soc.modules.gci.models import task as task_model
# this is for safety: when an AppEngine task is scheduled to be executed
# at certain point of time, let us give it some graceful period. If the
# scheduler decides to pick up the task a little bit earlier, it is still
# fine to execute it.
EPSILON = datetime.timedelta(minutes=5)
# Name of task queue to use for reminders
TASK_REMINDER_QUEUE_NAME = 'gci-update'
def scheduleTaskReminder(task, reminder_type, eta):
"""Schedules a reminder of the specified type for the specified task to be
sent out at a specific point in the future.
Args:
task: task_model.GCITask entity.
reminder_type: ReminderType that specifies the type of reminder to schedule.
eta: A datetime.datetime object which provides the earliest time when this
reminder will be sent.
"""
url = links.CI_LINKER.taskReminder(
task.key(), reminder_type, 'task_reminder_gci_task')
taskqueue.add(
eta=eta, url=url, queue_name=TASK_REMINDER_QUEUE_NAME,
transactional=True)
_DEADLINE_REMINDER_ID = 'deadline_reminder'
_NEEDS_REVIEW_REMINDER_ID = 'needs_review_reminder'
class ReminderType(enum.Enum):
"""Enumerates all types of reminders."""
deadline_reminder = _DEADLINE_REMINDER_ID
needs_review_reminder = _NEEDS_REVIEW_REMINDER_ID
@property
def handler(self):
"""Returns implementation of ReminderType for this reminder type."""
if self.value == _DEADLINE_REMINDER_ID:
return DEADLINE_REMINDER_HANDLER
if self.value == _NEEDS_REVIEW_REMINDER_ID:
return NEEDS_REVIEW_REMINDER_HANDLER
class ReminderHandler(object):
"""Interface that provides methods to handle one type of task reminders."""
def getReminderType(self):
"""Returns ReminderType associated with the handler.
Returns:
ReminderType associated with the handler.
"""
raise NotImplementedError
def shouldSendReminder(self, task):
"""Indicates whether a reminder for the specified task should be sent out
at this time.
Args:
task: task_model.GCITask entity.
Returns:
True, if a reminder should not sent. False, otherwise.
"""
raise NotImplementedError
def sendReminder(self, task):
"""Sends out a reminder for the specified task.
Args:
task: task_model.GCITask entity.
"""
raise NotImplementedError
class DeadlineReminerHandler(ReminderHandler):
"""Implementation of ReminderHandler that handles reminders of type
ReminderType.deadline_reminder.
"""
# reminder should be sent no sooner than MIN_DELTA before the deadline
# for a processed task
MIN_DELTA = datetime.timedelta(hours=24)
_CLOCK = time_utils.REAL_CLOCK
def getReminderType(self):
"""See ReminderHandler.getReminderType for specification."""
return ReminderType.deadline_reminder
def shouldSendReminder(self, task):
"""See ReminderHandler.shouldSendReminder for specification."""
# do nothing if there is no deadline
if not task.deadline:
return False
# a reminder makes sense only if it is possible to send the task for review
if task.status not in task_model.SEND_FOR_REVIEW_ALLOWED:
return False
# do not bother with sending a reminder after the work deadline ends
if self._CLOCK.utcnow() > task.program.timeline.stop_all_work_deadline:
return False
# it might be too early to send a reminder
if task.deadline - self._CLOCK.utcnow() > self.MIN_DELTA + EPSILON:
return False
return True
def sendReminder(self, task):
"""See ReminderHandler.sendReminder for specification."""
student = ndb.Key.from_old_key(
task_model.GCITask.student.get_value_for_datastore(task)).get()
context = notifications.getDeadlineReminderContext(
task, student.contact.email)
txn = mailer.getSpawnMailTaskTxn(context, parent=task)
db.run_in_transaction(txn)
def scheduleReminder(self, task):
"""See ReminderHandler.scheduleReminder for specification."""
# it makes sense to schedule this reminder only if the task can be sent
# for a review
if task.status not in task_model.SEND_FOR_REVIEW_ALLOWED:
return
# it might be too late for a reminder already
if (not task.deadline or
task.deadline - self._CLOCK.utcnow() < self.MIN_DELTA):
return
# task should be scheduled MIN_DELTA before the deadline
eta = task.deadline - self.MIN_DELTA
scheduleTaskReminder(task, self.getReminderType(), eta)
DEADLINE_REMINDER_HANDLER = DeadlineReminerHandler()
class NeedsReviewReminderHandler(ReminderHandler):
"""Implementation of ReminderHandler that handles reminders of type
ReminderType.needs_review_reminder.
"""
MIN_DELTA = datetime.timedelta(hours=24)
_CLOCK = time_utils.REAL_CLOCK
def getReminderType(self):
"""See ReminderHandler.getReminderType for specification."""
return ReminderType.needs_review_reminder
def shouldSendReminder(self, task):
"""See ReminderHandler.shouldSendReminder for specification."""
# do not send a reminder after the work review deadline, assuming that
# this is the latest moment upon which a task can be reviewed.
if datetime.datetime.now() > task.program.timeline.work_review_deadline:
return False
# the task does not need to be reviewed
if task.status != 'NeedsReview':
return False
# it may be too early for the reminder
if (self._CLOCK.utcnow() - task.last_status_change <
self.MIN_DELTA - EPSILON):
return False
return True
def sendReminder(self, task):
"""See ReminderHandler.sendReminder for specification."""
to_emails = []
mentors = ndb.get_multi(map(ndb.Key.from_old_key, task.mentors))
for mentor in mentors:
to_emails.append(mentor.contact.email)
context = notifications.getNeedsReviewReminderContext(task, to_emails)
txn = mailer.getSpawnMailTaskTxn(context, parent=task)
db.run_in_transaction(txn)
def scheduleReminder(self, task):
"""See ReminderHandler.scheduleReminder for specification."""
# it makes sense to schedule this reminder only if the task needs a review
if task.status == 'NeedsReview':
# task should be scheduled MIN_DELTA after the status was set
eta = task.last_status_change + self.MIN_DELTA
scheduleTaskReminder(task, self.getReminderType(), eta)
NEEDS_REVIEW_REMINDER_HANDLER = NeedsReviewReminderHandler()