Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upmerge from Nyaruka #465

Merged
merged 35 commits into from
Feb 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
114ac8b
Improve feedback errors for bad text from the excel file
norkans7 Feb 16, 2017
c8281de
change text
norkans7 Feb 16, 2017
a5508a1
show errors if we have them
norkans7 Feb 16, 2017
a11c0ff
use six which handles the invalid unicode characters from excel the r…
norkans7 Feb 20, 2017
ea13124
Merge pull request #1113 from rapidpro/master
nicpottier Feb 20, 2017
905adcc
Fix migrations
nicpottier Feb 20, 2017
11dcd32
Merge pull request #1104 from nyaruka/import-clean-cell-value
nicpottier Feb 20, 2017
f5aaa23
Add logging for nexmo hangups
ericnewcomer Feb 20, 2017
59a3ec8
fix build
ericnewcomer Feb 21, 2017
2015c22
Make sure calls are ended on hangup, tweak logging when saving media
ericnewcomer Feb 21, 2017
432e142
pragma nexmo code we couldnt super
ericnewcomer Feb 21, 2017
a18cbdf
Use json when logging for media saves
ericnewcomer Feb 21, 2017
549add5
remove erroneous ampersand for nexmo callbacks
ericnewcomer Feb 21, 2017
9a78621
fix test
ericnewcomer Feb 21, 2017
ff2ed5b
Tweak to how pg_restore connects in make_test_db command
rowanseymour Feb 21, 2017
001610c
integer parameters should be sent as integers
norkans7 Feb 21, 2017
e83647c
Add indexes for name for boundary and boundary alias
nicpottier Feb 21, 2017
def83fd
Create indexes manually so we can use UPPER
nicpottier Feb 21, 2017
af8ba59
missing migration file
nicpottier Feb 21, 2017
8338de5
Merge pull request #1114 from nyaruka/boundary-name-index
nicpottier Feb 21, 2017
414777e
Make migrations to populate new export task fields non-atomic
rowanseymour Feb 21, 2017
4f56a2e
add media/attachements to gitignore
norkans7 Feb 21, 2017
6293ae4
Merge pull request #1115 from nyaruka/migration_fix
nicpottier Feb 21, 2017
6a4e341
Merge branch 'nexmo_logging' of github.com:nyaruka/rapidpro into more…
norkans7 Feb 21, 2017
10bb57a
remove erroneous ampersand
norkans7 Feb 21, 2017
d9fd276
Merge pull request #1117 from nyaruka/more-control-on-nexmo-hangup
ericnewcomer Feb 21, 2017
4a73414
Merge branch 'master' into nexmo_logging
ericnewcomer Feb 21, 2017
1d4e6e2
Merge pull request #1116 from nyaruka/nexmo_logging
nicpottier Feb 21, 2017
16a3ec6
Update CHANGELOG.md for v3.0.61
nicpottier Feb 21, 2017
ee1e8d7
merge versions
nicpottier Feb 21, 2017
b0b199b
Fix channel preference for IVR flows
nicpottier Feb 21, 2017
b927b93
Slightly cleaner test
nicpottier Feb 21, 2017
6858a90
Merge branch 'master' into set-preferred-ivr-channel
nicpottier Feb 21, 2017
5348281
Merge pull request #1119 from nyaruka/set-preferred-ivr-channel
nicpottier Feb 21, 2017
25bb8ff
Update CHANGELOG.md for v3.0.62
nicpottier Feb 21, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ media/orgs
media/recordings
media/test_orgs
media/tmp
media/attachments


# External projects
Expand Down
15 changes: 8 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
v3.0.57
v3.0.62
----------
* Fix inbound calls on Nexmo to use conversation_uuid
* Style tweaks for zapier widget
* Fix preferred channels for non-msg channels

v3.0.54
v3.0.61
----------
* Make migrations to populate new export task fields non-atomic
* Add indexes for admin boundaries and aliases
* Nexmo: make sure calls are ended on hangup, log hangups and media
* Fix inbound calls on Nexmo to use conversation_uuid
* Style tweaks for zapier widget
* Use shorter timeout for IVR
* Issue hangups on expiration during IVR runs
* Catch all exceptions and log them when initiating call

v3.0.49
----------
* Fix update status for Nexmo calls

v3.0.48
Expand Down
Binary file not shown.
4 changes: 2 additions & 2 deletions temba/channels/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,9 +831,9 @@ def generate_ivr_response(self):
def get_ivr_client(self):
if self.channel_type == Channel.TYPE_TWILIO:
return self.org.get_twilio_client()
if self.channel_type == Channel.TYPE_TWIML:
elif self.channel_type == Channel.TYPE_TWIML:
return self.get_twiml_client()
if self.channel_type == Channel.TYPE_VERBOICE: # pragma: no cover
elif self.channel_type == Channel.TYPE_VERBOICE: # pragma: no cover
return self.org.get_verboice_client()
elif self.channel_type == Channel.TYPE_NEXMO:
return self.org.get_nexmo_client()
Expand Down
1 change: 1 addition & 0 deletions temba/contacts/migrations/0052_baseexporttask_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def populate_uuid(apps, schema_editor):


class Migration(migrations.Migration):
atomic = False

dependencies = [
('contacts', '0051_baseexporttask_1'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class Migration(migrations.Migration):

dependencies = [
('contacts', '0050_contactgroupcount_is_squashed'),
('contacts', '0052_baseexporttask_2'),
]

operations = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class Migration(migrations.Migration):

dependencies = [
('contacts', '0051_auto_20170208_1450'),
('contacts', '0053_auto_20170208_1450'),
]

operations = [
Expand Down
2 changes: 1 addition & 1 deletion temba/contacts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,7 @@ def create_instance(cls, field_dict):
if not value:
continue

value = str(value)
value = six.text_type(value)

urn_scheme = ContactURN.IMPORT_HEADER_TO_SCHEME[urn_header]

Expand Down
15 changes: 15 additions & 0 deletions temba/contacts/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2521,6 +2521,21 @@ def test_contact_import(self):
Contact.objects.all().delete()
ContactGroup.user_groups.all().delete()

self.assertContactImport('%s/test_imports/sample_contacts_bad_unicode.xls' % settings.MEDIA_ROOT,
dict(records=2, errors=0, creates=2, updates=0, error_messages=[]))

self.assertEquals(1, Contact.objects.filter(name='John Doe').count())
self.assertEquals(1, Contact.objects.filter(name='Mary Smith').count())

contact = Contact.objects.filter(name='John Doe').first()
contact2 = Contact.objects.filter(name='Mary Smith').first()

self.assertEqual(list(contact.get_urns().values_list('path', flat=True)), ['+250788123123'])
self.assertEqual(list(contact2.get_urns().values_list('path', flat=True)), ['+250788345345'])

Contact.objects.all().delete()
ContactGroup.user_groups.all().delete()

# import a spreadsheet with phone, name and twitter columns
self.assertContactImport('%s/test_imports/sample_contacts_twitter_and_phone.xls' % settings.MEDIA_ROOT,
dict(records=3, errors=0, error_messages=[], creates=3, updates=0))
Expand Down
1 change: 1 addition & 0 deletions temba/flows/migrations/0090_baseexporttask_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def populate_uuid(apps, schema_editor):


class Migration(migrations.Migration):
atomic = False

dependencies = [
('flows', '0089_baseexporttask_1'),
Expand Down
38 changes: 34 additions & 4 deletions temba/ivr/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from twilio import TwilioRestException
from twilio.rest import TwilioRestClient
from twilio.util import RequestValidator
from nexmo import AuthenticationError, ClientError, ServerError


class IVRException(Exception):
Expand Down Expand Up @@ -105,8 +106,37 @@ def download_media(self, media_url):

return None

def hangup(self, call_id):
self.update_call(call_id, action='hangup')
def hangup(self, call):
return self.update_call(call.external_id, action='hangup', call_id=call.external_id)

def parse(self, host, response):
try:
request = response.request
request_body = json.loads(response.request.body)
call_id = request_body.get('call_id', None)
if call_id:
call = IVRCall.objects.filter(external_id=call_id).first()
if call:
ChannelLog.log_ivr_interaction(call, "Nexmo call update", request.body, response.content, request.url,
request.method, status_code=response.status_code)
except:
pass

# Nexmo client doesn't extend object, so can't call super
if response.status_code == 401:
raise AuthenticationError
elif response.status_code == 204: # pragma: no cover
return None
elif 200 <= response.status_code < 300:
return response.json()
elif 400 <= response.status_code < 500: # pragma: no cover
message = "{code} response from {host}".format(code=response.status_code, host=host)

raise ClientError(message)
elif 500 <= response.status_code < 600: # pragma: no cover
message = "{code} response from {host}".format(code=response.status_code, host=host)

raise ServerError(message)


class TwilioClient(TwilioRestClient):
Expand Down Expand Up @@ -181,8 +211,8 @@ def download_media(self, media_url):

return None # pragma: needs cover

def hangup(self, sid):
self.calls.hangup(sid)
def hangup(self, call):
return self.calls.hangup(call.external_id)


class VerboiceClient: # pragma: needs cover
Expand Down
3 changes: 2 additions & 1 deletion temba/ivr/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@ def close(self):

# mark us as interrupted
self.status = ChannelSession.INTERRUPTED
self.ended_on = timezone.now()
self.save()

client = self.channel.get_ivr_client()
if client and self.external_id:
client.hangup(self.external_id)
client.hangup(self)

def do_start_call(self, qs=None):
client = self.channel.get_ivr_client()
Expand Down
75 changes: 71 additions & 4 deletions temba/ivr/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,61 @@ def tearDown(self):
super(IVRTests, self).tearDown()
settings.SEND_CALLS = False

@patch('nexmo.Client.create_application')
@patch('nexmo.Client.create_call')
@patch('nexmo.Client.update_call')
@patch('temba.orgs.models.TwilioRestClient', MockTwilioClient)
@patch('temba.ivr.clients.TwilioClient', MockTwilioClient)
@patch('twilio.util.RequestValidator', MockRequestValidator)
def test_preferred_channel(self, mock_update_call, mock_create_call, mock_create_application):
mock_create_application.return_value = dict(id='app-id', keys=dict(private_key='private-key'))
mock_create_call.return_value = dict(uuid='12345')
mock_update_call.return_value = dict(uuid='12345')

flow = self.get_flow('call_me_maybe')

# start our flow
contact = self.create_contact('Chuck D', number='+13603621737')
flow.start([], [contact])

call = IVRCall.objects.get()
self.assertEquals(IVRCall.PENDING, call.status)

# call should be on a Twilio channel since that's all we have
self.assertEquals(Channel.TYPE_TWILIO, call.channel.channel_type)

# connect Nexmo instead
self.org.connect_nexmo('123', '456', self.admin)
self.org.save()

# manually create a Nexmo channel
nexmo = Channel.create(self.org, self.user, 'RW', Channel.TYPE_NEXMO, role=Channel.ROLE_CALL + Channel.ROLE_ANSWER + Channel.ROLE_SEND,
name="Nexmo Channel", address="+250785551215")

# set the preferred channel on this contact to Twilio
contact.set_preferred_channel(self.channel)

# restart the flow
flow.start([], [contact], restart_participants=True)

call = IVRCall.objects.all().last()
self.assertEquals(IVRCall.PENDING, call.status)
self.assertEquals(Channel.TYPE_TWILIO, call.channel.channel_type)

# switch back to Nexmo being the preferred channel
contact.set_preferred_channel(nexmo)

# clear open calls and runs
IVRCall.objects.all().delete()
FlowRun.objects.all().delete()

# restart the flow
flow.start([], [contact], restart_participants=True)

call = IVRCall.objects.all().last()
self.assertEquals(IVRCall.PENDING, call.status)
self.assertEquals(Channel.TYPE_NEXMO, call.channel.channel_type)

@patch('temba.orgs.models.TwilioRestClient', MockTwilioClient)
@patch('temba.ivr.clients.TwilioClient', MockTwilioClient)
@patch('twilio.util.RequestValidator', MockRequestValidator)
Expand Down Expand Up @@ -616,13 +671,21 @@ def test_ivr_digital_gather_with_nexmo(self, mock_create_call, mock_create_appli

self.assertContains(response, '"eventUrl": ["https://%s%s"]}]' % (settings.TEMBA_HOST, callback_url))

@patch('nexmo.Client.update_call')
@patch('jwt.encode')
@patch('requests.put')
@patch('nexmo.Client.create_application')
@patch('nexmo.Client.create_call')
def test_hangup(self, mock_create_call, mock_create_application, mock_update_call):
def test_expiration_hangup(self, mock_create_call, mock_create_application, mock_put, mock_jwt):
mock_create_application.return_value = dict(id='app-id', keys=dict(private_key='private-key'))
mock_create_call.return_value = dict(uuid='12345')
mock_update_call.return_value = dict(uuid='12345')
mock_jwt.return_value = "Encoded data"

import mock
request = mock.MagicMock()
request.body = json.dumps(dict(call_id='12345'))
request.url = "http://api.nexmo.com/../"
request.method = "PUT"
mock_put.return_value = mock.MagicMock(call_id='12345', request=request, status_code=200, content='response')

self.org.connect_nexmo('123', '456', self.admin)
self.org.save()
Expand All @@ -644,10 +707,14 @@ def test_hangup(self, mock_create_call, mock_create_application, mock_update_cal
run = FlowRun.objects.get()
run.expire()

mock_update_call.assert_called()
mock_put.assert_called()
call = IVRCall.objects.filter(direction=IVRCall.OUTGOING).first()
self.assertEqual(ChannelSession.INTERRUPTED, call.status)

# call initiation and timeout should both be logged
self.assertEqual(2, ChannelLog.objects.filter(session=call).count())
self.assertIsNotNone(call.ended_on)

@patch('nexmo.Client.create_application')
@patch('nexmo.Client.create_call')
def test_ivr_subflow_with_nexmo(self, mock_create_call, mock_create_application):
Expand Down
7 changes: 4 additions & 3 deletions temba/ivr/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def post(self, request, *args, **kwargs):
if not request.user.is_anonymous():
user_org = request.user.get_org()
if user_org and user_org.pk == call.org.pk:
client.hangup(call.external_id)
client.hangup(call)
return HttpResponse(json.dumps(dict(status='Canceled')), content_type="application/json")
else: # pragma: no cover
return HttpResponse("Not found", status=404)
Expand Down Expand Up @@ -120,9 +120,10 @@ def post(self, request, *args, **kwargs):
cache.delete('last_call:media_url:%d' % call.pk)
else:
response_msg = 'Saved media for call %s' % call.external_id
ChannelLog.log_ivr_interaction(call, response_msg, request_body, six.text_type(response_msg),
response = dict(message=response_msg)
ChannelLog.log_ivr_interaction(call, response_msg, request_body, json.dumps(response),
request_path, request_method)
return HttpResponse(six.text_type(response_msg))
return JsonResponse(response)

if call.status not in IVRCall.DONE or hangup:
if call.is_ivr():
Expand Down
20 changes: 20 additions & 0 deletions temba/locations/migrations/0008_auto_20170221_1424.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-21 14:24
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):
atomic = False

dependencies = [
('locations', '0007_reset_2'),
]

operations = [
migrations.RunSQL(
'CREATE INDEX CONCURRENTLY locations_adminboundary_name on locations_adminboundary(upper("name"))'),
migrations.RunSQL(
'CREATE INDEX CONCURRENTLY locations_boundaryalias_name on locations_boundaryalias(upper("name"))')
]
1 change: 1 addition & 0 deletions temba/msgs/migrations/0082_baseexporttask_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def populate_uuid(apps, schema_editor):


class Migration(migrations.Migration):
atomic = False

dependencies = [
('msgs', '0081_baseexporttask_1'),
Expand Down
2 changes: 1 addition & 1 deletion temba/orgs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ def get_channel_for_role(self, role, scheme=None, contact_urn=None, country_code
scheme = contact_urn.scheme

# if URN has a previously used channel that is still active, use that
if contact_urn.channel and contact_urn.channel.is_active and role == Channel.ROLE_SEND:
if contact_urn.channel and contact_urn.channel.is_active:
previous_sender = self.get_channel_delegate(contact_urn.channel, role)
if previous_sender:
return previous_sender
Expand Down
2 changes: 1 addition & 1 deletion temba/sql/current_functions.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- Generated by collect_sql on 2017-02-03 08:33 UTC
-- Generated by collect_sql on 2017-02-21 14:28 UTC

----------------------------------------------------------------------
-- Trigger procedure to prevent illegal state changes to contacts
Expand Down
6 changes: 5 additions & 1 deletion temba/sql/current_indexes.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
-- Generated by collect_sql on 2017-02-03 08:33 UTC
-- Generated by collect_sql on 2017-02-21 14:28 UTC

CREATE INDEX CONCURRENTLY locations_adminboundary_name on locations_adminboundary(upper("name"))

CREATE INDEX CONCURRENTLY locations_boundaryalias_name on locations_boundaryalias(upper("name"))

-- index for fast fetching of unsquashed rows
CREATE INDEX channels_channelcount_unsquashed
Expand Down
2 changes: 1 addition & 1 deletion temba/sql/current_triggers.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- Generated by collect_sql on 2017-02-03 08:33 UTC
-- Generated by collect_sql on 2017-02-21 14:28 UTC

CREATE TRIGGER contact_check_update_trg
BEFORE UPDATE OF is_test, is_blocked, is_stopped ON contacts_contact
Expand Down
3 changes: 2 additions & 1 deletion temba/utils/management/commands/make_test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ def load_locations(self, path):
# load dump into current db with pg_restore
db_config = settings.DATABASES['default']
try:
check_call('pg_restore -U%s -w -d %s %s' % (db_config['USER'], db_config['NAME'], path), shell=True)
check_call('export PGPASSWORD=%s && pg_restore -U%s -w -d %s %s' %
(db_config['PASSWORD'], db_config['USER'], db_config['NAME'], path), shell=True)
except CalledProcessError: # pragma: no cover
raise CommandError("Error occurred whilst calling pg_restore to load locations dump")

Expand Down