def start_call_session(recipient, call_log_entry, logged_subevent, app, module, form): """ Returns (session, responses, error) """ try: session, responses = start_session(recipient.domain, recipient, app, module, form, call_log_entry.case_id, yield_responses=True, session_type=XFORMS_SESSION_IVR, case_for_case_submission=call_log_entry.case_for_case_submission) if logged_subevent: logged_subevent.xforms_session = session logged_subevent.save() if len(responses) == 0: log_error(MessagingEvent.ERROR_FORM_HAS_NO_QUESTIONS, call_log_entry, logged_subevent) return (session, responses, True) return (session, responses, False) except TouchformsError as e: additional_error_text = e.response_data.get('human_readable_message', None) log_error(MessagingEvent.ERROR_TOUCHFORMS_ERROR, call_log_entry, logged_subevent, additional_error_text=additional_error_text) return (None, None, True)
def start_session_from_keyword(survey_keyword, verified_number): try: form_unique_id = survey_keyword.form_unique_id form = Form.get_form(form_unique_id) app = form.get_app() module = form.get_module() if verified_number.owner_doc_type == "CommCareCase": case_id = verified_number.owner_id else: #TODO: Need a way to choose the case when it's a user that's playing the form case_id = None session, responses = start_session(verified_number.domain, verified_number.owner, app, module, form, case_id) if len(responses) > 0: message = format_message_list(responses) send_sms_to_verified_number(verified_number, message) except Exception: logging.exception( "Exception while starting survey for keyword %s, domain %s" % (survey_keyword.keyword, verified_number.domain))
def start_session_with_error_handling(domain, contact, app, module, form, case_id, keyword, logged_subevent=None): """ Returns (session, responses, error, error_code) """ try: session, responses = start_session(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 start_call_session(recipient, call_log_entry, logged_subevent, app, module, form): """ Returns (session, responses, error) """ try: session, responses = start_session( recipient.domain, recipient, app, module, form, call_log_entry.case_id, yield_responses=True, session_type=XFORMS_SESSION_IVR, case_for_case_submission=call_log_entry.case_for_case_submission) if logged_subevent: logged_subevent.xforms_session = session logged_subevent.save() if len(responses) == 0: log_error(MessagingEvent.ERROR_FORM_HAS_NO_QUESTIONS, call_log_entry, logged_subevent) return (session, responses, True) return (session, responses, False) except TouchformsError as e: additional_error_text = e.response_data.get('human_readable_message', None) log_error(MessagingEvent.ERROR_TOUCHFORMS_ERROR, call_log_entry, logged_subevent, additional_error_text=additional_error_text) return (None, None, True)
def start_session_with_error_handling(domain, contact, app, module, form, case_id, keyword, logged_subevent=None): """ Returns (session, responses, error, error_code) """ try: session, responses = start_session(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._id, domain, keyword))) error_code = MSG_TOUCHFORMS_ERROR return (None, None, True, error_code)
def test_case_integration(self): # load the app with open( os.path.join(os.path.dirname(__file__), "data", "app_with_cases.json")) as f: app_json = json.loads(f.read()) app = import_app(app_json, self.domain) # the first form opens the case session, responses = start_session(self.domain, self.contact, app, app.get_module(0), app.get_module(0).get_form(0)) [answer] = responses self.assertEqual("what's the case name?", answer) q_and_a(self, "some case", "thanks, you're done!", self.domain) def _get_case(session): session = XFormsSession.get(session.get_id) self.assertTrue(session.submission_id) instance = XFormInstance.get(session.submission_id) case_id = instance.xpath("form/case/@case_id") self.assertTrue(case_id) return CommCareCase.get(case_id) # check the case case = _get_case(session) self.assertEqual("some case", case.name) self.assertFalse(case.closed) self.assertFalse(hasattr(case, "feeling")) # the second form updates the case # NOTE: this currently fails for several reasons, the most # notable being that there needs to be a server running configured # to hit the test DB, and that there's no authentication built in session, responses = start_session(self.domain, self.contact, app, app.get_module(0), app.get_module(0).get_form(1), case_id=case.get_id) [answer] = responses self.assertEqual("how you feeling, some case?", answer) q_and_a(self, "groovy", "thanks, you're done!", self.domain)
def test_case_integration(self): # load the app with open(os.path.join(os.path.dirname(__file__), "data", "app_with_cases.json")) as f: app_json = json.loads(f.read()) app = import_app(app_json, self.domain) # the first form opens the case session, responses = start_session(self.domain, self.contact, app, app.get_module(0), app.get_module(0).get_form(0)) [answer] = responses self.assertEqual("what's the case name?", answer) q_and_a(self, "some case", "thanks, you're done!", self.domain) def _get_case(session): session = XFormsSession.get(session.get_id) self.assertTrue(session.submission_id) instance = XFormInstance.get(session.submission_id) case_id = instance.xpath("form/case/@case_id") self.assertTrue(case_id) return CommCareCase.get(case_id) # check the case case = _get_case(session) self.assertEqual("some case", case.name) self.assertFalse(case.closed) self.assertFalse(hasattr(case, "feeling")) # the second form updates the case # NOTE: this currently fails for several reasons, the most # notable being that there needs to be a server running configured # to hit the test DB, and that there's no authentication built in session, responses = start_session(self.domain, self.contact, app, app.get_module(0), app.get_module(0).get_form(1), case_id=case.get_id) [answer] = responses self.assertEqual("how you feeling, some case?", answer) q_and_a(self, "groovy", "thanks, you're done!", self.domain)
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 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 _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 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_basic_form_playing(self): # load the app with open( os.path.join(os.path.dirname(__file__), "data", "demo_app.json")) as f: app_json = json.loads(f.read()) app = import_app(app_json, self.domain) # start form session session, responses = start_session(self.domain, self.contact, app, app.get_module(0), app.get_module(0).get_form(0)) [answer] = responses self.assertEqual("what is your name?", answer) # check state of model self.assertEqual(session.start_time, session.modified_time) self.assertEqual("http://www.commcarehq.org/tests/smsforms", session.form_xmlns) self.assertFalse(session.end_time) self.assertEqual(False, session.completed) self.assertEqual(self.domain, session.domain) self.assertEqual(self.contact.get_id, session.user_id) self.assertEqual(app.get_id, session.app_id) self.assertFalse(session.submission_id) # play through the form, checking answers q_and_a(self, "sms contact", "how old are you, sms contact?", self.domain) q_and_a(self, "29", "what is your gender? 1:male, 2:female", self.domain) q_and_a(self, "2", "thanks for submitting!", self.domain) # check the instance session = XFormsSession.get(session.get_id) self.assertTrue(session.submission_id) instance = XFormInstance.get(session.submission_id) self.assertEqual("sms contact", instance.xpath("form/name")) self.assertEqual("29", instance.xpath("form/age")) self.assertEqual("f", instance.xpath("form/gender")) self.assertEqual(self.domain, instance.domain)
def start_session_from_keyword(survey_keyword, verified_number): try: form_unique_id = survey_keyword.form_unique_id form = Form.get_form(form_unique_id) app = form.get_app() module = form.get_module() if verified_number.owner_doc_type == "CommCareCase": case_id = verified_number.owner_id else: #TODO: Need a way to choose the case when it's a user that's playing the form case_id = None session, responses = start_session(verified_number.domain, verified_number.owner, app, module, form, case_id) if len(responses) > 0: message = format_message_list(responses) send_sms_to_verified_number(verified_number, message) except Exception: logging.exception("Exception while starting survey for keyword %s, domain %s" % (survey_keyword.keyword, verified_number.domain))
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_basic_form_playing(self): # load the app with open(os.path.join(os.path.dirname(__file__), "data", "demo_app.json")) as f: app_json = json.loads(f.read()) app = import_app(app_json, self.domain) # start form session session, responses = start_session(self.domain, self.contact, app, app.get_module(0), app.get_module(0).get_form(0)) [answer] = responses self.assertEqual("what is your name?", answer) # check state of model self.assertEqual(session.start_time, session.modified_time) self.assertEqual("http://www.commcarehq.org/tests/smsforms", session.form_xmlns) self.assertFalse(session.end_time) self.assertEqual(False, session.completed) self.assertEqual(self.domain, session.domain) self.assertEqual(self.contact.get_id, session.user_id) self.assertEqual(app.get_id, session.app_id) self.assertFalse(session.submission_id) # play through the form, checking answers q_and_a(self, "sms contact", "how old are you, sms contact?", self.domain) q_and_a(self, "29", "what is your gender? 1:male, 2:female", self.domain) q_and_a(self, "2", "thanks for submitting!", self.domain) # check the instance session = XFormsSession.get(session.get_id) self.assertTrue(session.submission_id) instance = XFormInstance.get(session.submission_id) self.assertEqual("sms contact", instance.xpath("form/name")) self.assertEqual("29", instance.xpath("form/age")) self.assertEqual("f", instance.xpath("form/gender")) self.assertEqual(self.domain, instance.domain)
def structured_sms_handler(verified_number, text): # Circular Import from corehq.apps.reminders.models import SurveyKeyword, FORM_TYPE_ALL_AT_ONCE text = text.strip() if text == "": return False for survey_keyword in SurveyKeyword.get_all(verified_number.domain): if survey_keyword.form_type == FORM_TYPE_ALL_AT_ONCE: if survey_keyword.delimiter is not None: args = text.split(survey_keyword.delimiter) else: args = text.split() keyword = args[0].strip().upper() if keyword != survey_keyword.keyword.upper(): continue try: error_occurred = False error_msg = "" form_complete = False # Close any open sessions close_open_sessions(verified_number.domain, verified_number.owner_id) # Start the session form = Form.get_form(survey_keyword.form_unique_id) app = form.get_app() module = form.get_module() if verified_number.owner_doc_type == "CommCareCase": case_id = verified_number.owner_id else: #TODO: Need a way to choose the case when it's a user that's playing the form case_id = None session, responses = start_session(verified_number.domain, verified_number.owner, app, module, form, case_id=case_id, yield_responses=True) assert len( responses) > 0, "There should be at least one response." current_question = responses[-1] form_complete = is_form_complete(current_question) if not form_complete: if survey_keyword.use_named_args: # Arguments in the sms are named xpath_answer = {} # Dictionary of {xpath : answer} for answer in args[1:]: answer = answer.strip() answer_upper = answer.upper() if survey_keyword.named_args_separator is not None: # A separator is used for naming arguments; for example, the "=" in "register name=joe age=25" answer_parts = answer.partition( survey_keyword.named_args_separator) if answer_parts[ 1] != survey_keyword.named_args_separator: error_occurred = True error_msg = "ERROR: Expected name and value to be joined by" + ( " '%s'" % survey_keyword.named_args_separator) break else: arg_name = answer_parts[0].upper().strip() xpath = survey_keyword.named_args.get( arg_name, None) if xpath is not None: if xpath in xpath_answer: error_occurred = True error_msg = "ERROR: More than one answer found for" + ( " '%s'" % arg_name) break xpath_answer[xpath] = answer_parts[ 2].strip() else: # Ignore unexpected named arguments pass else: # No separator is used for naming arguments; for example, "update a100 b34 c5" matches = 0 for k, v in survey_keyword.named_args.items(): if answer_upper.startswith(k): matches += 1 if matches > 1: error_occurred = True error_msg = "ERROR: More than one question matches" + ( " '%s'" % answer) break if v in xpath_answer: error_occurred = True error_msg = "ERROR: More than one answer found for" + ( " '%s'" % k) break xpath_answer[v] = answer[ len(k):].strip() if matches == 0: # Ignore unexpected named arguments pass if error_occurred: break # Go through each question in the form, answering only the questions that the sms has answers for while not form_complete and not error_occurred: if current_question.is_error: error_occurred = True error_msg = current_question.text_prompt or "ERROR: Internal server error" break xpath = current_question.event._dict["binding"] if xpath in xpath_answer: valid, answer, _error_msg = validate_answer( current_question.event, xpath_answer[xpath]) if not valid: error_occurred = True error_msg = "ERROR: " + _error_msg break responses = _get_responses( verified_number.domain, verified_number.owner_id, answer, yield_responses=True) else: responses = _get_responses( verified_number.domain, verified_number.owner_id, "", yield_responses=True) current_question = responses[-1] if is_form_complete(current_question): form_complete = True else: # Arguments in the sms are not named; pass each argument to each question in order for answer in args[1:]: if form_complete: # Form is complete, ignore remaining answers break if current_question.is_error: error_occurred = True error_msg = current_question.text_prompt or "ERROR: Internal server error" break valid, answer, _error_msg = validate_answer( current_question.event, answer.strip()) if not valid: error_occurred = True error_msg = "ERROR: " + _error_msg break responses = _get_responses( verified_number.domain, verified_number.owner_id, answer, yield_responses=True) current_question = responses[-1] form_complete = is_form_complete(current_question) # If the form isn't finished yet but we're out of arguments, try to leave each remaining question blank and continue while not form_complete and not error_occurred: responses = _get_responses( verified_number.domain, verified_number.owner_id, "", yield_responses=True) current_question = responses[-1] if current_question.is_error: error_occurred = True error_msg = current_question.text_prompt or "ERROR: Internal server error" if is_form_complete(current_question): form_complete = True except Exception: logging.exception( "Could not process structured sms for verified number %s, domain %s, keyword %s" % (verified_number._id, verified_number.domain, keyword)) error_occurred = True error_msg = "ERROR: Internal server error" if error_occurred: send_sms_to_verified_number(verified_number, error_msg) if error_occurred or not form_complete: session = XFormsSession.get(session._id) session.end(False) session.save() return True return False
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 fire(self, reminder): """ Sends the message associated with the given CaseReminder's current event. reminder The CaseReminder which to fire. return True on success, False on failure """ # Get the proper recipient recipient = reminder.recipient # Retrieve the VerifiedNumber entry for the recipient try: verified_number = recipient.get_verified_number() except Exception: verified_number = None # Get the language of the recipient try: lang = recipient.get_language_code() except Exception: lang = None if reminder.method == "survey": # Close all currently open sessions sessions = XFormsSession.view("smsforms/open_sessions_by_connection", key=[reminder.domain, recipient.get_id], include_docs=True).all() for session in sessions: session.end(False) session.save() # Start the new session 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 as e: print e print "ERROR: Could not load survey form for handler " + reminder.handler_id + ", event " + str(reminder.current_event_sequence_num) return False session, responses = start_session(reminder.domain, recipient, app, module, form, reminder.case_id) # Send out first message if len(responses) > 0: message = format_message_list(responses) if verified_number is not None: return send_sms_to_verified_number(verified_number, message) else: return True else: # If it is a callback reminder and the callback has been received, skip sending the next timeout message if (reminder.method == "callback" or reminder.method == "callback_test") and len(reminder.current_event.callback_timeout_intervals) > 0 and (reminder.callback_try_count > 0): if CallLog.inbound_call_exists(recipient.doc_type, recipient._id, reminder.last_fired): reminder.callback_received = True return True elif len(reminder.current_event.callback_timeout_intervals) == reminder.callback_try_count: # On the last callback timeout, instead of sending the SMS again, log the missed callback event = EventLog( domain = reminder.domain, date = self.get_now(), event_type = MISSED_EXPECTED_CALLBACK ) if verified_number is not None: event.couch_recipient_doc_type = verified_number.owner_doc_type event.couch_recipient = verified_number.owner_id event.save() return True reminder.last_fired = self.get_now() message = reminder.current_event.message.get(lang, reminder.current_event.message[self.default_lang]) message = Message.render(message, case=reminder.case.case_properties()) if reminder.method == "sms" or reminder.method == "callback": if verified_number is not None: return send_sms_to_verified_number(verified_number, message) elif self.recipient == RECIPIENT_USER: # If there is no verified number, but the recipient is a CommCareUser, still try to send it try: phone_number = reminder.user.phone_number except Exception: # If the user has no phone number, we cannot send any SMS return False return send_sms(reminder.domain, reminder.user_id, phone_number, message) else: return False elif reminder.method == "test" or reminder.method == "callback_test": print(message) return True
def incoming(phone_number, backend_module, gateway_session_id, ivr_event, input_data=None): # Look up the call if one already exists call_log_entry = CallLog.view("sms/call_by_session", startkey=[gateway_session_id, {}], endkey=[gateway_session_id], descending=True, include_docs=True, limit=1).one() answer_is_valid = False # This will be set to True if IVR validation passes error_occurred = False # This will be set to False if touchforms validation passes (i.e., no form constraints fail) if call_log_entry is not None and backend_module: if ivr_event == IVR_EVENT_NEW_CALL and call_log_entry.use_precached_first_response: return HttpResponse(call_log_entry.first_response) form = Form.get_form(call_log_entry.form_unique_id) app = form.get_app() module = form.get_module() recipient = call_log_entry.recipient if ivr_event == IVR_EVENT_NEW_CALL: case_id = call_log_entry.case_id case_for_case_submission = call_log_entry.case_for_case_submission session, responses = start_session(recipient.domain, recipient, app, module, form, case_id, yield_responses=True, session_type=XFORMS_SESSION_IVR, case_for_case_submission=case_for_case_submission) call_log_entry.xforms_session_id = session.session_id elif ivr_event == IVR_EVENT_INPUT: if call_log_entry.xforms_session_id is not None: current_q = current_question(call_log_entry.xforms_session_id) if validate_answer(input_data, current_q): answer_is_valid = True responses = _get_responses(recipient.domain, recipient._id, input_data, yield_responses=True, session_id=call_log_entry.xforms_session_id) else: call_log_entry.current_question_retry_count += 1 responses = [current_q] else: responses = [] else: responses = [] ivr_responses = [] hang_up = False for response in responses: if response.is_error: error_occurred = True call_log_entry.current_question_retry_count += 1 if response.text_prompt is None: ivr_responses = [] break else: ivr_responses.append(format_ivr_response(response.text_prompt, app)) elif response.event.type == "question": ivr_responses.append(format_ivr_response(response.event.caption, app)) elif response.event.type == "form-complete": hang_up = True if answer_is_valid and not error_occurred: call_log_entry.current_question_retry_count = 0 if call_log_entry.max_question_retries is not None and call_log_entry.current_question_retry_count > call_log_entry.max_question_retries: # Force hang-up ivr_responses = [] if len(ivr_responses) == 0: hang_up = True input_length = None if hang_up: if call_log_entry.xforms_session_id is not None: # Process disconnect session = get_session_by_session_id(call_log_entry.xforms_session_id) if session.end_time is None: if call_log_entry.submit_partial_form: submit_unfinished_form(session.session_id, call_log_entry.include_case_side_effects) else: session.end(completed=False) session.save() else: # Set input_length to let the ivr gateway know how many digits we need to collect. # Have to get the current question again, since the last XFormsResponse in responses # may not have an event if it was a response to a constraint error. if error_occurred: current_q = current_question(call_log_entry.xforms_session_id) else: current_q = responses[-1] input_length = get_input_length(current_q) call_log_entry.save() return HttpResponse(backend_module.get_http_response_string(gateway_session_id, ivr_responses, collect_input=(not hang_up), hang_up=hang_up, input_length=input_length)) # If not processed, just log the call if call_log_entry: # No need to log, already exists return HttpResponse("") cleaned_number = phone_number if cleaned_number is not None and len(cleaned_number) > 0 and cleaned_number[0] == "+": cleaned_number = cleaned_number[1:] # Try to look up the verified number entry v = VerifiedNumber.view("sms/verified_number_by_number", key=cleaned_number, include_docs=True ).one() # If none was found, try to match only the last digits of numbers in the database if v is None: v = VerifiedNumber.view("sms/verified_number_by_suffix", key=cleaned_number, include_docs=True ).one() # Save the call entry msg = CallLog( phone_number=cleaned_number, direction=INCOMING, date=datetime.utcnow(), backend_api=backend_module.API_ID if backend_module else None, gateway_session_id=gateway_session_id, ) if v is not None: msg.domain = v.domain msg.couch_recipient_doc_type = v.owner_doc_type msg.couch_recipient = v.owner_id msg.save() return HttpResponse("")
def structured_sms_handler(verified_number, text): # Circular Import from corehq.apps.reminders.models import SurveyKeyword, FORM_TYPE_ALL_AT_ONCE text = text.strip() if text == "": return False for survey_keyword in SurveyKeyword.get_all(verified_number.domain): if survey_keyword.form_type == FORM_TYPE_ALL_AT_ONCE: if survey_keyword.delimiter is not None: args = text.split(survey_keyword.delimiter) else: args = text.split() keyword = args[0].strip().upper() if keyword != survey_keyword.keyword.upper(): continue try: error_occurred = False error_msg = "" form_complete = False # Close any open sessions close_open_sessions(verified_number.domain, verified_number.owner_id) # Start the session form = Form.get_form(survey_keyword.form_unique_id) app = form.get_app() module = form.get_module() if verified_number.owner_doc_type == "CommCareCase": case_id = verified_number.owner_id else: # TODO: Need a way to choose the case when it's a user that's playing the form case_id = None session, responses = start_session( verified_number.domain, verified_number.owner, app, module, form, case_id=case_id, yield_responses=True, ) assert len(responses) > 0, "There should be at least one response." current_question = responses[-1] form_complete = is_form_complete(current_question) if not form_complete: if survey_keyword.use_named_args: # Arguments in the sms are named xpath_answer = {} # Dictionary of {xpath : answer} for answer in args[1:]: answer = answer.strip() answer_upper = answer.upper() if survey_keyword.named_args_separator is not None: # A separator is used for naming arguments; for example, the "=" in "register name=joe age=25" answer_parts = answer.partition(survey_keyword.named_args_separator) if answer_parts[1] != survey_keyword.named_args_separator: error_occurred = True error_msg = "ERROR: Expected name and value to be joined by" + ( " '%s'" % survey_keyword.named_args_separator ) break else: arg_name = answer_parts[0].upper().strip() xpath = survey_keyword.named_args.get(arg_name, None) if xpath is not None: if xpath in xpath_answer: error_occurred = True error_msg = "ERROR: More than one answer found for" + (" '%s'" % arg_name) break xpath_answer[xpath] = answer_parts[2].strip() else: # Ignore unexpected named arguments pass else: # No separator is used for naming arguments; for example, "update a100 b34 c5" matches = 0 for k, v in survey_keyword.named_args.items(): if answer_upper.startswith(k): matches += 1 if matches > 1: error_occurred = True error_msg = "ERROR: More than one question matches" + (" '%s'" % answer) break if v in xpath_answer: error_occurred = True error_msg = "ERROR: More than one answer found for" + (" '%s'" % k) break xpath_answer[v] = answer[len(k) :].strip() if matches == 0: # Ignore unexpected named arguments pass if error_occurred: break # Go through each question in the form, answering only the questions that the sms has answers for while not form_complete and not error_occurred: if current_question.is_error: error_occurred = True error_msg = current_question.text_prompt or "ERROR: Internal server error" break xpath = current_question.event._dict["binding"] if xpath in xpath_answer: valid, answer, _error_msg = validate_answer(current_question.event, xpath_answer[xpath]) if not valid: error_occurred = True error_msg = "ERROR: " + _error_msg break responses = _get_responses( verified_number.domain, verified_number.owner_id, answer, yield_responses=True ) else: responses = _get_responses( verified_number.domain, verified_number.owner_id, "", yield_responses=True ) current_question = responses[-1] if is_form_complete(current_question): form_complete = True else: # Arguments in the sms are not named; pass each argument to each question in order for answer in args[1:]: if form_complete: # Form is complete, ignore remaining answers break if current_question.is_error: error_occurred = True error_msg = current_question.text_prompt or "ERROR: Internal server error" break valid, answer, _error_msg = validate_answer(current_question.event, answer.strip()) if not valid: error_occurred = True error_msg = "ERROR: " + _error_msg break responses = _get_responses( verified_number.domain, verified_number.owner_id, answer, yield_responses=True ) current_question = responses[-1] form_complete = is_form_complete(current_question) # If the form isn't finished yet but we're out of arguments, try to leave each remaining question blank and continue while not form_complete and not error_occurred: responses = _get_responses( verified_number.domain, verified_number.owner_id, "", yield_responses=True ) current_question = responses[-1] if current_question.is_error: error_occurred = True error_msg = current_question.text_prompt or "ERROR: Internal server error" if is_form_complete(current_question): form_complete = True except Exception: logging.exception( "Could not process structured sms for verified number %s, domain %s, keyword %s" % (verified_number._id, verified_number.domain, keyword) ) error_occurred = True error_msg = "ERROR: Internal server error" if error_occurred: send_sms_to_verified_number(verified_number, error_msg) if error_occurred or not form_complete: session = XFormsSession.get(session._id) session.end(False) session.save() return True return False
error_msg = None session = None try: # Start the session app, module, form = get_form( survey_keyword_action.form_unique_id, include_app_module=True) if case: case_id = case._id elif contact_doc_type == "CommCareCase": case_id = contact_id else: case_id = None session, responses = start_session(domain, contact, app, module, form, case_id=case_id, yield_responses=True) session.workflow = WORKFLOW_KEYWORD session.save() assert len(responses) > 0, "There should be at least one response." first_question = responses[-1] if not is_form_complete(first_question): if survey_keyword_action.use_named_args: # Arguments in the sms are named xpath_answer = parse_structured_sms_named_args(args, survey_keyword_action, verified_number) _handle_structured_sms(domain, args, contact_id, session, first_question, verified_number, xpath_answer) else: # Arguments in the sms are not named; pass each argument to # each question in order
def initiate_outbound_call(call_log_entry, *args, **kwargs): phone_number = call_log_entry.phone_number if phone_number.startswith("+"): phone_number = phone_number[1:] if phone_number.startswith("91"): phone_number = "0" + phone_number[2:] else: call_log_entry.error = True call_log_entry.error_message = "Kookoo can only send to Indian phone numbers." call_log_entry.save() return False form = Form.get_form(call_log_entry.form_unique_id) app = form.get_app() module = form.get_module() # Only precache the first response if it's not an only-label form, otherwise we could end up # submitting the form regardless of whether the person actually answers the call. if form_requires_input(form): recipient = call_log_entry.recipient case_id = get_case_id(call_log_entry) session, responses = start_session(recipient.domain, recipient, app, module, form, case_id, yield_responses=True, session_type=XFORMS_SESSION_IVR) ivr_responses = [] if len(responses) == 0: call_log_entry.error = True call_log_entry.error_message = "No prompts seen in form. Please check that the form does not have errors." call_log_entry.save() return False for response in responses: ivr_responses.append( format_ivr_response(response.event.caption, app)) input_length = get_input_length(responses[-1]) call_log_entry.use_precached_first_response = True call_log_entry.xforms_session_id = session.session_id url_base = get_url_base() params = urlencode({ "phone_no": phone_number, "api_key": kwargs["api_key"], "outbound_version": "2", "url": url_base + reverse("corehq.apps.kookoo.views.ivr"), "callback_url": url_base + reverse("corehq.apps.kookoo.views.ivr_finished"), }) url = "http://www.kookoo.in/outbound/outbound.php?%s" % params response = urlopen(url, timeout=settings.IVR_GATEWAY_TIMEOUT).read() root = XML(response) for child in root: if child.tag.endswith("status"): status = child.text elif child.tag.endswith("message"): message = child.text if status == "queued": call_log_entry.error = False call_log_entry.gateway_session_id = "KOOKOO-" + message elif status == "error": call_log_entry.error = True call_log_entry.error_message = message else: call_log_entry.error = True call_log_entry.error_message = "Unknown status received from Kookoo." if call_log_entry.error: call_log_entry.use_precached_first_response = False if call_log_entry.use_precached_first_response: call_log_entry.first_response = get_http_response_string( call_log_entry.gateway_session_id, ivr_responses, collect_input=True, hang_up=False, input_length=input_length) call_log_entry.save() return not call_log_entry.error
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers): if handler.recipient in [RECIPIENT_CASE, RECIPIENT_SURVEY_SAMPLE]: 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 = XFormsSession.view("smsforms/sessions_by_touchforms_id", startkey=[session_id], endkey=[session_id, {}], include_docs=True).one() if session.end_time is None: vn = VerifiedNumber.view("sms/verified_number_by_owner_id", key=session.connection_id, include_docs=True).one() if vn is not None: resp = current_question(session_id) send_sms_to_verified_number(vn, resp.event.text_prompt) 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 as e: raise_error(reminder, ERROR_FORM) return False # Start a touchforms session for each recipient for recipient in recipients: verified_number = verified_numbers[recipient.get_id] if verified_number is None: if len(recipients) == 1: raise_error(reminder, ERROR_NO_VERIFIED_NUMBER) return False else: raise_warning() # ERROR_NO_VERIFIED_NUMBER continue # Close all currently open sessions close_open_sessions(reminder.domain, recipient.get_id) # Start the new session session, responses = start_session(reminder.domain, recipient, app, module, form, recipient.get_id) session.survey_incentive = handler.survey_incentive session.save() reminder.xforms_session_ids.append(session.session_id) # Send out first message if len(responses) > 0: message = format_message_list(responses) result = send_sms_to_verified_number(verified_number, message) if not result: raise_warning() # Could not send SMS if len(recipients) == 1: return result return True else: # TODO: Make sure the above flow works for RECIPIENT_USER and RECIPIENT_OWNER return False
def incoming(phone_number, backend_module, gateway_session_id, ivr_event, input_data=None): # Look up the call if one already exists call_log_entry = CallLog.view("sms/call_by_session", startkey=[gateway_session_id, {}], endkey=[gateway_session_id], descending=True, include_docs=True, limit=1).one() answer_is_valid = False # This will be set to True if IVR validation passes error_occurred = False # This will be set to False if touchforms validation passes (i.e., no form constraints fail) if call_log_entry is not None and backend_module: if ivr_event == IVR_EVENT_NEW_CALL and call_log_entry.use_precached_first_response: return HttpResponse(call_log_entry.first_response) form = Form.get_form(call_log_entry.form_unique_id) app = form.get_app() module = form.get_module() recipient = call_log_entry.recipient if ivr_event == IVR_EVENT_NEW_CALL: case_id = call_log_entry.case_id case_for_case_submission = call_log_entry.case_for_case_submission session, responses = start_session( recipient.domain, recipient, app, module, form, case_id, yield_responses=True, session_type=XFORMS_SESSION_IVR, case_for_case_submission=case_for_case_submission) call_log_entry.xforms_session_id = session.session_id elif ivr_event == IVR_EVENT_INPUT: if call_log_entry.xforms_session_id is not None: current_q = current_question(call_log_entry.xforms_session_id) if validate_answer(input_data, current_q): answer_is_valid = True responses = _get_responses( recipient.domain, recipient._id, input_data, yield_responses=True, session_id=call_log_entry.xforms_session_id) else: call_log_entry.current_question_retry_count += 1 responses = [current_q] else: responses = [] else: responses = [] ivr_responses = [] hang_up = False for response in responses: if response.is_error: error_occurred = True call_log_entry.current_question_retry_count += 1 if response.text_prompt is None: ivr_responses = [] break else: ivr_responses.append( format_ivr_response(response.text_prompt, app)) elif response.event.type == "question": ivr_responses.append( format_ivr_response(response.event.caption, app)) elif response.event.type == "form-complete": hang_up = True if answer_is_valid and not error_occurred: call_log_entry.current_question_retry_count = 0 if call_log_entry.max_question_retries is not None and call_log_entry.current_question_retry_count > call_log_entry.max_question_retries: # Force hang-up ivr_responses = [] if len(ivr_responses) == 0: hang_up = True input_length = None if hang_up: if call_log_entry.xforms_session_id is not None: # Process disconnect session = XFormsSession.latest_by_session_id( call_log_entry.xforms_session_id) if session.end_time is None: if call_log_entry.submit_partial_form: submit_unfinished_form( session.session_id, call_log_entry.include_case_side_effects) else: session.end(completed=False) session.save() else: # Set input_length to let the ivr gateway know how many digits we need to collect. # Have to get the current question again, since the last XFormsResponse in responses # may not have an event if it was a response to a constraint error. if error_occurred: current_q = current_question(call_log_entry.xforms_session_id) else: current_q = responses[-1] input_length = get_input_length(current_q) call_log_entry.save() return HttpResponse( backend_module.get_http_response_string( gateway_session_id, ivr_responses, collect_input=(not hang_up), hang_up=hang_up, input_length=input_length)) # If not processed, just log the call if call_log_entry: # No need to log, already exists return HttpResponse("") cleaned_number = phone_number if cleaned_number is not None and len( cleaned_number) > 0 and cleaned_number[0] == "+": cleaned_number = cleaned_number[1:] # Try to look up the verified number entry v = VerifiedNumber.view("sms/verified_number_by_number", key=cleaned_number, include_docs=True).one() # If none was found, try to match only the last digits of numbers in the database if v is None: v = VerifiedNumber.view("sms/verified_number_by_suffix", key=cleaned_number, include_docs=True).one() # Save the call entry msg = CallLog( phone_number=cleaned_number, direction=INCOMING, date=datetime.utcnow(), backend_api=backend_module.API_ID if backend_module else None, gateway_session_id=gateway_session_id, ) if v is not None: msg.domain = v.domain msg.couch_recipient_doc_type = v.owner_doc_type msg.couch_recipient = v.owner_id msg.save() return HttpResponse("")
def handle_structured_sms( survey_keyword, survey_keyword_action, contact, verified_number, text, send_response=False, msg=None ): domain = contact.domain contact_doc_type = contact.doc_type contact_id = contact._id text = text.strip() if survey_keyword.delimiter is not None: args = text.split(survey_keyword.delimiter) else: args = text.split() keyword = args[0].strip().upper() error_occurred = False error_msg = None session = None try: # Start the session form = Form.get_form(survey_keyword_action.form_unique_id) app = form.get_app() module = form.get_module() if contact_doc_type == "CommCareCase": case_id = contact_id else: # TODO: Need a way to choose the case when it's a user that's playing the form case_id = None session, responses = start_session(domain, contact, app, module, form, case_id=case_id, yield_responses=True) session.workflow = WORKFLOW_KEYWORD session.save() if msg is not None: msg.workflow = WORKFLOW_KEYWORD msg.xforms_session_couch_id = session._id msg.save() assert len(responses) > 0, "There should be at least one response." current_question = responses[-1] form_complete = is_form_complete(current_question) if not form_complete: if survey_keyword_action.use_named_args: # Arguments in the sms are named xpath_answer = {} # Dictionary of {xpath : answer} for answer in args[1:]: answer = answer.strip() answer_upper = answer.upper() if survey_keyword_action.named_args_separator is not None: # A separator is used for naming arguments; for example, the "=" in "register name=joe age=25" answer_parts = answer.partition(survey_keyword_action.named_args_separator) if answer_parts[1] != survey_keyword_action.named_args_separator: raise StructuredSMSException( response_text="ERROR: Expected name and value to be joined by '%(separator)s'" % {"separator": survey_keyword_action.named_args_separator} ) else: arg_name = answer_parts[0].upper().strip() xpath = survey_keyword_action.named_args.get(arg_name, None) if xpath is not None: if xpath in xpath_answer: raise StructuredSMSException( response_text="ERROR: More than one answer found for '%(arg_name)s'" % {"arg_name": arg_name} ) xpath_answer[xpath] = answer_parts[2].strip() else: # Ignore unexpected named arguments pass else: # No separator is used for naming arguments; for example, "update a100 b34 c5" matches = 0 for k, v in survey_keyword_action.named_args.items(): if answer_upper.startswith(k): matches += 1 if matches > 1: raise StructuredSMSException( response_text="ERROR: More than one question matches '%(answer)s'" % {"answer": answer} ) if v in xpath_answer: raise StructuredSMSException( response_text="ERROR: More than one answer found for '%(named_arg)s'" % {"named_arg": k} ) xpath_answer[v] = answer[len(k) :].strip() if matches == 0: # Ignore unexpected named arguments pass # Go through each question in the form, answering only the questions that the sms has answers for while not form_complete: if current_question.is_error: raise StructuredSMSException( response_text=(current_question.text_prompt or "ERROR: Internal server error") ) xpath = current_question.event._dict["binding"] if xpath in xpath_answer: valid, answer, _error_msg = validate_answer(current_question.event, xpath_answer[xpath]) if not valid: raise StructuredSMSException(response_text=_error_msg) responses = _get_responses( domain, contact_id, answer, yield_responses=True, session_id=session.session_id, update_timestamp=False, ) else: responses = _get_responses( domain, contact_id, "", yield_responses=True, session_id=session.session_id, update_timestamp=False, ) current_question = responses[-1] if is_form_complete(current_question): form_complete = True else: # Arguments in the sms are not named; pass each argument to each question in order for answer in args[1:]: if form_complete: # Form is complete, ignore remaining answers break if current_question.is_error: raise StructuredSMSException( response_text=(current_question.text_prompt or "ERROR: Internal server error") ) valid, answer, _error_msg = validate_answer(current_question.event, answer.strip()) if not valid: raise StructuredSMSException(response_text=_error_msg) responses = _get_responses( domain, contact_id, answer, yield_responses=True, session_id=session.session_id, update_timestamp=False, ) current_question = responses[-1] form_complete = is_form_complete(current_question) # If the form isn't finished yet but we're out of arguments, try to leave each remaining question blank and continue while not form_complete: responses = _get_responses( domain, contact_id, "", yield_responses=True, session_id=session.session_id, update_timestamp=False, ) current_question = responses[-1] if current_question.is_error: raise StructuredSMSException( response_text=(current_question.text_prompt or "ERROR: Internal server error") ) if is_form_complete(current_question): form_complete = True except StructuredSMSException as sse: error_occurred = True error_msg = sse.response_text except Exception: logging.exception( "Could not process structured sms for contact %s, domain %s, keyword %s" % (contact_id, domain, keyword) ) error_occurred = True error_msg = "ERROR: Internal server error" if session is not None: session = XFormsSession.get(session._id) if session.is_open: session.end(False) session.save() message_tags = { "workflow": WORKFLOW_KEYWORD, "xforms_session_couch_id": session._id if session is not None else None, } if msg is not None: msg.workflow = message_tags["workflow"] msg.xforms_session_couch_id = message_tags["xforms_session_couch_id"] msg.save() if error_occurred and verified_number is not None and send_response: send_sms_to_verified_number(verified_number, error_msg, **message_tags)
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 = XFormsSession.view("smsforms/sessions_by_touchforms_id", startkey=[session_id], endkey=[session_id, {}], include_docs=True).one() 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 as e: 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 # Close all currently open sessions XFormsSession.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 initiate_outbound_call(call_log_entry, *args, **kwargs): phone_number = call_log_entry.phone_number if phone_number.startswith("+"): phone_number = phone_number[1:] if phone_number.startswith("91"): phone_number = "0" + phone_number[2:] else: raise InvalidPhoneNumberException("Kookoo can only send to Indian phone numbers.") form = Form.get_form(call_log_entry.form_unique_id) app = form.get_app() module = form.get_module() # Only precache the first response if it's not an only-label form, otherwise we could end up # submitting the form regardless of whether the person actually answers the call. if form_requires_input(form): recipient = call_log_entry.recipient case_id = get_case_id(call_log_entry) session, responses = start_session(recipient.domain, recipient, app, module, form, case_id, yield_responses=True, session_type=XFORMS_SESSION_IVR) ivr_responses = [] if len(ivr_responses) == 0: call_log_entry.error = True call_log_entry.error_message = "No prompts seen in form. Please check that the form does not have errors." call_log_entry.save() return False for response in responses: ivr_responses.append(format_ivr_response(response.event.caption, app)) input_length = get_input_length(responses[-1]) call_log_entry.use_precached_first_response = True call_log_entry.xforms_session_id = session.session_id url_base = get_url_base() params = urlencode({ "phone_no" : phone_number, "api_key" : kwargs["api_key"], "outbound_version" : "2", "url" : url_base + reverse("corehq.apps.kookoo.views.ivr"), "callback_url" : url_base + reverse("corehq.apps.kookoo.views.ivr_finished"), }) url = "http://www.kookoo.in/outbound/outbound.php?%s" % params response = urlopen(url).read() root = XML(response) for child in root: if child.tag.endswith("status"): status = child.text elif child.tag.endswith("message"): message = child.text if status == "queued": call_log_entry.error = False call_log_entry.gateway_session_id = "KOOKOO-" + message elif status == "error": call_log_entry.error = True call_log_entry.error_message = message else: call_log_entry.error = True call_log_entry.error_message = "Unknown status received from Kookoo." if call_log_entry.error: call_log_entry.use_precached_first_response = False if call_log_entry.use_precached_first_response: call_log_entry.first_response = get_http_response_string(call_log_entry.gateway_session_id, ivr_responses, collect_input=True, hang_up=False, input_length=input_length) call_log_entry.save() return not call_log_entry.error
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers): if handler.recipient in [RECIPIENT_CASE, RECIPIENT_SURVEY_SAMPLE]: 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 = XFormsSession.view( "smsforms/sessions_by_touchforms_id", startkey=[session_id], endkey=[session_id, {}], include_docs=True).one() if session.end_time is None: vn = VerifiedNumber.view( "sms/verified_number_by_owner_id", key=session.connection_id, include_docs=True).one() if vn is not None: resp = current_question(session_id) send_sms_to_verified_number( vn, resp.event.text_prompt) 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 as e: raise_error(reminder, ERROR_FORM) return False # Start a touchforms session for each recipient for recipient in recipients: verified_number = verified_numbers[recipient.get_id] if verified_number is None: if len(recipients) == 1: raise_error(reminder, ERROR_NO_VERIFIED_NUMBER) return False else: raise_warning() # ERROR_NO_VERIFIED_NUMBER continue # Close all currently open sessions close_open_sessions(reminder.domain, recipient.get_id) # Start the new session session, responses = start_session(reminder.domain, recipient, app, module, form, recipient.get_id) session.survey_incentive = handler.survey_incentive session.save() reminder.xforms_session_ids.append(session.session_id) # Send out first message if len(responses) > 0: message = format_message_list(responses) result = send_sms_to_verified_number( verified_number, message) if not result: raise_warning() # Could not send SMS if len(recipients) == 1: return result return True else: # TODO: Make sure the above flow works for RECIPIENT_USER and RECIPIENT_OWNER return False
def initiate_outbound_call(call_log_entry, *args, **kwargs): phone_number = call_log_entry.phone_number if phone_number.startswith("+"): phone_number = phone_number[1:] if phone_number.startswith("91"): phone_number = "0" + phone_number[2:] else: call_log_entry.error = True call_log_entry.error_message = "Kookoo can only send to Indian phone numbers." call_log_entry.save() return False form = Form.get_form(call_log_entry.form_unique_id) app = form.get_app() module = form.get_module() # Only precache the first response if it's not an only-label form, otherwise we could end up # submitting the form regardless of whether the person actually answers the call. if form_requires_input(form): recipient = call_log_entry.recipient case_id = call_log_entry.case_id case_for_case_submission = call_log_entry.case_for_case_submission session, responses = start_session(recipient.domain, recipient, app, module, form, case_id, yield_responses=True, session_type=XFORMS_SESSION_IVR, case_for_case_submission=case_for_case_submission) ivr_responses = [] if len(responses) == 0: call_log_entry.error = True call_log_entry.error_message = "No prompts seen in form. Please check that the form does not have errors." call_log_entry.save() return False for response in responses: ivr_responses.append(format_ivr_response(response.event.caption, app)) input_length = get_input_length(responses[-1]) call_log_entry.use_precached_first_response = True call_log_entry.xforms_session_id = session.session_id url_base = get_url_base() params = urlencode({ "phone_no" : phone_number, "api_key" : kwargs["api_key"], "outbound_version" : "2", "url" : url_base + reverse("corehq.apps.kookoo.views.ivr"), "callback_url" : url_base + reverse("corehq.apps.kookoo.views.ivr_finished"), }) url = "http://www.kookoo.in/outbound/outbound.php?%s" % params if kwargs.get("is_test", False): session_id = hashlib.sha224(datetime.utcnow().isoformat()).hexdigest() response = "<request><status>queued</status><message>%s</message></request>" % session_id else: response = urlopen(url, timeout=settings.IVR_GATEWAY_TIMEOUT).read() root = XML(response) for child in root: if child.tag.endswith("status"): status = child.text elif child.tag.endswith("message"): message = child.text do_not_retry = False if status == "queued": call_log_entry.error = False call_log_entry.gateway_session_id = "KOOKOO-" + message elif status == "error": call_log_entry.error = True call_log_entry.error_message = message if (message.strip().upper() in [ 'CALLS WILL NOT BE MADE BETWEEN 9PM TO 9AM.', 'PHONE NUMBER IN DND LIST', ]): do_not_retry = True else: call_log_entry.error = True call_log_entry.error_message = "Unknown status received from Kookoo." if call_log_entry.error: call_log_entry.use_precached_first_response = False if call_log_entry.use_precached_first_response: call_log_entry.first_response = get_http_response_string(call_log_entry.gateway_session_id, ivr_responses, collect_input=True, hang_up=False, input_length=input_length) call_log_entry.save() return not call_log_entry.error or do_not_retry
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 handle_structured_sms(survey_keyword, survey_keyword_action, contact, verified_number, text, send_response=False, msg=None, case=None, text_args=None): domain = contact.domain contact_doc_type = contact.doc_type contact_id = contact._id if text_args is not None: args = text_args else: args = split_args(text, survey_keyword) args = args[1:] keyword = survey_keyword.keyword.upper() error_occurred = False error_msg = None session = None try: # Start the session app, module, form = get_form( survey_keyword_action.form_unique_id, include_app_module=True) if case: case_id = case._id elif contact_doc_type == "CommCareCase": case_id = contact_id else: case_id = None session, responses = start_session(domain, contact, app, module, form, case_id=case_id, yield_responses=True) session.workflow = WORKFLOW_KEYWORD session.save() assert len(responses) > 0, "There should be at least one response." first_question = responses[-1] if not is_form_complete(first_question): if survey_keyword_action.use_named_args: # Arguments in the sms are named xpath_answer = parse_structured_sms_named_args(args, survey_keyword_action, verified_number) _handle_structured_sms(domain, args, contact_id, session, first_question, verified_number, xpath_answer) else: # Arguments in the sms are not named; pass each argument to # each question in order _handle_structured_sms(domain, args, contact_id, session, first_question, verified_number) except StructuredSMSException as sse: error_occurred = True error_msg = "" if sse.xformsresponse and sse.xformsresponse.event: xpath_arg = None if survey_keyword_action.use_named_args: xpath_arg = \ {v: k for k, v in survey_keyword_action.named_args.items()} field_name = get_question_id(sse.xformsresponse, xpath_arg) error_msg = get_message(MSG_FIELD_DESCRIPTOR, verified_number, (field_name,)) error_msg = "%s%s" % (error_msg, sse.response_text) except Exception: notify_exception(None, message=("Could not process structured sms for" "contact %s, domain %s, keyword %s" % (contact_id, domain, keyword))) error_occurred = True error_msg = get_message(MSG_TOUCHFORMS_ERROR, verified_number) if session is not None: session = SQLXFormsSession.objects.get(couch_id=session._id) if session.is_open: session.end(False) session.save() metadata = MessageMetadata( workflow=WORKFLOW_KEYWORD, xforms_session_couch_id=session._id if session else None, ) if msg: add_msg_tags(msg, metadata) if error_occurred and verified_number is not None and send_response: send_sms_to_verified_number(verified_number, error_msg, metadata=metadata) return not error_occurred
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers, logged_event): current_event = reminder.current_event if reminder.callback_try_count > 0: # Leaving this as an explicit reminder that all survey related actions now happen # in a different process. Eventually all of this code will be removed when we move # to the new reminders framework. pass else: reminder.xforms_session_ids = [] domain_obj = Domain.get_by_name(reminder.domain, strict=True) # Get the app, module, and form try: form_unique_id = current_event.form_unique_id form = Form.get_form(form_unique_id) app = form.get_app() module = form.get_module() except Exception: logged_event.error(MessagingEvent.ERROR_CANNOT_FIND_FORM) return # Start a touchforms session for each recipient for recipient in recipients: logged_subevent = logged_event.create_subevent( handler, reminder, recipient) verified_number, unverified_number = get_recipient_phone_number( reminder, recipient, verified_numbers) no_verified_number = verified_number is None cant_use_unverified_number = (unverified_number is None or form_requires_input(form)) if no_verified_number and cant_use_unverified_number: logged_subevent.error( MessagingEvent.ERROR_NO_TWO_WAY_PHONE_NUMBER) continue if verified_number: pb = PhoneBlacklist.get_by_phone_number_or_none( verified_number.phone_number) else: pb = PhoneBlacklist.get_by_phone_number_or_none( unverified_number) if pb and not pb.send_sms: logged_subevent.error(MessagingEvent.ERROR_PHONE_OPTED_OUT) continue with critical_section_for_smsforms_sessions(recipient.get_id): # Get the case to submit the form against, if any if (is_commcarecase(recipient) and not handler.force_surveys_to_use_triggered_case): case_id = recipient.case_id else: case_id = reminder.case_id if form.requires_case() and not case_id: logged_subevent.error(MessagingEvent.ERROR_NO_CASE_GIVEN) continue # Close all currently open sessions SQLXFormsSession.close_all_open_sms_sessions( reminder.domain, recipient.get_id) # Start the new session try: if current_event.callback_timeout_intervals: if handler.submit_partial_forms: expire_after = sum( current_event.callback_timeout_intervals) reminder_intervals = current_event.callback_timeout_intervals[: -1] else: expire_after = SQLXFormsSession.MAX_SESSION_LENGTH reminder_intervals = current_event.callback_timeout_intervals submit_partially_completed_forms = handler.submit_partial_forms include_case_updates_in_partial_submissions = handler.include_case_side_effects else: expire_after = SQLXFormsSession.MAX_SESSION_LENGTH reminder_intervals = [] submit_partially_completed_forms = False include_case_updates_in_partial_submissions = False session, responses = start_session( SQLXFormsSession.create_session_object( reminder.domain, recipient, verified_number.phone_number if verified_number else unverified_number, app, form, expire_after=expire_after, reminder_intervals=reminder_intervals, submit_partially_completed_forms= submit_partially_completed_forms, include_case_updates_in_partial_submissions= include_case_updates_in_partial_submissions), reminder.domain, recipient, app, module, form, case_id, case_for_case_submission=handler. force_surveys_to_use_triggered_case) except TouchformsError as e: human_readable_message = get_formplayer_exception( reminder.domain, e) logged_subevent.error( MessagingEvent.ERROR_TOUCHFORMS_ERROR, additional_error_text=human_readable_message) if touchforms_error_is_config_error(reminder.domain, e): # Don't reraise the exception because this means there are configuration # issues with the form that need to be fixed continue else: # Reraise the exception so that the framework retries it again later raise except Exception as e: logged_subevent.error( MessagingEvent.ERROR_TOUCHFORMS_ERROR) # Reraise the exception so that the framework retries it again later raise session.survey_incentive = handler.survey_incentive session.workflow = get_workflow(handler) session.reminder_id = reminder._id session.save() reminder.xforms_session_ids.append(session.session_id) logged_subevent.xforms_session = session logged_subevent.save() # Send out first message if len(responses) > 0: message = format_message_list(responses) metadata = MessageMetadata( workflow=get_workflow(handler), reminder_id=reminder._id, xforms_session_couch_id=session._id, ) if verified_number: send_sms_to_verified_number( verified_number, message, metadata, logged_subevent=logged_subevent) else: send_sms(reminder.domain, recipient, unverified_number, message, metadata) logged_subevent.completed()
def 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 = XFormsSession.view( "smsforms/sessions_by_touchforms_id", startkey=[session_id], endkey=[session_id, {}], include_docs=True).one() 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 as e: 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 # Close all currently open sessions XFormsSession.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