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 answer_next_question(v, text, msg, session): resp = current_question(session.session_id) event = resp.event valid, text, error_msg = validate_answer(event, text, v) # metadata to be applied to the reply message outbound_metadata = MessageMetadata( workflow=session.workflow, reminder_id=session.reminder_id, xforms_session_couch_id=session._id, ) if valid: responses = _get_responses(v.domain, v.owner_id, text, yield_responses=True) if has_invalid_response(responses): 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, metadata=outbound_metadata) else: mark_as_invalid_response(msg) response_text = "%s %s" % (error_msg, event.text_prompt) send_sms_to_verified_number(v, response_text, metadata=outbound_metadata)
def answer_next_question(v, text, msg, session): resp = current_question(session.session_id, domain=v.domain) event = resp.event valid, text, error_msg = validate_answer(event, text, v) # metadata to be applied to the reply message outbound_metadata = MessageMetadata( workflow=session.workflow, reminder_id=session.reminder_id, xforms_session_couch_id=session._id, ) if valid: responses = get_responses(v.domain, session.session_id, text) if has_invalid_response(responses): 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, metadata=outbound_metadata) else: mark_as_invalid_response(msg) response_text = "%s %s" % (error_msg, event.text_prompt) send_sms_to_verified_number(v, response_text, metadata=outbound_metadata)
def send_first_message(domain, recipient, phone_entry_or_number, session, responses, logged_subevent, workflow): # This try/except section is just here (temporarily) to support future refactors # If any of these notify, they should be replaced with a comment as to why the two are different # so that someone refactoring in the future will know that this or that param is necessary. try: if session.workflow != workflow: # see if we can eliminate the workflow arg notify_error('Exploratory: session.workflow != workflow', details={ 'session.workflow': session.workflow, 'workflow': workflow}) if session.connection_id != recipient.get_id: # see if we can eliminate the recipient arg notify_error('Exploratory: session.connection_id != recipient.get_id', details={ 'session.connection_id': session.connection_id, 'recipient.get_id': recipient.get_id, 'recipient': recipient }) if session.related_subevent != logged_subevent: # see if we can eliminate the logged_subevent arg notify_error('Exploratory: session.related_subevent != logged_subevent', details={ 'session.connection_id': session.connection_id, 'logged_subevent': logged_subevent}) except Exception: # The above running is not mission critical, so if it errors just leave a message in the log # for us to follow up on. # Absence of the message below and messages above ever notifying # will indicate that we can remove these args. notify_exception(None, "Error in section of code that's just supposed help inform future refactors") if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(domain): if not XFormsSessionSynchronization.claim_channel_for_session(session): send_first_message.apply_async( args=(domain, recipient, phone_entry_or_number, session, responses, logged_subevent, workflow), countdown=60 ) return metrics_counter('commcare.smsforms.session_started', 1, tags={'domain': domain, 'workflow': workflow}) if len(responses) > 0: message = format_message_list(responses) metadata = MessageMetadata( workflow=workflow, xforms_session_couch_id=session.couch_id, ) if isinstance(phone_entry_or_number, PhoneNumber): send_sms_to_verified_number( phone_entry_or_number, message, metadata, logged_subevent=logged_subevent ) else: send_sms( domain, recipient, phone_entry_or_number, message, metadata ) logged_subevent.completed()
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 send_first_message(self, domain, recipient, phone_entry_or_number, session, responses, logged_subevent, workflow): if len(responses) > 0: message = format_message_list(responses) metadata = MessageMetadata( workflow=workflow, xforms_session_couch_id=session.couch_id, ) if isinstance(phone_entry_or_number, PhoneNumber): send_sms_to_verified_number(phone_entry_or_number, message, metadata, logged_subevent=logged_subevent) else: send_sms(domain, recipient, phone_entry_or_number, message, metadata)
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 send_first_message(self, domain, recipient, phone_entry_or_number, session, responses, logged_subevent, workflow): if len(responses) > 0: message = format_message_list(responses) metadata = MessageMetadata( workflow=workflow, xforms_session_couch_id=session.couch_id, ) if isinstance(phone_entry_or_number, PhoneNumber): send_sms_to_verified_number( phone_entry_or_number, message, metadata, logged_subevent=logged_subevent ) else: send_sms( domain, recipient, phone_entry_or_number, message, metadata )
def answer_next_question(verified_number, text, msg, session, subevent_id): resp = FormplayerInterface(session.session_id, verified_number.domain).current_question() event = resp.event valid, text, error_msg = validate_answer(event, text, verified_number) # metadata to be applied to the reply message outbound_metadata = MessageMetadata( workflow=session.workflow, reminder_id=session.reminder_id, xforms_session_couch_id=session._id, messaging_subevent_id=subevent_id, ) if valid: responses = get_responses(verified_number.domain, session.session_id, text) if has_invalid_response(responses): mark_as_invalid_response(msg) text_responses = _responses_to_text(responses) events = get_events_from_responses(responses) if len(text_responses) > 0: response_text = format_message_list(text_responses) send_sms_to_verified_number(verified_number, response_text, metadata=outbound_metadata, events=events) else: mark_as_invalid_response(msg) response_text = "%s %s" % (error_msg, event.text_prompt) send_sms_to_verified_number(verified_number, response_text, metadata=outbound_metadata, events=[event])
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
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 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, 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 incoming(phone_number, text, backend_api): phone_without_plus = str(phone_number) if phone_without_plus[0] == "+": phone_without_plus = phone_without_plus[1:] phone_with_plus = "+" + phone_without_plus # Circular Import from corehq.apps.reminders.models import SurveyKeyword v = VerifiedNumber.view("sms/verified_number_by_number", key=phone_without_plus, include_docs=True ).one() # Log message in message log msg = SMSLog( phone_number = phone_with_plus, direction = INCOMING, date = datetime.utcnow(), text = text, backend_api = backend_api ) if v is not None: msg.couch_recipient_doc_type = v.owner_doc_type msg.couch_recipient = v.owner_id msg.domain = v.domain msg.save() # Handle incoming sms if v is not None: session = XFormsSession.view("smsforms/open_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 error_msg = None # Validate select questions if event.datatype == "select": try: answer = int(text.strip()) if answer >= 1 and answer <= len(event._dict["choices"]): valid = True except Exception: pass if not valid: error_msg = "Invalid Response. " + event.text_prompt # For now, anything else passes else: valid = True if valid: responses = get_responses(msg) 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) # 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) else: #TODO: Registration via SMS pass
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_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 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
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 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