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