Browse Source

Role and RoleMapping Models

pull/333/head
Razi Rais 5 years ago
committed by margaretmeehan
parent
commit
c1082e14e9
10 changed files with 356 additions and 39 deletions
  1. 41
      app/api/migrations/0004_roles.py
  2. 48
      app/api/models.py
  3. 69
      app/api/permissions.py
  4. 136
      app/api/tests/test_api.py
  5. 39
      app/api/views.py
  6. 4
      app/app/settings.py
  7. 33
      app/server/management/commands/create_role_mapping.py
  8. 23
      app/server/management/commands/create_roles.py
  9. 1
      tools/dev-django.sh
  10. 1
      tools/run.sh

41
app/api/migrations/0004_roles.py

@ -0,0 +1,41 @@
# Generated by Django 2.1.7 on 2019-07-25 03:33
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('api', '0003_support_sql_server'),
]
operations = [
migrations.CreateModel(
name='Role',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('description', models.TextField(default='', null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='RoleMapping',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_mapping', to='api.Project')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Role')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_mapping', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterUniqueTogether(
name='rolemapping',
unique_together={('user', 'project', 'role')},
),
]

48
app/api/models.py

@ -1,6 +1,8 @@
import string
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save, pre_delete
from django.urls import reverse
from django.contrib.auth.models import User
from django.contrib.staticfiles.storage import staticfiles_storage
@ -237,3 +239,49 @@ class Seq2seqAnnotation(Annotation):
class Meta:
unique_together = ('document', 'user', 'text')
class Role(models.Model):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(default='')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class RoleMapping(models.Model):
user = models.ForeignKey(User, related_name='role_mapping', on_delete=models.CASCADE)
project = models.ForeignKey(Project, related_name='role_mapping', on_delete=models.CASCADE)
role = models.ForeignKey(Role, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def clean(self):
other_rolemappings = self.project.role_mappings.exclude(id=self.id)
if other_rolemappings.filter(user=self.user, project=self.project).exists():
raise ValidationError('This user is already assigned to a role in this project.')
class Meta:
unique_together = ("user", "project", "role")
@receiver(post_save, sender=RoleMapping)
def add_linked_project(sender, instance, created, **kwargs):
userInstance = instance.user
projectInstance = instance.project
if created and userInstance and projectInstance:
user = User.objects.get(pk=userInstance.pk)
project = Project.objects.get(pk=projectInstance.pk)
user.projects.add(project)
user.save()
@receiver(pre_delete, sender=RoleMapping)
def delete_linked_project(sender, instance, using, **kwargs):
userInstance = instance.user
projectInstance = instance.project
if userInstance and projectInstance:
user = User.objects.get(pk=userInstance.pk)
project = Project.objects.get(pk=projectInstance.pk)
user.projects.remove(project)
user.save()

69
app/api/permissions.py

@ -1,17 +1,21 @@
from django.conf import settings
from django.contrib.auth.mixins import UserPassesTestMixin
from django.db.models import Subquery
from django.shortcuts import get_object_or_404
from rest_framework.permissions import BasePermission, SAFE_METHODS, IsAdminUser
from .models import Project
from .models import Project, Role, RoleMapping
class IsProjectUser(BasePermission):
class ProjectMixin:
def get_project_id(self, request, view):
return view.kwargs.get('project_id') or request.query_params.get('project_id')
def has_permission(self, request, view):
user = request.user
project_id = view.kwargs.get('project_id') or request.query_params.get('project_id')
project = get_object_or_404(Project, pk=project_id)
class IsProjectUser(ProjectMixin, BasePermission):
def has_permission(self, request, view):
project = get_object_or_404(Project, pk=self.get_project_id(request, view))
return user in project.users.all()
@ -30,13 +34,62 @@ class SuperUserMixin(UserPassesTestMixin):
return self.request.user.is_superuser
class IsOwnAnnotation(BasePermission):
class IsOwnAnnotation(ProjectMixin, BasePermission):
def has_permission(self, request, view):
project_id = view.kwargs.get('project_id')
project_id = self.get_project_id(request, view)
annotation_id = view.kwargs.get('annotation_id')
project = get_object_or_404(Project, pk=project_id)
model = project.get_annotation_class()
annotation = model.objects.filter(id=annotation_id, user=request.user)
return annotation.exists()
class RolePermission(ProjectMixin, BasePermission):
UNSAFE_METHODS = ('POST', 'PATCH', 'DELETE')
unsafe_methods_check = True
role_name = ''
def is_super_user(self, user):
return user.is_superuser
def has_permission(self, request, view):
is_super_user = self.is_super_user(request.user)
if is_super_user:
return True
if self.unsafe_methods_check and request.method in self.UNSAFE_METHODS:
return is_super_user
project_id = self.get_project_id(request, view)
if not project_id and request.method in SAFE_METHODS:
return True
return is_in_role(self.role_name, request.user.id, project_id)
class IsProjectAdmin(RolePermission):
unsafe_methods_check = False
role_name = settings.ROLE_PROJECT_ADMIN
class IsAnnotatorAndCreator(RolePermission):
unsafe_methods_check = False
role_name = settings.ROLE_ANNOTATOR
class IsAnnotator(RolePermission):
role_name = settings.ROLE_ANNOTATOR
class IsAnnotationApprover(RolePermission):
role_name = settings.ROLE_ANNOTATION_APPROVER
def is_in_role(role_name, user_id, project_id):
return RoleMapping.objects.filter(
user_id=user_id,
project_id=project_id,
role_id=Subquery(Role.objects.filter(name=role_name).values('id')),
).exists()

136
app/api/tests/test_api.py

@ -1,18 +1,34 @@
import os
from django.conf import settings
from django.test import override_settings
from rest_framework import status
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase
from model_mommy import mommy
from ..models import User, SequenceAnnotation, Document
from ..models import User, SequenceAnnotation, Document, Role, RoleMapping
from ..models import DOCUMENT_CLASSIFICATION, SEQUENCE_LABELING, SEQ2SEQ
from ..utils import PlainTextParser, CoNLLParser, JSONParser, CSVParser
from ..exceptions import FileParseException
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
def create_default_roles():
Role.objects.get_or_create(name=settings.ROLE_PROJECT_ADMIN)
Role.objects.get_or_create(name=settings.ROLE_ANNOTATOR)
Role.objects.get_or_create(name=settings.ROLE_ANNOTATION_APPROVER)
def assign_user_to_role(project_member, project, role_name):
role, created = Role.objects.get_or_create(name=role_name)
RoleMapping.objects.get_or_create(role_id=role.id, user_id=project_member.id, project_id=project.id)
def remove_all_role_mappings():
RoleMapping.objects.all().delete()
@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
class TestProjectListAPI(APITestCase):
@ -32,10 +48,13 @@ class TestProjectListAPI(APITestCase):
super_user = User.objects.create_superuser(username=cls.super_user_name,
password=cls.super_user_pass,
email='fizz@buzz.com')
cls.main_project = mommy.make('TextClassificationProject', users=[main_project_member])
cls.sub_project = mommy.make('TextClassificationProject', users=[sub_project_member])
create_default_roles()
assign_user_to_role(project_member=main_project_member, project=cls.main_project,
role_name=settings.ROLE_ANNOTATOR)
assign_user_to_role(project_member=sub_project_member, project=cls.sub_project,
role_name=settings.ROLE_ANNOTATOR)
cls.url = reverse(viewname='project_list')
cls.data = {'name': 'example', 'project_type': 'DocumentClassification',
'description': 'example', 'guideline': 'example',
@ -72,6 +91,10 @@ class TestProjectListAPI(APITestCase):
response = self.client.post(self.url, format='json', data=self.data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
class TestProjectDetailAPI(APITestCase):
@ -88,14 +111,17 @@ class TestProjectDetailAPI(APITestCase):
password=cls.project_member_pass)
non_project_member = User.objects.create_user(username=cls.non_project_member_name,
password=cls.non_project_member_pass)
# Todo: change super_user to project_admin.
super_user = User.objects.create_superuser(username=cls.super_user_name,
password=cls.super_user_pass,
email='fizz@buzz.com')
cls.main_project = mommy.make('TextClassificationProject', users=[cls.project_member, super_user])
mommy.make('TextClassificationProject', users=[non_project_member])
cls.url = reverse(viewname='project_detail', args=[cls.main_project.id])
cls.data = {'description': 'lorem'}
create_default_roles()
assign_user_to_role(project_member=cls.project_member, project=cls.main_project,
role_name=settings.ROLE_ANNOTATOR)
def test_returns_main_project_detail_to_main_project_member(self):
self.client.login(username=self.project_member_name,
@ -115,9 +141,9 @@ class TestProjectDetailAPI(APITestCase):
response = self.client.patch(self.url, format='json', data=self.data)
self.assertEqual(response.data['description'], self.data['description'])
def test_disallows_project_member_to_update_project(self):
self.client.login(username=self.project_member_name,
password=self.project_member_pass)
def test_disallows_non_project_member_to_update_project(self):
self.client.login(username=self.non_project_member_name,
password=self.non_project_member_pass)
response = self.client.patch(self.url, format='json', data=self.data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@ -127,12 +153,16 @@ class TestProjectDetailAPI(APITestCase):
response = self.client.delete(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def test_disallows_project_member_to_delete_project(self):
self.client.login(username=self.project_member_name,
password=self.project_member_pass)
def test_disallows_non_project_member_to_delete_project(self):
self.client.login(username=self.non_project_member_name,
password=self.non_project_member_pass)
response = self.client.delete(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
class TestLabelListAPI(APITestCase):
@ -144,15 +174,15 @@ class TestLabelListAPI(APITestCase):
cls.non_project_member_pass = 'non_project_member_pass'
cls.super_user_name = 'super_user_name'
cls.super_user_pass = 'super_user_pass'
project_member = User.objects.create_user(username=cls.project_member_name,
password=cls.project_member_pass)
cls.project_member = User.objects.create_user(username=cls.project_member_name,
password=cls.project_member_pass)
non_project_member = User.objects.create_user(username=cls.non_project_member_name,
password=cls.non_project_member_pass)
# Todo: change super_user to project_admin.
super_user = User.objects.create_superuser(username=cls.super_user_name,
password=cls.super_user_pass,
email='fizz@buzz.com')
cls.main_project = mommy.make('Project', users=[project_member, super_user])
cls.main_project = mommy.make('Project', users=[cls.project_member, super_user])
cls.main_project_label = mommy.make('Label', project=cls.main_project)
sub_project = mommy.make('Project', users=[non_project_member])
@ -161,6 +191,9 @@ class TestLabelListAPI(APITestCase):
cls.url = reverse(viewname='label_list', args=[cls.main_project.id])
cls.other_url = reverse(viewname='label_list', args=[other_project.id])
cls.data = {'text': 'example'}
create_default_roles()
assign_user_to_role(project_member=cls.project_member, project=cls.main_project,
role_name=settings.ROLE_ANNOTATOR)
def test_returns_labels_to_project_member(self):
self.client.login(username=self.project_member_name,
@ -235,6 +268,10 @@ class TestLabelListAPI(APITestCase):
response = self.client.post(self.url, format='json', data=self.data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
class TestLabelDetailAPI(APITestCase):
@ -258,6 +295,9 @@ class TestLabelDetailAPI(APITestCase):
cls.label = mommy.make('Label', project=project)
cls.url = reverse(viewname='label_detail', args=[project.id, cls.label.id])
cls.data = {'text': 'example'}
create_default_roles()
assign_user_to_role(project_member=project_member, project=project,
role_name=settings.ROLE_ANNOTATOR)
def test_returns_label_to_project_member(self):
self.client.login(username=self.project_member_name,
@ -295,6 +335,10 @@ class TestLabelDetailAPI(APITestCase):
response = self.client.delete(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
class TestDocumentListAPI(APITestCase):
@ -310,7 +354,6 @@ class TestDocumentListAPI(APITestCase):
password=cls.project_member_pass)
non_project_member = User.objects.create_user(username=cls.non_project_member_name,
password=cls.non_project_member_pass)
# Todo: change super_user to project_admin.
super_user = User.objects.create_superuser(username=cls.super_user_name,
password=cls.super_user_pass,
email='fizz@buzz.com')
@ -327,6 +370,11 @@ class TestDocumentListAPI(APITestCase):
cls.url = reverse(viewname='doc_list', args=[cls.main_project.id])
cls.random_order_project_url = reverse(viewname='doc_list', args=[cls.random_order_project.id])
cls.data = {'text': 'example'}
create_default_roles()
assign_user_to_role(project_member=project_member, project=cls.main_project,
role_name=settings.ROLE_ANNOTATOR)
assign_user_to_role(project_member=project_member, project=cls.random_order_project,
role_name=settings.ROLE_ANNOTATOR)
def test_returns_docs_to_project_member(self):
self.client.login(username=self.project_member_name,
@ -385,6 +433,10 @@ class TestDocumentListAPI(APITestCase):
response = self.client.post(self.url, format='json', data=self.data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
class TestDocumentDetailAPI(APITestCase):
@ -408,6 +460,9 @@ class TestDocumentDetailAPI(APITestCase):
cls.doc = mommy.make('Document', project=project)
cls.url = reverse(viewname='doc_detail', args=[project.id, cls.doc.id])
cls.data = {'text': 'example'}
create_default_roles()
assign_user_to_role(project_member=project_member, project=project,
role_name=settings.ROLE_ANNOTATOR)
def test_returns_doc_to_project_member(self):
self.client.login(username=self.project_member_name,
@ -445,6 +500,10 @@ class TestDocumentDetailAPI(APITestCase):
response = self.client.delete(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
class TestApproveLabelsAPI(APITestCase):
@classmethod
@ -462,6 +521,9 @@ class TestApproveLabelsAPI(APITestCase):
project = mommy.make('TextClassificationProject', users=[project_member, super_user])
cls.doc = mommy.make('Document', project=project)
cls.url = reverse(viewname='approve_labels', args=[project.id, cls.doc.id])
create_default_roles()
assign_user_to_role(project_member=project_member, project=project,
role_name=settings.ROLE_ANNOTATOR)
def test_allows_superuser_to_approve_and_disapprove_labels(self):
self.client.login(username=self.super_user_name, password=self.super_user_pass)
@ -472,13 +534,17 @@ class TestApproveLabelsAPI(APITestCase):
response = self.client.post(self.url, format='json', data={'approved': False})
self.assertIsNone(response.data['annotation_approver'])
def test_disallows_project_member_to_approve_and_disapprove_labels(self):
def test_disallows_non_annotation_approver_to_approve_and_disapprove_labels(self):
self.client.login(username=self.project_member_name, password=self.project_member_pass)
response = self.client.post(self.url, format='json', data={'approved': True})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
class TestAnnotationListAPI(APITestCase):
@ -516,6 +582,10 @@ class TestAnnotationListAPI(APITestCase):
user=another_project_member).count()
cls.main_project = main_project
create_default_roles()
assign_user_to_role(project_member=project_member, project=main_project,
role_name=settings.ROLE_ANNOTATOR)
def test_returns_annotations_to_project_member(self):
self.client.login(username=self.project_member_name,
password=self.project_member_pass)
@ -566,6 +636,10 @@ class TestAnnotationListAPI(APITestCase):
self.addCleanup(cleanup_project)
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
class TestAnnotationDetailAPI(APITestCase):
@ -603,6 +677,9 @@ class TestAnnotationDetailAPI(APITestCase):
main_project_doc.id,
another_entity.id])
cls.post_data = {'start_offset': 0, 'end_offset': 10}
create_default_roles()
assign_user_to_role(project_member=project_member, project=main_project,
role_name=settings.ROLE_ANNOTATOR)
def test_returns_annotation_to_project_member(self):
self.client.login(username=self.project_member_name,
@ -658,6 +735,10 @@ class TestAnnotationDetailAPI(APITestCase):
response = self.client.delete(self.another_url, format='json', data=self.post_data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
class TestSearch(APITestCase):
@ -685,6 +766,9 @@ class TestSearch(APITestCase):
mommy.make('Document', text=cls.search_term, project=sub_project)
cls.url = reverse(viewname='doc_list', args=[cls.main_project.id])
cls.data = {'q': cls.search_term}
create_default_roles()
assign_user_to_role(project_member=project_member, project=cls.main_project,
role_name=settings.ROLE_ANNOTATOR)
def test_can_filter_doc_by_term(self):
self.client.login(username=self.project_member_name,
@ -730,6 +814,10 @@ class TestSearch(APITestCase):
for d1, d2 in zip(response.data['results'], docs):
self.assertEqual(d1['id'], d2['id'])
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
class TestFilter(APITestCase):
@ -749,6 +837,9 @@ class TestFilter(APITestCase):
mommy.make('SequenceAnnotation', document=doc2, user=project_member, label=cls.label2)
cls.url = reverse(viewname='doc_list', args=[cls.main_project.id])
cls.params = {'seq_annotations__label__id': cls.label1.id}
create_default_roles()
assign_user_to_role(project_member=project_member, project=cls.main_project,
role_name=settings.ROLE_ANNOTATOR)
def test_can_filter_by_label(self):
self.client.login(username=self.project_member_name,
@ -779,6 +870,10 @@ class TestFilter(APITestCase):
for d1, d2 in zip(response.data['results'], docs):
self.assertEqual(d1['id'], d2['id'])
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
class TestUploader(APITestCase):
@ -795,6 +890,13 @@ class TestUploader(APITestCase):
cls.labeling_project = mommy.make('SequenceLabelingProject',
users=[super_user], project_type=SEQUENCE_LABELING)
cls.seq2seq_project = mommy.make('Seq2seqProject', users=[super_user], project_type=SEQ2SEQ)
create_default_roles()
assign_user_to_role(project_member=super_user, project=cls.classification_project,
role_name=settings.ROLE_PROJECT_ADMIN)
assign_user_to_role(project_member=super_user, project=cls.labeling_project,
role_name=settings.ROLE_PROJECT_ADMIN)
assign_user_to_role(project_member=super_user, project=cls.seq2seq_project,
role_name=settings.ROLE_PROJECT_ADMIN)
def setUp(self):
self.client.login(username=self.super_user_name,
@ -956,6 +1058,10 @@ class TestUploader(APITestCase):
file_format='json',
expected_status=status.HTTP_201_CREATED)
@classmethod
def doCleanups(cls):
remove_all_role_mappings()
@override_settings(CLOUD_BROWSER_APACHE_LIBCLOUD_PROVIDER='LOCAL')
@override_settings(CLOUD_BROWSER_APACHE_LIBCLOUD_ACCOUNT=os.path.dirname(DATA_DIR))

39
app/api/views.py

@ -6,7 +6,7 @@ from libcloud.base import DriverType, get_driver
from libcloud.storage.types import ContainerDoesNotExistError, ObjectDoesNotExistError
from rest_framework import generics, filters, status
from rest_framework.exceptions import ParseError, ValidationError
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.parsers import MultiPartParser
@ -14,7 +14,7 @@ from rest_framework_csv.renderers import CSVRenderer
from .filters import DocumentFilter
from .models import Project, Label, Document
from .permissions import IsAdminUserAndWriteOnly, IsProjectUser, IsOwnAnnotation
from .permissions import IsProjectAdmin, IsAnnotator, IsAnnotationApprover, IsAnnotatorAndCreator, IsOwnAnnotation
from .serializers import ProjectSerializer, LabelSerializer, DocumentSerializer, UserSerializer
from .serializers import ProjectPolymorphicSerializer
from .utils import CSVParser, ExcelParser, JSONParser, PlainTextParser, CoNLLParser, iterable_to_io
@ -42,7 +42,7 @@ class Features(APIView):
class ProjectList(generics.ListCreateAPIView):
serializer_class = ProjectPolymorphicSerializer
pagination_class = None
permission_classes = (IsAuthenticated, IsAdminUserAndWriteOnly)
permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
def get_queryset(self):
return self.request.user.projects
@ -55,12 +55,12 @@ class ProjectDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
lookup_url_kwarg = 'project_id'
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUserAndWriteOnly)
permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
class StatisticsAPI(APIView):
pagination_class = None
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUserAndWriteOnly)
permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
def get(self, request, *args, **kwargs):
p = get_object_or_404(Project, pk=self.kwargs['project_id'])
@ -77,7 +77,7 @@ class StatisticsAPI(APIView):
annotation_class = project.get_annotation_class()
total = docs.count()
done = annotation_class.objects.filter(document_id__in=docs.all(),
user_id=self.request.user).\
user_id=self.request.user).\
aggregate(Count('document', distinct=True))['document__count']
remaining = total - done
return {'total': total, 'remaining': remaining}
@ -88,7 +88,7 @@ class StatisticsAPI(APIView):
class ApproveLabelsAPI(APIView):
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUser)
permission_classes = (IsAuthenticated, IsAnnotationApprover)
def post(self, request, *args, **kwargs):
approved = self.request.data.get('approved', True)
@ -101,7 +101,7 @@ class ApproveLabelsAPI(APIView):
class LabelList(generics.ListCreateAPIView):
serializer_class = LabelSerializer
pagination_class = None
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUserAndWriteOnly)
permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
def get_queryset(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
@ -116,7 +116,7 @@ class LabelDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Label.objects.all()
serializer_class = LabelSerializer
lookup_url_kwarg = 'label_id'
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUserAndWriteOnly)
permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
class DocumentList(generics.ListCreateAPIView):
@ -126,13 +126,12 @@ class DocumentList(generics.ListCreateAPIView):
ordering_fields = ('created_at', 'updated_at', 'doc_annotations__updated_at',
'seq_annotations__updated_at', 'seq2seq_annotations__updated_at')
filter_class = DocumentFilter
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUserAndWriteOnly)
permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
def get_queryset(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
queryset = project.documents
if project.randomize_document_order:
queryset = queryset.annotate(sort_id=F('id') % self.request.user.id).order_by('sort_id')
@ -147,12 +146,12 @@ class DocumentDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
lookup_url_kwarg = 'doc_id'
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUserAndWriteOnly)
permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
class AnnotationList(generics.ListCreateAPIView):
pagination_class = None
permission_classes = (IsAuthenticated, IsProjectUser)
permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
def get_serializer_class(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
@ -177,10 +176,17 @@ class AnnotationList(generics.ListCreateAPIView):
doc = get_object_or_404(Document, pk=self.kwargs['doc_id'])
serializer.save(document=doc, user=self.request.user)
def get_permissions(self):
if self.request.method in ('POST'):
self.permission_classes = (IsAnnotatorAndCreator,)
else:
self.permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
return super(AnnotationList, self).get_permissions()
class AnnotationDetail(generics.RetrieveUpdateDestroyAPIView):
lookup_url_kwarg = 'annotation_id'
permission_classes = (IsAuthenticated, IsProjectUser, IsOwnAnnotation)
permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover) and IsOwnAnnotation,)
def get_serializer_class(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
@ -196,7 +202,7 @@ class AnnotationDetail(generics.RetrieveUpdateDestroyAPIView):
class TextUploadAPI(APIView):
parser_classes = (MultiPartParser,)
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUser)
permission_classes = (IsAuthenticated, IsProjectAdmin)
def post(self, request, *args, **kwargs):
if 'file' not in request.data:
@ -287,7 +293,8 @@ class CloudUploadAPI(APIView):
class TextDownloadAPI(APIView):
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUser)
permission_classes = (IsAuthenticated, IsProjectAdmin)
renderer_classes = (CSVRenderer, JSONLRenderer)
def get(self, request, *args, **kwargs):

4
app/app/settings.py

@ -175,6 +175,10 @@ SOCIAL_AUTH_PIPELINE = [
'server.social_auth.fetch_azuread_permissions',
]
ROLE_PROJECT_ADMIN = env('ROLE_PROJECT_ADMIN', 'project_admin')
ROLE_ANNOTATOR = env('ROLE_ANNOTATOR', 'annotator')
ROLE_ANNOTATION_APPROVER = env('ROLE_ANNOTATION_APPROVER', 'annotation_approver')
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

33
app/server/management/commands/create_role_mapping.py

@ -0,0 +1,33 @@
from api.models import Project, Role, RoleMapping, User
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
help = 'Non-interactively create a rolemapping'
def add_arguments(self, parser):
parser.add_argument('--rolename', default=None,
help='The name of the role.')
parser.add_argument('--username', default=None,
help='The name of the user.')
parser.add_argument('--projectname', default=None,
help='The name of the project.')
def handle(self, *args, **options):
rolename = options.get('rolename')
username = options.get('username')
projectname = options.get('projectname')
if not rolename or not username or not projectname:
raise CommandError('--rolename --projectname --username are required for the rolemapping')
if rolename and projectname and username:
try:
role = Role.objects.get(name=rolename)
user = User.objects.get(username=username)
project = Project.objects.get(name=projectname)
rolemapping = RoleMapping.objects.create(role_id=role.id, user_id=user.id, project_id=project.id)
except Exception as ex:
self.stderr.write(self.style.ERROR('Error occurred while creating rolemapping "%s"' % ex))
else:
self.stdout.write(self.style.SUCCESS('Rolemapping created successfully "%s"' % rolemapping.id))

23
app/server/management/commands/create_roles.py

@ -0,0 +1,23 @@
from api.models import Role
from django.core.management.base import BaseCommand
from django.db import DatabaseError
from django.conf import settings
class Command(BaseCommand):
help = 'Non-interactively create default roles'
def handle(self, *args, **options):
try:
role_names = [settings.ROLE_PROJECT_ADMIN, settings.ROLE_ANNOTATOR, settings.ROLE_ANNOTATION_APPROVER]
except KeyError as key_error:
self.stderr.write(self.style.ERROR(f'Missing Key: "{key_error}"'))
for role_name in role_names:
role = Role()
role.name = role_name
try:
role.save()
except DatabaseError as db_error:
self.stderr.write(self.style.ERROR(f'Datbase Error: "{db_error}"'))
else:
self.stdout.write(self.style.SUCCESS(f'Role created successfully "{role_name}"'))

1
tools/dev-django.sh

@ -19,6 +19,7 @@ echo "Installing dependencies"
echo "Initializing database"
"${venv}/bin/python" "${app}/manage.py" wait_for_db
"${venv}/bin/python" "${app}/manage.py" migrate
"${venv}/bin/python" "${app}/manage.py" create_roles
if [[ -n "${ADMIN_USERNAME}" ]] && [[ -n "${ADMIN_PASSWORD}" ]] && [[ -n "${ADMIN_EMAIL}" ]]; then
"${venv}/bin/python" "${app}/manage.py" create_admin \

1
tools/run.sh

@ -6,6 +6,7 @@ if [[ ! -d "app/staticfiles" ]]; then python app/manage.py collectstatic --noinp
python app/manage.py wait_for_db
python app/manage.py migrate
python app/manage.py create_roles
if [[ -n "${ADMIN_USERNAME}" ]] && [[ -n "${ADMIN_EMAIL}" ]] && [[ -n "${ADMIN_PASSWORD}" ]]; then
python app/manage.py create_admin --noinput --username="${ADMIN_USERNAME}" --email="${ADMIN_EMAIL}" --password="${ADMIN_PASSWORD}"

Loading…
Cancel
Save