Esempio n. 1
0
    def start_smsforms_session(self, domain, recipient, case_id, phone_entry_or_number, logged_subevent, workflow,
            app, module, form):
        # Close all currently open sessions
        SQLXFormsSession.close_all_open_sms_sessions(domain, recipient.get_id)

        # Start the new session
        try:
            session, responses = start_session(
                SQLXFormsSession.create_session_object(
                    domain,
                    recipient,
                    (phone_entry_or_number.phone_number
                     if isinstance(phone_entry_or_number, PhoneNumber)
                     else phone_entry_or_number),
                    app,
                    form,
                    expire_after=self.expire_after,
                    reminder_intervals=self.reminder_intervals,
                    submit_partially_completed_forms=self.submit_partially_completed_forms,
                    include_case_updates_in_partial_submissions=self.include_case_updates_in_partial_submissions
                ),
                domain,
                recipient,
                app,
                module,
                form,
                case_id,
                yield_responses=True
            )
        except TouchformsError as e:
            logged_subevent.error(
                MessagingEvent.ERROR_TOUCHFORMS_ERROR,
                additional_error_text=get_formplayer_exception(domain, e)
            )

            if touchforms_error_is_config_error(domain, e):
                # Don't reraise the exception because this means there are configuration
                # issues with the form that need to be fixed. The error is logged in the
                # above lines.
                return None, None

            # Reraise the exception so that the framework retries it again later
            raise
        except:
            logged_subevent.error(MessagingEvent.ERROR_TOUCHFORMS_ERROR)
            # Reraise the exception so that the framework retries it again later
            raise

        session.workflow = workflow
        session.save()

        return session, responses
    def test_get_open_sms_session_multiple_results(self):
        domain = uuid.uuid4().hex
        contact = uuid.uuid4().hex
        for i in range(3):
            _make_session(
                domain=domain,
                connection_id=contact,
                end_time=None,
                session_type=XFORMS_SESSION_SMS,
            )

        with self.assertRaises(MultipleResultsFound):
            SQLXFormsSession.get_open_sms_session(domain, contact)
Esempio n. 3
0
    def test_get_open_sms_session_multiple_results(self):
        domain = uuid.uuid4().hex
        contact = uuid.uuid4().hex
        for i in range(3):
            _make_session(
                domain=domain,
                connection_id=contact,
                end_time=None,
                session_type=XFORMS_SESSION_SMS,
            )

        with self.assertRaises(MultipleResultsFound):
            SQLXFormsSession.get_open_sms_session(domain, contact)
Esempio n. 4
0
    def start_smsforms_session(self, domain, recipient, case_id, phone_entry_or_number, logged_subevent, workflow,
            app, module, form):
        # Close all currently open sessions
        SQLXFormsSession.close_all_open_sms_sessions(domain, recipient.get_id)

        # Start the new session
        try:
            session, responses = start_session(
                SQLXFormsSession.create_session_object(
                    domain,
                    recipient,
                    (phone_entry_or_number.phone_number
                     if isinstance(phone_entry_or_number, PhoneNumber)
                     else phone_entry_or_number),
                    app,
                    form,
                    expire_after=self.expire_after,
                    reminder_intervals=self.reminder_intervals,
                    submit_partially_completed_forms=self.submit_partially_completed_forms,
                    include_case_updates_in_partial_submissions=self.include_case_updates_in_partial_submissions
                ),
                domain,
                recipient,
                app,
                module,
                form,
                case_id,
            )
        except TouchformsError as e:
            logged_subevent.error(
                MessagingEvent.ERROR_TOUCHFORMS_ERROR,
                additional_error_text=get_formplayer_exception(domain, e)
            )

            if touchforms_error_is_config_error(domain, e):
                # Don't reraise the exception because this means there are configuration
                # issues with the form that need to be fixed. The error is logged in the
                # above lines.
                return None, None

            # Reraise the exception so that the framework retries it again later
            raise
        except:
            logged_subevent.error(MessagingEvent.ERROR_TOUCHFORMS_ERROR)
            # Reraise the exception so that the framework retries it again later
            raise

        session.workflow = workflow
        session.save()

        return session, responses
 def test_get_single_open_session(self):
     properties = _arbitrary_session_properties(
         end_time=None,
         session_is_open=True,
         session_type=XFORMS_SESSION_SMS,
     )
     session = SQLXFormsSession(**properties)
     session.save()
     (mult, session) = get_single_open_session_or_close_multiple(
         session.domain, session.connection_id)
     self.assertEqual(False, mult)
     [session_back] = SQLXFormsSession.get_all_open_sms_sessions(
         session.domain, session.connection_id)
     self.assertEqual(session._id, session_back.couch_id)
    def test_get_and_close_all_open_sessions(self):
        domain = uuid.uuid4().hex
        contact = uuid.uuid4().hex
        for i in range(3):
            _make_session(
                domain=domain,
                connection_id=contact,
                end_time=None,
                session_type=XFORMS_SESSION_SMS,
            )

        sql_sessions = SQLXFormsSession.get_all_open_sms_sessions(domain, contact)
        self.assertEqual(3, len(sql_sessions))
        SQLXFormsSession.close_all_open_sms_sessions(domain, contact)
        self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
 def test_get_single_open_session(self):
     properties = _arbitrary_session_properties(
         end_time=None,
         session_type=XFORMS_SESSION_SMS,
     )
     session = SQLXFormsSession(**properties)
     session.save()
     (mult, session) = get_single_open_session_or_close_multiple(
         session.domain, session.connection_id
     )
     self.assertEqual(False, mult)
     [session_back] = SQLXFormsSession.get_all_open_sms_sessions(
         session.domain, session.connection_id
     )
     self.assertEqual(session._id, session_back.couch_id)
Esempio n. 8
0
    def test_get_and_close_all_open_sessions(self):
        domain = uuid.uuid4().hex
        contact = uuid.uuid4().hex
        for i in range(3):
            _make_session(
                domain=domain,
                connection_id=contact,
                end_time=None,
                session_type=XFORMS_SESSION_SMS,
            )

        sql_sessions = SQLXFormsSession.get_all_open_sms_sessions(domain, contact)
        self.assertEqual(3, len(sql_sessions))
        SQLXFormsSession.close_all_open_sms_sessions(domain, contact)
        self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
Esempio n. 9
0
def process_survey_keyword_actions(verified_number, survey_keyword, text, msg):
    sender = verified_number.owner
    case = None
    args = split_args(text, survey_keyword)

    logged_event = MessagingEvent.create_from_keyword(survey_keyword, sender)

    # Close any open sessions even if it's just an sms that we're
    # responding with.
    SQLXFormsSession.close_all_open_sms_sessions(verified_number.domain,
        verified_number.owner_id)

    if sender.doc_type == "CommCareCase":
        case = sender
        args = args[1:]
Esempio n. 10
0
    def setUpClass(cls):
        super().setUpClass()
        cls.domain = Domain(name=uuid.uuid4().hex)
        cls.domain.save()

        cls.number = PhoneNumber(domain=cls.domain.name,
                                 owner_doc_type='CommCareCase',
                                 owner_id='fake-owner-id1',
                                 phone_number='01112223333',
                                 backend_id=None,
                                 ivr_backend_id=None,
                                 verified=True,
                                 is_two_way=True,
                                 pending_verification=False,
                                 contact_last_modified=datetime.utcnow())
        cls.number.save()

        cls.session = SQLXFormsSession.create_session_object(
            cls.domain.name,
            Mock(get_id=cls.number.owner_id),
            cls.number.phone_number,
            Mock(get_id='app_id'),
            Mock(xmlns='xmlns'),
            expire_after=24 * 60,
        )
        cls.session.save()

        cls.backend = SQLTestSMSBackend.objects.create(
            name='BACKEND',
            domain=cls.domain,
            is_global=False,
            hq_api_id=SQLTestSMSBackend.get_api_id())
Esempio n. 11
0
 def setUpClass(cls):
     super().setUpClass()
     cls.session = SQLXFormsSession(session_id='abc123',
                                    domain='test-domain',
                                    user_id='user_id')
     cls.formplayer_interface = FormplayerInterface(cls.session.session_id,
                                                    cls.session.domain)
    def test_move_to_next_action_with_fast_forwarding(self, utcnow_mock):
        utcnow_mock.return_value = datetime(2018, 1, 1, 0, 0)
        session = SQLXFormsSession.create_session_object(
            'test',
            Mock(get_id='contact_id'),
            '+9990001',
            Mock(get_id='app_id'),
            Mock(xmlns='xmlns'),
            expire_after=24 * 60,
            reminder_intervals=[30, 60])
        self.assertTrue(session.session_is_open)
        self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0))
        self.assertIsNone(session.end_time)
        self.assertEqual(session.current_action_due,
                         datetime(2018, 1, 1, 0, 30))
        self.assertTrue(session.current_action_is_a_reminder)

        utcnow_mock.return_value = datetime(2018, 1, 3, 0, 0)
        session.move_to_next_action()
        self.assertTrue(session.session_is_open)
        self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0))
        self.assertIsNone(session.end_time)
        self.assertEqual(session.current_action_due,
                         datetime(2018, 1, 2, 0, 0))
        self.assertFalse(session.current_action_is_a_reminder)
Esempio n. 13
0
def handle_due_survey_action(domain, contact_id, session_id):
    with critical_section_for_smsforms_sessions(contact_id):
        session = SQLXFormsSession.by_session_id(session_id)
        if (not session or not session.session_is_open
                or session.current_action_due > utcnow()):
            return

        if session.current_action_is_a_reminder:
            # Resend the current question in the open survey to the contact
            p = PhoneNumber.get_phone_number_for_owner(session.connection_id,
                                                       session.phone_number)
            if p:
                metadata = MessageMetadata(
                    workflow=session.workflow,
                    xforms_session_couch_id=session._id,
                )
                resp = current_question(session.session_id, domain)
                send_sms_to_verified_number(
                    p,
                    resp.event.text_prompt,
                    metadata,
                    logged_subevent=session.related_subevent)

            session.move_to_next_action()
            session.save()
        else:
            # Close the session
            session.close()
            session.save()
Esempio n. 14
0
 def test_get_by_session_id(self):
     session_id = uuid.uuid4().hex
     sql_session = SQLXFormsSession.objects.create(
         session_id=session_id,
         start_time=datetime.utcnow(),
         modified_time=datetime.utcnow(),
     )
     self.assertEqual(sql_session.pk, SQLXFormsSession.by_session_id(session_id).pk)
Esempio n. 15
0
 def test_get_by_session_id(self):
     session_id = uuid.uuid4().hex
     sql_session = SQLXFormsSession.objects.create(
         session_id=session_id,
         start_time=datetime.utcnow(),
         modified_time=datetime.utcnow(),
     )
     self.assertEqual(sql_session.pk, SQLXFormsSession.by_session_id(session_id).pk)
Esempio n. 16
0
def form_session_handler(v, text, msg):
    """
    The form session handler will use the inbound text to answer the next question
    in the open SQLXformsSession for the associated contact. If no session is open,
    the handler passes. If multiple sessions are open, they are all closed and an
    error message is displayed to the user.
    """
    with critical_section_for_smsforms_sessions(v.owner_id):
        if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(v.domain):
            channel = get_channel_for_contact(v.owner_id, v.phone_number)
            running_session_info = XFormsSessionSynchronization.get_running_session_info_for_channel(
                channel)
            if running_session_info.session_id:
                session = SQLXFormsSession.by_session_id(
                    running_session_info.session_id)
                if not session.session_is_open:
                    # This should never happen. But if it does we should set the channel free
                    # and act like there was no available session
                    notify_error(
                        "The supposedly running session was not open and was released. "
                        'No known way for this to happen, so worth investigating.'
                    )
                    XFormsSessionSynchronization.clear_stale_channel_claim(
                        channel)
                    session = None
            else:
                session = None
        else:
            multiple, session = get_single_open_session_or_close_multiple(
                v.domain, v.owner_id)
            if multiple:
                send_sms_to_verified_number(
                    v, get_message(MSG_MULTIPLE_SESSIONS, v))
                return True

        if session:
            session.phone_number = v.phone_number
            session.modified_time = datetime.utcnow()
            session.save()

            # Metadata to be applied to the inbound message
            inbound_metadata = MessageMetadata(
                workflow=session.workflow,
                reminder_id=session.reminder_id,
                xforms_session_couch_id=session._id,
            )
            add_msg_tags(msg, inbound_metadata)

            try:
                answer_next_question(v, text, msg, session)
            except Exception:
                # Catch any touchforms errors
                log_sms_exception(msg)
                send_sms_to_verified_number(
                    v, get_message(MSG_TOUCHFORMS_DOWN, v))
            return True
        else:
            return False
Esempio n. 17
0
 def test_get_all_open_sessions_wrong_type(self):
     domain = uuid.uuid4().hex
     contact = uuid.uuid4().hex
     _make_session(
         domain=domain,
         connection_id=contact,
         end_time=None,
         session_type=XFORMS_SESSION_IVR,
     )
     self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
Esempio n. 18
0
 def test_get_all_open_sessions_wrong_type(self):
     domain = uuid.uuid4().hex
     contact = uuid.uuid4().hex
     _make_session(
         domain=domain,
         connection_id=contact,
         end_time=None,
         session_type=XFORMS_SESSION_IVR,
     )
     self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
Esempio n. 19
0
 def test_get_all_open_sessions_already_ended(self):
     domain = uuid.uuid4().hex
     contact = uuid.uuid4().hex
     _make_session(
         domain=domain,
         connection_id=contact,
         end_time=datetime.utcnow(),
         session_type=XFORMS_SESSION_SMS,
     )
     self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
Esempio n. 20
0
 def test_get_all_open_sessions_already_ended(self):
     domain = uuid.uuid4().hex
     contact = uuid.uuid4().hex
     _make_session(
         domain=domain,
         connection_id=contact,
         end_time=datetime.utcnow(),
         session_type=XFORMS_SESSION_SMS,
     )
     self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
Esempio n. 21
0
    def user_id(self):
        from corehq.apps.smsforms.models import SQLXFormsSession

        if self._user_id is Ellipsis:
            session = SQLXFormsSession.by_session_id(self.session_id)
            if session:
                self._user_id = session.user_id
            else:
                self._user_id = None

        return self._user_id
Esempio n. 22
0
 def test_get_all_open_sessions_contact_mismatch(self):
     domain = uuid.uuid4().hex
     contact = uuid.uuid4().hex
     _make_session(
         domain=domain,
         connection_id='wrong',
         end_time=None,
         session_is_open=True,
         session_type=XFORMS_SESSION_SMS,
     )
     self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
Esempio n. 23
0
def process_survey_keyword_actions(verified_number, survey_keyword, text, msg):
    sender = verified_number.owner
    case = None
    args = split_args(text, survey_keyword)

    logged_event = MessagingEvent.create_from_keyword(survey_keyword, sender)

    # Log a messaging subevent for the incoming message
    subevent = logged_event.create_subevent_for_single_sms(
        msg.couch_recipient_doc_type, msg.couch_recipient, completed=True)
    add_msg_tags(msg, MessageMetadata(messaging_subevent_id=subevent.pk))

    # Close any open sessions even if it's just an sms that we're
    # responding with.
    SQLXFormsSession.close_all_open_sms_sessions(verified_number.domain,
                                                 verified_number.owner_id)

    if is_commcarecase(sender):
        case = sender
        args = args[1:]
 def _make_session(self, number):
     session = SQLXFormsSession.create_session_object(
         self.domain_name,
         Mock(get_id=number.owner_id),
         number.phone_number,
         Mock(get_id='app_id'),
         Mock(xmlns='xmlns'),
         expire_after=24 * 60,
     )
     session.session_id = uuid.uuid4().hex
     session.save()
     return session
Esempio n. 25
0
    def test_get_open_sms_session_one_result(self):
        domain = uuid.uuid4().hex
        contact = uuid.uuid4().hex
        new_session = _make_session(
            domain=domain,
            connection_id=contact,
            end_time=None,
            session_type=XFORMS_SESSION_SMS,
        )

        session = SQLXFormsSession.get_open_sms_session(domain, contact)
        self.assertEqual(new_session.session_id, session.session_id)
Esempio n. 26
0
    def test_get_open_sms_session_one_result(self):
        domain = uuid.uuid4().hex
        contact = uuid.uuid4().hex
        new_session = _make_session(
            domain=domain,
            connection_id=contact,
            end_time=None,
            session_type=XFORMS_SESSION_SMS,
        )

        session = SQLXFormsSession.get_open_sms_session(domain, contact)
        self.assertEqual(new_session.session_id, session.session_id)
Esempio n. 27
0
def sms_keyword_handler(v, text, msg):
    text = text.strip()
    if text == "":
        return False

    sessions = SQLXFormsSession.get_all_open_sms_sessions(v.domain, v.owner_id)
    text_words = text.upper().split()

    if text.startswith("#"):
        return handle_global_keywords(v, text, msg, text_words, sessions)
    else:
        return handle_domain_keywords(v, text, msg, text_words, sessions)
Esempio n. 28
0
def sms_keyword_handler(v, text, msg):
    text = text.strip()
    if text == "":
        return False

    sessions = SQLXFormsSession.get_all_open_sms_sessions(v.domain, v.owner_id)
    text_words = text.upper().split()

    if text.startswith("#"):
        return handle_global_keywords(v, text, msg, text_words, sessions)
    else:
        return handle_domain_keywords(v, text, msg, text_words, sessions)
Esempio n. 29
0
    def create_tasks(self):
        survey_sessions_due_for_action = self.get_survey_sessions_due_for_action(
        )
        all_open_session_ids = self.get_open_session_ids()
        for domain, connection_id, session_id, current_action_due, phone_number in survey_sessions_due_for_action:
            if skip_domain(domain):
                continue

            if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(domain):
                fake_session = SQLXFormsSession(
                    session_id=session_id,
                    connection_id=connection_id,
                    phone_number=phone_number,
                )
                if not XFormsSessionSynchronization.channel_is_available_for_session(
                        fake_session):
                    running_session_info = XFormsSessionSynchronization.get_running_session_info_for_channel(
                        fake_session.get_channel())
                    # First confirm the supposedly running session is even open
                    # and if it's not (should be exceedingly rare) release it and act like it wasn't there
                    if running_session_info.session_id \
                            and running_session_info.session_id not in all_open_session_ids:
                        notify_error(
                            "The supposedly running session was not open and was released. "
                            "No known way for this to happen, so worth investigating.",
                            details={
                                'running_session_info': running_session_info
                            })
                        XFormsSessionSynchronization.clear_stale_channel_claim(
                            fake_session.get_channel())
                    # This is the 99% case: there's a running session for the channel
                    # so leave this session/action in the queue for later and move on to the next one
                    else:
                        continue

            enqueue_lock = self.get_enqueue_lock(session_id,
                                                 current_action_due)
            if enqueue_lock.acquire(blocking=False):
                handle_due_survey_action.delay(domain, connection_id,
                                               session_id)
Esempio n. 30
0
def handle_sms_form_complete(sender, session_id, form, **kwargs):
    from corehq.apps.smsforms.models import SQLXFormsSession
    session = SQLXFormsSession.by_session_id(session_id)
    if session:
        resp = submit_form_locally(form, session.domain, app_id=session.app_id)
        xform_id = resp['X-CommCareHQ-FormID']
        session.end(completed=True)
        session.submission_id = xform_id
        session.save()

        xform = XFormInstance.get(xform_id)
        xform.survey_incentive = session.survey_incentive
        xform.save()
 def test_get_all_open_sessions_contact_mismatch(self):
     domain = uuid.uuid4().hex
     contact = uuid.uuid4().hex
     _make_session(
         domain=domain,
         connection_id='wrong',
         end_time=None,
         session_is_open=True,
         session_type=XFORMS_SESSION_SMS,
     )
     self.assertEqual(
         0, len(SQLXFormsSession.get_all_open_sms_sessions(domain,
                                                           contact)))
Esempio n. 32
0
def handle_sms_form_complete(sender, session_id, form, **kwargs):
    from corehq.apps.smsforms.models import SQLXFormsSession
    session = SQLXFormsSession.by_session_id(session_id)
    if session:
        resp = submit_form_locally(form, session.domain, app_id=session.app_id)
        xform_id = resp['X-CommCareHQ-FormID']
        session.end(completed=True)
        session.submission_id = xform_id
        session.save()
        
        xform = XFormInstance.get(xform_id)
        xform.survey_incentive = session.survey_incentive
        xform.save()
Esempio n. 33
0
def start_session_for_structured_sms(domain,
                                     contact,
                                     phone_number,
                                     app,
                                     module,
                                     form,
                                     case_id,
                                     keyword,
                                     logged_subevent=None):
    """
    Returns (session, responses, error, error_code)
    """
    try:
        session, responses = start_session(
            SQLXFormsSession.create_session_object(
                domain,
                contact,
                phone_number.phone_number,
                app,
                form,
                expire_after=0,
            ),
            domain,
            contact,
            app,
            module,
            form,
            case_id=case_id,
            yield_responses=True)
        if logged_subevent:
            logged_subevent.xforms_session_id = session.pk
            logged_subevent.save()
        return (session, responses, False, None)
    except TouchformsError as e:
        human_readable_message = e.response_data.get('human_readable_message',
                                                     None)
        logged_subevent.error(MessagingEvent.ERROR_TOUCHFORMS_ERROR,
                              additional_error_text=human_readable_message)

        if touchforms_error_is_config_error(e):
            error_code = MSG_FORM_ERROR
        else:
            notify_exception(None,
                             message=('Could not process structured sms for'
                                      'contact %s, domain %s, keyword %s' %
                                      (contact.get_id, domain, keyword)))
            error_code = MSG_TOUCHFORMS_ERROR

        return (None, None, True, error_code)
Esempio n. 34
0
def handle_due_survey_action(domain, contact_id, session_id):
    with critical_section_for_smsforms_sessions(contact_id):
        session = SQLXFormsSession.by_session_id(session_id)
        if (not session or not session.session_is_open
                or session.current_action_due > utcnow()):
            return

        if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(domain):
            if not XFormsSessionSynchronization.claim_channel_for_session(
                    session):
                from .management.commands import handle_survey_actions
                # Unless we release this lock, handle_survey_actions will be unable to requeue this task
                # for the default duration of 1h, which we don't want
                handle_survey_actions.Command.get_enqueue_lock(
                    session_id, session.current_action_due).release()
                return

        if session_is_stale(session):
            # If a session is having some unrecoverable errors that aren't benefitting from
            # being retried, those errors should show up in sentry log and the fix should
            # be dealt with. In terms of the current session itself, we just close it out
            # to allow new sessions to start.
            session.mark_completed(False)
            session.save()
            return

        if session.current_action_is_a_reminder:
            # Resend the current question in the open survey to the contact
            p = PhoneNumber.get_phone_number_for_owner(session.connection_id,
                                                       session.phone_number)
            if p:
                metadata = MessageMetadata(
                    workflow=session.workflow,
                    xforms_session_couch_id=session._id,
                )
                resp = FormplayerInterface(session.session_id,
                                           domain).current_question()
                send_sms_to_verified_number(
                    p,
                    resp.event.text_prompt,
                    metadata,
                    logged_subevent=session.related_subevent)

            session.move_to_next_action()
            session.save()
        else:
            # Close the session
            session.close()
            session.save()
Esempio n. 35
0
    def test_get_single_open_session_close_multiple(self):
        domain = uuid.uuid4().hex
        contact = uuid.uuid4().hex
        for i in range(3):
            _make_session(
                domain=domain,
                connection_id=contact,
                end_time=None,
                session_type=XFORMS_SESSION_SMS,
            )

        (mult, session) = get_single_open_session_or_close_multiple(domain, contact)
        self.assertEqual(True, mult)
        self.assertEqual(None, session)
        self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
Esempio n. 36
0
    def test_session_is_stale(self, utcnow_mock_1, utcnow_mock_2):
        utcnow_mock_2.return_value = datetime(2018, 1, 1, 0, 0)
        session = SQLXFormsSession.create_session_object(
            'test',
            Mock(get_id='contact_id'),
            '+9990001',
            Mock(get_id='app_id'),
            Mock(xmlns='xmlns'),
            expire_after=24 * 60,
            reminder_intervals=[30, 60],
            submit_partially_completed_forms=True,
        )
        session.save()
        self.addCleanup(session.delete)

        utcnow_mock_1.return_value = datetime(2018, 1, 14, 0, 0)
        self.assertFalse(session_is_stale(session))

        utcnow_mock_1.return_value = datetime(2018, 1, 16, 0, 0)
        self.assertTrue(session_is_stale(session))

        handle_due_survey_action('test', 'contact_id', session.session_id)
        session = SQLXFormsSession.by_session_id(session.session_id)
        self.assertFalse(session.session_is_open)
Esempio n. 37
0
    def test_get_single_open_session_close_multiple(self):
        domain = uuid.uuid4().hex
        contact = uuid.uuid4().hex
        for i in range(3):
            _make_session(
                domain=domain,
                connection_id=contact,
                end_time=None,
                session_type=XFORMS_SESSION_SMS,
            )

        (mult, session) = get_single_open_session_or_close_multiple(domain, contact)
        self.assertEqual(True, mult)
        self.assertEqual(None, session)
        self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
Esempio n. 38
0
 def _start_session(self, yield_responses=False):
     if not self.recipient:
         raise Exception("Set recipient")
     return start_session(SQLXFormsSession.create_session_object(
         self.domain,
         self.recipient,
         self.phone_number,
         self.app,
         self.basic_form,
     ),
                          self.domain,
                          self.recipient,
                          self.app,
                          self.basic_form,
                          yield_responses=yield_responses)
Esempio n. 39
0
    def test_session_is_stale(self, utcnow_mock_1, utcnow_mock_2):
        utcnow_mock_2.return_value = datetime(2018, 1, 1, 0, 0)
        session = SQLXFormsSession.create_session_object(
            'test',
            Mock(get_id='contact_id'),
            '+9990001',
            Mock(get_id='app_id'),
            Mock(xmlns='xmlns'),
            expire_after=24 * 60,
            reminder_intervals=[30, 60],
            submit_partially_completed_forms=True,
        )
        session.save()
        self.addCleanup(session.delete)

        utcnow_mock_1.return_value = datetime(2018, 1, 14, 0, 0)
        self.assertFalse(session_is_stale(session))

        utcnow_mock_1.return_value = datetime(2018, 1, 16, 0, 0)
        self.assertTrue(session_is_stale(session))

        handle_due_survey_action('test', 'contact_id', session.session_id)
        session = SQLXFormsSession.by_session_id(session.session_id)
        self.assertFalse(session.session_is_open)
Esempio n. 40
0
def sms_keyword_handler(verified_number, text, msg):
    with critical_section_for_smsforms_sessions(verified_number.owner_id):
        text = text.strip()
        if text == "":
            return False

        sessions = SQLXFormsSession.get_all_open_sms_sessions(
            verified_number.domain, verified_number.owner_id)
        text_words = text.upper().split()

        if text.startswith("#"):
            return handle_global_keywords(verified_number, text, msg,
                                          text_words, sessions)
        else:
            return handle_domain_keywords(verified_number, text, msg,
                                          text_words, sessions)
Esempio n. 41
0
def close_session(self, contact_id, session_id):
    with critical_section_for_smsforms_sessions(contact_id):
        session = SQLXFormsSession.by_session_id(session_id)
        try:
            session.close(force=False)
        except TouchformsError as e:
            try:
                self.retry(exc=e)
            except TouchformsError as e:
                raise e
            finally:
                # Eventually the session needs to get closed
                session.mark_completed(False)
                session.save()
                return
        session.save()
Esempio n. 42
0
def get_single_open_session_or_close_multiple(domain, contact_id):
    """
    Retrieves the current open SQLXFormsSession for the given contact.
    If multiple sessions are open, it closes all of them and returns
    None for the session.

    The return value is a tuple of (multiple, session), where multiple
    is True if there were multiple sessions, and session is the session if
    there was a single open session available.
    """
    sessions = SQLXFormsSession.get_all_open_sms_sessions(domain, contact_id)
    count = sessions.count()
    if count > 1:
        for session in sessions:
            session.end(False)
            session.save()
        return (True, None)

    session = sessions[0] if count == 1 else None
    return (False, session)
Esempio n. 43
0
def get_single_open_session_or_close_multiple(domain, contact_id):
    """
    Retrieves the current open SQLXFormsSession for the given contact.
    If multiple sessions are open, it closes all of them and returns
    None for the session.

    The return value is a tuple of (multiple, session), where multiple
    is True if there were multiple sessions, and session is the session if
    there was a single open session available.
    """
    sessions = SQLXFormsSession.get_all_open_sms_sessions(domain, contact_id)
    count = sessions.count()
    if count > 1:
        for session in sessions:
            session.mark_completed(False)
            session.save()
        return (True, None)

    session = sessions[0] if count == 1 else None
    return (False, session)
Esempio n. 44
0
def handle_due_survey_action(domain, contact_id, session_id):
    with critical_section_for_smsforms_sessions(contact_id):
        session = SQLXFormsSession.by_session_id(session_id)
        if (
            not session
            or not session.session_is_open
            or session.current_action_due > utcnow()
        ):
            return

        if session_is_stale(session):
            # If a session is having some unrecoverable errors that aren't benefitting from
            # being retried, those errors should show up in sentry log and the fix should
            # be dealt with. In terms of the current session itself, we just close it out
            # to allow new sessions to start.
            session.mark_completed(False)
            session.save()
            return

        if session.current_action_is_a_reminder:
            # Resend the current question in the open survey to the contact
            p = PhoneNumber.get_phone_number_for_owner(session.connection_id, session.phone_number)
            if p:
                metadata = MessageMetadata(
                    workflow=session.workflow,
                    xforms_session_couch_id=session._id,
                )
                resp = current_question(session.session_id, domain)
                send_sms_to_verified_number(
                    p,
                    resp.event.text_prompt,
                    metadata,
                    logged_subevent=session.related_subevent
                )

            session.move_to_next_action()
            session.save()
        else:
            # Close the session
            session.close()
            session.save()
Esempio n. 45
0
def handle_due_survey_action(domain, contact_id, session_id):
    with critical_section_for_smsforms_sessions(contact_id):
        session = SQLXFormsSession.by_session_id(session_id)
        if (
            not session
            or not session.session_is_open
            or session.current_action_due > utcnow()
        ):
            return

        if session_is_stale(session):
            # If a session is having some unrecoverable errors that aren't benefitting from
            # being retried, those errors should show up in sentry log and the fix should
            # be dealt with. In terms of the current session itself, we just close it out
            # to allow new sessions to start.
            session.mark_completed(False)
            session.save()
            return

        if session.current_action_is_a_reminder:
            # Resend the current question in the open survey to the contact
            p = PhoneNumber.get_phone_number_for_owner(session.connection_id, session.phone_number)
            if p:
                metadata = MessageMetadata(
                    workflow=session.workflow,
                    xforms_session_couch_id=session._id,
                )
                resp = current_question(session.session_id, domain)
                send_sms_to_verified_number(
                    p,
                    resp.event.text_prompt,
                    metadata,
                    logged_subevent=session.related_subevent
                )

            session.move_to_next_action()
            session.save()
        else:
            # Close the session
            session.close()
            session.save()
Esempio n. 46
0
def start_session_for_structured_sms(domain, contact, phone_number, app, module, form,
        case_id, keyword, logged_subevent=None):
    """
    Returns (session, responses, error, error_code)
    """
    try:
        session, responses = start_session(
            SQLXFormsSession.create_session_object(
                domain,
                contact,
                phone_number.phone_number,
                app,
                form,
                expire_after=0,
            ),
            domain,
            contact,
            app,
            module,
            form,
            case_id=case_id,
            yield_responses=True
        )
        if logged_subevent:
            logged_subevent.xforms_session_id = session.pk
            logged_subevent.save()
        return (session, responses, False, None)
    except TouchformsError as e:
        human_readable_message = get_formplayer_exception(domain, e)
        logged_subevent.error(MessagingEvent.ERROR_TOUCHFORMS_ERROR,
            additional_error_text=human_readable_message)

        if touchforms_error_is_config_error(domain, e):
            error_code = MSG_FORM_ERROR
        else:
            notify_exception(None, message=('Could not process structured sms for'
                'contact %s, domain %s, keyword %s' % (contact.get_id, domain, keyword)))
            error_code = MSG_TOUCHFORMS_ERROR

        return (None, None, True, error_code)
Esempio n. 47
0
    def test_move_to_next_action_with_reminders(self, utcnow_mock):
        utcnow_mock.return_value = datetime(2018, 1, 1, 0, 0)
        session = SQLXFormsSession.create_session_object(
            'test',
            Mock(get_id='contact_id'),
            '+9990001',
            Mock(get_id='app_id'),
            Mock(xmlns='xmlns'),
            expire_after=24 * 60,
            reminder_intervals=[30, 60]
        )
        self.assertTrue(session.session_is_open)
        self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0))
        self.assertIsNone(session.end_time)
        self.assertEqual(session.current_action_due, datetime(2018, 1, 1, 0, 30))
        self.assertTrue(session.current_action_is_a_reminder)

        utcnow_mock.return_value = datetime(2018, 1, 1, 0, 31)
        session.move_to_next_action()
        self.assertTrue(session.session_is_open)
        self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0))
        self.assertIsNone(session.end_time)
        self.assertEqual(session.current_action_due, datetime(2018, 1, 1, 1, 30))
        self.assertTrue(session.current_action_is_a_reminder)

        utcnow_mock.return_value = datetime(2018, 1, 1, 1, 31)
        session.move_to_next_action()
        self.assertTrue(session.session_is_open)
        self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0))
        self.assertIsNone(session.end_time)
        self.assertEqual(session.current_action_due, datetime(2018, 1, 2, 0, 0))
        self.assertFalse(session.current_action_is_a_reminder)

        utcnow_mock.return_value = datetime(2018, 1, 2, 0, 1)
        session.move_to_next_action()
        self.assertTrue(session.session_is_open)
        self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0))
        self.assertIsNone(session.end_time)
        self.assertEqual(session.current_action_due, datetime(2018, 1, 2, 0, 0))
        self.assertFalse(session.current_action_is_a_reminder)
Esempio n. 48
0
def process_survey_keyword_actions(verified_number, survey_keyword, text, msg):
    sender = verified_number.owner
    case = None
    args = split_args(text, survey_keyword)

    logged_event = MessagingEvent.create_from_keyword(survey_keyword, sender)

    # Log a messaging subevent for the incoming message
    subevent = logged_event.create_subevent_for_single_sms(
        msg.couch_recipient_doc_type,
        msg.couch_recipient
    )
    subevent.completed()
    add_msg_tags(msg, MessageMetadata(messaging_subevent_id=subevent.pk))

    # Close any open sessions even if it's just an sms that we're
    # responding with.
    SQLXFormsSession.close_all_open_sms_sessions(verified_number.domain,
        verified_number.owner_id)

    if sender.doc_type == "CommCareCase":
        case = sender
        args = args[1:]
    elif sender.doc_type == "CommCareUser":
        if keyword_uses_form_that_requires_case(survey_keyword):
            if len(args) > 1:
                external_id = args[1]
                case, matches = get_case_by_external_id(verified_number.domain,
                    external_id, sender)
                if matches == 0:
                    send_keyword_response(verified_number, MSG_CASE_NOT_FOUND, logged_event)
                    logged_event.error(MessagingEvent.ERROR_CASE_EXTERNAL_ID_NOT_FOUND)
                    return
                elif matches > 1:
                    send_keyword_response(verified_number, MSG_MULTIPLE_CASES_FOUND, logged_event)
                    logged_event.error(MessagingEvent.ERROR_MULTIPLE_CASES_WITH_EXTERNAL_ID_FOUND)
                    return
            else:
                send_keyword_response(verified_number, MSG_MISSING_EXTERNAL_ID, logged_event)
                logged_event.error(MessagingEvent.ERROR_NO_EXTERNAL_ID_GIVEN)
                return
            args = args[2:]
        else:
            args = args[1:]
    def cmp_fcn(a1, a2):
        a1_ss = (a1.action == METHOD_STRUCTURED_SMS)
        a2_ss = (a2.action == METHOD_STRUCTURED_SMS)
        if a1_ss and a2_ss:
            return 0
        elif a1_ss:
            return -1
        elif a2_ss:
            return 1
        else:
            return 0

    if case:
        subevent.case_id = case.get_id
        subevent.save()

    # Process structured sms actions first
    actions = sorted(survey_keyword.actions, cmp=cmp_fcn)
    for survey_keyword_action in actions:
        if survey_keyword_action.recipient == RECIPIENT_SENDER:
            contact = sender
        elif survey_keyword_action.recipient == RECIPIENT_OWNER:
            if sender.doc_type == "CommCareCase":
                contact = get_wrapped_owner(get_owner_id(sender))
            else:
                contact = None
        elif survey_keyword_action.recipient == RECIPIENT_USER_GROUP:
            try:
                contact = Group.get(survey_keyword_action.recipient_id)
                assert contact.doc_type == "Group"
                assert contact.domain == verified_number.domain
            except Exception:
                contact = None
        else:
            contact = None

        if contact is None:
            continue

        if survey_keyword_action.action == METHOD_SMS:
            create_immediate_reminder(contact, METHOD_SMS, 
                reminder_type=REMINDER_TYPE_KEYWORD_INITIATED,
                message=survey_keyword_action.message_content,
                case=case, logged_event=logged_event)
        elif survey_keyword_action.action == METHOD_SMS_SURVEY:
            create_immediate_reminder(contact, METHOD_SMS_SURVEY,
                reminder_type=REMINDER_TYPE_KEYWORD_INITIATED,
                form_unique_id=survey_keyword_action.form_unique_id,
                case=case, logged_event=logged_event)
        elif survey_keyword_action.action == METHOD_STRUCTURED_SMS:
            res = handle_structured_sms(survey_keyword, survey_keyword_action,
                sender, verified_number, text, send_response=True, msg=msg,
                case=case, text_args=args, logged_event=logged_event)
            if not res:
                # If the structured sms processing wasn't successful, don't
                # process any of the other actions
                return
    logged_event.completed()
Esempio n. 49
0
def global_keyword_stop(v, text, msg, text_words, open_sessions):
    SQLXFormsSession.close_all_open_sms_sessions(v.domain, v.owner_id)
    return True
Esempio n. 50
0
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers, logged_event):
    if reminder.callback_try_count > 0:
        # Handle timeouts
        if handler.submit_partial_forms and (
            reminder.callback_try_count == len(reminder.current_event.callback_timeout_intervals)
        ):
            # Submit partial form completions
            for session_id in reminder.xforms_session_ids:
                submit_unfinished_form(session_id, handler.include_case_side_effects)
        else:
            # Resend current question
            for session_id in reminder.xforms_session_ids:
                session = get_session_by_session_id(session_id)
                if session.end_time is None:
                    vn = VerifiedNumber.view(
                        "sms/verified_number_by_owner_id", key=session.connection_id, include_docs=True
                    ).first()
                    if vn is not None:
                        metadata = MessageMetadata(
                            workflow=get_workflow(handler),
                            reminder_id=reminder._id,
                            xforms_session_couch_id=session._id,
                        )
                        resp = current_question(session_id)
                        send_sms_to_verified_number(vn, resp.event.text_prompt, metadata)
    else:
        reminder.xforms_session_ids = []
        domain_obj = Domain.get_by_name(reminder.domain, strict=True)

        # Get the app, module, and form
        try:
            form_unique_id = reminder.current_event.form_unique_id
            form = Form.get_form(form_unique_id)
            app = form.get_app()
            module = form.get_module()
        except Exception:
            logged_event.error(MessagingEvent.ERROR_CANNOT_FIND_FORM)
            return

        # Start a touchforms session for each recipient
        for recipient in recipients:
            logged_subevent = logged_event.create_subevent(handler, reminder, recipient)

            verified_number, unverified_number = get_recipient_phone_number(reminder, recipient, verified_numbers)

            no_verified_number = verified_number is None
            cant_use_unverified_number = (
                unverified_number is None or not domain_obj.send_to_duplicated_case_numbers or form_requires_input(form)
            )
            if no_verified_number and cant_use_unverified_number:
                logged_subevent.error(MessagingEvent.ERROR_NO_TWO_WAY_PHONE_NUMBER)
                continue

            key = "start-sms-survey-for-contact-%s" % recipient.get_id
            with CriticalSection([key], timeout=60):
                # Get the case to submit the form against, if any
                if isinstance(recipient, CommCareCase) and not handler.force_surveys_to_use_triggered_case:
                    case_id = recipient.get_id
                else:
                    case_id = reminder.case_id

                if form.requires_case() and not case_id:
                    logged_subevent.error(MessagingEvent.ERROR_NO_CASE_GIVEN)
                    continue

                # Close all currently open sessions
                SQLXFormsSession.close_all_open_sms_sessions(reminder.domain, recipient.get_id)

                # Start the new session
                try:
                    session, responses = start_session(
                        reminder.domain,
                        recipient,
                        app,
                        module,
                        form,
                        case_id,
                        case_for_case_submission=handler.force_surveys_to_use_triggered_case,
                    )
                except TouchformsError as e:
                    human_readable_message = e.response_data.get("human_readable_message", None)

                    logged_subevent.error(
                        MessagingEvent.ERROR_TOUCHFORMS_ERROR, additional_error_text=human_readable_message
                    )

                    if touchforms_error_is_config_error(e):
                        # Don't reraise the exception because this means there are configuration
                        # issues with the form that need to be fixed
                        continue
                    else:
                        # Reraise the exception so that the framework retries it again later
                        raise
                except Exception as e:
                    logged_subevent.error(MessagingEvent.ERROR_TOUCHFORMS_ERROR)
                    # Reraise the exception so that the framework retries it again later
                    raise
                session.survey_incentive = handler.survey_incentive
                session.workflow = get_workflow(handler)
                session.reminder_id = reminder._id
                session.save()

            reminder.xforms_session_ids.append(session.session_id)
            logged_subevent.xforms_session = session
            logged_subevent.save()

            # Send out first message
            if len(responses) > 0:
                message = format_message_list(responses)
                metadata = MessageMetadata(
                    workflow=get_workflow(handler), reminder_id=reminder._id, xforms_session_couch_id=session._id
                )
                if verified_number:
                    send_sms_to_verified_number(verified_number, message, metadata)
                else:
                    send_sms(reminder.domain, recipient, unverified_number, message, metadata)

            logged_subevent.completed()
Esempio n. 51
0
 def test_get_open_sms_session_no_results(self):
     self.assertEqual(None, SQLXFormsSession.get_open_sms_session(uuid.uuid4().hex, uuid.uuid4().hex))
Esempio n. 52
0
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers):
    if reminder.callback_try_count > 0:
        # Handle timeouts
        if handler.submit_partial_forms and (reminder.callback_try_count == len(reminder.current_event.callback_timeout_intervals)):
            # Submit partial form completions
            for session_id in reminder.xforms_session_ids:
                submit_unfinished_form(session_id, handler.include_case_side_effects)
        else:
            # Resend current question
            for session_id in reminder.xforms_session_ids:
                session = get_session_by_session_id(session_id)
                if session.end_time is None:
                    vn = VerifiedNumber.view("sms/verified_number_by_owner_id",
                                             key=session.connection_id,
                                             include_docs=True).first()
                    if vn is not None:
                        metadata = MessageMetadata(
                            workflow=get_workflow(handler),
                            reminder_id=reminder._id,
                            xforms_session_couch_id=session._id,
                        )
                        resp = current_question(session_id)
                        send_sms_to_verified_number(vn, resp.event.text_prompt, metadata)
        return True
    else:
        reminder.xforms_session_ids = []

        # Get the app, module, and form
        try:
            form_unique_id = reminder.current_event.form_unique_id
            form = Form.get_form(form_unique_id)
            app = form.get_app()
            module = form.get_module()
        except Exception:
            raise_error(reminder, ERROR_FORM)
            return False

        # Start a touchforms session for each recipient
        for recipient in recipients:

            verified_number, unverified_number = get_recipient_phone_number(
                reminder, recipient, verified_numbers)

            domain_obj = Domain.get_by_name(reminder.domain, strict=True)
            no_verified_number = verified_number is None
            cant_use_unverified_number = (unverified_number is None or
                not domain_obj.send_to_duplicated_case_numbers or
                form_requires_input(form))
            if no_verified_number and cant_use_unverified_number:
                if len(recipients) == 1:
                    raise_error(reminder, ERROR_NO_VERIFIED_NUMBER)
                    return False
                else:
                    continue

            key = "start-sms-survey-for-contact-%s" % recipient.get_id
            with CriticalSection([key], timeout=60):
                # Close all currently open sessions
                SQLXFormsSession.close_all_open_sms_sessions(reminder.domain, recipient.get_id)

                # Start the new session
                if (isinstance(recipient, CommCareCase) and
                    not handler.force_surveys_to_use_triggered_case):
                    case_id = recipient.get_id
                else:
                    case_id = reminder.case_id
                session, responses = start_session(reminder.domain, recipient,
                    app, module, form, case_id, case_for_case_submission=
                    handler.force_surveys_to_use_triggered_case)
                session.survey_incentive = handler.survey_incentive
                session.workflow = get_workflow(handler)
                session.reminder_id = reminder._id
                session.save()

            reminder.xforms_session_ids.append(session.session_id)

            # Send out first message
            if len(responses) > 0:
                message = format_message_list(responses)
                metadata = MessageMetadata(
                    workflow=get_workflow(handler),
                    reminder_id=reminder._id,
                    xforms_session_couch_id=session._id,
                )
                if verified_number:
                    result = send_sms_to_verified_number(verified_number, message, metadata)
                else:
                    result = send_sms(reminder.domain, recipient, unverified_number,
                        message, metadata)

                if len(recipients) == 1:
                    return result

        return True
Esempio n. 53
0
def _make_session(**kwargs):
    properties = _arbitrary_session_properties(**kwargs)
    session = SQLXFormsSession(**properties)
    session.save()
    return session
Esempio n. 54
0
 def test_get_by_session_id_not_found(self):
     self.assertEqual(None, SQLXFormsSession.by_session_id(uuid.uuid4().hex))
Esempio n. 55
0
 def get_open_session(self, contact):
     return SQLXFormsSession.get_open_sms_session(self.domain, contact._id)
Esempio n. 56
0
def handle_sms_form_complete(sender, session_id, form, **kwargs):
    from corehq.apps.smsforms.models import SQLXFormsSession
    session = SQLXFormsSession.by_session_id(session_id)
    if session:
        process_sms_form_complete(session, form)
Esempio n. 57
0
 def test_get_by_session_id(self):
     sql_session = _make_session()
     self.assertEqual(sql_session.pk, SQLXFormsSession.by_session_id(sql_session.session_id).pk)
Esempio n. 58
0
def process_survey_keyword_actions(verified_number, survey_keyword, text, msg):
    sender = verified_number.owner
    case = None
    args = split_args(text, survey_keyword)

    logged_event = MessagingEvent.create_from_keyword(survey_keyword, sender)

    # Log a messaging subevent for the incoming message
    subevent = logged_event.create_subevent_for_single_sms(
        msg.couch_recipient_doc_type,
        msg.couch_recipient,
        completed=True
    )
    add_msg_tags(msg, MessageMetadata(messaging_subevent_id=subevent.pk))

    # Close any open sessions even if it's just an sms that we're
    # responding with.
    SQLXFormsSession.close_all_open_sms_sessions(verified_number.domain,
        verified_number.owner_id)

    if is_commcarecase(sender):
        case = sender
        args = args[1:]
    elif isinstance(sender, CommCareUser):
        if keyword_uses_form_that_requires_case(survey_keyword):
            if len(args) > 1:
                external_id = args[1]
                case, matches = get_case_by_external_id(verified_number.domain,
                    external_id, sender)
                if matches == 0:
                    send_keyword_response(verified_number, MSG_CASE_NOT_FOUND, logged_event)
                    logged_event.error(MessagingEvent.ERROR_CASE_EXTERNAL_ID_NOT_FOUND)
                    return
                elif matches > 1:
                    send_keyword_response(verified_number, MSG_MULTIPLE_CASES_FOUND, logged_event)
                    logged_event.error(MessagingEvent.ERROR_MULTIPLE_CASES_WITH_EXTERNAL_ID_FOUND)
                    return
            else:
                send_keyword_response(verified_number, MSG_MISSING_EXTERNAL_ID, logged_event)
                logged_event.error(MessagingEvent.ERROR_NO_EXTERNAL_ID_GIVEN)
                return
            args = args[2:]
        else:
            args = args[1:]

    def cmp_fcn(a1, a2):
        a1_ss = (a1.action == KeywordAction.ACTION_STRUCTURED_SMS)
        a2_ss = (a2.action == KeywordAction.ACTION_STRUCTURED_SMS)
        if a1_ss and a2_ss:
            return 0
        elif a1_ss:
            return -1
        elif a2_ss:
            return 1
        else:
            return 0

    if case:
        subevent.case_id = case.case_id
        subevent.save()

    # Process structured sms actions first
    actions = sorted(survey_keyword.keywordaction_set.all(), cmp=cmp_fcn)
    for survey_keyword_action in actions:
        if survey_keyword_action.recipient == KeywordAction.RECIPIENT_SENDER:
            contact = sender
        elif survey_keyword_action.recipient == KeywordAction.RECIPIENT_OWNER:
            if is_commcarecase(sender):
                contact = get_wrapped_owner(get_owner_id(sender))
            else:
                contact = None
        elif survey_keyword_action.recipient == KeywordAction.RECIPIENT_USER_GROUP:
            try:
                contact = Group.get(survey_keyword_action.recipient_id)
                assert contact.doc_type == "Group"
                assert contact.domain == verified_number.domain
            except Exception:
                contact = None
        else:
            contact = None

        if contact is None:
            continue

        # contact can be either a user, case, group, or location
        if survey_keyword_action.action in (KeywordAction.ACTION_SMS, KeywordAction.ACTION_SMS_SURVEY):
            if isinstance(contact, Group):
                recipients = list(ScheduleInstance.expand_group(contact))
            elif isinstance(contact, SQLLocation):
                recipients = list(ScheduleInstance.expand_location_ids(contact.domain, [contact.location_id]))
            else:
                recipients = [contact]

            recipient_is_sender = survey_keyword_action.recipient == KeywordAction.RECIPIENT_SENDER

            if survey_keyword_action.action == KeywordAction.ACTION_SMS:
                content = SMSContent(message={'*': survey_keyword_action.message_content})
                content.set_context(case=case)
            elif survey_keyword_action.action == KeywordAction.ACTION_SMS_SURVEY:
                content = SMSSurveyContent(
                    form_unique_id=survey_keyword_action.form_unique_id,
                    expire_after=SQLXFormsSession.MAX_SESSION_LENGTH,
                )
                content.set_context(
                    case=case,
                    critical_section_already_acquired=recipient_is_sender,
                )
            else:
                raise ValueError("Unexpected action %s" % survey_keyword_action.action)

            for recipient in recipients:
                phone_entry = verified_number if recipient_is_sender else None
                content.send(recipient, logged_event, phone_entry=phone_entry)

        elif survey_keyword_action.action == KeywordAction.ACTION_STRUCTURED_SMS:
            res = handle_structured_sms(survey_keyword, survey_keyword_action,
                sender, verified_number, text, send_response=True, msg=msg,
                case=case, text_args=args, logged_event=logged_event)
            if not res:
                # If the structured sms processing wasn't successful, don't
                # process any of the other actions
                return
    logged_event.completed()