def testCallLogSync(self): self.deleteAllLogs() self.assertEqual(self.getCallLogCount(), 0) self.assertEqual(self.getCallCount(), 0) # Test Create calllog = CallLog() self.setRandomCallLogValues(calllog) calllog.save() sleep(1) self.assertEqual(self.getCallLogCount(), 1) self.assertEqual(self.getCallCount(), 1) call = Call.objects.get(couch_id=calllog._id) self.checkFieldValues(calllog, call, Call._migration_get_fields()) self.assertTrue(CallLog.get_db().get_rev(calllog._id).startswith('1-')) # Test Update self.setRandomCallLogValues(calllog) calllog.save() sleep(1) self.assertEqual(self.getCallLogCount(), 1) self.assertEqual(self.getCallCount(), 1) call = Call.objects.get(couch_id=calllog._id) self.checkFieldValues(calllog, call, Call._migration_get_fields()) self.assertTrue(CallLog.get_db().get_rev(calllog._id).startswith('2-'))
def test_401_response(self): start_count = Call.by_domain(self.domain).count() response = Client().post('/twilio/ivr/xxxxx', { 'From': self.phone_number, 'CallSid': 'xyz', }) self.assertEqual(response.status_code, 401) end_count = Call.by_domain(self.domain).count() self.assertEqual(start_count, end_count)
def test_401_response(self): with self.create_case(): start_count = Call.by_domain(self.domain).count() response = Client().post('/twilio/ivr/xxxxx', { 'From': self.phone_number, 'CallSid': 'xyz', }) self.assertEqual(response.status_code, 401) end_count = Call.by_domain(self.domain).count() 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(Call.by_domain(self.domain).count(), 0) response = self.simulate_inbound_call(self.phone_number) self.check_response(response) self.assertEqual(Call.by_domain(self.domain).count(), 1) call = Call.by_domain(self.domain)[0] self.assertEqual(call.couch_recipient_doc_type, 'CommCareCase') self.assertEqual(call.couch_recipient, self.case.get_id) self.assertEqual(call.direction, INCOMING)
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 = Call.by_gateway_session_id(gateway_session_id) if call: log_metadata_received(call) try: duration = int(duration) except Exception: duration = None call.answered = (status == 'answered') call.duration = duration call.save() return HttpResponse('')
def incoming(phone_number, gateway_session_id, ivr_event, backend=None, input_data=None, duration=None): """ The main entry point for all incoming IVR requests. """ call = Call.by_gateway_session_id(gateway_session_id) logged_subevent = None if call and call.messaging_subevent_id: logged_subevent = MessagingSubEvent.objects.get( pk=call.messaging_subevent_id) if call: add_metadata(call, duration) if call and call.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=backend) if call and backend: return handle_known_call_session(call, backend, ivr_event, input_data=input_data, logged_subevent=logged_subevent) else: if not call: log_call(phone_number, gateway_session_id, backend=backend) return hang_up_response(gateway_session_id, backend=backend)
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 = Call.by_gateway_session_id(gateway_session_id) if call: log_metadata_received(call) try: duration = int(duration) except Exception: duration = None call.answered = status == "answered" call.duration = duration call.save() return HttpResponse("")
def test_log_call(self): with self.create_case() as case: 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(Call.by_domain(self.domain).count(), 0) response = self.simulate_inbound_call(self.phone_number) self.check_response(response) self.assertEqual(Call.by_domain(self.domain).count(), 1) call = Call.by_domain(self.domain)[0] self.assertEqual(call.couch_recipient_doc_type, 'CommCareCase') self.assertEqual(call.couch_recipient, case.case_id) self.assertEqual(call.direction, INCOMING)
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 = PhoneNumber.by_extensive_search(cleaned_number) else: v = None # Save the call entry msg = Call( 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 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 Call.answered_call_exists( recipient.doc_type, recipient.get_id, reminder.event_initiation_timestamp) if initiate_call: if (is_commcarecase(recipient) and not handler.force_surveys_to_use_triggered_case): case_id = recipient.case_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 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 = Call( 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 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 Call.answered_call_exists( recipient.doc_type, recipient.get_id, reminder.event_initiation_timestamp ) if initiate_call: if (is_commcarecase(recipient) and not handler.force_surveys_to_use_triggered_case): case_id = recipient.case_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_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 = ExpectedCallback.by_domain_recipient_date( reminder.domain, recipient.get_id, reminder.event_initiation_timestamp) if not event: continue if event.status == CALLBACK_RECEIVED: continue if Call.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 = ExpectedCallback.objects.create( domain=reminder.domain, date=reminder.event_initiation_timestamp, couch_recipient_doc_type=recipient.doc_type, couch_recipient=recipient.get_id, status=CALLBACK_PENDING, ) if send_message: fire_sms_event(reminder, handler, [recipient], verified_numbers, logged_event, workflow=WORKFLOW_CALLBACK)
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 = ExpectedCallback.by_domain_recipient_date( reminder.domain, recipient.get_id, reminder.event_initiation_timestamp ) if not event: continue if event.status == CALLBACK_RECEIVED: continue if Call.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 = ExpectedCallback.objects.create( domain=reminder.domain, date=reminder.event_initiation_timestamp, couch_recipient_doc_type=recipient.doc_type, couch_recipient=recipient.get_id, status=CALLBACK_PENDING, ) if send_message: fire_sms_event(reminder, handler, [recipient], verified_numbers, logged_event, workflow=WORKFLOW_CALLBACK)
def log_call(phone_number, gateway_session_id, backend=None): cleaned_number = strip_plus(phone_number) v = PhoneNumber.by_extensive_search(cleaned_number) call = Call( phone_number=cleaned_number, direction=INCOMING, date=datetime.utcnow(), backend_api=backend.get_api_id() if backend else None, backend_id=backend.couch_id if backend 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 delete_call_logs(self, domain): Call.by_domain(domain).delete()
def get_last_outbound_call(self, contact): return Call.get_last_log_for_recipient(contact.doc_type, contact.get_id, direction=OUTGOING)
def get_last_outbound_call(self, contact): return Call.get_last_log_for_recipient( contact.doc_type, contact.get_id, direction=OUTGOING )
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 = 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 = Call( 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 = None if backend.cache_first_ivr_response(): ivr_data, error = get_first_ivr_response_data(recipient, call, logged_subevent) if error: return True if ivr_data: logged_subevent.xforms_session = ivr_data.session logged_subevent.save() try: call.backend_api = backend.get_api_id() call.backend_id = backend.couch_id result = backend.initiate_outbound_call(call, logged_subevent) if ivr_data and not call.error: backend.set_first_ivr_response(call, call.gateway_session_id, ivr_data) call.save() logged_subevent.completed() return result except GatewayConnectionError: log_error(MessagingEvent.ERROR_GATEWAY_ERROR, call, logged_subevent) raise except Exception: log_error(MessagingEvent.ERROR_INTERNAL_SERVER_ERROR, call, logged_subevent) raise
def rows(self): data = Call.by_domain( self.domain, start_date=self.datespan.startdate_utc, end_date=self.datespan.enddate_utc).order_by('date') 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(self.domain, 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 = list(xforms_sessions) 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): data = Call.by_domain( self.domain, start_date=self.datespan.startdate_utc, end_date=self.datespan.enddate_utc ).order_by('date') 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