Browse Source

Move code from api to roles

pull/1627/head
Hironsan 3 years ago
parent
commit
f5d7f128ce
16 changed files with 261 additions and 256 deletions
  1. 2
      Pipfile
  2. 20
      backend/api/admin.py
  3. 10
      backend/api/exceptions.py
  4. 27
      backend/api/serializers.py
  5. 116
      backend/api/tests/api/test_role.py
  6. 17
      backend/api/urls.py
  7. 61
      backend/api/views/role.py
  8. 1
      backend/app/settings.py
  9. 1
      backend/app/urls.py
  10. 18
      backend/roles/admin.py
  11. 12
      backend/roles/exceptions.py
  12. 3
      backend/roles/models.py
  13. 28
      backend/roles/serializers.py
  14. 118
      backend/roles/tests.py
  15. 21
      backend/roles/urls.py
  16. 62
      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"
test = "python manage.py test api.tests roles.tests"
migrate = "python manage.py migrate"
collectstatic = "python manage.py collectstatic --noinput"

20
backend/api/admin.py

@ -1,9 +1,8 @@
from django.contrib import admin
from .models import (AutoLabelingConfig, Category, CategoryType, Comment,
Example, Project, Role, RoleMapping, Seq2seqProject,
SequenceLabelingProject, Span, SpanType, Tag,
TextClassificationProject, TextLabel)
Example, Project, Seq2seqProject, SequenceLabelingProject,
Span, SpanType, Tag, TextClassificationProject, TextLabel)
class LabelAdmin(admin.ModelAdmin):
@ -47,18 +46,6 @@ class TextLabelAdmin(admin.ModelAdmin):
ordering = ('example',)
class RoleAdmin(admin.ModelAdmin):
list_display = ('name', 'description')
ordering = ('name',)
search_fields = ('name',)
class RoleMappingAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'project', )
ordering = ('user',)
search_fields = ('user__username',)
class TagAdmin(admin.ModelAdmin):
list_display = ('project', 'text', )
ordering = ('project', 'text', )
@ -86,7 +73,6 @@ admin.site.register(AutoLabelingConfig, AutoLabelingConfigAdmin)
admin.site.register(Category, CategoryAdmin)
admin.site.register(Span, SpanAdmin)
admin.site.register(TextLabel, TextLabelAdmin)
# admin.site.register(Label, LabelAdmin)
admin.site.register(CategoryType, CategoryTypeAdmin)
admin.site.register(SpanType, SpanTypeAdmin)
admin.site.register(Example, ExampleAdmin)
@ -94,7 +80,5 @@ admin.site.register(Project, ProjectAdmin)
admin.site.register(TextClassificationProject, ProjectAdmin)
admin.site.register(SequenceLabelingProject, ProjectAdmin)
admin.site.register(Seq2seqProject, ProjectAdmin)
admin.site.register(Role, RoleAdmin)
admin.site.register(RoleMapping, RoleMappingAdmin)
admin.site.register(Comment, CommentAdmin)
admin.site.register(Tag, TagAdmin)

10
backend/api/exceptions.py

@ -53,13 +53,3 @@ class AnnotationRelationValidationError(APIException):
class RelationTypesValidationError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'You cannot create a relation type with same name or color.'
class RoleConstraintException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'The project needs at least one administrator.'
class RoleAlreadyAssignedException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'This user is already assigned to a role in this project.'

27
backend/api/serializers.py

@ -11,7 +11,7 @@ from .models import (DOCUMENT_CLASSIFICATION, IMAGE_CLASSIFICATION, SEQ2SEQ,
AutoLabelingConfig, Category, CategoryType, Comment,
Example, ExampleState, ImageClassificationProject,
IntentDetectionAndSlotFillingProject, Label, Project,
RelationTypes, Role, RoleMapping, Seq2seqProject,
RelationTypes, RoleMapping, Seq2seqProject,
SequenceLabelingProject, Span, SpanType,
Speech2textProject, Tag, TextClassificationProject,
TextLabel)
@ -306,31 +306,6 @@ class TextLabelSerializer(serializers.ModelSerializer):
read_only_fields = ('user',)
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = Role
fields = ('id', 'name')
class RoleMappingSerializer(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 = RoleMapping
fields = ('id', 'user', 'role', 'username', 'rolename')
class AutoLabelingConfigSerializer(serializers.ModelSerializer):
class Meta:

116
backend/api/tests/api/test_role.py

@ -1,116 +0,0 @@
from django.conf import settings
from rest_framework import status
from rest_framework.reverse import reverse
from ...models import Role, RoleMapping
from .utils import CRUDMixin, create_default_roles, make_user, prepare_project
class TestRoleAPI(CRUDMixin):
@classmethod
def setUpTestData(cls):
create_default_roles()
cls.user = make_user()
cls.url = reverse(viewname='roles')
def test_allows_authenticated_user_to_get_roles(self):
self.assert_fetch(self.user, status.HTTP_200_OK)
def test_disallows_unauthenticated_user_to_get_roles(self):
self.assert_fetch(expected=status.HTTP_403_FORBIDDEN)
class TestRoleMappingListAPI(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='rolemapping_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 TestRoleMappingDetailAPI(CRUDMixin):
def setUp(self):
self.project = prepare_project()
self.non_member = make_user()
admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
mapping = RoleMapping.objects.get(user=self.project.users[1])
self.url = reverse(viewname='rolemapping_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)

17
backend/api/urls.py

@ -3,7 +3,7 @@ from django.urls import include, path
from .views import (annotation, annotation_relations, auto_labeling, comment,
example, example_state, export_dataset, health,
import_dataset, import_export, label, project,
relation_types, role, statistics, tag, task, user)
relation_types, statistics, tag, task, user)
from .views.tasks import category, span, text
urlpatterns_project = [
@ -204,16 +204,6 @@ urlpatterns_project = [
view=example_state.ExampleStateList.as_view(),
name='example_state_list'
),
path(
route='roles',
view=role.RoleMappingList.as_view(),
name='rolemapping_list'
),
path(
route='roles/<int:rolemapping_id>',
view=role.RoleMappingDetail.as_view(),
name='rolemapping_detail'
),
path(
route='auto-labeling-templates',
view=auto_labeling.AutoLabelingTemplateListAPI.as_view(),
@ -289,11 +279,6 @@ urlpatterns = [
view=user.Users.as_view(),
name='user_list'
),
path(
route='roles',
view=role.Roles.as_view(),
name='roles'
),
path(
route='tasks/status/<task_id>',
view=task.TaskStatus.as_view(),

61
backend/api/views/role.py

@ -1,61 +0,0 @@
from django.db import IntegrityError
from django.shortcuts import get_object_or_404
from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from ..exceptions import RoleAlreadyAssignedException, RoleConstraintException
from ..models import Project, Role, RoleMapping
from ..permissions import IsProjectAdmin
from ..serializers import RoleMappingSerializer, RoleSerializer
class Roles(generics.ListAPIView):
serializer_class = RoleSerializer
pagination_class = None
permission_classes = [IsAuthenticated]
queryset = Role.objects.all()
class RoleMappingList(generics.ListCreateAPIView):
serializer_class = RoleMappingSerializer
pagination_class = None
permission_classes = [IsAuthenticated & IsProjectAdmin]
@property
def project(self):
return get_object_or_404(Project, pk=self.kwargs['project_id'])
def get_queryset(self):
return self.project.role_mappings
def perform_create(self, serializer):
try:
serializer.save(project=self.project)
except IntegrityError:
raise RoleAlreadyAssignedException
def delete(self, request, *args, **kwargs):
delete_ids = request.data['ids']
RoleMapping.objects.filter(project=self.project, pk__in=delete_ids)\
.exclude(user=self.request.user)\
.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class RoleMappingDetail(generics.RetrieveUpdateAPIView):
queryset = RoleMapping.objects.all()
serializer_class = RoleMappingSerializer
lookup_url_kwarg = 'rolemapping_id'
permission_classes = [IsAuthenticated & IsProjectAdmin]
def perform_update(self, serializer):
project_id = self.kwargs['project_id']
id = self.kwargs['rolemapping_id']
role = serializer.validated_data['role']
if not RoleMapping.objects.can_update(project_id, id, role.name):
raise RoleConstraintException
try:
super().perform_update(serializer)
except IntegrityError:
raise RoleAlreadyAssignedException

1
backend/app/settings.py

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

1
backend/app/urls.py

@ -41,6 +41,7 @@ urlpatterns += [
path('social/', include('social_django.urls')),
path('api-auth/', include('rest_framework.urls')),
path('v1/', include('api.urls')),
path('v1/', include('roles.urls')),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
re_path('', TemplateView.as_view(template_name='index.html')),
]

18
backend/roles/admin.py

@ -1,3 +1,19 @@
from django.contrib import admin
# Register your models here.
from api.models import Role, RoleMapping
class RoleAdmin(admin.ModelAdmin):
list_display = ('name', 'description')
ordering = ('name',)
search_fields = ('name',)
class RoleMappingAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'project', )
ordering = ('user',)
search_fields = ('user__username',)
admin.site.register(Role, RoleAdmin)
admin.site.register(RoleMapping, RoleMappingAdmin)

12
backend/roles/exceptions.py

@ -0,0 +1,12 @@
from rest_framework import status
from rest_framework.exceptions import APIException
class RoleConstraintException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'The project needs at least one administrator.'
class RoleAlreadyAssignedException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'This user is already assigned to a role in this project.'

3
backend/roles/models.py

@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

28
backend/roles/serializers.py

@ -0,0 +1,28 @@
from rest_framework import serializers
from api.models import Role, RoleMapping
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = Role
fields = ('id', 'name')
class RoleMappingSerializer(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 = RoleMapping
fields = ('id', 'user', 'role', 'username', 'rolename')

118
backend/roles/tests.py

@ -1,3 +1,117 @@
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 api.models import Role, RoleMapping
from api.tests.api.utils import (CRUDMixin, create_default_roles, make_user,
prepare_project)
class TestRoleAPI(CRUDMixin):
@classmethod
def setUpTestData(cls):
create_default_roles()
cls.user = make_user()
cls.url = reverse(viewname='roles')
def test_allows_authenticated_user_to_get_roles(self):
self.assert_fetch(self.user, status.HTTP_200_OK)
def test_disallows_unauthenticated_user_to_get_roles(self):
self.assert_fetch(expected=status.HTTP_403_FORBIDDEN)
class TestRoleMappingListAPI(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='rolemapping_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 TestRoleMappingDetailAPI(CRUDMixin):
def setUp(self):
self.project = prepare_project()
self.non_member = make_user()
admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)
mapping = RoleMapping.objects.get(user=self.project.users[1])
self.url = reverse(viewname='rolemapping_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)

21
backend/roles/urls.py

@ -0,0 +1,21 @@
from django.urls import path
from .views import RoleMappingDetail, RoleMappingList, Roles
urlpatterns = [
path(
route='roles',
view=Roles.as_view(),
name='roles'
),
path(
route='projects/<int:project_id>/roles',
view=RoleMappingList.as_view(),
name='rolemapping_list'
),
path(
route='projects/<int:project_id>/roles/<int:rolemapping_id>',
view=RoleMappingDetail.as_view(),
name='rolemapping_detail'
)
]

62
backend/roles/views.py

@ -1,3 +1,61 @@
from django.shortcuts import render
from django.db import IntegrityError
from django.shortcuts import get_object_or_404
from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
# Create your views here.
from api.models import Project, Role, RoleMapping
from api.permissions import IsProjectAdmin
from .serializers import RoleMappingSerializer, RoleSerializer
from .exceptions import RoleAlreadyAssignedException, RoleConstraintException
class Roles(generics.ListAPIView):
serializer_class = RoleSerializer
pagination_class = None
permission_classes = [IsAuthenticated]
queryset = Role.objects.all()
class RoleMappingList(generics.ListCreateAPIView):
serializer_class = RoleMappingSerializer
pagination_class = None
permission_classes = [IsAuthenticated & IsProjectAdmin]
@property
def project(self):
return get_object_or_404(Project, pk=self.kwargs['project_id'])
def get_queryset(self):
return self.project.role_mappings
def perform_create(self, serializer):
try:
serializer.save(project=self.project)
except IntegrityError:
raise RoleAlreadyAssignedException
def delete(self, request, *args, **kwargs):
delete_ids = request.data['ids']
RoleMapping.objects.filter(project=self.project, pk__in=delete_ids)\
.exclude(user=self.request.user)\
.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class RoleMappingDetail(generics.RetrieveUpdateAPIView):
queryset = RoleMapping.objects.all()
serializer_class = RoleMappingSerializer
lookup_url_kwarg = 'rolemapping_id'
permission_classes = [IsAuthenticated & IsProjectAdmin]
def perform_update(self, serializer):
project_id = self.kwargs['project_id']
id = self.kwargs['rolemapping_id']
role = serializer.validated_data['role']
if not RoleMapping.objects.can_update(project_id, id, role.name):
raise RoleConstraintException
try:
super().perform_update(serializer)
except IntegrityError:
raise RoleAlreadyAssignedException
Loading…
Cancel
Save