def test_sync_from_creation(self): properties = _arbitrary_session_properties() couch_session = XFormsSession(**properties) couch_session.save() sql_session = SQLXFormsSession.objects.get(couch_id=couch_session._id) for prop, value in properties.items(): self.assertEqual(getattr(sql_session, prop), value) # make sure we didn't do any excess saves self.assertTrue(XFormsSession.get_db().get_rev(couch_session._id).startswith('1-'))
def process_survey_keyword_actions(verified_number, survey_keyword, text, msg): sender = verified_number.owner case = None args = split_args(text, survey_keyword) # Close any open sessions even if it's just an sms that we're # responding with. XFormsSession.close_all_open_sms_sessions(verified_number.domain, verified_number.owner_id) if sender.doc_type == "CommCareCase": case = sender args = args[1:]
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)
def close_open_sessions(domain, connection_id): sessions = XFormsSession.view("smsforms/open_sms_sessions_by_connection", key=[domain, connection_id], include_docs=True).all() for session in sessions: session.end(False) session.save()
def close_open_sessions(domain, connection_id): sessions = XFormsSession.view( "smsforms/open_sms_sessions_by_connection", key=[domain, connection_id], include_docs=True ).all() for session in sessions: session.end(False) session.save()
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, ) couch_sessions = XFormsSession.get_all_open_sms_sessions(domain, contact) sql_sessions = SQLXFormsSession.get_all_open_sms_sessions(domain, contact) self.assertEqual(3, len(couch_sessions)) self.assertEqual(3, len(sql_sessions)) self.assertEqual(set([x._id for x in couch_sessions]), set([x.couch_id for x in sql_sessions])) SQLXFormsSession.close_all_open_sms_sessions(domain, contact) self.assertEqual(0, len(XFormsSession.get_all_open_sms_sessions(domain, contact))) self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
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_type=XFORMS_SESSION_SMS, ) self.assertEqual(0, len(XFormsSession.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(XFormsSession.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, ) couch_session = XFormsSession(**properties) couch_session.save() (mult, session) = get_single_open_session_or_close_multiple( couch_session.domain, couch_session.connection_id ) self.assertEqual(False, mult) self.assertEqual(couch_session._id, session._id) [couch_session_back] = XFormsSession.get_all_open_sms_sessions( couch_session.domain, couch_session.connection_id ) [sql_session] = SQLXFormsSession.get_all_open_sms_sessions( couch_session.domain, couch_session.connection_id ) self.assertEqual(couch_session._id, couch_session_back._id) self.assertEqual(couch_session._id, sql_session.couch_id)
def form_session_handler(v, text, msg=None): """ The form session handler will use the inbound text to answer the next question in the open XformsSession 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. """ sessions = XFormsSession.get_all_open_sms_sessions(v.domain, v.owner_id) if len(sessions) > 1: # If there are multiple sessions, there's no way for us to know which one this message # belongs to. So we should inform the user that there was an error and to try to restart # the survey. for session in sessions: session.end(False) session.save() send_sms_to_verified_number(v, "An error has occurred. Please try restarting the survey.") return True session = sessions[0] if len(sessions) == 1 else None if session is not None: if msg is not None: msg.workflow = session.workflow msg.reminder_id = session.reminder_id msg.xforms_session_couch_id = session._id msg.save() # If there's an open session, treat the inbound text as the answer to the next question try: resp = current_question(session.session_id) event = resp.event valid, text, error_msg = validate_answer(event, text) if valid: responses = _get_responses(v.domain, v.owner_id, text, yield_responses=True) if has_invalid_response(responses): if msg: mark_as_invalid_response(msg) text_responses = _responses_to_text(responses) if len(text_responses) > 0: response_text = format_message_list(text_responses) send_sms_to_verified_number(v, response_text, workflow=session.workflow, reminder_id=session.reminder_id, xforms_session_couch_id=session._id) else: if msg: mark_as_invalid_response(msg) send_sms_to_verified_number(v, error_msg + event.text_prompt, workflow=session.workflow, reminder_id=session.reminder_id, xforms_session_couch_id=session._id) except Exception: # Catch any touchforms errors msg_id = msg._id if msg is not None else "" logging.exception("Exception in form_session_handler for message id %s." % msg_id) send_sms_to_verified_number(v, "An error has occurred. Please try again later. If the problem persists, try restarting the survey.") return True else: return False
def test_sync_from_update(self): properties = _arbitrary_session_properties() couch_session = XFormsSession(**properties) couch_session.save() sql_session = SQLXFormsSession.objects.get(couch_id=couch_session._id) for prop, value in properties.items(): self.assertEqual(getattr(sql_session, prop), value) previous_count = SQLXFormsSession.objects.count() updated_properties = _arbitrary_session_properties() for attr, val in updated_properties.items(): couch_session[attr] = val couch_session.save() # make sure nothing new was created self.assertEqual(previous_count, SQLXFormsSession.objects.count()) # check updated props in the sql model sql_session = SQLXFormsSession.objects.get(pk=sql_session.pk) for prop, value in updated_properties.items(): self.assertEqual(getattr(sql_session, prop), value)
def sms_keyword_handler(v, text, msg): text = text.strip() if text == "": return False sessions = XFormsSession.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 handle_sms_form_complete(sender, session_id, form, **kwargs): from corehq.apps.smsforms.models import XFormsSession session = XFormsSession.latest_by_session_id(session_id) if session: # i don't know if app_id is the id of the overall app or the id of the specific build of the app # the thing i want to pass in is the id of the overall app resp = spoof_submission(get_submit_url(session.domain, session.app_id), form, hqsubmission=False) xform_id = resp["X-CommCareHQ-FormID"] session.end(completed=True) session.submission_id = xform_id session.save()
def handle_sms_form_complete(sender, session_id, form, **kwargs): from corehq.apps.smsforms.models import XFormsSession session = XFormsSession.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 handle(self, *args, **options): db = XFormsSession.get_db() session_ids = [row['id'] for row in db.view("smsforms/sessions_by_touchforms_id")] errors = [] for session_doc in iter_docs(db, session_ids): try: # Handle the old touchforms session id convention where it was # always an int session_id = session_doc.get("session_id", None) if isinstance(session_id, int): session_doc["session_id"] = str(session_id) couch_session = XFormsSession.wrap(session_doc) sync_sql_session_from_couch_session(couch_session) except Exception as e: logging.exception('problem migrating session {}: {}'.format(session_doc['_id'], e)) errors.append(session_doc['_id']) print 'migrated {} couch sessions. there are now {} in sql'.format( len(session_ids) - len(errors), SQLXFormsSession.objects.count() ) if errors: print 'errors: {}'.format(', '.join(errors))
def handle(self, *args, **options): sessions = XFormsSession.view( "smsforms/open_sms_sessions_by_connection", include_docs=True).all() for session in sessions: try: get_raw_instance(session.session_id) except InvalidSessionIdException: print "Closing %s %s" % (session.domain, session._id) session.end(False) session.save() except Exception as e: print "An unexpected error occurred when processing %s %s" % ( session.domain, session._id) print e
def handle(self, *args, **options): sessions = XFormsSession.view( "smsforms/open_sms_sessions_by_connection", include_docs=True ).all() for session in sessions: try: get_raw_instance(session.session_id) except InvalidSessionIdException: print "Closing %s %s" % (session.domain, session._id) session.end(False) session.save() except Exception as e: print "An unexpected error occurred when processing %s %s" % (session.domain, session._id) print e
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(XFormsSession.get_all_open_sms_sessions(domain, contact))) self.assertEqual(0, len(SQLXFormsSession.get_all_open_sms_sessions(domain, contact)))
def handle_sms_form_complete(sender, session_id, form, **kwargs): from corehq.apps.smsforms.models import XFormsSession session = XFormsSession.latest_by_session_id(session_id) if session: # i don't know if app_id is the id of the overall app or the id of the specific build of the app # the thing i want to pass in is the id of the overall app resp = spoof_submission(get_submit_url(session.domain, session.app_id), form, hqsubmission=False) 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 get_single_open_session(domain, contact_id): """ Retrieves the current open XFormsSession 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 = XFormsSession.get_all_open_sms_sessions(domain, contact_id) if len(sessions) > 1: for session in sessions: session.end(False) session.save() return (True, None) session = sessions[0] if len(sessions) == 1 else None return (False, session)
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 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 form_session_handler(v, text): # Circular Import from corehq.apps.reminders.models import SurveyKeyword, FORM_TYPE_ONE_BY_ONE # Handle incoming sms session = XFormsSession.view("smsforms/open_sms_sessions_by_connection", key=[v.domain, v.owner_id], include_docs=True).one() text_words = text.upper().split() # Respond to "#START <keyword>" command if len(text_words) > 0 and text_words[0] == "#START": if len(text_words) > 1: sk = SurveyKeyword.get_keyword(v.domain, text_words[1]) if sk is not None and sk.form_type == FORM_TYPE_ONE_BY_ONE: if session is not None: session.end(False) session.save() start_session_from_keyword(sk, v) else: send_sms_to_verified_number( v, "Survey '" + text_words[1] + "' not found.") else: send_sms_to_verified_number(v, "Usage: #START <keyword>") # Respond to "#STOP" keyword elif len(text_words) > 0 and text_words[0] == "#STOP": if session is not None: session.end(False) session.save() # Respond to "#CURRENT" keyword elif len(text_words) > 0 and text_words[0] == "#CURRENT": if session is not None: resp = current_question(session.session_id) send_sms_to_verified_number(v, resp.event.text_prompt) # Respond to unknown command elif len(text_words) > 0 and text_words[0][0] == "#": send_sms_to_verified_number(v, "Unknown command '" + text_words[0] + "'") # If there's an open session, treat the inbound text as the answer to the next question elif session is not None: resp = current_question(session.session_id) event = resp.event valid, text, error_msg = validate_answer(event, text) if valid: responses = _get_responses(v.domain, v.owner_id, text) if len(responses) > 0: response_text = format_message_list(responses) send_sms_to_verified_number(v, response_text) else: send_sms_to_verified_number(v, error_msg + event.text_prompt) # Try to match the text against a keyword to start a survey elif len(text_words) > 0: sk = SurveyKeyword.get_keyword(v.domain, text_words[0]) if sk is not None and sk.form_type == FORM_TYPE_ONE_BY_ONE: start_session_from_keyword(sk, v) # TODO should clarify what scenarios this handler actually handles. i.e., # should the error responses instead be handler by some generic error/fallback # handler return True
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
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 = XFormsSession.get(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)
def rows(self): startdate = json_format_datetime(self.datespan.startdate_utc) enddate = json_format_datetime(self.datespan.enddate_utc) data = CallLog.by_domain_date(self.domain, startdate, enddate) result = [] # Store the results of lookups for faster loading contact_cache = {} form_map = {} xforms_sessions = {} direction_map = { INCOMING: _("Incoming"), OUTGOING: _("Outgoing"), } # Retrieve message log options message_log_options = getattr(settings, "MESSAGE_LOG_OPTIONS", {}) abbreviated_phone_number_domains = message_log_options.get( "abbreviated_phone_number_domains", []) abbreviate_phone_number = (self.domain in abbreviated_phone_number_domains) for call in data: doc_info = self.get_recipient_info(call, contact_cache) form_unique_id = call.form_unique_id if form_unique_id in [None, ""]: form_name = "-" elif form_unique_id in form_map: form_name = form_map.get(form_unique_id) else: form_name = get_form_name(form_unique_id) form_map[form_unique_id] = form_name phone_number = call.phone_number if abbreviate_phone_number and phone_number is not None: phone_number = phone_number[0:7] if phone_number[ 0:1] == "+" else phone_number[0:6] timestamp = tz_utils.adjust_datetime_to_timezone( call.date, pytz.utc.zone, self.timezone.zone) if call.direction == INCOMING: answered = "-" else: answered = _("Yes") if call.answered else _("No") if call.xforms_session_id: xforms_sessions[call.xforms_session_id] = None row = [ call.xforms_session_id, self._fmt_timestamp(timestamp), self._fmt_contact_link(call, doc_info), self._fmt(phone_number), self._fmt(direction_map.get(call.direction, "-")), self._fmt(form_name), self._fmt("-"), self._fmt(answered), self._fmt(call.duration), self._fmt(_("Yes") if call.error else _("No")), self._fmt(call.error_message), ] if self.request.couch_user.is_previewer(): row.append(self._fmt(call.gateway_session_id)) result.append(row) # Look up the XFormsSession documents 500 at a time. # Had to do this because looking up one document at a time slows things # down a lot. all_session_ids = xforms_sessions.keys() limit = 500 range_max = int(ceil(len(all_session_ids) * 1.0 / limit)) for i in range(range_max): lower_bound = i * limit upper_bound = (i + 1) * limit sessions = XFormsSession.view( "smsforms/sessions_by_touchforms_id", keys=all_session_ids[lower_bound:upper_bound], include_docs=True).all() for session in sessions: xforms_sessions[session.session_id] = session.submission_id # Add into the final result the link to the submission based on the # outcome of the above lookups. final_result = [] for row in result: final_row = row[1:] session_id = row[0] if session_id: submission_id = xforms_sessions[session_id] if submission_id: final_row[5] = self._fmt_submission_link(submission_id) final_result.append(final_row) return final_result
def form_session_handler(v, text): # Circular Import from corehq.apps.reminders.models import SurveyKeyword, FORM_TYPE_ONE_BY_ONE # Handle incoming sms session = XFormsSession.view( "smsforms/open_sms_sessions_by_connection", key=[v.domain, v.owner_id], include_docs=True ).one() text_words = text.upper().split() # Respond to "#START <keyword>" command if len(text_words) > 0 and text_words[0] == "#START": if len(text_words) > 1: sk = SurveyKeyword.get_keyword(v.domain, text_words[1]) if sk is not None and sk.form_type == FORM_TYPE_ONE_BY_ONE: if session is not None: session.end(False) session.save() start_session_from_keyword(sk, v) else: send_sms_to_verified_number(v, "Survey '" + text_words[1] + "' not found.") else: send_sms_to_verified_number(v, "Usage: #START <keyword>") # Respond to "#STOP" keyword elif len(text_words) > 0 and text_words[0] == "#STOP": if session is not None: session.end(False) session.save() # Respond to "#CURRENT" keyword elif len(text_words) > 0 and text_words[0] == "#CURRENT": if session is not None: resp = current_question(session.session_id) send_sms_to_verified_number(v, resp.event.text_prompt) # Respond to unknown command elif len(text_words) > 0 and text_words[0][0] == "#": send_sms_to_verified_number(v, "Unknown command '" + text_words[0] + "'") # If there's an open session, treat the inbound text as the answer to the next question elif session is not None: resp = current_question(session.session_id) event = resp.event valid, text, error_msg = validate_answer(event, text) if valid: responses = _get_responses(v.domain, v.owner_id, text) if len(responses) > 0: response_text = format_message_list(responses) send_sms_to_verified_number(v, response_text) else: send_sms_to_verified_number(v, error_msg + event.text_prompt) # Try to match the text against a keyword to start a survey elif len(text_words) > 0: sk = SurveyKeyword.get_keyword(v.domain, text_words[0]) if sk is not None and sk.form_type == FORM_TYPE_ONE_BY_ONE: start_session_from_keyword(sk, v) # TODO should clarify what scenarios this handler actually handles. i.e., # should the error responses instead be handler by some generic error/fallback # handler 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 = 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) 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 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 sms_keyword_handler(v, text, msg=None): from corehq.apps.reminders.models import SurveyKeyword text = text.strip() if text == "": return False sessions = XFormsSession.get_all_open_sms_sessions(v.domain, v.owner_id) any_session_open = len(sessions) > 0 text_words = text.upper().split() if text.startswith("#"): if len(text_words) > 0 and text_words[0] == "#START": # Respond to "#START <keyword>" command if len(text_words) > 1: sk = SurveyKeyword.get_keyword(v.domain, text_words[1]) if sk is not None: if len(sk.initiator_doc_type_filter) > 0 and v.owner_doc_type not in sk.initiator_doc_type_filter: # The contact type is not allowed to invoke this keyword return False process_survey_keyword_actions(v, sk, text[6:].strip(), msg=msg) else: send_sms_to_verified_number( v, "Keyword not found: '%s'." % text_words[1], workflow=WORKFLOW_KEYWORD ) else: send_sms_to_verified_number(v, "Usage: #START <keyword>", workflow=WORKFLOW_KEYWORD) elif len(text_words) > 0 and text_words[0] == "#STOP": # Respond to "#STOP" keyword XFormsSession.close_all_open_sms_sessions(v.domain, v.owner_id) elif len(text_words) > 0 and text_words[0] == "#CURRENT": # Respond to "#CURRENT" keyword if len(sessions) == 1: resp = current_question(sessions[0].session_id) send_sms_to_verified_number( v, resp.event.text_prompt, workflow=sessions[0].workflow, reminder_id=sessions[0].reminder_id, xforms_session_couch_id=sessions[0]._id, ) else: # Response to unknown command send_sms_to_verified_number(v, "Unknown command: '%s'" % text_words[0]) if msg is not None: msg.workflow = WORKFLOW_KEYWORD msg.save() return True else: for survey_keyword in SurveyKeyword.get_all(v.domain): 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(): if any_session_open and not survey_keyword.override_open_sessions: # We don't want to override any open sessions, so just pass and let the form session handler handle the message return False elif ( len(survey_keyword.initiator_doc_type_filter) > 0 and v.owner_doc_type not in survey_keyword.initiator_doc_type_filter ): # The contact type is not allowed to invoke this keyword return False else: process_survey_keyword_actions(v, survey_keyword, text, msg=msg) if msg is not None: msg.workflow = WORKFLOW_KEYWORD msg.save() return True # No keywords matched, so pass the message onto the next handler 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(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 rows(self): startdate = json_format_datetime(self.datespan.startdate_utc) enddate = json_format_datetime(self.datespan.enddate_utc) data = CallLog.by_domain_date(self.domain, startdate, enddate) result = [] # Store the results of lookups for faster loading username_map = {} form_map = {} direction_map = { INCOMING: _("Incoming"), OUTGOING: _("Outgoing"), } # Retrieve message log options message_log_options = getattr(settings, "MESSAGE_LOG_OPTIONS", {}) abbreviated_phone_number_domains = message_log_options.get( "abbreviated_phone_number_domains", []) abbreviate_phone_number = (self.domain in abbreviated_phone_number_domains) for call in data: recipient_id = call.couch_recipient if recipient_id in [None, ""]: username = "******" elif recipient_id in username_map: username = username_map.get(recipient_id) else: username = "******" try: if call.couch_recipient_doc_type == "CommCareCase": username = CommCareCase.get(recipient_id).name else: username = CouchUser.get_by_user_id( recipient_id).username except Exception: pass username_map[recipient_id] = username form_unique_id = call.form_unique_id if form_unique_id in [None, ""]: form_name = "-" elif form_unique_id in form_map: form_name = form_map.get(form_unique_id) else: form_name = get_form_name(form_unique_id) form_map[form_unique_id] = form_name phone_number = call.phone_number if abbreviate_phone_number and phone_number is not None: phone_number = phone_number[0:7] if phone_number[ 0:1] == "+" else phone_number[0:6] timestamp = tz_utils.adjust_datetime_to_timezone( call.date, pytz.utc.zone, self.timezone.zone) if call.direction == INCOMING: answered = "-" else: answered = _("Yes") if call.answered else _("No") if call.xforms_session_id is None: submission_id = None else: session = XFormsSession.latest_by_session_id( call.xforms_session_id) submission_id = session.submission_id row = [ self._fmt_timestamp(timestamp), self._fmt(username), self._fmt(phone_number), self._fmt(direction_map.get(call.direction, "-")), self._fmt(form_name), self._fmt("-") if submission_id is None else self._fmt_submission_link(submission_id), self._fmt(answered), self._fmt(call.duration), self._fmt(_("Yes") if call.error else _("No")), self._fmt(call.error_message), ] if self.request.couch_user.is_previewer(): row.append(self._fmt(call.gateway_session_id)) result.append(row) return result
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 global_keyword_stop(v, text, msg, text_words, open_sessions): XFormsSession.close_all_open_sms_sessions(v.domain, v.owner_id) return True
def rows(self): startdate = json_format_datetime(self.datespan.startdate_utc) enddate = json_format_datetime(self.datespan.enddate_utc) data = CallLog.by_domain_date(self.domain, startdate, enddate) result = [] # Store the results of lookups for faster loading username_map = {} form_map = {} direction_map = { INCOMING: _("Incoming"), OUTGOING: _("Outgoing"), } # Retrieve message log options message_log_options = getattr(settings, "MESSAGE_LOG_OPTIONS", {}) abbreviated_phone_number_domains = message_log_options.get("abbreviated_phone_number_domains", []) abbreviate_phone_number = (self.domain in abbreviated_phone_number_domains) for call in data: recipient_id = call.couch_recipient if recipient_id in [None, ""]: username = "******" elif recipient_id in username_map: username = username_map.get(recipient_id) else: username = "******" try: if call.couch_recipient_doc_type == "CommCareCase": username = CommCareCase.get(recipient_id).name else: username = CouchUser.get_by_user_id(recipient_id).username except Exception: pass username_map[recipient_id] = username form_unique_id = call.form_unique_id if form_unique_id in [None, ""]: form_name = "-" elif form_unique_id in form_map: form_name = form_map.get(form_unique_id) else: form_name = get_form_name(form_unique_id) form_map[form_unique_id] = form_name phone_number = call.phone_number if abbreviate_phone_number and phone_number is not None: phone_number = phone_number[0:7] if phone_number[0:1] == "+" else phone_number[0:6] timestamp = tz_utils.adjust_datetime_to_timezone(call.date, pytz.utc.zone, self.timezone.zone) if call.direction == INCOMING: answered = "-" else: answered = _("Yes") if call.answered else _("No") if call.xforms_session_id is None: submission_id = None else: session = XFormsSession.latest_by_session_id(call.xforms_session_id) submission_id = session.submission_id row = [ self._fmt_timestamp(timestamp), self._fmt(username), self._fmt(phone_number), self._fmt(direction_map.get(call.direction,"-")), self._fmt(form_name), self._fmt("-") if submission_id is None else self._fmt_submission_link(submission_id), self._fmt(answered), self._fmt(call.duration), self._fmt(_("Yes") if call.error else _("No")), self._fmt(call.error_message), ] if self.request.couch_user.is_previewer(): row.append(self._fmt(call.gateway_session_id)) result.append(row) return result
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: logging.exception("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 = XFormsSession.get(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): 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 process_survey_keyword_actions(verified_number, survey_keyword, text, msg): sender = verified_number.owner case = None args = split_args(text, survey_keyword) # Close any open sessions even if it's just an sms that we're # responding with. XFormsSession.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 = get_case_by_external_id(verified_number.domain, external_id) if case is None or not user_can_access_case(sender, case): send_keyword_response(verified_number, MSG_CASE_NOT_FOUND) return else: send_keyword_response(verified_number, MSG_MISSING_EXTERNAL_ID) 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 # 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) 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) 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) if not res: # If the structured sms processing wasn't successful, don't # process any of the other actions return
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 rows(self): startdate = json_format_datetime(self.datespan.startdate_utc) enddate = json_format_datetime(self.datespan.enddate_utc) data = CallLog.by_domain_date(self.domain, startdate, enddate) result = [] # Store the results of lookups for faster loading contact_cache = {} form_map = {} xforms_sessions = {} direction_map = { INCOMING: _("Incoming"), OUTGOING: _("Outgoing"), } # Retrieve message log options message_log_options = getattr(settings, "MESSAGE_LOG_OPTIONS", {}) abbreviated_phone_number_domains = message_log_options.get("abbreviated_phone_number_domains", []) abbreviate_phone_number = (self.domain in abbreviated_phone_number_domains) for call in data: doc_info = self.get_recipient_info(call, contact_cache) form_unique_id = call.form_unique_id if form_unique_id in [None, ""]: form_name = "-" elif form_unique_id in form_map: form_name = form_map.get(form_unique_id) else: form_name = get_form_name(form_unique_id) form_map[form_unique_id] = form_name phone_number = call.phone_number if abbreviate_phone_number and phone_number is not None: phone_number = phone_number[0:7] if phone_number[0:1] == "+" else phone_number[0:6] timestamp = tz_utils.adjust_datetime_to_timezone(call.date, pytz.utc.zone, self.timezone.zone) if call.direction == INCOMING: answered = "-" else: answered = _("Yes") if call.answered else _("No") if call.xforms_session_id: xforms_sessions[call.xforms_session_id] = None row = [ call.xforms_session_id, self._fmt_timestamp(timestamp), self._fmt_contact_link(call, doc_info), self._fmt(phone_number), self._fmt(direction_map.get(call.direction,"-")), self._fmt(form_name), self._fmt("-"), self._fmt(answered), self._fmt(call.duration), self._fmt(_("Yes") if call.error else _("No")), self._fmt(call.error_message), ] if self.request.couch_user.is_previewer(): row.append(self._fmt(call.gateway_session_id)) result.append(row) # Look up the XFormsSession documents 500 at a time. # Had to do this because looking up one document at a time slows things # down a lot. all_session_ids = xforms_sessions.keys() limit = 500 range_max = int(ceil(len(all_session_ids) * 1.0 / limit)) for i in range(range_max): lower_bound = i * limit upper_bound = (i + 1) * limit sessions = XFormsSession.view("smsforms/sessions_by_touchforms_id", keys=all_session_ids[lower_bound:upper_bound], include_docs=True).all() for session in sessions: xforms_sessions[session.session_id] = session.submission_id # Add into the final result the link to the submission based on the # outcome of the above lookups. final_result = [] for row in result: final_row = row[1:] session_id = row[0] if session_id: submission_id = xforms_sessions[session_id] if submission_id: final_row[5] = self._fmt_submission_link(submission_id) final_result.append(final_row) return final_result
def form_session_handler(v, text): # Circular Import from corehq.apps.reminders.models import SurveyKeyword # Handle incoming sms session = XFormsSession.view("smsforms/open_sms_sessions_by_connection", key=[v.domain, v.owner_id], include_docs=True).one() text_words = text.upper().split() # Respond to "#START <keyword>" command if len(text_words) > 0 and text_words[0] == "#START": if len(text_words) > 1: sk = SurveyKeyword.get_keyword(v.domain, text_words[1]) if sk is not None: if session is not None: session.end(False) session.save() start_session_from_keyword(sk, v) else: send_sms_to_verified_number(v, "Survey '" + text_words[1] + "' not found.") else: send_sms_to_verified_number(v, "Usage: #START <keyword>") # Respond to "#STOP" keyword elif len(text_words) > 0 and text_words[0] == "#STOP": if session is not None: session.end(False) session.save() # Respond to "#CURRENT" keyword elif len(text_words) > 0 and text_words[0] == "#CURRENT": if session is not None: resp = current_question(session.session_id) send_sms_to_verified_number(v, resp.event.text_prompt) # Respond to unknown command elif len(text_words) > 0 and text_words[0][0] == "#": send_sms_to_verified_number(v, "Unknown command '" + text_words[0] + "'") # If there's an open session, treat the inbound text as the answer to the next question elif session is not None: resp = current_question(session.session_id) event = resp.event valid = False text = text.strip() upper_text = text.upper() # Validate select if event.datatype == "select": # Try to match on phrase (i.e., "Yes" or "No") choices = format_choices(event._dict["choices"]) if upper_text in choices: text = str(choices[upper_text]) valid = True else: try: answer = int(text) if answer >= 1 and answer <= len(event._dict["choices"]): valid = True except ValueError: pass # Validate multiselect elif event.datatype == "multiselect": choices = format_choices(event._dict["choices"]) max_index = len(event._dict["choices"]) proposed_answers = text.split() final_answers = {} try: if event._dict.get("required", True): assert len(proposed_answers) > 0 for answer in proposed_answers: upper_answer = answer.upper() if upper_answer in choices: final_answers[str(choices[upper_answer])] = "" else: int_answer = int(answer) assert int_answer >= 1 and int_answer <= max_index final_answers[str(int_answer)] = "" text = " ".join(final_answers.keys()) valid = True except Exception: pass # Validate int elif event.datatype == "int": try: int(text) valid = True except ValueError: pass # Validate float elif event.datatype == "float": try: float(text) valid = True except ValueError: pass # Validate longint elif event.datatype == "longint": try: long(text) valid = True except ValueError: pass # Validate date (Format: YYYYMMDD) elif event.datatype == "date": try: assert len(text) == 8 int(text) text = text[0:4] + "-" + text[4:6] + "-" + text[6:] parse(text) valid = True except Exception: pass # Validate time (Format: HHMM, 24-hour) elif event.datatype == "time": try: assert len(text) == 4 hour = int(text[0:2]) minute = int(text[2:]) assert hour >= 0 and hour <= 23 assert minute >= 0 and minute <= 59 text = "%s:%s" % (hour, minute) valid = True except Exception: pass # Other question types pass else: valid = True if valid: responses = _get_responses(v.domain, v.owner_id, text) if len(responses) > 0: response_text = format_message_list(responses) send_sms_to_verified_number(v, response_text) else: error_msg = "Invalid Response. " + event.text_prompt send_sms_to_verified_number(v, error_msg) # Try to match the text against a keyword to start a survey elif len(text_words) > 0: sk = SurveyKeyword.get_keyword(v.domain, text_words[0]) if sk is not None: start_session_from_keyword(sk, v) # TODO should clarify what scenarios this handler actually handles. i.e., # should the error responses instead be handler by some generic error/fallback # handler return True