From 10f2a6fce40f52156e8c1400e991bc38171adc6a Mon Sep 17 00:00:00 2001 From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de> Date: Sun, 11 Mar 2018 22:35:15 +0100 Subject: [PATCH] Reviewer can activate/deactivate student access The reviewer has the option to activate and deactivate all students access via the web interface in the student overview. The corresponding endpoints are additional list routes on the student viewset. Tests are in test_reviewer_viewset.py --- core/serializers/__init__.py | 2 +- core/serializers/feedback.py | 1 - core/serializers/student.py | 4 +- core/tests/test_student_reviewer_viewset.py | 32 ++++++-- core/views/common_views.py | 20 ++++- frontend/src/api.js | 8 ++ .../components/student_list/StudentList.vue | 3 + .../student_list/StudentListMenu.vue | 78 +++++++++++++++++++ 8 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/student_list/StudentListMenu.vue diff --git a/core/serializers/__init__.py b/core/serializers/__init__.py index 1e598184..20236b11 100644 --- a/core/serializers/__init__.py +++ b/core/serializers/__init__.py @@ -1,5 +1,5 @@ from .common_serializers import * # noqa -from .feedback import (FeedbackSerializer, FeedbackCommentSerializer, +from .feedback import (FeedbackSerializer, FeedbackCommentSerializer, # noqa VisibleCommentFeedbackSerializer) # noqa from .subscription import * # noqa from .student import * # noqa diff --git a/core/serializers/feedback.py b/core/serializers/feedback.py index 3f854ebd..9a4317ff 100644 --- a/core/serializers/feedback.py +++ b/core/serializers/feedback.py @@ -204,4 +204,3 @@ class VisibleCommentFeedbackSerializer(FeedbackSerializer): model = Feedback fields = ('pk', 'of_submission', 'is_final', 'score', 'feedback_lines', 'created', 'of_submission_type') - diff --git a/core/serializers/student.py b/core/serializers/student.py index d09aade1..efa8c537 100644 --- a/core/serializers/student.py +++ b/core/serializers/student.py @@ -22,7 +22,9 @@ class StudentInfoSerializerForListView(DynamicFieldsModelSerializer): user = serializers.ReadOnlyField(source='user.username') exam = serializers.ReadOnlyField(source='exam.module_reference') submissions = SubmissionNoTextFieldsSerializer(many=True) + is_active = serializers.BooleanField(source='user.is_active') class Meta: model = StudentInfo - fields = ('pk', 'name', 'user', 'exam', 'submissions', 'matrikel_no') + fields = ('pk', 'name', 'user', 'exam', 'submissions', + 'matrikel_no', 'is_active') diff --git a/core/tests/test_student_reviewer_viewset.py b/core/tests/test_student_reviewer_viewset.py index 9d22d7c1..9e07425e 100644 --- a/core/tests/test_student_reviewer_viewset.py +++ b/core/tests/test_student_reviewer_viewset.py @@ -3,6 +3,7 @@ from rest_framework import status from rest_framework.test import (APIRequestFactory, APITestCase, force_authenticate) +from core import models from core.views import StudentReviewerApiViewSet from util.factories import make_test_data @@ -26,11 +27,18 @@ class StudentPageTests(APITestCase): 'description': 'Very hard', 'solution': 'Impossible!' }], - 'students': [{ - 'username': 'user01', - 'fullname': 'us er01', - 'exam': 'TestExam B.Inf.0042' - }], + 'students': [ + { + 'username': 'user01', + 'fullname': 'us er01', + 'exam': 'TestExam B.Inf.0042' + }, + { + 'username': 'user02', + 'exam': 'TestExam B.Inf.0042' + } + + ], 'tutors': [{ 'username': 'tutor' }], @@ -67,7 +75,7 @@ class StudentPageTests(APITestCase): self.assertEqual(self.response.status_code, status.HTTP_200_OK) def test_can_see_all_students(self): - self.assertEqual(1, len(self.response.data)) + self.assertEqual(2, len(self.response.data)) def test_submissions_score_is_included(self): self.assertEqual(self.student.submissions.first().feedback.score, @@ -77,3 +85,15 @@ class StudentPageTests(APITestCase): print(self.response.data[0]['submissions'][0]) self.assertEqual(self.student.submissions.first().type.full_score, self.response.data[0]['submissions'][0]['full_score']) + + def test_can_deactivate_all_students(self): + self.client.force_authenticate(user=self.reviewer) + self.client.post(reverse('student-list') + 'deactivate/') + users = [stud.user for stud in models.StudentInfo.objects.all()] + self.assertTrue(all([not user.is_active for user in users])) + + def test_can_activate_all_students(self): + self.client.force_authenticate(user=self.reviewer) + self.client.post(reverse('student-list') + 'activate/') + users = [stud.user for stud in models.StudentInfo.objects.all()] + self.assertTrue(all([user.is_active for user in users])) diff --git a/core/views/common_views.py b/core/views/common_views.py index 8aed85b1..ff4d6abb 100644 --- a/core/views/common_views.py +++ b/core/views/common_views.py @@ -5,8 +5,8 @@ import logging from django.conf import settings from django.db.models import Avg -from rest_framework import generics, mixins, viewsets -from rest_framework.decorators import api_view +from rest_framework import generics, mixins, viewsets, status +from rest_framework.decorators import api_view, list_route from rest_framework.response import Response from core import models @@ -63,6 +63,22 @@ class StudentReviewerApiViewSet(viewsets.ReadOnlyModelViewSet): .all() serializer_class = StudentInfoSerializerForListView + def _set_students_active(self, active): + for student in self.get_queryset(): + user = student.user + user.is_active = active + user.save() + + @list_route(methods=['post']) + def deactivate(self, request): + self._set_students_active(False) + return Response(status=status.HTTP_200_OK) + + @list_route(methods=['post']) + def activate(self, request): + self._set_students_active(True) + return Response(status=status.HTTP_200_OK) + class ExamApiViewSet(viewsets.ReadOnlyModelViewSet): """ Gets a list of an individual exam by Id if provided """ diff --git a/frontend/src/api.js b/frontend/src/api.js index 0e040c64..26799ada 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -167,4 +167,12 @@ export async function patchComment (comment = {pk: undefined}) { return (await ax.patch(url, comment)).data } +export async function activateAllStudentAccess () { + return ax.post('/api/student/activate/') +} + +export async function deactivateAllStudentAccess () { + return ax.post('/api/student/deactivate/') +} + export default ax diff --git a/frontend/src/components/student_list/StudentList.vue b/frontend/src/components/student_list/StudentList.vue index cdca5006..fd8e2e16 100644 --- a/frontend/src/components/student_list/StudentList.vue +++ b/frontend/src/components/student_list/StudentList.vue @@ -14,6 +14,7 @@ ></v-text-field> <v-card-actions> <v-btn icon @click="refresh"><v-icon>refresh</v-icon></v-btn> + <student-list-menu/> </v-card-actions> </v-card-title> <v-data-table @@ -98,8 +99,10 @@ <script> import {mapActions, mapState} from 'vuex' + import StudentListMenu from '@/components/student_list/StudentListMenu' export default { + components: {StudentListMenu}, name: 'student-list', data () { return { diff --git a/frontend/src/components/student_list/StudentListMenu.vue b/frontend/src/components/student_list/StudentListMenu.vue new file mode 100644 index 00000000..a1a2c92f --- /dev/null +++ b/frontend/src/components/student_list/StudentListMenu.vue @@ -0,0 +1,78 @@ +<template> + <v-menu open-on-hover bottom offset-y> + <v-btn icon slot="activator"> + <v-icon>menu</v-icon> + </v-btn> + <v-list> + <v-list-tile v-for="item in items" :key="item.title" @click="item.action"> + <v-list-tile-title>{{ item.title }}</v-list-tile-title> + </v-list-tile> + </v-list> + </v-menu> +</template> + +<script> + import {activateAllStudentAccess, + deactivateAllStudentAccess} from '@/api' + + export default { + name: 'student-list-menu', + computed: { + studentsActive () { + const firstStudent = Object.values(this.$store.state.students)[0] + return firstStudent ? firstStudent.is_active === true : false + }, + items () { + return [ + { + title: this.studentsActive + ? 'Deactivate student access' + : 'Activate student access', + action: this.changeStudentsAccess + } + ] + } + }, + methods: { + updateStudentData (fields = []) { + this.$store.dispatch('getStudents', { + studentPks: Object.keys(this.$store.state.students), + fields + }).catch(() => { + this.$notify({ + title: 'ERROR', + text: 'Unable to update student data!', + type: 'error' + }) + }) + }, + changeStudentsAccess () { + if (this.studentsActive) { + deactivateAllStudentAccess().then(() => { + this.updateStudentData() + }).catch(() => { + this.$notify({ + title: 'ERROR', + text: 'Unable to disable access', + type: 'error' + }) + }) + } else { + activateAllStudentAccess().then(() => { + this.updateStudentData() + }).catch(() => { + this.$notify({ + title: 'ERROR', + text: 'Unable to activate access', + type: 'error' + }) + }) + } + } + } + } +</script> + +<style scoped> + +</style> -- GitLab