From 18373aa1600a50f8abdfa9c163c0b82c1081af4c Mon Sep 17 00:00:00 2001 From: Hironsan Date: Tue, 25 Jan 2022 14:19:31 +0900 Subject: [PATCH] Add SpanManager --- backend/api/managers.py | 13 +++ backend/api/models.py | 8 +- backend/api/tests/test_span.py | 142 +++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 backend/api/tests/test_span.py diff --git a/backend/api/managers.py b/backend/api/managers.py index a8e4fa0c..a500993a 100644 --- a/backend/api/managers.py +++ b/backend/api/managers.py @@ -53,6 +53,19 @@ class CategoryManager(AnnotationManager): return not categories.filter(label=label.label).exists() +class SpanManager(AnnotationManager): + + def can_annotate(self, label, project) -> bool: + overlapping = getattr(project, 'allow_overlapping', False) + spans = self.get_labels(label, project) + if overlapping: + return True + for span in spans: + if span.is_overlapping(label): + return False + return True + + class ExampleManager(Manager): def bulk_create(self, objs, batch_size=None, ignore_conflicts=False): diff --git a/backend/api/models.py b/backend/api/models.py index d2a3f18a..247f31d4 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -9,7 +9,7 @@ from django.db import models from polymorphic.models import PolymorphicModel from .managers import (AnnotationManager, CategoryManager, ExampleManager, - ExampleStateManager) + ExampleStateManager, SpanManager) DOCUMENT_CLASSIFICATION = 'DocumentClassification' SEQUENCE_LABELING = 'SequenceLabeling' @@ -352,6 +352,7 @@ class Category(Annotation): class Span(Annotation): + objects = SpanManager() example = models.ForeignKey( to=Example, on_delete=models.CASCADE, @@ -385,6 +386,11 @@ class Span(Annotation): self.full_clean() super().save(force_insert, force_update, using, update_fields) + def is_overlapping(self, other: 'Span'): + return (other.start_offset <= self.start_offset < other.end_offset) or\ + (other.start_offset < self.end_offset <= other.end_offset) or\ + (self.start_offset < other.start_offset and other.end_offset < self.end_offset) + class Meta: constraints = [ models.CheckConstraint(check=models.Q(start_offset__gte=0), name='startOffset >= 0'), diff --git a/backend/api/tests/test_span.py b/backend/api/tests/test_span.py new file mode 100644 index 00000000..eac8fe00 --- /dev/null +++ b/backend/api/tests/test_span.py @@ -0,0 +1,142 @@ +import abc + +from django.test import TestCase +from model_mommy import mommy + +from api.models import SEQUENCE_LABELING, Span + +from .api.utils import prepare_project + + +class TestSpanAnnotation(abc.ABC, TestCase): + overlapping = False + collaborative = False + + @classmethod + def setUpTestData(cls): + cls.project = prepare_project( + SEQUENCE_LABELING, + allow_overlapping=cls.overlapping, + collaborative_annotation=cls.collaborative + ) + cls.example = mommy.make('Example', project=cls.project.item) + cls.label_type = mommy.make('SpanType', project=cls.project.item) + users = cls.project.users + cls.user = users[0] + cls.another_user = users[1] + cls.span = Span( + example=cls.example, + label=cls.label_type, + user=cls.user, + start_offset=0, + end_offset=5 + ) + + def test_can_annotate_span_to_unannotated_data(self): + can_annotate = Span.objects.can_annotate(self.span, self.project.item) + self.assertTrue(can_annotate) + + +class NonCollaborativeMixin: + + def test_allow_another_user_to_annotate_same_span(self): + mommy.make( + 'Span', + example=self.example, + label=self.label_type, + user=self.another_user, + start_offset=self.span.start_offset, + end_offset=self.span.end_offset + ) + can_annotate = Span.objects.can_annotate(self.span, self.project.item) + self.assertTrue(can_annotate) + + +class TestNonOverlappingSpanAnnotation(TestSpanAnnotation, NonCollaborativeMixin): + overlapping = False + collaborative = False + + def test_cannot_annotate_same_span_to_annotated_data(self): + mommy.make( + 'Span', + example=self.example, + label=self.label_type, + user=self.user, + start_offset=self.span.start_offset, + end_offset=self.span.end_offset, + ) + can_annotate = Span.objects.can_annotate(self.span, self.project.item) + self.assertFalse(can_annotate) + + def test_cannot_annotate_different_span_type_to_annotated_data(self): + mommy.make( + 'Span', + example=self.example, + user=self.user, + start_offset=self.span.start_offset, + end_offset=self.span.end_offset + ) + can_annotate = Span.objects.can_annotate(self.span, self.project.item) + self.assertFalse(can_annotate) + + +class TestOverlappingSpanAnnotation(TestSpanAnnotation, NonCollaborativeMixin): + overlapping = True + collaborative = False + + def test_can_annotate_same_span_to_annotated_data(self): + mommy.make( + 'Span', + example=self.example, + label=self.label_type, + user=self.user, + start_offset=self.span.start_offset, + end_offset=self.span.end_offset, + ) + can_annotate = Span.objects.can_annotate(self.span, self.project.item) + self.assertTrue(can_annotate) + + +class TestCollaborativeNonOverlappingSpanAnnotation(TestSpanAnnotation): + overlapping = False + collaborative = True + + def test_deny_another_user_to_annotate_same_span_type(self): + mommy.make( + 'Span', + example=self.example, + label=self.label_type, + user=self.another_user, + start_offset=self.span.start_offset, + end_offset=self.span.end_offset + ) + can_annotate = Span.objects.can_annotate(self.span, self.project.item) + self.assertFalse(can_annotate) + + def test_deny_another_user_to_annotate_different_span_type(self): + mommy.make( + 'Span', + example=self.example, + user=self.another_user, + start_offset=self.span.start_offset, + end_offset=self.span.end_offset + ) + can_annotate = Span.objects.can_annotate(self.span, self.project.item) + self.assertFalse(can_annotate) + + +class TestCollaborativeOverlappingSpanAnnotation(TestSpanAnnotation): + overlapping = True + collaborative = True + + def test_allow_another_user_to_annotate_same_span(self): + mommy.make( + 'Span', + example=self.example, + label=self.label_type, + user=self.another_user, + start_offset=self.span.start_offset, + end_offset=self.span.end_offset + ) + can_annotate = Span.objects.can_annotate(self.span, self.project.item) + self.assertTrue(can_annotate)