PageRenderTime 106ms CodeModel.GetById 59ms RepoModel.GetById 10ms app.codeStats 0ms

/aremind/apps/reminders/app.py

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