Browse Source

Replace 'isSuperUser' with specific roles required for frontend

pull/333/head
margaretmeehan 6 years ago
parent
commit
8e21d5f23d
11 changed files with 215 additions and 146 deletions
  1. 32
      app/api/models.py
  2. 16
      app/api/permissions.py
  3. 49
      app/api/serializers.py
  4. 190
      app/api/tests/test_api.py
  5. 43
      app/api/views.py
  6. 2
      app/server/static/components/annotation.pug
  7. 9
      app/server/static/components/annotationMixin.js
  8. 9
      app/server/static/components/projects.vue
  9. 2
      app/server/templates/annotation.html
  10. 7
      app/server/views.py
  11. 2
      requirements.txt

32
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

16
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,

49
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):

190
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,

43
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]

2
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"
)

9
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();
},

9
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
</template>
@ -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];
}));
});
},

2
app/server/templates/annotation.html

@ -5,7 +5,7 @@
<link rel="stylesheet" href="{% static 'assets/css/annotation.css' %}">
{% endblock %}
{% block navigation %}
{% if user.is_superuser and 'project_id' in view.kwargs %}
{% if is_project_admin and 'project_id' in view.kwargs %}
<a class="navbar-item" href="{% url 'dataset' view.kwargs.project_id %}">
<span class="icon">
<i class="fas fa-edit"></i>

7
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

2
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

Loading…
Cancel
Save