Browse Source

Update models to handle image classification

pull/1370/head
Hironsan 3 years ago
parent
commit
a44cad79f7
12 changed files with 207 additions and 166 deletions
  1. 34
      backend/api/admin.py
  2. 16
      backend/api/filters.py
  3. 4
      backend/api/managers.py
  4. 137
      backend/api/models.py
  5. 93
      backend/api/serializers.py
  6. 6
      backend/api/tests/api/test_annotation.py
  7. 4
      backend/api/tests/api/test_statistics.py
  8. 10
      backend/api/tests/api/utils.py
  9. 39
      backend/api/tests/test_models.py
  10. 19
      backend/api/views/annotation.py
  11. 3
      backend/api/views/example.py
  12. 8
      backend/api/views/statistics.py

34
backend/api/admin.py

@ -1,9 +1,8 @@
from django.contrib import admin
from .models import (Comment, Document, DocumentAnnotation, Label, Project,
Role, RoleMapping, Seq2seqAnnotation, Seq2seqProject,
SequenceAnnotation, SequenceLabelingProject, Tag,
TextClassificationProject)
from .models import (Category, Comment, Document, Label, Project, Role,
RoleMapping, Seq2seqProject, SequenceLabelingProject,
Span, Tag, TextClassificationProject, TextLabel)
class LabelAdmin(admin.ModelAdmin):
@ -24,22 +23,19 @@ class ProjectAdmin(admin.ModelAdmin):
search_fields = ('name',)
class SequenceAnnotationAdmin(admin.ModelAdmin):
list_display = ('document', 'label', 'start_offset', 'user')
ordering = ('document',)
search_fields = ('document__text',)
class SpanAdmin(admin.ModelAdmin):
list_display = ('example', 'label', 'start_offset', 'user')
ordering = ('example',)
class DocumentAnnotationAdmin(admin.ModelAdmin):
list_display = ('document', 'label', 'user')
ordering = ('document',)
search_fields = ('document__text',)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('example', 'label', 'user')
ordering = ('example',)
class Seq2seqAnnotationAdmin(admin.ModelAdmin):
list_display = ('document', 'text', 'user')
ordering = ('document',)
search_fields = ('document__text',)
class TextLabelAdmin(admin.ModelAdmin):
list_display = ('example', 'text', 'user')
ordering = ('example',)
class RoleAdmin(admin.ModelAdmin):
@ -66,9 +62,9 @@ class CommentAdmin(admin.ModelAdmin):
search_fields = ('user',)
admin.site.register(DocumentAnnotation, DocumentAnnotationAdmin)
admin.site.register(SequenceAnnotation, SequenceAnnotationAdmin)
admin.site.register(Seq2seqAnnotation, Seq2seqAnnotationAdmin)
admin.site.register(Category, CategoryAdmin)
admin.site.register(Span, SpanAdmin)
admin.site.register(TextLabel, TextLabelAdmin)
admin.site.register(Label, LabelAdmin)
admin.site.register(Document, DocumentAdmin)
admin.site.register(Project, ProjectAdmin)

16
backend/api/filters.py

@ -5,10 +5,9 @@ from .models import Document
class DocumentFilter(FilterSet):
seq_annotations__isnull = BooleanFilter(field_name='seq_annotations', method='filter_annotations')
doc_annotations__isnull = BooleanFilter(field_name='doc_annotations', method='filter_annotations')
seq2seq_annotations__isnull = BooleanFilter(field_name='seq2seq_annotations', method='filter_annotations')
speech2text_annotations__isnull = BooleanFilter(field_name='speech2text_annotations', method='filter_annotations')
spans__isnull = BooleanFilter(field_name='spans', method='filter_annotations')
categories__isnull = BooleanFilter(field_name='categories', method='filter_annotations')
texts__isnull = BooleanFilter(field_name='texts', method='filter_annotations')
def filter_annotations(self, queryset, field_name, value):
queryset = queryset.annotate(num_annotations=Count(
@ -27,7 +26,8 @@ class DocumentFilter(FilterSet):
class Meta:
model = Document
fields = ('project', 'text', 'created_at', 'updated_at',
'doc_annotations__label__id', 'seq_annotations__label__id',
'doc_annotations__isnull', 'seq_annotations__isnull', 'seq2seq_annotations__isnull',
'speech2text_annotations__isnull')
fields = (
'project', 'text', 'created_at', 'updated_at',
'categories__label__id', 'spans__label__id',
'categories__isnull', 'spans__isnull', 'texts__isnull'
)

4
backend/api/managers.py

@ -10,7 +10,7 @@ class AnnotationManager(Manager):
label_count = Counter()
user_count = Counter()
docs = project.examples.all()
annotations = self.filter(document_id__in=docs.all())
annotations = self.filter(example_id__in=docs.all())
for d in annotations.values('label__text', 'user__username').annotate(Count('label'), Count('user')):
label_count[d['label__text']] += d['label__count']
@ -25,7 +25,7 @@ class Seq2seqAnnotationManager(Manager):
label_count = Counter()
user_count = Counter()
docs = project.examples.all()
annotations = self.filter(document_id__in=docs.all())
annotations = self.filter(example_id__in=docs.all())
for d in annotations.values('text', 'user__username').annotate(Count('text'), Count('user')):
label_count[d['text']] += d['text__count']

137
backend/api/models.py

@ -45,31 +45,31 @@ class Project(PolymorphicModel):
class TextClassificationProject(Project):
def get_annotation_class(self):
return DocumentAnnotation
return Category
class SequenceLabelingProject(Project):
def get_annotation_class(self):
return SequenceAnnotation
return Span
class Seq2seqProject(Project):
def get_annotation_class(self):
return Seq2seqAnnotation
return TextLabel
class Speech2textProject(Project):
def get_annotation_class(self):
return Speech2textAnnotation
return TextLabel
class ImageClassificationProject(Project):
def get_annotation_class(self):
return ImageCategoryLabel
return Category
class Label(models.Model):
@ -116,10 +116,19 @@ class Label(models.Model):
class Example(PolymorphicModel):
meta = models.JSONField(default=dict)
filename = models.FileField(default='.')
project = models.ForeignKey(Project, related_name='examples', on_delete=models.CASCADE)
project = models.ForeignKey(
to=Project,
on_delete=models.CASCADE,
related_name='examples'
)
annotations_approved_by = models.ForeignKey(
to=User,
on_delete=models.SET_NULL,
null=True,
blank=True
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
annotations_approved_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
@property
def comment_count(self):
@ -128,22 +137,26 @@ class Example(PolymorphicModel):
class Document(Example):
text = models.TextField()
example = models.OneToOneField(Example,
on_delete=models.CASCADE,
primary_key=True,
db_column='id',
parent_link=True)
example = models.OneToOneField(
to=Example,
on_delete=models.CASCADE,
primary_key=True,
db_column='id',
parent_link=True
)
def __str__(self):
return self.text[:50]
class Image(Example):
example = models.OneToOneField(Example,
on_delete=models.CASCADE,
primary_key=True,
db_column='id',
parent_link=True)
example = models.OneToOneField(
to=Example,
on_delete=models.CASCADE,
primary_key=True,
db_column='id',
parent_link=True
)
def __str__(self):
return self.filename
@ -151,8 +164,16 @@ class Image(Example):
class Comment(models.Model):
text = models.TextField()
example = models.ForeignKey(Example, related_name='comments', on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
example = models.ForeignKey(
to=Example,
on_delete=models.CASCADE,
related_name='comments'
)
user = models.ForeignKey(
to=User,
on_delete=models.CASCADE,
null=True
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@ -166,7 +187,11 @@ class Comment(models.Model):
class Tag(models.Model):
text = models.TextField()
project = models.ForeignKey(Project, related_name='tags', on_delete=models.CASCADE)
project = models.ForeignKey(
to=Project,
on_delete=models.CASCADE,
related_name='tags'
)
def __str__(self):
return self.text
@ -185,17 +210,35 @@ class Annotation(models.Model):
abstract = True
class DocumentAnnotation(Annotation):
document = models.ForeignKey(Document, related_name='doc_annotations', on_delete=models.CASCADE)
label = models.ForeignKey(Label, on_delete=models.CASCADE)
class Category(Annotation):
example = models.ForeignKey(
to=Example,
on_delete=models.CASCADE,
related_name='categories'
)
label = models.ForeignKey(
to=Label,
on_delete=models.CASCADE
)
class Meta:
unique_together = ('document', 'user', 'label')
unique_together = (
'example',
'user',
'label'
)
class SequenceAnnotation(Annotation):
document = models.ForeignKey(Document, related_name='seq_annotations', on_delete=models.CASCADE)
label = models.ForeignKey(Label, on_delete=models.CASCADE)
class Span(Annotation):
example = models.ForeignKey(
to=Example,
on_delete=models.CASCADE,
related_name='spans'
)
label = models.ForeignKey(
to=Label,
on_delete=models.CASCADE
)
start_offset = models.IntegerField()
end_offset = models.IntegerField()
@ -204,34 +247,30 @@ class SequenceAnnotation(Annotation):
raise ValidationError('start_offset is after end_offset')
class Meta:
unique_together = ('document', 'user', 'label', 'start_offset', 'end_offset')
unique_together = (
'example',
'user',
'label',
'start_offset',
'end_offset'
)
class Seq2seqAnnotation(Annotation):
# Override AnnotationManager for custom functionality
class TextLabel(Annotation):
objects = Seq2seqAnnotationManager()
document = models.ForeignKey(Document, related_name='seq2seq_annotations', on_delete=models.CASCADE)
text = models.CharField(max_length=500)
class Meta:
unique_together = ('document', 'user', 'text')
class Speech2textAnnotation(Annotation):
document = models.ForeignKey(Document, related_name='speech2text_annotations', on_delete=models.CASCADE)
example = models.ForeignKey(
to=Example,
on_delete=models.CASCADE,
related_name='texts'
)
text = models.TextField()
class Meta:
unique_together = ('document', 'user')
class ImageCategoryLabel(Annotation):
image = models.ForeignKey(Image, related_name='categories', on_delete=models.CASCADE)
label = models.ForeignKey(Label, on_delete=models.CASCADE)
class Meta:
unique_together = ('image', 'user', 'label')
unique_together = (
'example',
'user',
'text'
)
class Role(models.Model):

93
backend/api/serializers.py

@ -7,13 +7,11 @@ from rest_framework.exceptions import ValidationError
from rest_polymorphic.serializers import PolymorphicSerializer
from .models import (DOCUMENT_CLASSIFICATION, SEQ2SEQ, SEQUENCE_LABELING,
SPEECH2TEXT, AutoLabelingConfig, Comment, Document,
DocumentAnnotation, Example, Image, ImageCategoryLabel,
ImageClassificationProject, Label, Project, Role,
RoleMapping, Seq2seqAnnotation, Seq2seqProject,
SequenceAnnotation, SequenceLabelingProject,
Speech2textAnnotation, Speech2textProject, Tag,
TextClassificationProject)
SPEECH2TEXT, AutoLabelingConfig, Category, Comment,
Document, Example, Image, ImageClassificationProject,
Label, Project, Role, RoleMapping, Seq2seqProject,
SequenceLabelingProject, Span, Speech2textProject, Tag,
TextClassificationProject, TextLabel)
class UserSerializer(serializers.ModelSerializer):
@ -89,7 +87,7 @@ class BaseDataSerializer(serializers.ModelSerializer):
project = instance.project
model = project.get_annotation_class()
serializer = get_annotation_serializer(task=project.project_type)
annotations = model.objects.filter(document=instance.id)
annotations = model.objects.filter(example=instance.id)
if request and not project.collaborative_annotation:
annotations = annotations.filter(user=request.user)
serializer = serializer(annotations, many=True)
@ -225,51 +223,58 @@ class ProjectFilteredPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
return queryset.filter(project=view.kwargs['project_id'])
class DocumentAnnotationSerializer(serializers.ModelSerializer):
class CategorySerializer(serializers.ModelSerializer):
label = serializers.PrimaryKeyRelatedField(queryset=Label.objects.all())
document = serializers.PrimaryKeyRelatedField(queryset=Document.objects.all())
example = serializers.PrimaryKeyRelatedField(queryset=Example.objects.all())
class Meta:
model = DocumentAnnotation
fields = ('id', 'prob', 'label', 'user', 'document', 'created_at', 'updated_at')
read_only_fields = ('user', )
class SequenceAnnotationSerializer(serializers.ModelSerializer):
label = serializers.PrimaryKeyRelatedField(queryset=Label.objects.all())
document = serializers.PrimaryKeyRelatedField(queryset=Document.objects.all())
class Meta:
model = SequenceAnnotation
fields = ('id', 'prob', 'label', 'start_offset', 'end_offset', 'user', 'document', 'created_at', 'updated_at')
read_only_fields = ('user',)
class Seq2seqAnnotationSerializer(serializers.ModelSerializer):
document = serializers.PrimaryKeyRelatedField(queryset=Document.objects.all())
class Meta:
model = Seq2seqAnnotation
fields = ('id', 'text', 'user', 'document', 'prob', 'created_at', 'updated_at')
model = Category
fields = (
'id',
'prob',
'user',
'example',
'created_at',
'updated_at',
'label',
)
read_only_fields = ('user',)
class Speech2textAnnotationSerializer(serializers.ModelSerializer):
document = serializers.PrimaryKeyRelatedField(queryset=Document.objects.all())
class SpanSerializer(serializers.ModelSerializer):
label = serializers.PrimaryKeyRelatedField(queryset=Label.objects.all())
example = serializers.PrimaryKeyRelatedField(queryset=Example.objects.all())
class Meta:
model = Speech2textAnnotation
fields = ('id', 'prob', 'text', 'user', 'document', 'created_at', 'updated_at')
model = Span
fields = (
'id',
'prob',
'user',
'example',
'created_at',
'updated_at',
'label',
'start_offset',
'end_offset',
)
read_only_fields = ('user',)
class ImageClassificationLabelSerializer(serializers.ModelSerializer):
label = serializers.PrimaryKeyRelatedField(queryset=Label.objects.all())
image = serializers.PrimaryKeyRelatedField(queryset=Image.objects.all())
class TextLabelSerializer(serializers.ModelSerializer):
example = serializers.PrimaryKeyRelatedField(queryset=Example.objects.all())
class Meta:
model = ImageCategoryLabel
fields = ('id', 'prob', 'label', 'user', 'image', 'created_at', 'updated_at')
model = TextLabel
fields = (
'id',
'prob',
'user',
'example',
'created_at',
'updated_at',
'text',
)
read_only_fields = ('user',)
@ -334,10 +339,10 @@ class AutoLabelingConfigSerializer(serializers.ModelSerializer):
def get_annotation_serializer(task: str):
mapping = {
DOCUMENT_CLASSIFICATION: DocumentAnnotationSerializer,
SEQUENCE_LABELING: SequenceAnnotationSerializer,
SEQ2SEQ: Seq2seqAnnotationSerializer,
SPEECH2TEXT: Speech2textAnnotationSerializer
DOCUMENT_CLASSIFICATION: CategorySerializer,
SEQUENCE_LABELING: SpanSerializer,
SEQ2SEQ: TextLabelSerializer,
SPEECH2TEXT: TextLabelSerializer
}
try:
return mapping[task]

6
backend/api/tests/api/test_annotation.py

@ -1,7 +1,7 @@
from rest_framework import status
from rest_framework.reverse import reverse
from ...models import DOCUMENT_CLASSIFICATION, DocumentAnnotation
from ...models import DOCUMENT_CLASSIFICATION, Category
from .utils import (CRUDMixin, make_annotation, make_doc, make_label,
make_user, prepare_project)
@ -30,7 +30,7 @@ class TestAnnotationList(CRUDMixin):
def test_allows_project_member_to_bulk_delete_annotation(self):
self.assert_delete(self.project.users[0], status.HTTP_204_NO_CONTENT)
count = DocumentAnnotation.objects.count()
count = Category.objects.count()
self.assertEqual(count, 2) # delete only own annotation
@ -51,7 +51,7 @@ class TestSharedAnnotationList(CRUDMixin):
def test_allows_project_member_to_bulk_delete_annotation(self):
self.assert_delete(self.project.users[0], status.HTTP_204_NO_CONTENT)
count = DocumentAnnotation.objects.count()
count = Category.objects.count()
self.assertEqual(count, 0) # delete all annotation in the doc

4
backend/api/tests/api/test_statistics.py

@ -34,8 +34,8 @@ class TestStatisticsAPI(APITestCase, TestUtilsMixin):
)
doc1 = mommy.make('Document', project=cls.project)
doc2 = mommy.make('Document', project=cls.project)
mommy.make('DocumentAnnotation', document=doc1, user=super_user)
mommy.make('DocumentAnnotation', document=doc2, user=other_user)
mommy.make('Category', example=doc1, user=super_user)
mommy.make('Category', example=doc2, user=other_user)
cls.url = reverse(viewname='statistics', args=[cls.project.id])
cls.doc = Document.objects.filter(project=cls.project)

10
backend/api/tests/api/utils.py

@ -102,12 +102,12 @@ def make_comment(doc, user):
def make_annotation(task, doc, user):
annotation_model = {
DOCUMENT_CLASSIFICATION: 'DocumentAnnotation',
SEQUENCE_LABELING: 'SequenceAnnotation',
SEQ2SEQ: 'Seq2seqAnnotation',
SPEECH2TEXT: 'Speech2textAnnotation'
DOCUMENT_CLASSIFICATION: 'Category',
SEQUENCE_LABELING: 'Span',
SEQ2SEQ: 'TextLabel',
SPEECH2TEXT: 'TextLabel'
}.get(task)
return mommy.make(annotation_model, document=doc, user=user)
return mommy.make(annotation_model, example=doc, user=user)
def prepare_project(task: str = 'Any', collaborative_annotation=False):

39
backend/api/tests/test_models.py

@ -3,8 +3,7 @@ from django.db.utils import IntegrityError
from django.test import TestCase, override_settings
from model_mommy import mommy
from ..models import (DocumentAnnotation, Label, Seq2seqAnnotation,
SequenceAnnotation, Speech2textAnnotation)
from ..models import Category, Label, Span, TextLabel
@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
@ -16,7 +15,7 @@ class TestTextClassificationProject(TestCase):
def test_get_annotation_class(self):
klass = self.project.get_annotation_class()
self.assertEqual(klass, DocumentAnnotation)
self.assertEqual(klass, Category)
@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
@ -28,7 +27,7 @@ class TestSequenceLabelingProject(TestCase):
def test_get_annotation_class(self):
klass = self.project.get_annotation_class()
self.assertEqual(klass, SequenceAnnotation)
self.assertEqual(klass, Span)
@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
@ -40,7 +39,7 @@ class TestSeq2seqProject(TestCase):
def test_get_annotation_class(self):
klass = self.project.get_annotation_class()
self.assertEqual(klass, Seq2seqAnnotation)
self.assertEqual(klass, TextLabel)
@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
@ -52,7 +51,7 @@ class TestSpeech2textProject(TestCase):
def test_get_annotation_class(self):
klass = self.project.get_annotation_class()
self.assertEqual(klass, Speech2textAnnotation)
self.assertEqual(klass, TextLabel)
class TestLabel(TestCase):
@ -109,36 +108,36 @@ class TestLabel(TestCase):
self.fail(msg=ValidationError)
class TestDocumentAnnotation(TestCase):
class TestCategory(TestCase):
def test_uniqueness(self):
a = mommy.make('DocumentAnnotation')
a = mommy.make('Category')
with self.assertRaises(IntegrityError):
DocumentAnnotation(document=a.document, user=a.user, label=a.label).save()
Category(example=a.example, user=a.user, label=a.label).save()
class TestSequenceAnnotation(TestCase):
def test_uniqueness(self):
a = mommy.make('SequenceAnnotation')
a = mommy.make('Span')
with self.assertRaises(IntegrityError):
SequenceAnnotation(document=a.document,
user=a.user,
label=a.label,
start_offset=a.start_offset,
end_offset=a.end_offset).save()
Span(example=a.example,
user=a.user,
label=a.label,
start_offset=a.start_offset,
end_offset=a.end_offset).save()
def test_position_constraint(self):
with self.assertRaises(ValidationError):
mommy.make('SequenceAnnotation',
mommy.make('Span',
start_offset=1, end_offset=0).clean()
class TestSeq2seqAnnotation(TestCase):
def test_uniqueness(self):
a = mommy.make('Seq2seqAnnotation')
a = mommy.make('TextLabel')
with self.assertRaises(IntegrityError):
Seq2seqAnnotation(document=a.document,
user=a.user,
text=a.text).save()
TextLabel(example=a.example,
user=a.user,
text=a.text).save()

19
backend/api/views/annotation.py

@ -25,7 +25,7 @@ class AnnotationList(generics.ListCreateAPIView):
def get_queryset(self):
model = self.project.get_annotation_class()
queryset = model.objects.filter(document=self.kwargs['doc_id'])
queryset = model.objects.filter(example=self.kwargs['doc_id'])
if not self.project.collaborative_annotation:
queryset = queryset.filter(user=self.request.user)
return queryset
@ -33,11 +33,11 @@ class AnnotationList(generics.ListCreateAPIView):
def create(self, request, *args, **kwargs):
if self.project.single_class_classification:
self.get_queryset().delete()
request.data['document'] = self.kwargs['doc_id']
request.data['example'] = self.kwargs['doc_id']
return super().create(request, args, kwargs)
def perform_create(self, serializer):
serializer.save(document_id=self.kwargs['doc_id'], user=self.request.user)
serializer.save(example_id=self.kwargs['doc_id'], user=self.request.user)
def delete(self, request, *args, **kwargs):
queryset = self.get_queryset()
@ -49,22 +49,23 @@ class AnnotationDetail(generics.RetrieveUpdateDestroyAPIView):
lookup_url_kwarg = 'annotation_id'
swagger_schema = None
@property
def project(self):
return get_object_or_404(Project, pk=self.kwargs['project_id'])
def get_permissions(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
if project.collaborative_annotation:
if self.project.collaborative_annotation:
self.permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
else:
self.permission_classes = [IsAuthenticated & IsInProjectOrAdmin & IsOwnAnnotation]
return super().get_permissions()
def get_serializer_class(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
self.serializer_class = get_annotation_serializer(task=project.project_type)
self.serializer_class = get_annotation_serializer(task=self.project.project_type)
return self.serializer_class
def get_queryset(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
model = project.get_annotation_class()
model = self.project.get_annotation_class()
self.queryset = model.objects.all()
return self.queryset

3
backend/api/views/example.py

@ -10,7 +10,8 @@ from rest_framework.response import Response
from ..filters import DocumentFilter
from ..models import Document, Example, Image, Project
from ..permissions import IsInProjectReadOnlyOrAdmin
from ..serializers import DocumentSerializer, ExampleSerializer, ImageSerializer
from ..serializers import (DocumentSerializer, ExampleSerializer,
ImageSerializer)
class ExampleList(generics.ListCreateAPIView):

8
backend/api/views/statistics.py

@ -39,20 +39,20 @@ class StatisticsAPI(APIView):
def _get_user_completion_data(annotation_class, annotation_filter):
all_annotation_objects = annotation_class.objects.filter(annotation_filter)
set_user_data = collections.defaultdict(set)
for ind_obj in all_annotation_objects.values('user__username', 'document__id'):
set_user_data[ind_obj['user__username']].add(ind_obj['document__id'])
for ind_obj in all_annotation_objects.values('user__username', 'example__id'):
set_user_data[ind_obj['user__username']].add(ind_obj['example__id'])
return {i: len(set_user_data[i]) for i in set_user_data}
def progress(self, project):
docs = project.examples
annotation_class = project.get_annotation_class()
total = docs.count()
annotation_filter = Q(document_id__in=docs.all())
annotation_filter = Q(example_id__in=docs.all())
user_data = self._get_user_completion_data(annotation_class, annotation_filter)
if not project.collaborative_annotation:
annotation_filter &= Q(user_id=self.request.user)
done = annotation_class.objects.filter(annotation_filter)\
.aggregate(Count('document', distinct=True))['document__count']
.aggregate(Count('example', distinct=True))['example__count']
remaining = total - done
return {'total': total, 'remaining': remaining, 'user': user_data}

Loading…
Cancel
Save