Browse Source

Move Member model to members

pull/1627/head
Hironsan 3 years ago
parent
commit
72da8884ac
39 changed files with 401 additions and 313 deletions
  1. 2
      Pipfile
  2. 3
      backend/api/tests/api/utils.py
  3. 2
      backend/api/views/annotation.py
  4. 2
      backend/api/views/annotation_relations.py
  5. 2
      backend/api/views/auto_labeling.py
  6. 2
      backend/api/views/comment.py
  7. 2
      backend/api/views/example.py
  8. 2
      backend/api/views/example_state.py
  9. 2
      backend/api/views/export_dataset.py
  10. 2
      backend/api/views/import_dataset.py
  11. 2
      backend/api/views/label.py
  12. 2
      backend/api/views/project.py
  13. 2
      backend/api/views/relation_types.py
  14. 2
      backend/api/views/statistics.py
  15. 2
      backend/api/views/tag.py
  16. 2
      backend/api/views/tasks/base.py
  17. 2
      backend/api/views/user.py
  18. 1
      backend/app/settings.py
  19. 1
      backend/app/urls.py
  20. 11
      backend/members/admin.py
  21. 5
      backend/members/apps.py
  22. 0
      backend/members/exceptions.py
  23. 43
      backend/members/migrations/0001_initial.py
  24. 65
      backend/members/models.py
  25. 0
      backend/members/permissions.py
  26. 22
      backend/members/serializers.py
  27. 3
      backend/members/signals.py
  28. 132
      backend/members/tests.py
  29. 16
      backend/members/urls.py
  30. 56
      backend/members/views.py
  31. 9
      backend/roles/admin.py
  32. 5
      backend/roles/apps.py
  33. 3
      backend/roles/management/commands/create_member.py
  34. 26
      backend/roles/migrations/0003_delete_member.py
  35. 63
      backend/roles/models.py
  36. 21
      backend/roles/serializers.py
  37. 129
      backend/roles/tests.py
  38. 12
      backend/roles/urls.py
  39. 56
      backend/roles/views.py

2
Pipfile

@ -60,6 +60,6 @@ python_version = "3.8"
isort = "isort api -c --skip migrations"
flake8 = "flake8 --filename \"*.py\" --extend-exclude \"server,api/migrations,api/views/__init__.py,authentification,api/apps.py\""
wait_for_db = "python manage.py wait_for_db"
test = "python manage.py test api.tests roles.tests"
test = "python manage.py test api.tests roles.tests members.tests"
migrate = "python manage.py migrate"
collectstatic = "python manage.py collectstatic --noinput"

3
backend/api/tests/api/utils.py

@ -8,7 +8,8 @@ from model_mommy import mommy
from rest_framework import status
from rest_framework.test import APITestCase
from roles.models import Member, Role
from members.models import Member
from roles.models import Role
from ...models import (DOCUMENT_CLASSIFICATION, IMAGE_CLASSIFICATION,
INTENT_DETECTION_AND_SLOT_FILLING, SEQ2SEQ,

2
backend/api/views/annotation.py

@ -3,7 +3,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from roles.permissions import IsAnnotationApprover, IsProjectAdmin
from members.permissions import IsAnnotationApprover, IsProjectAdmin
from ..models import Example
from ..serializers import ApproverSerializer

2
backend/api/views/annotation_relations.py

@ -9,7 +9,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from roles.permissions import IsInProjectReadOnlyOrAdmin, IsProjectAdmin
from members.permissions import IsInProjectReadOnlyOrAdmin, IsProjectAdmin
from ..exceptions import AnnotationRelationValidationError
from ..models import AnnotationRelations, Project

2
backend/api/views/auto_labeling.py

@ -16,7 +16,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from roles.permissions import IsInProjectOrAdmin, IsProjectAdmin
from members.permissions import IsInProjectOrAdmin, IsProjectAdmin
from ..exceptions import (AutoLabelingException, AutoLabelingPermissionDenied,
AWSTokenError, SampleDataException,

2
backend/api/views/comment.py

@ -3,7 +3,7 @@ from rest_framework import filters, generics, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from roles.permissions import IsInProjectOrAdmin
from members.permissions import IsInProjectOrAdmin
from ..models import Comment
from ..permissions import IsOwnComment

2
backend/api/views/example.py

@ -7,7 +7,7 @@ from rest_framework import filters, generics, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from roles.permissions import IsInProjectReadOnlyOrAdmin
from members.permissions import IsInProjectReadOnlyOrAdmin
from ..filters import DocumentFilter, ExampleFilter
from ..models import Example, Project

2
backend/api/views/example_state.py

@ -2,7 +2,7 @@ from django.shortcuts import get_object_or_404
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from roles.permissions import IsInProjectOrAdmin
from members.permissions import IsInProjectOrAdmin
from ..models import Example, ExampleState, Project
from ..serializers import ExampleStateSerializer

2
backend/api/views/export_dataset.py

@ -6,7 +6,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from roles.permissions import IsProjectAdmin
from members.permissions import IsProjectAdmin
from ..models import Project
from ..tasks import export_dataset

2
backend/api/views/import_dataset.py

@ -8,7 +8,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from roles.permissions import IsProjectAdmin
from members.permissions import IsProjectAdmin
from ..models import Project
from ..tasks import ingest_data

2
backend/api/views/label.py

@ -11,7 +11,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from roles.permissions import IsInProjectReadOnlyOrAdmin, IsProjectAdmin
from members.permissions import IsInProjectReadOnlyOrAdmin, IsProjectAdmin
from ..exceptions import LabelValidationError
from ..models import CategoryType, Label, Project, SpanType

2
backend/api/views/project.py

@ -3,7 +3,7 @@ from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from roles.permissions import IsInProjectReadOnlyOrAdmin
from members.permissions import IsInProjectReadOnlyOrAdmin
from ..models import Project
from ..permissions import IsStaff

2
backend/api/views/relation_types.py

@ -9,7 +9,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from roles.permissions import IsInProjectReadOnlyOrAdmin, IsProjectAdmin
from members.permissions import IsInProjectReadOnlyOrAdmin, IsProjectAdmin
from ..exceptions import RelationTypesValidationError
from ..models import Project, RelationTypes

2
backend/api/views/statistics.py

@ -6,7 +6,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from roles.permissions import IsInProjectReadOnlyOrAdmin
from members.permissions import IsInProjectReadOnlyOrAdmin
from ..models import (Annotation, Category, CategoryType, Example,
ExampleState, Label, Project, Span, SpanType)

2
backend/api/views/tag.py

@ -2,7 +2,7 @@ from django.shortcuts import get_object_or_404
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from roles.permissions import IsInProjectReadOnlyOrAdmin
from members.permissions import IsInProjectReadOnlyOrAdmin
from ..models import Project, Tag
from ..serializers import TagSerializer

2
backend/api/views/tasks/base.py

@ -6,7 +6,7 @@ from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from roles.permissions import IsInProjectOrAdmin
from members.permissions import IsInProjectOrAdmin
from ...models import Project
from ...permissions import CanEditAnnotation

2
backend/api/views/user.py

@ -5,7 +5,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from roles.permissions import IsProjectAdmin
from members.permissions import IsProjectAdmin
from ..serializers import UserSerializer

1
backend/app/settings.py

@ -53,6 +53,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'api.apps.ApiConfig',
'roles.apps.RolesConfig',
'members.apps.MembersConfig',
'rest_framework',
'rest_framework.authtoken',
'django_filters',

1
backend/app/urls.py

@ -42,6 +42,7 @@ urlpatterns += [
path('api-auth/', include('rest_framework.urls')),
path('v1/', include('api.urls')),
path('v1/', include('roles.urls')),
path('v1/projects/<int:project_id>/', include('members.urls')),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
re_path('', TemplateView.as_view(template_name='index.html')),
]

11
backend/members/admin.py

@ -1,3 +1,12 @@
from django.contrib import admin
# Register your models here.
from .models import Member
class MemberAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'project', )
ordering = ('user',)
search_fields = ('user__username',)
admin.site.register(Member, MemberAdmin)

5
backend/members/apps.py

@ -1,6 +1,11 @@
import importlib
from django.apps import AppConfig
class MembersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'members'
def ready(self):
importlib.import_module('members.signals')

backend/roles/exceptions.py → backend/members/exceptions.py

43
backend/members/migrations/0001_initial.py

@ -0,0 +1,43 @@
# Generated by Django 3.2.8 on 2022-01-13 01:54
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('roles', '0003_delete_member'),
('api', '0028_auto_20220111_0655'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.CreateModel(
name='Member',
fields=[
('id',
models.BigAutoField(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_mappings',
to='api.project')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='roles.role')),
('user',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_mappings',
to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'project')},
},
),
],
database_operations=[]
)
]

65
backend/members/models.py

@ -1,3 +1,66 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models
# Create your models here.
from django.db.models import Manager
from api.models import Project
from roles.models import Role
class MemberManager(Manager):
def can_update(self, project: int, mapping_id: int, new_role: str) -> bool:
"""The project needs at least 1 admin.
Args:
project: The project id.
mapping_id: The mapping id.
new_role: The new role name.
Returns:
Whether the mapping can be updated or not.
"""
queryset = self.filter(
project=project, role__name=settings.ROLE_PROJECT_ADMIN
)
if queryset.count() > 1:
return True
else:
mapping = queryset.first()
if mapping.id == mapping_id and new_role != settings.ROLE_PROJECT_ADMIN:
return False
return True
def has_role(self, project_id: int, user: User, role_name: str):
return self.filter(project=project_id, user=user, role__name=role_name).exists()
class Member(models.Model):
user = models.ForeignKey(
to=User,
on_delete=models.CASCADE,
related_name='role_mappings'
)
project = models.ForeignKey(
to=Project,
on_delete=models.CASCADE,
related_name='role_mappings'
)
role = models.ForeignKey(
to=Role,
on_delete=models.CASCADE
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = MemberManager()
def clean(self):
members = self.objects.exclude(id=self.id)
if members.filter(user=self.user, project=self.project).exists():
message = 'This user is already assigned to a role in this project.'
raise ValidationError(message)
class Meta:
unique_together = ('user', 'project')

backend/roles/permissions.py → backend/members/permissions.py

22
backend/members/serializers.py

@ -0,0 +1,22 @@
from rest_framework import serializers
from .models import Member
class MemberSerializer(serializers.ModelSerializer):
username = serializers.SerializerMethodField()
rolename = serializers.SerializerMethodField()
@classmethod
def get_username(cls, instance):
user = instance.user
return user.username if user else None
@classmethod
def get_rolename(cls, instance):
role = instance.role
return role.name if role else None
class Meta:
model = Member
fields = ('id', 'user', 'role', 'username', 'rolename')

backend/roles/signals.py → backend/members/signals.py

@ -4,7 +4,8 @@ from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import receiver
from api.models import Project
from .models import Role, Member
from roles.models import Role
from .models import Member
@receiver(post_save, sender=Member)

132
backend/members/tests.py

@ -1,3 +1,131 @@
from django.test import TestCase
from django.conf import settings
from rest_framework import status
from rest_framework.reverse import reverse
# Create your tests here.
from roles.models import Role
from .models import Member
from api.tests.api.utils import (CRUDMixin, prepare_project, make_user)
class TestMemberListAPI(CRUDMixin):
def setUp(self):
self.project = prepare_project()
self.non_member = make_user()
admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
self.data = {'user': self.non_member.id, 'role': admin_role.id, 'project': self.project.item.id}
self.url = reverse(viewname='member_list', args=[self.project.item.id])
def test_allows_project_admin_to_get_mappings(self):
self.assert_fetch(self.project.users[0], status.HTTP_200_OK)
def test_denies_non_project_admin_to_get_mappings(self):
for member in self.project.users[1:]:
self.assert_fetch(member, status.HTTP_403_FORBIDDEN)
def test_denies_non_project_member_to_get_mappings(self):
self.assert_fetch(self.non_member, status.HTTP_403_FORBIDDEN)
def test_denies_unauthenticated_user_to_get_mappings(self):
self.assert_fetch(expected=status.HTTP_403_FORBIDDEN)
def test_allows_project_admin_to_create_mapping(self):
self.assert_create(self.project.users[0], status.HTTP_201_CREATED)
def test_denies_non_project_admin_to_create_mapping(self):
for member in self.project.users[1:]:
self.assert_create(member, status.HTTP_403_FORBIDDEN)
def test_denies_non_project_member_to_create_mapping(self):
self.assert_create(self.non_member, status.HTTP_403_FORBIDDEN)
def test_denies_unauthenticated_user_to_create_mapping(self):
self.assert_create(expected=status.HTTP_403_FORBIDDEN)
def assert_bulk_delete(self, user=None, expected=status.HTTP_403_FORBIDDEN):
if user:
self.client.force_login(user)
ids = [item.id for item in self.project.item.role_mappings.all()]
response = self.client.delete(self.url, data={'ids': ids}, format='json')
self.assertEqual(response.status_code, expected)
def test_allows_project_admin_to_bulk_delete(self):
self.assert_bulk_delete(self.project.users[0], status.HTTP_204_NO_CONTENT)
response = self.client.get(self.url)
self.assertEqual(len(response.data), 1)
def test_denies_non_project_admin_to_bulk_delete(self):
for member in self.project.users[1:]:
self.assert_bulk_delete(member, status.HTTP_403_FORBIDDEN)
def test_denies_non_project_member_to_bulk_delete(self):
self.assert_bulk_delete(self.non_member, status.HTTP_403_FORBIDDEN)
def test_denies_unauthenticated_user_to_bulk_delete(self):
self.assert_bulk_delete(expected=status.HTTP_403_FORBIDDEN)
class TestMemberRoleDetailAPI(CRUDMixin):
def setUp(self):
self.project = prepare_project()
self.non_member = make_user()
admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
mapping = Member.objects.get(user=self.project.users[1])
self.url = reverse(viewname='member_detail', args=[self.project.item.id, mapping.id])
self.data = {'role': admin_role.id}
def test_allows_project_admin_to_get_mapping(self):
self.assert_fetch(self.project.users[0], status.HTTP_200_OK)
def test_denies_non_project_admin_to_get_mapping(self):
for member in self.project.users[1:]:
self.assert_fetch(member, status.HTTP_403_FORBIDDEN)
def test_denies_non_project_member_to_get_mapping(self):
self.assert_fetch(self.non_member, status.HTTP_403_FORBIDDEN)
def test_denies_unauthenticated_user_to_get_mapping(self):
self.assert_fetch(expected=status.HTTP_403_FORBIDDEN)
def test_allows_project_admin_to_update_mapping(self):
self.assert_update(self.project.users[0], status.HTTP_200_OK)
def test_denies_non_project_admin_to_update_mapping(self):
for member in self.project.users[1:]:
self.assert_update(member, status.HTTP_403_FORBIDDEN)
def test_denies_non_project_member_to_update_mapping(self):
self.assert_update(self.non_member, status.HTTP_403_FORBIDDEN)
def test_denies_unauthenticated_user_to_update_mapping(self):
self.assert_update(expected=status.HTTP_403_FORBIDDEN)
class TestMemberFilter(CRUDMixin):
def setUp(self):
self.project = prepare_project()
self.url = reverse(viewname='member_list', args=[self.project.item.id])
self.url += f'?user={self.project.users[0].id}'
def test_filter_role_by_user_id(self):
response = self.assert_fetch(self.project.users[0], status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
class TestMemberManager(CRUDMixin):
def setUp(self):
pass
def test_has_role(self):
project = prepare_project()
admin = project.users[0]
expected = [
(settings.ROLE_PROJECT_ADMIN, True),
(settings.ROLE_ANNOTATION_APPROVER, False),
(settings.ROLE_ANNOTATOR, False)
]
for role, expect in expected:
self.assertEqual(Member.objects.has_role(project.item, admin, role), expect)

16
backend/members/urls.py

@ -0,0 +1,16 @@
from django.urls import path
from .views import MemberList, MemberDetail
urlpatterns = [
path(
route='members',
view=MemberList.as_view(),
name='member_list'
),
path(
route='members/<int:member_id>',
view=MemberDetail.as_view(),
name='member_detail'
)
]

56
backend/members/views.py

@ -1,3 +1,55 @@
from django.shortcuts import render
from django.db import IntegrityError
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
# Create your views here.
from .permissions import IsProjectAdmin
from .serializers import MemberSerializer
from .exceptions import RoleAlreadyAssignedException, RoleConstraintException
from .models import Member
class MemberList(generics.ListCreateAPIView):
filter_backends = [DjangoFilterBackend]
filterset_fields = ['user']
queryset = Member.objects.all()
serializer_class = MemberSerializer
pagination_class = None
permission_classes = [IsAuthenticated & IsProjectAdmin]
def filter_queryset(self, queryset):
queryset = queryset.filter(project=self.kwargs['project_id'])
return super().filter_queryset(queryset)
def perform_create(self, serializer):
try:
serializer.save(project_id=self.kwargs['project_id'])
except IntegrityError:
raise RoleAlreadyAssignedException
def delete(self, request, *args, **kwargs):
delete_ids = request.data['ids']
project_id = self.kwargs['project_id']
Member.objects.filter(project=project_id, pk__in=delete_ids)\
.exclude(user=self.request.user)\
.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class MemberDetail(generics.RetrieveUpdateAPIView):
queryset = Member.objects.all()
serializer_class = MemberSerializer
lookup_url_kwarg = 'member_id'
permission_classes = [IsAuthenticated & IsProjectAdmin]
def perform_update(self, serializer):
project_id = self.kwargs['project_id']
member_id = self.kwargs['member_id']
role = serializer.validated_data['role']
if not Member.objects.can_update(project_id, member_id, role.name):
raise RoleConstraintException
try:
super().perform_update(serializer)
except IntegrityError:
raise RoleAlreadyAssignedException

9
backend/roles/admin.py

@ -1,6 +1,6 @@
from django.contrib import admin
from .models import Role, Member
from .models import Role
class RoleAdmin(admin.ModelAdmin):
@ -9,11 +9,4 @@ class RoleAdmin(admin.ModelAdmin):
search_fields = ('name',)
class MemberAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'project', )
ordering = ('user',)
search_fields = ('user__username',)
admin.site.register(Role, RoleAdmin)
admin.site.register(Member, MemberAdmin)

5
backend/roles/apps.py

@ -1,11 +1,6 @@
import importlib
from django.apps import AppConfig
class RolesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'roles'
def ready(self):
importlib.import_module('roles.signals')

3
backend/roles/management/commands/create_member.py

@ -2,7 +2,8 @@ from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import User
from api.models import Project
from ...models import Member, Role
from ...models import Role
from backend.members.models import Member
class Command(BaseCommand):

26
backend/roles/migrations/0003_delete_member.py

@ -0,0 +1,26 @@
# Generated by Django 3.2.8 on 2022-01-13 01:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('roles', '0002_rename_rolemapping_member'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.DeleteModel(
name='Member',
),
],
database_operations=[
migrations.AlterModelTable(
name='Member',
table='members_member',
),
]
)
]

63
backend/roles/models.py

@ -1,10 +1,4 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Manager
from api.models import Project
class Role(models.Model):
@ -15,60 +9,3 @@ class Role(models.Model):
def __str__(self):
return self.name
class MemberManager(Manager):
def can_update(self, project: int, mapping_id: int, new_role: str) -> bool:
"""The project needs at least 1 admin.
Args:
project: The project id.
mapping_id: The mapping id.
new_role: The new role name.
Returns:
Whether the mapping can be updated or not.
"""
queryset = self.filter(
project=project, role__name=settings.ROLE_PROJECT_ADMIN
)
if queryset.count() > 1:
return True
else:
mapping = queryset.first()
if mapping.id == mapping_id and new_role != settings.ROLE_PROJECT_ADMIN:
return False
return True
def has_role(self, project_id: int, user: User, role_name: str):
return self.filter(project=project_id, user=user, role__name=role_name).exists()
class Member(models.Model):
user = models.ForeignKey(
to=User,
on_delete=models.CASCADE,
related_name='role_mappings'
)
project = models.ForeignKey(
to=Project,
on_delete=models.CASCADE,
related_name='role_mappings'
)
role = models.ForeignKey(
to=Role,
on_delete=models.CASCADE
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = MemberManager()
def clean(self):
members = self.objects.exclude(id=self.id)
if members.filter(user=self.user, project=self.project).exists():
message = 'This user is already assigned to a role in this project.'
raise ValidationError(message)
class Meta:
unique_together = ('user', 'project')

21
backend/roles/serializers.py

@ -1,28 +1,9 @@
from rest_framework import serializers
from .models import Role, Member
from .models import Role
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = Role
fields = ('id', 'name')
class MemberSerializer(serializers.ModelSerializer):
username = serializers.SerializerMethodField()
rolename = serializers.SerializerMethodField()
@classmethod
def get_username(cls, instance):
user = instance.user
return user.username if user else None
@classmethod
def get_rolename(cls, instance):
role = instance.role
return role.name if role else None
class Meta:
model = Member
fields = ('id', 'user', 'role', 'username', 'rolename')

129
backend/roles/tests.py

@ -1,10 +1,7 @@
from django.conf import settings
from rest_framework import status
from rest_framework.reverse import reverse
from .models import Role, Member
from api.tests.api.utils import (CRUDMixin, create_default_roles, make_user,
prepare_project)
from api.tests.api.utils import (CRUDMixin, create_default_roles, make_user)
class TestRoleAPI(CRUDMixin):
@ -20,127 +17,3 @@ class TestRoleAPI(CRUDMixin):
def test_disallows_unauthenticated_user_to_get_roles(self):
self.assert_fetch(expected=status.HTTP_403_FORBIDDEN)
class TestMemberListAPI(CRUDMixin):
def setUp(self):
self.project = prepare_project()
self.non_member = make_user()
admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
self.data = {'user': self.non_member.id, 'role': admin_role.id, 'project': self.project.item.id}
self.url = reverse(viewname='member_list', args=[self.project.item.id])
def test_allows_project_admin_to_get_mappings(self):
self.assert_fetch(self.project.users[0], status.HTTP_200_OK)
def test_denies_non_project_admin_to_get_mappings(self):
for member in self.project.users[1:]:
self.assert_fetch(member, status.HTTP_403_FORBIDDEN)
def test_denies_non_project_member_to_get_mappings(self):
self.assert_fetch(self.non_member, status.HTTP_403_FORBIDDEN)
def test_denies_unauthenticated_user_to_get_mappings(self):
self.assert_fetch(expected=status.HTTP_403_FORBIDDEN)
def test_allows_project_admin_to_create_mapping(self):
self.assert_create(self.project.users[0], status.HTTP_201_CREATED)
def test_denies_non_project_admin_to_create_mapping(self):
for member in self.project.users[1:]:
self.assert_create(member, status.HTTP_403_FORBIDDEN)
def test_denies_non_project_member_to_create_mapping(self):
self.assert_create(self.non_member, status.HTTP_403_FORBIDDEN)
def test_denies_unauthenticated_user_to_create_mapping(self):
self.assert_create(expected=status.HTTP_403_FORBIDDEN)
def assert_bulk_delete(self, user=None, expected=status.HTTP_403_FORBIDDEN):
if user:
self.client.force_login(user)
ids = [item.id for item in self.project.item.role_mappings.all()]
response = self.client.delete(self.url, data={'ids': ids}, format='json')
self.assertEqual(response.status_code, expected)
def test_allows_project_admin_to_bulk_delete(self):
self.assert_bulk_delete(self.project.users[0], status.HTTP_204_NO_CONTENT)
response = self.client.get(self.url)
self.assertEqual(len(response.data), 1)
def test_denies_non_project_admin_to_bulk_delete(self):
for member in self.project.users[1:]:
self.assert_bulk_delete(member, status.HTTP_403_FORBIDDEN)
def test_denies_non_project_member_to_bulk_delete(self):
self.assert_bulk_delete(self.non_member, status.HTTP_403_FORBIDDEN)
def test_denies_unauthenticated_user_to_bulk_delete(self):
self.assert_bulk_delete(expected=status.HTTP_403_FORBIDDEN)
class TestMemberRoleDetailAPI(CRUDMixin):
def setUp(self):
self.project = prepare_project()
self.non_member = make_user()
admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
mapping = Member.objects.get(user=self.project.users[1])
self.url = reverse(viewname='member_detail', args=[self.project.item.id, mapping.id])
self.data = {'role': admin_role.id}
def test_allows_project_admin_to_get_mapping(self):
self.assert_fetch(self.project.users[0], status.HTTP_200_OK)
def test_denies_non_project_admin_to_get_mapping(self):
for member in self.project.users[1:]:
self.assert_fetch(member, status.HTTP_403_FORBIDDEN)
def test_denies_non_project_member_to_get_mapping(self):
self.assert_fetch(self.non_member, status.HTTP_403_FORBIDDEN)
def test_denies_unauthenticated_user_to_get_mapping(self):
self.assert_fetch(expected=status.HTTP_403_FORBIDDEN)
def test_allows_project_admin_to_update_mapping(self):
self.assert_update(self.project.users[0], status.HTTP_200_OK)
def test_denies_non_project_admin_to_update_mapping(self):
for member in self.project.users[1:]:
self.assert_update(member, status.HTTP_403_FORBIDDEN)
def test_denies_non_project_member_to_update_mapping(self):
self.assert_update(self.non_member, status.HTTP_403_FORBIDDEN)
def test_denies_unauthenticated_user_to_update_mapping(self):
self.assert_update(expected=status.HTTP_403_FORBIDDEN)
class TestMemberRoleFilter(CRUDMixin):
def setUp(self):
self.project = prepare_project()
self.url = reverse(viewname='member_list', args=[self.project.item.id])
self.url += f'?user={self.project.users[0].id}'
def test_filter_role_by_user_id(self):
response = self.assert_fetch(self.project.users[0], status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
class TestMemberManager(CRUDMixin):
def setUp(self):
pass
def test_has_role(self):
project = prepare_project()
admin = project.users[0]
expected = [
(settings.ROLE_PROJECT_ADMIN, True),
(settings.ROLE_ANNOTATION_APPROVER, False),
(settings.ROLE_ANNOTATOR, False)
]
for role, expect in expected:
self.assertEqual(Member.objects.has_role(project.item, admin, role), expect)

12
backend/roles/urls.py

@ -1,21 +1,11 @@
from django.urls import path
from .views import MemberDetail, MemberList, Roles
from .views import Roles
urlpatterns = [
path(
route='roles',
view=Roles.as_view(),
name='roles'
),
path(
route='projects/<int:project_id>/members',
view=MemberList.as_view(),
name='member_list'
),
path(
route='projects/<int:project_id>/members/<int:member_id>',
view=MemberDetail.as_view(),
name='member_detail'
)
]

56
backend/roles/views.py

@ -1,13 +1,8 @@
from django.db import IntegrityError
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, status
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from .permissions import IsProjectAdmin
from .models import Role, Member
from .serializers import MemberSerializer, RoleSerializer
from .exceptions import RoleAlreadyAssignedException, RoleConstraintException
from .models import Role
from .serializers import RoleSerializer
class Roles(generics.ListAPIView):
@ -15,48 +10,3 @@ class Roles(generics.ListAPIView):
pagination_class = None
permission_classes = [IsAuthenticated]
queryset = Role.objects.all()
class MemberList(generics.ListCreateAPIView):
filter_backends = [DjangoFilterBackend]
filterset_fields = ['user']
queryset = Member.objects.all()
serializer_class = MemberSerializer
pagination_class = None
permission_classes = [IsAuthenticated & IsProjectAdmin]
def filter_queryset(self, queryset):
queryset = queryset.filter(project=self.kwargs['project_id'])
return super().filter_queryset(queryset)
def perform_create(self, serializer):
try:
serializer.save(project_id=self.kwargs['project_id'])
except IntegrityError:
raise RoleAlreadyAssignedException
def delete(self, request, *args, **kwargs):
delete_ids = request.data['ids']
project_id = self.kwargs['project_id']
Member.objects.filter(project=project_id, pk__in=delete_ids)\
.exclude(user=self.request.user)\
.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class MemberDetail(generics.RetrieveUpdateAPIView):
queryset = Member.objects.all()
serializer_class = MemberSerializer
lookup_url_kwarg = 'member_id'
permission_classes = [IsAuthenticated & IsProjectAdmin]
def perform_update(self, serializer):
project_id = self.kwargs['project_id']
member_id = self.kwargs['member_id']
role = serializer.validated_data['role']
if not Member.objects.can_update(project_id, member_id, role.name):
raise RoleConstraintException
try:
super().perform_update(serializer)
except IntegrityError:
raise RoleAlreadyAssignedException
Loading…
Cancel
Save