Browse Source

Add admin button to approve/reject labels

pull/258/head
Clemens Wolff 5 years ago
parent
commit
d0c566c5ab
8 changed files with 118 additions and 4 deletions
  1. 21
      app/api/migrations/0002_approve_document_labels.py
  2. 1
      app/api/models.py
  3. 8
      app/api/serializers.py
  4. 34
      app/api/tests/test_api.py
  5. 4
      app/api/urls.py
  6. 11
      app/api/views.py
  7. 11
      app/server/static/components/annotation.pug
  8. 32
      app/server/static/components/annotationMixin.js

21
app/api/migrations/0002_approve_document_labels.py

@ -0,0 +1,21 @@
# Generated by Django 2.1.7 on 2019-06-26 13:24
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),
('api', '0001_initial'),
]
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
app/api/models.py

@ -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]

8
app/api/serializers.py

@ -54,6 +54,7 @@ class LabelSerializer(serializers.ModelSerializer):
class DocumentSerializer(serializers.ModelSerializer):
annotations = serializers.SerializerMethodField()
annotation_approver = serializers.SerializerMethodField()
def get_annotations(self, instance):
request = self.context.get('request')
@ -66,9 +67,14 @@ class DocumentSerializer(serializers.ModelSerializer):
serializer = serializer(annotations, many=True)
return serializer.data
@classmethod
def get_annotation_approver(cls, 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):

34
app/api/tests/test_api.py

@ -426,6 +426,40 @@ class TestDocumentDetailAPI(APITestCase):
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('TextClassificationProject', users=[project_member, super_user])
cls.doc = mommy.make('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

4
app/api/urls.py

@ -3,7 +3,7 @@ from rest_framework.urlpatterns import format_suffix_patterns
from .views import Me, Features
from .views import ProjectList, ProjectDetail
from .views import LabelList, LabelDetail
from .views import LabelList, LabelDetail, ApproveLabelsAPI
from .views import DocumentList, DocumentDetail
from .views import AnnotationList, AnnotationDetail
from .views import TextUploadAPI, TextDownloadAPI, CloudUploadAPI
@ -26,6 +26,8 @@ urlpatterns = [
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>',

11
app/api/views.py

@ -97,6 +97,17 @@ class StatisticsAPI(APIView):
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

11
app/server/static/components/annotation.pug

@ -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="approveDocumentAnnotations"
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

32
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+)/);
@ -52,6 +54,7 @@ export default {
offset: getOffsetFromUrl(window.location.href),
picked: 'all',
count: 0,
isSuperuser: false,
isMetadataActive: false,
isAnnotationGuidelineActive: false,
};
@ -135,6 +138,17 @@ export default {
}
return shortcut;
},
approveDocumentAnnotations() {
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: {
@ -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();
},
@ -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
? `Annotations approved by ${document.annotation_approver}, click to reject annotations`
: 'Click to approve annotations';
},
documentMetadata() {
const document = this.docs[this.pageNumber];
if (document == null || document.meta == null) {

Loading…
Cancel
Save