mirror of https://github.com/doccano/doccano.git
Hironsan
2 years ago
13 changed files with 288 additions and 275 deletions
Split View
Diff Options
-
21backend/api/admin.py
-
94backend/api/serializers.py
-
48backend/api/urls.py
-
116backend/api/views/label.py
-
1backend/app/settings.py
-
1backend/app/urls.py
-
21backend/label_types/admin.py
-
0backend/label_types/exceptions.py
-
88backend/label_types/serializers.py
-
0backend/label_types/tests.py
-
4backend/label_types/tests/test_views.py
-
54backend/label_types/urls.py
-
115backend/label_types/views.py
@ -1,116 +0,0 @@ |
|||
import json |
|||
import re |
|||
|
|||
from django.db import IntegrityError, transaction |
|||
from django_filters.rest_framework import DjangoFilterBackend |
|||
from rest_framework import generics, status |
|||
from rest_framework.exceptions import ParseError |
|||
from rest_framework.parsers import MultiPartParser |
|||
from rest_framework.permissions import IsAuthenticated |
|||
from rest_framework.response import Response |
|||
from rest_framework.views import APIView |
|||
|
|||
from members.permissions import IsInProjectReadOnlyOrAdmin, IsProjectAdmin |
|||
|
|||
from ..exceptions import LabelValidationError |
|||
from ..models import CategoryType, Label, RelationTypes, SpanType |
|||
from ..serializers import (CategoryTypeSerializer, LabelSerializer, |
|||
RelationTypesSerializer, SpanTypeSerializer) |
|||
|
|||
|
|||
def camel_to_snake(name): |
|||
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) |
|||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() |
|||
|
|||
|
|||
def camel_to_snake_dict(d): |
|||
return {camel_to_snake(k): v for k, v in d.items()} |
|||
|
|||
|
|||
class LabelList(generics.ListCreateAPIView): |
|||
model = Label |
|||
filter_backends = [DjangoFilterBackend] |
|||
serializer_class = LabelSerializer |
|||
pagination_class = None |
|||
permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin] |
|||
|
|||
def get_queryset(self): |
|||
return self.model.objects.filter(project=self.kwargs['project_id']) |
|||
|
|||
def perform_create(self, serializer): |
|||
serializer.save(project_id=self.kwargs['project_id']) |
|||
|
|||
def delete(self, request, *args, **kwargs): |
|||
delete_ids = request.data['ids'] |
|||
self.model.objects.filter(pk__in=delete_ids).delete() |
|||
return Response(status=status.HTTP_204_NO_CONTENT) |
|||
|
|||
|
|||
class CategoryTypeList(LabelList): |
|||
model = CategoryType |
|||
serializer_class = CategoryTypeSerializer |
|||
|
|||
|
|||
class CategoryTypeDetail(generics.RetrieveUpdateDestroyAPIView): |
|||
queryset = CategoryType.objects.all() |
|||
serializer_class = CategoryTypeSerializer |
|||
lookup_url_kwarg = 'label_id' |
|||
permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin] |
|||
|
|||
|
|||
class SpanTypeList(LabelList): |
|||
model = SpanType |
|||
serializer_class = SpanTypeSerializer |
|||
|
|||
|
|||
class SpanTypeDetail(generics.RetrieveUpdateDestroyAPIView): |
|||
queryset = SpanType.objects.all() |
|||
serializer_class = SpanTypeSerializer |
|||
lookup_url_kwarg = 'label_id' |
|||
permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin] |
|||
|
|||
|
|||
class RelationTypeList(LabelList): |
|||
model = RelationTypes |
|||
serializer_class = RelationTypesSerializer |
|||
|
|||
|
|||
class RelationTypeDetail(generics.RetrieveUpdateDestroyAPIView): |
|||
queryset = RelationTypes.objects.all() |
|||
serializer_class = RelationTypesSerializer |
|||
lookup_url_kwarg = 'relation_type_id' |
|||
permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin] |
|||
|
|||
|
|||
class LabelUploadAPI(APIView): |
|||
parser_classes = (MultiPartParser,) |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
serializer_class = LabelSerializer |
|||
|
|||
@transaction.atomic |
|||
def post(self, request, *args, **kwargs): |
|||
if 'file' not in request.data: |
|||
raise ParseError('Empty content') |
|||
try: |
|||
labels = json.load(request.data['file']) |
|||
labels = list(map(camel_to_snake_dict, labels)) |
|||
serializer = self.serializer_class(data=labels, many=True) |
|||
serializer.is_valid(raise_exception=True) |
|||
serializer.save(project_id=kwargs['project_id']) |
|||
return Response(status=status.HTTP_201_CREATED) |
|||
except json.decoder.JSONDecodeError: |
|||
raise ParseError('The file format is invalid.') |
|||
except IntegrityError: |
|||
raise LabelValidationError |
|||
|
|||
|
|||
class CategoryTypeUploadAPI(LabelUploadAPI): |
|||
serializer_class = CategoryTypeSerializer |
|||
|
|||
|
|||
class SpanTypeUploadAPI(LabelUploadAPI): |
|||
serializer_class = SpanTypeSerializer |
|||
|
|||
|
|||
class RelationTypeUploadAPI(LabelUploadAPI): |
|||
serializer_class = RelationTypesSerializer |
@ -0,0 +1,21 @@ |
|||
from django.contrib import admin |
|||
|
|||
from api.models import CategoryType, SpanType |
|||
|
|||
|
|||
class LabelAdmin(admin.ModelAdmin): |
|||
list_display = ('text', 'project', 'text_color', 'background_color') |
|||
ordering = ('project',) |
|||
search_fields = ('text',) |
|||
|
|||
|
|||
class CategoryTypeAdmin(LabelAdmin): |
|||
pass |
|||
|
|||
|
|||
class SpanTypeAdmin(LabelAdmin): |
|||
pass |
|||
|
|||
|
|||
admin.site.register(CategoryType, CategoryTypeAdmin) |
|||
admin.site.register(SpanType, SpanTypeAdmin) |
@ -0,0 +1,88 @@ |
|||
from rest_framework import serializers |
|||
from rest_framework.exceptions import ValidationError |
|||
|
|||
from api.models import Label, CategoryType, SpanType, RelationTypes |
|||
|
|||
|
|||
class LabelSerializer(serializers.ModelSerializer): |
|||
|
|||
def validate(self, attrs): |
|||
prefix_key = attrs.get('prefix_key') |
|||
suffix_key = attrs.get('suffix_key') |
|||
|
|||
# In the case of user don't set any shortcut key. |
|||
if prefix_key is None and suffix_key is None: |
|||
return super().validate(attrs) |
|||
|
|||
# Don't allow shortcut key not to have a suffix key. |
|||
if prefix_key and not suffix_key: |
|||
raise ValidationError('Shortcut key may not have a suffix key.') |
|||
|
|||
# Don't allow to save same shortcut key when prefix_key is null. |
|||
try: |
|||
context = self.context['request'].parser_context |
|||
project_id = context['kwargs']['project_id'] |
|||
label_id = context['kwargs'].get('label_id') |
|||
except (AttributeError, KeyError): |
|||
pass # unit tests don't always have the correct context set up |
|||
else: |
|||
conflicting_labels = self.Meta.model.objects.filter( |
|||
suffix_key=suffix_key, |
|||
prefix_key=prefix_key, |
|||
project=project_id, |
|||
) |
|||
|
|||
if label_id is not None: |
|||
conflicting_labels = conflicting_labels.exclude(id=label_id) |
|||
|
|||
if conflicting_labels.exists(): |
|||
raise ValidationError('Duplicate shortcut key.') |
|||
|
|||
return super().validate(attrs) |
|||
|
|||
class Meta: |
|||
model = Label |
|||
fields = ( |
|||
'id', |
|||
'text', |
|||
'prefix_key', |
|||
'suffix_key', |
|||
'background_color', |
|||
'text_color', |
|||
) |
|||
|
|||
|
|||
class CategoryTypeSerializer(LabelSerializer): |
|||
class Meta: |
|||
model = CategoryType |
|||
fields = ( |
|||
'id', |
|||
'text', |
|||
'prefix_key', |
|||
'suffix_key', |
|||
'background_color', |
|||
'text_color', |
|||
) |
|||
|
|||
|
|||
class SpanTypeSerializer(LabelSerializer): |
|||
class Meta: |
|||
model = SpanType |
|||
fields = ( |
|||
'id', |
|||
'text', |
|||
'prefix_key', |
|||
'suffix_key', |
|||
'background_color', |
|||
'text_color', |
|||
) |
|||
|
|||
|
|||
class RelationTypesSerializer(serializers.ModelSerializer): |
|||
|
|||
def validate(self, attrs): |
|||
return super().validate(attrs) |
|||
|
|||
class Meta: |
|||
model = RelationTypes |
|||
fields = ('id', 'color', 'name') |
@ -0,0 +1,54 @@ |
|||
from django.urls import path |
|||
|
|||
from .views import CategoryTypeList, CategoryTypeDetail, CategoryTypeUploadAPI |
|||
from .views import SpanTypeList, SpanTypeDetail, SpanTypeUploadAPI |
|||
from .views import RelationTypeList, RelationTypeDetail, RelationTypeUploadAPI |
|||
|
|||
|
|||
urlpatterns = [ |
|||
path( |
|||
route='category-types', |
|||
view=CategoryTypeList.as_view(), |
|||
name='category_types' |
|||
), |
|||
path( |
|||
route='category-types/<int:label_id>', |
|||
view=CategoryTypeDetail.as_view(), |
|||
name='category_type' |
|||
), |
|||
path( |
|||
route='span-types', |
|||
view=SpanTypeList.as_view(), |
|||
name='span_types' |
|||
), |
|||
path( |
|||
route='span-types/<int:label_id>', |
|||
view=SpanTypeDetail.as_view(), |
|||
name='span_type' |
|||
), |
|||
path( |
|||
route='category-type-upload', |
|||
view=CategoryTypeUploadAPI.as_view(), |
|||
name='category_type_upload' |
|||
), |
|||
path( |
|||
route='span-type-upload', |
|||
view=SpanTypeUploadAPI.as_view(), |
|||
name='span_type_upload' |
|||
), |
|||
path( |
|||
route='relation_types', |
|||
view=RelationTypeList.as_view(), |
|||
name='relation_types_list' |
|||
), |
|||
path( |
|||
route='relation_type-upload', |
|||
view=RelationTypeUploadAPI.as_view(), |
|||
name='relation_type-upload' |
|||
), |
|||
path( |
|||
route='relation_types/<int:relation_type_id>', |
|||
view=RelationTypeDetail.as_view(), |
|||
name='relation_type_detail' |
|||
), |
|||
] |
@ -0,0 +1,115 @@ |
|||
import json |
|||
import re |
|||
|
|||
from django.db import IntegrityError, transaction |
|||
from django_filters.rest_framework import DjangoFilterBackend |
|||
from rest_framework import generics, status |
|||
from rest_framework.exceptions import ParseError |
|||
from rest_framework.parsers import MultiPartParser |
|||
from rest_framework.permissions import IsAuthenticated |
|||
from rest_framework.response import Response |
|||
from rest_framework.views import APIView |
|||
|
|||
from members.permissions import IsInProjectReadOnlyOrAdmin, IsProjectAdmin |
|||
from api.models import CategoryType, Label, RelationTypes, SpanType |
|||
from .exceptions import LabelValidationError |
|||
from .serializers import (CategoryTypeSerializer, LabelSerializer, |
|||
RelationTypesSerializer, SpanTypeSerializer) |
|||
|
|||
|
|||
def camel_to_snake(name): |
|||
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) |
|||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() |
|||
|
|||
|
|||
def camel_to_snake_dict(d): |
|||
return {camel_to_snake(k): v for k, v in d.items()} |
|||
|
|||
|
|||
class LabelList(generics.ListCreateAPIView): |
|||
model = Label |
|||
filter_backends = [DjangoFilterBackend] |
|||
serializer_class = LabelSerializer |
|||
pagination_class = None |
|||
permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin] |
|||
|
|||
def get_queryset(self): |
|||
return self.model.objects.filter(project=self.kwargs['project_id']) |
|||
|
|||
def perform_create(self, serializer): |
|||
serializer.save(project_id=self.kwargs['project_id']) |
|||
|
|||
def delete(self, request, *args, **kwargs): |
|||
delete_ids = request.data['ids'] |
|||
self.model.objects.filter(pk__in=delete_ids).delete() |
|||
return Response(status=status.HTTP_204_NO_CONTENT) |
|||
|
|||
|
|||
class CategoryTypeList(LabelList): |
|||
model = CategoryType |
|||
serializer_class = CategoryTypeSerializer |
|||
|
|||
|
|||
class CategoryTypeDetail(generics.RetrieveUpdateDestroyAPIView): |
|||
queryset = CategoryType.objects.all() |
|||
serializer_class = CategoryTypeSerializer |
|||
lookup_url_kwarg = 'label_id' |
|||
permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin] |
|||
|
|||
|
|||
class SpanTypeList(LabelList): |
|||
model = SpanType |
|||
serializer_class = SpanTypeSerializer |
|||
|
|||
|
|||
class SpanTypeDetail(generics.RetrieveUpdateDestroyAPIView): |
|||
queryset = SpanType.objects.all() |
|||
serializer_class = SpanTypeSerializer |
|||
lookup_url_kwarg = 'label_id' |
|||
permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin] |
|||
|
|||
|
|||
class RelationTypeList(LabelList): |
|||
model = RelationTypes |
|||
serializer_class = RelationTypesSerializer |
|||
|
|||
|
|||
class RelationTypeDetail(generics.RetrieveUpdateDestroyAPIView): |
|||
queryset = RelationTypes.objects.all() |
|||
serializer_class = RelationTypesSerializer |
|||
lookup_url_kwarg = 'relation_type_id' |
|||
permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin] |
|||
|
|||
|
|||
class LabelUploadAPI(APIView): |
|||
parser_classes = (MultiPartParser,) |
|||
permission_classes = [IsAuthenticated & IsProjectAdmin] |
|||
serializer_class = LabelSerializer |
|||
|
|||
@transaction.atomic |
|||
def post(self, request, *args, **kwargs): |
|||
if 'file' not in request.data: |
|||
raise ParseError('Empty content') |
|||
try: |
|||
labels = json.load(request.data['file']) |
|||
labels = list(map(camel_to_snake_dict, labels)) |
|||
serializer = self.serializer_class(data=labels, many=True) |
|||
serializer.is_valid(raise_exception=True) |
|||
serializer.save(project_id=kwargs['project_id']) |
|||
return Response(status=status.HTTP_201_CREATED) |
|||
except json.decoder.JSONDecodeError: |
|||
raise ParseError('The file format is invalid.') |
|||
except IntegrityError: |
|||
raise LabelValidationError |
|||
|
|||
|
|||
class CategoryTypeUploadAPI(LabelUploadAPI): |
|||
serializer_class = CategoryTypeSerializer |
|||
|
|||
|
|||
class SpanTypeUploadAPI(LabelUploadAPI): |
|||
serializer_class = SpanTypeSerializer |
|||
|
|||
|
|||
class RelationTypeUploadAPI(LabelUploadAPI): |
|||
serializer_class = RelationTypesSerializer |
Write
Preview
Loading…
Cancel
Save