def get_data(ids): """ returns the data in the format: { '2015-03': { 'domain1': { 'KOOKOO': {'calls': 40, 'minutes': 45} }, 'domain2': { 'KOOKOO': {'calls': 20, 'minutes': 25} 'TELERIVET': {'calls': 5, 'minutes': 0} } } } """ data = {} for doc in iter_docs(CallLog.get_db(), ids): call = CallLog.wrap(doc) month_data = get_month_data(data, call.date) domain_data = get_domain_data(month_data, call.domain) backend_api = get_backend_api(call) backend_data = get_backend_data(domain_data, backend_api) backend_data['calls'] += 1 duration = (call.duration or 0) / 60.0 duration = int(ceil(duration)) backend_data['minutes'] += duration return data
def testCallSync(self): self.deleteAllLogs() self.assertEqual(self.getCallLogCount(), 0) self.assertEqual(self.getCallCount(), 0) # Test Create call = Call() self.setRandomCallValues(call) call.save() sleep(1) self.assertEqual(self.getCallLogCount(), 1) self.assertEqual(self.getCallCount(), 1) calllog = CallLog.get(call.couch_id) self.checkFieldValues(calllog, call, Call._migration_get_fields()) self.assertTrue(CallLog.get_db().get_rev(calllog._id).startswith('2-')) # Test Update self.setRandomCallValues(call) call.save() sleep(1) self.assertEqual(self.getCallLogCount(), 1) self.assertEqual(self.getCallCount(), 1) callog = CallLog.get(call.couch_id) self.checkFieldValues(callog, call, Call._migration_get_fields()) self.assertTrue(CallLog.get_db().get_rev(callog._id).startswith('3-'))
def test_401_response(self): start_count = CallLog.count_by_domain(self.domain) response = Client().post('/twilio/ivr/xxxxx', { 'From': self.phone_number, 'CallSid': 'xyz', }) self.assertEqual(response.status_code, 401) end_count = CallLog.count_by_domain(self.domain) self.assertEqual(start_count, end_count)
def test_log_call(self): if self.__class__ == LogCallTestCase: # The test runner picks up this base class too, but we only # want to run the test on subclasses. return self.assertEqual(CallLog.count_by_domain(self.domain), 0) response = self.simulate_inbound_call(self.phone_number) self.check_response(response) self.assertEqual(CallLog.count_by_domain(self.domain), 1) call = CallLog.by_domain_asc(self.domain).all()[0] self.assertEqual(call.couch_recipient_doc_type, 'CommCareCase') self.assertEqual(call.couch_recipient, self.case.get_id) self.assertEqual(call.direction, INCOMING)
def incoming(phone_number, backend_module, gateway_session_id, ivr_event, input_data=None, duration=None): """ The main entry point for all incoming IVR requests. """ call_log_entry = CallLog.get_call_by_gateway_session_id(gateway_session_id) logged_subevent = None if call_log_entry and call_log_entry.messaging_subevent_id: logged_subevent = MessagingSubEvent.objects.get( pk=call_log_entry.messaging_subevent_id) if call_log_entry: add_metadata(call_log_entry, duration) if call_log_entry and call_log_entry.form_unique_id is None: # If this request is for a call with no form, # then just short circuit everything and hang up return hang_up_response(gateway_session_id, backend_module=backend_module) if call_log_entry and backend_module: return handle_known_call_session(call_log_entry, backend_module, ivr_event, input_data=input_data, logged_subevent=logged_subevent) else: if not call_log_entry: log_call(phone_number, gateway_session_id, backend_module=backend_module) return hang_up_response(gateway_session_id, backend_module=backend_module)
def ivr_finished(request): # Retrieve all parameters status = request.POST.get("status", None) start_time = request.POST.get("start_time", None) caller_id = request.POST.get("caller_id", None) phone_no = request.POST.get("phone_no", None) sid = request.POST.get("sid", "") duration = request.POST.get("duration", None) ringing_time = request.POST.get("ringing_time", None) status_details = request.POST.get("status_details", None) gateway_session_id = "KOOKOO-" + sid 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() if call_log_entry is not None: try: duration = int(duration) except Exception: duration = None call_log_entry.answered = (status == "answered") call_log_entry.duration = duration call_log_entry.save() return HttpResponse("")
def get_call_couch_ids(self): result = CallLog.view( 'sms/by_domain', include_docs=False, reduce=False, ).all() return [row['id'] for row in result if row['key'][1] == 'CallLog']
def fire_ivr_survey_event(reminder, handler, recipients, verified_numbers): if handler.recipient == RECIPIENT_CASE: # If there are no recipients, just move to the next reminder event if len(recipients) == 0: return True # If last_fired is None, it means that the reminder fired for the first time on a timeout interval. So we can # skip the lookup for the answered call since no call went out yet. if reminder.last_fired is not None and reminder.callback_try_count > 0 and CallLog.answered_call_exists(recipients[0].doc_type, recipients[0].get_id, reminder.last_fired): reminder.skip_remaining_timeouts = True return True verified_number = verified_numbers[recipients[0].get_id] if verified_number is not None: if initiate_outbound_call(verified_number, reminder.current_event.form_unique_id, handler.submit_partial_forms, handler.include_case_side_effects, handler.max_question_retries): return True else: reminder = CaseReminder.get(reminder._id) reminder.error_retry_count += 1 if reminder.error_retry_count > getattr(settings, "IVR_OUTBOUND_RETRIES", DEFAULT_OUTBOUND_RETRIES): return True else: reminder.next_fire += timedelta(minutes=getattr(settings, "IVR_OUTBOUND_RETRY_INTERVAL", DEFAULT_OUTBOUND_RETRY_INTERVAL)) reminder.save() return False else: raise_error(reminder, ERROR_NO_VERIFIED_NUMBER) return False else: # TODO: Implement ivr survey for RECIPIENT_USER, RECIPIENT_OWNER, and RECIPIENT_SURVEY_SAMPLE return False
def ivr_finished(request): # Retrieve all parameters status = request.POST.get("status", None) start_time = request.POST.get("start_time", None) caller_id = request.POST.get("caller_id", None) phone_no = request.POST.get("phone_no", None) sid = request.POST.get("sid", "") duration = request.POST.get("duration", None) ringing_time = request.POST.get("ringing_time", None) status_details = request.POST.get("status_details", None) gateway_session_id = "KOOKOO-" + sid with CriticalSection([gateway_session_id]): 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() if call_log_entry is not None: try: duration = int(duration) except Exception: duration = None call_log_entry.answered = (status == "answered") call_log_entry.duration = duration call_log_entry.save() return HttpResponse("")
def ivr_finished(request): """ Kookoo invokes this view after a call is finished (whether answered or not) with status and some statistics. Point Kookoo's 'callback_url' parameter here. """ # Retrieve all parameters status = request.POST.get("status", None) start_time = request.POST.get("start_time", None) caller_id = request.POST.get("caller_id", None) phone_no = request.POST.get("phone_no", None) sid = request.POST.get("sid", "") duration = request.POST.get("duration", None) ringing_time = request.POST.get("ringing_time", None) status_details = request.POST.get("status_details", None) gateway_session_id = "KOOKOO-" + sid with CriticalSection([gateway_session_id], timeout=300): 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() if call_log_entry is not None: try: duration = int(duration) except Exception: duration = None call_log_entry.answered = (status == "answered") call_log_entry.duration = duration call_log_entry.save() return HttpResponse("")
def fire_sms_callback_event(reminder, handler, recipients, verified_numbers): current_event = reminder.current_event if handler.recipient in [RECIPIENT_CASE, RECIPIENT_USER]: # If there are no recipients, just move to the next reminder event if len(recipients) == 0: return True # If the callback has been received, skip sending the next timeout message if reminder.callback_try_count > 0: # Lookup the expected callback event if reminder.event_initiation_timestamp is None: event = None else: event = ExpectedCallbackEventLog.view( "sms/expected_callback_event", key=[ reminder.domain, json_format_datetime( reminder.event_initiation_timestamp), recipients[0].get_id ], include_docs=True, limit=1).one() # NOTE: If last_fired is None, it means that the reminder fired for the first time on a timeout interval if reminder.last_fired is not None and CallLog.inbound_entry_exists( recipients[0].doc_type, recipients[0].get_id, reminder.last_fired): reminder.skip_remaining_timeouts = True if event is not None: event.status = CALLBACK_RECEIVED event.save() return True elif reminder.callback_try_count >= len( current_event.callback_timeout_intervals): # On the last callback timeout, instead of sending the SMS again, log the missed callback if event is not None: event.status = CALLBACK_MISSED event.save() return True else: # It's the first time sending the sms, so create an expected callback event event = ExpectedCallbackEventLog( domain=reminder.domain, date=reminder.event_initiation_timestamp, couch_recipient_doc_type=recipients[0].doc_type, couch_recipient=recipients[0].get_id, status=CALLBACK_PENDING, ) event.save() return fire_sms_event(reminder, handler, recipients, verified_numbers, workflow=WORKFLOW_CALLBACK) else: # TODO: Implement sms callback for RECIPIENT_OWNER and RECIPIENT_SURVEY_SAMPLE return False
def fire_sms_callback_event(reminder, handler, recipients, verified_numbers, logged_event): current_event = reminder.current_event for recipient in recipients: send_message = False if reminder.callback_try_count > 0: if reminder.event_initiation_timestamp: event = ExpectedCallbackEventLog.view( "sms/expected_callback_event", key=[ reminder.domain, json_format_datetime( reminder.event_initiation_timestamp), recipient.get_id ], include_docs=True, limit=1).one() if not event: continue if event.status == CALLBACK_RECEIVED: continue if CallLog.inbound_entry_exists( recipient.doc_type, recipient.get_id, reminder.event_initiation_timestamp): event.status = CALLBACK_RECEIVED event.save() continue else: continue if (reminder.callback_try_count >= len( current_event.callback_timeout_intervals)): # On the last callback timeout, instead of sending the SMS # again, log the missed callback if event: event.status = CALLBACK_MISSED event.save() else: send_message = True else: # It's the first time sending the sms, so create an expected # callback event send_message = True event = ExpectedCallbackEventLog( domain=reminder.domain, date=reminder.event_initiation_timestamp, couch_recipient_doc_type=recipient.doc_type, couch_recipient=recipient.get_id, status=CALLBACK_PENDING, ) event.save() if send_message: fire_sms_event(reminder, handler, [recipient], verified_numbers, logged_event, workflow=WORKFLOW_CALLBACK)
def initiate_outbound_call(recipient, form_unique_id, submit_partial_form, include_case_side_effects, max_question_retries, verified_number=None, unverified_number=None, case_id=None, case_for_case_submission=False, timestamp=None): """ Returns True if the call was queued successfully, or False if an error occurred. """ call_log_entry = None try: if not verified_number and not unverified_number: return False phone_number = (verified_number.phone_number if verified_number else unverified_number) call_log_entry = CallLog( couch_recipient_doc_type=recipient.doc_type, couch_recipient=recipient.get_id, phone_number="+%s" % str(phone_number), direction=OUTGOING, date=timestamp or datetime.utcnow(), domain=recipient.domain, form_unique_id=form_unique_id, submit_partial_form=submit_partial_form, include_case_side_effects=include_case_side_effects, max_question_retries=max_question_retries, current_question_retry_count=0, case_id=case_id, case_for_case_submission=case_for_case_submission, ) backend = get_ivr_backend(recipient, verified_number, unverified_number) if not backend: return False kwargs = backend.get_cleaned_outbound_params() module = __import__(backend.outbound_module, fromlist=["initiate_outbound_call"]) call_log_entry.backend_api = module.API_ID call_log_entry.save() return module.initiate_outbound_call(call_log_entry, **kwargs) except Exception: if call_log_entry: call_log_entry.error = True call_log_entry.error_message = "Internal Server Error" call_log_entry.save() raise
def ivr_in(request): """ Handles tropo call requests """ if request.method == "POST": data = json.loads(request.body) phone_number = data["session"]["from"]["id"] # TODO: Implement tropo as an ivr backend. In the meantime, just log the call. if phone_number: cleaned_number = strip_plus(phone_number) v = VerifiedNumber.by_extensive_search(cleaned_number) else: v = None # Save the call entry msg = CallLog( phone_number=cleaned_number, direction=INCOMING, date=datetime.utcnow(), backend_api=TropoBackend.get_api_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() t = Tropo() t.reject() return HttpResponse(t.RenderJson()) else: return HttpResponseBadRequest("Bad Request")
def getCallLogCount(self): result = CallLog.view( 'sms/by_domain', startkey=[self.domain, 'CallLog'], endkey=[self.domain, 'CallLog', {}], include_docs=False, reduce=True, ).all() if result: return result[0]['value'] return 0
def initiate_outbound_call(verified_number, form_unique_id, submit_partial_form, include_case_side_effects, max_question_retries): call_log_entry = CallLog( couch_recipient_doc_type = verified_number.owner_doc_type, couch_recipient = verified_number.owner_id, phone_number = "+" + str(verified_number.phone_number), direction = OUTGOING, date = datetime.utcnow(), domain = verified_number.domain, form_unique_id = form_unique_id, submit_partial_form = submit_partial_form, include_case_side_effects = include_case_side_effects, max_question_retries = max_question_retries, current_question_retry_count = 0, ) backend = verified_number.ivr_backend kwargs = backend.get_cleaned_outbound_params() module = __import__(backend.outbound_module, fromlist=["initiate_outbound_call"]) call_log_entry.backend_api = module.API_ID call_log_entry.save() return module.initiate_outbound_call(call_log_entry, **kwargs)
def fire_ivr_survey_event(reminder, handler, recipients, verified_numbers, logged_event): domain_obj = Domain.get_by_name(reminder.domain, strict=True) for recipient in recipients: initiate_call = True if reminder.callback_try_count > 0 and reminder.event_initiation_timestamp: initiate_call = not CallLog.answered_call_exists( recipient.doc_type, recipient.get_id, reminder.event_initiation_timestamp, CaseReminderHandler.get_now()) if initiate_call: if (isinstance(recipient, CommCareCase) and not handler.force_surveys_to_use_triggered_case): case_id = recipient.get_id else: case_id = reminder.case_id verified_number, unverified_number = get_recipient_phone_number( reminder, recipient, verified_numbers) if verified_number: initiate_outbound_call.delay( recipient, reminder.current_event.form_unique_id, handler.submit_partial_forms, handler.include_case_side_effects, handler.max_question_retries, logged_event.pk, verified_number=verified_number, case_id=case_id, case_for_case_submission=handler. force_surveys_to_use_triggered_case, timestamp=CaseReminderHandler.get_now(), ) elif domain_obj.send_to_duplicated_case_numbers and unverified_number: initiate_outbound_call.delay( recipient, reminder.current_event.form_unique_id, handler.submit_partial_forms, handler.include_case_side_effects, handler.max_question_retries, logged_event.pk, unverified_number=unverified_number, case_id=case_id, case_for_case_submission=handler. force_surveys_to_use_triggered_case, timestamp=CaseReminderHandler.get_now(), ) else: # initiate_outbound_call will create the subevent automatically, # so since we're not initiating the call here, we have to create # the subevent explicitly in order to log the error. logged_subevent = logged_event.create_subevent( handler, reminder, recipient) logged_subevent.error(MessagingEvent.ERROR_NO_PHONE_NUMBER)
def get_data(ids, timezone=None): """ returns the data in the format: { '2015-03': { 'domain1': { 'KOOKOO': { 'I': {'calls': 2, 'minutes': 3}, 'O': {'calls': 40, 'minutes': 45}, '?': {'calls': 0, 'minutes': 0}, }, }, 'domain2': { 'KOOKOO': { 'I': {'calls': 1, 'minutes': 1}, 'O': {'calls': 20, 'minutes': 25}, '?': {'calls': 0, 'minutes': 0}, }, 'TELERIVET': { 'I': {'calls': 10, 'minutes': 0}, 'O': {'calls': 0, 'minutes': 0}, '?': {'calls': 0, 'minutes': 0}, }, } } } """ data = {} for doc in iter_docs(CallLog.get_db(), ids): call = CallLog.wrap(doc) date = get_naive_user_datetime(call.date, timezone=timezone) month_data = get_month_data(data, date) domain_data = get_domain_data(month_data, call.domain) backend_api = get_backend_api(call) backend_data = get_backend_data(domain_data, backend_api) direction = get_direction(call) backend_data[direction]['calls'] += 1 duration = (call.duration or 0) / 60.0 duration = int(ceil(duration)) backend_data[direction]['minutes'] += duration return data
def get_last_outbound_call(self, contact): # Not clear why this should be necessary, but without it the latest # call may not be returned sleep(0.25) call = CallLog.view("sms/by_recipient", startkey=[contact.doc_type, contact._id, "CallLog", "O", {}], endkey=[contact.doc_type, contact._id, "CallLog", "O"], descending=True, include_docs=True, reduce=False, ).first() return call
def ivr_in(request): """ Handles tropo call requests """ if request.method == "POST": data = json.loads(request.body) phone_number = data["session"]["from"]["id"] # TODO: Implement tropo as an ivr backend. In the meantime, just log the call. if phone_number: cleaned_number = strip_plus(phone_number) v = VerifiedNumber.by_extensive_search(cleaned_number) else: v = None # Save the call entry msg = CallLog( phone_number=cleaned_number, direction=INCOMING, date=datetime.utcnow(), backend_api=SQLTropoBackend.get_api_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() t = Tropo() t.reject() return HttpResponse(t.RenderJson()) else: return HttpResponseBadRequest("Bad Request")
def incoming(phone_number, backend_api): cleaned_number = phone_number if 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_api ) 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()
def fire_sms_callback_event(reminder, handler, recipients, verified_numbers): current_event = reminder.current_event for recipient in recipients: send_message = False if reminder.callback_try_count > 0: if reminder.event_initiation_timestamp: event = ExpectedCallbackEventLog.view("sms/expected_callback_event", key=[reminder.domain, json_format_datetime(reminder.event_initiation_timestamp), recipient.get_id], include_docs=True, limit=1).one() if not event: continue if event.status == CALLBACK_RECEIVED: continue if CallLog.inbound_entry_exists(recipient.doc_type, recipient.get_id, reminder.event_initiation_timestamp): event.status = CALLBACK_RECEIVED event.save() continue else: continue if (reminder.callback_try_count >= len(current_event.callback_timeout_intervals)): # On the last callback timeout, instead of sending the SMS # again, log the missed callback if event: event.status = CALLBACK_MISSED event.save() else: send_message = True else: # It's the first time sending the sms, so create an expected # callback event send_message = True event = ExpectedCallbackEventLog( domain=reminder.domain, date=reminder.event_initiation_timestamp, couch_recipient_doc_type=recipient.doc_type, couch_recipient=recipient.get_id, status=CALLBACK_PENDING, ) event.save() if send_message: fire_sms_event(reminder, handler, [recipient], verified_numbers, workflow=WORKFLOW_CALLBACK) return True
def get_ids(start_date=None): result = CallLog.view( 'sms/call_by_session', include_docs=False, ).all() def include_row(row): if start_date: try: date = parse(row['key'][1]).replace(tzinfo=None) except: return False return date >= start_date return True return [row['id'] for row in result if include_row(row)]
def handle(self, *args, **options): for domain in Domain.get_all(): count = (SMSLog.count_by_domain(domain.name) + CallLog.count_by_domain(domain.name)) if count > 0: if not domain.send_to_duplicated_case_numbers: # if not True, explicitly set to False print "Setting %s to False" % domain.name domain.send_to_duplicated_case_numbers = False domain.save() else: print "Setting %s to True" % domain.name domain.send_to_duplicated_case_numbers = True domain.save()
def fire_ivr_survey_event(reminder, handler, recipients, verified_numbers, logged_event): domain_obj = Domain.get_by_name(reminder.domain, strict=True) for recipient in recipients: initiate_call = True if reminder.callback_try_count > 0 and reminder.event_initiation_timestamp: initiate_call = not CallLog.answered_call_exists( recipient.doc_type, recipient.get_id, reminder.event_initiation_timestamp, CaseReminderHandler.get_now()) if initiate_call: if (isinstance(recipient, CommCareCase) and not handler.force_surveys_to_use_triggered_case): case_id = recipient.get_id else: case_id = reminder.case_id verified_number, unverified_number = get_recipient_phone_number( reminder, recipient, verified_numbers) if verified_number: initiate_outbound_call.delay( recipient, reminder.current_event.form_unique_id, handler.submit_partial_forms, handler.include_case_side_effects, handler.max_question_retries, logged_event.pk, verified_number=verified_number, case_id=case_id, case_for_case_submission=handler.force_surveys_to_use_triggered_case, timestamp=CaseReminderHandler.get_now(), ) elif domain_obj.send_to_duplicated_case_numbers and unverified_number: initiate_outbound_call.delay( recipient, reminder.current_event.form_unique_id, handler.submit_partial_forms, handler.include_case_side_effects, handler.max_question_retries, logged_event.pk, unverified_number=unverified_number, case_id=case_id, case_for_case_submission=handler.force_surveys_to_use_triggered_case, timestamp=CaseReminderHandler.get_now(), ) else: # initiate_outbound_call will create the subevent automatically, # so since we're not initiating the call here, we have to create # the subevent explicitly in order to log the error. logged_subevent = logged_event.create_subevent(handler, reminder, recipient) logged_subevent.error(MessagingEvent.ERROR_NO_PHONE_NUMBER)
def fire_ivr_survey_event(reminder, handler, recipients, verified_numbers): domain_obj = Domain.get_by_name(reminder.domain, strict=True) for recipient in recipients: initiate_call = True if reminder.callback_try_count > 0 and reminder.event_initiation_timestamp: initiate_call = not CallLog.answered_call_exists( recipient.doc_type, recipient.get_id, reminder.event_initiation_timestamp, CaseReminderHandler.get_now()) if initiate_call: if (isinstance(recipient, CommCareCase) and not handler.force_surveys_to_use_triggered_case): case_id = recipient.get_id else: case_id = reminder.case_id verified_number, unverified_number = get_recipient_phone_number( reminder, recipient, verified_numbers) if verified_number: initiate_outbound_call.delay( recipient, reminder.current_event.form_unique_id, handler.submit_partial_forms, handler.include_case_side_effects, handler.max_question_retries, verified_number=verified_number, case_id=case_id, case_for_case_submission=handler. force_surveys_to_use_triggered_case, timestamp=CaseReminderHandler.get_now(), ) elif domain_obj.send_to_duplicated_case_numbers and unverified_number: initiate_outbound_call.delay( recipient, reminder.current_event.form_unique_id, handler.submit_partial_forms, handler.include_case_side_effects, handler.max_question_retries, unverified_number=unverified_number, case_id=case_id, case_for_case_submission=handler. force_surveys_to_use_triggered_case, timestamp=CaseReminderHandler.get_now(), ) else: #No phone number to send to pass return True
def get_ids(start_date=None, timezone=None): result = CallLog.view( 'sms/call_by_session', include_docs=False, ).all() def include_row(row): if start_date: date = row['key'][1] if date: date = parse(date).replace(tzinfo=None) date = get_naive_user_datetime(date, timezone=timezone) return date >= start_date else: return False return True return [row['id'] for row in result if include_row(row)]
def fire_ivr_survey_event(reminder, handler, recipients, verified_numbers): domain_obj = Domain.get_by_name(reminder.domain, strict=True) for recipient in recipients: initiate_call = True if reminder.callback_try_count > 0 and reminder.event_initiation_timestamp: initiate_call = not CallLog.answered_call_exists( recipient.doc_type, recipient.get_id, reminder.event_initiation_timestamp, CaseReminderHandler.get_now()) if initiate_call: if (isinstance(recipient, CommCareCase) and not handler.force_surveys_to_use_triggered_case): case_id = recipient.get_id else: case_id = reminder.case_id verified_number, unverified_number = get_recipient_phone_number( reminder, recipient, verified_numbers) if verified_number: initiate_outbound_call.delay( recipient, reminder.current_event.form_unique_id, handler.submit_partial_forms, handler.include_case_side_effects, handler.max_question_retries, verified_number=verified_number, case_id=case_id, case_for_case_submission=handler.force_surveys_to_use_triggered_case, timestamp=CaseReminderHandler.get_now(), ) elif domain_obj.send_to_duplicated_case_numbers and unverified_number: initiate_outbound_call.delay( recipient, reminder.current_event.form_unique_id, handler.submit_partial_forms, handler.include_case_side_effects, handler.max_question_retries, unverified_number=unverified_number, case_id=case_id, case_for_case_submission=handler.force_surveys_to_use_triggered_case, timestamp=CaseReminderHandler.get_now(), ) else: #No phone number to send to pass return True
def fire_sms_callback_event(reminder, handler, recipients, verified_numbers): current_event = reminder.current_event if handler.recipient in [RECIPIENT_CASE, RECIPIENT_USER]: # If there are no recipients, just move to the next reminder event if len(recipients) == 0: return True # If the callback has been received, skip sending the next timeout message if reminder.callback_try_count > 0: # Lookup the expected callback event if reminder.event_initiation_timestamp is None: event = None else: event = ExpectedCallbackEventLog.view("sms/expected_callback_event", key=[reminder.domain, json_format_datetime(reminder.event_initiation_timestamp), recipients[0].get_id], include_docs=True, limit=1).one() # NOTE: If last_fired is None, it means that the reminder fired for the first time on a timeout interval if reminder.last_fired is not None and CallLog.inbound_entry_exists(recipients[0].doc_type, recipients[0].get_id, reminder.last_fired): reminder.skip_remaining_timeouts = True if event is not None: event.status = CALLBACK_RECEIVED event.save() return True elif reminder.callback_try_count >= len(current_event.callback_timeout_intervals): # On the last callback timeout, instead of sending the SMS again, log the missed callback if event is not None: event.status = CALLBACK_MISSED event.save() return True else: # It's the first time sending the sms, so create an expected callback event event = ExpectedCallbackEventLog( domain = reminder.domain, date = reminder.event_initiation_timestamp, couch_recipient_doc_type = recipients[0].doc_type, couch_recipient = recipients[0].get_id, status = CALLBACK_PENDING, ) event.save() return fire_sms_event(reminder, handler, recipients, verified_numbers, workflow=WORKFLOW_CALLBACK) else: # TODO: Implement sms callback for RECIPIENT_OWNER and RECIPIENT_SURVEY_SAMPLE return False
def log_call(phone_number, gateway_session_id, backend_module=None): cleaned_number = strip_plus(phone_number) v = VerifiedNumber.by_extensive_search(cleaned_number) call = 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: call.domain = v.domain call.couch_recipient_doc_type = v.owner_doc_type call.couch_recipient = v.owner_id call.save()
def ivr_in(request): """ Handles tropo call requests """ if request.method == "POST": data = json.loads(request.raw_post_data) phone_number = data["session"]["from"]["id"] #### # TODO: Implement tropo as an ivr backend. In the meantime, just log the call. 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=TROPO_BACKEND_API_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() #### t = Tropo() t.reject() return HttpResponse(t.RenderJson()) else: return HttpResponseBadRequest("Bad Request")
def deleteAllLogs(self): for smslog in SMSLog.view( 'sms/by_domain', startkey=[self.domain, 'SMSLog'], endkey=[self.domain, 'SMSLog', {}], include_docs=True, reduce=False, ).all(): smslog.delete() for callog in CallLog.view( 'sms/by_domain', startkey=[self.domain, 'CallLog'], endkey=[self.domain, 'CallLog', {}], include_docs=True, reduce=False, ).all(): callog.delete() for obj in LastReadMessage.view( 'sms/last_read_message', startkey=['by_anyone', self.domain], endkey=['by_anyone', self.domain, {}], include_docs=True, ).all(): obj.delete() for obj in ExpectedCallbackEventLog.view( 'sms/expected_callback_event', startkey=[self.domain], endkey=[self.domain, {}], include_docs=True, ).all(): obj.delete() SMS.objects.filter(domain=self.domain).delete() Call.objects.filter(domain=self.domain).delete() MessagingSubEvent.objects.filter(parent__domain=self.domain).delete() MessagingEvent.objects.filter(domain=self.domain).delete() SQLLastReadMessage.objects.filter(domain=self.domain).delete() ExpectedCallback.objects.filter(domain=self.domain).delete()
def fire_ivr_survey_event(reminder, handler, recipients, verified_numbers): if handler.recipient == RECIPIENT_CASE: # If there are no recipients, just move to the next reminder event if len(recipients) == 0: return True # If last_fired is None, it means that the reminder fired for the first time on a timeout interval. So we can # skip the lookup for the answered call since no call went out yet. if reminder.last_fired is not None and reminder.callback_try_count > 0 and CallLog.answered_call_exists( recipients[0].doc_type, recipients[0].get_id, reminder.last_fired): reminder.skip_remaining_timeouts = True return True verified_number = verified_numbers[recipients[0].get_id] if verified_number is not None: if initiate_outbound_call(verified_number, reminder.current_event.form_unique_id, handler.submit_partial_forms, handler.include_case_side_effects, handler.max_question_retries): return True else: reminder = CaseReminder.get(reminder._id) reminder.error_retry_count += 1 if reminder.error_retry_count > getattr( settings, "IVR_OUTBOUND_RETRIES", DEFAULT_OUTBOUND_RETRIES): return True else: reminder.next_fire += timedelta(minutes=getattr( settings, "IVR_OUTBOUND_RETRY_INTERVAL", DEFAULT_OUTBOUND_RETRY_INTERVAL)) reminder.save() return False else: raise_error(reminder, ERROR_NO_VERIFIED_NUMBER) return False else: # TODO: Implement ivr survey for RECIPIENT_USER, RECIPIENT_OWNER, and RECIPIENT_SURVEY_SAMPLE return False
def ivr_in(request): """ Handles tropo call requests """ if request.method == "POST": data = json.loads(request.raw_post_data) phone_number = data["session"]["from"]["id"] #### # TODO: Implement tropo as an ivr backend. In the meantime, just log the call. 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 = TropoBackend.get_api_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() #### t = Tropo() t.reject() return HttpResponse(t.RenderJson()) else: return HttpResponseBadRequest("Bad Request")
def delete_models(self, delete_interval): print('Deleting SMSLogs...') count = iter_bulk_delete_with_doc_type_verification(SMSLog.get_db(), self.get_sms_couch_ids(), 'SMSLog', wait_time=delete_interval, max_fetch_attempts=5) print('Deleted %s documents' % count) print('Deleting CallLogs...') count = iter_bulk_delete_with_doc_type_verification(CallLog.get_db(), self.get_call_couch_ids(), 'CallLog', wait_time=delete_interval, max_fetch_attempts=5) print('Deleted %s documents' % count) print('Deleting ExpectedCallbackEventLogs...') count = iter_bulk_delete_with_doc_type_verification(ExpectedCallbackEventLog.get_db(), self.get_callback_couch_ids(), 'ExpectedCallbackEventLog', wait_time=delete_interval, max_fetch_attempts=5) print('Deleted %s documents' % count) print('Deleting LastReadMessages...') count = iter_bulk_delete_with_doc_type_verification(LastReadMessage.get_db(), self.get_lastreadmessage_couch_ids(), 'LastReadMessage', wait_time=delete_interval, max_fetch_attempts=5) print('Deleted %s documents' % count)
def delete_models(self, delete_interval): print 'Deleting SMSLogs...' count = iter_bulk_delete_with_doc_type_verification(SMSLog.get_db(), self.get_sms_couch_ids(), 'SMSLog', wait_time=delete_interval, max_fetch_attempts=5) print 'Deleted %s documents' % count print 'Deleting CallLogs...' count = iter_bulk_delete_with_doc_type_verification(CallLog.get_db(), self.get_call_couch_ids(), 'CallLog', wait_time=delete_interval, max_fetch_attempts=5) print 'Deleted %s documents' % count print 'Deleting ExpectedCallbackEventLogs...' count = iter_bulk_delete_with_doc_type_verification(ExpectedCallbackEventLog.get_db(), self.get_callback_couch_ids(), 'ExpectedCallbackEventLog', wait_time=delete_interval, max_fetch_attempts=5) print 'Deleted %s documents' % count print 'Deleting LastReadMessages...' count = iter_bulk_delete_with_doc_type_verification(LastReadMessage.get_db(), self.get_lastreadmessage_couch_ids(), 'LastReadMessage', wait_time=delete_interval, max_fetch_attempts=5) print 'Deleted %s documents' % count
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 initiate_outbound_call(recipient, form_unique_id, submit_partial_form, include_case_side_effects, max_question_retries, messaging_event_id, verified_number=None, unverified_number=None, case_id=None, case_for_case_submission=False, timestamp=None): """ Returns False if an error occurred and the call should be retried. Returns True if the call should not be retried (either because it was queued successfully or because an unrecoverable error occurred). """ call_log_entry = None logged_event = MessagingEvent.objects.get(pk=messaging_event_id) logged_subevent = logged_event.create_ivr_subevent(recipient, form_unique_id, case_id=case_id) if not verified_number and not unverified_number: log_error(MessagingEvent.ERROR_NO_PHONE_NUMBER, logged_subevent=logged_subevent) return True backend = get_ivr_backend(recipient, verified_number, unverified_number) if not backend: log_error(MessagingEvent.ERROR_NO_SUITABLE_GATEWAY, logged_subevent=logged_subevent) return True phone_number = (verified_number.phone_number if verified_number else unverified_number) call_log_entry = CallLog( couch_recipient_doc_type=recipient.doc_type, couch_recipient=recipient.get_id, phone_number='+%s' % str(phone_number), direction=OUTGOING, date=timestamp or datetime.utcnow(), domain=recipient.domain, form_unique_id=form_unique_id, submit_partial_form=submit_partial_form, include_case_side_effects=include_case_side_effects, max_question_retries=max_question_retries, current_question_retry_count=0, case_id=case_id, case_for_case_submission=case_for_case_submission, messaging_subevent_id=logged_subevent.pk, ) ivr_data, error = get_first_ivr_response_data(recipient, call_log_entry, logged_subevent) if error: return True if ivr_data: logged_subevent.xforms_session = ivr_data.session logged_subevent.save() try: kwargs = backend.get_cleaned_outbound_params() module = __import__(backend.outbound_module, fromlist=['initiate_outbound_call']) call_log_entry.backend_api = module.API_ID call_log_entry.save() result = module.initiate_outbound_call(call_log_entry, logged_subevent, ivr_data=ivr_data, **kwargs) logged_subevent.completed() return result except GatewayConnectionError: log_error(MessagingEvent.ERROR_GATEWAY_ERROR, call_log_entry, logged_subevent) raise except Exception: log_error(MessagingEvent.ERROR_INTERNAL_SERVER_ERROR, call_log_entry, logged_subevent) raise
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.couch_recipient_doc_type, call.couch_recipient, 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 = ServerTime(call.date).user_time(self.timezone).done() 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.couch_recipient, 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(cgi.escape(call.error_message) if call.error_message else None), ] if self.request.couch_user.is_previewer(): row.append(self._fmt(call.gateway_session_id)) result.append(row) all_session_ids = xforms_sessions.keys() session_submission_map = dict( SQLXFormsSession.objects.filter(session_id__in=all_session_ids).values_list( 'session_id', 'submission_id' ) ) xforms_sessions.update(session_submission_map) # 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 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 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 test_ok(self): self.assertEqual(self.handler.get_reminder(self.case), None) # Spawn CaseReminder CaseReminderHandler.now = datetime(year=2011, month=12, day=31, hour=23, minute=0) self.case.set_case_property('start_sending', 'ok') self.case.save() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=1, hour=7, minute=0)) self.assertEqual(reminder.start_date, date(year=2012, month=1, day=1)) self.assertEqual(reminder.schedule_iteration_num, 1) self.assertEqual(reminder.current_event_sequence_num, 0) self.assertEqual(reminder.last_fired, None) ###################### # Day1, 10:00 reminder CaseReminderHandler.now = datetime(year=2012, month=1, day=1, hour=7, minute=0) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=1, hour=8, minute=0)) self.assertEqual(reminder.schedule_iteration_num, 1) self.assertEqual(reminder.current_event_sequence_num, 1) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) # Day1, 11:00 reminder CaseReminderHandler.now = datetime(year=2012, month=1, day=1, hour=8, minute=1) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=1, hour=8, minute=15)) self.assertEqual(reminder.schedule_iteration_num, 1) self.assertEqual(reminder.current_event_sequence_num, 1) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) event = ExpectedCallbackEventLog.view("sms/expected_callback_event", key=["test", json_format_datetime(datetime(year=2012, month=1, day=1, hour=8, minute=1)), self.user_id], include_docs=True).one() self.assertNotEqual(event, None) self.assertEqual(event.status, CALLBACK_PENDING) # Create a callback c = CallLog( couch_recipient_doc_type = "CommCareUser", couch_recipient = self.user_id, phone_number = "14445551234", direction = "I", date = datetime(year=2012, month=1, day=1, hour=8, minute=5) ) c.save() # Day1, 11:15 timeout (should move on to next event) CaseReminderHandler.now = datetime(year=2012, month=1, day=1, hour=8, minute=15) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=1, hour=8, minute=45)) self.assertEqual(reminder.schedule_iteration_num, 1) self.assertEqual(reminder.current_event_sequence_num, 1) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) # Day1, 11:45 timeout (should move on to next event) CaseReminderHandler.now = datetime(year=2012, month=1, day=1, hour=8, minute=45) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=2, hour=7, minute=0)) self.assertEqual(reminder.schedule_iteration_num, 2) self.assertEqual(reminder.current_event_sequence_num, 0) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) event = ExpectedCallbackEventLog.view("sms/expected_callback_event", key=["test", json_format_datetime(datetime(year=2012, month=1, day=1, hour=8, minute=1)), self.user_id], include_docs=True).one() self.assertNotEqual(event, None) self.assertEqual(event.status, CALLBACK_RECEIVED) ###################### # Day2, 10:00 reminder CaseReminderHandler.now = datetime(year=2012, month=1, day=2, hour=7, minute=0) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=2, hour=8, minute=0)) self.assertEqual(reminder.schedule_iteration_num, 2) self.assertEqual(reminder.current_event_sequence_num, 1) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) # Day2, 11:00 reminder CaseReminderHandler.now = datetime(year=2012, month=1, day=2, hour=8, minute=1) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=2, hour=8, minute=15)) self.assertEqual(reminder.schedule_iteration_num, 2) self.assertEqual(reminder.current_event_sequence_num, 1) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) event = ExpectedCallbackEventLog.view("sms/expected_callback_event", key=["test", json_format_datetime(datetime(year=2012, month=1, day=2, hour=8, minute=1)), self.user_id], include_docs=True).one() self.assertNotEqual(event, None) self.assertEqual(event.status, CALLBACK_PENDING) # Day2, 11:15 timeout (should move on to next timeout) CaseReminderHandler.now = datetime(year=2012, month=1, day=2, hour=8, minute=15) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=2, hour=8, minute=45)) self.assertEqual(reminder.schedule_iteration_num, 2) self.assertEqual(reminder.current_event_sequence_num, 1) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) event = ExpectedCallbackEventLog.view("sms/expected_callback_event", key=["test", json_format_datetime(datetime(year=2012, month=1, day=2, hour=8, minute=1)), self.user_id], include_docs=True).one() self.assertNotEqual(event, None) self.assertEqual(event.status, CALLBACK_PENDING) # Day2, 11:45 timeout (should move on to next day) CaseReminderHandler.now = datetime(year=2012, month=1, day=2, hour=8, minute=45) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=3, hour=7, minute=0)) self.assertEqual(reminder.schedule_iteration_num, 3) self.assertEqual(reminder.current_event_sequence_num, 0) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) event = ExpectedCallbackEventLog.view("sms/expected_callback_event", key=["test", json_format_datetime(datetime(year=2012, month=1, day=2, hour=8, minute=1)), self.user_id], include_docs=True).one() self.assertNotEqual(event, None) self.assertEqual(event.status, CALLBACK_MISSED) ###################### # Day3, 10:00 reminder CaseReminderHandler.now = datetime(year=2012, month=1, day=3, hour=7, minute=0) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=3, hour=8, minute=0)) self.assertEqual(reminder.schedule_iteration_num, 3) self.assertEqual(reminder.current_event_sequence_num, 1) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) # Day3, 11:00 reminder CaseReminderHandler.now = datetime(year=2012, month=1, day=3, hour=8, minute=1) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=3, hour=8, minute=15)) self.assertEqual(reminder.schedule_iteration_num, 3) self.assertEqual(reminder.current_event_sequence_num, 1) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) event = ExpectedCallbackEventLog.view("sms/expected_callback_event", key=["test", json_format_datetime(datetime(year=2012, month=1, day=3, hour=8, minute=1)), self.user_id], include_docs=True).one() self.assertNotEqual(event, None) self.assertEqual(event.status, CALLBACK_PENDING) # Day3, 11:15 timeout (should move on to next timeout) CaseReminderHandler.now = datetime(year=2012, month=1, day=3, hour=8, minute=15) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.next_fire, datetime(year=2012, month=1, day=3, hour=8, minute=45)) self.assertEqual(reminder.schedule_iteration_num, 3) self.assertEqual(reminder.current_event_sequence_num, 1) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) event = ExpectedCallbackEventLog.view("sms/expected_callback_event", key=["test", json_format_datetime(datetime(year=2012, month=1, day=3, hour=8, minute=1)), self.user_id], include_docs=True).one() self.assertNotEqual(event, None) self.assertEqual(event.status, CALLBACK_PENDING) # Create a callback (with phone_number missing country code) c = CallLog( couch_recipient_doc_type = "CommCareUser", couch_recipient = self.user_id, phone_number = "4445551234", direction = "I", date = datetime(year=2012, month=1, day=3, hour=8, minute=22) ) c.save() # Day3, 11:45 timeout (should deactivate the reminder) CaseReminderHandler.now = datetime(year=2012, month=1, day=3, hour=8, minute=45) CaseReminderHandler.fire_reminders() reminder = self.handler.get_reminder(self.case) self.assertNotEqual(reminder, None) self.assertEqual(reminder.schedule_iteration_num, 4) self.assertEqual(reminder.current_event_sequence_num, 0) self.assertEqual(reminder.last_fired, CaseReminderHandler.now) self.assertEqual(reminder.active, False) event = ExpectedCallbackEventLog.view("sms/expected_callback_event", key=["test", json_format_datetime(datetime(year=2012, month=1, day=3, hour=8, minute=1)), self.user_id], include_docs=True).one() self.assertNotEqual(event, None) self.assertEqual(event.status, CALLBACK_RECEIVED)
def testOutbound(self): # Send an outbound call using self.reminder1 to self.case # and answer it CaseReminderHandler.now = datetime(2014, 6, 23, 10, 0) self.case = CommCareCase.get( register_sms_contact( self.domain, 'participant', 'case1', self.user1._id, '91000', owner_id=self.groups[0]._id, )) CaseReminderHandler.now = datetime(2014, 6, 23, 12, 0) CaseReminderHandler.fire_reminders() reminder = self.reminder1.get_reminder(self.case) self.assertEquals(reminder.next_fire, datetime(2014, 6, 23, 12, 30)) call = self.get_last_outbound_call(self.case) self.assertTrue(call.use_precached_first_response) kookoo_session_id = call.gateway_session_id[7:] resp = self.kookoo_in({ "cid": "0000", "sid": kookoo_session_id, "event": "NewCall", }) self.assertEqual( resp, '<response sid="%s"><collectdtmf l="1" o="3000">' '<playtext>How do you feel today? Press 1 for good, 2 for bad.' '</playtext></collectdtmf></response>' % kookoo_session_id) resp = self.kookoo_in({ "cid": "0000", "sid": kookoo_session_id, "event": "GotDTMF", "data": "1", }) self.assertEqual( resp, '<response sid="%s"><collectdtmf l="1" o="3000">' '<playtext>Did you remember to take your meds today? Press 1 for yes, 2 for no.' '</playtext></collectdtmf></response>' % kookoo_session_id) resp = self.kookoo_in({ "cid": "0000", "sid": kookoo_session_id, "event": "GotDTMF", "data": "2", }) self.assertEqual( resp, '<response sid="%s"><hangup/></response>' % kookoo_session_id) self.kookoo_finished({ "sid": kookoo_session_id, "status": "answered", "duration": "20", }) call = CallLog.get(call._id) self.assertTrue(call.answered) self.assertEqual(call.duration, 20) form = self.get_last_form_submission() self.assertFormQuestionEquals(form, "how_feel", "1") self.assertFormQuestionEquals(form, "take_meds", "2") case = CommCareCase.get(self.case._id) self.assertCasePropertyEquals(case, "how_feel", "1") self.assertCasePropertyEquals(case, "take_meds", "2") CaseReminderHandler.now = datetime(2014, 6, 23, 12, 30) CaseReminderHandler.fire_reminders() reminder = self.reminder1.get_reminder(self.case) self.assertEquals(reminder.next_fire, datetime(2014, 6, 23, 13, 0)) last_call = self.get_last_outbound_call(self.case) self.assertEqual(call._id, last_call._id) # Move on to the second event which now uses an all-label form and # should not precache the first ivr response CaseReminderHandler.now = datetime(2014, 6, 23, 13, 0) CaseReminderHandler.fire_reminders() reminder = self.reminder1.get_reminder(self.case) self.assertEquals(reminder.next_fire, datetime(2014, 6, 23, 13, 30)) call = self.get_last_outbound_call(self.case) self.assertFalse(call.use_precached_first_response) kookoo_session_id = call.gateway_session_id[7:] resp = self.kookoo_in({ "cid": "0000", "sid": kookoo_session_id, "event": "NewCall", }) self.assertEqual( resp, '<response sid="%s">' '<playtext>This is just a reminder to take your meds.' '</playtext><hangup/></response>' % kookoo_session_id) self.kookoo_finished({ "sid": kookoo_session_id, "status": "answered", "duration": "5", }) call = CallLog.get(call._id) self.assertTrue(call.answered) self.assertEqual(call.duration, 5) form = self.get_last_form_submission() self.assertFormQuestionEquals(form, "label", "ok") CaseReminderHandler.now = datetime(2014, 6, 23, 13, 30) CaseReminderHandler.fire_reminders() reminder = self.reminder1.get_reminder(self.case) self.assertEquals(reminder.next_fire, datetime(2014, 6, 24, 12, 0)) last_call = self.get_last_outbound_call(self.case) self.assertEqual(call._id, last_call._id) # Now test sending outbound calls to a group of users (the owners # of the case) # Allow sending to unverified numbers self.domain_obj = Domain.get(self.domain_obj._id) self.domain_obj.send_to_duplicated_case_numbers = True self.domain_obj.save() CaseReminderHandler.now = datetime(2014, 6, 24, 10, 0) self.case = CommCareCase.get( register_sms_contact( self.domain, 'participant', 'case2', self.user1._id, '91003', owner_id=self.groups[0]._id, )) reminder = self.reminder2.get_reminder(self.case) self.assertEquals(reminder.next_fire, datetime(2014, 6, 24, 12, 0)) CaseReminderHandler.now = datetime(2014, 6, 24, 12, 0) CaseReminderHandler.fire_reminders() reminder = self.reminder2.get_reminder(self.case) self.assertEquals(reminder.next_fire, datetime(2014, 6, 24, 12, 30)) call1 = self.get_last_outbound_call(self.user1) self.assertTrue(call1.use_precached_first_response) self.assertFalse(call1.answered) call2 = self.get_last_outbound_call(self.user2) self.assertTrue(call2.use_precached_first_response) self.assertFalse(call2.answered) old_call1 = call1 old_call2 = call2 CaseReminderHandler.now = datetime(2014, 6, 24, 12, 30) CaseReminderHandler.fire_reminders() reminder = self.reminder2.get_reminder(self.case) self.assertEquals(reminder.next_fire, datetime(2014, 6, 24, 13, 0)) call1 = self.get_last_outbound_call(self.user1) self.assertTrue(call1.use_precached_first_response) self.assertNotEqual(call1._id, old_call1._id) call2 = self.get_last_outbound_call(self.user2) self.assertTrue(call2.use_precached_first_response) self.assertFalse(call2.answered) self.assertNotEqual(call2._id, old_call2._id) kookoo_session_id = call1.gateway_session_id[7:] resp = self.kookoo_in({ "cid": "0001", "sid": kookoo_session_id, "event": "NewCall", }) self.assertEqual( resp, '<response sid="%s"><collectdtmf l="1" o="3000">' '<playtext>How do you feel today? Press 1 for good, 2 for bad.' '</playtext></collectdtmf></response>' % kookoo_session_id) resp = self.kookoo_in({ "cid": "0001", "sid": kookoo_session_id, "event": "GotDTMF", "data": "2", }) self.assertEqual( resp, '<response sid="%s"><collectdtmf l="1" o="3000">' '<playtext>Did you remember to take your meds today? Press 1 for yes, 2 for no.' '</playtext></collectdtmf></response>' % kookoo_session_id) resp = self.kookoo_in({ "cid": "0001", "sid": kookoo_session_id, "event": "GotDTMF", "data": "1", }) self.assertEqual( resp, '<response sid="%s"><hangup/></response>' % kookoo_session_id) self.kookoo_finished({ "sid": kookoo_session_id, "status": "answered", "duration": "20", }) call1 = CallLog.get(call1._id) self.assertTrue(call1.answered) self.assertEqual(call1.duration, 20) form = self.get_last_form_submission() self.assertFormQuestionEquals(form, "how_feel", "2") self.assertFormQuestionEquals(form, "take_meds", "1") self.assertEqual(form.form["meta"]["userID"], self.user1._id) case = CommCareCase.get(self.case._id) self.assertCasePropertyEquals(case, "how_feel", "2") self.assertCasePropertyEquals(case, "take_meds", "1") self.assertEqual(case.user_id, self.user1._id) old_call1 = call1 old_call2 = call2 CaseReminderHandler.now = datetime(2014, 6, 24, 13, 0) CaseReminderHandler.fire_reminders() reminder = self.reminder2.get_reminder(self.case) self.assertEquals(reminder.next_fire, datetime(2014, 6, 25, 12, 0)) call1 = self.get_last_outbound_call(self.user1) # No new call for user1 since it was already answered self.assertEqual(call1._id, old_call1._id) call2 = self.get_last_outbound_call(self.user2) self.assertTrue(call2.use_precached_first_response) self.assertNotEqual(call2._id, old_call2._id) kookoo_session_id = call2.gateway_session_id[7:] resp = self.kookoo_in({ "cid": "0002", "sid": kookoo_session_id, "event": "NewCall", }) self.assertEqual( resp, '<response sid="%s"><collectdtmf l="1" o="3000">' '<playtext>How do you feel today? Press 1 for good, 2 for bad.' '</playtext></collectdtmf></response>' % kookoo_session_id) resp = self.kookoo_in({ "cid": "0002", "sid": kookoo_session_id, "event": "GotDTMF", "data": "1", }) self.assertEqual( resp, '<response sid="%s"><collectdtmf l="1" o="3000">' '<playtext>Did you remember to take your meds today? Press 1 for yes, 2 for no.' '</playtext></collectdtmf></response>' % kookoo_session_id) resp = self.kookoo_in({ "cid": "0002", "sid": kookoo_session_id, "event": "GotDTMF", "data": "2", }) self.assertEqual( resp, '<response sid="%s"><hangup/></response>' % kookoo_session_id) self.kookoo_finished({ "sid": kookoo_session_id, "status": "answered", "duration": "20", }) call2 = CallLog.get(call2._id) self.assertTrue(call2.answered) self.assertEqual(call2.duration, 20) form = self.get_last_form_submission() self.assertFormQuestionEquals(form, "how_feel", "1") self.assertFormQuestionEquals(form, "take_meds", "2") self.assertEqual(form.form["meta"]["userID"], self.user2._id) case = CommCareCase.get(self.case._id) self.assertCasePropertyEquals(case, "how_feel", "1") self.assertCasePropertyEquals(case, "take_meds", "2") self.assertEqual(case.user_id, self.user2._id)
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 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("")