diff --git a/app/api/exceptions.py b/app/api/exceptions.py index 8e3f3582..7a17259f 100644 --- a/app/api/exceptions.py +++ b/app/api/exceptions.py @@ -41,6 +41,11 @@ class LabelValidationError(APIException): default_detail = 'You cannot create a label with same name or shortcut key.' +class AnnotationRelationValidationError(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = 'You cannot create an annotation relation between the same annotation.' + + class RelationTypesValidationError(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = 'You cannot create a relation type with same name or color.' diff --git a/app/api/migrations/0009_annotations_relations_20210421_1445.py b/app/api/migrations/0009_annotations_relations_20210421_1445.py index 7106b755..369a1146 100644 --- a/app/api/migrations/0009_annotations_relations_20210421_1445.py +++ b/app/api/migrations/0009_annotations_relations_20210421_1445.py @@ -26,9 +26,10 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('annotation_id_1', models.IntegerField()), ('annotation_id_2', models.IntegerField()), - ('type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.RelationTypes')), - ('timestamp', models.DateTimeField(auto_now_add=True)) + ('type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.RelationTypes')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='annotation_relations', to='api.Project')) ], ) ] diff --git a/app/api/models.py b/app/api/models.py index ae77324b..8f4c1112 100644 --- a/app/api/models.py +++ b/app/api/models.py @@ -265,20 +265,6 @@ class SequenceAnnotation(Annotation): unique_together = ('document', 'user', 'label', 'start_offset', 'end_offset') -class AnnotationRelations(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE) - timestamp = models.DateTimeField(auto_now_add=True) - annotation_id_1 = models.IntegerField() - annotation_id_2 = models.IntegerField() - type = models.CharField(max_length=50) - - def __str__(self): - return f"{self.annotation_id_1} - {self.annotation_id_2} - {type}" - - class Meta: - unique_together = ('timestamp', 'user', 'annotation_id_1', 'annotation_id_2', 'type') - - class RelationTypes(models.Model): color = models.TextField() name = models.TextField() @@ -291,6 +277,21 @@ class RelationTypes(models.Model): unique_together = ('color', 'name') +class AnnotationRelations(models.Model): + annotation_id_1 = models.IntegerField() + annotation_id_2 = models.IntegerField() + type = models.ForeignKey(RelationTypes, related_name='annotation_relations', on_delete=models.CASCADE) + timestamp = models.DateTimeField() + user = models.ForeignKey(User, related_name='annotation_relations', on_delete=models.CASCADE) + project = models.ForeignKey(Project, related_name='annotation_relations', on_delete=models.CASCADE) + + def __str__(self): + return self.__dict__.__str__() + + class Meta: + unique_together = ('annotation_id_1', 'annotation_id_2', 'type', 'project') + + class Seq2seqAnnotation(Annotation): # Override AnnotationManager for custom functionality objects = Seq2seqAnnotationManager() diff --git a/app/api/serializers.py b/app/api/serializers.py index 6ce6206d..66b98912 100644 --- a/app/api/serializers.py +++ b/app/api/serializers.py @@ -10,7 +10,7 @@ from .models import (AutoLabelingConfig, Comment, Document, DocumentAnnotation, Label, Project, Role, RoleMapping, Seq2seqAnnotation, Seq2seqProject, SequenceAnnotation, SequenceLabelingProject, Speech2textAnnotation, - Speech2textProject, TextClassificationProject, RelationTypes) + Speech2textProject, TextClassificationProject, RelationTypes, AnnotationRelations) class UserSerializer(serializers.ModelSerializer): @@ -286,4 +286,14 @@ class RelationTypesSerializer(serializers.ModelSerializer): class Meta: model = RelationTypes - fields = ('id', 'color', 'name') \ No newline at end of file + fields = ('id', 'color', 'name') + + +class AnnotationRelationsSerializer(serializers.ModelSerializer): + + def validate(self, attrs): + return super().validate(attrs) + + class Meta: + model = AnnotationRelations + fields = ('id', 'annotation_id_1', 'annotation_id_2', 'type', 'user', 'timestamp') \ No newline at end of file diff --git a/app/api/urls.py b/app/api/urls.py index a4a1beef..b9a51524 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -38,6 +38,21 @@ urlpatterns_project = [ view=views.RelationTypesDetail.as_view(), name='relation_type_detail' ), + path( + route='annotation_relations', + view=views.AnnotationRelationsList.as_view(), + name='relation_types_list' + ), + path( + route='annotation_relation-upload', + view=views.AnnotationRelationsUploadAPI.as_view(), + name='annotation_relation-upload' + ), + path( + route='annotation_relations/', + view=views.AnnotationRelationsDetail.as_view(), + name='annotation_relation_detail' + ), path( route='docs', view=views.DocumentList.as_view(), diff --git a/app/api/views/__init__.py b/app/api/views/__init__.py index ea35446a..95d983bd 100644 --- a/app/api/views/__init__.py +++ b/app/api/views/__init__.py @@ -10,3 +10,4 @@ from .role import * from .statistics import * from .user import * from .relation_types import * +from .annotation_relations import * diff --git a/app/api/views/annotation_relations.py b/app/api/views/annotation_relations.py new file mode 100644 index 00000000..55db69ec --- /dev/null +++ b/app/api/views/annotation_relations.py @@ -0,0 +1,63 @@ +import json +import logging + +from django.db import IntegrityError, transaction +from django.shortcuts import get_object_or_404 +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 ..exceptions import AnnotationRelationValidationError +from ..models import AnnotationRelations, Project +from ..permissions import IsInProjectReadOnlyOrAdmin, IsProjectAdmin +from ..serializers import AnnotationRelationsSerializer + + +class AnnotationRelationsList(generics.ListCreateAPIView): + serializer_class = AnnotationRelationsSerializer + pagination_class = None + permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin] + + def get_queryset(self): + project = get_object_or_404(Project, pk=self.kwargs['project_id']) + return project.annotation_relations + + def perform_create(self, serializer): + project = get_object_or_404(Project, pk=self.kwargs['project_id']) + serializer.save(project=project) + + def delete(self, request, *args, **kwargs): + delete_ids = request.data['ids'] + AnnotationRelations.objects.filter(pk__in=delete_ids).delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class AnnotationRelationsDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = AnnotationRelations.objects.all() + serializer_class = AnnotationRelationsSerializer + lookup_url_kwarg = 'annotation_relation_id' + permission_classes = [IsAuthenticated & IsInProjectReadOnlyOrAdmin] + + +class AnnotationRelationsUploadAPI(APIView): + parser_classes = (MultiPartParser,) + permission_classes = [IsAuthenticated & IsProjectAdmin] + + @transaction.atomic + def post(self, request, *args, **kwargs): + if 'file' not in request.data: + raise ParseError('Empty content') + project = get_object_or_404(Project, pk=kwargs['project_id']) + try: + annotation_relations = json.load(request.data) + serializer = AnnotationRelationsSerializer(data=annotation_relations, many=True) + serializer.is_valid(raise_exception=True) + serializer.save(project=project) + return Response(status=status.HTTP_201_CREATED) + except json.decoder.JSONDecodeError: + raise ParseError('The file format is invalid.') + except IntegrityError: + raise AnnotationRelationValidationError