Skip to content

Commit

Permalink
Merge pull request #112 from rapidpro/contact_cases
Browse files Browse the repository at this point in the history
List a contact's cases on their read page
  • Loading branch information
rowanseymour committed Jul 22, 2016
2 parents 96b20ce + e091f00 commit ae468b3
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 62 deletions.
36 changes: 29 additions & 7 deletions casepro/contacts/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,19 +142,13 @@ def test_read(self):
self.login(self.user1)
response = self.url_get('unicef', url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Age")
self.assertNotContains(response, "View External")

# administrators get button linking to backend
self.login(self.admin)
response = self.url_get('unicef', url)
self.assertContains(response, "View External")

# superusers see extra fields
self.login(self.superuser)
response = self.url_get('unicef', url)
self.assertContains(response, "Is Blocked")
self.assertContains(response, "Is Active")

# users from other orgs get nothing
self.login(self.user4)
response = self.url_get('unicef', url)
Expand All @@ -175,6 +169,34 @@ def test_fetch(self):
'language': {'code': 'eng', 'name': "English"}
})

def test_cases(self):
url = reverse('contacts.contact_cases', args=[self.ann.pk])

msg1 = self.create_message(self.unicef, 101, self.ann, "What is tea?")
case1 = self.create_case(self.unicef, self.ann, self.moh, msg1, [])
case1.close(self.admin)
msg2 = self.create_message(self.unicef, 102, self.ann, "I'm pregnant")
case2 = self.create_case(self.unicef, self.ann, self.moh, msg2, [self.pregnancy, self.aids])

# log in as admin
self.login(self.admin)

# should see all cases in reverse chronological order
response = self.url_get('unicef', url)
self.assertEqual([c['id'] for c in response.json['results']], [case2.pk, case1.pk])

self.login(self.user1)

# should see both cases because of assignment/labels
response = self.url_get('unicef', url)
self.assertEqual([c['id'] for c in response.json['results']], [case2.pk, case1.pk])

self.login(self.user3)

# should see only case with pregnancy label
response = self.url_get('unicef', url)
self.assertEqual([c['id'] for c in response.json['results']], [case2.pk])


class FieldCRUDLTest(BaseCasesTest):
def test_list(self):
Expand Down
66 changes: 28 additions & 38 deletions casepro/contacts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from django.utils.translation import ugettext_lazy as _
from smartmin.views import SmartCRUDL, SmartReadView, SmartListView, SmartFormView

from casepro.utils import JSONEncoder, get_language_name
from casepro.cases.models import Case
from casepro.utils import json_encode, JSONEncoder

from .models import Contact, Group, Field

Expand All @@ -17,7 +18,7 @@ class ContactCRUDL(SmartCRUDL):
Simple contact CRUDL for debugging by superusers, i.e. not exposed to regular users for now
"""
model = Contact
actions = ('list', 'read', 'fetch')
actions = ('list', 'read', 'fetch', 'cases')

class List(OrgPermsMixin, SmartListView):
fields = ('uuid', 'name', 'language', 'created_on')
Expand All @@ -26,44 +27,15 @@ def get_queryset(self, **kwargs):
return self.model.objects.filter(org=self.request.org)

class Read(OrgObjPermsMixin, SmartReadView):
base_fields = ['language', 'groups']
superuser_fields = ['uuid', 'created_on', 'is_blocked', 'is_active']

def get_queryset(self, **kwargs):
contact_fields = Field.get_all(self.request.org, visible=True)
self.contact_field_keys = [f.key for f in contact_fields]
self.contact_field_labels = {f.key: f.label for f in contact_fields}

return self.model.objects.filter(org=self.request.org)

def derive_fields(self):
fields = self.base_fields + self.contact_field_keys

if self.request.user.is_superuser:
fields += self.superuser_fields

return fields

def get_language(self, obj):
return get_language_name(obj.language) if obj.language else None

def get_groups(self, obj):
return ", ".join([g.name for g in obj.groups.all()])

def lookup_field_value(self, context, obj, field):
if field in self.base_fields or field in self.superuser_fields:
return super(ContactCRUDL.Read, self).lookup_field_value(context, obj, field)
else:
return obj.get_fields().get(field)

def lookup_field_label(self, context, field, default=None):
if field in self.base_fields or field in self.superuser_fields:
return super(ContactCRUDL.Read, self).lookup_field_label(context, field, default)
else:
return self.contact_field_labels.get(field, default)

def get_context_data(self, **kwargs):
context = super(ContactCRUDL.Read, self).get_context_data(**kwargs)

fields = Field.get_all(self.object.org, visible=True).order_by('label')

context['context_data_json'] = json_encode({
'contact': self.object.as_json(full=True),
'fields': [f.as_json() for f in fields]
})
context['backend_url'] = settings.SITE_EXTERNAL_CONTACT_URL % self.object.uuid
return context

Expand All @@ -76,6 +48,24 @@ class Fetch(OrgObjPermsMixin, SmartReadView):
def render_to_response(self, context, **response_kwargs):
return JsonResponse(self.object.as_json(full=True), encoder=JSONEncoder)

class Cases(OrgObjPermsMixin, SmartReadView):
"""
JSON endpoint for fetching a contact's cases
"""
permission = 'contacts.contact_read'

def get_context_data(self, **kwargs):
context = super(ContactCRUDL.Cases, self).get_context_data(**kwargs)

cases = Case.get_all(self.request.org, self.request.user).filter(contact=self.object).order_by('-opened_on')
cases = cases.prefetch_related('labels').select_related('contact', 'assignee')

context['object_list'] = cases
return context

def render_to_response(self, context, **response_kwargs):
return JsonResponse({'results': [c.as_json() for c in context['object_list']]}, encoder=JSONEncoder)


class GroupCRUDL(SmartCRUDL):
model = Group
Expand Down
39 changes: 37 additions & 2 deletions karma/test-controllers.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ describe('controllers:', () ->
moh: {id: 301, name: "MOH"},
who: {id: 302, name: "WHO"},

# fields
nickname: {key: 'nickname', label: "Nickname", value_type: 'T'},
age: {key: 'age', label: "Age", value_type: 'N'},

# contacts
ann: {id: 401, name: "Ann"},
bob: {id: 402, name: "Bob"}
ann: {id: 401, name: "Ann", fields: {'age': 35}},
bob: {id: 402, name: "Bob", fields: {}}
}
)

Expand Down Expand Up @@ -526,6 +530,37 @@ describe('controllers:', () ->
)
)

#=======================================================================
# Tests for ContactController
#=======================================================================
describe('ContactController', () ->
ContactService = null
$scope = null

beforeEach(inject((_ContactService_) ->
ContactService = _ContactService_

$scope = $rootScope.$new()
$window.contextData = {contact: test.ann, fields: [test.age, test.nickname]}
$controller('ContactController', {$scope: $scope})
))

it('init should fetch contact cases', () ->
expect($scope.contact).toEqual(test.ann)
expect($scope.fields).toEqual([test.age, test.nickname])

fetchCases = spyOnPromise($q, $scope, ContactService, 'fetchCases')

$scope.init()

cases = [{id: 501, opened_on: utcdate(2016, 5, 17, 8, 49, 13, 698)}]
fetchCases.resolve(cases)

expect(ContactService.fetchCases).toHaveBeenCalledWith(test.ann)
expect($scope.cases).toEqual(cases)
)
)

#=======================================================================
# Tests for HomeController
#=======================================================================
Expand Down
10 changes: 10 additions & 0 deletions karma/test-services.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,16 @@ describe('services:', () ->
$httpBackend.flush()
)
)

describe('fetchCases', () ->
it('gets contacts cases from fetch endpoint', () ->
$httpBackend.expectGET('/contact/cases/401/').respond('{"results":[{"id": 501, "opened_on": "2016-05-17T08:49:13.698864"}]}')
ContactService.fetchCases(test.ann).then((cases) ->
expect(cases).toEqual([{id: 501, opened_on: utcdate(2016, 5, 17, 8, 49, 13, 698)}])
)
$httpBackend.flush()
)
)
)

#=======================================================================
Expand Down
15 changes: 15 additions & 0 deletions static/coffee/controllers.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,21 @@ controllers.controller('CaseTimelineController', ['$scope', '$timeout', 'CaseSer
])


#============================================================================
# Contact dashboard controller
#============================================================================
controllers.controller('ContactController', ['$scope', '$window', 'ContactService', ($scope, $window, ContactService) ->

$scope.contact = $window.contextData.contact
$scope.fields = $window.contextData.fields

$scope.init = () ->
ContactService.fetchCases($scope.contact).then((cases) ->
$scope.cases = cases
)
])


#============================================================================
# Label dashboard controller
#============================================================================
Expand Down
9 changes: 9 additions & 0 deletions static/coffee/services.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ services.factory('ContactService', ['$http', ($http) ->
return $http.get('/contact/fetch/' + id + '/').then((response) ->
return response.data
)

#----------------------------------------------------------------------------
# Fetches a contact's cases
#----------------------------------------------------------------------------
fetchCases: (contact) ->
return $http.get('/contact/cases/' + contact.id + '/').then((response) ->
utils.parseDates(response.data.results, 'opened_on')
return response.data.results
)
])


Expand Down
83 changes: 68 additions & 15 deletions templates/contacts/contact_read.haml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,71 @@
- load smartmin i18n thumbnail

- block pre-content
.page-header.clearfix{ style:"border-bottom: none" }
.page-header-buttons
- if user_is_admin
%a.btn.btn-default{ href:"{{ backend_url }}", target:"_blank" }
%span.glyphicon.glyphicon-new-window
- trans "View External"
%h2
%span.glyphicon.glyphicon-phone
{{ title }}
<uib-tabset active="active">
<uib-tab index="0" heading="{% trans "Summary" %}"></uib-tab>
</uib-tabset>
<br/>

- block content
%script{ type:"text/javascript" }
var contextData = {{ context_data_json|safe }};

.ng-cloak{ ng-controller:"ContactController", ng-init:"init()", ng-cloak:"" }
.page-header.clearfix
.page-header-buttons
- if user_is_admin
%a.btn.btn-default{ href:"{{ backend_url }}", target:"_blank" }
%span.glyphicon.glyphicon-new-window
- trans "View External"

%h2
%span.glyphicon.glyphicon-phone
[[ contact.name ]]

%br

.row
.col-md-8
.panel.panel-default
.panel-heading
- trans "Cases"
.list-group
%a.list-group-item{ ng-repeat:"item in cases", ng-href:"/case/read/[[ item.id ]]/" }
.case-time
[[ item.opened_on | autodate ]]
.case-text
%span.label-container
%span.label.label-warning
[[ item.assignee.name ]]
&nbsp;
%span.label-container{ ng-repeat:"label in item.labels" }
%span.label.label-success
[[ label.name ]]
&nbsp;
[[ item.summary ]]
.col-md-4
.panel.panel-default
.panel-heading
- trans "Details"
.panel-body
.container-fluid
.row
.contact-field-label.col-sm-6
- trans "Language"
.contact-field-value.col-sm-6
[[ contact.language.name || "--" ]]
.row{ ng-repeat:"field in fields" }
.contact-field-label.col-sm-6
[[ field.label ]]
.contact-field-value.col-sm-6
<cp-fieldvalue contact="contact" field="field" />
- block extra-style
{{ block.super }}
:css
.case-time {
float: right;
font-size: 0.7em;
margin-left: 3px;
margin-bottom: 3px;
}
.contact-field-label {
font-weight: bold;
}

0 comments on commit ae468b3

Please sign in to comment.