def send_reminder(self, reminder_id): """ Retrieves the reminder from the database, passes the message content to the Flowroute SMS client for dispatch. If it fails, the task is re-queued. """ try: appt = Reminder.query.filter_by(id=reminder_id).one() except NoResultFound: log.error( {"message": "Received unknown appointment with id {}.".format( reminder_id), "reminder_id": reminder_id}) return msg_body = create_message_body(appt) message = Message( to=appt.contact_num, from_=FLOWROUTE_NUMBER, content=msg_body) try: sms_controller.create_message(message) except Exception as e: strerr = vars(e).get('response_body', None) log.critical({"message": "Raised an exception sending SMS", "exc": e, "strerr": strerr, "reminder_id": reminder_id}) raise self.retry(exc=e) else: log.info( {"message": "Reminder sent to {} for reminder_id {}".format( appt.contact_num, reminder_id), "reminder_id": reminder_id}) appt.reminder_sent = True db_session.add(appt) db_session.commit()
def test_inbound_handler_expired_appointment(mock_send_reply, appointment_details): contact_0 = '12223333333' contact_1 = '12229991111' appt_str_time_0 = '2000-01-01T13:00+0000' appt_str_time_1 = '2020-01-01T12:00+0000' notify_win = 24 location = 'Flowroute HQ' participant_0 = 'Development Teams' reminder_0 = Reminder(contact_0, arrow.get(appt_str_time_0), notify_win, location, participant_0) reminder_1 = Reminder(contact_1, arrow.get(appt_str_time_1), notify_win, location, participant_0) db_session.add(reminder_0) db_session.add(reminder_1) db_session.commit() client = app.test_client() inbound_req = { 'to': 'myflowroutenumber', 'from': reminder_1.contact_num, 'body': 'Yes' } resp = client.post('/', data=json.dumps(inbound_req), content_type='application/json') reminders = Reminder.query.all() assert mock_send_reply.apply_async.called is True assert len(reminders) == 1 reminders[0].contact_num == contact_1 assert resp.status_code == 200
def test_inbound_handler_expired_appointment(mock_send_reply, appointment_details): contact_0 = '12223333333' contact_1 = '12229991111' appt_str_time_0 = '2000-01-01T13:00+0000' appt_str_time_1 = '2020-01-01T12:00+0000' notify_win = 24 location = 'Flowroute HQ' participant_0 = 'Development Teams' reminder_0 = Reminder(contact_0, arrow.get(appt_str_time_0), notify_win, location, participant_0) reminder_1 = Reminder(contact_1, arrow.get(appt_str_time_1), notify_win, location, participant_0) db_session.add(reminder_0) db_session.add(reminder_1) db_session.commit() client = app.test_client() inbound_req = {'to': 'myflowroutenumber', 'from': reminder_1.contact_num, 'body': 'Yes'} resp = client.post('/', data=json.dumps(inbound_req), content_type='application/json') reminders = Reminder.query.all() assert mock_send_reply.apply_async.called is True assert len(reminders) == 1 reminders[0].contact_num == contact_1 assert resp.status_code == 200
def inbound_handler(): """ The inbound request handler for consuming HTTP wrapped SMS content from Flowroute's messaging service. Validates proper message content, and takes time to clear the expired reminders before continuing. Retrieves the reminder based on the sender, and marks the 'will_attend' attribute according to whether there is 'yes' or 'no' (case insensitive) anywhere in the message. Responds to the user with a confirmation message and returns 200. """ req = request.json # Take the time to clear out any past reminders try: virtual_tn = req['to'] assert len(virtual_tn) <= 18 sms_from = req['from'] assert len(sms_from) <= 18 req['body'] except (TypeError, KeyError, AssertionError) as e: msg = ("Malformed inbound message: {}".format(req)) log.error({"message": msg, "status": "failed", "exc": str(e)}) return Response('There was an issue parsing your request.', status=400) else: Reminder.clean_expired() try: appt = Reminder.query.filter_by( contact_num=sms_from).one() except NoResultFound: msg = "No existing un-responded reminder for contact {}.".format( sms_from) log.info({"message": msg}) return Response(status=200) else: message = req['body'].upper() if 'YES' in message: appt.will_attend = True confirm = True elif 'NO' in message: appt.will_attend = False confirm = False else: confirm = None db_session.add(appt) try: send_reply.apply_async((appt.id,), {'confirm': confirm}) except ConnectionError as e: log.critical({"message": "unable to connect to redis", "exc": type(e)}) db_session.rollback() return Response(status=500) else: db_session.commit() log.info({"message": ("successfully recorded response from {}, scheduled " "SMS confirmation for appointment {}").format( sms_from, appt.id), "reminder_id": appt.id}) return Response(status=200)
def test_send_reply(mock_sms_controller, num, new_reminder, confirm, content): new_reminder.contact_num = num db_session.add(new_reminder) db_session.commit() reminder_id = new_reminder.id send_reply(reminder_id, confirm=confirm) assert mock_sms_controller.create_message.called is True msg = mock_sms_controller.create_message.call_args[0][0].content assert msg == content
def test_create_message_body(new_reminder): new_reminder.contact_num = '122998778' db_session.add(new_reminder) db_session.commit() msg = create_message_body(new_reminder) assert msg == (u"[Your Org Name]\nYou have an appointment on" u" Monday Mar 23, 1:00 pm at Central Park with NY" " Running Club. Please reply 'Yes' to confirm, " "or 'No' to cancel.")
def test_get_reminder_success(new_appointment): reminder_id = new_appointment.id db_session.add(new_appointment) db_session.commit() client = app.test_client() resp = client.get('/reminder/{}'.format(reminder_id)) assert resp.status_code == 200 details = json.loads(resp.data) for field in REMINDER_FIELDS: assert field in details
def test_delete_reminder_success(new_appointment): reminder_id = new_appointment.id db_session.add(new_appointment) db_session.commit() client = app.test_client() stored_reminder = Reminder.query.filter_by(id=reminder_id).one() assert stored_reminder is not None resp = client.delete('/reminder/{}'.format(reminder_id)) assert resp.status_code == 200 with pytest.raises(NoResultFound): Reminder.query.filter_by(id=reminder_id).one()
def test_send_reminder(mock_sms_controller, new_reminder): db_session.add(new_reminder) db_session.commit() reminder_id = new_reminder.id send_reminder(reminder_id) assert mock_sms_controller.create_message.called == 1 msg = mock_sms_controller.create_message.call_args[0][0].content assert msg == ("[Your Org Name]\nYou have an appointment on Monday Mar 23, " "1:00 pm at Central Park with NY Running Club. Please " "reply 'Yes' to confirm, or 'No' to cancel.") reminder = Reminder.query.filter_by(id=reminder_id).one() assert reminder.reminder_sent is True
def test_add_reminder_unique_constraint(mock_send_reminder, new_appointment, appointment_details): client = app.test_client() db_session.add(new_appointment) db_session.commit() dup_num = new_appointment.contact_num appointment_details['contact_number'] = dup_num resp = client.post('/reminder', data=json.dumps(appointment_details), content_type='application/json') data = json.loads(resp.data) assert data['message'] == ("unable to create a new reminder. duplicate " "contact_number {}".format(dup_num)) assert resp.status_code == 400
def test_send_reminder(mock_sms_controller, new_reminder): db_session.add(new_reminder) db_session.commit() reminder_id = new_reminder.id send_reminder(reminder_id) assert mock_sms_controller.create_message.called == 1 msg = mock_sms_controller.create_message.call_args[0][0].content assert msg == ( "[Your Org Name]\nYou have an appointment on Monday Mar 23, " "1:00 pm at Central Park with NY Running Club. Please " "reply 'Yes' to confirm, or 'No' to cancel.") reminder = Reminder.query.filter_by(id=reminder_id).one() assert reminder.reminder_sent is True
def test_reminder_honors_uniqueness(): contact = '12223334445' appt_str_time = '2016-01-01T13:00+00:00' appt_dt = arrow.get(appt_str_time, "YYYY-MM-DDTHH:mmZ") notify_win = 24 location = 'Family Physicians' participant = 'Dr Smith' reminder0 = Reminder(contact, appt_dt, notify_win, location, participant) db_session.add(reminder0) db_session.commit() reminder1 = Reminder(contact, appt_dt, notify_win, location, participant) db_session.add(reminder1) with pytest.raises(IntegrityError): db_session.commit()
def test_reminder_init(): contact = '12223338888' appt_str_time = '2016-01-01T13:00+00:00' appt_dt = arrow.get(appt_str_time, "YYYY-MM-DDTHH:mmZ") notify_win = 24 location = 'Family Physicians' participant = 'Dr Smith' reminder = Reminder(contact, appt_dt, notify_win, location, participant) db_session.add(reminder) db_session.commit() assert reminder.contact_num == contact assert reminder.id is not None assert arrow.get(reminder.appt_user_dt) == appt_dt assert arrow.get(reminder.appt_sys_dt) == appt_dt.to('utc') assert reminder.location == location assert reminder.participant == participant
def test_inbound_handler_success(mock_send_reply, new_appointment, response, status): appt_id = new_appointment.id db_session.add(new_appointment) db_session.commit() client = app.test_client() inbound_req = {'to': 'myflowroutenumber', 'from': new_appointment.contact_num, 'body': response, } resp = client.post('/', data=json.dumps(inbound_req), content_type='application/json') assert mock_send_reply.apply_async.called == 1 assert resp.status_code == 200 appt = Reminder.query.filter_by(id=appt_id).one() assert appt.will_attend is status
def send_reply(self, reminder_id, confirm=True): """ Retrieves the reminder from the database, and sends either a confirmation or an unparsable message response to the Flowroute SMS client. If that fails, the task is re-queued. """ try: appt = Reminder.query.filter_by(id=reminder_id).one() except NoResultFound: log.error({ "message": "Received unknown appointment with id {}.".format(reminder_id), "reminder_id": reminder_id }) return if confirm is None: msg_content = UNPARSABLE_RESPONSE elif confirm is True: msg_content = CONFIRMATION_RESPONSE elif confirm is False: msg_content = CANCEL_RESPONSE message = Message(to=appt.contact_num, from_=FLOWROUTE_NUMBER, content=msg_content) try: sms_controller.create_message(message) except Exception as e: strerr = vars(e).get('response_body', None) log.critical({ "message": "Raised an exception sending SMS", "exc": e, "strerr": strerr, "reminder_id": reminder_id }) raise self.retry(exc=e) else: appt.confirm_sent = True db_session.add(appt) db_session.commit() log.info({ "message": "Confirmation sent to {} for reminder_id {}".format( appt.contact_num, reminder_id), "reminder_id": reminder_id })
def test_inbound_handler_success(mock_send_reply, new_appointment, response, status): appt_id = new_appointment.id db_session.add(new_appointment) db_session.commit() client = app.test_client() inbound_req = { 'to': 'myflowroutenumber', 'from': new_appointment.contact_num, 'body': response, } resp = client.post('/', data=json.dumps(inbound_req), content_type='application/json') assert mock_send_reply.apply_async.called == 1 assert resp.status_code == 200 appt = Reminder.query.filter_by(id=appt_id).one() assert appt.will_attend is status
def send_reminder(self, reminder_id): """ Retrieves the reminder from the database, passes the message content to the Flowroute SMS client for dispatch. If it fails, the task is re-queued. """ try: appt = Reminder.query.filter_by(id=reminder_id).one() except NoResultFound: log.error({ "message": "Received unknown appointment with id {}.".format(reminder_id), "reminder_id": reminder_id }) return msg_body = create_message_body(appt) message = Message(to=appt.contact_num, from_=FLOWROUTE_NUMBER, content=msg_body) try: sms_controller.create_message(message) except Exception as e: strerr = vars(e).get('response_body', None) log.critical({ "message": "Raised an exception sending SMS", "exc": e, "strerr": strerr, "reminder_id": reminder_id }) raise self.retry(exc=e) else: log.info({ "message": "Reminder sent to {} for reminder_id {}".format( appt.contact_num, reminder_id), "reminder_id": reminder_id }) appt.reminder_sent = True db_session.add(appt) db_session.commit()
def send_reply(self, reminder_id, confirm=True): """ Retrieves the reminder from the database, and sends either a confirmation or an unparsable message response to the Flowroute SMS client. If that fails, the task is re-queued. """ try: appt = Reminder.query.filter_by(id=reminder_id).one() except NoResultFound: log.error( {"message": "Received unknown appointment with id {}.".format( reminder_id), "reminder_id": reminder_id}) return if confirm is None: msg_content = UNPARSABLE_RESPONSE elif confirm is True: msg_content = CONFIRMATION_RESPONSE elif confirm is False: msg_content = CANCEL_RESPONSE message = Message( to=appt.contact_num, from_=FLOWROUTE_NUMBER, content=msg_content) try: sms_controller.create_message(message) except Exception as e: strerr = vars(e).get('response_body', None) log.critical({"message": "Raised an exception sending SMS", "exc": e, "strerr": strerr, "reminder_id": reminder_id}) raise self.retry(exc=e) else: appt.confirm_sent = True db_session.add(appt) db_session.commit() log.info( {"message": "Confirmation sent to {} for reminder_id {}".format( appt.contact_num, reminder_id), "reminder_id": reminder_id})
def test_clean_expired(): contact_0 = '12223334444' appt_str_time_0 = '2016-01-01T13:00+00:00' notify_win = 24 location = 'Family Physicians' participant_0 = 'Dr Smith' appt_dt_0 = arrow.get(appt_str_time_0, "YYYY-MM-DDTHH:mmZ") reminder_0 = Reminder(contact_0, appt_dt_0, notify_win, location, participant_0) appt_str_time_1 = '2020-01-01T13:00+00:00' appt_dt_1 = arrow.get(appt_str_time_1, "YYYY-MM-DDTHH:mmZ") participant_1 = 'Dr John' contact_1 = '12223339999' reminder_1 = Reminder(contact_1, appt_dt_1, notify_win, location, participant_1) db_session.add(reminder_0) db_session.add(reminder_1) db_session.commit() stored_reminders = Reminder.query.all() assert len(stored_reminders) == 2 Reminder.clean_expired() stored_reminders = Reminder.query.all() assert len(stored_reminders) == 1 assert stored_reminders[0].participant == participant_1
def new_appointments(new_appointment): contact_0 = '12223334444' appt_str_time_0 = '2016-01-01T13:00+0700' notify_win = 24 location = 'Family Physicians' participant_0 = 'Dr Smith' reminder_0 = Reminder(contact_0, arrow.get(appt_str_time_0), notify_win, location, participant_0) appt_str_time_1 = '2020-01-01T13:00+0000' participant_1 = 'Dr Martinez' contact_1 = '12223335555' reminder_1 = Reminder(contact_1, arrow.get(appt_str_time_1), notify_win, location, participant_1) db_session.add(reminder_0) db_session.add(new_appointment) db_session.commit() db_session.add(reminder_1) db_session.commit() return [reminder_0, reminder_1, new_appointment]
def test_get_locale_aware_dt_string(new_reminder, lang, expected, num): new_reminder.contact_num = num db_session.add(new_reminder) db_session.commit() msg = get_locale_aware_dt_str(new_reminder.appt_user_dt, language=lang) assert msg == expected
def add_reminder(): """ Adds a new reminder, and schedules an sms message handling celery task. If the redis is unavailable and unable to schedule a task the reminder is removed and an internal error is raised, otherwise the new reminder id is returned. Required: contact_number : string (The users phone number) appointment_time : string (The datetime of the appointment) "YYYY-MM-DDTHH:mmZ" notify_window : integer (The hours before the appointment to send reminder) Optional: location : string (Where the appointment is) participant : string (Who the appoinment is with) """ body = request.json try: contact_num = str(body['contact_number']) appt_dt = arrow.get(body['appointment_time'], "YYYY-MM-DDTHH:mmZ") notify_win = int(body['notify_window']) location = body.get('location', None) participant = body.get('participant', None) except (KeyError, ParserError): raise InvalidAPIUsage( ("Required arguments: 'contact_number' (str), " "'appointment_time' (str) eg. '2016-01-01T13:00+02:00', " "'notify_window' (int)")) else: Reminder.clean_expired() reminder = Reminder(contact_num, appt_dt, notify_win, location, participant) db_session.add(reminder) try: db_session.commit() except IntegrityError: msg = ("Unable to create a new reminder. Duplicate " "contact_number {}.".format(contact_num)) log.error({"message": msg}) return Response(json.dumps({"message": msg}), status=400, content_type="application/json") else: try: send_reminder.apply_async(args=[reminder.id], eta=reminder.notify_sys_dt) except ConnectionError as e: log.critical({"message": "unable to connect to redis", "exc": type(e)}) db_session.delete(reminder) db_session.commit() return Response(json.dumps( {"message": ("Unable to create a new reminder." " Redis is unreachable."), "exc": "RedisConnectionError"}), status=500, content_type="application/json") msg = "Successfully created a reminder with id {}.".format(reminder.id) log.info({"message": msg}) content = json.dumps({"message": msg, "reminder_id": reminder.id}) return Response(content, status=200, content_type="application/json")
def add_reminder(): """ Adds a new reminder, and schedules an sms message handling celery task. If the redis is unavailable and unable to schedule a task the reminder is removed and an internal error is raised, otherwise the new reminder id is returned. Required: contact_number : string (The users phone number) appointment_time : string (The datetime of the appointment) "YYYY-MM-DDTHH:mmZ" notify_window : integer (The hours before the appointment to send reminder) Optional: location : string (Where the appointment is) participant : string (Who the appoinment is with) """ body = request.json try: contact_num = str(body['contact_number']) appt_dt = arrow.get(body['appointment_time'], "YYYY-MM-DDTHH:mmZ") notify_win = int(body['notify_window']) location = body.get('location', None) participant = body.get('participant', None) except (KeyError, ParserError): raise InvalidAPIUsage( ("Required arguments: 'contact_number' (str), " "'appointment_time' (str) eg. '2016-01-01T13:00+02:00', " "'notify_window' (int)")) else: Reminder.clean_expired() reminder = Reminder(contact_num, appt_dt, notify_win, location, participant) db_session.add(reminder) try: db_session.commit() except IntegrityError: msg = ("Unable to create a new reminder. Duplicate " "contact_number {}.".format(contact_num)) log.error({"message": msg}) return Response(json.dumps({"message": msg}), status=400, content_type="application/json") else: try: send_reminder.apply_async(args=[reminder.id], eta=reminder.notify_sys_dt) except ConnectionError as e: log.critical({ "message": "unable to connect to redis", "exc": type(e) }) db_session.delete(reminder) db_session.commit() return Response(json.dumps({ "message": ("Unable to create a new reminder." " Redis is unreachable."), "exc": "RedisConnectionError" }), status=500, content_type="application/json") msg = "Successfully created a reminder with id {}.".format(reminder.id) log.info({"message": msg}) content = json.dumps({"message": msg, "reminder_id": reminder.id}) return Response(content, status=200, content_type="application/json")
def inbound_handler(): """ The inbound request handler for consuming HTTP wrapped SMS content from Flowroute's messaging service. Validates proper message content, and takes time to clear the expired reminders before continuing. Retrieves the reminder based on the sender, and marks the 'will_attend' attribute according to whether there is 'yes' or 'no' (case insensitive) anywhere in the message. Responds to the user with a confirmation message and returns 200. """ req = request.json # Take the time to clear out any past reminders try: virtual_tn = req['to'] assert len(virtual_tn) <= 18 sms_from = req['from'] assert len(sms_from) <= 18 req['body'] except (TypeError, KeyError, AssertionError) as e: msg = ("Malformed inbound message: {}".format(req)) log.error({"message": msg, "status": "failed", "exc": str(e)}) return Response('There was an issue parsing your request.', status=400) else: Reminder.clean_expired() try: appt = Reminder.query.filter_by(contact_num=sms_from).one() except NoResultFound: msg = "No existing un-responded reminder for contact {}.".format( sms_from) log.info({"message": msg}) return Response(status=200) else: message = req['body'].upper() if 'YES' in message: appt.will_attend = True confirm = True elif 'NO' in message: appt.will_attend = False confirm = False else: confirm = None db_session.add(appt) try: send_reply.apply_async((appt.id, ), {'confirm': confirm}) except ConnectionError as e: log.critical({ "message": "unable to connect to redis", "exc": type(e) }) db_session.rollback() return Response(status=500) else: db_session.commit() log.info({ "message": ("successfully recorded response from {}, scheduled " "SMS confirmation for appointment {}").format( sms_from, appt.id), "reminder_id": appt.id }) return Response(status=200)