def _allowed_to_participate(self, message): if message.reporter: iavi_reporter = IaviReporter.objects.get(pk=message.reporter.pk) if iavi_reporter.pin: return True else: message.respond(_(strings["rejection_no_pin"], get_language_code(message.persistant_connection))) else: message.respond(_(strings["rejection_unknown_user"], get_language_code(message.persistant_connection))) return False
def _process_pin(self, message): language = get_language_code(message.persistant_connection) incoming_pin = message.text.strip() reporter = IaviReporter.objects.get(pk=message.reporter.pk) if self.pending_pins[reporter.pk]: # this means it has already been set once # check if they are equal and if so save pending_pin = self.pending_pins.pop(reporter.pk) if incoming_pin == pending_pin: # success! reporter.pin = pending_pin reporter.save() message.respond(_(strings["pin_set"], language)) else: # oops they didn't match. send a failure string message.respond(_(strings["pin_mismatch"], language) % {"alias": reporter.study_id}) # put an empty value back in the list of pins self.pending_pins[reporter.pk] = None else: # this is their first try. make sure # it's 4 numeric digits and if so ask for confirmation if re.match(r"^(\d{4})$", incoming_pin): self.pending_pins[reporter.pk] = incoming_pin message.respond(_(strings["pin_request_again"], language)) else: # bad format. send a failure string and prompt again message.respond(_(strings["bad_pin_format"], language) % {"alias": reporter.study_id}) return True
def _initiate_tree_sequence(self, user, initiator=None): user_conn = user.connection() if user_conn: db_backend = user_conn.backend # we need to get the real backend from the router # to properly send it real_backend = self.router.get_backend(db_backend.slug) if real_backend: connection = Connection(real_backend, user_conn.identity) text = self._get_tree_sequence(user) if not text: return _(strings["unknown_survey_location"], get_language_code(user.connection)) % ({"location":user.location, "alias":user.study_id}) else: # first ask the tree app to end any sessions it has open if self.tree_app: self.tree_app.end_sessions(user_conn) if initiator: # if this was initiated by someone else # create an entry for this so they can be # notified upon completion, and also so # we can ignore the data TestSession.objects.create(initiator=initiator, tester=user, status="A") start_msg = Message(connection, text) self.router.incoming(start_msg) return else: error = "Can't find backend %s. Messages will not be sent" % connection.backend.slug self.error(error) return error else: error = "Can't find connection %s. Messages will not be sent" % user_conn self.error(error) return error
def actions(self, *args, **kwargs): message = args[0] form_entry = args[1] # we'll be using the language in all our responses so # keep it handy language = get_language_code(message.persistant_connection) if form_entry.form.code.abbreviation == "register": # todo: registration alias = form_entry.reg_data["alias"] language = form_entry.reg_data["language"] location = form_entry.reg_data["location"] real_time = form_entry.reg_data["time"] study_id = form_entry.reg_data["study_id"] # create the reporter object for this person reporter = IaviReporter(alias=alias, language=language, location=location, registered=message.date) reporter.save() # create the study participant for this too. Assume they're starting # tomorrow and don't set a stop date. start_date = (datetime.today() + timedelta(days=1)).date() participant = StudyParticipant.objects.create(reporter=reporter, start_date = start_date, notification_time = real_time) # also attach the reporter to the connection message.persistant_connection.reporter=reporter message.persistant_connection.save() message.respond(_(strings["registration_complete"], language) % {"alias": study_id }) # also send the PIN request and add this user to the # pending pins self.app.pending_pins[reporter.pk] = None message.respond(_(strings["pin_request"], language)) elif form_entry.form.code.abbreviation == "test": # initiate the tree sequence for the user we set # during validation user = form_entry.test_data["reporter"] errors = self.app._initiate_tree_sequence(user, message.persistant_connection) if errors: message.respond(errors) else: message.respond(_(strings["test_initiated"], language) % {"alias" : form_entry.test_data["study_id"]})
def _send_question(self, session, msg=None): '''Sends the next question in the session, if there is one''' state = session.state if state and state.question: response = _(state.question.text, get_language_code(session.connection)) self.info("Sending: %s" % response) if msg: msg.respond(response) else: # we need to get the real backend from the router # to properly send it real_backend = self.router.get_backend(session.connection.backend.slug) if real_backend: connection = Connection(real_backend, session.connection.identity) outgoing_msg = Message(connection, response) self.router.outgoing(outgoing_msg) else: # todo: do we want to fail more loudly than this? error = "Can't find backend %s. Messages will not be sent" % connection.backend.slug self.error(error)
def _initiate_tree_sequence(self, user, initiator=None): user_conn = user.connection() if user_conn: tree= self._get_tree(user) if not tree: return _(strings["unknown_survey_location"], get_language_code(user.connection)) % ({"location":user.location, "alias":user.study_id}) if initiator: # if this was initiated by someone else # create an entry for this so they can be # notified upon completion, and also so # we can ignore the data TestSession.objects.create(initiator=initiator, tester=user, status="A") if self.tree_app: self.tree_app.start_tree(tree, user_conn) else: error = "Can't find connection %s. Messages will not be sent" % user_conn self.error(error) return error
def handle(self, msg): # if this caller doesn't have a session attribute, # they're not currently answering a question tree, so # just search for triggers and return sessions = Session.objects.all().filter(state__isnull=False)\ .filter(connection=msg.persistant_connection) if not sessions: try: tree = Tree.objects.get(trigger=msg.text) # start a new session for this person and save it session = Session(connection=msg.persistant_connection, tree=tree, state=tree.root_state, num_tries=0) session.save() self.debug("session %s saved" % session) # also notify any session listeners of this # so they can do their thing if self.session_listeners.has_key(tree.trigger): for func in self.session_listeners[tree.trigger]: func(session, False) # no trigger found? no big deal. the # message is probably for another app except Tree.DoesNotExist: return False # the caller is part-way though a question # tree, so check their answer and respond else: session = sessions[0] state = session.state self.debug(state) # loop through all transitions starting with # this state and try each one depending on the type # this will be a greedy algorithm and NOT safe if # multiple transitions can match the same answer transitions = Transition.objects.filter(current_state=state) found_transition = None for transition in transitions: if self.matches(transition.answer, msg): found_transition = transition break # the number of tries they have to get out of this state # if empty there is no limit. When the num_retries is hit # a user's session will be terminated. # not a valid answer, so remind # the user of the valid options. if not found_transition: transitions = Transition.objects.filter(current_state=state) # there are no defined answers. therefore there are no more questions to ask if len(transitions) == 0: # send back some precanned response msg.respond(self.last_message) # end the connection so the caller can start a new session self._end_session(session) return else: # send them some hints about how to respond if state.question.error_response: response = (_(state.question.error_response, get_language_code(session.connection))) if "%(answer)s" in response: response = response % ({"answer" : msg.text}) else: flat_answers = " or ".join([trans.answer.helper_text() for trans in transitions]) translated_answers = _(flat_answers, get_language_code(session.connection)) response = _('"%(answer)s" is not a valid answer. You must enter %(hint)s', get_language_code(session.connection))% ({"answer" : msg.text, "hint": translated_answers}) msg.respond(response) # update the number of times the user has tried # to answer this. If they have reached the # maximum allowed then end their session and # send them an error message. session.num_tries = session.num_tries + 1 if state.num_retries and session.num_tries >= state.num_retries: session.state = None msg.respond(_("Sorry, invalid answer %(retries)s times. Your session will now end. Please try again later.", get_language_code(session.connection)) % {"retries": session.num_tries }) session.save() return True # create an entry for this response # first have to know what sequence number to insert ids = Entry.objects.all().filter(session=session).order_by('sequence_id').values_list('sequence_id', flat=True) if ids: # not sure why pop() isn't allowed... sequence = ids[len(ids) -1] + 1 else: sequence = 1 entry = Entry(session=session,sequence_id=sequence,transition=found_transition,text=msg.text) entry.save() self.debug("entry %s saved" % entry) # advance to the next question, or remove # this caller's state if there are no more # this might be "None" but that's ok, it will be the # equivalent of ending the session session.state = found_transition.next_state session.num_tries = 0 session.save() # if this was the last message, end the session, # and also check if the tree has a defined # completion text and if so send it if not session.state: self._end_session(session) if session.tree.completion_text: msg.respond(_(session.tree.completion_text, get_language_code(session.connection))) # if there is a next question ready to ask # (and this includes THE FIRST), send it along sessions = Session.objects.all().filter(state__isnull=False).filter(connection=msg.persistant_connection) if sessions: state = sessions[0].state if state.question: msg.respond(_(state.question.text, get_language_code(sessions[0].connection))) self.info(_(state.question.text, get_language_code(sessions[0].connection))) # if we haven't returned long before now, we're # long committed to dealing with this message return True
def _handle_session(self, session, is_ending, klass): self.debug("%s session: %s" % (klass, session)) # get the reporter object reporter = session.connection.reporter iavi_reporter = IaviReporter.objects.get(pk=reporter.pk) if not is_ending: # check if the reporter has an active test session # and if so, link it. Otherwise treat it normally try: test_session = TestSession.objects.get(tester=iavi_reporter, status="A") test_session.tree_session = session test_session.save() except TestSession.DoesNotExist: # not an error, just means this wasn't a test # create a new report for this klass.objects.create(reporter=iavi_reporter, started=session.start_date, session=session, status="A") else: # if we have a test session mark the status and save try: test_session = TestSession.objects.get(tree_session=session) if session.canceled: test_session.status = "F" response = _(strings["test_fail"], get_language_code(test_session.initiator)) % ({"alias": iavi_reporter.study_id}) else: test_session.status = "P" response = _(strings["test_pass"], get_language_code(test_session.initiator)) % ({"alias": iavi_reporter.study_id}) test_session.save() # also have to initiate a callback to the # original person who initiated the test db_backend = test_session.initiator.backend real_backend = self.router.get_backend(db_backend.slug) if real_backend: real_connection = Connection(real_backend, test_session.initiator.identity) response_msg = Message(real_connection, response) self.router.outgoing(response_msg) else: error = "Can't find backend %s. Messages will not be sent" % connection.backend.slug self.error(error) except TestSession.DoesNotExist: # not a big deal. it wasn't a test. # if we have a report # update the data and save try: report = klass.objects.get(session=session) for entry in session.entry_set.all(): answer = entry.transition.answer column = self._get_column(entry.transition.current_state) if column: clean_answer = self._get_clean_answer(answer, entry.text) setattr(report, column, clean_answer) report.completed = datetime.now() if session.canceled: report.status = "C" else: report.status = "F" report.save() except klass.DoesNotExist: # oops, not sure how this could happen, but we don't # want to puke self.error("No report found for session %s" % session)
def handle (self, message): # there are three things this app deals with primarily: # registration, pin setup, and testing # but we'll also use it to circumvent some logic in the tree # app with a few custom codes. if message.text.lower() == "iavi uganda" or message.text.lower() == "iavi kenya": # if they are allowed to participate, return false so # that the message propagates to the tree app. If they # aren't this will come back as handled and the tree app # will never see it. # ASSUMES ORDERING OF APPS AND THAT THIS IS BEFORE TREE return not self._allowed_to_participate(message) # we'll be using the language in all our responses so # keep it handy language = get_language_code(message.persistant_connection) # check pin conditions and process if they match if message.reporter and message.reporter.pk in self.pending_pins: return self._process_pin(message) # registration block # first make sure the string starts and ends with the *# - #* combination match = re.match(r"^\*\#(.*?)\#\*$", message.text) if match: self.info("Message matches! %s", message) body_groups = match.groups()[0].split("#") if len(body_groups)== 3 and body_groups[0] == "8377": # this is the testing format # this is the (extremely ugly) format of testing # *#8377#<Site Number>#<Last 4 Digits of Participant ID>#* # TODO: implement testing code, site, id = body_groups alias = IaviReporter.get_alias(site, id) try: # lookup the user in question and initiate the tree # sequence for them. If there are errors, respond # with them user = IaviReporter.objects.get(alias=alias) errors = self._initiate_tree_sequence(user, message.persistant_connection) if errors: message.respond(errors) except IaviReporter.DoesNotExist: message.respond(_(strings["unknown_user"], language) % {"alias":id}) return True else: # assume this is the registration format # this is the (extremely ugly) format of registration # time is optional # *#<Country/Language Group>#<Site Number>#<Last 3 Digits of Participant ID>#<time?>#* if len(body_groups) == 3: language, site, id = body_groups study_time = "1600" elif len(body_groups) == 4: language, site, id, study_time = body_groups else: message.respond(_(strings["unknown_format"], get_language_code(message.persistant_connection))) # validate the format of the id, existence of location if not re.match(r"^\d{3}$", id): message.respond(_(strings["id_format"], get_language_code(message.persistant_connection)) % {"alias" : id}) return True try: location = Location.objects.get(code=site) except Location.DoesNotExist: message.respond(_(strings["unknown_location"], get_language_code(message.persistant_connection)) % {"alias" : id, "location" : site}) return True # TODO: validate the language # validate and get the time object if re.match(r"^\d{4}$", study_time): hour = int(study_time[0:2]) minute = int(study_time[2:4]) if hour < 0 or hour >= 24 or minute < 0 or minute >= 60: message.respond(_(strings["time_format"], get_language_code(message.persistant_connection)) % {"alias" : id, "time" : study_time}) return real_time = dtt(hour, minute) else: message.respond(_(strings["time_format"], get_language_code(message.persistant_connection)) % {"alias" : id, "time" : study_time}) return # user ids are unique per-location so use location-id # as the alias alias = IaviReporter.get_alias(location.code, id) # make sure this isn't a duplicate alias if len(IaviReporter.objects.filter(alias=alias)) > 0: message.respond(_(strings["already_registered"], language) % {"alias": id, "location":location.code}) return True # create the reporter object for this person reporter = IaviReporter(alias=alias, language=language, location=location, registered=message.date) reporter.save() # create the study participant for this too. Assume they're starting # tomorrow and don't set a stop date. start_date = (datetime.today() + timedelta(days=1)).date() participant = StudyParticipant.objects.create(reporter=reporter, start_date = start_date, notification_time = real_time) # also attach the reporter to the connection message.persistant_connection.reporter=reporter message.persistant_connection.save() message.respond(_(strings["registration_complete"], language) % {"alias": id }) # also send the PIN request and add this user to the # pending pins self.pending_pins[reporter.pk] = None message.respond(_(strings["pin_request"], language)) else: self.info("Message doesn't match. %s", message)
def validate(self, *args, **kwargs): message = args[0] form_entry = args[1] data = form_entry.to_dict() if form_entry.form.code.abbreviation == "register": language = data["language"] site = data["location"] id = data["study_id"] # time is optional study_time = data["time"] if not study_time: study_time = "1600" # validate the format of the id, existence of location if not re.match(r"^\d{3}$", id): return [(_(strings["id_format"], get_language_code(message.persistant_connection)) % {"alias" : id})] try: location = Location.objects.get(code=site) except Location.DoesNotExist: return[(_(strings["unknown_location"], get_language_code(message.persistant_connection)) % {"alias" : id, "location" : site})] # TODO: validate the language # validate and get the time object if re.match(r"^\d{4}$", study_time): hour = int(study_time[0:2]) minute = int(study_time[2:4]) if hour < 0 or hour >= 24 or minute < 0 or minute >= 60: return [(_(strings["time_format"], get_language_code(message.persistant_connection)) % {"alias" : id, "time" : study_time})] real_time = dtt(hour, minute) else: return [(_(strings["time_format"], get_language_code(message.persistant_connection)) % {"alias" : id, "time" : study_time})] # user ids are unique per-location so use location-id # as the alias alias = IaviReporter.get_alias(location.code, id) # make sure this isn't a duplicate alias if len(IaviReporter.objects.filter(alias=alias)) > 0: return [(_(strings["already_registered"], language) % {"alias": id, "location":location.code})] return True data["alias"] = alias data["location"] = location data["time"] = real_time # all fields were present and correct, so copy them into the # form_entry, for "actions" to pick up again without re-fetching form_entry.reg_data = data return [] elif form_entry.form.code.abbreviation == "test": print "test!" print data site = data["location"] id = data["study_id"] alias = IaviReporter.get_alias(site, id) try: # make sure the user in question exists. If not, # respond with an error user = IaviReporter.objects.get(alias=alias) # set some data in the form_entry so we have easy # access to it from within actions data["reporter"] = user data["alias"] = alias form_entry.test_data = data except IaviReporter.DoesNotExist: return [(_(strings["unknown_user"], language) % {"alias":id})]