Skip to content

Commit

Permalink
Merge pull request #1583 from doccano/fix/constraint
Browse files Browse the repository at this point in the history
Update unique constraint of Span
  • Loading branch information
Hironsan committed Dec 2, 2021
2 parents 2a37d5d + 1c9c682 commit 3350fd1
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 38 deletions.
17 changes: 12 additions & 5 deletions backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,14 +284,21 @@ class Span(Annotation):

def validate_unique(self, exclude=None):
allow_overlapping = getattr(self.example.project, 'allow_overlapping', False)
is_collaborative = self.example.project.collaborative_annotation
if allow_overlapping:
super().validate_unique(exclude=exclude)
return

overlapping_span = Span.objects.exclude(id=self.id).filter(example=self.example).filter(
models.Q(start_offset__gte=self.start_offset, start_offset__lt=self.end_offset) |
models.Q(end_offset__gt=self.start_offset, end_offset__lte=self.end_offset) |
models.Q(start_offset__lte=self.start_offset, end_offset__gte=self.end_offset)
)
if is_collaborative:
if overlapping_span.exists():
raise ValidationError('This overlapping is not allowed in this project.')
else:
if Span.objects.exclude(id=self.id).filter(example=self.example).filter(
models.Q(start_offset__gte=self.start_offset, start_offset__lt=self.end_offset) |
models.Q(end_offset__gt=self.start_offset, end_offset__lte=self.end_offset) |
models.Q(start_offset__lte=self.start_offset, end_offset__gte=self.end_offset)
).exists():
if overlapping_span.filter(user=self.user).exists():
raise ValidationError('This overlapping is not allowed in this project.')

def save(self, force_insert=False, force_update=False, using=None,
Expand Down
11 changes: 7 additions & 4 deletions backend/api/tests/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ def make_project(
task: str,
users: List[str],
roles: List[str] = None,
collaborative_annotation=False):
collaborative_annotation=False,
**kwargs):
create_default_roles()

# create users.
Expand All @@ -68,7 +69,8 @@ def make_project(
_model=project_model,
project_type=task,
users=users,
collaborative_annotation=collaborative_annotation
collaborative_annotation=collaborative_annotation,
**kwargs
)

# assign roles to the users.
Expand Down Expand Up @@ -119,7 +121,7 @@ def make_annotation(task, doc, user, **kwargs):
return mommy.make(annotation_model, example=doc, user=user, **kwargs)


def prepare_project(task: str = 'Any', collaborative_annotation=False):
def prepare_project(task: str = 'Any', collaborative_annotation=False, **kwargs):
return make_project(
task=task,
users=['admin', 'approver', 'annotator'],
Expand All @@ -128,7 +130,8 @@ def prepare_project(task: str = 'Any', collaborative_annotation=False):
settings.ROLE_ANNOTATION_APPROVER,
settings.ROLE_ANNOTATOR,
],
collaborative_annotation=collaborative_annotation
collaborative_annotation=collaborative_annotation,
**kwargs
)


Expand Down
85 changes: 62 additions & 23 deletions backend/api/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from django.test import TestCase, override_settings
from model_mommy import mommy

from ..models import (Category, Label, Span, TextLabel,
from ..models import (SEQUENCE_LABELING, Category, Label, Span, TextLabel,
generate_random_hex_color)
from .api.utils import prepare_project


@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
Expand Down Expand Up @@ -117,7 +118,12 @@ def test_uniqueness(self):
Category(example=a.example, user=a.user, label=a.label).save()


class TestSequenceAnnotation(TestCase):
class TestSpan(TestCase):

def setUp(self):
self.project = prepare_project(SEQUENCE_LABELING, allow_overlapping=False)
self.example = mommy.make('Example', project=self.project.item)
self.user = self.project.users[0]

def test_start_offset_is_not_negative(self):
with self.assertRaises(IntegrityError):
Expand All @@ -131,33 +137,66 @@ def test_start_offset_is_less_than_end_offset(self):
with self.assertRaises(IntegrityError):
mommy.make('Span', start_offset=0, end_offset=0)

def test_overlapping(self):
project = mommy.make('SequenceLabelingProject', allow_overlapping=False)
example = mommy.make('Example', project=project)
mommy.make('Span', example=example, start_offset=5, end_offset=10)
with self.assertRaises(ValidationError):
mommy.make('Span', example=example, start_offset=5, end_offset=10)
with self.assertRaises(ValidationError):
mommy.make('Span', example=example, start_offset=5, end_offset=11)
with self.assertRaises(ValidationError):
mommy.make('Span', example=example, start_offset=4, end_offset=10)
with self.assertRaises(ValidationError):
mommy.make('Span', example=example, start_offset=6, end_offset=9)
with self.assertRaises(ValidationError):
mommy.make('Span', example=example, start_offset=9, end_offset=15)
with self.assertRaises(ValidationError):
mommy.make('Span', example=example, start_offset=0, end_offset=6)
mommy.make('Span', example=example, start_offset=0, end_offset=5)
mommy.make('Span', example=example, start_offset=10, end_offset=15)
def test_unique_constraint(self):
mommy.make('Span', example=self.example, start_offset=5, end_offset=10, user=self.user)
mommy.make('Span', example=self.example, start_offset=0, end_offset=5, user=self.user)
mommy.make('Span', example=self.example, start_offset=10, end_offset=15, user=self.user)

def test_unique_constraint_violated(self):
mommy.make('Span', example=self.example, start_offset=5, end_offset=10, user=self.user)
spans = [(5, 10), (5, 11), (4, 10), (6, 9), (9, 15), (0, 6)]
for start_offset, end_offset in spans:
with self.assertRaises(ValidationError):
mommy.make(
'Span',
example=self.example,
start_offset=start_offset,
end_offset=end_offset,
user=self.user
)

def test_unique_constraint_if_overlapping_is_allowed(self):
project = prepare_project(SEQUENCE_LABELING, allow_overlapping=True)
example = mommy.make('Example', project=project.item)
user = project.users[0]
mommy.make('Span', example=example, start_offset=5, end_offset=10, user=user)
spans = [(5, 10), (5, 11), (4, 10), (6, 9), (9, 15), (0, 6)]
for start_offset, end_offset in spans:
mommy.make('Span', example=example, start_offset=start_offset, end_offset=end_offset, user=user)

def test_update(self):
project = mommy.make('SequenceLabelingProject', allow_overlapping=False)
example = mommy.make('Example', project=project)
span = mommy.make('Span', example=example, start_offset=0, end_offset=5)
span = mommy.make('Span', example=self.example, start_offset=0, end_offset=5)
span.end_offset = 6
span.save()


class TestSpanWithoutCollaborativeMode(TestCase):

def setUp(self):
self.project = prepare_project(SEQUENCE_LABELING, False, allow_overlapping=False)
self.example = mommy.make('Example', project=self.project.item)

def test_allow_users_to_create_same_spans(self):
mommy.make('Span', example=self.example, start_offset=5, end_offset=10, user=self.project.users[0])
mommy.make('Span', example=self.example, start_offset=5, end_offset=10, user=self.project.users[1])


class TestSpanWithCollaborativeMode(TestCase):

def test_deny_users_to_create_same_spans(self):
project = prepare_project(SEQUENCE_LABELING, True, allow_overlapping=False)
example = mommy.make('Example', project=project.item)
mommy.make('Span', example=example, start_offset=5, end_offset=10, user=project.users[0])
with self.assertRaises(ValidationError):
mommy.make('Span', example=example, start_offset=5, end_offset=10, user=project.users[1])

def test_allow_users_to_create_same_spans_if_overlapping_is_allowed(self):
project = prepare_project(SEQUENCE_LABELING, True, allow_overlapping=True)
example = mommy.make('Example', project=project.item)
mommy.make('Span', example=example, start_offset=5, end_offset=10, user=project.users[0])
mommy.make('Span', example=example, start_offset=5, end_offset=10, user=project.users[1])


class TestSeq2seqAnnotation(TestCase):

def test_uniqueness(self):
Expand Down
7 changes: 6 additions & 1 deletion backend/api/views/annotation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404
from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated
Expand Down Expand Up @@ -34,7 +35,11 @@ def create(self, request, *args, **kwargs):
if self.project.single_class_classification:
self.get_queryset().delete()
request.data['example'] = self.kwargs['doc_id']
return super().create(request, args, kwargs)
try:
response = super().create(request, args, kwargs)
except ValidationError as err:
response = Response({'detail': err.messages}, status=status.HTTP_400_BAD_REQUEST)
return response

def perform_create(self, serializer):
serializer.save(example_id=self.kwargs['doc_id'], user=self.request.user)
Expand Down
5 changes: 3 additions & 2 deletions frontend/components/tasks/sequenceLabeling/EntityEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,9 @@ export default Vue.extend({
// Todo: a bit hacky. I want to fix this problem.
// https://github.com/vuetifyjs/vuetify/issues/10765
this.$nextTick(() => {
// @ts-ignore
this.$refs.autocomplete!.selectedItems = []
if (this.$refs.autocomplete) {
(this.$refs.autocomplete as any).selectedItems = []
}
})
},
Expand Down
10 changes: 10 additions & 0 deletions frontend/pages/projects/_id/sequence-labeling/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,19 @@ export default {
},
methods: {
async maybeFetchLabels(annotations) {
const labelIds = new Set(this.labels.map((label) => label.id));
if (annotations.some((item) => !labelIds.has(item.label))) {
this.labels = await this.$services.label.list(this.projectId);
}
},
async list(docId) {
const annotations = await this.$services.sequenceLabeling.list(this.projectId, docId);
const links = await this.$services.sequenceLabeling.listLinks(this.projectId);
// In colab mode, if someone add a new label and annotate data with the label during your work,
// it occurs exception because there is no corresponding label.
await this.maybeFetchLabels(annotations);
this.annotations = annotations;
this.links = links;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ export class AnnotationApplicationService<T extends AnnotationModel> {
) {}

public async delete(projectId: string, docId: number, annotationId: number): Promise<void> {
await this.repository.delete(projectId, docId, annotationId)
try {
await this.repository.delete(projectId, docId, annotationId)
} catch(e) {
console.log(e.response.data.detail)
}
}

public async clear(projectId: string, docId: number): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ export class SequenceLabelingApplicationService extends AnnotationApplicationSer

public async create(projectId: string, docId: number, labelId: number, startOffset: number, endOffset: number): Promise<void> {
const item = new SequenceLabelingLabel(0, labelId, 0, startOffset, endOffset)
await this.repository.create(projectId, docId, item)
try {
await this.repository.create(projectId, docId, item)
} catch(e) {
console.log(e.response.data.detail)
}
}

public async changeLabel(projectId: string, docId: number, annotationId: number, labelId: number): Promise<void> {
await this.repository.update(projectId, docId, annotationId, labelId)
try {
await this.repository.update(projectId, docId, annotationId, labelId)
} catch(e) {
console.log(e.response.data.detail)
}
}

public async listLinks(projectId: string): Promise<LinkItem[]> {
Expand Down

0 comments on commit 3350fd1

Please sign in to comment.