Ejemplo n.º 1
0
def answer_next_question(v, text, msg, session):
    resp = current_question(session.session_id)
    event = resp.event
    valid, text, error_msg = validate_answer(event, text, v)

    # metadata to be applied to the reply message
    outbound_metadata = MessageMetadata(
        workflow=session.workflow,
        reminder_id=session.reminder_id,
        xforms_session_couch_id=session._id,
    )

    if valid:
        responses = _get_responses(v.domain, v.owner_id, text,
            yield_responses=True)

        if has_invalid_response(responses):
            mark_as_invalid_response(msg)

        text_responses = _responses_to_text(responses)
        if len(text_responses) > 0:
            response_text = format_message_list(text_responses)
            send_sms_to_verified_number(v, response_text, 
                metadata=outbound_metadata)
    else:
        mark_as_invalid_response(msg)
        response_text = "%s %s" % (error_msg, event.text_prompt)
        send_sms_to_verified_number(v, response_text, 
            metadata=outbound_metadata)
Ejemplo n.º 2
0
def answer_next_question(v, text, msg, session):
    resp = current_question(session.session_id)
    event = resp.event
    valid, text, error_msg = validate_answer(event, text, v)

    # metadata to be applied to the reply message
    outbound_metadata = MessageMetadata(
        workflow=session.workflow,
        reminder_id=session.reminder_id,
        xforms_session_couch_id=session._id,
    )

    if valid:
        responses = _get_responses(v.domain, v.owner_id, text,
            yield_responses=True)

        if has_invalid_response(responses):
            mark_as_invalid_response(msg)

        text_responses = _responses_to_text(responses)
        if len(text_responses) > 0:
            response_text = format_message_list(text_responses)
            send_sms_to_verified_number(v, response_text, 
                metadata=outbound_metadata)
    else:
        mark_as_invalid_response(msg)
        response_text = "%s %s" % (error_msg, event.text_prompt)
        send_sms_to_verified_number(v, response_text, 
            metadata=outbound_metadata)
Ejemplo n.º 3
0
def _handle_structured_sms(domain,
                           args,
                           contact_id,
                           session_id,
                           first_question,
                           verified_number,
                           xpath_answer=None):

    form_complete = False
    current_question = first_question
    internal_error_msg = get_message(MSG_TOUCHFORMS_DOWN, verified_number)

    used_named_args = xpath_answer is not None
    answer_num = 0
    while not form_complete:
        if current_question.is_error:
            error_msg = current_question.text_prompt or internal_error_msg
            raise StructuredSMSException(response_text=error_msg,
                                         xformsresponse=current_question)

        xpath = current_question.event._dict["binding"]
        if used_named_args and xpath in xpath_answer:
            valid, answer, error_msg = validate_answer(current_question.event,
                                                       xpath_answer[xpath],
                                                       verified_number)
            if not valid:
                raise StructuredSMSException(response_text=error_msg,
                                             xformsresponse=current_question)
        elif not used_named_args and answer_num < len(args):
            answer = args[answer_num].strip()
            valid, answer, error_msg = validate_answer(current_question.event,
                                                       answer, verified_number)
            if not valid:
                raise StructuredSMSException(response_text=error_msg,
                                             xformsresponse=current_question)
        else:
            # We're out of arguments, so try to leave each remaining question
            # blank and continue
            answer = ""
            if current_question.event._dict.get("required", False):
                error_msg = get_message(MSG_FIELD_REQUIRED, verified_number)
                raise StructuredSMSException(response_text=error_msg,
                                             xformsresponse=current_question)

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

        form_complete = is_form_complete(current_question)
        answer_num += 1
Ejemplo n.º 4
0
def form_session_handler(v, text, msg=None):
    """
    The form session handler will use the inbound text to answer the next question
    in the open XformsSession for the associated contact. If no session is open,
    the handler passes. If multiple sessions are open, they are all closed and an
    error message is displayed to the user.
    """
    sessions = XFormsSession.get_all_open_sms_sessions(v.domain, v.owner_id)
    if len(sessions) > 1:
        # If there are multiple sessions, there's no way for us to know which one this message
        # belongs to. So we should inform the user that there was an error and to try to restart
        # the survey.
        for session in sessions:
            session.end(False)
            session.save()
        send_sms_to_verified_number(v, "An error has occurred. Please try restarting the survey.")
        return True

    session = sessions[0] if len(sessions) == 1 else None

    if session is not None:
        if msg is not None:
            msg.workflow = session.workflow
            msg.reminder_id = session.reminder_id
            msg.xforms_session_couch_id = session._id
            msg.save()

        # If there's an open session, treat the inbound text as the answer to the next question
        try:
            resp = current_question(session.session_id)
            event = resp.event
            valid, text, error_msg = validate_answer(event, text)

            if valid:
                responses = _get_responses(v.domain, v.owner_id, text, yield_responses=True)
                if has_invalid_response(responses):
                    if msg:
                        mark_as_invalid_response(msg)
                text_responses = _responses_to_text(responses)
                if len(text_responses) > 0:
                    response_text = format_message_list(text_responses)
                    send_sms_to_verified_number(v, response_text, workflow=session.workflow, reminder_id=session.reminder_id, xforms_session_couch_id=session._id)
            else:
                if msg:
                    mark_as_invalid_response(msg)
                send_sms_to_verified_number(v, error_msg + event.text_prompt, workflow=session.workflow, reminder_id=session.reminder_id, xforms_session_couch_id=session._id)
        except Exception:
            # Catch any touchforms errors
            msg_id = msg._id if msg is not None else ""
            logging.exception("Exception in form_session_handler for message id %s." % msg_id)
            send_sms_to_verified_number(v, "An error has occurred. Please try again later. If the problem persists, try restarting the survey.")
        return True
    else:
        return False
Ejemplo n.º 5
0
def _handle_structured_sms(domain, args, contact_id, session_id,
    first_question, verified_number, xpath_answer=None):

    form_complete = False
    current_question = first_question
    internal_error_msg = get_message(MSG_TOUCHFORMS_DOWN, verified_number)

    used_named_args = xpath_answer is not None
    answer_num = 0
    while not form_complete:
        if current_question.is_error:
            error_msg = current_question.text_prompt or internal_error_msg
            raise StructuredSMSException(response_text=error_msg,
                xformsresponse=current_question)

        xpath = current_question.event._dict["binding"]
        if used_named_args and xpath in xpath_answer:
            valid, answer, error_msg = validate_answer(current_question.event,
                xpath_answer[xpath], verified_number)
            if not valid:
                raise StructuredSMSException(response_text=error_msg,
                    xformsresponse=current_question)
        elif not used_named_args and answer_num < len(args):
            answer = args[answer_num].strip()
            valid, answer, error_msg = validate_answer(current_question.event,
                answer, verified_number)
            if not valid:
                raise StructuredSMSException(response_text=error_msg,
                    xformsresponse=current_question)
        else:
            # We're out of arguments, so try to leave each remaining question
            # blank and continue
            answer = ""
            if current_question.event._dict.get("required", False):
                error_msg = get_message(MSG_FIELD_REQUIRED,
                    verified_number)
                raise StructuredSMSException(response_text=error_msg,
                    xformsresponse=current_question)

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

        form_complete = is_form_complete(current_question)
        answer_num += 1
Ejemplo n.º 6
0
def answer_question(call_log_entry,
                    recipient,
                    input_data,
                    logged_subevent=None):
    """
    Returns a list of (responses, answer_is_valid), where responses is the
    list of XFormsResponse objects from touchforms and answer_is_valid is
    True if input_data passes validation and False if not.

    Returning an empty list for responses will end up forcing a hangup
    later on in the workflow.
    """
    if call_log_entry.xforms_session_id is None:
        return ([], None)

    try:
        current_q = current_question(call_log_entry.xforms_session_id)
    except TouchformsError as e:
        log_touchforms_error(e, call_log_entry, logged_subevent)
        return ([], None)

    if current_q.status == 'http-error':
        log_error(MessagingEvent.ERROR_TOUCHFORMS_ERROR, call_log_entry,
                  logged_subevent)
        return ([], None)

    if validate_answer(input_data, current_q):
        answer_is_valid = True
        try:
            responses = _get_responses(
                recipient.domain,
                recipient._id,
                input_data,
                yield_responses=True,
                session_id=call_log_entry.xforms_session_id)
        except TouchformsError as e:
            log_touchforms_error(e, call_log_entry, logged_subevent)
            return ([], None)
    else:
        answer_is_valid = False
        call_log_entry.current_question_retry_count += 1
        responses = [current_q]

    return (responses, answer_is_valid)
Ejemplo n.º 7
0
def answer_question(call_log_entry, recipient, input_data, logged_subevent=None):
    """
    Returns a list of (responses, answer_is_valid), where responses is the
    list of XFormsResponse objects from touchforms and answer_is_valid is
    True if input_data passes validation and False if not.

    Returning an empty list for responses will end up forcing a hangup
    later on in the workflow.
    """
    if call_log_entry.xforms_session_id is None:
        return ([], None)

    try:
        current_q = current_question(call_log_entry.xforms_session_id)
    except TouchformsError as e:
        log_touchforms_error(e, call_log_entry, logged_subevent)
        return ([], None)

    if current_q.status == "http-error":
        log_error(MessagingEvent.ERROR_TOUCHFORMS_ERROR, call_log_entry, logged_subevent)
        return ([], None)

    if validate_answer(input_data, current_q):
        answer_is_valid = True
        try:
            responses = _get_responses(
                recipient.domain,
                recipient._id,
                input_data,
                yield_responses=True,
                session_id=call_log_entry.xforms_session_id,
            )
        except TouchformsError as e:
            log_touchforms_error(e, call_log_entry, logged_subevent)
            return ([], None)
    else:
        answer_is_valid = False
        call_log_entry.current_question_retry_count += 1
        responses = [current_q]

    return (responses, answer_is_valid)
Ejemplo n.º 8
0
def form_session_handler(v, text):
    # Circular Import
    from corehq.apps.reminders.models import SurveyKeyword, FORM_TYPE_ONE_BY_ONE

    # Handle incoming sms
    session = XFormsSession.view(
        "smsforms/open_sms_sessions_by_connection", key=[v.domain, v.owner_id], include_docs=True
    ).one()

    text_words = text.upper().split()

    # Respond to "#START <keyword>" command
    if len(text_words) > 0 and text_words[0] == "#START":
        if len(text_words) > 1:
            sk = SurveyKeyword.get_keyword(v.domain, text_words[1])
            if sk is not None and sk.form_type == FORM_TYPE_ONE_BY_ONE:
                if session is not None:
                    session.end(False)
                    session.save()
                start_session_from_keyword(sk, v)
            else:
                send_sms_to_verified_number(v, "Survey '" + text_words[1] + "' not found.")
        else:
            send_sms_to_verified_number(v, "Usage: #START <keyword>")

    # Respond to "#STOP" keyword
    elif len(text_words) > 0 and text_words[0] == "#STOP":
        if session is not None:
            session.end(False)
            session.save()

    # Respond to "#CURRENT" keyword
    elif len(text_words) > 0 and text_words[0] == "#CURRENT":
        if session is not None:
            resp = current_question(session.session_id)
            send_sms_to_verified_number(v, resp.event.text_prompt)

    # Respond to unknown command
    elif len(text_words) > 0 and text_words[0][0] == "#":
        send_sms_to_verified_number(v, "Unknown command '" + text_words[0] + "'")

    # If there's an open session, treat the inbound text as the answer to the next question
    elif session is not None:
        resp = current_question(session.session_id)
        event = resp.event
        valid, text, error_msg = validate_answer(event, text)

        if valid:
            responses = _get_responses(v.domain, v.owner_id, text)
            if len(responses) > 0:
                response_text = format_message_list(responses)
                send_sms_to_verified_number(v, response_text)
        else:
            send_sms_to_verified_number(v, error_msg + event.text_prompt)

    # Try to match the text against a keyword to start a survey
    elif len(text_words) > 0:
        sk = SurveyKeyword.get_keyword(v.domain, text_words[0])
        if sk is not None and sk.form_type == FORM_TYPE_ONE_BY_ONE:
            start_session_from_keyword(sk, v)

    # TODO should clarify what scenarios this handler actually handles. i.e.,
    # should the error responses instead be handler by some generic error/fallback
    # handler
    return True
Ejemplo n.º 9
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
Ejemplo n.º 10
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("")
Ejemplo n.º 11
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("")
Ejemplo n.º 12
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)
Ejemplo n.º 13
0
def form_session_handler(v, text):
    # Circular Import
    from corehq.apps.reminders.models import SurveyKeyword
    
    # Handle incoming sms
    session = XFormsSession.view("smsforms/open_sms_sessions_by_connection",
                                 key=[v.domain, v.owner_id],
                                 include_docs=True).one()
    
    text_words = text.upper().split()
    
    # Respond to "#START <keyword>" command
    if len(text_words) > 0 and text_words[0] == "#START":
        if len(text_words) > 1:
            sk = SurveyKeyword.get_keyword(v.domain, text_words[1])
            if sk is not None:
                if session is not None:
                    session.end(False)
                    session.save()
                start_session_from_keyword(sk, v)
            else:
                send_sms_to_verified_number(v, "Survey '" + text_words[1] + "' not found.")
        else:
            send_sms_to_verified_number(v, "Usage: #START <keyword>")
        
    # Respond to "#STOP" keyword
    elif len(text_words) > 0 and text_words[0] == "#STOP":
        if session is not None:
            session.end(False)
            session.save()
        
    # Respond to "#CURRENT" keyword
    elif len(text_words) > 0 and text_words[0] == "#CURRENT":
        if session is not None:
            resp = current_question(session.session_id)
            send_sms_to_verified_number(v, resp.event.text_prompt)
        
    # Respond to unknown command
    elif len(text_words) > 0 and text_words[0][0] == "#":
        send_sms_to_verified_number(v, "Unknown command '" + text_words[0] + "'")
        
    # If there's an open session, treat the inbound text as the answer to the next question
    elif session is not None:
        resp = current_question(session.session_id)
        event = resp.event
        valid = False
        text = text.strip()
        upper_text = text.upper()
        
        # Validate select
        if event.datatype == "select":
            # Try to match on phrase (i.e., "Yes" or "No")
            choices = format_choices(event._dict["choices"])
            if upper_text in choices:
                text = str(choices[upper_text])
                valid = True
            else:
                try:
                    answer = int(text)
                    if answer >= 1 and answer <= len(event._dict["choices"]):
                        valid = True
                except ValueError:
                    pass
        
        # Validate multiselect
        elif event.datatype == "multiselect":
            choices = format_choices(event._dict["choices"])
            max_index = len(event._dict["choices"])
            proposed_answers = text.split()
            final_answers = {}
            
            try:
                if event._dict.get("required", True):
                    assert len(proposed_answers) > 0
                for answer in proposed_answers:
                    upper_answer = answer.upper()
                    if upper_answer in choices:
                        final_answers[str(choices[upper_answer])] = ""
                    else:
                        int_answer = int(answer)
                        assert int_answer >= 1 and int_answer <= max_index
                        final_answers[str(int_answer)] = ""
                text = " ".join(final_answers.keys())
                valid = True
            except Exception:
                pass
        
        # Validate int
        elif event.datatype == "int":
            try:
                int(text)
                valid = True
            except ValueError:
                pass
        
        # Validate float
        elif event.datatype == "float":
            try:
                float(text)
                valid = True
            except ValueError:
                pass
        
        # Validate longint
        elif event.datatype == "longint":
            try:
                long(text)
                valid = True
            except ValueError:
                pass
        
        # Validate date (Format: YYYYMMDD)
        elif event.datatype == "date":
            try:
                assert len(text) == 8
                int(text)
                text = text[0:4] + "-" + text[4:6] + "-" + text[6:]
                parse(text)
                valid = True
            except Exception:
                pass
        
        # Validate time (Format: HHMM, 24-hour)
        elif event.datatype == "time":
            try:
                assert len(text) == 4
                hour = int(text[0:2])
                minute = int(text[2:])
                assert hour >= 0 and hour <= 23
                assert minute >= 0 and minute <= 59
                text = "%s:%s" % (hour, minute)
                valid = True
            except Exception:
                pass
        
        # Other question types pass
        else:
            valid = True
        
        if valid:
            responses = _get_responses(v.domain, v.owner_id, text)
            if len(responses) > 0:
                response_text = format_message_list(responses)
                send_sms_to_verified_number(v, response_text)
        else:
            error_msg = "Invalid Response. " + event.text_prompt
            send_sms_to_verified_number(v, error_msg)
        
    # Try to match the text against a keyword to start a survey
    elif len(text_words) > 0:
        sk = SurveyKeyword.get_keyword(v.domain, text_words[0])
        if sk is not None:
            start_session_from_keyword(sk, v)

    # TODO should clarify what scenarios this handler actually handles. i.e.,
    # should the error responses instead be handler by some generic error/fallback
    # handler
    return True
Ejemplo n.º 14
0
def form_session_handler(v, text):
    # Circular Import
    from corehq.apps.reminders.models import SurveyKeyword, FORM_TYPE_ONE_BY_ONE

    # Handle incoming sms
    session = XFormsSession.view("smsforms/open_sms_sessions_by_connection",
                                 key=[v.domain, v.owner_id],
                                 include_docs=True).one()

    text_words = text.upper().split()

    # Respond to "#START <keyword>" command
    if len(text_words) > 0 and text_words[0] == "#START":
        if len(text_words) > 1:
            sk = SurveyKeyword.get_keyword(v.domain, text_words[1])
            if sk is not None and sk.form_type == FORM_TYPE_ONE_BY_ONE:
                if session is not None:
                    session.end(False)
                    session.save()
                start_session_from_keyword(sk, v)
            else:
                send_sms_to_verified_number(
                    v, "Survey '" + text_words[1] + "' not found.")
        else:
            send_sms_to_verified_number(v, "Usage: #START <keyword>")

    # Respond to "#STOP" keyword
    elif len(text_words) > 0 and text_words[0] == "#STOP":
        if session is not None:
            session.end(False)
            session.save()

    # Respond to "#CURRENT" keyword
    elif len(text_words) > 0 and text_words[0] == "#CURRENT":
        if session is not None:
            resp = current_question(session.session_id)
            send_sms_to_verified_number(v, resp.event.text_prompt)

    # Respond to unknown command
    elif len(text_words) > 0 and text_words[0][0] == "#":
        send_sms_to_verified_number(v,
                                    "Unknown command '" + text_words[0] + "'")

    # If there's an open session, treat the inbound text as the answer to the next question
    elif session is not None:
        resp = current_question(session.session_id)
        event = resp.event
        valid, text, error_msg = validate_answer(event, text)

        if valid:
            responses = _get_responses(v.domain, v.owner_id, text)
            if len(responses) > 0:
                response_text = format_message_list(responses)
                send_sms_to_verified_number(v, response_text)
        else:
            send_sms_to_verified_number(v, error_msg + event.text_prompt)

    # Try to match the text against a keyword to start a survey
    elif len(text_words) > 0:
        sk = SurveyKeyword.get_keyword(v.domain, text_words[0])
        if sk is not None and sk.form_type == FORM_TYPE_ONE_BY_ONE:
            start_session_from_keyword(sk, v)

    # TODO should clarify what scenarios this handler actually handles. i.e.,
    # should the error responses instead be handler by some generic error/fallback
    # handler
    return True
Ejemplo n.º 15
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