Skip to content

Commit d0c566c

Browse files
committed
Add admin button to approve/reject labels
1 parent c2fbe77 commit d0c566c

File tree

8 files changed

+118
-4
lines changed

8 files changed

+118
-4
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 2.1.7 on 2019-06-26 13:24
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('api', '0001_initial'),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name='document',
18+
name='annotations_approved_by',
19+
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
20+
),
21+
]

app/api/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ class Document(models.Model):
185185
meta = models.TextField(default='{}')
186186
created_at = models.DateTimeField(auto_now_add=True)
187187
updated_at = models.DateTimeField(auto_now=True)
188+
annotations_approved_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
188189

189190
def __str__(self):
190191
return self.text[:50]

app/api/serializers.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class Meta:
5454

5555
class DocumentSerializer(serializers.ModelSerializer):
5656
annotations = serializers.SerializerMethodField()
57+
annotation_approver = serializers.SerializerMethodField()
5758

5859
def get_annotations(self, instance):
5960
request = self.context.get('request')
@@ -66,9 +67,14 @@ def get_annotations(self, instance):
6667
serializer = serializer(annotations, many=True)
6768
return serializer.data
6869

70+
@classmethod
71+
def get_annotation_approver(cls, instance):
72+
approver = instance.annotations_approved_by
73+
return approver.username if approver else None
74+
6975
class Meta:
7076
model = Document
71-
fields = ('id', 'text', 'annotations', 'meta')
77+
fields = ('id', 'text', 'annotations', 'meta', 'annotation_approver')
7278

7379

7480
class ProjectSerializer(serializers.ModelSerializer):

app/api/tests/test_api.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,40 @@ def test_disallows_project_member_to_delete_doc(self):
426426
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
427427

428428

429+
class TestApproveLabelsAPI(APITestCase):
430+
@classmethod
431+
def setUpTestData(cls):
432+
cls.project_member_name = 'project_member_name'
433+
cls.project_member_pass = 'project_member_pass'
434+
cls.super_user_name = 'super_user_name'
435+
cls.super_user_pass = 'super_user_pass'
436+
project_member = User.objects.create_user(username=cls.project_member_name,
437+
password=cls.project_member_pass)
438+
# Todo: change super_user to project_admin.
439+
super_user = User.objects.create_superuser(username=cls.super_user_name,
440+
password=cls.super_user_pass,
441+
442+
project = mommy.make('TextClassificationProject', users=[project_member, super_user])
443+
cls.doc = mommy.make('Document', project=project)
444+
cls.url = reverse(viewname='approve_labels', args=[project.id, cls.doc.id])
445+
446+
def test_allows_superuser_to_approve_and_disapprove_labels(self):
447+
self.client.login(username=self.super_user_name, password=self.super_user_pass)
448+
449+
response = self.client.post(self.url, format='json', data={'approved': True})
450+
self.assertEqual(response.data['annotation_approver'], self.super_user_name)
451+
452+
response = self.client.post(self.url, format='json', data={'approved': False})
453+
self.assertIsNone(response.data['annotation_approver'])
454+
455+
def test_disallows_project_member_to_approve_and_disapprove_labels(self):
456+
self.client.login(username=self.project_member_name, password=self.project_member_pass)
457+
458+
response = self.client.post(self.url, format='json', data={'approved': True})
459+
460+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
461+
462+
429463
class TestAnnotationListAPI(APITestCase):
430464

431465
@classmethod

app/api/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from .views import Me, Features
55
from .views import ProjectList, ProjectDetail
6-
from .views import LabelList, LabelDetail
6+
from .views import LabelList, LabelDetail, ApproveLabelsAPI
77
from .views import DocumentList, DocumentDetail
88
from .views import AnnotationList, AnnotationDetail
99
from .views import TextUploadAPI, TextDownloadAPI, CloudUploadAPI
@@ -26,6 +26,8 @@
2626
DocumentList.as_view(), name='doc_list'),
2727
path('projects/<int:project_id>/docs/<int:doc_id>',
2828
DocumentDetail.as_view(), name='doc_detail'),
29+
path('projects/<int:project_id>/docs/<int:doc_id>/approve-labels',
30+
ApproveLabelsAPI.as_view(), name='approve_labels'),
2931
path('projects/<int:project_id>/docs/<int:doc_id>/annotations',
3032
AnnotationList.as_view(), name='annotation_list'),
3133
path('projects/<int:project_id>/docs/<int:doc_id>/annotations/<int:annotation_id>',

app/api/views.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@ def label_per_data(self, project):
9797
return label_count, user_count
9898

9999

100+
class ApproveLabelsAPI(APIView):
101+
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUser)
102+
103+
def post(self, request, *args, **kwargs):
104+
approved = self.request.data.get('approved', True)
105+
document = get_object_or_404(Document, pk=self.kwargs['doc_id'])
106+
document.annotations_approved_by = self.request.user if approved else None
107+
document.save()
108+
return Response(DocumentSerializer(document).data)
109+
110+
100111
class LabelList(generics.ListCreateAPIView):
101112
queryset = Label.objects.all()
102113
serializer_class = LabelSerializer

app/server/static/components/annotation.pug

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,20 @@ div.columns(v-cloak="")
106106
v-bind:value="achievement"
107107
max="100"
108108
) 30%
109-
div.column.is-7
109+
div.column.is-6
110110
span.ml10
111111
strong {{ total - remaining }}
112112
| /
113113
span {{ total }}
114+
115+
div.column.is-1.has-text-right
116+
a.button.tooltip.is-tooltip-bottom(
117+
v-if="isSuperuser"
118+
v-on:click="approveDocumentAnnotations"
119+
v-bind:data-tooltip="documentAnnotationsApprovalTooltip"
120+
)
121+
span.icon
122+
i.far(v-bind:class="[documentAnnotationsAreApproved ? 'fa-check-circle' : 'fa-circle']")
114123
div.column.is-1.has-text-right
115124
a.button(v-on:click="isAnnotationGuidelineActive = !isAnnotationGuidelineActive")
116125
span.icon

app/server/static/components/annotationMixin.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import * as marked from 'marked';
22
import VueJsonPretty from 'vue-json-pretty';
33
import isEmpty from 'lodash.isempty';
4-
import HTTP from './http';
4+
import HTTP, { rootUrl, newHttpClient } from './http';
5+
6+
const httpClient = newHttpClient();
57

68
const getOffsetFromUrl = (url) => {
79
const offsetMatch = url.match(/[?#].*offset=(\d+)/);
@@ -52,6 +54,7 @@ export default {
5254
offset: getOffsetFromUrl(window.location.href),
5355
picked: 'all',
5456
count: 0,
57+
isSuperuser: false,
5558
isMetadataActive: false,
5659
isAnnotationGuidelineActive: false,
5760
};
@@ -135,6 +138,17 @@ export default {
135138
}
136139
return shortcut;
137140
},
141+
142+
approveDocumentAnnotations() {
143+
const document = this.docs[this.pageNumber];
144+
const approved = !this.documentAnnotationsAreApproved;
145+
146+
HTTP.post(`docs/${document.id}/approve-labels`, { approved }).then((response) => {
147+
const documents = this.docs.slice();
148+
documents[this.pageNumber] = response.data;
149+
this.docs = documents;
150+
});
151+
},
138152
},
139153

140154
watch: {
@@ -162,6 +176,9 @@ export default {
162176
HTTP.get().then((response) => {
163177
this.guideline = response.data.guideline;
164178
});
179+
httpClient.get(`${rootUrl}/v1/me`).then((response) => {
180+
this.isSuperuser = response.data.is_superuser;
181+
});
165182
this.submit();
166183
},
167184

@@ -178,6 +195,19 @@ export default {
178195
});
179196
},
180197

198+
documentAnnotationsAreApproved() {
199+
const document = this.docs[this.pageNumber];
200+
return document != null && document.annotation_approver != null;
201+
},
202+
203+
documentAnnotationsApprovalTooltip() {
204+
const document = this.docs[this.pageNumber];
205+
206+
return this.documentAnnotationsAreApproved
207+
? `Annotations approved by ${document.annotation_approver}, click to reject annotations`
208+
: 'Click to approve annotations';
209+
},
210+
181211
documentMetadata() {
182212
const document = this.docs[this.pageNumber];
183213
if (document == null || document.meta == null) {

0 commit comments

Comments
 (0)