Exemple #1
0
def handle_due_survey_action(domain, contact_id, session_id):
    with critical_section_for_smsforms_sessions(contact_id):
        session = SQLXFormsSession.by_session_id(session_id)
        if (not session or not session.session_is_open
                or session.current_action_due > utcnow()):
            return

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

            session.move_to_next_action()
            session.save()
        else:
            # Close the session
            session.close()
            session.save()
Exemple #2
0
def form_session_handler(v, text, msg):
    """
    The form session handler will use the inbound text to answer the next question
    in the open SQLXformsSession for the associated contact. If no session is open,
    the handler passes. If multiple sessions are open, they are all closed and an
    error message is displayed to the user.
    """
    with critical_section_for_smsforms_sessions(v.owner_id):
        multiple, session = get_single_open_session_or_close_multiple(v.domain, v.owner_id)
        if multiple:
            send_sms_to_verified_number(v, get_message(MSG_MULTIPLE_SESSIONS, v))
            return True

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

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

            try:
                answer_next_question(v, text, msg, session)
            except Exception:
                # Catch any touchforms errors
                log_sms_exception(msg)
                send_sms_to_verified_number(v, get_message(MSG_TOUCHFORMS_DOWN, v))
            return True
        else:
            return False
Exemple #3
0
def form_session_handler(v, text, msg):
    """
    The form session handler will use the inbound text to answer the next question
    in the open SQLXformsSession for the associated contact. If no session is open,
    the handler passes. If multiple sessions are open, they are all closed and an
    error message is displayed to the user.
    """
    with critical_section_for_smsforms_sessions(v.owner_id):
        multiple, session = get_single_open_session_or_close_multiple(v.domain, v.owner_id)
        if multiple:
            send_sms_to_verified_number(v, get_message(MSG_MULTIPLE_SESSIONS, v))
            return True

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

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

            try:
                answer_next_question(v, text, msg, session)
            except Exception:
                # Catch any touchforms errors
                log_sms_exception(msg)
                send_sms_to_verified_number(v, get_message(MSG_TOUCHFORMS_DOWN, v))
            return True
        else:
            return False
Exemple #4
0
def form_session_handler(v, text, msg):
    """
    The form session handler will use the inbound text to answer the next question
    in the open SQLXformsSession for the associated contact. If no session is open,
    the handler passes. If multiple sessions are open, they are all closed and an
    error message is displayed to the user.
    """
    with critical_section_for_smsforms_sessions(v.owner_id):
        if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(v.domain):
            channel = get_channel_for_contact(v.owner_id, v.phone_number)
            running_session_info = XFormsSessionSynchronization.get_running_session_info_for_channel(
                channel)
            if running_session_info.session_id:
                session = SQLXFormsSession.by_session_id(
                    running_session_info.session_id)
                if not session.session_is_open:
                    # This should never happen. But if it does we should set the channel free
                    # and act like there was no available session
                    notify_error(
                        "The supposedly running session was not open and was released. "
                        'No known way for this to happen, so worth investigating.'
                    )
                    XFormsSessionSynchronization.clear_stale_channel_claim(
                        channel)
                    session = None
            else:
                session = None
        else:
            multiple, session = get_single_open_session_or_close_multiple(
                v.domain, v.owner_id)
            if multiple:
                send_sms_to_verified_number(
                    v, get_message(MSG_MULTIPLE_SESSIONS, v))
                return True

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

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

            try:
                answer_next_question(v, text, msg, session)
            except Exception:
                # Catch any touchforms errors
                log_sms_exception(msg)
                send_sms_to_verified_number(
                    v, get_message(MSG_TOUCHFORMS_DOWN, v))
            return True
        else:
            return False
def sms_keyword_handler(v, text, msg):
    with critical_section_for_smsforms_sessions(v.owner_id):
        text = text.strip()
        if text == "":
            return False

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

        if text.startswith("#"):
            return handle_global_keywords(v, text, msg, text_words, sessions)
        else:
            return handle_domain_keywords(v, text, msg, text_words, sessions)
Exemple #6
0
def sms_keyword_handler(v, text, msg):
    with critical_section_for_smsforms_sessions(v.owner_id):
        text = text.strip()
        if text == "":
            return False

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

        if text.startswith("#"):
            return handle_global_keywords(v, text, msg, text_words, sessions)
        else:
            return handle_domain_keywords(v, text, msg, text_words, sessions)
Exemple #7
0
def handle_due_survey_action(domain, contact_id, session_id):
    with critical_section_for_smsforms_sessions(contact_id):
        session = SQLXFormsSession.by_session_id(session_id)
        if (not session or not session.session_is_open
                or session.current_action_due > utcnow()):
            return

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

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

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

            session.move_to_next_action()
            session.save()
        else:
            # Close the session
            session.close()
            session.save()
Exemple #8
0
def close_session(self, contact_id, session_id):
    with critical_section_for_smsforms_sessions(contact_id):
        session = SQLXFormsSession.by_session_id(session_id)
        try:
            session.close(force=False)
        except TouchformsError as e:
            try:
                self.retry(exc=e)
            except TouchformsError as e:
                raise e
            finally:
                # Eventually the session needs to get closed
                session.mark_completed(False)
                session.save()
                return
        session.save()
Exemple #9
0
def handle_due_survey_action(domain, contact_id, session_id):
    with critical_section_for_smsforms_sessions(contact_id):
        session = SQLXFormsSession.by_session_id(session_id)
        if (
            not session
            or not session.session_is_open
            or session.current_action_due > utcnow()
        ):
            return

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

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

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

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

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

            session.move_to_next_action()
            session.save()
        else:
            # Close the session
            session.close()
            session.save()
Exemple #11
0
    def get_critical_section(self, recipient):
        if self.critical_section_already_acquired:
            return no_op_context_manager()

        return critical_section_for_smsforms_sessions(recipient.get_id)
Exemple #12
0
    def get_critical_section(self, recipient):
        if self.critical_section_already_acquired:
            return no_op_context_manager()

        return critical_section_for_smsforms_sessions(recipient.get_id)
Exemple #13
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()
    def send(self, recipient, logged_event):
        app, module, form, requires_input = self.get_memoized_app_module_form(
            logged_event.domain)
        if any([o is None for o in (app, module, form)]):
            logged_event.error(MessagingEvent.ERROR_CANNOT_FIND_FORM)
            return

        logged_subevent = logged_event.create_subevent_from_contact_and_content(
            recipient,
            self,
            case_id=self.case.case_id if self.case else None,
        )

        # We don't try to look up the phone number from the user case in this scenario
        # because this use case involves starting a survey session, which can be
        # very different if the contact is a user or is a case. So here if recipient
        # is a user we only allow them to fill out the survey as the user contact, and
        # not the user case contact.
        phone_entry_or_number = self.get_two_way_entry_or_phone_number(
            recipient, try_user_case=False)

        if phone_entry_or_number is None:
            logged_subevent.error(MessagingEvent.ERROR_NO_PHONE_NUMBER)
            return

        if requires_input and not isinstance(phone_entry_or_number,
                                             PhoneNumber):
            logged_subevent.error(MessagingEvent.ERROR_NO_TWO_WAY_PHONE_NUMBER)
            return

        # The SMS framework already checks if the number has opted out before sending to
        # it. But for this use case we check for it here because we don't want to start
        # the survey session if they've opted out.
        if self.phone_has_opted_out(phone_entry_or_number):
            logged_subevent.error(MessagingEvent.ERROR_PHONE_OPTED_OUT)
            return

        with critical_section_for_smsforms_sessions(recipient.get_id):
            # Get the case to submit the form against, if any
            case_id = None
            if is_commcarecase(recipient):
                case_id = recipient.case_id
            elif self.case:
                case_id = self.case.case_id

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

            session, responses = self.start_smsforms_session(
                logged_event.domain, recipient, case_id,
                phone_entry_or_number, logged_subevent,
                self.get_workflow(logged_event), app, module, form)

            if session:
                logged_subevent.xforms_session = session
                logged_subevent.save()
                self.send_first_message(logged_event.domain, recipient,
                                        phone_entry_or_number, session,
                                        responses, logged_subevent,
                                        self.get_workflow(logged_event))
                logged_subevent.completed()