Skip to content

Commit

Permalink
Merge pull request #25 from rapidpro/message_sending
Browse files Browse the repository at this point in the history
Outgoing message improvements
  • Loading branch information
rowanseymour committed May 13, 2016
2 parents 1ef04e6 + c6c6a53 commit 3a10af5
Show file tree
Hide file tree
Showing 15 changed files with 499 additions and 165 deletions.
9 changes: 3 additions & 6 deletions casepro/backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,12 @@ def create_label(self, org, name):
"""

@abstractmethod
def create_outgoing(self, org, text, contacts, urns):
def push_outgoing(self, org, outgoing):
"""
Creates an outgoing broadcast message
Pushes (i.e. sends) an outgoing broadcast message
:param org: the org
:param text: the message text
:param contacts: the contact recipients
:param urns: the raw URN recipients
:return: tuple of the broadcast id and it's timestamp
:param outgoing: the outgoing message
"""

@abstractmethod
Expand Down
9 changes: 6 additions & 3 deletions casepro/backend/rapidpro.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,13 @@ def create_label(self, org, name):

return remote.uuid

def create_outgoing(self, org, text, contacts, urns):
def push_outgoing(self, org, outgoing):
client = self._get_client(org, 1)
broadcast = client.create_broadcast(text=text, contacts=[c.uuid for c in contacts], urns=urns)
return broadcast.id, broadcast.created_on
contact_uuids = [c.uuid for c in outgoing.contacts.all()]
broadcast = client.create_broadcast(text=outgoing.text, contacts=contact_uuids, urns=list(outgoing.urns))

outgoing.backend_id = broadcast.id
outgoing.save(update_fields=('backend_id',))

def add_to_group(self, org, contact, group):
client = self._get_client(org, 1)
Expand Down
43 changes: 29 additions & 14 deletions casepro/backend/tests/test_rapidpro.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from unittest import skip

from casepro.contacts.models import Contact, Field, Group
from casepro.msgs.models import Label, Message
from casepro.msgs.models import Label, Message, Outgoing
from casepro.test import BaseCasesTest

from ..rapidpro import RapidProBackend, ContactSyncer, MessageSyncer
Expand Down Expand Up @@ -546,19 +546,34 @@ def test_create_label(self, mock_get_labels, mock_create_label):
mock_create_label.assert_called_once_with(name="Ebola")

@patch('dash.orgs.models.TembaClient1.create_broadcast')
def test_create_outgoing(self, mock_create_broadcast):
d1 = datetime(2014, 1, 2, 6, 0, tzinfo=pytz.UTC)
mock_create_broadcast.return_value = TembaBroadcast.create(id=201,
text="That's great",
urns=["tel:+250783935665"],
contacts=["C-001"],
created_on=d1)

res = self.backend.create_outgoing(self.unicef, "That's great", [self.ann], ["tel:+250783935665"])

self.assertEqual(res, (201, d1))
mock_create_broadcast.assert_called_once_with(text="That's great", contacts=["C-001"],
urns=["tel:+250783935665"])
def test_push_outgoing(self, mock_create_broadcast):
msg1 = self.create_message(self.unicef, 101, self.ann, "Hello")
msg2 = self.create_message(self.unicef, 102, self.bob, "Bonjour")

mock_create_broadcast.return_value = TembaBroadcast.create(id=201, text="That's great",
urns=[], contacts=["C-001", "C-002"])

# test with bulk reply
out1 = Outgoing.create_bulk_reply(self.unicef, self.user1, "That's great", [msg1, msg2])
self.backend.push_outgoing(self.unicef, out1)

mock_create_broadcast.assert_called_once_with(text="That's great", contacts=["C-001", "C-002"], urns=[])
mock_create_broadcast.reset_mock()

out1.refresh_from_db()
self.assertEqual(out1.backend_id, 201)

mock_create_broadcast.return_value = TembaBroadcast.create(id=202, text="That's great",
urns=["tel:+250783935665"], contacts=[])

# test with forward
out2 = Outgoing.create_forward(self.unicef, self.user1, "That's great", ["tel:+250783935665"], msg1)
self.backend.push_outgoing(self.unicef, out2)

mock_create_broadcast.assert_called_once_with(text="That's great", contacts=[], urns=["tel:+250783935665"])

out2.refresh_from_db()
self.assertEqual(out2.backend_id, 202)

@patch('dash.orgs.models.TembaClient1.add_contacts')
def test_add_to_group(self, mock_add_contacts):
Expand Down
10 changes: 7 additions & 3 deletions casepro/cases/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from casepro.backend import get_backend
from casepro.contacts.models import Contact
from casepro.msgs.models import Label, Message
from casepro.msgs.models import Label, Message, Outgoing
from casepro.utils.export import BaseExport


Expand Down Expand Up @@ -102,7 +102,7 @@ def __call__(self, func):
def wrapped(case, user, *args, **kwargs):
access = case.access_level(user)
if (access == AccessLevel.update) or (not self.require_update and access == AccessLevel.read):
func(case, user, *args, **kwargs)
return func(case, user, *args, **kwargs)
else:
raise PermissionDenied()
return wrapped
Expand Down Expand Up @@ -235,7 +235,7 @@ def get_timeline(self, after, before, merge_from_backend):
backend = get_backend()
backend_messages = backend.fetch_contact_messages(self.org, self.contact, after, before)

local_by_backend_id = {o.backend_id: o for o in local_outgoing}
local_by_backend_id = {o.backend_id: o for o in local_outgoing if o.backend_id}

for msg in backend_messages:
# annotate with sender from local message if there is one
Expand Down Expand Up @@ -309,6 +309,10 @@ def label(self, user, label):

CaseAction.create(self, user, CaseAction.LABEL, label=label)

@case_action()
def reply(self, user, text):
return Outgoing.create_case_reply(self.org, user, text, self)

@case_action()
def unlabel(self, user, label):
self.labels.remove(label)
Expand Down
221 changes: 213 additions & 8 deletions casepro/cases/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.test.utils import override_settings
from django.utils import timezone
from mock import patch
from temba_client.utils import format_iso8601
from xlrd import open_workbook

from casepro.contacts.models import Contact
Expand Down Expand Up @@ -305,11 +306,18 @@ def setUp(self):
self.ann = self.create_contact(self.unicef, 'C-001', "Ann",
fields={'age': "34"}, groups=[self.females, self.reporters])

msg = self.create_message(self.unicef, 101, self.ann, "Hello", [self.aids])
self.case = self.create_case(self.unicef, self.ann, self.moh, msg, [self.aids], summary="Summary")

@patch('casepro.test.TestBackend.archive_contact_messages')
@patch('casepro.test.TestBackend.stop_runs')
@patch('casepro.test.TestBackend.add_to_group')
@patch('casepro.test.TestBackend.remove_from_group')
def test_open(self, mock_remove_contacts, mock_add_contacts, mock_stop_runs, mock_archive_contact_messages):
CaseAction.objects.all().delete()
Case.objects.all().delete()
Message.objects.all().delete()

msg1 = self.create_message(self.unicef, 101, self.ann, "Hello", [self.aids])

url = reverse('cases.case_open')
Expand Down Expand Up @@ -346,20 +354,216 @@ def test_open(self, mock_remove_contacts, mock_add_contacts, mock_stop_runs, moc
self.assertEqual(set(case2.labels.all()), {self.aids})

def test_read(self):
msg = self.create_message(self.unicef, 101, self.ann, "Hello", [self.aids])
case = self.create_case(self.unicef, self.ann, self.moh, msg, [self.aids], summary="Summary")

url = reverse('cases.case_read', args=[case.pk])
url = reverse('cases.case_read', args=[self.case.pk])

# log in as non-administrator
self.login(self.user1)

response = self.url_get('unicef', url)
self.assertEqual(response.status_code, 200)

def test_note(self):
url = reverse('cases.case_note', args=[self.case.pk])

# log in as manager user in assigned partner
self.login(self.user1)

response = self.url_post('unicef', url, {'note': "This is a note"})
self.assertEqual(response.status_code, 204)

action = CaseAction.objects.get()
self.assertEqual(action.case, self.case)
self.assertEqual(action.action, CaseAction.ADD_NOTE)
self.assertEqual(action.note, "This is a note")
self.assertEqual(action.created_by, self.user1)

# users from other partners with label access are allowed to add notes
self.login(self.user3)

response = self.url_post('unicef', url, {'note': "This is another note"})
self.assertEqual(response.status_code, 204)

# but not if they lose label-based access
self.case.update_labels(self.admin, [self.pregnancy])

response = self.url_post('unicef', url, {'note': "Yet another"})
self.assertEqual(response.status_code, 403)

# and users from other orgs certainly aren't allowed to
self.login(self.user4)

response = self.url_post('unicef', url, {'note': "Hey guys"})
self.assertEqual(response.status_code, 302)

def test_reassign(self):
url = reverse('cases.case_reassign', args=[self.case.pk])

# log in as manager user in currently assigned partner
self.login(self.user1)

response = self.url_post('unicef', url, {'assignee': self.who.pk})
self.assertEqual(response.status_code, 204)

action = CaseAction.objects.get()
self.assertEqual(action.case, self.case)
self.assertEqual(action.action, CaseAction.REASSIGN)
self.assertEqual(action.created_by, self.user1)

self.case.refresh_from_db()
self.assertEqual(self.case.assignee, self.who)

# only user from assigned partner can re-assign
response = self.url_post('unicef', url, {'assignee': self.moh.pk})
self.assertEqual(response.status_code, 403)

def test_close(self):
url = reverse('cases.case_close', args=[self.case.pk])

# log in as manager user in currently assigned partner
self.login(self.user1)

response = self.url_post('unicef', url, {'note': "It's over"})
self.assertEqual(response.status_code, 204)

action = CaseAction.objects.get()
self.assertEqual(action.case, self.case)
self.assertEqual(action.action, CaseAction.CLOSE)
self.assertEqual(action.created_by, self.user1)

self.case.refresh_from_db()
self.assertIsNotNone(self.case.closed_on)

# only user from assigned partner can close
self.login(self.user3)

self.case.reopen(self.admin, "Because")

response = self.url_post('unicef', url, {'note': "It's over"})
self.assertEqual(response.status_code, 403)

def test_reopen(self):
self.case.close(self.admin, "Done")

url = reverse('cases.case_reopen', args=[self.case.pk])

# log in as manager user in currently assigned partner
self.login(self.user1)

response = self.url_post('unicef', url, {'note': "Unfinished business"})
self.assertEqual(response.status_code, 204)

action = CaseAction.objects.get(created_by=self.user1)
self.assertEqual(action.case, self.case)
self.assertEqual(action.action, CaseAction.REOPEN)

self.case.refresh_from_db()
self.assertIsNone(self.case.closed_on)

# only user from assigned partner can reopen
self.login(self.user3)

self.case.close(self.admin, "Done")

response = self.url_post('unicef', url, {'note': "Unfinished business"})
self.assertEqual(response.status_code, 403)

def test_label(self):
url = reverse('cases.case_label', args=[self.case.pk])

# log in as manager user in currently assigned partner
self.login(self.user1)

response = self.url_post('unicef', url, {'labels': "%s" % self.pregnancy.pk})
self.assertEqual(response.status_code, 204)

actions = CaseAction.objects.filter(case=self.case).order_by('pk')
self.assertEqual(len(actions), 2)
self.assertEqual(actions[0].action, CaseAction.LABEL)
self.assertEqual(actions[0].label, self.pregnancy)
self.assertEqual(actions[1].action, CaseAction.UNLABEL)
self.assertEqual(actions[1].label, self.aids)

self.case.refresh_from_db()
self.assertEqual(set(self.case.labels.all()), {self.pregnancy})

# only user from assigned partner can label
self.login(self.user3)

response = self.url_post('unicef', url, {'labels': "%s" % self.aids.pk})
self.assertEqual(response.status_code, 403)

def test_update_summary(self):
url = reverse('cases.case_update_summary', args=[self.case.pk])

# log in as manager user in currently assigned partner
self.login(self.user1)

response = self.url_post('unicef', url, {'summary': "New summary"})
self.assertEqual(response.status_code, 204)

action = CaseAction.objects.get(case=self.case)
self.assertEqual(action.action, CaseAction.UPDATE_SUMMARY)

self.case.refresh_from_db()
self.assertEqual(self.case.summary, "New summary")

# only user from assigned partner can change the summary
self.login(self.user3)

response = self.url_post('unicef', url, {'summary': "Something else"})
self.assertEqual(response.status_code, 403)

def test_reply(self):
url = reverse('cases.case_reply', args=[self.case.pk])

# log in as manager user in currently assigned partner
self.login(self.user1)

response = self.url_post('unicef', url, {'text': "We can help"})
self.assertEqual(response.status_code, 200)

outgoing = Outgoing.objects.get()
self.assertEqual(outgoing.activity, Outgoing.CASE_REPLY)
self.assertEqual(outgoing.text, "We can help")
self.assertEqual(outgoing.created_by, self.user1)

# only user from assigned partner can reply
self.login(self.user3)

response = self.url_post('unicef', url, {'text': "Hi"})
self.assertEqual(response.status_code, 403)

def test_fetch(self):
url = reverse('cases.case_fetch', args=[self.case.pk])

# log in as manager user in currently assigned partner
self.login(self.user1)

response = self.url_get('unicef', url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {
'id': self.case.pk,
'contact': {'uuid': "C-001", 'name': "Ann"},
'assignee': {'id': self.moh.pk, 'name': "MOH"},
'labels': [{'id': self.aids.pk, 'name': "AIDS"}],
'summary': "Summary",
'opened_on': format_iso8601(self.case.opened_on),
'is_closed': False
})

# users with label access can also fetch
self.login(self.user3)

response = self.url_get('unicef', url)
self.assertEqual(response.status_code, 200)

@patch('casepro.test.TestBackend.fetch_contact_messages')
@patch('casepro.test.TestBackend.create_outgoing')
def test_timeline(self, mock_create_outgoing, mock_fetch_contact_messages):
@patch('casepro.test.TestBackend.push_outgoing')
def test_timeline(self, mock_push_outgoing, mock_fetch_contact_messages):
CaseAction.objects.all().delete()
Case.objects.all().delete()
Message.objects.all().delete()

d0 = datetime(2014, 1, 2, 12, 0, tzinfo=pytz.UTC)
d1 = datetime(2014, 1, 2, 13, 0, tzinfo=pytz.UTC)
d2 = datetime(2014, 1, 2, 14, 0, tzinfo=pytz.UTC)
Expand Down Expand Up @@ -446,8 +650,9 @@ def test_timeline(self, mock_create_outgoing, mock_fetch_contact_messages):

# user sends an outgoing message
d3 = timezone.now()
mock_create_outgoing.return_value = (202, d3)
Outgoing.create(self.unicef, self.user1, Outgoing.CASE_REPLY, "It's bad", ['C-001'], [], case)
outgoing = Outgoing.create_case_reply(self.unicef, self.user1, "It's bad", case)
outgoing.backend_id = 202
outgoing.save()

# page again looks for new timeline activity
response = self.url_get('unicef', '%s?after=%s' % (timeline_url, datetime_to_microseconds(t2)))
Expand Down
Loading

0 comments on commit 3a10af5

Please sign in to comment.