def clean_csv_file(self): try: Contact.get_import_file_headers(ContentFile(self.cleaned_data['csv_file'].read())) except Exception as e: raise forms.ValidationError(str(e)) return self.cleaned_data['csv_file']
def start_call(self, call, to, from_, status_callback): if not settings.SEND_CALLS: raise ValueError("SEND_CALLS set to False, skipping call start") channel = call.channel Contact.get_or_create(channel.org, URN.from_tel(to), channel) # Verboice differs from Twilio in that they expect the first block of twiml up front payload = str(Flow.handle_call(call)) # now we can post that to verboice url = "%s?%s" % (self.endpoint, urlencode(dict(channel=self.verboice_channel, address=to))) response = requests.post(url, data=payload, auth=self.auth).json() if "call_id" not in response: call.status = IVRCall.FAILED call.save() raise IVRException(_("Verboice connection failed.")) # store the verboice call id in our IVRCall call.external_id = response["call_id"] # the call was successfully sent to the IVR provider call.status = IVRCall.WIRED call.save()
def test_twilio_failed_auth(self): def create(self, to=None, from_=None, url=None, status_callback=None): from twilio import TwilioRestException raise TwilioRestException(403, 'http://twilio.com', code=20003) MockTwilioClient.MockCalls.create = create # connect it and check our client is configured self.org.connect_twilio("TEST_SID", "TEST_TOKEN") self.org.save() # import an ivr flow self.import_file('call-me-maybe') flow = Flow.objects.filter(name='Call me maybe').first() user_settings = self.admin.get_settings() user_settings.tel = '+18005551212' user_settings.save() test_contact = Contact.get_test_contact(self.admin) Contact.set_simulation(True) flow.start([], [test_contact]) log = ActionLog.objects.all().order_by('-pk').first() self.assertEquals(log.text, 'Call ended. Could not authenticate with your Twilio account. ' 'Check your token and try again.')
def send_message( self, flow, message, restart_participants=False, contact=None, initiate_flow=False, assert_reply=True, assert_handle=True, ): """ Starts the flow, sends the message, returns the reply """ if not contact: contact = self.contact try: if contact.is_test: Contact.set_simulation(True) incoming = self.create_msg( direction=INCOMING, contact=contact, contact_urn=contact.get_urn(), text=message ) # start the flow if initiate_flow: flow.start( groups=[], contacts=[contact], restart_participants=restart_participants, start_msg=incoming ) else: flow.start(groups=[], contacts=[contact], restart_participants=restart_participants) (handled, msgs) = Flow.find_and_handle(incoming) Msg.mark_handled(incoming) if assert_handle: self.assertTrue(handled, "'%s' did not handle message as expected" % flow.name) else: self.assertFalse(handled, "'%s' handled message, was supposed to ignore" % flow.name) # our message should have gotten a reply if assert_reply: replies = Msg.objects.filter(response_to=incoming).order_by("pk") self.assertGreaterEqual(len(replies), 1) if len(replies) == 1: self.assertEqual(contact, replies.first().contact) return replies.first().text # if it's more than one, send back a list of replies return [reply.text for reply in replies] else: # assert we got no reply replies = Msg.objects.filter(response_to=incoming).order_by("pk") self.assertFalse(replies) return None finally: Contact.set_simulation(False)
def send_message(self, flow, message, restart_participants=False, contact=None, initiate_flow=False, assert_reply=True): """ Starts the flow, sends the message, returns the reply """ if not contact: contact = self.contact try: if contact.is_test: Contact.set_simulation(True) incoming = self.create_msg(direction=INCOMING, contact=contact, text=message) # start the flow if initiate_flow: flow.start(groups=[], contacts=[contact], restart_participants=restart_participants, start_msg=incoming) else: flow.start(groups=[], contacts=[contact], restart_participants=restart_participants) self.assertTrue(flow.find_and_handle(incoming)) # our message should have gotten a reply if assert_reply: reply = Msg.objects.get(response_to=incoming) self.assertEquals(contact, reply.contact) return reply.text return None finally: Contact.set_simulation(False)
def test_rule_first_ivr_flow(self): # connect it and check our client is configured self.org.connect_twilio("TEST_SID", "TEST_TOKEN") self.org.save() # import an ivr flow self.import_file('rule-first-ivr') flow = Flow.objects.filter(name='Rule First IVR').first() user_settings = self.admin.get_settings() user_settings.tel = '+18005551212' user_settings.save() # start our flow` eric = self.create_contact('Eric Newcomer', number='+13603621737') eric.is_test = True eric.save() Contact.set_simulation(True) flow.start([], [eric]) # should be using the usersettings number in test mode self.assertEquals('Placing test call to +1 800-555-1212', ActionLog.objects.all().first().text) # we should have an outbound ivr call now call = IVRCall.objects.filter(direction=OUTGOING).first() self.assertEquals(0, call.get_duration()) self.assertIsNotNone(call) self.assertEquals('CallSid', call.external_id) # after a call is picked up, twilio will call back to our server post_data = dict(CallSid='CallSid', CallStatus='in-progress', CallDuration=20) response = self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), post_data) self.assertContains(response, '<Say>Thanks for calling!</Say>')
def get_context_data(self, **kwargs): org = self.request.user.get_org() # if there isn't a search filtering the queryset, we can replace the count function with a quick cache lookup to # speed up paging if hasattr(self, 'folder') and 'search' not in self.request.REQUEST: org.patch_folder_queryset(self.object_list, self.folder, self.request) context = super(ContactListView, self).get_context_data(**kwargs) folders = [dict(count=org.get_folder_count(OrgFolder.contacts_all), label=_("All Contacts"), url=reverse('contacts.contact_list')), dict(count=org.get_folder_count(OrgFolder.contacts_failed), label=_("Failed"), url=reverse('contacts.contact_failed')), dict(count=org.get_folder_count(OrgFolder.contacts_blocked), label=_("Blocked"), url=reverse('contacts.contact_blocked'))] groups_qs = ContactGroup.user_groups.filter(org=org, is_active=True).select_related('org') groups_qs = groups_qs.extra(select={'lower_group_name': 'lower(contacts_contactgroup.name)'}).order_by('lower_group_name') groups = [dict(pk=g.pk, label=g.name, count=g.get_member_count(), is_dynamic=g.is_dynamic) for g in groups_qs] # resolve the paginated object list so we can initialize a cache of URNs and fields contacts = list(context['object_list']) Contact.bulk_cache_initialize(org, contacts, for_show_only=True) context['contacts'] = contacts context['groups'] = groups context['folders'] = folders context['has_contacts'] = contacts or org.has_contacts() context['send_form'] = SendMessageForm(self.request.user) return context
def save(self, obj): urns = [] for field_key, value in self.form.cleaned_data.iteritems(): if field_key.startswith('__urn__') and value: scheme = field_key[7:] urns.append((scheme, value)) Contact.get_or_create(obj.org, self.request.user, obj.name, urns)
def send(self, message, contact=None): if not contact: contact = self.contact if contact.is_test: Contact.set_simulation(True) incoming = self.create_msg(direction=INCOMING, contact=contact, text=message) Flow.find_and_handle(incoming) return Msg.all_messages.filter(response_to=incoming).order_by('pk').first()
def setUp(self): # if we are super verbose, turn on debug for sql queries if self.get_verbosity() > 2: settings.DEBUG = True self.clear_cache() self.superuser = User.objects.create_superuser(username="******", email="*****@*****.**", password="******") # create different user types self.non_org_user = self.create_user("NonOrg") self.user = self.create_user("User") self.editor = self.create_user("Editor") self.admin = self.create_user("Administrator") self.surveyor = self.create_user("Surveyor") # setup admin boundaries for Rwanda self.country = AdminBoundary.objects.create(osm_id='171496', name='Rwanda', level=0) self.state1 = AdminBoundary.objects.create(osm_id='1708283', name='Kigali City', level=1, parent=self.country) self.state2 = AdminBoundary.objects.create(osm_id='171591', name='Eastern Province', level=1, parent=self.country) self.district1 = AdminBoundary.objects.create(osm_id='1711131', name='Gatsibo', level=2, parent=self.state2) self.district2 = AdminBoundary.objects.create(osm_id='1711163', name='Kayônza', level=2, parent=self.state2) self.district3 = AdminBoundary.objects.create(osm_id='3963734', name='Nyarugenge', level=2, parent=self.state1) self.district4 = AdminBoundary.objects.create(osm_id='1711142', name='Rwamagana', level=2, parent=self.state2) self.ward1 = AdminBoundary.objects.create(osm_id='171113181', name='Kageyo', level=3, parent=self.district1) self.ward2 = AdminBoundary.objects.create(osm_id='171116381', name='Kabare', level=3, parent=self.district2) self.ward3 = AdminBoundary.objects.create(osm_id='171114281', name='Bukure', level=3, parent=self.district4) self.org = Org.objects.create(name="Temba", timezone="Africa/Kigali", country=self.country, brand=settings.DEFAULT_BRAND, created_by=self.user, modified_by=self.user) self.org.initialize(topup_size=1000) # add users to the org self.user.set_org(self.org) self.org.viewers.add(self.user) self.editor.set_org(self.org) self.org.editors.add(self.editor) self.admin.set_org(self.org) self.org.administrators.add(self.admin) self.surveyor.set_org(self.org) self.org.surveyors.add(self.surveyor) self.superuser.set_org(self.org) # welcome topup with 1000 credits self.welcome_topup = self.org.topups.all()[0] # a single Android channel self.channel = Channel.create(self.org, self.user, 'RW', 'A', name="Test Channel", address="+250785551212", device="Nexus 5X", secret="12345", gcm_id="123") # reset our simulation to False Contact.set_simulation(False)
def send(self, message, contact=None): if not contact: contact = self.contact if contact.is_test: Contact.set_simulation(True) incoming = self.create_msg(direction=INCOMING, contact=contact, text=message) # evaluate the inbound message against our triggers first from temba.triggers.models import Trigger if not Trigger.find_and_handle(incoming): Flow.find_and_handle(incoming) return Msg.objects.filter(response_to=incoming).order_by('pk').first()
def setUp(self): self.clear_cache() self.superuser = User.objects.create_superuser(username="******", email="*****@*****.**", password="******") # some users not tied to our org self.non_org_user = self.create_user("NonOrg") self.non_org_manager = self.create_user("NonOrgManager") # our three user types inside our org self.user = self.create_user("User") self.root = self.create_user("Root") self.root.groups.add(Group.objects.get(name="Alpha")) self.admin = self.create_user("Administrator") # setup admin boundaries for Rwanda self.country = AdminBoundary.objects.create(osm_id='171496', name='Rwanda', level=0) state1 = AdminBoundary.objects.create(osm_id='1708283', name='Kigali City', level=1, parent=self.country) state2 = AdminBoundary.objects.create(osm_id='171591', name='Eastern Province', level=1, parent=self.country) AdminBoundary.objects.create(osm_id='1711131', name='Gatsibo', level=2, parent=state2) AdminBoundary.objects.create(osm_id='1711163', name='Kayonza', level=2, parent=state2) AdminBoundary.objects.create(osm_id='60485579', name='Kigali', level=2, parent=state1) AdminBoundary.objects.create(osm_id='1711142', name='Rwamagana', level=2, parent=state2) self.org = Org.objects.create(name="Temba", timezone="Africa/Kigali", country=self.country, created_by=self.user, modified_by=self.user) self.org.initialize() # add users to the org self.org.administrators.add(self.admin) self.admin.set_org(self.org) self.org.administrators.add(self.root) self.root.set_org(self.org) self.user.set_org(self.org) self.superuser.set_org(self.org) # welcome topup with 1000 credits self.welcome_topup = self.org.topups.all()[0] # a single Android channel self.channel = Channel.objects.create(org=self.org, name="Test Channel", address="+250785551212", country='RW', channel_type='A', secret="12345", gcm_id="123", created_by=self.user, modified_by=self.user) # reset our simulation to False Contact.set_simulation(False)
def setUp(self): self.clear_cache() self.superuser = User.objects.create_superuser(username="******", email="*****@*****.**", password="******") # create different user types self.non_org_user = self.create_user("NonOrg") self.user = self.create_user("User") self.editor = self.create_user("Editor") self.admin = self.create_user("Administrator") self.surveyor = self.create_user("Surveyor") # setup admin boundaries for Rwanda self.country = AdminBoundary.objects.create(osm_id='171496', name='Rwanda', level=0) self.state1 = AdminBoundary.objects.create(osm_id='1708283', name='Kigali City', level=1, parent=self.country) self.state2 = AdminBoundary.objects.create(osm_id='171591', name='Eastern Province', level=1, parent=self.country) self.district1 = AdminBoundary.objects.create(osm_id='1711131', name='Gatsibo', level=2, parent=self.state2) self.district2 = AdminBoundary.objects.create(osm_id='1711163', name='Kayonza', level=2, parent=self.state2) self.district3 = AdminBoundary.objects.create(osm_id='60485579', name='Kigali', level=2, parent=self.state1) self.district4 = AdminBoundary.objects.create(osm_id='1711142', name='Rwamagana', level=2, parent=self.state2) self.org = Org.objects.create(name="Temba", timezone="Africa/Kigali", country=self.country, created_by=self.user, modified_by=self.user) self.org.initialize() # add users to the org self.user.set_org(self.org) self.org.viewers.add(self.user) self.editor.set_org(self.org) self.org.editors.add(self.editor) self.admin.set_org(self.org) self.org.administrators.add(self.admin) self.surveyor.set_org(self.org) self.org.surveyors.add(self.surveyor) self.superuser.set_org(self.org) # welcome topup with 1000 credits self.welcome_topup = self.org.topups.all()[0] # a single Android channel self.channel = Channel.create(self.org, self.user, 'RW', 'A', name="Test Channel", address="+250785551212", secret="12345", gcm_id="123") # reset our simulation to False Contact.set_simulation(False)
def test_contact_search(self): contacts = self._create_contacts(10000, ["Bobby", "Jimmy", "Mary"]) self._create_values(contacts, self.field_nick, lambda c: c.name.lower().replace(' ', '_')) with SegmentProfiler("Contact search with simple query", self, force_profile=True): qs, is_complex = Contact.search(self.org, 'bob') self.assertEqual(3334, qs.count()) self.assertEqual(False, is_complex) with SegmentProfiler("Contact search with complex query", self, force_profile=True): qs, is_complex = Contact.search(self.org, 'name = bob or tel has 078 or twitter = tweep_123 or nick is bob') self.assertEqual(3377, qs.count()) self.assertEqual(True, is_complex)
def restore_object(self, attrs, instance=None): """ Actually start our flows for each contact """ if instance: # pragma: no cover raise ValidationError("Invalid operation") flow = attrs['flow'] groups = attrs.get('groups', []) contacts = attrs.get('contacts', []) extra = attrs.get('extra', None) # include contacts created/matched via deprecated phone field phone_urns = attrs.get('phone', []) if phone_urns: channel = self.org.get_send_channel(TEL_SCHEME) for urn in phone_urns: # treat each URN as separate contact contact = Contact.get_or_create(self.user, channel.org, urns=[urn], channel=channel) contacts.append(contact) if contacts or groups: return flow.start(groups, contacts, restart_participants=True, extra=extra) else: return []
def restore_object(self, attrs, instance=None): """ Create a new broadcast to send out """ if instance: # pragma: no cover raise ValidationError("Invalid operation") user = self.user org = self.org if 'urn' in attrs and attrs['urn']: urns = attrs.get('urn', []) else: urns = attrs.get('phone', []) channel = attrs['channel'] contacts = list() for urn in urns: # treat each urn as a separate contact contacts.append(Contact.get_or_create(user, channel.org, urns=[urn], channel=channel)) # add any contacts specified by uuids uuid_contacts = attrs.get('contact', []) for contact in uuid_contacts: contacts.append(contact) # create the broadcast broadcast = Broadcast.create(org, user, attrs['text'], recipients=contacts) # send it broadcast.send() return broadcast
def save(self): """ Create a new broadcast to send out """ if "urn" in self.validated_data and self.validated_data["urn"]: urns = self.validated_data.get("urn") else: urns = self.validated_data.get("phone", []) channel = self.validated_data.get("channel") contacts = list() for urn in urns: # treat each urn as a separate contact contact, urn_obj = Contact.get_or_create(channel.org, urn, user=self.user) contacts.append(contact) # add any contacts specified by uuids uuid_contacts = self.validated_data.get("contact", []) for contact in uuid_contacts: contacts.append(contact) # create the broadcast broadcast = Broadcast.create( self.org, self.user, self.validated_data["text"], contacts=contacts, channel=channel ) # send it broadcast.send(expressions_context={}) return broadcast
def start_call(self, call, to, from_, status_callback): channel = call.channel Contact.get_or_create(channel.org, channel.created_by, urns=[(TEL_SCHEME, to)]) # Verboice differs from Twilio in that they expect the first block of twiml up front payload = unicode(Flow.handle_call(call, {})) # now we can post that to verboice url = "%s?%s" % (self.endpoint, urlencode(dict(channel=self.verboice_channel, address=to))) response = requests.post(url, data=payload, auth=self.auth).json() # store the verboice call id in our IVRCall call.external_id = response['call_id'] call.status = IN_PROGRESS call.save()
def save(self): """ Create a new broadcast to send out """ contact_urns = [] for urn in self.validated_data.get("urns", []): # create contacts for URNs if necessary __, contact_urn = Contact.get_or_create(self.context["org"], urn, user=self.context["user"]) contact_urns.append(contact_urn) text, base_language = self.validated_data["text"] # create the broadcast broadcast = Broadcast.create( self.context["org"], self.context["user"], text=text, base_language=base_language, groups=self.validated_data.get("groups", []), contacts=self.validated_data.get("contacts", []), urns=contact_urns, channel=self.validated_data.get("channel"), ) # send in task on_transaction_commit(lambda: send_broadcast_task.delay(broadcast.id)) return broadcast
def send_spam(user_id, contact_id): # pragma: no cover """ Processses a single incoming message through our queue. """ from django.contrib.auth.models import User from temba.contacts.models import Contact, TEL_SCHEME from temba.msgs.models import Broadcast contact = Contact.all().get(pk=contact_id) user = User.objects.get(pk=user_id) channel = contact.org.get_send_channel(TEL_SCHEME) if not channel: # pragma: no cover print("Sorry, no channel to be all spammy with") return long_text = ( "Test Message #%d. The path of the righteous man is beset on all sides by the iniquities of the " "selfish and the tyranny of evil men. Blessed is your face." ) # only trigger sync on the last one for idx in range(10): broadcast = Broadcast.create(contact.org, user, long_text % (idx + 1), contacts=[contact]) broadcast.send(trigger_send=(idx == 149))
def create_contact(self, name=None, number=None, twitter=None, urn=None, is_test=False, **kwargs): """ Create a contact in the master test org """ urns = [] if number: urns.append((TEL_SCHEME, number)) if twitter: urns.append((TWITTER_SCHEME, twitter)) if urn: urns.append(urn) if not name and not urns: # pragma: no cover raise ValueError("Need a name or URN to create a contact") kwargs['name'] = name kwargs['urns'] = urns kwargs['is_test'] = is_test if 'org' not in kwargs: kwargs['org'] = self.org if 'user' not in kwargs: kwargs['user'] = self.user return Contact.get_or_create(**kwargs)
def validate_urns(self, value): urn_contacts = [] for urn in value: contact = Contact.get_or_create(self.context['org'], self.context['user'], urns=[urn]) urn_contacts.append(contact) return urn_contacts
def create_contact(self, name=None, number=None, twitter=None, twitterid=None, urn=None, is_test=False, **kwargs): """ Create a contact in the master test org """ urns = [] if number: urns.append(URN.from_tel(number)) if twitter: urns.append(URN.from_twitter(twitter)) if twitterid: urns.append(URN.from_twitterid(twitterid)) if urn: urns.append(urn) if not name and not urns: # pragma: no cover raise ValueError("Need a name or URN to create a contact") kwargs["name"] = name kwargs["urns"] = urns kwargs["is_test"] = is_test if "org" not in kwargs: kwargs["org"] = self.org if "user" not in kwargs: kwargs["user"] = self.user return Contact.get_or_create_by_urns(**kwargs)
def restore_object(self, attrs, instance=None): """ Actually start our flows for each contact """ if instance: # pragma: no cover raise ValidationError("Invalid operation") flow = attrs['flow'] channel = self.org.get_send_channel(TEL_SCHEME) if 'urn' in attrs and attrs['urn']: urns = attrs.get('urn', []) else: urns = attrs.get('phone', []) contacts = [] for urn in urns: # treat each URN as separate contact contact = Contact.get_or_create(self.user, channel.org, urns=[urn], channel=channel) contacts.append(contact) # also add in any contacts specified by uuid uuid_contacts = attrs.get('contact', []) contacts += uuid_contacts if contacts: runs = flow.start([], contacts, restart_participants=True, extra=attrs.get('extra', None)) else: runs = [] return runs
def get_form(self, form_class): form = super(ContactCRUDL.Customize, self).get_form(form_class) form.fields.clear() self.headers = Contact.get_import_file_headers(self.get_object().csv_file.file) self.column_controls = self.create_column_controls(self.headers) return form
def get_or_create_contact(self, urn): if ':' in urn: parsed = ContactURN.parse_urn(urn) urn = (parsed.scheme, parsed.path) else: urn = (TEL_SCHEME, urn) # assume phone number return Contact.get_or_create(self.org, self.user, name=None, urns=[urn])
def get_contact_fields(self, obj): if not obj.is_active: return {} fields = {} for contact_field in self.context['contact_fields']: value = obj.get_field(contact_field.key) fields[contact_field.key] = Contact.serialize_field_value(contact_field, value) return fields
def start_call(self, call, to, from_, status_callback): channel = call.channel Contact.get_or_create(channel.org, channel.created_by, urns=[(TEL_SCHEME, to)]) # Verboice differs from Twilio in that they expect the first block of twiml up front payload = unicode(Flow.handle_call(call, {})) # our config should have our http basic auth parameters and verboice channel config = channel.config_json() # now we can post that to verboice url = "%s?%s" % (self.endpoint, urlencode(dict(channel=config["channel"], address=to))) response = requests.post(url, data=payload, auth=(config["username"], config["password"])).json() # store the verboice call id in our IVRCall call.external_id = response["call_id"] call.status = IN_PROGRESS call.save()
def get_queryset(self, **kwargs): qs = super(ContactListView, self).get_queryset(**kwargs) org = self.request.user.get_org() # contact list views don't use regular field searching but use more complex contact searching query = self.request.REQUEST.get('search', None) if query: qs, self.request.compiled_query = Contact.search(org, query, qs) return qs.prefetch_related('groups')
def _create_contacts(self, count, base_names): """ Creates the given number of contacts with URNs of each type, and fields value for dob and nickname """ contacts = [] for c in range(0, count): name = '%s %d' % (base_names[c % len(base_names)], c + 1) scheme, path, channel = self.urn_generators[c % len(self.urn_generators)](c) contacts.append(Contact.get_or_create(self.org, self.user, name, urns=[':'.join([scheme, path])])) return contacts
def post(self, request, *args, **kwargs): from twilio.util import RequestValidator from temba.flows.models import FlowSession signature = request.META.get("HTTP_X_TWILIO_SIGNATURE", "") url = "https://" + request.get_host() + "%s" % request.get_full_path() channel_uuid = kwargs.get("uuid") call_sid = self.get_param("CallSid") direction = self.get_param("Direction") status = self.get_param("CallStatus") to_number = self.get_param("To") to_country = self.get_param("ToCountry") from_number = self.get_param("From") # Twilio sometimes sends un-normalized numbers if to_number and not to_number.startswith( "+") and to_country: # pragma: no cover to_number, valid = URN.normalize_number(to_number, to_country) # see if it's a twilio call being initiated if to_number and call_sid and direction == "inbound" and status == "ringing": # find a channel that knows how to answer twilio calls channel = self.get_ringing_channel(uuid=channel_uuid) if not channel: response = twiml.Response() response.say( "Sorry, there is no channel configured to take this call. Goodbye." ) response.hangup() return HttpResponse(str(response)) org = channel.org if self.get_channel_type( ) == "T" and not org.is_connected_to_twilio(): return HttpResponse("No Twilio account is connected", status=400) client = self.get_client(channel=channel) validator = RequestValidator(client.auth[1]) signature = request.META.get("HTTP_X_TWILIO_SIGNATURE", "") url = "https://%s%s" % (request.get_host(), request.get_full_path()) if validator.validate(url, request.POST, signature): from temba.ivr.models import IVRCall # find a contact for the one initiating us urn = URN.from_tel(from_number) contact, urn_obj = Contact.get_or_create( channel.org, urn, channel) flow = Trigger.find_flow_for_inbound_call(contact) if flow: call = IVRCall.create_incoming(channel, contact, urn_obj, channel.created_by, call_sid) session = FlowSession.create(contact, connection=call) call.update_status(request.POST.get("CallStatus", None), request.POST.get("CallDuration", None), "T") call.save() FlowRun.create(flow, contact, session=session, connection=call) response = Flow.handle_call(call) return HttpResponse(str(response)) else: # we don't have an inbound trigger to deal with this call. response = channel.generate_ivr_response() # say nothing and hangup, this is a little rude, but if we reject the call, then # they'll get a non-working number error. We send 'busy' when our server is down # so we don't want to use that here either. response.say("") response.hangup() # if they have a missed call trigger, fire that off Trigger.catch_triggers(contact, Trigger.TYPE_MISSED_CALL, channel) # either way, we need to hangup now return HttpResponse(str(response)) # check for call progress events, these include post-call hangup notifications if request.POST.get("CallbackSource", None) == "call-progress-events": if call_sid: from temba.ivr.models import IVRCall call = IVRCall.objects.filter(external_id=call_sid).first() if call: call.update_status(request.POST.get("CallStatus", None), request.POST.get("CallDuration", None), "TW") call.save() return HttpResponse("Call status updated") return HttpResponse("No call found") return HttpResponse("Not Handled, unknown action", status=400) # pragma: no cover
def get(self, request, *args, **kwargs): from temba.flows.models import FlowSession from temba.ivr.models import IVRCall action = kwargs["action"].lower() request_body = force_text(request.body) request_path = request.get_full_path() request_method = request.method request_uuid = kwargs["uuid"] if action == "event": if not request_body: return HttpResponse("") body_json = json.loads(request_body) status = body_json.get("status", None) duration = body_json.get("duration", None) call_uuid = body_json.get("uuid", None) conversation_uuid = body_json.get("conversation_uuid", None) if call_uuid is None: return HttpResponse("Missing uuid parameter, ignoring") call = IVRCall.objects.filter(external_id=call_uuid).first() if not call: # try looking up by the conversation uuid (inbound calls start with that) call = IVRCall.objects.filter( external_id=conversation_uuid).first() if call: call.external_id = call_uuid call.save() else: response = dict(message="Call not found for %s" % call_uuid) return JsonResponse(response) channel = call.channel channel_type = channel.channel_type call.update_status(status, duration, channel_type) call.save() response = dict(description="Updated call status", call=dict(status=call.get_status_display(), duration=call.duration)) event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response)) ChannelLog.log_ivr_interaction(call, "Updated call status", event) if call.status == IVRCall.COMPLETED: # if our call is completed, hangup runs = FlowRun.objects.filter(connection=call) for run in runs: if not run.is_completed(): run.set_completed(exit_uuid=None) return JsonResponse(response) if action == "answer": if not request_body: return HttpResponse("") body_json = json.loads(request_body) from_number = body_json.get("from", None) channel_number = body_json.get("to", None) external_id = body_json.get("conversation_uuid", None) if not from_number or not channel_number or not external_id: return HttpResponse("Missing parameters, Ignoring") # look up the channel address_q = Q(address=channel_number) | Q(address=("+" + channel_number)) channel = Channel.objects.filter(address_q).filter( is_active=True, channel_type="NX").first() # make sure we got one, and that it matches the key for our org org_uuid = None if channel: org_uuid = channel.org.config.get(NEXMO_UUID, None) if not channel or org_uuid != request_uuid: return HttpResponse("Channel not found for number: %s" % channel_number, status=404) urn = URN.from_tel(from_number) contact, urn_obj = Contact.get_or_create(channel.org, urn, channel) flow = Trigger.find_flow_for_inbound_call(contact) if flow: call = IVRCall.create_incoming(channel, contact, urn_obj, channel.created_by, external_id) session = FlowSession.create(contact, connection=call) FlowRun.create(flow, contact, session=session, connection=call) response = Flow.handle_call(call) event = HttpEvent(request_method, request_path, request_body, 200, str(response)) ChannelLog.log_ivr_interaction(call, "Incoming request for call", event) return JsonResponse(json.loads(str(response)), safe=False) else: # we don't have an inbound trigger to deal with this call. response = channel.generate_ivr_response() # say nothing and hangup, this is a little rude, but if we reject the call, then # they'll get a non-working number error. We send 'busy' when our server is down # so we don't want to use that here either. response.say("") response.hangup() # if they have a missed call trigger, fire that off Trigger.catch_triggers(contact, Trigger.TYPE_MISSED_CALL, channel) # either way, we need to hangup now return JsonResponse(json.loads(str(response)), safe=False)
def prepare_for_serialization(self, object_list): # initialize caches of all contact fields and URNs org = self.request.user.get_org() Contact.bulk_cache_initialize(org, object_list)
def setUp(self): self.clear_cache() self.superuser = User.objects.create_superuser(username="******", email="*****@*****.**", password="******") # create different user types self.non_org_user = self.create_user("NonOrg") self.user = self.create_user("User") self.editor = self.create_user("Editor") self.admin = self.create_user("Administrator") # setup admin boundaries for Rwanda self.country = AdminBoundary.objects.create(osm_id='171496', name='Rwanda', level=0) state1 = AdminBoundary.objects.create(osm_id='1708283', name='Kigali City', level=1, parent=self.country) state2 = AdminBoundary.objects.create(osm_id='171591', name='Eastern Province', level=1, parent=self.country) AdminBoundary.objects.create(osm_id='1711131', name='Gatsibo', level=2, parent=state2) AdminBoundary.objects.create(osm_id='1711163', name='Kayonza', level=2, parent=state2) AdminBoundary.objects.create(osm_id='60485579', name='Kigali', level=2, parent=state1) AdminBoundary.objects.create(osm_id='1711142', name='Rwamagana', level=2, parent=state2) self.org = Org.objects.create(name="Temba", timezone="Africa/Kigali", country=self.country, created_by=self.user, modified_by=self.user) self.org.initialize() # add users to the org self.user.set_org(self.org) self.org.viewers.add(self.user) self.editor.set_org(self.org) self.org.editors.add(self.editor) self.admin.set_org(self.org) self.org.administrators.add(self.admin) self.superuser.set_org(self.org) # welcome topup with 1000 credits self.welcome_topup = self.org.topups.all()[0] # a single Android channel self.channel = Channel.objects.create(org=self.org, name="Test Channel", address="+250785551212", country='RW', channel_type='A', secret="12345", gcm_id="123", created_by=self.user, modified_by=self.user) # reset our simulation to False Contact.set_simulation(False)
def validate_urn(self, value): if self.context['org'].is_anon: raise serializers.ValidationError("Referencing by URN not allowed for anonymous organizations") self.instance = Contact.from_urn(self.context['org'], value) return value
def save(self): """ Update our contact """ name = self.validated_data.get("name") fields = self.validated_data.get("fields") language = self.validated_data.get("language") # treat empty names as None if not name: name = None changed = [] if self.instance: if self.parsed_urns is not None: self.instance.update_urns(self.user, self.parsed_urns) # update our name and language if name != self.instance.name: self.instance.name = name changed.append("name") else: self.instance = Contact.get_or_create_by_urns( self.org, self.user, name, urns=self.parsed_urns, language=language, force_urn_update=True) # Contact.get_or_create doesn't nullify language so do that here if "language" in self.validated_data and language is None: self.instance.language = language.lower() if language else None changed.append("language") # save our contact if it changed if changed: self.instance.save(update_fields=changed, handle_update=True) # update our fields if fields is not None: for key, value in fields.items(): existing_by_key = ContactField.user_fields.filter( org=self.org, key__iexact=key, is_active=True).first() if existing_by_key: self.instance.set_field(self.user, existing_by_key.key, value) continue elif self.new_fields and key in self.new_fields: new_field = ContactField.get_or_create( org=self.org, user=self.user, key=regex.sub("[^A-Za-z0-9]+", "_", key).lower(), label=key) self.instance.set_field(self.user, new_field.key, value) # TODO as above, need to get users to stop updating via label existing_by_label = ContactField.get_by_label(self.org, key) if existing_by_label: self.instance.set_field(self.user, existing_by_label.key, value) # update our contact's groups if self.group_objs is not None: self.instance.update_static_groups(self.user, self.group_objs) return self.instance
class USSDSession(ChannelSession): USSD_PULL = INCOMING = "I" USSD_PUSH = OUTGOING = "O" objects = USSDQuerySet.as_manager() class Meta: proxy = True @property def should_end(self): return self.status == self.ENDING def mark_ending(self): # session to be ended if self.status != self.ENDING: self.status = self.ENDING self.save(update_fields=["status"]) def close(self): # session has successfully ended if self.status == self.ENDING: self.status = self.COMPLETED else: self.status = self.INTERRUPTED self.ended_on = timezone.now() self.save(update_fields=["status", "ended_on"]) def start_async(self, flow, date, message_id): from temba.msgs.models import Msg, USSD message = Msg.objects.create( channel=self.channel, contact=self.contact, contact_urn=self.contact_urn, sent_on=date, connection=self, msg_type=USSD, external_id=message_id, created_on=timezone.now(), modified_on=timezone.now(), org=self.channel.org, direction=self.INCOMING, ) flow.start([], [self.contact], start_msg=message, restart_participants=True, connection=self) def handle_async(self, urn, content, date, message_id): from temba.msgs.models import Msg, USSD Msg.create_incoming( channel=self.channel, org=self.org, urn=urn, text=content or "", sent_on=date, connection=self, msg_type=USSD, external_id=message_id, ) def handle_sync(self): # pragma: needs cover # TODO: implement for InfoBip and other sync APIs pass @classmethod def handle_incoming( cls, channel, urn, date, external_id, contact=None, message_id=None, status=None, content=None, starcode=None, org=None, async=True, ): trigger = None contact_urn = None # handle contact with channel urn = URN.from_tel(urn) if not contact: contact, contact_urn = Contact.get_or_create( channel.org, urn, channel) elif urn: contact_urn = ContactURN.get_or_create(org, contact, urn, channel=channel) contact.set_preferred_channel(channel) if contact_urn: contact_urn.update_affinity(channel) # setup session defaults = dict(channel=channel, contact=contact, contact_urn=contact_urn, org=channel.org if channel else contact.org) if status == cls.TRIGGERED: trigger = Trigger.find_trigger_for_ussd_session(contact, starcode) if not trigger: return False defaults.update( dict(started_on=date, direction=cls.USSD_PULL, status=status)) elif status == cls.INTERRUPTED: defaults.update(dict(ended_on=date, status=status)) else: defaults.update(dict(status=cls.IN_PROGRESS)) # check if there's an initiated PUSH connection connection = cls.objects.get_initiated_push(contact) created = False if not connection: try: connection = (cls.objects.select_for_update().exclude( status__in=ChannelSession.DONE).get( external_id=external_id)) created = False for k, v in defaults.items(): setattr(connection, k, v() if callable(v) else v) connection.save() except cls.DoesNotExist: defaults["external_id"] = external_id connection = cls.objects.create(**defaults) FlowSession.create(contact, connection=connection) created = True else: defaults.update(dict(external_id=external_id)) for key, value in defaults.items(): setattr(connection, key, value) connection.save() created = None # start session if created and async and trigger: connection.start_async(trigger.flow, date, message_id)
def test_ivr_options(self): # should be able to create an ivr flow self.assertTrue(self.org.supports_ivr()) self.assertTrue(self.admin.groups.filter(name="Beta")) self.assertContains(self.client.get(reverse('flows.flow_create')), 'Phone Call') # no twilio config yet self.assertFalse(self.org.is_connected_to_twilio()) self.assertIsNone(self.org.get_twilio_client()) # connect it and check our client is configured self.org.connect_twilio("TEST_SID", "TEST_TOKEN") self.org.save() self.assertTrue(self.org.is_connected_to_twilio()) self.assertIsNotNone(self.org.get_twilio_client()) # import an ivr flow self.import_file('call-me-maybe') # make sure our flow is there as expected flow = Flow.objects.filter(name='Call me maybe').first() self.assertEquals('callme', flow.triggers.all().first().keyword) user_settings = self.admin.get_settings() user_settings.tel = '+18005551212' user_settings.save() # start our flow` eric = self.create_contact('Eric Newcomer', number='+13603621737') eric.is_test = True eric.save() Contact.set_simulation(True) flow.start([], [eric]) # should be using the usersettings number in test mode self.assertEquals('Placing test call to +1 800-555-1212', ActionLog.objects.all().first().text) # we should have an outbound ivr call now call = IVRCall.objects.filter(direction=OUTGOING).first() self.assertEquals(0, call.get_duration()) self.assertIsNotNone(call) self.assertEquals('CallSid', call.external_id) # after a call is picked up, twilio will call back to our server post_data = dict(CallSid='CallSid', CallStatus='in-progress', CallDuration=20) response = self.client.post( reverse('ivr.ivrcall_handle', args=[call.pk]), post_data) self.assertContains( response, '<Say>Would you like me to call you? Press one for yes, two for no, or three for maybe.</Say>' ) # updated our status and duration accordingly call = IVRCall.objects.get(pk=call.pk) self.assertEquals(20, call.duration) self.assertEquals(IN_PROGRESS, call.status) # should mention our our action log that we read a message to them run = FlowRun.objects.all().first() logs = ActionLog.objects.filter(run=run).order_by('-pk') self.assertEquals(2, len(logs)) self.assertEquals( 'Read message "Would you like me to call you? Press one for yes, two for no, or three for maybe."', logs.first().text) # press the number 4 (unexpected) response = self.client.post( reverse('ivr.ivrcall_handle', args=[call.pk]), dict(Digits=4)) self.assertContains(response, '<Say>Press one, two, or three. Thanks.</Say>') # now let's have them press the number 3 (for maybe) response = self.client.post( reverse('ivr.ivrcall_handle', args=[call.pk]), dict(Digits=3)) self.assertContains(response, '<Say>This might be crazy.</Say>') # twilio would then disconnect the user and notify us of a completed call self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), dict(CallStatus='completed')) self.assertEquals(COMPLETED, IVRCall.objects.get(pk=call.pk).status) # simulation gets flipped off by middleware, and this unhandled message doesn't flip it back on self.assertFalse(Contact.get_simulation()) # test other our call status mappings with twilio def test_status_update(call_to_update, twilio_status, temba_status): call_to_update.update_status(twilio_status, 0) call_to_update.save() self.assertEquals(temba_status, IVRCall.objects.get(pk=call_to_update.pk).status) test_status_update(call, 'queued', QUEUED) test_status_update(call, 'ringing', RINGING) test_status_update(call, 'canceled', CANCELED) test_status_update(call, 'busy', BUSY) test_status_update(call, 'failed', FAILED) test_status_update(call, 'no-answer', NO_ANSWER) # explicitly hanging up an in progress call should remove it call.update_status('in-progress', 0) call.save() IVRCall.hangup_test_call(flow) self.assertIsNone(IVRCall.objects.filter(pk=call.pk).first())
def __call__(self, request): Contact.set_simulation(False) response = self.get_response(request) return response
def test_new_conversation_trigger(self): self.login(self.admin) flow = self.create_flow() flow2 = self.create_flow() # see if we list new conversation triggers on the trigger page create_trigger_url = reverse('triggers.trigger_create', args=[]) response = self.client.get(create_trigger_url) self.assertNotContains(response, "conversation is started") # create a facebook channel fb_channel = Channel.add_facebook_channel(self.org, self.user, 'Temba', 1001, 'fb_token') # should now be able to create one response = self.client.get(create_trigger_url) self.assertContains(response, "conversation is started") # go create it with patch('requests.post') as mock_post: mock_post.return_value = MockResponse(200, '{"message": "Success"}') response = self.client.post(reverse( 'triggers.trigger_new_conversation', args=[]), data=dict(channel=fb_channel.id, flow=flow.id)) self.assertEqual(response.status_code, 200) self.assertEqual(mock_post.call_count, 1) # check that it is right trigger = Trigger.objects.get( trigger_type=Trigger.TYPE_NEW_CONVERSATION, is_active=True, is_archived=False) self.assertEqual(trigger.channel, fb_channel) self.assertEqual(trigger.flow, flow) # try to create another one, fails as we already have a trigger for that channel response = self.client.post(reverse( 'triggers.trigger_new_conversation', args=[]), data=dict(channel=fb_channel.id, flow=flow2.id)) self.assertEqual(response.status_code, 200) self.assertFormError(response, 'form', 'channel', 'Trigger with this Channel already exists.') # ok, trigger a facebook event data = json.loads("""{ "object": "page", "entry": [ { "id": "620308107999975", "time": 1467841778778, "messaging": [ { "sender":{ "id":"1001" }, "recipient":{ "id":"%s" }, "timestamp":1458692752478, "postback":{ "payload":"get_started" } } ] } ] } """ % fb_channel.address) with patch('requests.get') as mock_get: mock_get.return_value = MockResponse( 200, '{"first_name": "Ben","last_name": "Haggerty"}') callback_url = reverse('handlers.facebook_handler', args=[fb_channel.uuid]) response = self.client.post(callback_url, json.dumps(data), content_type="application/json") self.assertEqual(response.status_code, 200) # should have a new flow run for Ben contact = Contact.from_urn(self.org, 'facebook:1001') self.assertTrue(contact.name, "Ben Haggerty") run = FlowRun.objects.get(contact=contact) self.assertEqual(run.flow, flow) # archive our trigger, should unregister our callback with patch('requests.post') as mock_post: mock_post.return_value = MockResponse(200, '{"message": "Success"}') Trigger.apply_action_archive(self.admin, Trigger.objects.filter(pk=trigger.pk)) self.assertEqual(response.status_code, 200) self.assertEqual(mock_post.call_count, 1) trigger.refresh_from_db() self.assertTrue(trigger.is_archived)
def test_event_deliveries(self): sms = self.create_msg(contact=self.joe, direction="I", status="H", text="I'm gonna pop some tags") with patch("requests.Session.send") as mock: now = timezone.now() mock.return_value = MockResponse(200, "Hello World") # trigger an event, shouldnn't fire as we don't have a webhook WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now) self.assertFalse(WebHookEvent.objects.all()) self.setupChannel() with patch("requests.Session.send") as mock: # clear out which events we listen for, we still shouldnt be notified though we have a webhook self.channel.org.webhook_events = 0 self.channel.org.save() now = timezone.now() mock.return_value = MockResponse(200, "Hello World") # trigger an event, shouldnn't fire as we don't have a webhook WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now) self.assertFalse(WebHookEvent.objects.all()) self.setupChannel() with patch("requests.Session.send") as mock: # remove all the org users self.org.administrators.clear() self.org.editors.clear() self.org.viewers.clear() mock.return_value = MockResponse(200, "Hello World") # trigger an event WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() self.assertEqual("F", event.status) self.assertEqual(0, event.try_count) self.assertFalse(event.next_attempt) result = WebHookResult.objects.get() self.assertIn("No active user", result.message) self.assertEqual(0, result.status_code) self.assertFalse(mock.called) # what if they send weird json back? self.release(WebHookEvent.objects.all()) # add ad manager back in self.org.administrators.add(self.admin) self.admin.set_org(self.org) with patch("requests.Session.send") as mock: mock.return_value = MockResponse(200, "Hello World") # trigger an event WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() self.assertEqual("C", event.status) self.assertEqual(1, event.try_count) self.assertFalse(event.next_attempt) result = WebHookResult.objects.get() self.assertIn("Event delivered successfully", result.message) self.assertIn("not JSON", result.message) self.assertEqual(200, result.status_code) self.assertTrue(mock.called) self.release(WebHookEvent.objects.all()) with patch("requests.Session.send") as mock: mock.side_effect = [MockResponse(500, "I am error")] # trigger an event WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now) event = WebHookEvent.objects.all().first() self.assertEqual("E", event.status) self.assertEqual(1, event.try_count) self.assertTrue(event.next_attempt) mock.return_value = MockResponse(200, "Hello World") # simulate missing channel event.channel = None event.save() # no exception should raised event.deliver() self.assertTrue(mock.called) self.assertEqual(mock.call_count, 2) self.release(WebHookEvent.objects.all()) with patch("requests.Session.send") as mock: # valid json, but not our format bad_json = '{ "thrift_shops": ["Goodwill", "Value Village"] }' mock.return_value = MockResponse(200, bad_json) WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() self.assertEqual("C", event.status) self.assertEqual(1, event.try_count) self.assertFalse(event.next_attempt) self.assertTrue(mock.called) result = WebHookResult.objects.get() self.assertIn("Event delivered successfully", result.message) self.assertIn("ignoring", result.message) self.assertEqual(200, result.status_code) self.assertEqual(bad_json, result.body) self.release(WebHookEvent.objects.all()) with patch("requests.Session.send") as mock: mock.return_value = MockResponse(200, '{ "phone": "+250788123123", "text": "I am success" }') WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() self.assertEqual("C", event.status) self.assertEqual(1, event.try_count) self.assertFalse(event.next_attempt) result = WebHookResult.objects.get() self.assertEqual(200, result.status_code) self.assertTrue(mock.called) broadcast = Broadcast.objects.get() contact, urn_obj = Contact.get_or_create(self.org, "tel:+250788123123", self.channel, user=self.admin) self.assertTrue(broadcast.text, {"base": "I am success"}) self.assertTrue(contact, broadcast.contacts.all()) self.assertTrue(mock.called) args = mock.call_args_list[0][0] prepared_request = args[0] self.assertEqual(self.org.get_webhook_url(), prepared_request.url) data = parse_qs(prepared_request.body) self.assertEqual(self.joe.get_urn(TEL_SCHEME).path, data["phone"][0]) self.assertEqual(str(self.joe.get_urn(TEL_SCHEME)), data["urn"][0]) self.assertEqual(self.joe.uuid, data["contact"][0]) self.assertEqual(self.joe.name, data["contact_name"][0]) self.assertEqual(sms.pk, int(data["sms"][0])) self.assertEqual(self.channel.pk, int(data["channel"][0])) self.assertEqual(WebHookEvent.TYPE_SMS_RECEIVED, data["event"][0]) self.assertEqual("I'm gonna pop some tags", data["text"][0]) self.assertIn("time", data) self.release(WebHookEvent.objects.all()) with patch("requests.Session.send") as mock: mock.return_value = MockResponse(500, "I am error") next_attempt_earliest = timezone.now() + timedelta(minutes=4) next_attempt_latest = timezone.now() + timedelta(minutes=6) WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() self.assertEqual("E", event.status) self.assertEqual(1, event.try_count) self.assertTrue(event.next_attempt) self.assertTrue(next_attempt_earliest < event.next_attempt and next_attempt_latest > event.next_attempt) result = WebHookResult.objects.get() self.assertIn("Error", result.message) self.assertEqual(500, result.status_code) self.assertEqual("I am error", result.body) # make sure things become failures after three retries event.try_count = 2 event.deliver() event.save() self.assertTrue(mock.called) self.assertEqual("F", event.status) self.assertEqual(3, event.try_count) self.assertFalse(event.next_attempt) result = WebHookResult.objects.get() self.assertIn("Error", result.message) self.assertEqual(500, result.status_code) self.assertEqual("I am error", result.body) self.assertEqual("http://fake.com/webhook.php", result.url) self.assertTrue(result.data.find("pop+some+tags") > 0) # check out our api log response = self.client.get(reverse("api.log")) self.assertRedirect(response, reverse("users.user_login")) response = self.client.get(reverse("api.log_read", args=[event.pk])) self.assertRedirect(response, reverse("users.user_login")) self.release(WebHookEvent.objects.all()) # add a webhook header to the org self.channel.org.webhook = { "url": "http://fake.com/webhook.php", "headers": {"X-My-Header": "foobar", "Authorization": "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}, "method": "POST", } self.channel.org.save() # check that our webhook settings have saved self.assertEqual("http://fake.com/webhook.php", self.channel.org.get_webhook_url()) self.assertDictEqual( {"X-My-Header": "foobar", "Authorization": "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}, self.channel.org.get_webhook_headers(), ) with patch("requests.Session.send") as mock: mock.return_value = MockResponse(200, "Boom") WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() result = WebHookResult.objects.get() # both headers should be in the json-encoded url string self.assertIn("X-My-Header: foobar", result.request) self.assertIn("Authorization: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", result.request)
def test_event_deliveries(self): sms = self.create_msg(contact=self.joe, direction='I', status='H', text="I'm gonna pop some tags") with patch('requests.Session.send') as mock: now = timezone.now() mock.return_value = MockResponse(200, "Hello World") # trigger an event, shouldnn't fire as we don't have a webhook WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now) self.assertFalse(WebHookEvent.objects.all()) self.setupChannel() with patch('requests.Session.send') as mock: # clear out which events we listen for, we still shouldnt be notified though we have a webhook self.channel.org.webhook_events = 0 self.channel.org.save() now = timezone.now() mock.return_value = MockResponse(200, "Hello World") # trigger an event, shouldnn't fire as we don't have a webhook WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now) self.assertFalse(WebHookEvent.objects.all()) self.setupChannel() with patch('requests.Session.send') as mock: # remove all the org users self.org.administrators.clear() self.org.editors.clear() self.org.viewers.clear() mock.return_value = MockResponse(200, "Hello World") # trigger an event WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() self.assertEquals('F', event.status) self.assertEquals(0, event.try_count) self.assertFalse(event.next_attempt) result = WebHookResult.objects.get() self.assertStringContains("No active user", result.message) self.assertEquals(0, result.status_code) self.assertFalse(mock.called) # what if they send weird json back? WebHookEvent.objects.all().delete() WebHookResult.objects.all().delete() # add ad manager back in self.org.administrators.add(self.admin) self.admin.set_org(self.org) with patch('requests.Session.send') as mock: mock.return_value = MockResponse(200, "Hello World") # trigger an event WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() self.assertEquals('C', event.status) self.assertEquals(1, event.try_count) self.assertFalse(event.next_attempt) result = WebHookResult.objects.get() self.assertStringContains("Event delivered successfully", result.message) self.assertStringContains("not JSON", result.message) self.assertEquals(200, result.status_code) self.assertTrue(mock.called) WebHookEvent.objects.all().delete() WebHookResult.objects.all().delete() with patch('requests.Session.send') as mock: # valid json, but not our format bad_json = '{ "thrift_shops": ["Goodwill", "Value Village"] }' mock.return_value = MockResponse(200, bad_json) WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() self.assertEquals('C', event.status) self.assertEquals(1, event.try_count) self.assertFalse(event.next_attempt) self.assertTrue(mock.called) result = WebHookResult.objects.get() self.assertStringContains("Event delivered successfully", result.message) self.assertStringContains("ignoring", result.message) self.assertEquals(200, result.status_code) self.assertEquals(bad_json, result.body) WebHookEvent.objects.all().delete() WebHookResult.objects.all().delete() with patch('requests.Session.send') as mock: mock.return_value = MockResponse( 200, '{ "phone": "+250788123123", "text": "I am success" }') WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() self.assertEquals('C', event.status) self.assertEquals(1, event.try_count) self.assertFalse(event.next_attempt) result = WebHookResult.objects.get() self.assertEquals(200, result.status_code) self.assertTrue(mock.called) broadcast = Broadcast.objects.get() contact = Contact.get_or_create(self.org, self.admin, name=None, urns=["tel:+250788123123"], channel=self.channel) self.assertTrue("I am success", broadcast.text) self.assertTrue(contact, broadcast.contacts.all()) self.assertTrue(mock.called) args = mock.call_args_list[0][0] prepared_request = args[0] self.assertEquals(self.org.get_webhook_url(), prepared_request.url) data = parse_qs(prepared_request.body) self.assertEquals( self.joe.get_urn(TEL_SCHEME).path, data['phone'][0]) self.assertEquals(unicode(self.joe.get_urn(TEL_SCHEME)), data['urn'][0]) self.assertEquals(self.joe.uuid, data['contact'][0]) self.assertEquals(sms.pk, int(data['sms'][0])) self.assertEquals(self.channel.pk, int(data['channel'][0])) self.assertEquals(SMS_RECEIVED, data['event'][0]) self.assertEquals("I'm gonna pop some tags", data['text'][0]) self.assertTrue('time' in data) WebHookEvent.objects.all().delete() WebHookResult.objects.all().delete() with patch('requests.Session.send') as mock: mock.return_value = MockResponse(500, "I am error") next_attempt_earliest = timezone.now() + timedelta(minutes=4) next_attempt_latest = timezone.now() + timedelta(minutes=6) WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() self.assertEquals('E', event.status) self.assertEquals(1, event.try_count) self.assertTrue(event.next_attempt) self.assertTrue(next_attempt_earliest < event.next_attempt and next_attempt_latest > event.next_attempt) result = WebHookResult.objects.get() self.assertStringContains("Error", result.message) self.assertEquals(500, result.status_code) self.assertEquals("I am error", result.body) # make sure things become failures after three retries event.try_count = 2 event.deliver() event.save() self.assertTrue(mock.called) self.assertEquals('F', event.status) self.assertEquals(3, event.try_count) self.assertFalse(event.next_attempt) result = WebHookResult.objects.get() self.assertStringContains("Error", result.message) self.assertEquals(500, result.status_code) self.assertEquals("I am error", result.body) self.assertEquals("http://fake.com/webhook.php", result.url) self.assertTrue(result.data.find("pop+some+tags") > 0) # check out our api log response = self.client.get(reverse('api.log')) self.assertRedirect(response, reverse('users.user_login')) response = self.client.get(reverse('api.log_read', args=[event.pk])) self.assertRedirect(response, reverse('users.user_login')) WebHookEvent.objects.all().delete() WebHookResult.objects.all().delete() # add a webhook header to the org self.channel.org.webhook = u'{"url": "http://fake.com/webhook.php", "headers": {"X-My-Header": "foobar", "Authorization": "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}, "method": "POST"}' self.channel.org.save() # check that our webhook settings have saved self.assertEquals('http://fake.com/webhook.php', self.channel.org.get_webhook_url()) self.assertDictEqual( { 'X-My-Header': 'foobar', 'Authorization': 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' }, self.channel.org.get_webhook_headers()) with patch('requests.Session.send') as mock: mock.return_value = MockResponse(200, "Boom") WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now) event = WebHookEvent.objects.get() result = WebHookResult.objects.get() # both headers should be in the json-encoded url string self.assertStringContains('X-My-Header: foobar', result.request) self.assertStringContains( 'Authorization: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', result.request)
def test_deleting_reimport_contact_groups(self): campaign = Campaign.create(self.org, self.admin, "Planting Reminders", self.farmers) # create a reminder for our first planting event planting_reminder = CampaignEvent.create_flow_event( self.org, self.admin, campaign, relative_to=self.planting_date, offset=3, unit="D", flow=self.reminder_flow ) self.assertEqual(0, EventFire.objects.all().count()) self.farmer1.set_field(self.user, "planting_date", "10-05-2020 12:30:10") self.farmer2.set_field(self.user, "planting_date", "15-05-2020 12:30:10") # now we have event fires accordingly self.assertEqual(2, EventFire.objects.all().count()) # farmer one fire scheduled = EventFire.objects.get(contact=self.farmer1, event=planting_reminder).scheduled self.assertEqual("13-5-2020", "%s-%s-%s" % (scheduled.day, scheduled.month, scheduled.year)) # farmer two fire scheduled = EventFire.objects.get(contact=self.farmer2, event=planting_reminder).scheduled self.assertEqual("18-5-2020", "%s-%s-%s" % (scheduled.day, scheduled.month, scheduled.year)) # delete our farmers group self.farmers.release() # this should have removed all the event fires for that group self.assertEqual(0, EventFire.objects.filter(event=planting_reminder).count()) # and our group is no longer active self.assertFalse(campaign.group.is_active) # now import the group again filename = "farmers.csv" extra_fields = [dict(key="planting_date", header="planting_date", label="Planting Date", type="D")] import_params = dict( org_id=self.org.id, timezone=str(self.org.timezone), extra_fields=extra_fields, original_filename=filename ) task = ImportTask.objects.create( created_by=self.admin, modified_by=self.admin, csv_file="test_imports/" + filename, model_class="Contact", import_params=json.dumps(import_params), import_log="", task_id="A", ) Contact.import_csv(task, log=None) # check that we have new planting dates self.farmer1 = Contact.objects.get(pk=self.farmer1.pk) self.farmer2 = Contact.objects.get(pk=self.farmer2.pk) planting = self.farmer1.get_field_value(self.planting_date) self.assertEqual("10-8-2020", "%s-%s-%s" % (planting.day, planting.month, planting.year)) planting = self.farmer2.get_field_value(self.planting_date) self.assertEqual("15-8-2020", "%s-%s-%s" % (planting.day, planting.month, planting.year)) # now update the campaign self.farmers = ContactGroup.user_groups.get(name="Farmers") self.login(self.admin) post_data = dict(name="Planting Reminders", group=self.farmers.pk) self.client.post(reverse("campaigns.campaign_update", args=[campaign.pk]), post_data) # should have two fresh new fires self.assertEqual(2, EventFire.objects.all().count()) # check their new planting dates scheduled = EventFire.objects.get(contact=self.farmer1, event=planting_reminder).scheduled self.assertEqual("13-8-2020", "%s-%s-%s" % (scheduled.day, scheduled.month, scheduled.year)) # farmer two fire scheduled = EventFire.objects.get(contact=self.farmer2, event=planting_reminder).scheduled self.assertEqual("18-8-2020", "%s-%s-%s" % (scheduled.day, scheduled.month, scheduled.year)) # give our non farmer a planting date self.nonfarmer.set_field(self.user, "planting_date", "20-05-2020 12:30:10") # now update to the non-farmer group self.nonfarmers = self.create_group("Not Farmers", [self.nonfarmer]) post_data = dict(name="Planting Reminders", group=self.nonfarmers.pk) self.client.post(reverse("campaigns.campaign_update", args=[campaign.pk]), post_data) # only one fire for the non-farmer the previous two should be deleted by the group change self.assertEqual(1, EventFire.objects.all().count()) self.assertEqual(1, EventFire.objects.filter(contact=self.nonfarmer).count())
def send_message( self, flow, message, restart_participants=False, contact=None, initiate_flow=False, assert_reply=True, assert_handle=True, ): """ Starts the flow, sends the message, returns the reply """ if not contact: contact = self.contact try: if contact.is_test: Contact.set_simulation(True) incoming = self.create_msg(direction=INCOMING, contact=contact, contact_urn=contact.get_urn(), text=message) # start the flow if initiate_flow: flow.start(groups=[], contacts=[contact], restart_participants=restart_participants, start_msg=incoming) else: flow.start(groups=[], contacts=[contact], restart_participants=restart_participants) (handled, msgs) = Flow.find_and_handle(incoming) Msg.mark_handled(incoming) if assert_handle: self.assertTrue( handled, "'%s' did not handle message as expected" % flow.name) else: self.assertFalse( handled, "'%s' handled message, was supposed to ignore" % flow.name) # our message should have gotten a reply if assert_reply: replies = Msg.objects.filter( response_to=incoming).order_by("pk") self.assertGreaterEqual(len(replies), 1) if len(replies) == 1: self.assertEqual(contact, replies.first().contact) return replies.first().text # if it's more than one, send back a list of replies return [reply.text for reply in replies] else: # assert we got no reply replies = Msg.objects.filter( response_to=incoming).order_by("pk") self.assertFalse(replies) return None finally: Contact.set_simulation(False)
def test_ivr_flow(self): # should be able to create an ivr flow self.assertTrue(self.org.supports_ivr()) self.assertTrue(self.admin.groups.filter(name="Beta")) self.assertContains(self.client.get(reverse('flows.flow_create')), 'Phone Call') # no twilio config yet self.assertFalse(self.org.is_connected_to_twilio()) self.assertIsNone(self.org.get_twilio_client()) # connect it and check our client is configured self.org.connect_twilio("TEST_SID", "TEST_TOKEN") self.org.save() self.assertTrue(self.org.is_connected_to_twilio()) self.assertIsNotNone(self.org.get_twilio_client()) # import an ivr flow self.import_file('call-me-maybe') # make sure our flow is there as expected flow = Flow.objects.filter(name='Call me maybe').first() self.assertEquals( 'callme', flow.triggers.filter(trigger_type='K').first().keyword) user_settings = self.admin.get_settings() user_settings.tel = '+18005551212' user_settings.save() # start our flow as a test contact test_contact = Contact.get_test_contact(self.admin) Contact.set_simulation(True) flow.start([], [test_contact]) call = IVRCall.objects.filter(direction=OUTGOING).first() # should be using the usersettings number in test mode self.assertEquals('Placing test call to +1 800-555-1212', ActionLog.objects.all().first().text) # explicitly hanging up on a test call should remove it call.update_status('in-progress', 0) call.save() IVRCall.hangup_test_call(flow) self.assertIsNone(IVRCall.objects.filter(pk=call.pk).first()) ActionLog.objects.all().delete() IVRCall.objects.all().delete() # now pretend we are a normal caller eric = self.create_contact('Eric Newcomer', number='+13603621737') Contact.set_simulation(False) flow.start([], [eric], restart_participants=True) # we should have an outbound ivr call now call = IVRCall.objects.filter(direction=OUTGOING).first() self.assertEquals(0, call.get_duration()) self.assertIsNotNone(call) self.assertEquals('CallSid', call.external_id) # after a call is picked up, twilio will call back to our server post_data = dict(CallSid='CallSid', CallStatus='in-progress', CallDuration=20) response = self.client.post( reverse('ivr.ivrcall_handle', args=[call.pk]), post_data) self.assertContains( response, '<Say>Would you like me to call you? Press one for yes, two for no, or three for maybe.</Say>' ) self.assertEquals(1, Msg.all_messages.filter(msg_type=IVR).count()) self.assertEquals(1, self.org.get_credits_used()) # make sure a message from the person on the call goes to the # inbox since our flow doesn't handle text messages msg = self.create_msg(direction='I', contact=eric, text="message during phone call") self.assertFalse(Flow.find_and_handle(msg)) # updated our status and duration accordingly call = IVRCall.objects.get(pk=call.pk) self.assertEquals(20, call.duration) self.assertEquals(IN_PROGRESS, call.status) # press the number 4 (unexpected) response = self.client.post( reverse('ivr.ivrcall_handle', args=[call.pk]), dict(Digits=4)) self.assertContains(response, '<Say>Press one, two, or three. Thanks.</Say>') self.assertEquals(4, self.org.get_credits_used()) # two more messages, one inbound and it's response self.assertEquals(3, Msg.all_messages.filter(msg_type=IVR).count()) # now let's have them press the number 3 (for maybe) response = self.client.post( reverse('ivr.ivrcall_handle', args=[call.pk]), dict(Digits=3)) self.assertContains(response, '<Say>This might be crazy.</Say>') messages = Msg.all_messages.filter(msg_type=IVR).order_by('pk') self.assertEquals(5, messages.count()) self.assertEquals(6, self.org.get_credits_used()) for msg in messages: self.assertEquals(1, msg.steps.all().count(), msg="Message '%s' not attached to step" % msg.text) # twilio would then disconnect the user and notify us of a completed call self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), dict(CallStatus='completed')) call = IVRCall.objects.get(pk=call.pk) self.assertEquals(COMPLETED, call.status) self.assertFalse(FlowRun.objects.filter(call=call).first().is_active) # simulation gets flipped off by middleware, and this unhandled message doesn't flip it back on self.assertFalse(Contact.get_simulation()) # also shouldn't have any ActionLogs for non-test users self.assertEquals(0, ActionLog.objects.all().count()) self.assertEquals(1, flow.get_completed_runs()) # should still have no active runs self.assertEquals(0, FlowRun.objects.filter(is_active=True).count()) # and we've exited the flow step = FlowStep.objects.all().order_by('-pk').first() self.assertTrue(step.left_on) # test other our call status mappings with twilio def test_status_update(call_to_update, twilio_status, temba_status): call_to_update.update_status(twilio_status, 0) call_to_update.save() self.assertEquals(temba_status, IVRCall.objects.get(pk=call_to_update.pk).status) test_status_update(call, 'queued', QUEUED) test_status_update(call, 'ringing', RINGING) test_status_update(call, 'canceled', CANCELED) test_status_update(call, 'busy', BUSY) test_status_update(call, 'failed', FAILED) test_status_update(call, 'no-answer', NO_ANSWER) FlowStep.objects.all().delete() IVRCall.objects.all().delete() # try sending callme trigger from temba.msgs.models import INCOMING msg = self.create_msg(direction=INCOMING, contact=eric, text="callme") # make sure if we are started with a message we still create a normal voice run flow.start([], [eric], restart_participants=True, start_msg=msg) # we should have an outbound ivr call now, and no steps yet call = IVRCall.objects.filter(direction=OUTGOING).first() self.assertIsNotNone(call) self.assertEquals(0, FlowStep.objects.all().count()) # after a call is picked up, twilio will call back to our server post_data = dict(CallSid='CallSid', CallStatus='in-progress', CallDuration=20) self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), post_data) # should have two flow steps (the outgoing messages, and the step to handle the response) steps = FlowStep.objects.all().order_by('pk') # the first step has exactly one message which is an outgoing IVR message self.assertEquals(1, steps.first().messages.all().count()) self.assertEquals( 1, steps.first().messages.filter(direction=OUTGOING, msg_type=IVR).count()) # the next step shouldn't have any messages yet since they haven't pressed anything self.assertEquals(0, steps[1].messages.all().count()) # try updating our status to completed for a test contact Contact.set_simulation(True) flow.start([], [test_contact]) call = IVRCall.objects.filter( direction=OUTGOING).order_by('-pk').first() call.update_status('completed', 30) call.save() call.refresh_from_db() self.assertEqual(ActionLog.objects.all().order_by('-pk').first().text, 'Call ended.') self.assertEqual(call.duration, 30) # now look at implied duration call.update_status('in-progress', None) call.save() call.refresh_from_db() self.assertIsNotNone(call.get_duration())
def omnibox_mixed_search(org, query, types): """ Performs a mixed group, contact and URN search, returning the first N matches of each type. """ query_terms = query.split(" ") if query else None search_types = types or (SEARCH_ALL_GROUPS, SEARCH_CONTACTS, SEARCH_URNS) per_type_limit = 25 results = [] if SEARCH_ALL_GROUPS in search_types or SEARCH_STATIC_GROUPS in search_types: groups = ContactGroup.get_user_groups(org, ready_only=True) # exclude dynamic groups if not searching all groups if SEARCH_ALL_GROUPS not in search_types: groups = groups.filter(query=None) if query: groups = term_search(groups, ("name__icontains", ), query_terms) results += list(groups.order_by(Upper("name"))[:per_type_limit]) if SEARCH_CONTACTS in search_types: try: # query elastic search for contact ids, then fetch contacts from db search_results = search_contacts(org, query, group=org.active_contacts_group, sort="name") contacts = IDSliceQuerySet( Contact, search_results.contact_ids, offset=0, total=len(search_results.contact_ids), only=("id", "uuid", "name", "org_id"), ).prefetch_related("org") results += list(contacts[:per_type_limit]) Contact.bulk_urn_cache_initialize(contacts=results) except SearchException: pass if SEARCH_URNS in search_types: if not org.is_anon and query and len(query) >= 3: try: # build an OR'ed query of all sendable schemes sendable_schemes = org.get_schemes(Channel.ROLE_SEND) scheme_query = " OR ".join(f"{s} ~ {json.dumps(query)}" for s in sendable_schemes) search_results = search_contacts( org, scheme_query, group=org.active_contacts_group, sort="name") urns = ContactURN.objects.filter( contact_id__in=search_results.contact_ids, scheme__in=sendable_schemes) results += list( urns.prefetch_related("contact").order_by( Upper("path"))[:per_type_limit]) except SearchException: pass return results
def setUp(self): self.maxDiff = 4096 self.mock_server = mock_server # if we are super verbose, turn on debug for sql queries if self.get_verbosity() > 2: settings.DEBUG = True # make sure we start off without any service users Group.objects.get(name="Service Users").user_set.clear() self.clear_cache() self.create_anonymous_user() self.superuser = User.objects.create_superuser(username="******", email="*****@*****.**", password="******") # create different user types self.non_org_user = self.create_user("NonOrg") self.user = self.create_user("User") self.editor = self.create_user("Editor") self.admin = self.create_user("Administrator") self.surveyor = self.create_user("Surveyor") self.customer_support = self.create_user("support", ("Customer Support", )) # setup admin boundaries for Rwanda self.country = AdminBoundary.create(osm_id="171496", name="Rwanda", level=0) self.state1 = AdminBoundary.create(osm_id="1708283", name="Kigali City", level=1, parent=self.country) self.state2 = AdminBoundary.create(osm_id="171591", name="Eastern Province", level=1, parent=self.country) self.district1 = AdminBoundary.create(osm_id="R1711131", name="Gatsibo", level=2, parent=self.state2) self.district2 = AdminBoundary.create(osm_id="1711163", name="Kayônza", level=2, parent=self.state2) self.district3 = AdminBoundary.create(osm_id="3963734", name="Nyarugenge", level=2, parent=self.state1) self.district4 = AdminBoundary.create(osm_id="1711142", name="Rwamagana", level=2, parent=self.state2) self.ward1 = AdminBoundary.create(osm_id="171113181", name="Kageyo", level=3, parent=self.district1) self.ward2 = AdminBoundary.create(osm_id="171116381", name="Kabare", level=3, parent=self.district2) self.ward3 = AdminBoundary.create(osm_id="VMN.49.1_1", name="Bukure", level=3, parent=self.district4) self.country.update_path() self.org = Org.objects.create( name="Temba", timezone=pytz.timezone("Africa/Kigali"), country=self.country, brand=settings.DEFAULT_BRAND, created_by=self.user, modified_by=self.user, ) self.org.initialize(topup_size=1000) # add users to the org self.user.set_org(self.org) self.org.viewers.add(self.user) self.editor.set_org(self.org) self.org.editors.add(self.editor) self.admin.set_org(self.org) self.org.administrators.add(self.admin) self.surveyor.set_org(self.org) self.org.surveyors.add(self.surveyor) self.superuser.set_org(self.org) # welcome topup with 1000 credits self.welcome_topup = self.org.topups.all()[0] # a single Android channel self.channel = Channel.create( self.org, self.user, "RW", "A", name="Test Channel", address="+250785551212", device="Nexus 5X", secret="12345", config={Channel.CONFIG_FCM_ID: "123"}, ) # don't cache anon user between tests from temba import utils utils._anon_user = None clear_flow_users() # reset our simulation to False Contact.set_simulation(False)