Example #1
0
 def test_case_recipient(self):
     with create_case(self.domain, 'person') as case:
         instance = CaseTimedScheduleInstance(domain=self.domain,
                                              case_id=case.case_id,
                                              recipient_type='Self')
         self.assertTrue(is_commcarecase(instance.recipient))
         self.assertEqual(instance.recipient.case_id, case.case_id)
    def get_recipient_details(self, sms):
        details = {
            'type': None,
            'name': None,
            'location_id': None,
        }

        if sms.couch_recipient:
            if sms.couch_recipient in self.recipient_details:
                return self.recipient_details[sms.couch_recipient]

            recipient = sms.recipient
            if is_commcarecase(recipient):
                details['name'] = recipient.name
                details['type'] = 'case: %s' % recipient.type

                if recipient.type == 'commcare-user':
                    user = CommCareUser.get_by_user_id(recipient.owner_id)
                    if user:
                        details['location_id'] = user.location_id
                else:
                    details['location_id'] = recipient.owner_id
            elif isinstance(recipient, CommCareUser):
                details['name'] = recipient.username
                details['type'] = 'mobile worker'
                details['location_id'] = recipient.location_id

            self.recipient_details[sms.couch_recipient] = details

        return details
Example #3
0
 def test_case_owner(self):
     with create_test_case(self.domain, 'participant', 'test') as case:
         number = PhoneNumber(owner_doc_type='CommCareCase',
                              owner_id=case.case_id)
         owner = number.owner
         self.assertTrue(is_commcarecase(owner))
         self.assertEqual(owner.case_id, case.case_id)
Example #4
0
def get_recipient_name(recipient, include_desc=True):
    if recipient is None:
        return "(no recipient)"
    elif isinstance(recipient, list):
        if len(recipient) > 0:
            return ",".join([get_recipient_name(r, include_desc) for r in recipient])
        else:
            return "(no recipient)"
    elif isinstance(recipient, CouchUser):
        name = recipient.raw_username
        desc = "User"
    elif is_commcarecase(recipient):
        name = recipient.name
        desc = "Case"
    elif isinstance(recipient, Group):
        name = recipient.name
        desc = "Group"
    elif isinstance(recipient, CommCareCaseGroup):
        name = recipient.name
        desc = "Case Group"
    elif isinstance(recipient, SQLLocation):
        name = recipient.name
        desc = "Location"
    else:
        name = "(unknown)"
        desc = ""
    
    if include_desc:
        return "%s '%s'" % (desc, name)
    else:
        return name
Example #5
0
    def send(self, recipient, logged_event, phone_entry=None):
        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 = (phone_entry
                                 or self.get_two_way_entry_or_phone_number(
                                     recipient,
                                     try_usercase=False,
                                     domain_for_toggles=logged_event.domain))

        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

        with self.get_critical_section(recipient):
            # 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, form)

            if session:
                logged_subevent.xforms_session = session
                logged_subevent.save()
                # send_first_message is a celery task
                # but we first call it synchronously to save resources in the 99% case
                # send_first_message will retry itself as a delayed celery task
                # if there are conflicting sessions preventing it from sending immediately
                send_first_message(logged_event.domain, recipient,
                                   phone_entry_or_number, session, responses,
                                   logged_subevent,
                                   self.get_workflow(logged_event))
Example #6
0
def get_recipient_name(recipient, include_desc=True):
    if recipient is None:
        return "(no recipient)"
    elif isinstance(recipient, list):
        if len(recipient) > 0:
            return ",".join(
                [get_recipient_name(r, include_desc) for r in recipient])
        else:
            return "(no recipient)"
    elif isinstance(recipient, CouchUser):
        name = recipient.raw_username
        desc = "User"
    elif is_commcarecase(recipient):
        name = recipient.name
        desc = "Case"
    elif isinstance(recipient, Group):
        name = recipient.name
        desc = "Group"
    elif isinstance(recipient, CommCareCaseGroup):
        name = recipient.name
        desc = "Case Group"
    elif isinstance(recipient, SQLLocation):
        name = recipient.name
        desc = "Location"
    else:
        name = "(unknown)"
        desc = ""

    if include_desc:
        return "%s '%s'" % (desc, name)
    else:
        return name
    def get_recipient_details(self, sms):
        details = {
            'type': None,
            'name': None,
            'location_id': None,
        }

        if sms.couch_recipient:
            if sms.couch_recipient in self.recipient_details:
                return self.recipient_details[sms.couch_recipient]

            recipient = sms.recipient
            if is_commcarecase(recipient):
                details['name'] = recipient.name
                details['type'] = 'case: %s' % recipient.type

                if recipient.type == 'commcare-user':
                    user = CommCareUser.get_by_user_id(recipient.owner_id)
                    if user:
                        details['location_id'] = user.location_id
                else:
                    details['location_id'] = recipient.owner_id
            elif isinstance(recipient, CommCareUser):
                details['name'] = recipient.username
                details['type'] = 'mobile worker'
                details['location_id'] = recipient.location_id

            self.recipient_details[sms.couch_recipient] = details

        return details
Example #8
0
    def test_get_contact_for_case(self):
        with create_test_case(self.domain, "contact", "test-case") as case:
            contact = get_contact(self.domain, case.case_id)
            self.assertEqual(contact.case_id, case.case_id)
            self.assertTrue(is_commcarecase(contact))

            with self.assertRaises(ContactNotFoundException):
                get_contact(self.domain + "x", case.case_id)
    def test_get_contact_for_case(self):
        with create_test_case(self.domain, 'contact', 'test-case') as case:
            contact = get_contact(self.domain, case.case_id)
            self.assertEqual(contact.case_id, case.case_id)
            self.assertTrue(is_commcarecase(contact))

            with self.assertRaises(ContactNotFoundException):
                get_contact(self.domain + 'x', case.case_id)
Example #10
0
def get_case_id(contact, case=None):
    if case:
        case_id = case.case_id
    elif is_commcarecase(contact):
        case_id = contact.case_id
    else:
        case_id = None
    return case_id
Example #11
0
def start_session(session, domain, contact, app, module, form, case_id=None, yield_responses=False,
                  case_for_case_submission=False):
    """
    Starts a session in touchforms and saves the record in the database.
    
    Returns a tuple containing the session object and the (text-only) 
    list of generated questions/responses based on the form.
    
    Special params:
    yield_responses - If True, the list of xforms responses is returned, otherwise the text prompt for each is returned
    session_type - XFORMS_SESSION_SMS or XFORMS_SESSION_IVR
    case_for_case_submission - True if this is a submission that a case is making to alter another related case. For example, if a parent case is filling out
        an SMS survey which will update its child case, this should be True.
    """
    # NOTE: this call assumes that "contact" will expose three
    # properties: .raw_username, .get_id, and .get_language_code
    session_data = CaseSessionDataHelper(domain, contact, case_id, app, form).get_session_data(COMMCONNECT_DEVICE_ID)

    # since the API user is a superuser, force touchforms to query only
    # the contact's cases by specifying it as an additional filter
    if is_commcarecase(contact) and form.requires_case():
        session_data["additional_filters"] = {
            "case_id": case_id,
            "footprint": "true" if form.uses_parent_case() else "false",
        }
    elif isinstance(contact, CouchUser):
        session_data["additional_filters"] = {
            "user_id": contact.get_id,
            "footprint": "true"
        }

    if app and form:
        session_data.update(get_cloudcare_session_data(domain, form, contact))

    language = contact.get_language_code()
    config = XFormsConfig(form_content=form.render_xform(),
                          language=language,
                          session_data=session_data,
                          auth=AUTH)

    session_start_info = tfsms.start_session(config)
    session.session_id = session_start_info.session_id
    session.save()
    responses = session_start_info.first_responses

    if len(responses) > 0 and responses[0].status == 'http-error':
        session.mark_completed(False)
        session.save()
        raise TouchformsError('Cannot connect to touchforms.')

    # Prevent future update conflicts by getting the session again from the db
    # since the session could have been updated separately in the first_responses call
    session = SQLXFormsSession.objects.get(pk=session.pk)
    if yield_responses:
        return (session, responses)
    else:
        return (session, _responses_to_text(responses))
Example #12
0
def _generic_message_bank_content(fixture_name, reminder, handler, recipient):
    domain = reminder.domain
    message_bank = FixtureDataType.by_domain_tag(domain, fixture_name).first()

    if not message_bank:
        message = "Lookup Table {} not found in {}".format(
            fixture_name, domain)
        notify_dimagi_project_admins(domain, message=message)
        return None

    fields = message_bank.fields_without_attributes

    if any(field not in fields for field in REQUIRED_FIXTURE_FIELDS):
        message = "{} in {} must have {}".format(
            fixture_name, domain, ','.join(REQUIRED_FIXTURE_FIELDS))
        notify_dimagi_project_admins(domain, message=message)
        return None

    if not is_commcarecase(recipient):
        recipient_id = getattr(recipient, '_id') if hasattr(
            recipient, '_id') else 'id_unknown'
        message = "recipient {} must be a case in domain {}".format(
            recipient_id, domain)
        notify_dimagi_project_admins(domain, message=message)
        return None

    try:
        risk_profile = recipient.dynamic_case_properties()[RISK_PROFILE_FIELD]
    except KeyError:
        message = "case {} does not include risk_profile".format(
            recipient.case_id)
        notify_dimagi_project_admins(domain, message=message)
        return None

    current_message_seq_num = str((
        (reminder.schedule_iteration_num - 1) * len(handler.events)) +
                                  reminder.current_event_sequence_num + 1)
    custom_messages = FixtureDataItem.by_field_value(domain, message_bank,
                                                     RISK_PROFILE_FIELD,
                                                     risk_profile)
    custom_messages = [
        m for m in custom_messages
        if m.fields_without_attributes['sequence'] == current_message_seq_num
    ]

    if len(custom_messages) != 1:
        if not custom_messages:
            message = "No message for case {}, risk {}, seq {} in domain {} in fixture {}"
        else:
            message = "Multiple messages for case {}, risk {}, seq {} in domain {} in fixture {}"
        message = message.format(recipient.case_id, risk_profile,
                                 current_message_seq_num, domain, fixture_name)
        notify_dimagi_project_admins(domain, message=message)
        return None

    return custom_messages[0].fields_without_attributes['message']
Example #13
0
def start_session(session,
                  domain,
                  contact,
                  app,
                  form,
                  case_id=None,
                  yield_responses=False):
    """
    Starts a session in touchforms and saves the record in the database.
    
    Returns a tuple containing the session object and the (text-only) 
    list of generated questions/responses based on the form.
    
    Special params:
    yield_responses - If True, the list of xforms responses is returned, otherwise the text prompt for each is returned
    """
    # NOTE: this call assumes that "contact" will expose three
    # properties: .raw_username, .get_id, and .get_language_code
    session_data = CaseSessionDataHelper(
        domain, contact, case_id, app,
        form).get_session_data(COMMCONNECT_DEVICE_ID)

    kwargs = {}
    if is_commcarecase(contact):
        kwargs['restore_as_case_id'] = contact.case_id
    else:
        kwargs['restore_as'] = contact.raw_username

    if app and form:
        session_data.update(get_cloudcare_session_data(domain, form, contact))

    language = contact.get_language_code()
    config = XFormsConfig(form_content=form.render_xform().decode('utf-8'),
                          language=language,
                          session_data=session_data,
                          domain=domain,
                          **kwargs)

    session_start_info = tfsms.start_session(config)
    session.session_id = session_start_info.session_id
    session.save()
    responses = session_start_info.first_responses

    if len(responses) > 0 and responses[0].status == 'http-error':
        session.mark_completed(False)
        session.save()
        raise TouchformsError('Cannot connect to touchforms.')

    # Prevent future update conflicts by getting the session again from the db
    # since the session could have been updated separately in the first_responses call
    session = SQLXFormsSession.objects.get(pk=session.pk)
    if yield_responses:
        return (session, responses)
    else:
        return (session, _responses_to_text(responses))
def fire_ivr_survey_event(reminder, handler, recipients, verified_numbers,
                          logged_event):
    domain_obj = Domain.get_by_name(reminder.domain, strict=True)
    for recipient in recipients:
        initiate_call = True
        if reminder.callback_try_count > 0 and reminder.event_initiation_timestamp:
            initiate_call = not Call.answered_call_exists(
                recipient.doc_type, recipient.get_id,
                reminder.event_initiation_timestamp)

        if initiate_call:
            if (is_commcarecase(recipient)
                    and not handler.force_surveys_to_use_triggered_case):
                case_id = recipient.case_id
            else:
                case_id = reminder.case_id
            verified_number, unverified_number = get_recipient_phone_number(
                reminder, recipient, verified_numbers)
            if verified_number:
                initiate_outbound_call.delay(
                    recipient,
                    reminder.current_event.form_unique_id,
                    handler.submit_partial_forms,
                    handler.include_case_side_effects,
                    handler.max_question_retries,
                    logged_event.pk,
                    verified_number=verified_number,
                    case_id=case_id,
                    case_for_case_submission=handler.
                    force_surveys_to_use_triggered_case,
                    timestamp=CaseReminderHandler.get_now(),
                )
            elif domain_obj.send_to_duplicated_case_numbers and unverified_number:
                initiate_outbound_call.delay(
                    recipient,
                    reminder.current_event.form_unique_id,
                    handler.submit_partial_forms,
                    handler.include_case_side_effects,
                    handler.max_question_retries,
                    logged_event.pk,
                    unverified_number=unverified_number,
                    case_id=case_id,
                    case_for_case_submission=handler.
                    force_surveys_to_use_triggered_case,
                    timestamp=CaseReminderHandler.get_now(),
                )
            else:
                # initiate_outbound_call will create the subevent automatically,
                # so since we're not initiating the call here, we have to create
                # the subevent explicitly in order to log the error.
                logged_subevent = logged_event.create_subevent(
                    handler, reminder, recipient)
                logged_subevent.error(MessagingEvent.ERROR_NO_PHONE_NUMBER)
Example #15
0
def fire_sms_event(reminder, handler, recipients, verified_numbers, logged_event, workflow=None):
    current_event = reminder.current_event
    case = reminder.case
    template_params = get_message_template_params(case)

    uses_custom_content_handler, content_handler = get_custom_content_handler(handler, logged_event)
    if uses_custom_content_handler and not content_handler:
        return

    domain_obj = Domain.get_by_name(reminder.domain, strict=True)
    for recipient in recipients:
        logged_subevent = logged_event.create_subevent(handler, reminder, recipient)

        try:
            lang = recipient.get_language_code()
        except Exception:
            lang = None

        if content_handler:
            message = content_handler(reminder, handler, recipient)
        else:
            message = current_event.message.get(lang, current_event.message[handler.default_lang])
            try:
                message = Message.render(message, **template_params)
            except Exception:
                logged_subevent.error(MessagingEvent.ERROR_CANNOT_RENDER_MESSAGE)
                continue

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

        if message:
            metadata = MessageMetadata(
                workflow=workflow or get_workflow(handler),
                reminder_id=reminder._id,
                messaging_subevent_id=logged_subevent.pk,
            )
            if verified_number is not None:
                send_sms_to_verified_number(verified_number,
                    message, metadata, logged_subevent=logged_subevent)
            elif isinstance(recipient, CouchUser) and unverified_number:
                send_sms(reminder.domain, recipient, unverified_number,
                    message, metadata)
            elif (is_commcarecase(recipient) and unverified_number and
                    domain_obj.send_to_duplicated_case_numbers):
                send_sms(reminder.domain, recipient, unverified_number,
                    message, metadata)
            else:
                logged_subevent.error(MessagingEvent.ERROR_NO_PHONE_NUMBER)
                continue

        logged_subevent.completed()
Example #16
0
def fire_sms_event(reminder, handler, recipients, verified_numbers, logged_event, workflow=None):
    current_event = reminder.current_event
    case = reminder.case
    template_params = get_message_template_params(case)

    uses_custom_content_handler, content_handler = get_custom_content_handler(handler, logged_event)
    if uses_custom_content_handler and not content_handler:
        return

    domain_obj = Domain.get_by_name(reminder.domain, strict=True)
    for recipient in recipients:
        logged_subevent = logged_event.create_subevent(handler, reminder, recipient)

        try:
            lang = recipient.get_language_code()
        except Exception:
            lang = None

        if content_handler:
            message = content_handler(reminder, handler, recipient)
        else:
            message = current_event.message.get(lang, current_event.message[handler.default_lang])
            try:
                message = Message.render(message, **template_params)
            except Exception:
                logged_subevent.error(MessagingEvent.ERROR_CANNOT_RENDER_MESSAGE)
                continue

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

        if message:
            metadata = MessageMetadata(
                workflow=workflow or get_workflow(handler),
                reminder_id=reminder._id,
                messaging_subevent_id=logged_subevent.pk,
            )
            if verified_number is not None:
                send_sms_to_verified_number(verified_number,
                    message, metadata, logged_subevent=logged_subevent)
            elif isinstance(recipient, CouchUser) and unverified_number:
                send_sms(reminder.domain, recipient, unverified_number,
                    message, metadata)
            elif (is_commcarecase(recipient) and unverified_number and
                    domain_obj.send_to_duplicated_case_numbers):
                send_sms(reminder.domain, recipient, unverified_number,
                    message, metadata)
            else:
                logged_subevent.error(MessagingEvent.ERROR_NO_PHONE_NUMBER)
                continue

        logged_subevent.completed()
Example #17
0
def get_one_way_number_for_recipient(recipient):
    if isinstance(recipient, CouchUser):
        return recipient.phone_number
    elif is_commcarecase(recipient):
        one_way_number = recipient.get_case_property('contact_phone_number')
        one_way_number = apply_leniency(one_way_number)
        if one_way_number:
            try:
                CommCareMobileContactMixin.validate_number_format(one_way_number)
                return one_way_number
            except InvalidFormatException:
                return None
    return None
Example #18
0
def get_one_way_number_for_recipient(recipient):
    if isinstance(recipient, CouchUser):
        return recipient.phone_number
    elif is_commcarecase(recipient):
        one_way_number = recipient.get_case_property('contact_phone_number')
        one_way_number = apply_leniency(one_way_number)
        if one_way_number:
            try:
                CommCareMobileContactMixin.validate_number_format(one_way_number)
                return one_way_number
            except InvalidFormatException:
                return None
    return None
Example #19
0
def _get_obj_template_info(obj):
    if is_commcarecase(obj):
        return _get_case_template_info(obj)
    elif isinstance(obj, WebUser):
        return _get_web_user_template_info(obj)
    elif isinstance(obj, CommCareUser):
        return _get_mobile_user_template_info(obj)
    elif isinstance(obj, Group):
        return _get_group_template_info(obj)
    elif isinstance(obj, SQLLocation):
        return _get_location_template_info(obj)

    return {}
Example #20
0
def _get_obj_template_info(obj):
    if is_commcarecase(obj):
        return _get_case_template_info(obj)
    elif isinstance(obj, WebUser):
        return _get_web_user_template_info(obj)
    elif isinstance(obj, CommCareUser):
        return _get_mobile_user_template_info(obj)
    elif isinstance(obj, Group):
        return _get_group_template_info(obj)
    elif isinstance(obj, Location):
        return _get_location_template_info(obj)

    return {}
Example #21
0
def fire_ivr_survey_event(reminder, handler, recipients, verified_numbers, logged_event):
    domain_obj = Domain.get_by_name(reminder.domain, strict=True)
    for recipient in recipients:
        initiate_call = True
        if reminder.callback_try_count > 0 and reminder.event_initiation_timestamp:
            initiate_call = not Call.answered_call_exists(
                recipient.doc_type,
                recipient.get_id,
                reminder.event_initiation_timestamp
            )

        if initiate_call:
            if (is_commcarecase(recipient) and
                not handler.force_surveys_to_use_triggered_case):
                case_id = recipient.case_id
            else:
                case_id = reminder.case_id
            verified_number, unverified_number = get_recipient_phone_number(
                reminder, recipient, verified_numbers)
            if verified_number:
                initiate_outbound_call.delay(
                    recipient,
                    reminder.current_event.form_unique_id,
                    handler.submit_partial_forms,
                    handler.include_case_side_effects,
                    handler.max_question_retries,
                    logged_event.pk,
                    verified_number=verified_number,
                    case_id=case_id,
                    case_for_case_submission=handler.force_surveys_to_use_triggered_case,
                    timestamp=CaseReminderHandler.get_now(),
                )
            elif domain_obj.send_to_duplicated_case_numbers and unverified_number:
                initiate_outbound_call.delay(
                    recipient,
                    reminder.current_event.form_unique_id,
                    handler.submit_partial_forms,
                    handler.include_case_side_effects,
                    handler.max_question_retries,
                    logged_event.pk,
                    unverified_number=unverified_number,
                    case_id=case_id,
                    case_for_case_submission=handler.force_surveys_to_use_triggered_case,
                    timestamp=CaseReminderHandler.get_now(),
                )
            else:
                # initiate_outbound_call will create the subevent automatically,
                # so since we're not initiating the call here, we have to create
                # the subevent explicitly in order to log the error.
                logged_subevent = logged_event.create_subevent(handler, reminder, recipient)
                logged_subevent.error(MessagingEvent.ERROR_NO_PHONE_NUMBER)
Example #22
0
def _get_message_bank_content(fixture_name, domain, schedule_iteration_num, current_event_num, num_events,
        recipient):
    message_bank = FixtureDataType.by_domain_tag(domain, fixture_name).first()

    if not message_bank:
        message = "Lookup Table {} not found in {}".format(fixture_name, domain)
        notify_dimagi_project_admins(domain, message=message)
        return None

    fields = message_bank.fields_without_attributes

    if any(field not in fields for field in REQUIRED_FIXTURE_FIELDS):
        message = "{} in {} must have {}".format(
            fixture_name, domain, ','.join(REQUIRED_FIXTURE_FIELDS)
        )
        notify_dimagi_project_admins(domain, message=message)
        return None

    if not is_commcarecase(recipient):
        recipient_id = getattr(recipient, '_id') if hasattr(recipient, '_id') else 'id_unknown'
        message = "recipient {} must be a case in domain {}".format(recipient_id, domain)
        notify_dimagi_project_admins(domain, message=message)
        return None

    try:
        risk_profile = recipient.dynamic_case_properties()[RISK_PROFILE_FIELD]
    except KeyError:
        message = "case {} does not include risk_profile".format(recipient.case_id)
        notify_dimagi_project_admins(domain, message=message)
        return None

    current_message_seq_num = str(
        ((schedule_iteration_num - 1) * num_events) +
        current_event_num + 1
    )
    custom_messages = FixtureDataItem.by_field_value(
        domain, message_bank, RISK_PROFILE_FIELD, risk_profile
    )
    custom_messages = [m for m in custom_messages if m.fields_without_attributes['sequence'] == current_message_seq_num]

    if len(custom_messages) != 1:
        if not custom_messages:
            message = "No message for case {}, risk {}, seq {} in domain {} in fixture {}"
        else:
            message = "Multiple messages for case {}, risk {}, seq {} in domain {} in fixture {}"
        message = message.format(recipient.case_id, risk_profile, current_message_seq_num, domain, fixture_name)
        notify_dimagi_project_admins(domain, message=message)
        return None

    return custom_messages[0].fields_without_attributes['message']
Example #23
0
def send_sms(domain,
             contact,
             phone_number,
             text,
             metadata=None,
             logged_subevent=None):
    """
    Sends an outbound SMS. Returns false if it fails.
    """
    if phone_number is None:
        return False
    if isinstance(phone_number, six.integer_types):
        phone_number = str(phone_number)
    phone_number = clean_phone_number(phone_number)

    msg = get_sms_class()(domain=domain,
                          phone_number=phone_number,
                          direction=OUTGOING,
                          date=get_utcnow(),
                          backend_id=None,
                          location_id=get_location_id_by_contact(
                              domain, contact),
                          text=text)
    if contact:
        msg.couch_recipient = contact.get_id
        msg.couch_recipient_doc_type = contact.doc_type

    if domain and contact and is_commcarecase(contact):
        backend_name = contact.get_case_property('contact_backend_id')
        backend_name = backend_name.strip() if isinstance(
            backend_name, six.string_types) else ''
        soft_assert_type_text(backend_name)
        if backend_name:
            try:
                backend = SQLMobileBackend.load_by_name(
                    SQLMobileBackend.SMS, domain, backend_name)
            except BadSMSConfigException as e:
                if logged_subevent:
                    logged_subevent.error(
                        MessagingEvent.ERROR_GATEWAY_NOT_FOUND,
                        additional_error_text=six.text_type(e))
                return False

            msg.backend_id = backend.couch_id

    add_msg_tags(msg, metadata)

    return queue_outgoing_sms(msg)
Example #24
0
    def get_content_send_lock(self, client, recipient):
        if is_commcarecase(recipient):
            doc_type = 'CommCareCase'
            doc_id = recipient.case_id
        else:
            doc_type = recipient.doc_type
            doc_id = recipient.get_id

        key = "send-content-for-%s-%s-%s-%s-%s" % (
            self.__class__.__name__,
            self.schedule_instance_id.hex,
            self.next_event_due.strftime('%Y-%m-%d %H:%M:%S'),
            doc_type,
            doc_id,
        )
        return client.lock(key, timeout=STALE_SCHEDULE_INSTANCE_INTERVAL * 60)
Example #25
0
    def get_content_send_lock(self, client, recipient):
        if is_commcarecase(recipient):
            doc_type = 'CommCareCase'
            doc_id = recipient.case_id
        else:
            doc_type = recipient.doc_type
            doc_id = recipient.get_id

        key = "send-content-for-%s-%s-%s-%s-%s" % (
            self.__class__.__name__,
            self.schedule_instance_id.hex,
            self.next_event_due.strftime('%Y-%m-%d %H:%M:%S'),
            doc_type,
            doc_id,
        )
        return client.lock(key, timeout=STALE_SCHEDULE_INSTANCE_INTERVAL * 60)
Example #26
0
def get_unverified_number_for_recipient(recipient):
    if isinstance(recipient, CouchUser):
        try:
            return recipient.phone_number
        except Exception:
            # todo: catch more specific error
            return None
    elif is_commcarecase(recipient):
        unverified_number = recipient.get_case_property("contact_phone_number")
        unverified_number = apply_leniency(unverified_number)
        if unverified_number:
            try:
                CommCareMobileContactMixin.validate_number_format(unverified_number)
                return unverified_number
            except InvalidFormatException:
                return None
    return None
Example #27
0
def get_unverified_number_for_recipient(recipient):
    if isinstance(recipient, CouchUser):
        try:
            return recipient.phone_number
        except Exception:
            # todo: catch more specific error
            return None
    elif is_commcarecase(recipient):
        unverified_number = recipient.get_case_property("contact_phone_number")
        unverified_number = apply_leniency(unverified_number)
        if unverified_number:
            try:
                CommCareMobileContactMixin.validate_number_format(
                    unverified_number)
                return unverified_number
            except InvalidFormatException:
                return None
    return None
Example #28
0
def process_survey_keyword_actions(verified_number, survey_keyword, text, msg):
    sender = verified_number.owner
    case = None
    args = split_args(text, survey_keyword)

    logged_event = MessagingEvent.create_from_keyword(survey_keyword, sender)

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

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

    if is_commcarecase(sender):
        case = sender
        args = args[1:]
Example #29
0
def send_sms(domain, contact, phone_number, text, metadata=None, logged_subevent=None):
    """
    Sends an outbound SMS. Returns false if it fails.
    """
    if phone_number is None:
        return False
    if isinstance(phone_number, six.integer_types):
        phone_number = str(phone_number)
    phone_number = clean_phone_number(phone_number)

    msg = get_sms_class()(
        domain=domain,
        phone_number=phone_number,
        direction=OUTGOING,
        date=get_utcnow(),
        backend_id=None,
        location_id=get_location_id_by_contact(domain, contact),
        text = text
    )
    if contact:
        msg.couch_recipient = contact.get_id
        msg.couch_recipient_doc_type = contact.doc_type

    if domain and contact and is_commcarecase(contact):
        backend_name = contact.get_case_property('contact_backend_id')
        backend_name = backend_name.strip() if isinstance(backend_name, six.string_types) else ''
        soft_assert_type_text(backend_name)
        if backend_name:
            try:
                backend = SQLMobileBackend.load_by_name(SQLMobileBackend.SMS, domain, backend_name)
            except BadSMSConfigException as e:
                if logged_subevent:
                    logged_subevent.error(MessagingEvent.ERROR_GATEWAY_NOT_FOUND,
                        additional_error_text=six.text_type(e))
                return False

            msg.backend_id = backend.couch_id

    add_msg_tags(msg, metadata)

    return queue_outgoing_sms(msg)
Example #30
0
    def test_get_contact(self):
        with create_test_case(self.domain, 'contact', 'test-case') as case:
            user = CommCareUser.create(self.domain, 'test-user', '123')
            self.addCleanup(user.delete)

            contact = get_contact(self.domain, case.case_id)
            self.assertEqual(contact.case_id, case.case_id)
            self.assertTrue(is_commcarecase(contact))

            contact = get_contact(self.domain, user.get_id)
            self.assertEqual(contact.get_id, user.get_id)
            self.assertTrue(isinstance(contact, CommCareUser))

            with self.assertRaises(ContactNotFoundException):
                get_contact(self.domain, 'this-id-should-not-be-found')

            with self.assertRaises(ContactNotFoundException):
                get_contact(self.domain + 'x', case.case_id)

            with self.assertRaises(ContactNotFoundException):
                get_contact(self.domain + 'x', user.get_id)
    def get_location_id(self, sms):
        location_id = None

        if sms.couch_recipient:
            if sms.couch_recipient in self.recipient_id_to_location_id:
                return self.recipient_id_to_location_id[sms.couch_recipient]

            recipient = sms.recipient
            if is_commcarecase(recipient):
                if recipient.type == 'commcare-user':
                    user = CommCareUser.get_by_user_id(recipient.owner_id)
                    if user:
                        location_id = user.location_id
                else:
                    location_id = recipient.owner_id
            elif isinstance(recipient, CommCareUser):
                location_id = recipient.location_id

            self.recipient_id_to_location_id[sms.couch_recipient] = location_id

        return location_id
Example #32
0
    def get_location_id(self, sms):
        location_id = None

        if sms.couch_recipient:
            if sms.couch_recipient in self.recipient_id_to_location_id:
                return self.recipient_id_to_location_id[sms.couch_recipient]

            recipient = sms.recipient
            if is_commcarecase(recipient):
                if recipient.type == 'commcare-user':
                    user = CommCareUser.get_by_user_id(recipient.owner_id)
                    if user:
                        location_id = user.location_id
                else:
                    location_id = recipient.owner_id
            elif isinstance(recipient, CommCareUser):
                location_id = recipient.location_id

            self.recipient_id_to_location_id[sms.couch_recipient] = location_id

        return location_id
Example #33
0
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers, logged_event):
    if reminder.callback_try_count > 0:
        # Handle timeouts
        if handler.submit_partial_forms and (reminder.callback_try_count == len(reminder.current_event.callback_timeout_intervals)):
            # Submit partial form completions
            for session_id in reminder.xforms_session_ids:
                submit_unfinished_form(session_id, handler.include_case_side_effects)
        else:
            # Resend current question
            for session_id in reminder.xforms_session_ids:
                session = get_session_by_session_id(session_id)
                if session.end_time is None:
                    vn = verified_numbers.get(session.connection_id)
                    if vn is not None:
                        metadata = MessageMetadata(
                            workflow=get_workflow(handler),
                            reminder_id=reminder._id,
                            xforms_session_couch_id=session._id,
                        )
                        resp = current_question(session_id)
                        send_sms_to_verified_number(vn, resp.event.text_prompt, metadata,
                            logged_subevent=session.related_subevent)
    else:
        reminder.xforms_session_ids = []
        domain_obj = Domain.get_by_name(reminder.domain, strict=True)

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

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

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

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

            key = "start-sms-survey-for-contact-%s" % recipient.get_id
            with CriticalSection([key], timeout=60):
                # Get the case to submit the form against, if any
                if (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:
                    session, responses = start_session(reminder.domain, recipient,
                        app, module, form, case_id,
                        case_for_case_submission=handler.force_surveys_to_use_triggered_case)
                except TouchformsError as e:
                    human_readable_message = e.response_data.get('human_readable_message', None)

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

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

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

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

            logged_subevent.completed()
Example #34
0
        args = text.split(survey_keyword.delimiter)
    else:
        args = text.split()
    args = [arg.strip() for arg in args]
    return args


def log_error(error, logged_subevent=None):
    if logged_subevent:
        logged_subevent.error(error)


def get_case_id(contact, case=None):
    if case:
        case_id = case.case_id
    elif is_commcarecase(contact):
        case_id = contact.case_id
    else:
        case_id = None
    return case_id


def get_app_module_form(form_unique_id, logged_subevent=None):
    """
    Returns (app, module, form, error, error_code)
    """
    try:
        form = Form.get_form(form_unique_id)
        app = form.get_app()
        module = form.get_module()
        return (app, module, form, False, None)
Example #35
0
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers,
                          logged_event):
    current_event = reminder.current_event
    if reminder.callback_try_count > 0:
        # Leaving this as an explicit reminder that all survey related actions now happen
        # in a different process. Eventually all of this code will be removed when we move
        # to the new reminders framework.
        pass
    else:
        reminder.xforms_session_ids = []
        domain_obj = Domain.get_by_name(reminder.domain, strict=True)

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

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

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

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

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

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

            with critical_section_for_smsforms_sessions(recipient.get_id):
                # Get the case to submit the form against, if any
                if (is_commcarecase(recipient)
                        and not handler.force_surveys_to_use_triggered_case):
                    case_id = recipient.case_id
                else:
                    case_id = reminder.case_id

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

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

                # Start the new session
                try:
                    if current_event.callback_timeout_intervals:
                        if handler.submit_partial_forms:
                            expire_after = sum(
                                current_event.callback_timeout_intervals)
                            reminder_intervals = current_event.callback_timeout_intervals[:
                                                                                          -1]
                        else:
                            expire_after = SQLXFormsSession.MAX_SESSION_LENGTH
                            reminder_intervals = current_event.callback_timeout_intervals

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

                    session, responses = start_session(
                        SQLXFormsSession.create_session_object(
                            reminder.domain,
                            recipient,
                            verified_number.phone_number
                            if verified_number else unverified_number,
                            app,
                            form,
                            expire_after=expire_after,
                            reminder_intervals=reminder_intervals,
                            submit_partially_completed_forms=
                            submit_partially_completed_forms,
                            include_case_updates_in_partial_submissions=
                            include_case_updates_in_partial_submissions),
                        reminder.domain,
                        recipient,
                        app,
                        module,
                        form,
                        case_id,
                        case_for_case_submission=handler.
                        force_surveys_to_use_triggered_case)
                except TouchformsError as e:
                    human_readable_message = get_formplayer_exception(
                        reminder.domain, e)

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

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

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

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

            logged_subevent.completed()
Example #36
0
    """
    from corehq.apps.reminders.models import (
        CaseReminderHandler,
        CaseReminderEvent,
        ON_DATETIME,
        EVENT_AS_OFFSET,
        REMINDER_TYPE_DEFAULT,
        REMINDER_TYPE_KEYWORD_INITIATED,
        METHOD_SMS,
        METHOD_SMS_SURVEY,
        RECIPIENT_CASE,
        RECIPIENT_USER,
        RECIPIENT_SURVEY_SAMPLE,
        RECIPIENT_USER_GROUP,
    )
    if is_commcarecase(contact):
        recipient = RECIPIENT_CASE
    elif isinstance(contact, CommCareCaseGroup):
        recipient = RECIPIENT_SURVEY_SAMPLE
    elif isinstance(contact, CommCareUser):
        recipient = RECIPIENT_USER
    elif isinstance(contact, Group):
        recipient = RECIPIENT_USER_GROUP
    else:
        raise Exception("Unsupported contact type for %s" % contact._id)

    reminder_type = reminder_type or REMINDER_TYPE_DEFAULT
    if recipient == RECIPIENT_CASE:
        case_id = contact.case_id
    elif case is not None:
        case_id = case.case_id
Example #37
0
    def send(self, recipient, logged_event, phone_entry=None):
        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 = (
            phone_entry or
            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 self.get_critical_section(recipient):
            # 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()
Example #38
0
def start_session(session, domain, contact, app, module, form, case_id=None, yield_responses=False,
                  case_for_case_submission=False):
    """
    Starts a session in touchforms and saves the record in the database.
    
    Returns a tuple containing the session object and the (text-only) 
    list of generated questions/responses based on the form.
    
    Special params:
    yield_responses - If True, the list of xforms responses is returned, otherwise the text prompt for each is returned
    session_type - XFORMS_SESSION_SMS or XFORMS_SESSION_IVR
    case_for_case_submission - True if this is a submission that a case is making to alter another related case. For example, if a parent case is filling out
        an SMS survey which will update its child case, this should be True.
    """
    # NOTE: this call assumes that "contact" will expose three
    # properties: .raw_username, .get_id, and .get_language_code
    session_data = CaseSessionDataHelper(domain, contact, case_id, app, form).get_session_data(COMMCONNECT_DEVICE_ID)

    # since the API user is a superuser, force touchforms to query only
    # the contact's cases by specifying it as an additional filter
    if is_commcarecase(contact) and form.requires_case():
        session_data["additional_filters"] = {
            "case_id": case_id,
            "footprint": "true" if form.uses_parent_case() else "false",
        }
    elif isinstance(contact, CouchUser):
        session_data["additional_filters"] = {
            "user_id": contact.get_id,
            "footprint": "true"
        }

    kwargs = {}
    if is_commcarecase(contact):
        kwargs['restore_as_case_id'] = contact.case_id
    else:
        kwargs['restore_as'] = contact.raw_username

    if app and form:
        session_data.update(get_cloudcare_session_data(domain, form, contact))

    language = contact.get_language_code()
    config = XFormsConfig(form_content=form.render_xform(),
                          language=language,
                          session_data=session_data,
                          domain=domain,
                          **kwargs)

    session_start_info = tfsms.start_session(config)
    session.session_id = session_start_info.session_id
    session.save()
    responses = session_start_info.first_responses

    if len(responses) > 0 and responses[0].status == 'http-error':
        session.mark_completed(False)
        session.save()
        raise TouchformsError('Cannot connect to touchforms.')

    # Prevent future update conflicts by getting the session again from the db
    # since the session could have been updated separately in the first_responses call
    session = SQLXFormsSession.objects.get(pk=session.pk)
    if yield_responses:
        return (session, responses)
    else:
        return (session, _responses_to_text(responses))
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers,
                          logged_event):
    if reminder.callback_try_count > 0:
        # Handle timeouts
        if handler.submit_partial_forms and (
                reminder.callback_try_count == len(
                    reminder.current_event.callback_timeout_intervals)):
            # Submit partial form completions
            for session_id in reminder.xforms_session_ids:
                submit_unfinished_form(session_id,
                                       handler.include_case_side_effects)
        else:
            # Resend current question
            for session_id in reminder.xforms_session_ids:
                session = get_session_by_session_id(session_id)
                if session.end_time is None:
                    vn = verified_numbers.get(session.connection_id)
                    if vn is not None:
                        metadata = MessageMetadata(
                            workflow=get_workflow(handler),
                            reminder_id=reminder._id,
                            xforms_session_couch_id=session._id,
                        )
                        resp = current_question(session_id)
                        send_sms_to_verified_number(
                            vn,
                            resp.event.text_prompt,
                            metadata,
                            logged_subevent=session.related_subevent)
    else:
        reminder.xforms_session_ids = []
        domain_obj = Domain.get_by_name(reminder.domain, strict=True)

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

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

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

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

            key = "start-sms-survey-for-contact-%s" % recipient.get_id
            with CriticalSection([key], timeout=60):
                # Get the case to submit the form against, if any
                if (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:
                    session, responses = start_session(
                        reminder.domain,
                        recipient,
                        app,
                        module,
                        form,
                        case_id,
                        case_for_case_submission=handler.
                        force_surveys_to_use_triggered_case)
                except TouchformsError as e:
                    human_readable_message = e.response_data.get(
                        'human_readable_message', None)

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

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

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

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

            logged_subevent.completed()
Example #40
0
    def send(self, recipient, logged_event, phone_entry=None):
        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 = (phone_entry
                                 or 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 self.get_critical_section(recipient):
            # 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()
Example #41
0
 def test_case_recipient(self):
     with create_case(self.domain, 'person') as case:
         instance = CaseTimedScheduleInstance(domain=self.domain, case_id=case.case_id, recipient_type='Self')
         self.assertTrue(is_commcarecase(instance.recipient))
         self.assertEqual(instance.recipient.case_id, case.case_id)
Example #42
0
def create_immediate_reminder(contact, content_type, reminder_type=None,
        message=None, form_unique_id=None, case=None, logged_event=None):
    """
    contact - the contact to send to
    content_type - METHOD_SMS or METHOD_SMS_SURVEY (see corehq.apps.reminders.models)
    reminder_type - either REMINDER_TYPE_DEFAULT, REMINDER_TYPE_ONE_TIME, or REMINDER_TYPE_KEYWORD_INITIATED
    message - the message to send if content_type == METHOD_SMS
    form_unique_id - the form_unique_id of the form to send if content_type == METHOD_SMS_SURVEY
    case - the case that is associated with this reminder (so that you can embed case properties into the message)
    logged_event - if this reminder is being created as a subevent of a
        MessagingEvent, this is the MessagingEvent
    """
    from corehq.apps.reminders.models import (
        CaseReminderHandler,
        CaseReminderEvent,
        ON_DATETIME,
        EVENT_AS_OFFSET,
        REMINDER_TYPE_DEFAULT,
        REMINDER_TYPE_KEYWORD_INITIATED,
        METHOD_SMS,
        METHOD_SMS_SURVEY,
        RECIPIENT_CASE,
        RECIPIENT_USER,
        RECIPIENT_SURVEY_SAMPLE,
        RECIPIENT_USER_GROUP,
    )
    if is_commcarecase(contact):
        recipient = RECIPIENT_CASE
    elif isinstance(contact, CommCareCaseGroup):
        recipient = RECIPIENT_SURVEY_SAMPLE
    elif isinstance(contact, CommCareUser):
        recipient = RECIPIENT_USER
    elif isinstance(contact, Group):
        recipient = RECIPIENT_USER_GROUP
    else:
        raise Exception("Unsupported contact type for %s" % contact._id)

    reminder_type = reminder_type or REMINDER_TYPE_DEFAULT
    if recipient == RECIPIENT_CASE:
        case_id = contact.case_id
    elif case is not None:
        case_id = case.case_id
    else:
        case_id = None

    handler = CaseReminderHandler(
        domain=contact.domain,
        reminder_type=reminder_type,
        nickname="One-time Reminder",
        default_lang="xx",
        method=content_type,
        recipient=recipient,
        start_condition_type=ON_DATETIME,
        start_datetime=datetime.utcnow(),
        start_offset=0,
        events = [CaseReminderEvent(
            day_num=0,
            fire_time=time(0, 0),
            form_unique_id=form_unique_id if content_type == METHOD_SMS_SURVEY else None,
            message={'xx': message} if content_type == METHOD_SMS else {},
            callback_timeout_intervals = [],
        )],
        schedule_length=1,
        event_interpretation=EVENT_AS_OFFSET,
        max_iteration_count=1,
        case_id=case_id,
        user_id=contact.get_id if recipient == RECIPIENT_USER else None,
        sample_id=contact.get_id if recipient == RECIPIENT_SURVEY_SAMPLE else None,
        user_group_id=contact.get_id if recipient == RECIPIENT_USER_GROUP else None,
        messaging_event_id=logged_event.pk if logged_event else None,
    )
    handler.save(send_immediately=True)
Example #43
0
def process_survey_keyword_actions(verified_number, survey_keyword, text, msg):
    sender = verified_number.owner
    case = None
    args = split_args(text, survey_keyword)

    logged_event = MessagingEvent.create_from_keyword(survey_keyword, sender)

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

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

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

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

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

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

        if contact is None:
            continue

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

            recipient_is_sender = survey_keyword_action.recipient == KeywordAction.RECIPIENT_SENDER

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

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

        elif survey_keyword_action.action == KeywordAction.ACTION_STRUCTURED_SMS:
            res = handle_structured_sms(survey_keyword, survey_keyword_action,
                sender, verified_number, text, send_response=True, msg=msg,
                case=case, text_args=args, logged_event=logged_event)
            if not res:
                # If the structured sms processing wasn't successful, don't
                # process any of the other actions
                return
    logged_event.completed()
Example #44
0
 def test_case_owner(self):
     with create_test_case(self.domain, 'participant', 'test') as case:
         number = PhoneNumber(owner_doc_type='CommCareCase', owner_id=case.case_id)
         owner = number.owner
         self.assertTrue(is_commcarecase(owner))
         self.assertEqual(owner.case_id, case.case_id)
Example #45
0
 def recipient_is_an_individual_contact(recipient):
     return (
         isinstance(recipient, (CommCareUser, WebUser)) or
         is_commcarecase(recipient)
     )
Example #46
0
def process_survey_keyword_actions(verified_number, survey_keyword, text, msg):
    sender = verified_number.owner
    case = None
    args = split_args(text, survey_keyword)

    logged_event = MessagingEvent.create_from_keyword(survey_keyword, sender)

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

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

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

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

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

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

        if contact is None:
            continue

        if survey_keyword_action.action == METHOD_SMS:
            create_immediate_reminder(contact, METHOD_SMS, 
                reminder_type=REMINDER_TYPE_KEYWORD_INITIATED,
                message=survey_keyword_action.message_content,
                case=case, logged_event=logged_event)
        elif survey_keyword_action.action == METHOD_SMS_SURVEY:
            create_immediate_reminder(contact, METHOD_SMS_SURVEY,
                reminder_type=REMINDER_TYPE_KEYWORD_INITIATED,
                form_unique_id=survey_keyword_action.form_unique_id,
                case=case, logged_event=logged_event)
        elif survey_keyword_action.action == METHOD_STRUCTURED_SMS:
            res = handle_structured_sms(survey_keyword, survey_keyword_action,
                sender, verified_number, text, send_response=True, msg=msg,
                case=case, text_args=args, logged_event=logged_event)
            if not res:
                # If the structured sms processing wasn't successful, don't
                # process any of the other actions
                return
    logged_event.completed()