diff --git a/app/server/api.py b/app/server/api.py index 93d8c0e4..736bec98 100644 --- a/app/server/api.py +++ b/app/server/api.py @@ -3,7 +3,7 @@ from collections import Counter from django.conf import settings from django.shortcuts import get_object_or_404, redirect from django_filters.rest_framework import DjangoFilterBackend -from django.db.models import Count +from django.db.models import Count, F from libcloud.base import DriverType, get_driver from libcloud.storage.types import ContainerDoesNotExistError, ObjectDoesNotExistError from rest_framework import generics, filters, status @@ -129,7 +129,13 @@ class DocumentList(generics.ListCreateAPIView): permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUserAndWriteOnly) def get_queryset(self): - queryset = self.queryset.filter(project=self.kwargs['project_id']) + project = get_object_or_404(Project, pk=self.kwargs['project_id']) + + queryset = self.queryset.filter(project=project) + + if project.randomize_document_order: + queryset = queryset.annotate(sort_id=F('id') % self.request.user.id).order_by('sort_id') + return queryset def perform_create(self, serializer): diff --git a/app/server/forms.py b/app/server/forms.py index b21c3c55..6cce9578 100644 --- a/app/server/forms.py +++ b/app/server/forms.py @@ -7,4 +7,4 @@ class ProjectForm(forms.ModelForm): class Meta: model = Project - fields = ('name', 'description', 'project_type', 'users') + fields = ('name', 'description', 'project_type', 'users', 'randomize_document_order') diff --git a/app/server/migrations/0002_project_randomize_document_order.py b/app/server/migrations/0002_project_randomize_document_order.py new file mode 100644 index 00000000..8c76d160 --- /dev/null +++ b/app/server/migrations/0002_project_randomize_document_order.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.7 on 2019-05-22 18:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('server', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='randomize_document_order', + field=models.BooleanField(default=False), + ), + ] diff --git a/app/server/models.py b/app/server/models.py index 5049d173..2b661189 100644 --- a/app/server/models.py +++ b/app/server/models.py @@ -25,6 +25,7 @@ class Project(PolymorphicModel): updated_at = models.DateTimeField(auto_now=True) users = models.ManyToManyField(User, related_name='projects') project_type = models.CharField(max_length=30, choices=PROJECT_CHOICES) + randomize_document_order = models.BooleanField(default=False) def get_absolute_url(self): return reverse('upload', args=[self.id]) diff --git a/app/server/serializers.py b/app/server/serializers.py index 8acd0aff..783da955 100644 --- a/app/server/serializers.py +++ b/app/server/serializers.py @@ -75,7 +75,8 @@ class ProjectSerializer(serializers.ModelSerializer): class Meta: model = Project - fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at') + fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at', + 'randomize_document_order') read_only_fields = ('image', 'updated_at') @@ -83,7 +84,8 @@ class TextClassificationProjectSerializer(serializers.ModelSerializer): class Meta: model = TextClassificationProject - fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at') + fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at', + 'randomize_document_order') read_only_fields = ('image', 'updated_at', 'users') @@ -91,7 +93,8 @@ class SequenceLabelingProjectSerializer(serializers.ModelSerializer): class Meta: model = SequenceLabelingProject - fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at') + fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at', + 'randomize_document_order') read_only_fields = ('image', 'updated_at', 'users') @@ -99,7 +102,8 @@ class Seq2seqProjectSerializer(serializers.ModelSerializer): class Meta: model = Seq2seqProject - fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at') + fields = ('id', 'name', 'description', 'guideline', 'users', 'project_type', 'image', 'updated_at', + 'randomize_document_order') read_only_fields = ('image', 'updated_at', 'users') diff --git a/app/server/static/components/projects.vue b/app/server/static/components/projects.vue index b527a4a9..d17198f9 100644 --- a/app/server/static/components/projects.vue +++ b/app/server/static/components/projects.vue @@ -42,6 +42,17 @@ option(value="Seq2seq") sequence to sequence p.help.is-danger {{ projectTypeError }} + div.field + label.checkbox + input( + v-model="randomizeDocumentOrder" + name="randomize_document_order" + type="checkbox" + style="margin-right: 0.25em;" + required + ) + | Randomize document order per user + footer.modal-card-foot.pt20.pb20.pr20.pl20.has-background-white-ter button.button.is-primary(v-on:click="create()") Create button.button(v-on:click="isActive = !isActive") Cancel @@ -131,6 +142,7 @@ export default { projectNameError: '', username: '', isSuperuser: false, + randomizeDocumentOrder: false, }), computed: { @@ -182,6 +194,7 @@ export default { name: this.projectName, description: this.description, project_type: this.projectType, + randomize_document_order: this.randomizeDocumentOrder, guideline: 'Please write annotation guideline.', resourcetype: this.resourceType(), }; diff --git a/app/server/tests/test_api.py b/app/server/tests/test_api.py index f93a88b7..12bf733b 100644 --- a/app/server/tests/test_api.py +++ b/app/server/tests/test_api.py @@ -297,9 +297,14 @@ class TestDocumentListAPI(APITestCase): cls.main_project = mommy.make('server.TextClassificationProject', users=[project_member, super_user]) mommy.make('server.Document', project=cls.main_project) + cls.random_order_project = mommy.make('server.TextClassificationProject', users=[project_member, super_user], + randomize_document_order=True) + mommy.make('server.Document', 100, project=cls.random_order_project) + sub_project = mommy.make('server.TextClassificationProject', users=[non_project_member]) mommy.make('server.Document', project=sub_project) cls.url = reverse(viewname='doc_list', args=[cls.main_project.id]) + cls.random_order_project_url = reverse(viewname='doc_list', args=[cls.random_order_project.id]) cls.data = {'text': 'example'} def test_returns_docs_to_project_member(self): @@ -308,6 +313,33 @@ class TestDocumentListAPI(APITestCase): response = self.client.get(self.url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_returns_docs_in_consistent_order_for_all_users(self): + self.client.login(username=self.project_member_name, password=self.project_member_pass) + user1_documents = self.client.get(self.url, format='json').json().get('results') + self.client.logout() + + self.client.login(username=self.super_user_name, password=self.super_user_pass) + user2_documents = self.client.get(self.url, format='json').json().get('results') + self.client.logout() + + self.assertEqual(user1_documents, user2_documents) + + def test_can_return_docs_in_consistent_random_order(self): + self.client.login(username=self.project_member_name, password=self.project_member_pass) + user1_documents1 = self.client.get(self.random_order_project_url, format='json').json().get('results') + user1_documents2 = self.client.get(self.random_order_project_url, format='json').json().get('results') + self.client.logout() + self.assertEqual(user1_documents1, user1_documents2) + + self.client.login(username=self.super_user_name, password=self.super_user_pass) + user2_documents1 = self.client.get(self.random_order_project_url, format='json').json().get('results') + user2_documents2 = self.client.get(self.random_order_project_url, format='json').json().get('results') + self.client.logout() + self.assertEqual(user2_documents1, user2_documents2) + + self.assertNotEqual(user1_documents1, user2_documents1) + self.assertNotEqual(user1_documents2, user2_documents2) + def test_do_not_return_docs_to_non_project_member(self): self.client.login(username=self.non_project_member_name, password=self.non_project_member_pass)