Exemple #1
0
    def create_session_object(cls, domain, contact, phone_number, app, form, expire_after=MAX_SESSION_LENGTH,
            reminder_intervals=None, submit_partially_completed_forms=False,
            include_case_updates_in_partial_submissions=False):

        now = utcnow()

        session = cls(
            couch_id=uuid.uuid4().hex,
            connection_id=contact.get_id,
            form_xmlns=form.xmlns,
            start_time=now,
            modified_time=now,
            completed=False,
            domain=domain,
            user_id=contact.get_id,
            app_id=app.get_id,
            session_type=XFORMS_SESSION_SMS,
            phone_number=strip_plus(phone_number),
            expire_after=expire_after,
            session_is_open=True,
            reminder_intervals=reminder_intervals or [],
            current_reminder_num=0,
            submit_partially_completed_forms=submit_partially_completed_forms,
            include_case_updates_in_partial_submissions=include_case_updates_in_partial_submissions,
        )

        session.set_current_action_due_timestamp()

        return session
Exemple #2
0
def _sync_case_for_messaging_rule(domain, case_id, rule_id):
    case_load_counter("messaging_rule_sync", domain)()
    case = CaseAccessors(domain).get_case(case_id)
    rule = _get_cached_rule(domain, rule_id)
    if rule:
        rule.run_rule(case, utcnow())
        MessagingRuleProgressHelper(rule_id).increment_current_case_count()
Exemple #3
0
def _clean_xml_for_partial_submission(xml, should_remove_case_actions):
    """
    Helper method to cleanup partially completed xml for submission
    :param xml: partially completed xml
    :param should_remove_case_actions: if True, remove case actions (create, update, close) from xml
    :return: byte str of cleaned xml
    """
    root = XML(xml)
    case_tag_regex = re.compile(
        r"^(\{.*\}){0,1}case$"
    )  # Use regex in order to search regardless of namespace
    meta_tag_regex = re.compile(r"^(\{.*\}){0,1}meta$")
    timeEnd_tag_regex = re.compile(r"^(\{.*\}){0,1}timeEnd$")
    current_timestamp = json_format_datetime(utcnow())
    for child in root:
        if case_tag_regex.match(child.tag) is not None:
            # Found the case tag
            case_element = child
            case_element.set("date_modified", current_timestamp)
            if should_remove_case_actions:
                child_elements = [case_action for case_action in case_element]
                for case_action in child_elements:
                    case_element.remove(case_action)
        elif meta_tag_regex.match(child.tag) is not None:
            # Found the meta tag, now set the value for timeEnd
            for meta_child in child:
                if timeEnd_tag_regex.match(meta_child.tag):
                    meta_child.text = current_timestamp
    return tostring(root)
Exemple #4
0
    def create_session_object(cls, domain, contact, phone_number, app, form, expire_after=MAX_SESSION_LENGTH,
            reminder_intervals=None, submit_partially_completed_forms=False,
            include_case_updates_in_partial_submissions=False):

        now = utcnow()

        session = cls(
            couch_id=uuid.uuid4().hex,
            connection_id=contact.get_id,
            form_xmlns=form.xmlns,
            start_time=now,
            modified_time=now,
            completed=False,
            domain=domain,
            user_id=contact.get_id,
            app_id=app.get_id,
            session_type=XFORMS_SESSION_SMS,
            phone_number=strip_plus(phone_number),
            expire_after=expire_after,
            session_is_open=True,
            reminder_intervals=reminder_intervals or [],
            current_reminder_num=0,
            submit_partially_completed_forms=submit_partially_completed_forms,
            include_case_updates_in_partial_submissions=include_case_updates_in_partial_submissions,
        )

        session.set_current_action_due_timestamp()

        return session
Exemple #5
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 #6
0
    def set_first_event_due_timestamp(self, instance, start_date=None):
        """
        If start_date is None, we set it automatically ensuring that
        self.next_event_due does not get set in the past for the first
        event.
        """
        if start_date:
            instance.start_date = start_date
        else:
            instance.start_date = ServerTime(util.utcnow()).user_time(
                instance.timezone).done().date()

        self.set_next_event_due_timestamp(instance)

        if (not self.schedule_length == self.MONTHLY and not start_date
                and instance.next_event_due < util.utcnow()):
            instance.start_date += timedelta(days=1)
            instance.next_event_due += timedelta(days=1)
Exemple #7
0
    def handle(self, **options):
        rule = self.get_rule(options['domain'], options['rule_id'])

        print("Fetching case ids...")
        case_ids = CaseAccessors(rule.domain).get_case_ids_in_domain(rule.case_type)
        case_id_chunks = list(chunked(case_ids, 10))

        for case_id_chunk in with_progress_bar(case_id_chunks):
            case_id_chunk = list(case_id_chunk)
            with CriticalSection([get_sync_key(case_id) for case_id in case_id_chunk], timeout=5 * 60):
                for case in CaseAccessors(rule.domain).get_cases(case_id_chunk):
                    rule.run_rule(case, utcnow())
Exemple #8
0
def submit_unfinished_form(session):
    """
    Gets the raw instance of the session's form and submits it. This is used with
    sms and ivr surveys to save all questions answered so far in a session that
    needs to close.

    If session.include_case_updates_in_partial_submissions is False, no case
    create / update / close actions will be performed, but the form will still be submitted.

    The form is only submitted if the smsforms session has not yet completed.
    """
    # Get and clean the raw xml
    try:
        response = FormplayerInterface(session.session_id,
                                       session.domain).get_raw_instance()
        # Formplayer's ExceptionResponseBean includes the exception message,
        # stautus ("error"), url, and type ("text")
        if response.get('status') == 'error':
            raise TouchformsError(response.get('exception'))
        xml = response['output']
    except InvalidSessionIdException:
        return
    root = XML(xml)
    case_tag_regex = re.compile(
        r"^(\{.*\}){0,1}case$"
    )  # Use regex in order to search regardless of namespace
    meta_tag_regex = re.compile(r"^(\{.*\}){0,1}meta$")
    timeEnd_tag_regex = re.compile(r"^(\{.*\}){0,1}timeEnd$")
    current_timstamp = json_format_datetime(utcnow())
    for child in root:
        if case_tag_regex.match(child.tag) is not None:
            # Found the case tag
            case_element = child
            case_element.set("date_modified", current_timstamp)
            if not session.include_case_updates_in_partial_submissions:
                # Remove case actions (create, update, close)
                child_elements = [case_action for case_action in case_element]
                for case_action in child_elements:
                    case_element.remove(case_action)
        elif meta_tag_regex.match(child.tag) is not None:
            # Found the meta tag, now set the value for timeEnd
            for meta_child in child:
                if timeEnd_tag_regex.match(meta_child.tag):
                    meta_child.text = current_timstamp
    cleaned_xml = tostring(root)

    # Submit the xml
    result = submit_form_locally(cleaned_xml,
                                 session.domain,
                                 app_id=session.app_id,
                                 partial_submission=True)
    session.submission_id = result.xform.form_id
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 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 #10
0
    def handle(self, **options):
        rule = self.get_rule(options['domain'], options['rule_id'])

        print("Fetching case ids...")
        case_ids = CaseAccessors(rule.domain).get_case_ids_in_domain(
            rule.case_type)
        case_id_chunks = list(chunked(case_ids, 10))

        for case_id_chunk in with_progress_bar(case_id_chunks):
            case_id_chunk = list(case_id_chunk)
            with CriticalSection(
                [get_sync_key(case_id) for case_id in case_id_chunk],
                    timeout=5 * 60):
                for case in CaseAccessors(
                        rule.domain).get_cases(case_id_chunk):
                    rule.run_rule(case, utcnow())
Exemple #11
0
    def mark_completed(self, completed):
        self.session_is_open = False
        self.completed = completed
        self.modified_time = self.end_time = utcnow()
        if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(self.domain):
            XFormsSessionSynchronization.release_channel_for_session(self)

        metrics_counter('commcare.smsforms.session_ended', 1, tags={
            'domain': self.domain,
            'workflow': self.workflow,
            'status': (
                'success' if self.completed and self.submission_id else
                'terminated_partial_submission' if not self.completed and self.submission_id else
                'terminated_without_submission' if not self.completed and not self.submission_id else
                # Not sure if/how this could ever happen, but worth tracking if it does
                'completed_without_submission'
            )
        })
Exemple #12
0
    def test_timeEnd_value_is_set(self):
        xml_with_case_action = '''
        <data>
            <question>answer</question>
            <meta>
                <timeEnd/>
            </meta>
        </data>
        '''.strip()

        now = utcnow()
        expected_time_end = json_format_datetime(now)
        with patch('corehq.apps.smsforms.app.utcnow', return_value=now):
            cleaned_xml = _clean_xml_for_partial_submission(
                xml_with_case_action, should_remove_case_actions=True)

        xml = XML(cleaned_xml)
        self.assertEqual(
            xml.find('meta').find('timeEnd').text, expected_time_end)
Exemple #13
0
def _sync_case_for_messaging(domain, case_id):
    try:
        case = CaseAccessors(domain).get_case(case_id)
        sms_tasks.clear_case_caches(case)
    except CaseNotFound:
        case = None

    if case is None or case.is_deleted:
        sms_tasks.delete_phone_numbers_for_owners([case_id])
        delete_schedule_instances_for_cases(domain, [case_id])
        return

    if use_phone_entries():
        sms_tasks._sync_case_phone_number(case)

    rules = AutomaticUpdateRule.by_domain_cached(case.domain, AutomaticUpdateRule.WORKFLOW_SCHEDULING)
    rules_by_case_type = AutomaticUpdateRule.organize_rules_by_case_type(rules)
    for rule in rules_by_case_type.get(case.type, []):
        rule.run_rule(case, utcnow())
Exemple #14
0
def submit_unfinished_form(session):
    """
    Gets the raw instance of the session's form and submits it. This is used with
    sms and ivr surveys to save all questions answered so far in a session that
    needs to close.

    If session.include_case_updates_in_partial_submissions is False, no case
    create / update / close actions will be performed, but the form will still be submitted.

    The form is only submitted if the smsforms session has not yet completed.
    """
    # Get and clean the raw xml
    try:
        xml = get_raw_instance(session.session_id, session.domain)['output']
    except InvalidSessionIdException:
        return
    root = XML(xml)
    case_tag_regex = re.compile(r"^(\{.*\}){0,1}case$") # Use regex in order to search regardless of namespace
    meta_tag_regex = re.compile(r"^(\{.*\}){0,1}meta$")
    timeEnd_tag_regex = re.compile(r"^(\{.*\}){0,1}timeEnd$")
    current_timstamp = json_format_datetime(utcnow())
    for child in root:
        if case_tag_regex.match(child.tag) is not None:
            # Found the case tag
            case_element = child
            case_element.set("date_modified", current_timstamp)
            if not session.include_case_updates_in_partial_submissions:
                # Remove case actions (create, update, close)
                child_elements = [case_action for case_action in case_element]
                for case_action in child_elements:
                    case_element.remove(case_action)
        elif meta_tag_regex.match(child.tag) is not None:
            # Found the meta tag, now set the value for timeEnd
            for meta_child in child:
                if timeEnd_tag_regex.match(meta_child.tag):
                    meta_child.text = current_timstamp
    cleaned_xml = tostring(root)
    
    # Submit the xml
    result = submit_form_locally(cleaned_xml, session.domain, app_id=session.app_id, partial_submission=True)
    session.submission_id = result.xform.form_id
Exemple #15
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 #16
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 #17
0
    def set_first_event_due_timestamp(self, instance, start_date=None):
        """
        If start_date is None, we set it automatically ensuring that
        self.next_event_due does not get set in the past for the first
        event.
        """
        if start_date:
            instance.start_date = start_date
        else:
            instance.start_date = instance.today_for_recipient

        self.set_next_event_due_timestamp(instance)

        if (self.schedule_length != self.MONTHLY and not start_date
                and instance.next_event_due < util.utcnow()):
            if self.start_day_of_week == self.ANY_DAY:
                instance.start_date += timedelta(days=1)
                instance.next_event_due += timedelta(days=1)
            else:
                instance.start_date += timedelta(days=7)
                instance.next_event_due += timedelta(days=7)
Exemple #18
0
    def set_first_event_due_timestamp(self, instance, start_date=None):
        """
        If start_date is None, we set it automatically ensuring that
        self.next_event_due does not get set in the past for the first
        event.
        """
        if start_date:
            instance.start_date = start_date
        else:
            instance.start_date = instance.get_today_for_recipient(self)

        self.set_next_event_due_timestamp(instance)

        # If there was no specific start date for the schedule, we
        # start it today. But that can cause us to put the first event
        # in the past if it has already passed for the day. So if that
        # happens, push the schedule out by 1 day for daily schedules,
        # 1 week for weekly schedules, or 1 month for monthly schedules.
        if (
            not start_date and
            instance.next_event_due < util.utcnow()
        ):
            if self.is_monthly:
                # Monthly
                new_start_date = instance.start_date + relativedelta(months=1)
                instance.start_date = date(new_start_date.year, new_start_date.month, 1)
                # Current event and schedule iteration might be updated
                # in the call to set_next_event_due_timestamp, so reset them
                instance.current_event_num = 0
                instance.schedule_iteration_num = 1
            elif self.start_day_of_week == self.ANY_DAY:
                # Daily
                instance.start_date += timedelta(days=1)
            else:
                # Weekly
                instance.start_date += timedelta(days=7)

            self.set_next_event_due_timestamp(instance)
Exemple #19
0
    def set_first_event_due_timestamp(self, instance, start_date=None):
        """
        If start_date is None, we set it automatically ensuring that
        self.next_event_due does not get set in the past for the first
        event.
        """
        if start_date:
            instance.start_date = start_date
        else:
            instance.start_date = instance.get_today_for_recipient(self)

        self.set_next_event_due_timestamp(instance)

        # If there was no specific start date for the schedule, we
        # start it today. But that can cause us to put the first event
        # in the past if it has already passed for the day. So if that
        # happens, push the schedule out by 1 day for daily schedules,
        # 1 week for weekly schedules, or 1 month for monthly schedules.
        if (
            not start_date and
            instance.next_event_due < util.utcnow()
        ):
            if self.is_monthly:
                # Monthly
                new_start_date = instance.start_date + relativedelta(months=1)
                instance.start_date = date(new_start_date.year, new_start_date.month, 1)
                # Current event and schedule iteration might be updated
                # in the call to set_next_event_due_timestamp, so reset them
                instance.current_event_num = 0
                instance.schedule_iteration_num = 1
            elif self.start_day_of_week == self.ANY_DAY:
                # Daily
                instance.start_date += timedelta(days=1)
            else:
                # Weekly
                instance.start_date += timedelta(days=7)

            self.set_next_event_due_timestamp(instance)
Exemple #20
0
 def get_today_for_recipient(self, schedule):
     return ServerTime(util.utcnow()).user_time(self.get_timezone(schedule)).done().date()
Exemple #21
0
 def is_stale(self):
     return (util.utcnow() - self.next_event_due) > timedelta(minutes=STALE_SCHEDULE_INSTANCE_INTERVAL)
Exemple #22
0
 def mark_completed(self, completed):
     self.session_is_open = False
     self.completed = completed
     self.modified_time = self.end_time = utcnow()
Exemple #23
0
def run_auto_update_rules_for_case(case):
    rules = AutomaticUpdateRule.by_domain_cached(
        case.domain, AutomaticUpdateRule.WORKFLOW_SCHEDULING)
    rules_by_case_type = AutomaticUpdateRule.organize_rules_by_case_type(rules)
    for rule in rules_by_case_type.get(case.type, []):
        rule.run_rule(case, utcnow())
Exemple #24
0
    rules = AutomaticUpdateRule.by_domain_cached(
        domain, AutomaticUpdateRule.WORKFLOW_SCHEDULING)
    rules = [rule for rule in rules if rule.pk == rule_id]
    return rules[0] if len(rules) == 1 else None


def _sync_case_for_messaging_rule(domain, case_id, rule_id):
    case_load_counter("messaging_rule_sync", domain)()
    try:
        case = CaseAccessors(domain).get_case(case_id)
    except CaseNotFound:
        clear_messaging_for_case(domain, case_id)
        return
    rule = _get_cached_rule(domain, rule_id)
    if rule:
        rule.run_rule(case, utcnow())
        MessagingRuleProgressHelper(rule_id).increment_current_case_count()


def initiate_messaging_rule_run(rule):
    if not rule.active:
        return
    AutomaticUpdateRule.objects.filter(pk=rule.pk).update(
        locked_for_editing=True)
    transaction.on_commit(
        lambda: run_messaging_rule.delay(rule.domain, rule.pk))


def paginated_case_ids(domain, case_type):
    row_generator = paginate_query_across_partitioned_databases(
        CommCareCaseSQL,
Exemple #25
0
 def move_to_next_action(self):
     while self.current_action_is_a_reminder and self.current_action_due < utcnow():
         self.current_reminder_num += 1
         self.set_current_action_due_timestamp()
Exemple #26
0
 def set_first_event_due_timestamp(self, instance, start_date=None):
     instance.next_event_due = util.utcnow()
     self.set_next_event_due_timestamp(instance)
 def mark_completed(self, completed):
     self.session_is_open = False
     self.completed = completed
     self.modified_time = self.end_time = utcnow()
Exemple #28
0
def session_is_stale(session):
    return utcnow() > (session.start_time + timedelta(
        minutes=SQLXFormsSession.MAX_SESSION_LENGTH * 2))
Exemple #29
0
 def today_for_recipient(self):
     return ServerTime(util.utcnow()).user_time(self.timezone).done().date()
 def set_first_event_due_timestamp(self, instance, start_date=None):
     instance.next_event_due = util.utcnow()
     self.set_next_event_due_timestamp(instance)
Exemple #31
0
 def move_to_next_event_not_in_the_past(self, instance):
     while instance.active and instance.next_event_due < util.utcnow():
         self.move_to_next_event(instance)
Exemple #32
0
 def move_to_next_action(self):
     while self.current_action_is_a_reminder and self.current_action_due < utcnow():
         self.current_reminder_num += 1
         self.set_current_action_due_timestamp()
Exemple #33
0
def session_is_stale(session):
    return utcnow() > (session.start_time + timedelta(minutes=SQLXFormsSession.MAX_SESSION_LENGTH * 2))