PageRenderTime 3ms CodeModel.GetById 51ms app.highlight 9ms RepoModel.GetById 0ms app.codeStats 0ms

/aremind/apps/reminders/app.py

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