def start_smsforms_session(self, domain, recipient, case_id, phone_entry_or_number, logged_subevent, workflow, app, module, form): # Close all currently open sessions SQLXFormsSession.close_all_open_sms_sessions(domain, recipient.get_id) # Start the new session try: session, responses = start_session( SQLXFormsSession.create_session_object( domain, recipient, (phone_entry_or_number.phone_number if isinstance(phone_entry_or_number, PhoneNumber) else phone_entry_or_number), app, form, expire_after=self.expire_after, reminder_intervals=self.reminder_intervals, submit_partially_completed_forms=self.submit_partially_completed_forms, include_case_updates_in_partial_submissions=self.include_case_updates_in_partial_submissions ), domain, recipient, app, module, form, case_id, yield_responses=True ) except TouchformsError as e: logged_subevent.error( MessagingEvent.ERROR_TOUCHFORMS_ERROR, additional_error_text=get_formplayer_exception(domain, e) ) if touchforms_error_is_config_error(domain, e): # Don't reraise the exception because this means there are configuration # issues with the form that need to be fixed. The error is logged in the # above lines. return None, None # Reraise the exception so that the framework retries it again later raise except: logged_subevent.error(MessagingEvent.ERROR_TOUCHFORMS_ERROR) # Reraise the exception so that the framework retries it again later raise session.workflow = workflow session.save() return session, responses
def test_get_open_sms_session_multiple_results(self): domain = uuid.uuid4().hex contact = uuid.uuid4().hex for i in range(3): _make_session( domain=domain, connection_id=contact, end_time=None, session_type=XFORMS_SESSION_SMS, ) with self.assertRaises(MultipleResultsFound): SQLXFormsSession.get_open_sms_session(domain, contact)
def start_smsforms_session(self, domain, recipient, case_id, phone_entry_or_number, logged_subevent, workflow, app, module, form): # Close all currently open sessions SQLXFormsSession.close_all_open_sms_sessions(domain, recipient.get_id) # Start the new session try: session, responses = start_session( SQLXFormsSession.create_session_object( domain, recipient, (phone_entry_or_number.phone_number if isinstance(phone_entry_or_number, PhoneNumber) else phone_entry_or_number), app, form, expire_after=self.expire_after, reminder_intervals=self.reminder_intervals, submit_partially_completed_forms=self.submit_partially_completed_forms, include_case_updates_in_partial_submissions=self.include_case_updates_in_partial_submissions ), domain, recipient, app, module, form, case_id, ) except TouchformsError as e: logged_subevent.error( MessagingEvent.ERROR_TOUCHFORMS_ERROR, additional_error_text=get_formplayer_exception(domain, e) ) if touchforms_error_is_config_error(domain, e): # Don't reraise the exception because this means there are configuration # issues with the form that need to be fixed. The error is logged in the # above lines. return None, None # Reraise the exception so that the framework retries it again later raise except: logged_subevent.error(MessagingEvent.ERROR_TOUCHFORMS_ERROR) # Reraise the exception so that the framework retries it again later raise session.workflow = workflow session.save() return session, responses
def test_get_single_open_session(self): properties = _arbitrary_session_properties( end_time=None, session_is_open=True, session_type=XFORMS_SESSION_SMS, ) session = SQLXFormsSession(**properties) session.save() (mult, session) = get_single_open_session_or_close_multiple( session.domain, session.connection_id) self.assertEqual(False, mult) [session_back] = SQLXFormsSession.get_all_open_sms_sessions( session.domain, session.connection_id) self.assertEqual(session._id, session_back.couch_id)
def test_get_and_close_all_open_sessions(self): domain = uuid.uuid4().hex contact = uuid.uuid4().hex for i in range(3): _make_session( domain=domain, connection_id=contact, end_time=None, session_type=XFORMS_SESSION_SMS, ) sql_sessions = SQLXFormsSession.get_all_open_sms_sessions(domain, contact) self.assertEqual(3, len(sql_sessions)) SQLXFormsSession.close_all_open_sms_sessions(domain, contact) self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
def test_get_single_open_session(self): properties = _arbitrary_session_properties( end_time=None, session_type=XFORMS_SESSION_SMS, ) session = SQLXFormsSession(**properties) session.save() (mult, session) = get_single_open_session_or_close_multiple( session.domain, session.connection_id ) self.assertEqual(False, mult) [session_back] = SQLXFormsSession.get_all_open_sms_sessions( session.domain, session.connection_id ) self.assertEqual(session._id, session_back.couch_id)
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) # 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 sender.doc_type == "CommCareCase": case = sender args = args[1:]
def setUpClass(cls): super().setUpClass() cls.domain = Domain(name=uuid.uuid4().hex) cls.domain.save() cls.number = PhoneNumber(domain=cls.domain.name, owner_doc_type='CommCareCase', owner_id='fake-owner-id1', phone_number='01112223333', backend_id=None, ivr_backend_id=None, verified=True, is_two_way=True, pending_verification=False, contact_last_modified=datetime.utcnow()) cls.number.save() cls.session = SQLXFormsSession.create_session_object( cls.domain.name, Mock(get_id=cls.number.owner_id), cls.number.phone_number, Mock(get_id='app_id'), Mock(xmlns='xmlns'), expire_after=24 * 60, ) cls.session.save() cls.backend = SQLTestSMSBackend.objects.create( name='BACKEND', domain=cls.domain, is_global=False, hq_api_id=SQLTestSMSBackend.get_api_id())
def setUpClass(cls): super().setUpClass() cls.session = SQLXFormsSession(session_id='abc123', domain='test-domain', user_id='user_id') cls.formplayer_interface = FormplayerInterface(cls.session.session_id, cls.session.domain)
def test_move_to_next_action_with_fast_forwarding(self, utcnow_mock): utcnow_mock.return_value = datetime(2018, 1, 1, 0, 0) session = SQLXFormsSession.create_session_object( 'test', Mock(get_id='contact_id'), '+9990001', Mock(get_id='app_id'), Mock(xmlns='xmlns'), expire_after=24 * 60, reminder_intervals=[30, 60]) self.assertTrue(session.session_is_open) self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0)) self.assertIsNone(session.end_time) self.assertEqual(session.current_action_due, datetime(2018, 1, 1, 0, 30)) self.assertTrue(session.current_action_is_a_reminder) utcnow_mock.return_value = datetime(2018, 1, 3, 0, 0) session.move_to_next_action() self.assertTrue(session.session_is_open) self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0)) self.assertIsNone(session.end_time) self.assertEqual(session.current_action_due, datetime(2018, 1, 2, 0, 0)) self.assertFalse(session.current_action_is_a_reminder)
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()
def test_get_by_session_id(self): session_id = uuid.uuid4().hex sql_session = SQLXFormsSession.objects.create( session_id=session_id, start_time=datetime.utcnow(), modified_time=datetime.utcnow(), ) self.assertEqual(sql_session.pk, SQLXFormsSession.by_session_id(session_id).pk)
def form_session_handler(v, text, msg): """ The form session handler will use the inbound text to answer the next question in the open SQLXformsSession for the associated contact. If no session is open, the handler passes. If multiple sessions are open, they are all closed and an error message is displayed to the user. """ with critical_section_for_smsforms_sessions(v.owner_id): if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(v.domain): channel = get_channel_for_contact(v.owner_id, v.phone_number) running_session_info = XFormsSessionSynchronization.get_running_session_info_for_channel( channel) if running_session_info.session_id: session = SQLXFormsSession.by_session_id( running_session_info.session_id) if not session.session_is_open: # This should never happen. But if it does we should set the channel free # and act like there was no available session notify_error( "The supposedly running session was not open and was released. " 'No known way for this to happen, so worth investigating.' ) XFormsSessionSynchronization.clear_stale_channel_claim( channel) session = None else: session = None else: multiple, session = get_single_open_session_or_close_multiple( v.domain, v.owner_id) if multiple: send_sms_to_verified_number( v, get_message(MSG_MULTIPLE_SESSIONS, v)) return True if session: session.phone_number = v.phone_number session.modified_time = datetime.utcnow() session.save() # Metadata to be applied to the inbound message inbound_metadata = MessageMetadata( workflow=session.workflow, reminder_id=session.reminder_id, xforms_session_couch_id=session._id, ) add_msg_tags(msg, inbound_metadata) try: answer_next_question(v, text, msg, session) except Exception: # Catch any touchforms errors log_sms_exception(msg) send_sms_to_verified_number( v, get_message(MSG_TOUCHFORMS_DOWN, v)) return True else: return False
def test_get_all_open_sessions_wrong_type(self): domain = uuid.uuid4().hex contact = uuid.uuid4().hex _make_session( domain=domain, connection_id=contact, end_time=None, session_type=XFORMS_SESSION_IVR, ) self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
def test_get_all_open_sessions_already_ended(self): domain = uuid.uuid4().hex contact = uuid.uuid4().hex _make_session( domain=domain, connection_id=contact, end_time=datetime.utcnow(), session_type=XFORMS_SESSION_SMS, ) self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
def user_id(self): from corehq.apps.smsforms.models import SQLXFormsSession if self._user_id is Ellipsis: session = SQLXFormsSession.by_session_id(self.session_id) if session: self._user_id = session.user_id else: self._user_id = None return self._user_id
def test_get_all_open_sessions_contact_mismatch(self): domain = uuid.uuid4().hex contact = uuid.uuid4().hex _make_session( domain=domain, connection_id='wrong', end_time=None, session_is_open=True, session_type=XFORMS_SESSION_SMS, ) self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
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:]
def _make_session(self, number): session = SQLXFormsSession.create_session_object( self.domain_name, Mock(get_id=number.owner_id), number.phone_number, Mock(get_id='app_id'), Mock(xmlns='xmlns'), expire_after=24 * 60, ) session.session_id = uuid.uuid4().hex session.save() return session
def test_get_open_sms_session_one_result(self): domain = uuid.uuid4().hex contact = uuid.uuid4().hex new_session = _make_session( domain=domain, connection_id=contact, end_time=None, session_type=XFORMS_SESSION_SMS, ) session = SQLXFormsSession.get_open_sms_session(domain, contact) self.assertEqual(new_session.session_id, session.session_id)
def sms_keyword_handler(v, text, msg): text = text.strip() if text == "": return False sessions = SQLXFormsSession.get_all_open_sms_sessions(v.domain, v.owner_id) text_words = text.upper().split() if text.startswith("#"): return handle_global_keywords(v, text, msg, text_words, sessions) else: return handle_domain_keywords(v, text, msg, text_words, sessions)
def create_tasks(self): survey_sessions_due_for_action = self.get_survey_sessions_due_for_action( ) all_open_session_ids = self.get_open_session_ids() for domain, connection_id, session_id, current_action_due, phone_number in survey_sessions_due_for_action: if skip_domain(domain): continue if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(domain): fake_session = SQLXFormsSession( session_id=session_id, connection_id=connection_id, phone_number=phone_number, ) if not XFormsSessionSynchronization.channel_is_available_for_session( fake_session): running_session_info = XFormsSessionSynchronization.get_running_session_info_for_channel( fake_session.get_channel()) # First confirm the supposedly running session is even open # and if it's not (should be exceedingly rare) release it and act like it wasn't there if running_session_info.session_id \ and running_session_info.session_id not in all_open_session_ids: notify_error( "The supposedly running session was not open and was released. " "No known way for this to happen, so worth investigating.", details={ 'running_session_info': running_session_info }) XFormsSessionSynchronization.clear_stale_channel_claim( fake_session.get_channel()) # This is the 99% case: there's a running session for the channel # so leave this session/action in the queue for later and move on to the next one else: continue enqueue_lock = self.get_enqueue_lock(session_id, current_action_due) if enqueue_lock.acquire(blocking=False): handle_due_survey_action.delay(domain, connection_id, session_id)
def handle_sms_form_complete(sender, session_id, form, **kwargs): from corehq.apps.smsforms.models import SQLXFormsSession session = SQLXFormsSession.by_session_id(session_id) if session: resp = submit_form_locally(form, session.domain, app_id=session.app_id) xform_id = resp['X-CommCareHQ-FormID'] session.end(completed=True) session.submission_id = xform_id session.save() xform = XFormInstance.get(xform_id) xform.survey_incentive = session.survey_incentive xform.save()
def test_get_all_open_sessions_contact_mismatch(self): domain = uuid.uuid4().hex contact = uuid.uuid4().hex _make_session( domain=domain, connection_id='wrong', end_time=None, session_is_open=True, session_type=XFORMS_SESSION_SMS, ) self.assertEqual( 0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
def start_session_for_structured_sms(domain, contact, phone_number, app, module, form, case_id, keyword, logged_subevent=None): """ Returns (session, responses, error, error_code) """ try: session, responses = start_session( SQLXFormsSession.create_session_object( domain, contact, phone_number.phone_number, app, form, expire_after=0, ), domain, contact, app, module, form, case_id=case_id, yield_responses=True) if logged_subevent: logged_subevent.xforms_session_id = session.pk logged_subevent.save() return (session, responses, False, None) except TouchformsError as e: human_readable_message = e.response_data.get('human_readable_message', None) logged_subevent.error(MessagingEvent.ERROR_TOUCHFORMS_ERROR, additional_error_text=human_readable_message) if touchforms_error_is_config_error(e): error_code = MSG_FORM_ERROR else: notify_exception(None, message=('Could not process structured sms for' 'contact %s, domain %s, keyword %s' % (contact.get_id, domain, keyword))) error_code = MSG_TOUCHFORMS_ERROR return (None, None, True, error_code)
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()
def test_get_single_open_session_close_multiple(self): domain = uuid.uuid4().hex contact = uuid.uuid4().hex for i in range(3): _make_session( domain=domain, connection_id=contact, end_time=None, session_type=XFORMS_SESSION_SMS, ) (mult, session) = get_single_open_session_or_close_multiple(domain, contact) self.assertEqual(True, mult) self.assertEqual(None, session) self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
def test_session_is_stale(self, utcnow_mock_1, utcnow_mock_2): utcnow_mock_2.return_value = datetime(2018, 1, 1, 0, 0) session = SQLXFormsSession.create_session_object( 'test', Mock(get_id='contact_id'), '+9990001', Mock(get_id='app_id'), Mock(xmlns='xmlns'), expire_after=24 * 60, reminder_intervals=[30, 60], submit_partially_completed_forms=True, ) session.save() self.addCleanup(session.delete) utcnow_mock_1.return_value = datetime(2018, 1, 14, 0, 0) self.assertFalse(session_is_stale(session)) utcnow_mock_1.return_value = datetime(2018, 1, 16, 0, 0) self.assertTrue(session_is_stale(session)) handle_due_survey_action('test', 'contact_id', session.session_id) session = SQLXFormsSession.by_session_id(session.session_id) self.assertFalse(session.session_is_open)
def _start_session(self, yield_responses=False): if not self.recipient: raise Exception("Set recipient") return start_session(SQLXFormsSession.create_session_object( self.domain, self.recipient, self.phone_number, self.app, self.basic_form, ), self.domain, self.recipient, self.app, self.basic_form, yield_responses=yield_responses)
def sms_keyword_handler(verified_number, text, msg): with critical_section_for_smsforms_sessions(verified_number.owner_id): text = text.strip() if text == "": return False sessions = SQLXFormsSession.get_all_open_sms_sessions( verified_number.domain, verified_number.owner_id) text_words = text.upper().split() if text.startswith("#"): return handle_global_keywords(verified_number, text, msg, text_words, sessions) else: return handle_domain_keywords(verified_number, text, msg, text_words, sessions)
def close_session(self, contact_id, session_id): with critical_section_for_smsforms_sessions(contact_id): session = SQLXFormsSession.by_session_id(session_id) try: session.close(force=False) except TouchformsError as e: try: self.retry(exc=e) except TouchformsError as e: raise e finally: # Eventually the session needs to get closed session.mark_completed(False) session.save() return session.save()
def get_single_open_session_or_close_multiple(domain, contact_id): """ Retrieves the current open SQLXFormsSession for the given contact. If multiple sessions are open, it closes all of them and returns None for the session. The return value is a tuple of (multiple, session), where multiple is True if there were multiple sessions, and session is the session if there was a single open session available. """ sessions = SQLXFormsSession.get_all_open_sms_sessions(domain, contact_id) count = sessions.count() if count > 1: for session in sessions: session.end(False) session.save() return (True, None) session = sessions[0] if count == 1 else None return (False, session)
def get_single_open_session_or_close_multiple(domain, contact_id): """ Retrieves the current open SQLXFormsSession for the given contact. If multiple sessions are open, it closes all of them and returns None for the session. The return value is a tuple of (multiple, session), where multiple is True if there were multiple sessions, and session is the session if there was a single open session available. """ sessions = SQLXFormsSession.get_all_open_sms_sessions(domain, contact_id) count = sessions.count() if count > 1: for session in sessions: session.mark_completed(False) session.save() return (True, None) session = sessions[0] if count == 1 else None return (False, session)
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()
def start_session_for_structured_sms(domain, contact, phone_number, app, module, form, case_id, keyword, logged_subevent=None): """ Returns (session, responses, error, error_code) """ try: session, responses = start_session( SQLXFormsSession.create_session_object( domain, contact, phone_number.phone_number, app, form, expire_after=0, ), domain, contact, app, module, form, case_id=case_id, yield_responses=True ) if logged_subevent: logged_subevent.xforms_session_id = session.pk logged_subevent.save() return (session, responses, False, None) except TouchformsError as e: human_readable_message = get_formplayer_exception(domain, e) logged_subevent.error(MessagingEvent.ERROR_TOUCHFORMS_ERROR, additional_error_text=human_readable_message) if touchforms_error_is_config_error(domain, e): error_code = MSG_FORM_ERROR else: notify_exception(None, message=('Could not process structured sms for' 'contact %s, domain %s, keyword %s' % (contact.get_id, domain, keyword))) error_code = MSG_TOUCHFORMS_ERROR return (None, None, True, error_code)
def test_move_to_next_action_with_reminders(self, utcnow_mock): utcnow_mock.return_value = datetime(2018, 1, 1, 0, 0) session = SQLXFormsSession.create_session_object( 'test', Mock(get_id='contact_id'), '+9990001', Mock(get_id='app_id'), Mock(xmlns='xmlns'), expire_after=24 * 60, reminder_intervals=[30, 60] ) self.assertTrue(session.session_is_open) self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0)) self.assertIsNone(session.end_time) self.assertEqual(session.current_action_due, datetime(2018, 1, 1, 0, 30)) self.assertTrue(session.current_action_is_a_reminder) utcnow_mock.return_value = datetime(2018, 1, 1, 0, 31) session.move_to_next_action() self.assertTrue(session.session_is_open) self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0)) self.assertIsNone(session.end_time) self.assertEqual(session.current_action_due, datetime(2018, 1, 1, 1, 30)) self.assertTrue(session.current_action_is_a_reminder) utcnow_mock.return_value = datetime(2018, 1, 1, 1, 31) session.move_to_next_action() self.assertTrue(session.session_is_open) self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0)) self.assertIsNone(session.end_time) self.assertEqual(session.current_action_due, datetime(2018, 1, 2, 0, 0)) self.assertFalse(session.current_action_is_a_reminder) utcnow_mock.return_value = datetime(2018, 1, 2, 0, 1) session.move_to_next_action() self.assertTrue(session.session_is_open) self.assertEqual(session.start_time, datetime(2018, 1, 1, 0, 0)) self.assertIsNone(session.end_time) self.assertEqual(session.current_action_due, datetime(2018, 1, 2, 0, 0)) self.assertFalse(session.current_action_is_a_reminder)
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 ) subevent.completed() 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 sender.doc_type == "CommCareCase": case = sender args = args[1:] elif sender.doc_type == "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.get_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 sender.doc_type == "CommCareCase": 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()
def global_keyword_stop(v, text, msg, text_words, open_sessions): SQLXFormsSession.close_all_open_sms_sessions(v.domain, v.owner_id) return True
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers, logged_event): if reminder.callback_try_count > 0: # Handle timeouts if handler.submit_partial_forms and ( reminder.callback_try_count == len(reminder.current_event.callback_timeout_intervals) ): # Submit partial form completions for session_id in reminder.xforms_session_ids: submit_unfinished_form(session_id, handler.include_case_side_effects) else: # Resend current question for session_id in reminder.xforms_session_ids: session = get_session_by_session_id(session_id) if session.end_time is None: vn = VerifiedNumber.view( "sms/verified_number_by_owner_id", key=session.connection_id, include_docs=True ).first() if vn is not None: metadata = MessageMetadata( workflow=get_workflow(handler), reminder_id=reminder._id, xforms_session_couch_id=session._id, ) resp = current_question(session_id) send_sms_to_verified_number(vn, resp.event.text_prompt, metadata) else: reminder.xforms_session_ids = [] domain_obj = Domain.get_by_name(reminder.domain, strict=True) # Get the app, module, and form try: form_unique_id = reminder.current_event.form_unique_id form = Form.get_form(form_unique_id) app = form.get_app() module = form.get_module() except Exception: logged_event.error(MessagingEvent.ERROR_CANNOT_FIND_FORM) return # Start a touchforms session for each recipient for recipient in recipients: logged_subevent = logged_event.create_subevent(handler, reminder, recipient) verified_number, unverified_number = get_recipient_phone_number(reminder, recipient, verified_numbers) no_verified_number = verified_number is None cant_use_unverified_number = ( unverified_number is None or not domain_obj.send_to_duplicated_case_numbers or form_requires_input(form) ) if no_verified_number and cant_use_unverified_number: logged_subevent.error(MessagingEvent.ERROR_NO_TWO_WAY_PHONE_NUMBER) continue key = "start-sms-survey-for-contact-%s" % recipient.get_id with CriticalSection([key], timeout=60): # Get the case to submit the form against, if any if isinstance(recipient, CommCareCase) and not handler.force_surveys_to_use_triggered_case: case_id = recipient.get_id else: case_id = reminder.case_id if form.requires_case() and not case_id: logged_subevent.error(MessagingEvent.ERROR_NO_CASE_GIVEN) continue # Close all currently open sessions SQLXFormsSession.close_all_open_sms_sessions(reminder.domain, recipient.get_id) # Start the new session try: session, responses = start_session( reminder.domain, recipient, app, module, form, case_id, case_for_case_submission=handler.force_surveys_to_use_triggered_case, ) except TouchformsError as e: human_readable_message = e.response_data.get("human_readable_message", None) logged_subevent.error( MessagingEvent.ERROR_TOUCHFORMS_ERROR, additional_error_text=human_readable_message ) if touchforms_error_is_config_error(e): # Don't reraise the exception because this means there are configuration # issues with the form that need to be fixed continue else: # Reraise the exception so that the framework retries it again later raise except Exception as e: logged_subevent.error(MessagingEvent.ERROR_TOUCHFORMS_ERROR) # Reraise the exception so that the framework retries it again later raise session.survey_incentive = handler.survey_incentive session.workflow = get_workflow(handler) session.reminder_id = reminder._id session.save() reminder.xforms_session_ids.append(session.session_id) logged_subevent.xforms_session = session logged_subevent.save() # Send out first message if len(responses) > 0: message = format_message_list(responses) metadata = MessageMetadata( workflow=get_workflow(handler), reminder_id=reminder._id, xforms_session_couch_id=session._id ) if verified_number: send_sms_to_verified_number(verified_number, message, metadata) else: send_sms(reminder.domain, recipient, unverified_number, message, metadata) logged_subevent.completed()
def test_get_open_sms_session_no_results(self): self.assertEqual(None, SQLXFormsSession.get_open_sms_session(uuid.uuid4().hex, uuid.uuid4().hex))
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers): if reminder.callback_try_count > 0: # Handle timeouts if handler.submit_partial_forms and (reminder.callback_try_count == len(reminder.current_event.callback_timeout_intervals)): # Submit partial form completions for session_id in reminder.xforms_session_ids: submit_unfinished_form(session_id, handler.include_case_side_effects) else: # Resend current question for session_id in reminder.xforms_session_ids: session = get_session_by_session_id(session_id) if session.end_time is None: vn = VerifiedNumber.view("sms/verified_number_by_owner_id", key=session.connection_id, include_docs=True).first() if vn is not None: metadata = MessageMetadata( workflow=get_workflow(handler), reminder_id=reminder._id, xforms_session_couch_id=session._id, ) resp = current_question(session_id) send_sms_to_verified_number(vn, resp.event.text_prompt, metadata) return True else: reminder.xforms_session_ids = [] # Get the app, module, and form try: form_unique_id = reminder.current_event.form_unique_id form = Form.get_form(form_unique_id) app = form.get_app() module = form.get_module() except Exception: raise_error(reminder, ERROR_FORM) return False # Start a touchforms session for each recipient for recipient in recipients: verified_number, unverified_number = get_recipient_phone_number( reminder, recipient, verified_numbers) domain_obj = Domain.get_by_name(reminder.domain, strict=True) no_verified_number = verified_number is None cant_use_unverified_number = (unverified_number is None or not domain_obj.send_to_duplicated_case_numbers or form_requires_input(form)) if no_verified_number and cant_use_unverified_number: if len(recipients) == 1: raise_error(reminder, ERROR_NO_VERIFIED_NUMBER) return False else: continue key = "start-sms-survey-for-contact-%s" % recipient.get_id with CriticalSection([key], timeout=60): # Close all currently open sessions SQLXFormsSession.close_all_open_sms_sessions(reminder.domain, recipient.get_id) # Start the new session if (isinstance(recipient, CommCareCase) and not handler.force_surveys_to_use_triggered_case): case_id = recipient.get_id else: case_id = reminder.case_id session, responses = start_session(reminder.domain, recipient, app, module, form, case_id, case_for_case_submission= handler.force_surveys_to_use_triggered_case) session.survey_incentive = handler.survey_incentive session.workflow = get_workflow(handler) session.reminder_id = reminder._id session.save() reminder.xforms_session_ids.append(session.session_id) # Send out first message if len(responses) > 0: message = format_message_list(responses) metadata = MessageMetadata( workflow=get_workflow(handler), reminder_id=reminder._id, xforms_session_couch_id=session._id, ) if verified_number: result = send_sms_to_verified_number(verified_number, message, metadata) else: result = send_sms(reminder.domain, recipient, unverified_number, message, metadata) if len(recipients) == 1: return result return True
def _make_session(**kwargs): properties = _arbitrary_session_properties(**kwargs) session = SQLXFormsSession(**properties) session.save() return session
def test_get_by_session_id_not_found(self): self.assertEqual(None, SQLXFormsSession.by_session_id(uuid.uuid4().hex))
def get_open_session(self, contact): return SQLXFormsSession.get_open_sms_session(self.domain, contact._id)
def handle_sms_form_complete(sender, session_id, form, **kwargs): from corehq.apps.smsforms.models import SQLXFormsSession session = SQLXFormsSession.by_session_id(session_id) if session: process_sms_form_complete(session, form)
def test_get_by_session_id(self): sql_session = _make_session() self.assertEqual(sql_session.pk, SQLXFormsSession.by_session_id(sql_session.session_id).pk)
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()