diff --git a/core/serializers/__init__.py b/core/serializers/__init__.py index 1e5981843df9b2221856bf405f1abeda760e4f17..20236b110f2d5e4079f7e7af4d2c13456667aef0 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 3f854ebd99b87cd481070fbed5aa74667922d14d..9a4317ff814323d4015a1fa13ee85b1248e59412 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 d09aade105325a0dceb4fb5c9f5478c6f915152e..efa8c537fbe9ed2023c1ab16a4d1f1217c97a3af 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 9d22d7c1f24e4bc8fec393f2bdb6eb06d187ede7..9e07425edfe4f2808f406b9d97319f9276940723 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 8aed85b1e5e90870d6e1cd5412f25921cd85c8da..ff4d6abbcd8fa28dce6b5aa20e71bb243f5b3161 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 0e040c64290f0510f4dd1393704f6af999fbfb38..26799adabe41b99cebce55c8b9acfd1eec67fe6c 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 cdca5006422f1345fcbb92a6f36260e678ec33f4..fd8e2e16bd559cc736a2edc640d3e8141e7ac097 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 0000000000000000000000000000000000000000..a1a2c92ffd62be04e7c3872ef330933d1c57c7ed --- /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>