Example #1
0
def start_call_session(recipient, call_log_entry, logged_subevent, app, module, form):
    """
    Returns (session, responses, error)
    """
    try:
        session, responses = start_session(recipient.domain, recipient, app,
            module, form, call_log_entry.case_id, yield_responses=True,
            session_type=XFORMS_SESSION_IVR,
            case_for_case_submission=call_log_entry.case_for_case_submission)

        if logged_subevent:
            logged_subevent.xforms_session = session
            logged_subevent.save()

        if len(responses) == 0:
            log_error(MessagingEvent.ERROR_FORM_HAS_NO_QUESTIONS,
                call_log_entry, logged_subevent)
            return (session, responses, True)

        return (session, responses, False)
    except TouchformsError as e:
        additional_error_text = e.response_data.get('human_readable_message', None)
        log_error(MessagingEvent.ERROR_TOUCHFORMS_ERROR,
            call_log_entry, logged_subevent, additional_error_text=additional_error_text)
        return (None, None, True)
Example #2
0
def start_session_from_keyword(survey_keyword, verified_number):
    try:
        form_unique_id = survey_keyword.form_unique_id
        form = Form.get_form(form_unique_id)
        app = form.get_app()
        module = form.get_module()

        if verified_number.owner_doc_type == "CommCareCase":
            case_id = verified_number.owner_id
        else:
            #TODO: Need a way to choose the case when it's a user that's playing the form
            case_id = None

        session, responses = start_session(verified_number.domain,
                                           verified_number.owner, app, module,
                                           form, case_id)

        if len(responses) > 0:
            message = format_message_list(responses)
            send_sms_to_verified_number(verified_number, message)

    except Exception:
        logging.exception(
            "Exception while starting survey for keyword %s, domain %s" %
            (survey_keyword.keyword, verified_number.domain))
Example #3
0
def start_session_with_error_handling(domain, contact, app, module, form,
        case_id, keyword, logged_subevent=None):
    """
    Returns (session, responses, error, error_code)
    """
    try:
        session, responses = start_session(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)
Example #4
0
def start_call_session(recipient, call_log_entry, logged_subevent, app, module,
                       form):
    """
    Returns (session, responses, error)
    """
    try:
        session, responses = start_session(
            recipient.domain,
            recipient,
            app,
            module,
            form,
            call_log_entry.case_id,
            yield_responses=True,
            session_type=XFORMS_SESSION_IVR,
            case_for_case_submission=call_log_entry.case_for_case_submission)

        if logged_subevent:
            logged_subevent.xforms_session = session
            logged_subevent.save()

        if len(responses) == 0:
            log_error(MessagingEvent.ERROR_FORM_HAS_NO_QUESTIONS,
                      call_log_entry, logged_subevent)
            return (session, responses, True)

        return (session, responses, False)
    except TouchformsError as e:
        additional_error_text = e.response_data.get('human_readable_message',
                                                    None)
        log_error(MessagingEvent.ERROR_TOUCHFORMS_ERROR,
                  call_log_entry,
                  logged_subevent,
                  additional_error_text=additional_error_text)
        return (None, None, True)
Example #5
0
def start_session_with_error_handling(domain, contact, app, module, form,
        case_id, keyword, logged_subevent=None):
    """
    Returns (session, responses, error, error_code)
    """
    try:
        session, responses = start_session(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._id, domain, keyword)))
            error_code = MSG_TOUCHFORMS_ERROR

        return (None, None, True, error_code)
Example #6
0
    def test_case_integration(self):
        # load the app
        with open(
                os.path.join(os.path.dirname(__file__), "data",
                             "app_with_cases.json")) as f:
            app_json = json.loads(f.read())
            app = import_app(app_json, self.domain)

        # the first form opens the case
        session, responses = start_session(self.domain, self.contact, app,
                                           app.get_module(0),
                                           app.get_module(0).get_form(0))

        [answer] = responses
        self.assertEqual("what's the case name?", answer)
        q_and_a(self, "some case", "thanks, you're done!", self.domain)

        def _get_case(session):
            session = XFormsSession.get(session.get_id)
            self.assertTrue(session.submission_id)
            instance = XFormInstance.get(session.submission_id)
            case_id = instance.xpath("form/case/@case_id")
            self.assertTrue(case_id)
            return CommCareCase.get(case_id)

        # check the case
        case = _get_case(session)
        self.assertEqual("some case", case.name)
        self.assertFalse(case.closed)
        self.assertFalse(hasattr(case, "feeling"))

        # the second form updates the case
        # NOTE: this currently fails for several reasons, the most
        # notable being that there needs to be a server running configured
        # to hit the test DB, and that there's no authentication built in
        session, responses = start_session(self.domain,
                                           self.contact,
                                           app,
                                           app.get_module(0),
                                           app.get_module(0).get_form(1),
                                           case_id=case.get_id)

        [answer] = responses
        self.assertEqual("how you feeling, some case?", answer)
        q_and_a(self, "groovy", "thanks, you're done!", self.domain)
Example #7
0
 def test_case_integration(self):
     # load the app
     with open(os.path.join(os.path.dirname(__file__), "data", "app_with_cases.json")) as f:
         app_json = json.loads(f.read())
         app = import_app(app_json, self.domain)
         
     # the first form opens the case
     session, responses = start_session(self.domain, self.contact, app, 
                                        app.get_module(0), 
                                        app.get_module(0).get_form(0))
     
     [answer] = responses 
     self.assertEqual("what's the case name?", answer)
     q_and_a(self, "some case", "thanks, you're done!", self.domain)
     
     def _get_case(session):
         session = XFormsSession.get(session.get_id)
         self.assertTrue(session.submission_id)
         instance = XFormInstance.get(session.submission_id)
         case_id = instance.xpath("form/case/@case_id")
         self.assertTrue(case_id)
         return CommCareCase.get(case_id)
     
     # check the case
     case = _get_case(session)
     self.assertEqual("some case", case.name)
     self.assertFalse(case.closed)
     self.assertFalse(hasattr(case, "feeling"))
     
     # the second form updates the case
     # NOTE: this currently fails for several reasons, the most
     # notable being that there needs to be a server running configured
     # to hit the test DB, and that there's no authentication built in
     session, responses = start_session(self.domain, self.contact, app, 
                                        app.get_module(0), 
                                        app.get_module(0).get_form(1),
                                        case_id=case.get_id)
     
     [answer] = responses 
     self.assertEqual("how you feeling, some case?", answer)
     q_and_a(self, "groovy", "thanks, you're done!", self.domain)
     
     
Example #8
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
Example #9
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
Example #10
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)
Example #11
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)
Example #12
0
    def test_basic_form_playing(self):
        # load the app
        with open(
                os.path.join(os.path.dirname(__file__), "data",
                             "demo_app.json")) as f:
            app_json = json.loads(f.read())
            app = import_app(app_json, self.domain)

        # start form session
        session, responses = start_session(self.domain, self.contact, app,
                                           app.get_module(0),
                                           app.get_module(0).get_form(0))

        [answer] = responses
        self.assertEqual("what is your name?", answer)

        # check state of model
        self.assertEqual(session.start_time, session.modified_time)
        self.assertEqual("http://www.commcarehq.org/tests/smsforms",
                         session.form_xmlns)
        self.assertFalse(session.end_time)
        self.assertEqual(False, session.completed)
        self.assertEqual(self.domain, session.domain)
        self.assertEqual(self.contact.get_id, session.user_id)
        self.assertEqual(app.get_id, session.app_id)
        self.assertFalse(session.submission_id)

        # play through the form, checking answers
        q_and_a(self, "sms contact", "how old are you, sms contact?",
                self.domain)
        q_and_a(self, "29", "what is your gender? 1:male, 2:female",
                self.domain)
        q_and_a(self, "2", "thanks for submitting!", self.domain)

        # check the instance
        session = XFormsSession.get(session.get_id)
        self.assertTrue(session.submission_id)
        instance = XFormInstance.get(session.submission_id)
        self.assertEqual("sms contact", instance.xpath("form/name"))
        self.assertEqual("29", instance.xpath("form/age"))
        self.assertEqual("f", instance.xpath("form/gender"))
        self.assertEqual(self.domain, instance.domain)
Example #13
0
def start_session_from_keyword(survey_keyword, verified_number):
    try:
        form_unique_id = survey_keyword.form_unique_id
        form = Form.get_form(form_unique_id)
        app = form.get_app()
        module = form.get_module()
        
        if verified_number.owner_doc_type == "CommCareCase":
            case_id = verified_number.owner_id
        else:
            #TODO: Need a way to choose the case when it's a user that's playing the form
            case_id = None
        
        session, responses = start_session(verified_number.domain, verified_number.owner, app, module, form, case_id)
        
        if len(responses) > 0:
            message = format_message_list(responses)
            send_sms_to_verified_number(verified_number, message)
        
    except Exception:
        logging.exception("Exception while starting survey for keyword %s, domain %s" % (survey_keyword.keyword, verified_number.domain))
Example #14
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)
Example #15
0
 def test_basic_form_playing(self):
     # load the app
     with open(os.path.join(os.path.dirname(__file__), "data", "demo_app.json")) as f:
         app_json = json.loads(f.read())
         app = import_app(app_json, self.domain)
         
     # start form session
     session, responses = start_session(self.domain, self.contact, app, 
                                        app.get_module(0), 
                                        app.get_module(0).get_form(0))
     
     [answer] = responses 
     self.assertEqual("what is your name?", answer)
     
     # check state of model
     self.assertEqual(session.start_time, session.modified_time)
     self.assertEqual("http://www.commcarehq.org/tests/smsforms", session.form_xmlns)
     self.assertFalse(session.end_time)
     self.assertEqual(False, session.completed)
     self.assertEqual(self.domain, session.domain)
     self.assertEqual(self.contact.get_id, session.user_id)
     self.assertEqual(app.get_id, session.app_id)
     self.assertFalse(session.submission_id)
     
     # play through the form, checking answers
     q_and_a(self, "sms contact", "how old are you, sms contact?", self.domain)
     q_and_a(self, "29", "what is your gender? 1:male, 2:female", self.domain)
     q_and_a(self, "2", "thanks for submitting!", self.domain)
     
     # check the instance
     session = XFormsSession.get(session.get_id)
     self.assertTrue(session.submission_id)
     instance = XFormInstance.get(session.submission_id)
     self.assertEqual("sms contact", instance.xpath("form/name"))
     self.assertEqual("29", instance.xpath("form/age"))
     self.assertEqual("f", instance.xpath("form/gender"))
     self.assertEqual(self.domain, instance.domain)
Example #16
0
def structured_sms_handler(verified_number, text):

    # Circular Import
    from corehq.apps.reminders.models import SurveyKeyword, FORM_TYPE_ALL_AT_ONCE

    text = text.strip()
    if text == "":
        return False
    for survey_keyword in SurveyKeyword.get_all(verified_number.domain):
        if survey_keyword.form_type == FORM_TYPE_ALL_AT_ONCE:
            if survey_keyword.delimiter is not None:
                args = text.split(survey_keyword.delimiter)
            else:
                args = text.split()

            keyword = args[0].strip().upper()
            if keyword != survey_keyword.keyword.upper():
                continue

            try:
                error_occurred = False
                error_msg = ""
                form_complete = False

                # Close any open sessions
                close_open_sessions(verified_number.domain,
                                    verified_number.owner_id)

                # Start the session
                form = Form.get_form(survey_keyword.form_unique_id)
                app = form.get_app()
                module = form.get_module()

                if verified_number.owner_doc_type == "CommCareCase":
                    case_id = verified_number.owner_id
                else:
                    #TODO: Need a way to choose the case when it's a user that's playing the form
                    case_id = None

                session, responses = start_session(verified_number.domain,
                                                   verified_number.owner,
                                                   app,
                                                   module,
                                                   form,
                                                   case_id=case_id,
                                                   yield_responses=True)
                assert len(
                    responses) > 0, "There should be at least one response."

                current_question = responses[-1]
                form_complete = is_form_complete(current_question)

                if not form_complete:
                    if survey_keyword.use_named_args:
                        # Arguments in the sms are named
                        xpath_answer = {}  # Dictionary of {xpath : answer}
                        for answer in args[1:]:
                            answer = answer.strip()
                            answer_upper = answer.upper()
                            if survey_keyword.named_args_separator is not None:
                                # A separator is used for naming arguments; for example, the "=" in "register name=joe age=25"
                                answer_parts = answer.partition(
                                    survey_keyword.named_args_separator)
                                if answer_parts[
                                        1] != survey_keyword.named_args_separator:
                                    error_occurred = True
                                    error_msg = "ERROR: Expected name and value to be joined by" + (
                                        " '%s'" %
                                        survey_keyword.named_args_separator)
                                    break
                                else:
                                    arg_name = answer_parts[0].upper().strip()
                                    xpath = survey_keyword.named_args.get(
                                        arg_name, None)
                                    if xpath is not None:
                                        if xpath in xpath_answer:
                                            error_occurred = True
                                            error_msg = "ERROR: More than one answer found for" + (
                                                " '%s'" % arg_name)
                                            break

                                        xpath_answer[xpath] = answer_parts[
                                            2].strip()
                                    else:
                                        # Ignore unexpected named arguments
                                        pass
                            else:
                                # No separator is used for naming arguments; for example, "update a100 b34 c5"
                                matches = 0
                                for k, v in survey_keyword.named_args.items():
                                    if answer_upper.startswith(k):
                                        matches += 1
                                        if matches > 1:
                                            error_occurred = True
                                            error_msg = "ERROR: More than one question matches" + (
                                                " '%s'" % answer)
                                            break

                                        if v in xpath_answer:
                                            error_occurred = True
                                            error_msg = "ERROR: More than one answer found for" + (
                                                " '%s'" % k)
                                            break

                                        xpath_answer[v] = answer[
                                            len(k):].strip()

                                if matches == 0:
                                    # Ignore unexpected named arguments
                                    pass

                            if error_occurred:
                                break

                        # Go through each question in the form, answering only the questions that the sms has answers for
                        while not form_complete and not error_occurred:
                            if current_question.is_error:
                                error_occurred = True
                                error_msg = current_question.text_prompt or "ERROR: Internal server error"
                                break

                            xpath = current_question.event._dict["binding"]
                            if xpath in xpath_answer:
                                valid, answer, _error_msg = validate_answer(
                                    current_question.event,
                                    xpath_answer[xpath])
                                if not valid:
                                    error_occurred = True
                                    error_msg = "ERROR: " + _error_msg
                                    break
                                responses = _get_responses(
                                    verified_number.domain,
                                    verified_number.owner_id,
                                    answer,
                                    yield_responses=True)
                            else:
                                responses = _get_responses(
                                    verified_number.domain,
                                    verified_number.owner_id,
                                    "",
                                    yield_responses=True)

                            current_question = responses[-1]
                            if is_form_complete(current_question):
                                form_complete = True
                    else:
                        # Arguments in the sms are not named; pass each argument to each question in order
                        for answer in args[1:]:
                            if form_complete:
                                # Form is complete, ignore remaining answers
                                break

                            if current_question.is_error:
                                error_occurred = True
                                error_msg = current_question.text_prompt or "ERROR: Internal server error"
                                break

                            valid, answer, _error_msg = validate_answer(
                                current_question.event, answer.strip())
                            if not valid:
                                error_occurred = True
                                error_msg = "ERROR: " + _error_msg
                                break

                            responses = _get_responses(
                                verified_number.domain,
                                verified_number.owner_id,
                                answer,
                                yield_responses=True)
                            current_question = responses[-1]
                            form_complete = is_form_complete(current_question)

                        # If the form isn't finished yet but we're out of arguments, try to leave each remaining question blank and continue
                        while not form_complete and not error_occurred:
                            responses = _get_responses(
                                verified_number.domain,
                                verified_number.owner_id,
                                "",
                                yield_responses=True)
                            current_question = responses[-1]

                            if current_question.is_error:
                                error_occurred = True
                                error_msg = current_question.text_prompt or "ERROR: Internal server error"

                            if is_form_complete(current_question):
                                form_complete = True
            except Exception:
                logging.exception(
                    "Could not process structured sms for verified number %s, domain %s, keyword %s"
                    % (verified_number._id, verified_number.domain, keyword))
                error_occurred = True
                error_msg = "ERROR: Internal server error"

            if error_occurred:
                send_sms_to_verified_number(verified_number, error_msg)

            if error_occurred or not form_complete:
                session = XFormsSession.get(session._id)
                session.end(False)
                session.save()

            return True

    return False
Example #17
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()
Example #18
0
 def fire(self, reminder):
     """
     Sends the message associated with the given CaseReminder's current event.
     
     reminder    The CaseReminder which to fire.
     
     return      True on success, False on failure
     """
     # Get the proper recipient
     recipient = reminder.recipient
     
     # Retrieve the VerifiedNumber entry for the recipient
     try:
         verified_number = recipient.get_verified_number()
     except Exception:
         verified_number = None
         
     # Get the language of the recipient
     try:
         lang = recipient.get_language_code()
     except Exception:
         lang = None
     
     if reminder.method == "survey":
         # Close all currently open sessions
         sessions = XFormsSession.view("smsforms/open_sessions_by_connection",
                                      key=[reminder.domain, recipient.get_id],
                                      include_docs=True).all()
         for session in sessions:
             session.end(False)
             session.save()
         
         # Start the new session
         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 as e:
             print e
             print "ERROR: Could not load survey form for handler " + reminder.handler_id + ", event " + str(reminder.current_event_sequence_num)
             return False
         session, responses = start_session(reminder.domain, recipient, app, module, form, reminder.case_id)
         
         # Send out first message
         if len(responses) > 0:
             message = format_message_list(responses)
             if verified_number is not None:
                 return send_sms_to_verified_number(verified_number, message)
             else:
                 return True
     else:
         # If it is a callback reminder and the callback has been received, skip sending the next timeout message
         if (reminder.method == "callback" or reminder.method == "callback_test") and len(reminder.current_event.callback_timeout_intervals) > 0 and (reminder.callback_try_count > 0):
             if CallLog.inbound_call_exists(recipient.doc_type, recipient._id, reminder.last_fired):
                 reminder.callback_received = True
                 return True
             elif len(reminder.current_event.callback_timeout_intervals) == reminder.callback_try_count:
                 # On the last callback timeout, instead of sending the SMS again, log the missed callback
                 event = EventLog(
                     domain          = reminder.domain,
                     date            = self.get_now(),
                     event_type      = MISSED_EXPECTED_CALLBACK
                 )
                 if verified_number is not None:
                     event.couch_recipient_doc_type = verified_number.owner_doc_type
                     event.couch_recipient = verified_number.owner_id
                 event.save()
                 return True
         reminder.last_fired = self.get_now()
         message = reminder.current_event.message.get(lang, reminder.current_event.message[self.default_lang])
         message = Message.render(message, case=reminder.case.case_properties())
         if reminder.method == "sms" or reminder.method == "callback":
             if verified_number is not None:
                 return send_sms_to_verified_number(verified_number, message)
             elif self.recipient == RECIPIENT_USER:
                 # If there is no verified number, but the recipient is a CommCareUser, still try to send it
                 try:
                     phone_number = reminder.user.phone_number
                 except Exception:
                     # If the user has no phone number, we cannot send any SMS
                     return False
                 return send_sms(reminder.domain, reminder.user_id, phone_number, message)
             else:
                 return False
         elif reminder.method == "test" or reminder.method == "callback_test":
             print(message)
             return True
Example #19
0
def incoming(phone_number, backend_module, gateway_session_id, ivr_event, input_data=None):
    # Look up the call if one already exists
    call_log_entry = CallLog.view("sms/call_by_session",
                                  startkey=[gateway_session_id, {}],
                                  endkey=[gateway_session_id],
                                  descending=True,
                                  include_docs=True,
                                  limit=1).one()
    
    answer_is_valid = False # This will be set to True if IVR validation passes
    error_occurred = False # This will be set to False if touchforms validation passes (i.e., no form constraints fail)
    
    if call_log_entry is not None and backend_module:
        if ivr_event == IVR_EVENT_NEW_CALL and call_log_entry.use_precached_first_response:
            return HttpResponse(call_log_entry.first_response)
        
        form = Form.get_form(call_log_entry.form_unique_id)
        app = form.get_app()
        module = form.get_module()
        recipient = call_log_entry.recipient
        
        if ivr_event == IVR_EVENT_NEW_CALL:
            case_id = call_log_entry.case_id
            case_for_case_submission = call_log_entry.case_for_case_submission
            session, responses = start_session(recipient.domain, recipient, app,
                module, form, case_id, yield_responses=True,
                session_type=XFORMS_SESSION_IVR,
                case_for_case_submission=case_for_case_submission)
            call_log_entry.xforms_session_id = session.session_id
        elif ivr_event == IVR_EVENT_INPUT:
            if call_log_entry.xforms_session_id is not None:
                current_q = current_question(call_log_entry.xforms_session_id)
                if validate_answer(input_data, current_q):
                    answer_is_valid = True
                    responses = _get_responses(recipient.domain, recipient._id, input_data, yield_responses=True, session_id=call_log_entry.xforms_session_id)
                else:
                    call_log_entry.current_question_retry_count += 1
                    responses = [current_q]
            else:
                responses = []
        else:
            responses = []
        
        ivr_responses = []
        hang_up = False
        for response in responses:
            if response.is_error:
                error_occurred = True
                call_log_entry.current_question_retry_count += 1
                if response.text_prompt is None:
                    ivr_responses = []
                    break
                else:
                    ivr_responses.append(format_ivr_response(response.text_prompt, app))
            elif response.event.type == "question":
                ivr_responses.append(format_ivr_response(response.event.caption, app))
            elif response.event.type == "form-complete":
                hang_up = True
        
        if answer_is_valid and not error_occurred:
            call_log_entry.current_question_retry_count = 0
        
        if call_log_entry.max_question_retries is not None and call_log_entry.current_question_retry_count > call_log_entry.max_question_retries:
            # Force hang-up
            ivr_responses = []
        
        if len(ivr_responses) == 0:
            hang_up = True
        
        input_length = None
        
        if hang_up:
            if call_log_entry.xforms_session_id is not None:
                # Process disconnect
                session = get_session_by_session_id(call_log_entry.xforms_session_id)
                if session.end_time is None:
                    if call_log_entry.submit_partial_form:
                        submit_unfinished_form(session.session_id, call_log_entry.include_case_side_effects)
                    else:
                        session.end(completed=False)
                        session.save()
        else:
            # Set input_length to let the ivr gateway know how many digits we need to collect.
            # Have to get the current question again, since the last XFormsResponse in responses
            # may not have an event if it was a response to a constraint error.
            if error_occurred:
                current_q = current_question(call_log_entry.xforms_session_id)
            else:
                current_q = responses[-1]
            
            input_length = get_input_length(current_q)
        
        call_log_entry.save()
        return HttpResponse(backend_module.get_http_response_string(gateway_session_id, ivr_responses, collect_input=(not hang_up), hang_up=hang_up, input_length=input_length))
    
    # If not processed, just log the call

    if call_log_entry:
        # No need to log, already exists
        return HttpResponse("")

    cleaned_number = phone_number
    if cleaned_number is not None and len(cleaned_number) > 0 and cleaned_number[0] == "+":
        cleaned_number = cleaned_number[1:]
    
    # Try to look up the verified number entry
    v = VerifiedNumber.view("sms/verified_number_by_number",
        key=cleaned_number,
        include_docs=True
    ).one()
    
    # If none was found, try to match only the last digits of numbers in the database
    if v is None:
        v = VerifiedNumber.view("sms/verified_number_by_suffix",
            key=cleaned_number,
            include_docs=True
        ).one()
    
    # Save the call entry
    msg = CallLog(
        phone_number=cleaned_number,
        direction=INCOMING,
        date=datetime.utcnow(),
        backend_api=backend_module.API_ID if backend_module else None,
        gateway_session_id=gateway_session_id,
    )
    if v is not None:
        msg.domain = v.domain
        msg.couch_recipient_doc_type = v.owner_doc_type
        msg.couch_recipient = v.owner_id
    msg.save()
    
    return HttpResponse("")
Example #20
0
def structured_sms_handler(verified_number, text):

    # Circular Import
    from corehq.apps.reminders.models import SurveyKeyword, FORM_TYPE_ALL_AT_ONCE

    text = text.strip()
    if text == "":
        return False
    for survey_keyword in SurveyKeyword.get_all(verified_number.domain):
        if survey_keyword.form_type == FORM_TYPE_ALL_AT_ONCE:
            if survey_keyword.delimiter is not None:
                args = text.split(survey_keyword.delimiter)
            else:
                args = text.split()

            keyword = args[0].strip().upper()
            if keyword != survey_keyword.keyword.upper():
                continue

            try:
                error_occurred = False
                error_msg = ""
                form_complete = False

                # Close any open sessions
                close_open_sessions(verified_number.domain, verified_number.owner_id)

                # Start the session
                form = Form.get_form(survey_keyword.form_unique_id)
                app = form.get_app()
                module = form.get_module()

                if verified_number.owner_doc_type == "CommCareCase":
                    case_id = verified_number.owner_id
                else:
                    # TODO: Need a way to choose the case when it's a user that's playing the form
                    case_id = None

                session, responses = start_session(
                    verified_number.domain,
                    verified_number.owner,
                    app,
                    module,
                    form,
                    case_id=case_id,
                    yield_responses=True,
                )
                assert len(responses) > 0, "There should be at least one response."

                current_question = responses[-1]
                form_complete = is_form_complete(current_question)

                if not form_complete:
                    if survey_keyword.use_named_args:
                        # Arguments in the sms are named
                        xpath_answer = {}  # Dictionary of {xpath : answer}
                        for answer in args[1:]:
                            answer = answer.strip()
                            answer_upper = answer.upper()
                            if survey_keyword.named_args_separator is not None:
                                # A separator is used for naming arguments; for example, the "=" in "register name=joe age=25"
                                answer_parts = answer.partition(survey_keyword.named_args_separator)
                                if answer_parts[1] != survey_keyword.named_args_separator:
                                    error_occurred = True
                                    error_msg = "ERROR: Expected name and value to be joined by" + (
                                        " '%s'" % survey_keyword.named_args_separator
                                    )
                                    break
                                else:
                                    arg_name = answer_parts[0].upper().strip()
                                    xpath = survey_keyword.named_args.get(arg_name, None)
                                    if xpath is not None:
                                        if xpath in xpath_answer:
                                            error_occurred = True
                                            error_msg = "ERROR: More than one answer found for" + (" '%s'" % arg_name)
                                            break

                                        xpath_answer[xpath] = answer_parts[2].strip()
                                    else:
                                        # Ignore unexpected named arguments
                                        pass
                            else:
                                # No separator is used for naming arguments; for example, "update a100 b34 c5"
                                matches = 0
                                for k, v in survey_keyword.named_args.items():
                                    if answer_upper.startswith(k):
                                        matches += 1
                                        if matches > 1:
                                            error_occurred = True
                                            error_msg = "ERROR: More than one question matches" + (" '%s'" % answer)
                                            break

                                        if v in xpath_answer:
                                            error_occurred = True
                                            error_msg = "ERROR: More than one answer found for" + (" '%s'" % k)
                                            break

                                        xpath_answer[v] = answer[len(k) :].strip()

                                if matches == 0:
                                    # Ignore unexpected named arguments
                                    pass

                            if error_occurred:
                                break

                        # Go through each question in the form, answering only the questions that the sms has answers for
                        while not form_complete and not error_occurred:
                            if current_question.is_error:
                                error_occurred = True
                                error_msg = current_question.text_prompt or "ERROR: Internal server error"
                                break

                            xpath = current_question.event._dict["binding"]
                            if xpath in xpath_answer:
                                valid, answer, _error_msg = validate_answer(current_question.event, xpath_answer[xpath])
                                if not valid:
                                    error_occurred = True
                                    error_msg = "ERROR: " + _error_msg
                                    break
                                responses = _get_responses(
                                    verified_number.domain, verified_number.owner_id, answer, yield_responses=True
                                )
                            else:
                                responses = _get_responses(
                                    verified_number.domain, verified_number.owner_id, "", yield_responses=True
                                )

                            current_question = responses[-1]
                            if is_form_complete(current_question):
                                form_complete = True
                    else:
                        # Arguments in the sms are not named; pass each argument to each question in order
                        for answer in args[1:]:
                            if form_complete:
                                # Form is complete, ignore remaining answers
                                break

                            if current_question.is_error:
                                error_occurred = True
                                error_msg = current_question.text_prompt or "ERROR: Internal server error"
                                break

                            valid, answer, _error_msg = validate_answer(current_question.event, answer.strip())
                            if not valid:
                                error_occurred = True
                                error_msg = "ERROR: " + _error_msg
                                break

                            responses = _get_responses(
                                verified_number.domain, verified_number.owner_id, answer, yield_responses=True
                            )
                            current_question = responses[-1]
                            form_complete = is_form_complete(current_question)

                        # If the form isn't finished yet but we're out of arguments, try to leave each remaining question blank and continue
                        while not form_complete and not error_occurred:
                            responses = _get_responses(
                                verified_number.domain, verified_number.owner_id, "", yield_responses=True
                            )
                            current_question = responses[-1]

                            if current_question.is_error:
                                error_occurred = True
                                error_msg = current_question.text_prompt or "ERROR: Internal server error"

                            if is_form_complete(current_question):
                                form_complete = True
            except Exception:
                logging.exception(
                    "Could not process structured sms for verified number %s, domain %s, keyword %s"
                    % (verified_number._id, verified_number.domain, keyword)
                )
                error_occurred = True
                error_msg = "ERROR: Internal server error"

            if error_occurred:
                send_sms_to_verified_number(verified_number, error_msg)

            if error_occurred or not form_complete:
                session = XFormsSession.get(session._id)
                session.end(False)
                session.save()

            return True

    return False
Example #21
0
    error_msg = None
    session = None

    try:
        # Start the session
        app, module, form = get_form(
            survey_keyword_action.form_unique_id, include_app_module=True)

        if case:
            case_id = case._id
        elif contact_doc_type == "CommCareCase":
            case_id = contact_id
        else:
            case_id = None

        session, responses = start_session(domain, contact, app, module,
            form, case_id=case_id, yield_responses=True)
        session.workflow = WORKFLOW_KEYWORD
        session.save()
        assert len(responses) > 0, "There should be at least one response."

        first_question = responses[-1]
        if not is_form_complete(first_question):
            if survey_keyword_action.use_named_args:
                # Arguments in the sms are named
                xpath_answer = parse_structured_sms_named_args(args,
                    survey_keyword_action, verified_number)
                _handle_structured_sms(domain, args, contact_id, session,
                    first_question, verified_number, xpath_answer)
            else:
                # Arguments in the sms are not named; pass each argument to
                # each question in order
Example #22
0
def initiate_outbound_call(call_log_entry, *args, **kwargs):
    phone_number = call_log_entry.phone_number
    if phone_number.startswith("+"):
        phone_number = phone_number[1:]

    if phone_number.startswith("91"):
        phone_number = "0" + phone_number[2:]
    else:
        call_log_entry.error = True
        call_log_entry.error_message = "Kookoo can only send to Indian phone numbers."
        call_log_entry.save()
        return False

    form = Form.get_form(call_log_entry.form_unique_id)
    app = form.get_app()
    module = form.get_module()

    # Only precache the first response if it's not an only-label form, otherwise we could end up
    # submitting the form regardless of whether the person actually answers the call.
    if form_requires_input(form):
        recipient = call_log_entry.recipient
        case_id = get_case_id(call_log_entry)
        session, responses = start_session(recipient.domain,
                                           recipient,
                                           app,
                                           module,
                                           form,
                                           case_id,
                                           yield_responses=True,
                                           session_type=XFORMS_SESSION_IVR)

        ivr_responses = []
        if len(responses) == 0:
            call_log_entry.error = True
            call_log_entry.error_message = "No prompts seen in form. Please check that the form does not have errors."
            call_log_entry.save()
            return False

        for response in responses:
            ivr_responses.append(
                format_ivr_response(response.event.caption, app))

        input_length = get_input_length(responses[-1])

        call_log_entry.use_precached_first_response = True
        call_log_entry.xforms_session_id = session.session_id

    url_base = get_url_base()

    params = urlencode({
        "phone_no":
        phone_number,
        "api_key":
        kwargs["api_key"],
        "outbound_version":
        "2",
        "url":
        url_base + reverse("corehq.apps.kookoo.views.ivr"),
        "callback_url":
        url_base + reverse("corehq.apps.kookoo.views.ivr_finished"),
    })
    url = "http://www.kookoo.in/outbound/outbound.php?%s" % params
    response = urlopen(url, timeout=settings.IVR_GATEWAY_TIMEOUT).read()

    root = XML(response)
    for child in root:
        if child.tag.endswith("status"):
            status = child.text
        elif child.tag.endswith("message"):
            message = child.text

    if status == "queued":
        call_log_entry.error = False
        call_log_entry.gateway_session_id = "KOOKOO-" + message
    elif status == "error":
        call_log_entry.error = True
        call_log_entry.error_message = message
    else:
        call_log_entry.error = True
        call_log_entry.error_message = "Unknown status received from Kookoo."

    if call_log_entry.error:
        call_log_entry.use_precached_first_response = False

    if call_log_entry.use_precached_first_response:
        call_log_entry.first_response = get_http_response_string(
            call_log_entry.gateway_session_id,
            ivr_responses,
            collect_input=True,
            hang_up=False,
            input_length=input_length)

    call_log_entry.save()
    return not call_log_entry.error
Example #23
0
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers):
    if handler.recipient in [RECIPIENT_CASE, RECIPIENT_SURVEY_SAMPLE]:
        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 = XFormsSession.view("smsforms/sessions_by_touchforms_id",
                                                    startkey=[session_id],
                                                    endkey=[session_id, {}],
                                                    include_docs=True).one()
                    if session.end_time is None:
                        vn = VerifiedNumber.view("sms/verified_number_by_owner_id",
                                                  key=session.connection_id,
                                                  include_docs=True).one()
                        if vn is not None:
                            resp = current_question(session_id)
                            send_sms_to_verified_number(vn, resp.event.text_prompt)
            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 as e:
                raise_error(reminder, ERROR_FORM)
                return False
            
            # Start a touchforms session for each recipient
            for recipient in recipients:
                verified_number = verified_numbers[recipient.get_id]
                if verified_number is None:
                    if len(recipients) == 1:
                        raise_error(reminder, ERROR_NO_VERIFIED_NUMBER)
                        return False
                    else:
                        raise_warning() # ERROR_NO_VERIFIED_NUMBER
                        continue
                
                # Close all currently open sessions
                close_open_sessions(reminder.domain, recipient.get_id)
                
                # Start the new session
                session, responses = start_session(reminder.domain, recipient, app, module, form, recipient.get_id)
                session.survey_incentive = handler.survey_incentive
                session.save()
                reminder.xforms_session_ids.append(session.session_id)
                
                # Send out first message
                if len(responses) > 0:
                    message = format_message_list(responses)
                    result = send_sms_to_verified_number(verified_number, message)
                    if not result:
                        raise_warning() # Could not send SMS
                    
                    if len(recipients) == 1:
                        return result
                
            return True
    else:
        # TODO: Make sure the above flow works for RECIPIENT_USER and RECIPIENT_OWNER
       return False
Example #24
0
def incoming(phone_number,
             backend_module,
             gateway_session_id,
             ivr_event,
             input_data=None):
    # Look up the call if one already exists
    call_log_entry = CallLog.view("sms/call_by_session",
                                  startkey=[gateway_session_id, {}],
                                  endkey=[gateway_session_id],
                                  descending=True,
                                  include_docs=True,
                                  limit=1).one()

    answer_is_valid = False  # This will be set to True if IVR validation passes
    error_occurred = False  # This will be set to False if touchforms validation passes (i.e., no form constraints fail)

    if call_log_entry is not None and backend_module:
        if ivr_event == IVR_EVENT_NEW_CALL and call_log_entry.use_precached_first_response:
            return HttpResponse(call_log_entry.first_response)

        form = Form.get_form(call_log_entry.form_unique_id)
        app = form.get_app()
        module = form.get_module()
        recipient = call_log_entry.recipient

        if ivr_event == IVR_EVENT_NEW_CALL:
            case_id = call_log_entry.case_id
            case_for_case_submission = call_log_entry.case_for_case_submission
            session, responses = start_session(
                recipient.domain,
                recipient,
                app,
                module,
                form,
                case_id,
                yield_responses=True,
                session_type=XFORMS_SESSION_IVR,
                case_for_case_submission=case_for_case_submission)
            call_log_entry.xforms_session_id = session.session_id
        elif ivr_event == IVR_EVENT_INPUT:
            if call_log_entry.xforms_session_id is not None:
                current_q = current_question(call_log_entry.xforms_session_id)
                if validate_answer(input_data, current_q):
                    answer_is_valid = True
                    responses = _get_responses(
                        recipient.domain,
                        recipient._id,
                        input_data,
                        yield_responses=True,
                        session_id=call_log_entry.xforms_session_id)
                else:
                    call_log_entry.current_question_retry_count += 1
                    responses = [current_q]
            else:
                responses = []
        else:
            responses = []

        ivr_responses = []
        hang_up = False
        for response in responses:
            if response.is_error:
                error_occurred = True
                call_log_entry.current_question_retry_count += 1
                if response.text_prompt is None:
                    ivr_responses = []
                    break
                else:
                    ivr_responses.append(
                        format_ivr_response(response.text_prompt, app))
            elif response.event.type == "question":
                ivr_responses.append(
                    format_ivr_response(response.event.caption, app))
            elif response.event.type == "form-complete":
                hang_up = True

        if answer_is_valid and not error_occurred:
            call_log_entry.current_question_retry_count = 0

        if call_log_entry.max_question_retries is not None and call_log_entry.current_question_retry_count > call_log_entry.max_question_retries:
            # Force hang-up
            ivr_responses = []

        if len(ivr_responses) == 0:
            hang_up = True

        input_length = None

        if hang_up:
            if call_log_entry.xforms_session_id is not None:
                # Process disconnect
                session = XFormsSession.latest_by_session_id(
                    call_log_entry.xforms_session_id)
                if session.end_time is None:
                    if call_log_entry.submit_partial_form:
                        submit_unfinished_form(
                            session.session_id,
                            call_log_entry.include_case_side_effects)
                    else:
                        session.end(completed=False)
                        session.save()
        else:
            # Set input_length to let the ivr gateway know how many digits we need to collect.
            # Have to get the current question again, since the last XFormsResponse in responses
            # may not have an event if it was a response to a constraint error.
            if error_occurred:
                current_q = current_question(call_log_entry.xforms_session_id)
            else:
                current_q = responses[-1]

            input_length = get_input_length(current_q)

        call_log_entry.save()
        return HttpResponse(
            backend_module.get_http_response_string(
                gateway_session_id,
                ivr_responses,
                collect_input=(not hang_up),
                hang_up=hang_up,
                input_length=input_length))

    # If not processed, just log the call

    if call_log_entry:
        # No need to log, already exists
        return HttpResponse("")

    cleaned_number = phone_number
    if cleaned_number is not None and len(
            cleaned_number) > 0 and cleaned_number[0] == "+":
        cleaned_number = cleaned_number[1:]

    # Try to look up the verified number entry
    v = VerifiedNumber.view("sms/verified_number_by_number",
                            key=cleaned_number,
                            include_docs=True).one()

    # If none was found, try to match only the last digits of numbers in the database
    if v is None:
        v = VerifiedNumber.view("sms/verified_number_by_suffix",
                                key=cleaned_number,
                                include_docs=True).one()

    # Save the call entry
    msg = CallLog(
        phone_number=cleaned_number,
        direction=INCOMING,
        date=datetime.utcnow(),
        backend_api=backend_module.API_ID if backend_module else None,
        gateway_session_id=gateway_session_id,
    )
    if v is not None:
        msg.domain = v.domain
        msg.couch_recipient_doc_type = v.owner_doc_type
        msg.couch_recipient = v.owner_id
    msg.save()

    return HttpResponse("")
Example #25
0
def handle_structured_sms(
    survey_keyword, survey_keyword_action, contact, verified_number, text, send_response=False, msg=None
):
    domain = contact.domain
    contact_doc_type = contact.doc_type
    contact_id = contact._id

    text = text.strip()
    if survey_keyword.delimiter is not None:
        args = text.split(survey_keyword.delimiter)
    else:
        args = text.split()

    keyword = args[0].strip().upper()

    error_occurred = False
    error_msg = None
    session = None

    try:
        # Start the session
        form = Form.get_form(survey_keyword_action.form_unique_id)
        app = form.get_app()
        module = form.get_module()

        if contact_doc_type == "CommCareCase":
            case_id = contact_id
        else:
            # TODO: Need a way to choose the case when it's a user that's playing the form
            case_id = None

        session, responses = start_session(domain, contact, app, module, form, case_id=case_id, yield_responses=True)
        session.workflow = WORKFLOW_KEYWORD
        session.save()
        if msg is not None:
            msg.workflow = WORKFLOW_KEYWORD
            msg.xforms_session_couch_id = session._id
            msg.save()
        assert len(responses) > 0, "There should be at least one response."

        current_question = responses[-1]
        form_complete = is_form_complete(current_question)

        if not form_complete:
            if survey_keyword_action.use_named_args:
                # Arguments in the sms are named
                xpath_answer = {}  # Dictionary of {xpath : answer}
                for answer in args[1:]:
                    answer = answer.strip()
                    answer_upper = answer.upper()
                    if survey_keyword_action.named_args_separator is not None:
                        # A separator is used for naming arguments; for example, the "=" in "register name=joe age=25"
                        answer_parts = answer.partition(survey_keyword_action.named_args_separator)
                        if answer_parts[1] != survey_keyword_action.named_args_separator:
                            raise StructuredSMSException(
                                response_text="ERROR: Expected name and value to be joined by '%(separator)s'"
                                % {"separator": survey_keyword_action.named_args_separator}
                            )
                        else:
                            arg_name = answer_parts[0].upper().strip()
                            xpath = survey_keyword_action.named_args.get(arg_name, None)
                            if xpath is not None:
                                if xpath in xpath_answer:
                                    raise StructuredSMSException(
                                        response_text="ERROR: More than one answer found for '%(arg_name)s'"
                                        % {"arg_name": arg_name}
                                    )

                                xpath_answer[xpath] = answer_parts[2].strip()
                            else:
                                # Ignore unexpected named arguments
                                pass
                    else:
                        # No separator is used for naming arguments; for example, "update a100 b34 c5"
                        matches = 0
                        for k, v in survey_keyword_action.named_args.items():
                            if answer_upper.startswith(k):
                                matches += 1
                                if matches > 1:
                                    raise StructuredSMSException(
                                        response_text="ERROR: More than one question matches '%(answer)s'"
                                        % {"answer": answer}
                                    )

                                if v in xpath_answer:
                                    raise StructuredSMSException(
                                        response_text="ERROR: More than one answer found for '%(named_arg)s'"
                                        % {"named_arg": k}
                                    )

                                xpath_answer[v] = answer[len(k) :].strip()

                        if matches == 0:
                            # Ignore unexpected named arguments
                            pass

                # Go through each question in the form, answering only the questions that the sms has answers for
                while not form_complete:
                    if current_question.is_error:
                        raise StructuredSMSException(
                            response_text=(current_question.text_prompt or "ERROR: Internal server error")
                        )

                    xpath = current_question.event._dict["binding"]
                    if xpath in xpath_answer:
                        valid, answer, _error_msg = validate_answer(current_question.event, xpath_answer[xpath])
                        if not valid:
                            raise StructuredSMSException(response_text=_error_msg)
                        responses = _get_responses(
                            domain,
                            contact_id,
                            answer,
                            yield_responses=True,
                            session_id=session.session_id,
                            update_timestamp=False,
                        )
                    else:
                        responses = _get_responses(
                            domain,
                            contact_id,
                            "",
                            yield_responses=True,
                            session_id=session.session_id,
                            update_timestamp=False,
                        )

                    current_question = responses[-1]
                    if is_form_complete(current_question):
                        form_complete = True
            else:
                # Arguments in the sms are not named; pass each argument to each question in order
                for answer in args[1:]:
                    if form_complete:
                        # Form is complete, ignore remaining answers
                        break

                    if current_question.is_error:
                        raise StructuredSMSException(
                            response_text=(current_question.text_prompt or "ERROR: Internal server error")
                        )

                    valid, answer, _error_msg = validate_answer(current_question.event, answer.strip())
                    if not valid:
                        raise StructuredSMSException(response_text=_error_msg)

                    responses = _get_responses(
                        domain,
                        contact_id,
                        answer,
                        yield_responses=True,
                        session_id=session.session_id,
                        update_timestamp=False,
                    )
                    current_question = responses[-1]
                    form_complete = is_form_complete(current_question)

                # If the form isn't finished yet but we're out of arguments, try to leave each remaining question blank and continue
                while not form_complete:
                    responses = _get_responses(
                        domain,
                        contact_id,
                        "",
                        yield_responses=True,
                        session_id=session.session_id,
                        update_timestamp=False,
                    )
                    current_question = responses[-1]

                    if current_question.is_error:
                        raise StructuredSMSException(
                            response_text=(current_question.text_prompt or "ERROR: Internal server error")
                        )

                    if is_form_complete(current_question):
                        form_complete = True
    except StructuredSMSException as sse:
        error_occurred = True
        error_msg = sse.response_text
    except Exception:
        logging.exception(
            "Could not process structured sms for contact %s, domain %s, keyword %s" % (contact_id, domain, keyword)
        )
        error_occurred = True
        error_msg = "ERROR: Internal server error"

    if session is not None:
        session = XFormsSession.get(session._id)
        if session.is_open:
            session.end(False)
            session.save()

    message_tags = {
        "workflow": WORKFLOW_KEYWORD,
        "xforms_session_couch_id": session._id if session is not None else None,
    }

    if msg is not None:
        msg.workflow = message_tags["workflow"]
        msg.xforms_session_couch_id = message_tags["xforms_session_couch_id"]
        msg.save()

    if error_occurred and verified_number is not None and send_response:
        send_sms_to_verified_number(verified_number, error_msg, **message_tags)
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 = XFormsSession.view("smsforms/sessions_by_touchforms_id",
                                             startkey=[session_id],
                                             endkey=[session_id, {}],
                                             include_docs=True).one()
                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 as e:
            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

            # Close all currently open sessions
            XFormsSession.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
Example #27
0
def initiate_outbound_call(call_log_entry, *args, **kwargs):
    phone_number = call_log_entry.phone_number
    if phone_number.startswith("+"):
        phone_number = phone_number[1:]
    
    if phone_number.startswith("91"):
        phone_number = "0" + phone_number[2:]
    else:
        raise InvalidPhoneNumberException("Kookoo can only send to Indian phone numbers.")
    
    form = Form.get_form(call_log_entry.form_unique_id)
    app = form.get_app()
    module = form.get_module()
    
    # Only precache the first response if it's not an only-label form, otherwise we could end up
    # submitting the form regardless of whether the person actually answers the call.
    if form_requires_input(form):
        recipient = call_log_entry.recipient
        case_id = get_case_id(call_log_entry)
        session, responses = start_session(recipient.domain, recipient, app, module, form, case_id, yield_responses=True, session_type=XFORMS_SESSION_IVR)
        
        ivr_responses = []
        if len(ivr_responses) == 0:
            call_log_entry.error = True
            call_log_entry.error_message = "No prompts seen in form. Please check that the form does not have errors."
            call_log_entry.save()
            return False

        for response in responses:
            ivr_responses.append(format_ivr_response(response.event.caption, app))
        
        input_length = get_input_length(responses[-1])
        
        call_log_entry.use_precached_first_response = True
        call_log_entry.xforms_session_id = session.session_id
    
    url_base = get_url_base()
    
    params = urlencode({
        "phone_no" : phone_number,
        "api_key" : kwargs["api_key"],
        "outbound_version" : "2",
        "url" : url_base + reverse("corehq.apps.kookoo.views.ivr"),
        "callback_url" : url_base + reverse("corehq.apps.kookoo.views.ivr_finished"),
    })
    url = "http://www.kookoo.in/outbound/outbound.php?%s" % params
    response = urlopen(url).read()
    
    root = XML(response)
    for child in root:
        if child.tag.endswith("status"):
            status = child.text
        elif child.tag.endswith("message"):
            message = child.text
    
    if status == "queued":
        call_log_entry.error = False
        call_log_entry.gateway_session_id = "KOOKOO-" + message
    elif status == "error":
        call_log_entry.error = True
        call_log_entry.error_message = message
    else:
        call_log_entry.error = True
        call_log_entry.error_message = "Unknown status received from Kookoo."
    
    if call_log_entry.error:
        call_log_entry.use_precached_first_response = False
    
    if call_log_entry.use_precached_first_response:
        call_log_entry.first_response = get_http_response_string(call_log_entry.gateway_session_id, ivr_responses, collect_input=True, hang_up=False, input_length=input_length)
    
    call_log_entry.save()
    return not call_log_entry.error
Example #28
0
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers):
    if handler.recipient in [RECIPIENT_CASE, RECIPIENT_SURVEY_SAMPLE]:
        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 = XFormsSession.view(
                        "smsforms/sessions_by_touchforms_id",
                        startkey=[session_id],
                        endkey=[session_id, {}],
                        include_docs=True).one()
                    if session.end_time is None:
                        vn = VerifiedNumber.view(
                            "sms/verified_number_by_owner_id",
                            key=session.connection_id,
                            include_docs=True).one()
                        if vn is not None:
                            resp = current_question(session_id)
                            send_sms_to_verified_number(
                                vn, resp.event.text_prompt)
            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 as e:
                raise_error(reminder, ERROR_FORM)
                return False

            # Start a touchforms session for each recipient
            for recipient in recipients:
                verified_number = verified_numbers[recipient.get_id]
                if verified_number is None:
                    if len(recipients) == 1:
                        raise_error(reminder, ERROR_NO_VERIFIED_NUMBER)
                        return False
                    else:
                        raise_warning()  # ERROR_NO_VERIFIED_NUMBER
                        continue

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

                # Start the new session
                session, responses = start_session(reminder.domain, recipient,
                                                   app, module, form,
                                                   recipient.get_id)
                session.survey_incentive = handler.survey_incentive
                session.save()
                reminder.xforms_session_ids.append(session.session_id)

                # Send out first message
                if len(responses) > 0:
                    message = format_message_list(responses)
                    result = send_sms_to_verified_number(
                        verified_number, message)
                    if not result:
                        raise_warning()  # Could not send SMS

                    if len(recipients) == 1:
                        return result

            return True
    else:
        # TODO: Make sure the above flow works for RECIPIENT_USER and RECIPIENT_OWNER
        return False
Example #29
0
def initiate_outbound_call(call_log_entry, *args, **kwargs):
    phone_number = call_log_entry.phone_number
    if phone_number.startswith("+"):
        phone_number = phone_number[1:]

    if phone_number.startswith("91"):
        phone_number = "0" + phone_number[2:]
    else:
        call_log_entry.error = True
        call_log_entry.error_message = "Kookoo can only send to Indian phone numbers."
        call_log_entry.save()
        return False

    form = Form.get_form(call_log_entry.form_unique_id)
    app = form.get_app()
    module = form.get_module()

    # Only precache the first response if it's not an only-label form, otherwise we could end up
    # submitting the form regardless of whether the person actually answers the call.
    if form_requires_input(form):
        recipient = call_log_entry.recipient
        case_id = call_log_entry.case_id
        case_for_case_submission = call_log_entry.case_for_case_submission
        session, responses = start_session(recipient.domain, recipient, app,
            module, form, case_id, yield_responses=True,
            session_type=XFORMS_SESSION_IVR,
            case_for_case_submission=case_for_case_submission)

        ivr_responses = []
        if len(responses) == 0:
            call_log_entry.error = True
            call_log_entry.error_message = "No prompts seen in form. Please check that the form does not have errors."
            call_log_entry.save()
            return False

        for response in responses:
            ivr_responses.append(format_ivr_response(response.event.caption, app))

        input_length = get_input_length(responses[-1])

        call_log_entry.use_precached_first_response = True
        call_log_entry.xforms_session_id = session.session_id

    url_base = get_url_base()

    params = urlencode({
        "phone_no" : phone_number,
        "api_key" : kwargs["api_key"],
        "outbound_version" : "2",
        "url" : url_base + reverse("corehq.apps.kookoo.views.ivr"),
        "callback_url" : url_base + reverse("corehq.apps.kookoo.views.ivr_finished"),
    })
    url = "http://www.kookoo.in/outbound/outbound.php?%s" % params
    if kwargs.get("is_test", False):
        session_id = hashlib.sha224(datetime.utcnow().isoformat()).hexdigest()
        response = "<request><status>queued</status><message>%s</message></request>" % session_id
    else:
        response = urlopen(url, timeout=settings.IVR_GATEWAY_TIMEOUT).read()

    root = XML(response)
    for child in root:
        if child.tag.endswith("status"):
            status = child.text
        elif child.tag.endswith("message"):
            message = child.text

    do_not_retry = False
    if status == "queued":
        call_log_entry.error = False
        call_log_entry.gateway_session_id = "KOOKOO-" + message
    elif status == "error":
        call_log_entry.error = True
        call_log_entry.error_message = message
        if (message.strip().upper() in [
            'CALLS WILL NOT BE MADE BETWEEN 9PM TO 9AM.',
            'PHONE NUMBER IN DND LIST',
        ]):
            do_not_retry = True
    else:
        call_log_entry.error = True
        call_log_entry.error_message = "Unknown status received from Kookoo."

    if call_log_entry.error:
        call_log_entry.use_precached_first_response = False

    if call_log_entry.use_precached_first_response:
        call_log_entry.first_response = get_http_response_string(call_log_entry.gateway_session_id, ivr_responses, collect_input=True, hang_up=False, input_length=input_length)

    call_log_entry.save()
    return not call_log_entry.error or do_not_retry
Example #30
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()
Example #31
0
def handle_structured_sms(survey_keyword, survey_keyword_action, contact,
    verified_number, text, send_response=False, msg=None, case=None,
    text_args=None):

    domain = contact.domain
    contact_doc_type = contact.doc_type
    contact_id = contact._id

    if text_args is not None:
        args = text_args
    else:
        args = split_args(text, survey_keyword)
        args = args[1:]
    keyword = survey_keyword.keyword.upper()

    error_occurred = False
    error_msg = None
    session = None

    try:
        # Start the session
        app, module, form = get_form(
            survey_keyword_action.form_unique_id, include_app_module=True)

        if case:
            case_id = case._id
        elif contact_doc_type == "CommCareCase":
            case_id = contact_id
        else:
            case_id = None

        session, responses = start_session(domain, contact, app, module,
            form, case_id=case_id, yield_responses=True)
        session.workflow = WORKFLOW_KEYWORD
        session.save()
        assert len(responses) > 0, "There should be at least one response."

        first_question = responses[-1]
        if not is_form_complete(first_question):
            if survey_keyword_action.use_named_args:
                # Arguments in the sms are named
                xpath_answer = parse_structured_sms_named_args(args,
                    survey_keyword_action, verified_number)
                _handle_structured_sms(domain, args, contact_id, session,
                    first_question, verified_number, xpath_answer)
            else:
                # Arguments in the sms are not named; pass each argument to
                # each question in order
                _handle_structured_sms(domain, args, contact_id, session,
                    first_question, verified_number)

    except StructuredSMSException as sse:
        error_occurred = True
        error_msg = ""
        if sse.xformsresponse and sse.xformsresponse.event:
            xpath_arg = None
            if survey_keyword_action.use_named_args:
                xpath_arg = \
                    {v: k for k, v in survey_keyword_action.named_args.items()}
            field_name = get_question_id(sse.xformsresponse, xpath_arg)
            error_msg = get_message(MSG_FIELD_DESCRIPTOR, verified_number,
                (field_name,))
        error_msg = "%s%s" % (error_msg, sse.response_text)
    except Exception:
        notify_exception(None, message=("Could not process structured sms for"
            "contact %s, domain %s, keyword %s" % (contact_id, domain, keyword)))
        error_occurred = True
        error_msg = get_message(MSG_TOUCHFORMS_ERROR, verified_number)

    if session is not None:
        session = SQLXFormsSession.objects.get(couch_id=session._id)
        if session.is_open:
            session.end(False)
            session.save()

    metadata = MessageMetadata(
        workflow=WORKFLOW_KEYWORD,
        xforms_session_couch_id=session._id if session else None,
    )

    if msg:
        add_msg_tags(msg, metadata)

    if error_occurred and verified_number is not None and send_response:
        send_sms_to_verified_number(verified_number, error_msg, metadata=metadata)

    return not error_occurred
Example #32
0
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers,
                          logged_event):
    current_event = reminder.current_event
    if reminder.callback_try_count > 0:
        # Leaving this as an explicit reminder that all survey related actions now happen
        # in a different process. Eventually all of this code will be removed when we move
        # to the new reminders framework.
        pass
    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 = 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 form_requires_input(form))
            if no_verified_number and cant_use_unverified_number:
                logged_subevent.error(
                    MessagingEvent.ERROR_NO_TWO_WAY_PHONE_NUMBER)
                continue

            if verified_number:
                pb = PhoneBlacklist.get_by_phone_number_or_none(
                    verified_number.phone_number)
            else:
                pb = PhoneBlacklist.get_by_phone_number_or_none(
                    unverified_number)

            if pb and not pb.send_sms:
                logged_subevent.error(MessagingEvent.ERROR_PHONE_OPTED_OUT)
                continue

            with critical_section_for_smsforms_sessions(recipient.get_id):
                # Get the case to submit the form against, if any
                if (is_commcarecase(recipient)
                        and not handler.force_surveys_to_use_triggered_case):
                    case_id = recipient.case_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:
                    if current_event.callback_timeout_intervals:
                        if handler.submit_partial_forms:
                            expire_after = sum(
                                current_event.callback_timeout_intervals)
                            reminder_intervals = current_event.callback_timeout_intervals[:
                                                                                          -1]
                        else:
                            expire_after = SQLXFormsSession.MAX_SESSION_LENGTH
                            reminder_intervals = current_event.callback_timeout_intervals

                        submit_partially_completed_forms = handler.submit_partial_forms
                        include_case_updates_in_partial_submissions = handler.include_case_side_effects
                    else:
                        expire_after = SQLXFormsSession.MAX_SESSION_LENGTH
                        reminder_intervals = []
                        submit_partially_completed_forms = False
                        include_case_updates_in_partial_submissions = False

                    session, responses = start_session(
                        SQLXFormsSession.create_session_object(
                            reminder.domain,
                            recipient,
                            verified_number.phone_number
                            if verified_number else unverified_number,
                            app,
                            form,
                            expire_after=expire_after,
                            reminder_intervals=reminder_intervals,
                            submit_partially_completed_forms=
                            submit_partially_completed_forms,
                            include_case_updates_in_partial_submissions=
                            include_case_updates_in_partial_submissions),
                        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 = get_formplayer_exception(
                        reminder.domain, e)

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

                    if touchforms_error_is_config_error(reminder.domain, 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,
                        logged_subevent=logged_subevent)
                else:
                    send_sms(reminder.domain, recipient, unverified_number,
                             message, metadata)

            logged_subevent.completed()
Example #33
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 = XFormsSession.view(
                    "smsforms/sessions_by_touchforms_id",
                    startkey=[session_id],
                    endkey=[session_id, {}],
                    include_docs=True).one()
                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 as e:
            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

            # Close all currently open sessions
            XFormsSession.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