PageRenderTime 57ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/afrims/apps/reminders/app.py

https://github.com/afrims/afrims
Python | 317 lines | 262 code | 20 blank | 35 comment | 30 complexity | 6522b87582030814fd46905e3612b6dc MD5 | raw file
  1. import datetime
  2. import logging
  3. import re
  4. from django.conf import settings
  5. from django.core.mail import send_mail
  6. from django.db.models import Q, signals
  7. from django.template.loader import render_to_string
  8. from django.utils.translation import ugettext
  9. from rapidsms.apps.base import AppBase
  10. from rapidsms.contrib.scheduler.models import EventSchedule
  11. from rapidsms.messages.outgoing import OutgoingMessage
  12. from rapidsms.models import Contact
  13. from afrims.apps.broadcast.models import Broadcast
  14. from afrims.apps.groups import models as groups
  15. from afrims.apps.reminders import models as reminders
  16. # In RapidSMS, message translation is done in OutgoingMessage, so no need
  17. # to attempt the real translation here. Use _ so that ./manage.py makemessages
  18. # finds our text.
  19. _ = lambda s: s
  20. def scheduler_callback(router):
  21. """
  22. Basic rapidsms.contrib.scheduler.models.EventSchedule callback
  23. function that runs RemindersApp.cronjob()
  24. """
  25. app = router.get_app("afrims.apps.reminders")
  26. app.cronjob()
  27. def daily_email_callback(router, *args, **kwargs):
  28. """
  29. Send out daily email report of confirmed/unconfirmed appointments.
  30. """
  31. days = kwargs.get('days', 7)
  32. try:
  33. days = int(days)
  34. except ValueError:
  35. logging.debug('ValueError for specified days in daily_email_callback. Value used was %s. Using default value:7' % days)
  36. days = 7
  37. today = datetime.date.today()
  38. appt_date = today + datetime.timedelta(days=days)
  39. confirmed_patients = reminders.Patient.objects.confirmed_for_date(appt_date)
  40. unconfirmed_patients = reminders.Patient.objects.unconfirmed_for_date(appt_date)
  41. context = {
  42. 'appt_date': appt_date,
  43. 'confirmed_patients': confirmed_patients,
  44. 'unconfirmed_patients': unconfirmed_patients,
  45. }
  46. patients_exist = confirmed_patients or unconfirmed_patients
  47. if patients_exist:
  48. subject_template = u'Confirmation Report For Appointments on {appt_date}'
  49. subject = subject_template.format(**context)
  50. body = render_to_string('reminders/emails/daily_report_message.html', context)
  51. group_name = settings.DEFAULT_DAILY_REPORT_GROUP_NAME
  52. group, created = groups.Group.objects.get_or_create(name=group_name)
  53. if not created:
  54. emails = [c.email for c in group.contacts.all() if c.email]
  55. if emails:
  56. send_mail(subject, body, None, emails, fail_silently=True)
  57. class RemindersApp(AppBase):
  58. """ RapidSMS app to send appointment reminders """
  59. # for production, we'll probably want something like:
  60. # cron_schedule = {'minutes': ['0'], 'hours': ['10']}
  61. # for testing and debugging, run every minute:
  62. cron_schedule = {'minutes': '*'}
  63. cron_name = 'reminders-cron-job'
  64. cron_callback = 'afrims.apps.reminders.app.scheduler_callback'
  65. pin_regex = re.compile(r'^\d{4,6}$')
  66. conf_keyword = '1'
  67. future_appt_msg = _('You have an upcoming appointment in '
  68. '{days} days, on {date}. Please reply with '
  69. '{confirm_response} to confirm.')
  70. near_appt_msg = _('You have an appointment {day}, {date}. '
  71. 'Please reply with {confirm_response} to confirm.')
  72. not_registered = _('Sorry, your mobile number is not registered.')
  73. no_reminders = _('Sorry, I could not find any reminders awaiting '
  74. 'confirmation.')
  75. incorrect_pin = _('That is not your PIN code. Please reply with the '
  76. 'correct PIN code.')
  77. pin_required = _('Please confirm appointments by sending your PIN code.')
  78. thank_you = _('Thank you for confirming your upcoming appointment.')
  79. def start(self):
  80. """ setup event schedule to run cron job """
  81. try:
  82. schedule = EventSchedule.objects.get(description=self.name)
  83. except EventSchedule.DoesNotExist:
  84. schedule = EventSchedule.objects.create(description=self.name,
  85. callback=self.cron_callback,
  86. **self.cron_schedule)
  87. schedule.callback = self.cron_callback
  88. for key, val in self.cron_schedule.iteritems():
  89. if hasattr(schedule, key):
  90. setattr(schedule, key, val)
  91. schedule.save()
  92. # Daily email schedule
  93. name = 'reminders-daily-email'
  94. info = {
  95. 'callback': 'afrims.apps.reminders.app.daily_email_callback',
  96. 'hours': [12],
  97. 'minutes': [0],
  98. 'callback_kwargs': {'days': 7},
  99. }
  100. schedule, created = EventSchedule.objects.get_or_create(description=name,
  101. defaults=info
  102. )
  103. # if not created:
  104. # for key, val in info.iteritems():
  105. # if hasattr(schedule, key):
  106. # setattr(schedule, key, val)
  107. # schedule.save()
  108. group_name = settings.DEFAULT_DAILY_REPORT_GROUP_NAME
  109. group, _ = groups.Group.objects.get_or_create(name=group_name)
  110. group_name = settings.DEFAULT_CONFIRMATIONS_GROUP_NAME
  111. group, _ = groups.Group.objects.get_or_create(name=group_name)
  112. self.info('started')
  113. @classmethod
  114. def _notification_msg(cls, appt_date, confirm_response=None):
  115. """
  116. Formats an appointment reminder message for the given notification,
  117. appointment date, and confirm response (usually the confirm keyword or
  118. 'your PIN').
  119. """
  120. num_days = (appt_date - datetime.date.today()).days
  121. if confirm_response is None:
  122. confirm_response = cls.conf_keyword
  123. month_for_translate = appt_date.strftime('%B')
  124. month_for_translate = ugettext(month_for_translate)
  125. msg_data = {
  126. 'days': num_days,
  127. 'date': '%s %s' % (appt_date.strftime('%d'), month_for_translate),
  128. 'confirm_response': confirm_response,
  129. }
  130. if num_days == 0:
  131. msg_data['day'] = ugettext('today')
  132. message = ugettext(cls.near_appt_msg)
  133. elif num_days == 1:
  134. msg_data['day'] = ugettext('tomorrow')
  135. message = ugettext(cls.near_appt_msg)
  136. else:
  137. message = ugettext(cls.future_appt_msg)
  138. return message.format(**msg_data)
  139. def handle(self, msg):
  140. """
  141. Handles messages that start with a '1' (to ease responding in
  142. alternate character sets).
  143. Updates the SentNotification status to 'confirmed' for the given user
  144. and replies with a thank you message.
  145. """
  146. msg_parts = msg.text.split()
  147. if not msg_parts:
  148. return False
  149. response = msg_parts[0]
  150. if response != self.conf_keyword and not self.pin_regex.match(response):
  151. return False
  152. contact = msg.connection.contact
  153. if not contact:
  154. msg.respond(self.not_registered)
  155. return True
  156. if contact.pin and response == self.conf_keyword:
  157. msg.respond(self.pin_required)
  158. return True
  159. if contact.pin and response != contact.pin:
  160. msg.respond(self.incorrect_pin)
  161. return True
  162. notifs = reminders.SentNotification.objects.filter(recipient=contact,
  163. status='sent')
  164. if not notifs.exists():
  165. msg.respond(self.no_reminders)
  166. return True
  167. now = datetime.datetime.now()
  168. sent_notification = notifs.order_by('-date_sent')[0]
  169. sent_notification.status = 'confirmed'
  170. sent_notification.date_confirmed = now
  171. sent_notification.save()
  172. contact = msg.connection.contact
  173. patient = reminders.Patient.objects.get(contact=contact)
  174. study_id = patient.subject_number
  175. msg_text = u'Appointment on %s confirmed.' % sent_notification.appt_date
  176. full_msg = u'From {number} ({study_id}): {body}'.format(
  177. number=msg.connection.identity, body=msg_text,
  178. study_id=study_id,
  179. )
  180. broadcast = Broadcast.objects.create(
  181. date_created=now, date=now,
  182. schedule_frequency='one-time', body=full_msg
  183. )
  184. group_name = settings.DEFAULT_CONFIRMATIONS_GROUP_NAME
  185. group, _ = groups.Group.objects.get_or_create(name=group_name)
  186. broadcast.groups.add(group)
  187. msg.respond(self.thank_you)
  188. def queue_outgoing_notifications(self):
  189. """ generate queued appointment reminders """
  190. # TODO: make sure this task is atomic
  191. today = datetime.date.today()
  192. for notification in reminders.Notification.objects.all():
  193. self.info('Scheduling notifications for %s' % notification)
  194. days_delta = datetime.timedelta(days=notification.num_days)
  195. appt_date = today + days_delta
  196. patients = reminders.Patient.objects.filter(next_visit=appt_date,
  197. contact__isnull=False,)
  198. already_sent = Q(contact__sent_notifications__appt_date=appt_date) &\
  199. Q(contact__sent_notifications__notification=notification)
  200. confirmed = Q(contact__sent_notifications__appt_date=appt_date) &\
  201. Q(contact__sent_notifications__status__in=['confirmed', 'manual'])
  202. # .exclude() generates erroneous results - use filter(~...) instead
  203. patients = patients.filter(~already_sent)
  204. if notification.recipients == 'confirmed':
  205. patients = patients.filter(confirmed)
  206. elif notification.recipients == 'unconfirmed':
  207. # unconfirmed should include those to whom we haven't sent
  208. # any reminders yet, so just exclude the confirmed ones
  209. patients = patients.filter(~confirmed)
  210. self.info('Queuing reminders for %s patients.' % patients.count())
  211. for patient in patients:
  212. # _() is not an alias for gettext, since normally translation
  213. # is done in the OutgoingMessage class. In the code below, we
  214. # force translation by calling ugettext directly, since the
  215. # message gets queued in the database before being passed to
  216. # OutgoingMessage.
  217. self.debug('Creating notification for %s' % patient)
  218. if patient.contact.pin:
  219. confirm_response = ugettext('your PIN')
  220. else:
  221. confirm_response = self.conf_keyword
  222. message = self._notification_msg(appt_date, confirm_response)
  223. time_of_day = patient.reminder_time or notification.time_of_day
  224. date_to_send = datetime.datetime.combine(today, time_of_day)
  225. notification.sent_notifications.create(
  226. recipient=patient.contact,
  227. appt_date=appt_date,
  228. date_queued=datetime.datetime.now(),
  229. date_to_send=date_to_send,
  230. message=message)
  231. def send_notifications(self):
  232. """ send queued for delivery messages """
  233. notifications = reminders.SentNotification.objects.filter(
  234. status='queued',
  235. date_to_send__lt=datetime.datetime.now())
  236. count = notifications.count()
  237. if count > 0:
  238. self.info('found {0} notification(s) to send'.format(count))
  239. for notification in notifications:
  240. connection = notification.recipient.default_connection
  241. if not connection:
  242. self.debug('no connection found for recipient {0}, unable '
  243. 'to send'.format(notification.recipient))
  244. continue
  245. msg = OutgoingMessage(connection=connection,
  246. template=notification.message)
  247. success = True
  248. try:
  249. msg.send()
  250. except Exception, e:
  251. self.exception(e)
  252. success = False
  253. if success and (msg.sent or msg.sent is None):
  254. self.debug('notification sent successfully')
  255. notification.status = 'sent'
  256. notification.date_sent = datetime.datetime.now()
  257. else:
  258. self.debug('notification failed to send')
  259. notification.status = 'error'
  260. notification.save()
  261. def cronjob(self):
  262. self.debug('{0} running'.format(self.cron_name))
  263. # grab all reminders ready to go out and queue their messages
  264. self.queue_outgoing_notifications()
  265. # send queued notifications
  266. self.send_notifications()
  267. def queue_message_to_contact(contact, text):
  268. """Just queue a single message to a contact"""
  269. now = datetime.datetime.now()
  270. broadcast = Broadcast(schedule_frequency='one-time',
  271. date=now,
  272. body=text)
  273. broadcast.save()
  274. broadcast.messages.create(recipient=contact)
  275. def new_contact_created(sender, **kwargs):
  276. """When a new contact is created, send a welcome message"""
  277. created = kwargs['created']
  278. if created:
  279. contact = kwargs['instance']
  280. connection = contact.default_connection
  281. if not connection:
  282. logging.debug('no connection found for recipient {0}, unable '
  283. 'to send'.format(contact))
  284. return
  285. text = _("You have been registered for TrialConnect.")
  286. try:
  287. queue_message_to_contact(contact, text)
  288. logging.debug("Sending wecome message '%s' to '%s'." % (text, contact))
  289. except Exception, e:
  290. logging.exception(e)
  291. signals.post_save.connect(new_contact_created, sender=Contact,
  292. dispatch_uid='just once per new contact created')