Skip to content

Commit

Permalink
Merge pull request #1627 from doccano/enhancement/separateApp
Browse files Browse the repository at this point in the history
[Enhancement] Separate applications
  • Loading branch information
Hironsan committed Jan 13, 2022
2 parents 789d944 + d885e52 commit 6a9160e
Show file tree
Hide file tree
Showing 72 changed files with 706 additions and 575 deletions.
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 members.tests"
migrate = "python manage.py migrate"
collectstatic = "python manage.py collectstatic --noinput"
20 changes: 2 additions & 18 deletions backend/api/admin.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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', )
Expand Down Expand Up @@ -86,15 +73,12 @@ def get_readonly_fields(self, request, obj=None):
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)
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)
5 changes: 0 additions & 5 deletions backend/api/apps.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import importlib

from django.apps import AppConfig


class ApiConfig(AppConfig):
name = 'api'
verbose_name = 'Api'

def ready(self):
importlib.import_module('api.signals')
10 changes: 0 additions & 10 deletions backend/api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
16 changes: 0 additions & 16 deletions backend/api/managers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from django.conf import settings
from django.db.models import Count, Manager


Expand Down Expand Up @@ -31,21 +30,6 @@ def calc_label_distribution(self, examples, users, labels):
return distribution


class RoleMappingManager(Manager):

def can_update(self, project: int, mapping_id: int, rolename: str):
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 rolename != settings.ROLE_PROJECT_ADMIN:
return False
return True


class ExampleManager(Manager):

def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
Expand Down
54 changes: 54 additions & 0 deletions backend/api/migrations/0028_auto_20220111_0655.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Generated by Django 3.2.8 on 2022-01-11 06:55

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('api', '0027_auto_20211222_0454'),
]

operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.AlterUniqueTogether(
name='rolemapping',
unique_together=None,
),
migrations.RemoveField(
model_name='rolemapping',
name='project',
),
migrations.RemoveField(
model_name='rolemapping',
name='role',
),
migrations.RemoveField(
model_name='rolemapping',
name='user',
),
],
database_operations=[],
),
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.DeleteModel(
name='Role',
),
migrations.DeleteModel(
name='RoleMapping',
),
],
database_operations=[
migrations.AlterModelTable(
name='Role',
table='roles_role'
),
migrations.AlterModelTable(
name='RoleMapping',
table='roles_rolemapping'
)
]
)
]
48 changes: 1 addition & 47 deletions backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
from django.db import models
from polymorphic.models import PolymorphicModel

from .managers import (AnnotationManager, ExampleManager, ExampleStateManager,
RoleMappingManager)
from .managers import AnnotationManager, ExampleManager, ExampleStateManager

DOCUMENT_CLASSIFICATION = 'DocumentClassification'
SEQUENCE_LABELING = 'SequenceLabeling'
Expand Down Expand Up @@ -222,11 +221,6 @@ class ExampleState(models.Model):
class Meta:
unique_together = (('example', 'confirmed_by'),)

@property
def confirmed_user_role(self):
role_mapping = RoleMapping.objects.get(user=self.confirmed_by, project=self.example.project)
return role_mapping.role


class Comment(models.Model):
text = models.TextField()
Expand Down Expand Up @@ -350,46 +344,6 @@ class Meta:
)


class Role(models.Model):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(default='')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return self.name


class RoleMapping(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 = RoleMappingManager()

def clean(self):
other_rolemappings = self.project.role_mappings.exclude(id=self.id)

if other_rolemappings.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")


class AutoLabelingConfig(models.Model):
model_name = models.CharField(max_length=100)
model_attrs = models.JSONField(default=dict)
Expand Down
106 changes: 3 additions & 103 deletions backend/api/permissions.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,7 @@
from django.conf import settings
from django.contrib.auth.mixins import UserPassesTestMixin
from django.db.models import Subquery
from django.shortcuts import get_object_or_404
from rest_framework.permissions import (SAFE_METHODS, BasePermission,
IsAdminUser)
from rest_framework.permissions import BasePermission

from .models import Project, Role, RoleMapping


class ProjectMixin:
@classmethod
def get_project_id(self, request, view):
return view.kwargs.get('project_id') or request.query_params.get('project_id')


class IsAdminUserAndWriteOnly(BasePermission):

def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True

return IsAdminUser().has_permission(request, view)


class ProjectAdminMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.is_superuser or is_in_role(
role_name=IsProjectAdmin.role_name,
user_id=self.request.user.id,
project_id=self.kwargs['project_id'],
)


class IsOwnAnnotation(ProjectMixin, BasePermission):

def has_permission(self, request, view):
if request.user.is_superuser:
return True

project_id = self.get_project_id(request, view)
annotation_id = view.kwargs.get('annotation_id')
project = get_object_or_404(Project, pk=project_id)
model = project.get_annotation_class()
annotation = model.objects.filter(id=annotation_id, user=request.user)

return annotation.exists()


class CanEditAnnotation(ProjectMixin, BasePermission):
class CanEditAnnotation(BasePermission):

def __init__(self, queryset):
super().__init__()
Expand All @@ -61,7 +15,7 @@ def has_permission(self, request, view):
return self.queryset.filter(id=annotation_id, user=request.user).exists()


class IsOwnComment(ProjectMixin, BasePermission):
class IsOwnComment(BasePermission):
@classmethod
def has_object_permission(cls, request, view, obj):
if request.user.is_superuser:
Expand All @@ -70,62 +24,8 @@ def has_object_permission(cls, request, view, obj):
return obj.user.id == request.user.id


class RolePermission(ProjectMixin, BasePermission):
UNSAFE_METHODS = ('POST', 'PATCH', 'DELETE')
unsafe_methods_check = True
role_name = ''

def has_permission(self, request, view):
if request.user.is_superuser:
return True

if self.unsafe_methods_check and request.method in self.UNSAFE_METHODS:
return request.user.is_superuser

project_id = self.get_project_id(request, view)
if not project_id and request.method in SAFE_METHODS:
return True

return is_in_role(self.role_name, request.user.id, project_id)


class IsProjectAdmin(RolePermission):
unsafe_methods_check = False
role_name = settings.ROLE_PROJECT_ADMIN


class 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


class IsStaff(BasePermission):
def has_permission(self, request, view):
if request.user.is_superuser or request.user.is_staff:
return True
return False


def is_in_role(role_name, user_id, project_id):
return RoleMapping.objects.filter(
user_id=user_id,
project_id=project_id,
role_id=Subquery(Role.objects.filter(name=role_name).values('id')),
).exists()


IsInProjectReadOnlyOrAdmin = (IsAnnotatorAndReadOnly | IsAnnotationApproverAndReadOnly | IsProjectAdmin)
IsInProjectOrAdmin = (IsAnnotator | IsAnnotationApprover | IsProjectAdmin)

0 comments on commit 6a9160e

Please sign in to comment.