Skip to content

Commit 5608523

Browse files
Roles API and UI
1 parent c1082e1 commit 5608523

File tree

12 files changed

+505
-10
lines changed

12 files changed

+505
-10
lines changed

app/api/admin.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.contrib import admin
22

33
from .models import Label, Document, Project
4+
from .models import Role, RoleMapping
45
from .models import DocumentAnnotation, SequenceAnnotation, Seq2seqAnnotation
56
from .models import TextClassificationProject, SequenceLabelingProject, Seq2seqProject
67

@@ -41,6 +42,18 @@ class Seq2seqAnnotationAdmin(admin.ModelAdmin):
4142
search_fields = ('document',)
4243

4344

45+
class RoleAdmin(admin.ModelAdmin):
46+
list_display = ('name', 'description')
47+
ordering = ('name',)
48+
search_fields = ('name',)
49+
50+
51+
class RoleMappingAdmin(admin.ModelAdmin):
52+
list_display = ('user', 'role', 'project', )
53+
ordering = ('user',)
54+
search_fields = ('user',)
55+
56+
4457
admin.site.register(DocumentAnnotation, DocumentAnnotationAdmin)
4558
admin.site.register(SequenceAnnotation, SequenceAnnotationAdmin)
4659
admin.site.register(Seq2seqAnnotation, Seq2seqAnnotationAdmin)
@@ -50,3 +63,5 @@ class Seq2seqAnnotationAdmin(admin.ModelAdmin):
5063
admin.site.register(TextClassificationProject, ProjectAdmin)
5164
admin.site.register(SequenceLabelingProject, ProjectAdmin)
5265
admin.site.register(Seq2seqProject, ProjectAdmin)
66+
admin.site.register(Role, RoleAdmin)
67+
admin.site.register(RoleMapping, RoleMappingAdmin)

app/api/serializers.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from rest_framework.exceptions import ValidationError
55

66

7-
from .models import Label, Project, Document
7+
from .models import Label, Project, Document, RoleMapping, Role
88
from .models import TextClassificationProject, SequenceLabelingProject, Seq2seqProject
99
from .models import DocumentAnnotation, SequenceAnnotation, Seq2seqAnnotation
1010

@@ -161,3 +161,28 @@ class Meta:
161161
model = Seq2seqAnnotation
162162
fields = ('id', 'text', 'user', 'document', 'prob')
163163
read_only_fields = ('user',)
164+
165+
166+
class RoleSerializer(serializers.ModelSerializer):
167+
class Meta:
168+
model = Role
169+
fields = ('id', 'name')
170+
171+
172+
class RoleMappingSerializer(serializers.ModelSerializer):
173+
username = serializers.SerializerMethodField()
174+
rolename = serializers.SerializerMethodField()
175+
176+
@classmethod
177+
def get_username(cls, instance):
178+
user = instance.user
179+
return user.username if user else None
180+
181+
@classmethod
182+
def get_rolename(cls, instance):
183+
role = instance.role
184+
return role.name if role else None
185+
186+
class Meta:
187+
model = RoleMapping
188+
fields = ('id', 'user', 'role', 'username', 'rolename')

app/api/tests/test_api.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,3 +1293,192 @@ def test_returns_label_count(self):
12931293
response = self.client.get(self.url, format='json')
12941294
self.assertIn('user', response.data)
12951295
self.assertIsInstance(response.data['user'], dict)
1296+
1297+
1298+
class TestUserAPI(APITestCase):
1299+
1300+
@classmethod
1301+
def setUpTestData(cls):
1302+
cls.super_user_name = 'super_user_name'
1303+
cls.super_user_pass = 'super_user_pass'
1304+
User.objects.create_superuser(username=cls.super_user_name,
1305+
password=cls.super_user_pass,
1306+
1307+
cls.url = reverse(viewname='user_list')
1308+
1309+
def test_returns_user_count(self):
1310+
self.client.login(username=self.super_user_name,
1311+
password=self.super_user_pass)
1312+
response = self.client.get(self.url, format='json')
1313+
self.assertEqual(1, len(response.data))
1314+
1315+
1316+
class TestRoleAPI(APITestCase):
1317+
1318+
@classmethod
1319+
def setUpTestData(cls):
1320+
cls.user_name = 'user_name'
1321+
cls.user_pass = 'user_pass'
1322+
cls.project_admin_name = 'project_admin_name'
1323+
cls.project_admin_pass = 'project_admin_pass'
1324+
cls.user = User.objects.create_user(username=cls.user_name,
1325+
password=cls.user_pass)
1326+
project_admin = User.objects.create_superuser(username=cls.project_admin_name,
1327+
password=cls.project_admin_pass,
1328+
1329+
cls.url = reverse(viewname='roles')
1330+
1331+
def test_cannot_create_multiple_roles_with_same_name(self):
1332+
self.client.login(username=self.project_admin_name,
1333+
password=self.project_admin_pass)
1334+
roles = [
1335+
{'name': 'examplerole', 'description': 'example'},
1336+
{'name': 'examplerole', 'description': 'example'}
1337+
]
1338+
self.client.post(self.url, format='json', data=roles[0])
1339+
second_response = self.client.post(self.url, format='json', data=roles[1])
1340+
self.assertEqual(second_response.status_code, status.HTTP_400_BAD_REQUEST)
1341+
1342+
def test_nonadmin_cannot_create_role(self):
1343+
self.client.login(username=self.user_name,
1344+
password=self.user_pass)
1345+
data = {'name': 'testrole', 'description': 'example'}
1346+
response = self.client.post(self.url, format='json', data=data)
1347+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
1348+
1349+
def test_admin_can_create_role(self):
1350+
self.client.login(username=self.project_admin_name,
1351+
password=self.project_admin_pass)
1352+
data = {'name': 'testrole', 'description': 'example'}
1353+
response = self.client.post(self.url, format='json', data=data)
1354+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
1355+
1356+
def test_admin_can_get_roles(self):
1357+
self.client.login(username=self.project_admin_name,
1358+
password=self.project_admin_pass)
1359+
response = self.client.get(self.url, format='json')
1360+
self.assertEqual(response.status_code, status.HTTP_200_OK)
1361+
1362+
1363+
class TestRoleMappingListAPI(APITestCase):
1364+
1365+
@classmethod
1366+
def setUpTestData(cls):
1367+
cls.project_member_name = 'project_member_name'
1368+
cls.project_member_pass = 'project_member_pass'
1369+
cls.second_project_member_name = 'second_project_member_name'
1370+
cls.second_project_member_pass = 'second_project_member_pass'
1371+
cls.project_admin_name = 'project_admin_name'
1372+
cls.project_admin_pass = 'project_admin_pass'
1373+
project_member = User.objects.create_user(username=cls.project_member_name,
1374+
password=cls.project_member_pass)
1375+
cls.second_project_member = User.objects.create_user(username=cls.second_project_member_name,
1376+
password=cls.second_project_member_pass)
1377+
project_admin = User.objects.create_superuser(username=cls.project_admin_name,
1378+
password=cls.project_admin_pass,
1379+
1380+
cls.main_project = mommy.make('Project', users=[project_member, project_admin, cls.second_project_member])
1381+
cls.other_project = mommy.make('Project', users=[cls.second_project_member, project_admin])
1382+
1383+
cls.role = mommy.make('Role', name=settings.ROLE_PROJECT_ADMIN)
1384+
rolemapping = mommy.make('RoleMapping', role=cls.role, project=cls.main_project, user=project_admin)
1385+
cls.data = {'user': project_member.id, 'role': cls.role.id, 'project': cls.main_project.id}
1386+
cls.other_url = reverse(viewname='rolemapping_list', args=[cls.other_project.id])
1387+
cls.url = reverse(viewname='rolemapping_list', args=[cls.main_project.id])
1388+
1389+
def test_returns_mappings_to_project_admin(self):
1390+
self.client.login(username=self.project_admin_name,
1391+
password=self.project_admin_pass)
1392+
response = self.client.get(self.url, format='json')
1393+
self.assertEqual(response.status_code, status.HTTP_200_OK)
1394+
1395+
def test_allows_superuser_to_create_mapping(self):
1396+
self.client.login(username=self.project_admin_name,
1397+
password=self.project_admin_pass)
1398+
response = self.client.post(self.url, format='json', data=self.data)
1399+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
1400+
1401+
def test_do_not_allow_nonadmin_to_create_mapping(self):
1402+
self.client.login(username=self.project_member_name,
1403+
password=self.project_member_pass)
1404+
response = self.client.post(self.url, format='json', data=self.data)
1405+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
1406+
1407+
def test_do_not_return_mappings_to_nonadmin(self):
1408+
self.client.login(username=self.project_member_name,
1409+
password=self.project_member_pass)
1410+
response = self.client.get(self.url, format='json')
1411+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
1412+
1413+
def test_can_create_same_mapping_in_multiple_projects(self):
1414+
self.client.login(username=self.project_admin_name,
1415+
password=self.project_admin_pass)
1416+
mapping = [
1417+
{'user': self.second_project_member.id, 'role': self.role.id, 'project': self.main_project.id},
1418+
{'user': self.second_project_member.id, 'role': self.role.id, 'project': self.other_project.id}
1419+
]
1420+
response = self.client.post(self.url, format='json', data=mapping[0])
1421+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
1422+
response = self.client.post(self.other_url, format='json', data=mapping[1])
1423+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
1424+
1425+
1426+
class TestRoleMappingDetailAPI(APITestCase):
1427+
1428+
@classmethod
1429+
def setUpTestData(cls):
1430+
cls.project_admin_name = 'project_admin_name'
1431+
cls.project_admin_pass = 'project_admin_pass'
1432+
cls.project_member_name = 'project_member_name'
1433+
cls.project_member_pass = 'project_member_pass'
1434+
cls.non_project_member_name = 'non_project_member_name'
1435+
cls.non_project_member_pass = 'non_project_member_pass'
1436+
project_admin = User.objects.create_superuser(username=cls.project_admin_name,
1437+
password=cls.project_admin_pass,
1438+
1439+
project_member = User.objects.create_user(username=cls.project_member_name,
1440+
password=cls.project_member_pass)
1441+
non_project_member = User.objects.create_user(username=cls.non_project_member_name,
1442+
password=cls.non_project_member_pass)
1443+
project = mommy.make('Project', users=[project_admin, project_member])
1444+
role = mommy.make('Role', name=settings.ROLE_PROJECT_ADMIN)
1445+
change_role = mommy.make('Role', name=settings.ROLE_ANNOTATOR)
1446+
cls.rolemapping = mommy.make('RoleMapping', role=role, project=project, user=project_admin)
1447+
cls.url = reverse(viewname='rolemapping_detail', args=[project.id, cls.rolemapping.id])
1448+
cls.data = {'role': change_role.id }
1449+
1450+
def test_returns_rolemapping_to_project_member(self):
1451+
self.client.login(username=self.project_admin_name,
1452+
password=self.project_admin_pass)
1453+
response = self.client.get(self.url, format='json')
1454+
self.assertEqual(response.data['id'], self.rolemapping.id)
1455+
1456+
def test_do_not_return_mapping_to_non_project_member(self):
1457+
self.client.login(username=self.non_project_member_name,
1458+
password=self.non_project_member_pass)
1459+
response = self.client.get(self.url, format='json')
1460+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
1461+
1462+
def test_allows_admin_to_update_mapping(self):
1463+
self.client.login(username=self.project_admin_name,
1464+
password=self.project_admin_pass)
1465+
response = self.client.patch(self.url, format='json', data=self.data)
1466+
self.assertEqual(response.data['role'], self.data['role'])
1467+
1468+
def test_disallows_project_member_to_update_mapping(self):
1469+
self.client.login(username=self.project_member_name,
1470+
password=self.project_member_pass)
1471+
response = self.client.patch(self.url, format='json', data=self.data)
1472+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
1473+
1474+
def test_allows_admin_to_delete_mapping(self):
1475+
self.client.login(username=self.project_admin_name,
1476+
password=self.project_admin_pass)
1477+
response = self.client.delete(self.url, format='json')
1478+
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
1479+
1480+
def test_disallows_project_member_to_delete_mapping(self):
1481+
self.client.login(username=self.project_member_name,
1482+
password=self.project_member_pass)
1483+
response = self.client.delete(self.url, format='json')
1484+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

app/api/urls.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@
22
from rest_framework.authtoken.views import obtain_auth_token
33
from rest_framework.urlpatterns import format_suffix_patterns
44

5-
from .views import Me, Features
5+
from .views import Me, Features, Users
66
from .views import ProjectList, ProjectDetail
77
from .views import LabelList, LabelDetail, ApproveLabelsAPI
88
from .views import DocumentList, DocumentDetail
99
from .views import AnnotationList, AnnotationDetail
1010
from .views import TextUploadAPI, TextDownloadAPI, CloudUploadAPI
1111
from .views import StatisticsAPI
12-
12+
from .views import RoleMappingList, RoleMappingDetail, Roles
1313

1414
urlpatterns = [
1515
path('auth-token', obtain_auth_token),
1616
path('me', Me.as_view(), name='me'),
1717
path('features', Features.as_view(), name='features'),
1818
path('cloud-upload', CloudUploadAPI.as_view(), name='cloud_uploader'),
1919
path('projects', ProjectList.as_view(), name='project_list'),
20+
path('users', Users.as_view(), name='user_list'),
21+
path('roles', Roles.as_view(), name='roles'),
2022
path('projects/<int:project_id>', ProjectDetail.as_view(), name='project_detail'),
2123
path('projects/<int:project_id>/statistics',
2224
StatisticsAPI.as_view(), name='statistics'),
@@ -37,7 +39,11 @@
3739
path('projects/<int:project_id>/docs/upload',
3840
TextUploadAPI.as_view(), name='doc_uploader'),
3941
path('projects/<int:project_id>/docs/download',
40-
TextDownloadAPI.as_view(), name='doc_downloader')
42+
TextDownloadAPI.as_view(), name='doc_downloader'),
43+
path('projects/<int:project_id>/roles',
44+
RoleMappingList.as_view(), name='rolemapping_list'),
45+
path('projects/<int:project_id>/roles/<int:rolemapping_id>',
46+
RoleMappingDetail.as_view(), name='rolemapping_detail'),
4147
]
4248

4349
urlpatterns = format_suffix_patterns(urlpatterns, allowed=['json', 'xml'])

app/api/views.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.conf import settings
2+
from django.contrib.auth.models import User
23
from django.shortcuts import get_object_or_404, redirect
34
from django_filters.rest_framework import DjangoFilterBackend
45
from django.db.models import Count, F
@@ -13,11 +14,12 @@
1314
from rest_framework_csv.renderers import CSVRenderer
1415

1516
from .filters import DocumentFilter
16-
from .models import Project, Label, Document
17+
from .models import Project, Label, Document, RoleMapping, Role
1718
from .permissions import IsProjectAdmin, IsAnnotator, IsAnnotationApprover, IsAnnotatorAndCreator, IsOwnAnnotation
1819
from .serializers import ProjectSerializer, LabelSerializer, DocumentSerializer, UserSerializer
19-
from .serializers import ProjectPolymorphicSerializer
20+
from .serializers import ProjectPolymorphicSerializer, RoleMappingSerializer, RoleSerializer
2021
from .utils import CSVParser, ExcelParser, JSONParser, PlainTextParser, CoNLLParser, iterable_to_io
22+
from .serializers import ProjectPolymorphicSerializer, RoleMappingSerializer, RoleSerializer
2123
from .utils import JSONLRenderer
2224
from .utils import JSONPainter, CSVPainter
2325

@@ -319,3 +321,40 @@ def select_painter(self, format):
319321
return JSONPainter()
320322
else:
321323
raise ValidationError('format {} is invalid.'.format(format))
324+
325+
326+
class Users(APIView):
327+
permission_classes = (IsAuthenticated, IsProjectAdmin)
328+
329+
def get(self, request, *args, **kwargs):
330+
queryset = User.objects.all()
331+
serialized_data = UserSerializer(queryset, many=True).data
332+
return Response(serialized_data)
333+
334+
335+
class Roles(generics.ListCreateAPIView):
336+
serializer_class = RoleSerializer
337+
pagination_class = None
338+
permission_classes = (IsAuthenticated, IsProjectAdmin)
339+
queryset = Role.objects.all()
340+
341+
342+
class RoleMappingList(generics.ListCreateAPIView):
343+
serializer_class = RoleMappingSerializer
344+
pagination_class = None
345+
permission_classes = (IsAuthenticated, IsProjectAdmin)
346+
347+
def get_queryset(self):
348+
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
349+
return project.role_mapping
350+
351+
def perform_create(self, serializer):
352+
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
353+
serializer.save(project=project)
354+
355+
356+
class RoleMappingDetail(generics.RetrieveUpdateDestroyAPIView):
357+
queryset = RoleMapping.objects.all()
358+
serializer_class = RoleMappingSerializer
359+
lookup_url_kwarg = 'rolemapping_id'
360+
permission_classes = (IsAuthenticated, IsProjectAdmin)

0 commit comments

Comments
 (0)