Skip to content

Commit

Permalink
Add admin button to approve/reject labels
Browse files Browse the repository at this point in the history
  • Loading branch information
c-w committed Jun 25, 2019
1 parent eb93a43 commit 290894b
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 4 deletions.
11 changes: 11 additions & 0 deletions app/server/api.py
Expand Up @@ -97,6 +97,17 @@ def label_per_data(self, project):
return label_count, user_count


class ApproveLabelsAPI(APIView):
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUser)

def post(self, request, *args, **kwargs):
approved = self.request.data.get('approved', True)
document = get_object_or_404(Document, pk=self.kwargs['doc_id'])
document.annotations_approved_by = self.request.user if approved else None
document.save()
return Response(DocumentSerializer(document).data)


class LabelList(generics.ListCreateAPIView):
queryset = Label.objects.all()
serializer_class = LabelSerializer
Expand Down
4 changes: 3 additions & 1 deletion app/server/api_urls.py
Expand Up @@ -3,7 +3,7 @@

from .api import Me, Features
from .api import ProjectList, ProjectDetail
from .api import LabelList, LabelDetail
from .api import LabelList, LabelDetail, ApproveLabelsAPI
from .api import DocumentList, DocumentDetail
from .api import AnnotationList, AnnotationDetail
from .api import TextUploadAPI, TextDownloadAPI, CloudUploadAPI
Expand All @@ -26,6 +26,8 @@
DocumentList.as_view(), name='doc_list'),
path('projects/<int:project_id>/docs/<int:doc_id>',
DocumentDetail.as_view(), name='doc_detail'),
path('projects/<int:project_id>/docs/<int:doc_id>/approve-labels',
ApproveLabelsAPI.as_view(), name='approve_labels'),
path('projects/<int:project_id>/docs/<int:doc_id>/annotations',
AnnotationList.as_view(), name='annotation_list'),
path('projects/<int:project_id>/docs/<int:doc_id>/annotations/<int:annotation_id>',
Expand Down
21 changes: 21 additions & 0 deletions app/server/migrations/0003_approve_document_labels.py
@@ -0,0 +1,21 @@
# Generated by Django 2.1.7 on 2019-06-25 16:15

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('server', '0002_project_randomize_document_order'),
]

operations = [
migrations.AddField(
model_name='document',
name='annotations_approved_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]
1 change: 1 addition & 0 deletions app/server/models.py
Expand Up @@ -185,6 +185,7 @@ class Document(models.Model):
meta = models.TextField(default='{}')
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)

def __str__(self):
return self.text[:50]
Expand Down
7 changes: 6 additions & 1 deletion app/server/serializers.py
Expand Up @@ -54,6 +54,7 @@ class Meta:

class DocumentSerializer(serializers.ModelSerializer):
annotations = serializers.SerializerMethodField()
annotation_approver = serializers.SerializerMethodField()

def get_annotations(self, instance):
request = self.context.get('request')
Expand All @@ -66,9 +67,13 @@ def get_annotations(self, instance):
serializer = serializer(annotations, many=True)
return serializer.data

def get_annotation_approver(self, instance):
approver = instance.annotations_approved_by
return approver.username if approver else None

class Meta:
model = Document
fields = ('id', 'text', 'annotations', 'meta')
fields = ('id', 'text', 'annotations', 'meta', 'annotation_approver')


class ProjectSerializer(serializers.ModelSerializer):
Expand Down
11 changes: 10 additions & 1 deletion app/server/static/components/annotation.pug
Expand Up @@ -106,11 +106,20 @@ div.columns(v-cloak="")
v-bind:value="achievement"
max="100"
) 30%
div.column.is-7
div.column.is-6
span.ml10
strong {{ total - remaining }}
| /
span {{ total }}

div.column.is-1.has-text-right
a.button.tooltip.is-tooltip-bottom(
v-if="isSuperuser"
v-on:click="approveDocumentLabels"
v-bind:data-tooltip="documentAnnotationsApprovalTooltip"
)
span.icon
i.far(v-bind:class="[documentAnnotationsAreApproved ? 'fa-check-circle' : 'fa-circle']")
div.column.is-1.has-text-right
a.button(v-on:click="isAnnotationGuidelineActive = !isAnnotationGuidelineActive")
span.icon
Expand Down
32 changes: 31 additions & 1 deletion app/server/static/components/annotationMixin.js
@@ -1,7 +1,9 @@
import * as marked from 'marked';
import VueJsonPretty from 'vue-json-pretty';
import isEmpty from 'lodash.isempty';
import HTTP from './http';
import HTTP, { rootUrl, newHttpClient } from './http';

const httpClient = newHttpClient();

const getOffsetFromUrl = (url) => {
const offsetMatch = url.match(/[?#].*offset=(\d+)/);
Expand Down Expand Up @@ -52,6 +54,7 @@ export default {
offset: getOffsetFromUrl(window.location.href),
picked: 'all',
count: 0,
isSuperuser: false,
isMetadataActive: false,
isAnnotationGuidelineActive: false,
};
Expand Down Expand Up @@ -135,6 +138,17 @@ export default {
}
return shortcut;
},

approveDocumentLabels() {
const document = this.docs[this.pageNumber];
const approved = !this.documentAnnotationsAreApproved;

HTTP.post(`docs/${document.id}/approve-labels`, { approved }).then((response) => {
const documents = this.docs.slice();
documents[this.pageNumber] = response.data;
this.docs = documents;
});
},
},

watch: {
Expand Down Expand Up @@ -162,6 +176,9 @@ export default {
HTTP.get().then((response) => {
this.guideline = response.data.guideline;
});
httpClient.get(`${rootUrl}/v1/me`).then((response) => {
this.isSuperuser = response.data.is_superuser;
});
this.submit();
},

Expand All @@ -178,6 +195,19 @@ export default {
});
},

documentAnnotationsAreApproved() {
const document = this.docs[this.pageNumber];
return document != null && document.annotation_approver != null;
},

documentAnnotationsApprovalTooltip() {
const document = this.docs[this.pageNumber];

return this.documentAnnotationsAreApproved
? `Labels approved by ${document.annotation_approver}, click to reject labels`
: 'Click to approve labels';
},

documentMetadata() {
const document = this.docs[this.pageNumber];
if (document == null || document.meta == null) {
Expand Down
34 changes: 34 additions & 0 deletions app/server/tests/test_api.py
Expand Up @@ -425,6 +425,40 @@ def test_disallows_project_member_to_delete_doc(self):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class TestApproveLabelsAPI(APITestCase):
@classmethod
def setUpTestData(cls):
cls.project_member_name = 'project_member_name'
cls.project_member_pass = 'project_member_pass'
cls.super_user_name = 'super_user_name'
cls.super_user_pass = 'super_user_pass'
project_member = User.objects.create_user(username=cls.project_member_name,
password=cls.project_member_pass)
# Todo: change super_user to project_admin.
super_user = User.objects.create_superuser(username=cls.super_user_name,
password=cls.super_user_pass,
email='fizz@buzz.com')
project = mommy.make('server.TextClassificationProject', users=[project_member, super_user])
cls.doc = mommy.make('server.Document', project=project)
cls.url = reverse(viewname='approve_labels', args=[project.id, cls.doc.id])

def test_allows_superuser_to_approve_and_disapprove_labels(self):
self.client.login(username=self.super_user_name, password=self.super_user_pass)

response = self.client.post(self.url, format='json', data={'approved': True})
self.assertEqual(response.data['annotation_approver'], self.super_user_name)

response = self.client.post(self.url, format='json', data={'approved': False})
self.assertIsNone(response.data['annotation_approver'])

def test_disallows_project_member_to_approve_and_disapprove_labels(self):
self.client.login(username=self.project_member_name, password=self.project_member_pass)

response = self.client.post(self.url, format='json', data={'approved': True})

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)


class TestAnnotationListAPI(APITestCase):

@classmethod
Expand Down

0 comments on commit 290894b

Please sign in to comment.