Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update unique constraint of Span #1583

Merged
merged 4 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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