diff --git a/app/api/models.py b/app/api/models.py
index a7a88641..90ae90a3 100644
--- a/app/api/models.py
+++ b/app/api/models.py
@@ -4,6 +4,7 @@ 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.conf import settings
from django.contrib.auth.models import User
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.exceptions import ValidationError
@@ -270,15 +271,44 @@ class RoleMapping(models.Model):
@receiver(post_save, sender=RoleMapping)
def add_linked_project(sender, instance, created, **kwargs):
+ if not created:
+ return
userInstance = instance.user
projectInstance = instance.project
- if created and userInstance and projectInstance:
+ if userInstance and projectInstance:
user = User.objects.get(pk=userInstance.pk)
project = Project.objects.get(pk=projectInstance.pk)
user.projects.add(project)
user.save()
+@receiver(post_save)
+def add_superusers_to_project(sender, instance, created, **kwargs):
+ if not created:
+ return
+ if sender not in Project.__subclasses__():
+ return
+ superusers = User.objects.filter(is_superuser=True)
+ admin_role = Role.objects.filter(name=settings.ROLE_PROJECT_ADMIN).first()
+ if superusers and admin_role:
+ RoleMapping.objects.bulk_create(
+ [RoleMapping(role_id=admin_role.id, user_id=superuser.id, project_id=instance.id)
+ for superuser in superusers]
+ )
+
+
+@receiver(post_save, sender=User)
+def add_new_superuser_to_projects(sender, instance, created, **kwargs):
+ if created and instance.is_superuser:
+ admin_role = Role.objects.filter(name=settings.ROLE_PROJECT_ADMIN).first()
+ projects = Project.objects.all()
+ if admin_role and projects:
+ RoleMapping.objects.bulk_create(
+ [RoleMapping(role_id=admin_role.id, user_id=instance.id, project_id=project.id)
+ for project in projects]
+ )
+
+
@receiver(pre_delete, sender=RoleMapping)
def delete_linked_project(sender, instance, using, **kwargs):
userInstance = instance.user
diff --git a/app/api/permissions.py b/app/api/permissions.py
index b17f327f..fb469e63 100644
--- a/app/api/permissions.py
+++ b/app/api/permissions.py
@@ -12,13 +12,6 @@ class ProjectMixin:
return view.kwargs.get('project_id') or request.query_params.get('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()
-
-
class IsAdminUserAndWriteOnly(BasePermission):
def has_permission(self, request, view):
@@ -74,19 +67,20 @@ class IsProjectAdmin(RolePermission):
role_name = settings.ROLE_PROJECT_ADMIN
-class IsAnnotatorAndCreator(RolePermission):
- unsafe_methods_check = False
+class IsAnnotatorAndReadOnly(RolePermission):
role_name = settings.ROLE_ANNOTATOR
-
class IsAnnotator(RolePermission):
+ unsafe_methods_check = False
role_name = settings.ROLE_ANNOTATOR
+class IsAnnotationApproverAndReadOnly(RolePermission):
+ role_name = settings.ROLE_ANNOTATION_APPROVER
class IsAnnotationApprover(RolePermission):
+ unsafe_methods_check = False
role_name = settings.ROLE_ANNOTATION_APPROVER
-
def is_in_role(role_name, user_id, project_id):
return RoleMapping.objects.filter(
user_id=user_id,
diff --git a/app/api/serializers.py b/app/api/serializers.py
index e30a16b2..24747b54 100644
--- a/app/api/serializers.py
+++ b/app/api/serializers.py
@@ -1,4 +1,6 @@
+from django.conf import settings
from django.contrib.auth import get_user_model
+from django.shortcuts import get_object_or_404
from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer
from rest_framework.exceptions import ValidationError
@@ -77,39 +79,56 @@ class DocumentSerializer(serializers.ModelSerializer):
class ProjectSerializer(serializers.ModelSerializer):
+ current_users_role = serializers.SerializerMethodField()
+
+ def get_current_users_role(self, instance):
+ role_abstractor = {
+ "is_project_admin": settings.ROLE_PROJECT_ADMIN,
+ "is_annotator": settings.ROLE_ANNOTATOR,
+ "is_annotation_approver": settings.ROLE_ANNOTATION_APPROVER,
+ }
+ queryset = RoleMapping.objects.values("role_id__name")
+ if queryset:
+ users_role = get_object_or_404(
+ queryset, project=instance.id, user=self.context.get("request").user.id
+ )
+ for key, val in role_abstractor.items():
+ role_abstractor[key] = users_role["role_id__name"] == val
+ return role_abstractor
class Meta:
model = Project
- fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at',
- 'randomize_document_order', 'collaborative_annotation')
- read_only_fields = ('image', 'updated_at')
+ fields = ('id', 'name', 'description', 'guideline', 'users', 'current_users_role', 'project_type', 'image',
+ 'updated_at', 'randomize_document_order', 'collaborative_annotation')
+ read_only_fields = ('image', 'updated_at', 'current_users_role')
-class TextClassificationProjectSerializer(serializers.ModelSerializer):
+class TextClassificationProjectSerializer(ProjectSerializer):
class Meta:
model = TextClassificationProject
- fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at',
- 'randomize_document_order')
- read_only_fields = ('image', 'updated_at', 'users')
+ fields = ('id', 'name', 'description', 'guideline', 'users', 'current_users_role', 'project_type', 'image',
+ 'updated_at', 'randomize_document_order')
+ read_only_fields = ('image', 'updated_at', 'users', 'current_users_role')
-class SequenceLabelingProjectSerializer(serializers.ModelSerializer):
+class SequenceLabelingProjectSerializer(ProjectSerializer):
+
class Meta:
model = SequenceLabelingProject
- fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at',
- 'randomize_document_order')
- read_only_fields = ('image', 'updated_at', 'users')
+ fields = ('id', 'name', 'description', 'guideline', 'users', 'current_users_role', 'project_type', 'image',
+ 'updated_at', 'randomize_document_order')
+ read_only_fields = ('image', 'updated_at', 'users', 'current_users_role')
-class Seq2seqProjectSerializer(serializers.ModelSerializer):
+class Seq2seqProjectSerializer(ProjectSerializer):
class Meta:
model = Seq2seqProject
- fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at',
- 'randomize_document_order')
- read_only_fields = ('image', 'updated_at', 'users')
+ fields = ('id', 'name', 'description', 'guideline', 'users', 'current_users_role', 'project_type', 'image',
+ 'updated_at', 'randomize_document_order')
+ read_only_fields = ('image', 'updated_at', 'users', 'current_users_role')
class ProjectPolymorphicSerializer(PolymorphicSerializer):
diff --git a/app/api/tests/test_api.py b/app/api/tests/test_api.py
index d71e9bd0..6f18a16f 100644
--- a/app/api/tests/test_api.py
+++ b/app/api/tests/test_api.py
@@ -38,29 +38,43 @@ class TestProjectListAPI(APITestCase):
cls.main_project_member_pass = 'project_member_pass'
cls.sub_project_member_name = 'sub_project_member_name'
cls.sub_project_member_pass = 'sub_project_member_pass'
+ cls.approver_name = 'approver_name_name'
+ cls.approver_pass = 'approver_pass'
cls.super_user_name = 'super_user_name'
cls.super_user_pass = 'super_user_pass'
+ create_default_roles()
main_project_member = User.objects.create_user(username=cls.main_project_member_name,
password=cls.main_project_member_pass)
sub_project_member = User.objects.create_user(username=cls.sub_project_member_name,
password=cls.sub_project_member_pass)
- # Todo: change super_user to project_admin.
- super_user = User.objects.create_superuser(username=cls.super_user_name,
+ approver = User.objects.create_user(username=cls.approver_name,
+ password=cls.approver_pass)
+ project_admin = 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)
+ assign_user_to_role(project_member=approver, project=cls.main_project,
+ role_name=settings.ROLE_ANNOTATION_APPROVER)
cls.url = reverse(viewname='project_list')
cls.data = {'name': 'example', 'project_type': 'DocumentClassification',
'description': 'example', 'guideline': 'example',
'resourcetype': 'TextClassificationProject'}
cls.num_project = main_project_member.projects.count()
+ def test_returns_main_project_to_approver(self):
+ self.client.login(username=self.approver_name,
+ password=self.approver_pass)
+ response = self.client.get(self.url, format='json')
+ project = response.data[0]
+ num_project = len(response.data)
+ self.assertEqual(num_project, self.num_project)
+ self.assertEqual(project['id'], self.main_project.id)
+
def test_returns_main_project_to_main_project_member(self):
self.client.login(username=self.main_project_member_name,
password=self.main_project_member_pass)
@@ -105,23 +119,25 @@ class TestProjectDetailAPI(APITestCase):
cls.project_member_pass = 'project_member_pass'
cls.non_project_member_name = 'non_project_member_name'
cls.non_project_member_pass = 'non_project_member_pass'
- cls.super_user_name = 'super_user_name'
- cls.super_user_pass = 'super_user_pass'
+ cls.admin_user_name = 'admin_user_name'
+ cls.admin_user_pass = 'admin_user_pass'
+ create_default_roles()
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)
- super_user = User.objects.create_superuser(username=cls.super_user_name,
- password=cls.super_user_pass,
+ project_admin = User.objects.create_superuser(username=cls.admin_user_name,
+ password=cls.admin_user_pass,
email='fizz@buzz.com')
- cls.main_project = mommy.make('TextClassificationProject', users=[cls.project_member, super_user])
+ cls.main_project = mommy.make('TextClassificationProject', users=[cls.project_member, project_admin])
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)
+ assign_user_to_role(project_member=project_admin, project=cls.main_project,
+ role_name=settings.ROLE_PROJECT_ADMIN)
def test_returns_main_project_detail_to_main_project_member(self):
self.client.login(username=self.project_member_name,
@@ -135,9 +151,9 @@ class TestProjectDetailAPI(APITestCase):
response = self.client.get(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
- def test_allows_superuser_to_update_project(self):
- self.client.login(username=self.super_user_name,
- password=self.super_user_pass)
+ def test_allows_admin_to_update_project(self):
+ self.client.login(username=self.admin_user_name,
+ password=self.admin_user_pass)
response = self.client.patch(self.url, format='json', data=self.data)
self.assertEqual(response.data['description'], self.data['description'])
@@ -147,9 +163,9 @@ class TestProjectDetailAPI(APITestCase):
response = self.client.patch(self.url, format='json', data=self.data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
- def test_allows_superuser_to_delete_project(self):
- self.client.login(username=self.super_user_name,
- password=self.super_user_pass)
+ def test_allows_admin_to_delete_project(self):
+ self.client.login(username=self.admin_user_name,
+ password=self.admin_user_pass)
response = self.client.delete(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@@ -172,26 +188,25 @@ class TestLabelListAPI(APITestCase):
cls.project_member_pass = 'project_member_pass'
cls.non_project_member_name = 'non_project_member_name'
cls.non_project_member_pass = 'non_project_member_pass'
- cls.super_user_name = 'super_user_name'
- cls.super_user_pass = 'super_user_pass'
+ cls.admin_user_name = 'admin_user_name'
+ cls.admin_user_pass = 'admin_user_pass'
+ create_default_roles()
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,
+ project_admin = User.objects.create_superuser(username=cls.admin_user_name,
+ password=cls.admin_user_pass,
email='fizz@buzz.com')
- cls.main_project = mommy.make('Project', users=[cls.project_member, super_user])
+ cls.main_project = mommy.make('Project', users=[cls.project_member, project_admin])
cls.main_project_label = mommy.make('Label', project=cls.main_project)
sub_project = mommy.make('Project', users=[non_project_member])
- other_project = mommy.make('Project', users=[super_user])
+ other_project = mommy.make('Project', users=[project_admin])
mommy.make('Label', project=sub_project)
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)
@@ -216,15 +231,15 @@ class TestLabelListAPI(APITestCase):
self.assertEqual(num_labels, len(self.main_project.labels.all()))
self.assertEqual(label['id'], self.main_project_label.id)
- def test_allows_superuser_to_create_label(self):
- self.client.login(username=self.super_user_name,
- password=self.super_user_pass)
+ def test_allows_admin_to_create_label(self):
+ self.client.login(username=self.admin_user_name,
+ password=self.admin_user_pass)
response = self.client.post(self.url, format='json', data=self.data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_can_create_multiple_labels_without_shortcut_key(self):
- self.client.login(username=self.super_user_name,
- password=self.super_user_pass)
+ self.client.login(username=self.admin_user_name,
+ password=self.admin_user_pass)
labels = [
{'text': 'Ruby', 'prefix_key': None, 'suffix_key': None},
{'text': 'PHP', 'prefix_key': None, 'suffix_key': None}
@@ -234,8 +249,8 @@ class TestLabelListAPI(APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_can_create_same_label_in_multiple_projects(self):
- self.client.login(username=self.super_user_name,
- password=self.super_user_pass)
+ self.client.login(username=self.admin_user_name,
+ password=self.admin_user_pass)
label = {'text': 'LOC', 'prefix_key': None, 'suffix_key': 'l'}
response = self.client.post(self.url, format='json', data=label)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@@ -283,6 +298,7 @@ class TestLabelDetailAPI(APITestCase):
cls.non_project_member_pass = 'non_project_member_pass'
cls.super_user_name = 'super_user_name'
cls.super_user_pass = 'super_user_pass'
+ create_default_roles()
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,
@@ -350,6 +366,7 @@ class TestDocumentListAPI(APITestCase):
cls.non_project_member_pass = 'non_project_member_pass'
cls.super_user_name = 'super_user_name'
cls.super_user_pass = 'super_user_pass'
+ create_default_roles()
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,
@@ -370,7 +387,6 @@ 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,
@@ -448,6 +464,7 @@ class TestDocumentDetailAPI(APITestCase):
cls.non_project_member_pass = 'non_project_member_pass'
cls.super_user_name = 'super_user_name'
cls.super_user_pass = 'super_user_pass'
+ create_default_roles()
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,
@@ -460,7 +477,6 @@ 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)
@@ -508,34 +524,49 @@ class TestDocumentDetailAPI(APITestCase):
class TestApproveLabelsAPI(APITestCase):
@classmethod
def setUpTestData(cls):
- cls.project_member_name = 'project_member_name'
- cls.project_member_pass = '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)
- # 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')
- project = mommy.make('TextClassificationProject', users=[project_member, super_user])
+ cls.annotator_name = 'annotator_name'
+ cls.annotator_pass = 'annotator_pass'
+ cls.approver_name = 'approver_name_name'
+ cls.approver_pass = 'approver_pass'
+ cls.project_admin_name = 'project_admin_name'
+ cls.project_admin_pass = 'project_admin_pass'
+ annotator = User.objects.create_user(username=cls.annotator_name,
+ password=cls.annotator_pass)
+ approver = User.objects.create_user(username=cls.approver_name,
+ password=cls.approver_pass)
+ project_admin = User.objects.create_user(username=cls.project_admin_name,
+ password=cls.project_admin_pass)
+ project = mommy.make('TextClassificationProject', users=[annotator, approver, project_admin])
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,
+ assign_user_to_role(project_member=annotator, project=project,
role_name=settings.ROLE_ANNOTATOR)
+ assign_user_to_role(project_member=approver, project=project,
+ role_name=settings.ROLE_ANNOTATION_APPROVER)
+ assign_user_to_role(project_member=project_admin, project=project,
+ role_name=settings.ROLE_PROJECT_ADMIN)
- def test_allows_superuser_to_approve_and_disapprove_labels(self):
- self.client.login(username=self.super_user_name, password=self.super_user_pass)
+ def test_allow_project_admin_to_approve_and_disapprove_labels(self):
+ self.client.login(username=self.project_admin_name, password=self.project_admin_pass)
response = self.client.post(self.url, format='json', data={'approved': True})
- self.assertEqual(response.data['annotation_approver'], self.super_user_name)
+ self.assertEqual(response.data['annotation_approver'], self.project_admin_name)
+
+ response = self.client.post(self.url, format='json', data={'approved': False})
+ self.assertIsNone(response.data['annotation_approver'])
+
+ def test_allow_approver_to_approve_and_disapprove_labels(self):
+ self.client.login(username=self.approver_name, password=self.approver_pass)
+
+ response = self.client.post(self.url, format='json', data={'approved': True})
+ self.assertEqual(response.data['annotation_approver'], self.approver_name)
response = self.client.post(self.url, format='json', data={'approved': False})
self.assertIsNone(response.data['annotation_approver'])
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)
+ self.client.login(username=self.annotator_name, password=self.annotator_pass)
response = self.client.post(self.url, format='json', data={'approved': True})
@@ -556,6 +587,7 @@ class TestAnnotationListAPI(APITestCase):
cls.another_project_member_pass = 'another_project_member_pass'
cls.non_project_member_name = 'non_project_member_name'
cls.non_project_member_pass = 'non_project_member_pass'
+ create_default_roles()
project_member = User.objects.create_user(username=cls.project_member_name,
password=cls.project_member_pass)
another_project_member = User.objects.create_user(username=cls.another_project_member_name,
@@ -581,8 +613,6 @@ class TestAnnotationListAPI(APITestCase):
document=main_project_doc,
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)
@@ -651,6 +681,7 @@ class TestAnnotationDetailAPI(APITestCase):
cls.another_project_member_pass = 'another_project_member_pass'
cls.non_project_member_name = 'non_project_member_name'
cls.non_project_member_pass = 'non_project_member_pass'
+ create_default_roles()
project_member = User.objects.create_user(username=cls.project_member_name,
password=cls.project_member_pass)
another_project_member = User.objects.create_user(username=cls.another_project_member_name,
@@ -677,7 +708,6 @@ 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)
@@ -748,6 +778,7 @@ class TestSearch(APITestCase):
cls.project_member_pass = 'project_member_pass'
cls.non_project_member_name = 'non_project_member_name'
cls.non_project_member_pass = 'non_project_member_pass'
+ create_default_roles()
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,
@@ -766,7 +797,6 @@ 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)
@@ -825,6 +855,7 @@ class TestFilter(APITestCase):
def setUpTestData(cls):
cls.project_member_name = 'project_member_name'
cls.project_member_pass = 'project_member_pass'
+ create_default_roles()
project_member = User.objects.create_user(username=cls.project_member_name,
password=cls.project_member_pass)
cls.main_project = mommy.make('SequenceLabelingProject', users=[project_member])
@@ -837,7 +868,6 @@ 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)
@@ -882,6 +912,7 @@ class TestUploader(APITestCase):
cls.super_user_name = 'super_user_name'
cls.super_user_pass = 'super_user_pass'
# Todo: change super_user to project_admin.
+ create_default_roles()
super_user = User.objects.create_superuser(username=cls.super_user_name,
password=cls.super_user_pass,
email='fizz@buzz.com')
@@ -890,7 +921,6 @@ 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,
@@ -1119,7 +1149,7 @@ class TestFeatures(APITestCase):
def setUpTestData(cls):
cls.user_name = 'user_name'
cls.user_pass = 'user_pass'
-
+ create_default_roles()
cls.user = User.objects.create_user(username=cls.user_name, password=cls.user_pass, email='fizz@buzz.com')
def setUp(self):
@@ -1187,6 +1217,7 @@ class TestDownloader(APITestCase):
cls.super_user_name = 'super_user_name'
cls.super_user_pass = 'super_user_pass'
# Todo: change super_user to project_admin.
+ create_default_roles()
super_user = User.objects.create_superuser(username=cls.super_user_name,
password=cls.super_user_pass,
email='fizz@buzz.com')
@@ -1259,6 +1290,7 @@ class TestStatisticsAPI(APITestCase):
def setUpTestData(cls):
cls.super_user_name = 'super_user_name'
cls.super_user_pass = 'super_user_pass'
+ create_default_roles()
# Todo: change super_user to project_admin.
super_user = User.objects.create_superuser(username=cls.super_user_name,
password=cls.super_user_pass,
@@ -1301,6 +1333,7 @@ class TestUserAPI(APITestCase):
def setUpTestData(cls):
cls.super_user_name = 'super_user_name'
cls.super_user_pass = 'super_user_pass'
+ create_default_roles()
User.objects.create_superuser(username=cls.super_user_name,
password=cls.super_user_pass,
email='fizz@buzz.com')
@@ -1321,11 +1354,12 @@ class TestRoleAPI(APITestCase):
cls.user_pass = 'user_pass'
cls.project_admin_name = 'project_admin_name'
cls.project_admin_pass = 'project_admin_pass'
+ create_default_roles()
cls.user = User.objects.create_user(username=cls.user_name,
password=cls.user_pass)
- project_admin = User.objects.create_superuser(username=cls.project_admin_name,
- password=cls.project_admin_pass,
- email='fizz@buzz.com')
+ User.objects.create_superuser(username=cls.project_admin_name,
+ password=cls.project_admin_pass,
+ email='fizz@buzz.com')
cls.url = reverse(viewname='roles')
def test_cannot_create_multiple_roles_with_same_name(self):
@@ -1370,19 +1404,19 @@ class TestRoleMappingListAPI(APITestCase):
cls.second_project_member_pass = 'second_project_member_pass'
cls.project_admin_name = 'project_admin_name'
cls.project_admin_pass = 'project_admin_pass'
+ create_default_roles()
project_member = User.objects.create_user(username=cls.project_member_name,
password=cls.project_member_pass)
cls.second_project_member = User.objects.create_user(username=cls.second_project_member_name,
password=cls.second_project_member_pass)
- project_admin = User.objects.create_superuser(username=cls.project_admin_name,
- password=cls.project_admin_pass,
- email='fizz@buzz.com')
+ project_admin = User.objects.create_user(username=cls.project_admin_name,
+ password=cls.project_admin_pass)
cls.main_project = mommy.make('Project', users=[project_member, project_admin, cls.second_project_member])
cls.other_project = mommy.make('Project', users=[cls.second_project_member, project_admin])
-
- cls.role = mommy.make('Role', name=settings.ROLE_PROJECT_ADMIN)
- rolemapping = mommy.make('RoleMapping', role=cls.role, project=cls.main_project, user=project_admin)
- cls.data = {'user': project_member.id, 'role': cls.role.id, 'project': cls.main_project.id}
+ cls.admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
+ cls.role = mommy.make('Role', name='otherrole')
+ rolemapping = mommy.make('RoleMapping', role=cls.admin_role, project=cls.main_project, user=project_admin)
+ cls.data = {'user': project_member.id, 'role': cls.admin_role.id, 'project': cls.main_project.id}
cls.other_url = reverse(viewname='rolemapping_list', args=[cls.other_project.id])
cls.url = reverse(viewname='rolemapping_list', args=[cls.main_project.id])
@@ -1410,18 +1444,6 @@ class TestRoleMappingListAPI(APITestCase):
response = self.client.get(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
- def test_can_create_same_mapping_in_multiple_projects(self):
- self.client.login(username=self.project_admin_name,
- password=self.project_admin_pass)
- mapping = [
- {'user': self.second_project_member.id, 'role': self.role.id, 'project': self.main_project.id},
- {'user': self.second_project_member.id, 'role': self.role.id, 'project': self.other_project.id}
- ]
- response = self.client.post(self.url, format='json', data=mapping[0])
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- response = self.client.post(self.other_url, format='json', data=mapping[1])
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
class TestRoleMappingDetailAPI(APITestCase):
@@ -1433,19 +1455,19 @@ class TestRoleMappingDetailAPI(APITestCase):
cls.project_member_pass = 'project_member_pass'
cls.non_project_member_name = 'non_project_member_name'
cls.non_project_member_pass = 'non_project_member_pass'
- project_admin = User.objects.create_superuser(username=cls.project_admin_name,
- password=cls.project_admin_pass,
- email='fizz@buzz.com')
+ create_default_roles()
+ project_admin = User.objects.create_user(username=cls.project_admin_name,
+ password=cls.project_admin_pass)
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)
project = mommy.make('Project', users=[project_admin, project_member])
- role = mommy.make('Role', name=settings.ROLE_PROJECT_ADMIN)
- change_role = mommy.make('Role', name=settings.ROLE_ANNOTATOR)
- cls.rolemapping = mommy.make('RoleMapping', role=role, project=project, user=project_admin)
+ admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
+ annotator_role = Role.objects.get(name=settings.ROLE_ANNOTATOR)
+ cls.rolemapping = mommy.make('RoleMapping', role=admin_role, project=project, user=project_admin)
cls.url = reverse(viewname='rolemapping_detail', args=[project.id, cls.rolemapping.id])
- cls.data = {'role': change_role.id }
+ cls.data = {'role': annotator_role.id }
def test_returns_rolemapping_to_project_member(self):
self.client.login(username=self.project_admin_name,
diff --git a/app/api/views.py b/app/api/views.py
index 4fa62956..3392f2c0 100644
--- a/app/api/views.py
+++ b/app/api/views.py
@@ -15,7 +15,7 @@ from rest_framework_csv.renderers import CSVRenderer
from .filters import DocumentFilter
from .models import Project, Label, Document, RoleMapping, Role
-from .permissions import IsProjectAdmin, IsAnnotator, IsAnnotationApprover, IsAnnotatorAndCreator, IsOwnAnnotation
+from .permissions import IsProjectAdmin, IsAnnotatorAndReadOnly, IsAnnotator, IsAnnotationApproverAndReadOnly, IsOwnAnnotation, IsAnnotationApprover
from .serializers import ProjectSerializer, LabelSerializer, DocumentSerializer, UserSerializer
from .serializers import ProjectPolymorphicSerializer, RoleMappingSerializer, RoleSerializer
from .utils import CSVParser, ExcelParser, JSONParser, PlainTextParser, CoNLLParser, iterable_to_io
@@ -23,6 +23,8 @@ from .serializers import ProjectPolymorphicSerializer, RoleMappingSerializer, Ro
from .utils import JSONLRenderer
from .utils import JSONPainter, CSVPainter
+IsInProjectReadOnlyOrAdmin = (IsAnnotatorAndReadOnly | IsAnnotationApproverAndReadOnly | IsProjectAdmin)
+IsInProjectOrAdmin = (IsAnnotator | IsAnnotationApprover | IsProjectAdmin)
class Me(APIView):
permission_classes = (IsAuthenticated,)
@@ -44,7 +46,7 @@ class Features(APIView):
class ProjectList(generics.ListCreateAPIView):
serializer_class = ProjectPolymorphicSerializer
pagination_class = None
- permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
+ permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
def get_queryset(self):
return self.request.user.projects
@@ -57,12 +59,12 @@ class ProjectDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
lookup_url_kwarg = 'project_id'
- permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
+ permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
class StatisticsAPI(APIView):
pagination_class = None
- permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
+ permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
def get(self, request, *args, **kwargs):
p = get_object_or_404(Project, pk=self.kwargs['project_id'])
@@ -90,7 +92,7 @@ class StatisticsAPI(APIView):
class ApproveLabelsAPI(APIView):
- permission_classes = (IsAuthenticated, IsAnnotationApprover)
+ permission_classes = [IsAuthenticated & (IsAnnotationApprover | IsProjectAdmin)]
def post(self, request, *args, **kwargs):
approved = self.request.data.get('approved', True)
@@ -103,7 +105,7 @@ class ApproveLabelsAPI(APIView):
class LabelList(generics.ListCreateAPIView):
serializer_class = LabelSerializer
pagination_class = None
- permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
+ permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
def get_queryset(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
@@ -118,7 +120,7 @@ class LabelDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Label.objects.all()
serializer_class = LabelSerializer
lookup_url_kwarg = 'label_id'
- permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
+ permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
class DocumentList(generics.ListCreateAPIView):
@@ -128,7 +130,7 @@ 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 and (IsAnnotator or IsAnnotationApprover),)
+ permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
def get_queryset(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
@@ -148,12 +150,12 @@ class DocumentDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
lookup_url_kwarg = 'doc_id'
- permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
+ permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin]
class AnnotationList(generics.ListCreateAPIView):
pagination_class = None
- permission_classes = (IsAuthenticated and (IsAnnotator or IsAnnotationApprover),)
+ permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
def get_serializer_class(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
@@ -178,17 +180,10 @@ 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 and (IsAnnotator or IsAnnotationApprover) and IsOwnAnnotation,)
+ permission_classes = [IsAuthenticated & (((IsAnnotator | IsAnnotationApprover) & IsOwnAnnotation) | IsProjectAdmin)]
def get_serializer_class(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
@@ -204,7 +199,7 @@ class AnnotationDetail(generics.RetrieveUpdateDestroyAPIView):
class TextUploadAPI(APIView):
parser_classes = (MultiPartParser,)
- permission_classes = (IsAuthenticated, IsProjectAdmin)
+ permission_classes = [IsAuthenticated & IsProjectAdmin]
def post(self, request, *args, **kwargs):
if 'file' not in request.data:
@@ -295,7 +290,7 @@ class CloudUploadAPI(APIView):
class TextDownloadAPI(APIView):
- permission_classes = (IsAuthenticated, IsProjectAdmin)
+ permission_classes = TextUploadAPI.permission_classes
renderer_classes = (CSVRenderer, JSONLRenderer)
@@ -324,7 +319,7 @@ class TextDownloadAPI(APIView):
class Users(APIView):
- permission_classes = (IsAuthenticated, IsProjectAdmin)
+ permission_classes = [IsAuthenticated & IsProjectAdmin]
def get(self, request, *args, **kwargs):
queryset = User.objects.all()
@@ -335,14 +330,14 @@ class Users(APIView):
class Roles(generics.ListCreateAPIView):
serializer_class = RoleSerializer
pagination_class = None
- permission_classes = (IsAuthenticated, IsProjectAdmin)
+ permission_classes = [IsAuthenticated & IsProjectAdmin]
queryset = Role.objects.all()
class RoleMappingList(generics.ListCreateAPIView):
serializer_class = RoleMappingSerializer
pagination_class = None
- permission_classes = (IsAuthenticated, IsProjectAdmin)
+ permission_classes = [IsAuthenticated & IsProjectAdmin]
def get_queryset(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
@@ -357,4 +352,4 @@ class RoleMappingDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = RoleMapping.objects.all()
serializer_class = RoleMappingSerializer
lookup_url_kwarg = 'rolemapping_id'
- permission_classes = (IsAuthenticated, IsProjectAdmin)
+ permission_classes = [IsAuthenticated & IsProjectAdmin]
diff --git a/app/server/static/components/annotation.pug b/app/server/static/components/annotation.pug
index 28044eee..91b90a7a 100644
--- a/app/server/static/components/annotation.pug
+++ b/app/server/static/components/annotation.pug
@@ -114,7 +114,7 @@ div.columns(v-cloak="")
div.column.is-1.has-text-right
a.button.tooltip.is-tooltip-bottom(
- v-if="isSuperuser"
+ v-if="isAnnotationApprover"
v-on:click="approveDocumentAnnotations"
v-bind:data-tooltip="documentAnnotationsApprovalTooltip"
)
diff --git a/app/server/static/components/annotationMixin.js b/app/server/static/components/annotationMixin.js
index 7a9975a4..03b9ab67 100644
--- a/app/server/static/components/annotationMixin.js
+++ b/app/server/static/components/annotationMixin.js
@@ -1,7 +1,7 @@
import * as marked from 'marked';
import VueJsonPretty from 'vue-json-pretty';
import isEmpty from 'lodash.isempty';
-import HTTP, { defaultHttpClient } from './http';
+import HTTP from './http';
import Preview from './preview.vue';
const getOffsetFromUrl = (url) => {
@@ -91,7 +91,7 @@ export default {
prevLimit: 0,
paginationPages: 0,
paginationPage: 0,
- isSuperuser: false,
+ isAnnotationApprover: false,
isMetadataActive: false,
isAnnotationGuidelineActive: false,
};
@@ -253,9 +253,8 @@ export default {
});
HTTP.get().then((response) => {
this.guideline = response.data.guideline;
- });
- defaultHttpClient.get('/v1/me').then((response) => {
- this.isSuperuser = response.data.is_superuser;
+ const roles = response.data.current_users_role;
+ this.isAnnotationApprover = roles.is_annotation_approver || roles.is_project_admin;
});
this.submit();
},
diff --git a/app/server/static/components/projects.vue b/app/server/static/components/projects.vue
index f8845e9a..a692ce4a 100644
--- a/app/server/static/components/projects.vue
+++ b/app/server/static/components/projects.vue
@@ -122,10 +122,10 @@
td.is-vertical
span.tag.is-normal {{ project.project_type }}
- td.is-vertical(v-if="isSuperuser")
+ td.is-vertical(v-if="isProjectAdmin.get(project.id)")
a(v-bind:href="'/projects/' + project.id + '/docs'") Edit
- td.is-vertical(v-if="isSuperuser")
+ td.is-vertical(v-if="isProjectAdmin.get(project.id)")
a.has-text-danger(v-on:click="setProject(project)") Delete
@@ -152,6 +152,7 @@ export default {
isSuperuser: false,
randomizeDocumentOrder: false,
collaborativeAnnotation: false,
+ isProjectAdmin: null,
}),
computed: {
@@ -168,6 +169,10 @@ export default {
this.items = projects.data;
this.username = me.data.username;
this.isSuperuser = me.data.is_superuser;
+ this.isProjectAdmin = new Map(this.items.map((project) => {
+ const isProjectAdmin = project.current_users_role.is_project_admin;
+ return [project.id, isProjectAdmin];
+ }));
});
},
diff --git a/app/server/templates/annotation.html b/app/server/templates/annotation.html
index cd071d1a..cd3a2752 100644
--- a/app/server/templates/annotation.html
+++ b/app/server/templates/annotation.html
@@ -5,7 +5,7 @@
{% endblock %}
{% block navigation %}
-{% if user.is_superuser and 'project_id' in view.kwargs %}
+{% if is_project_admin and 'project_id' in view.kwargs %}
diff --git a/app/server/views.py b/app/server/views.py
index a729bde1..e8be32ba 100644
--- a/app/server/views.py
+++ b/app/server/views.py
@@ -9,7 +9,7 @@ from django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from api.permissions import SuperUserMixin
-from api.models import Project
+from api.models import Project, RoleMapping
from app import settings
logger = logging.getLogger(__name__)
@@ -25,6 +25,11 @@ class ProjectView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
+ context['is_project_admin'] = RoleMapping.objects.filter(
+ role_id__name=settings.ROLE_PROJECT_ADMIN,
+ project=project.id,
+ user=self.request.user.id
+ ).exists()
context['bundle_name'] = project.get_bundle_name()
return context
diff --git a/requirements.txt b/requirements.txt
index 6625b265..66a5f372 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,7 +11,7 @@ django-widget-tweaks==1.4.2
django-polymorphic==2.0.3
django-pyodbc-azure==2.1.0.0
django-rest-polymorphic==0.1.8
-djangorestframework==3.8.2
+djangorestframework==3.10
djangorestframework-csv==2.1.0
djangorestframework-filters==0.10.2
environs==4.1.0