diff --git a/core/serializers/tutor.py b/core/serializers/tutor.py index 98a7b982b946b2c096a628bc58fe74bfb2618526..b2eff1e8829433b1806e14b00cbece143231f5e4 100644 --- a/core/serializers/tutor.py +++ b/core/serializers/tutor.py @@ -59,4 +59,5 @@ class CorrectorSerializer(DynamicFieldsModelSerializer): 'username', 'feedback_created', 'feedback_validated', + 'exercise_groups', 'role') diff --git a/core/tests/test_student_reviewer_viewset.py b/core/tests/test_student_reviewer_viewset.py index ffbdac9daf21f021598eb6beb1f1260f70cb166c..b351f8661821d7d99cbc8c0fbfaa69c9772ff723 100644 --- a/core/tests/test_student_reviewer_viewset.py +++ b/core/tests/test_student_reviewer_viewset.py @@ -93,10 +93,10 @@ class StudentPageTests(APITestCase): self.assertEqual(3, len(self.rev_response.data)) @override_config(EXERCISE_MODE=True) - def test_tutor_can_only_see_students_when_in_exercise_mode(self): + def test_tutor_can_only_see_group_members_when_in_exercise_mode(self): force_authenticate(self.request, user=self.tutor) response = self.view(self.request) - self.assertEqual(3, len(response.data)) + self.assertEqual(2, len(response.data)) def test_submissions_score_is_included(self): res_with_sub = None diff --git a/core/views/common_views.py b/core/views/common_views.py index 3ea1f890a625c52a990c64b939ae93303b3e2455..0b6fcbe1855f79d358802ea3dfb97d877db92dd1 100644 --- a/core/views/common_views.py +++ b/core/views/common_views.py @@ -81,7 +81,9 @@ class StudentReviewerApiViewSet(viewsets.ReadOnlyModelViewSet): return queryset elif self.request.user.is_tutor() and config.EXERCISE_MODE: - return queryset + return queryset.filter( + user__exercise_groups__in=self.request.user.exercise_groups.all() + ) else: return [] @@ -328,6 +330,29 @@ class UserAccountViewSet(viewsets.ReadOnlyModelViewSet): user.save() return Response(status.HTTP_200_OK) + @action(detail=True, methods=['patch'], permission_classes=(IsReviewer,)) + def change_groups(self, request, *args, **kwargs): + # for some reason only the newly added groups come as a group object + groups = [x.get('pk') if type(x) is not str else x for x in request.data] + req_user = request.user + user = self.get_object() + if groups is None: + error_msg = "You need to provide an 'groups' field" + return Response({'Error': error_msg}, status.HTTP_400_BAD_REQUEST) + if req_user.is_student() or req_user.is_tutor(): + return Response(status.HTTP_403_FORBIDDEN) + user.set_groups(groups) + user.save() + return Response(status.HTTP_200_OK) + + @action(detail=True) + def get_groups(self, request, *args, **kwargs): + req_user = request.user + if req_user.is_student() or req_user.is_tutor(): + return Response(status.HTTP_403_FORBIDDEN) + user = self.get_object() + return Response(user.exercise_groups, status=status.HTTP_200_OK) + @action(detail=True, methods=["patch"]) def change_role(self, request, *args, **kwargs): new_role = request.data.get('role') diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 0d34730e29fce2d7f3f19d8a872545aecb2ec04b..6ecaedaf34c3edcc84f0596ff0a5ddc4e56b27c3 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -153,6 +153,15 @@ export async function fetchGroups(): Promise<Group[]> { return (await ax.get(url)).data } +export async function fetchUserGroups(userPk: string): Promise<Group[]> { + const url = `/api/user/${userPk}/get_groups/` + return (await ax.get(url)).data +} + +export async function setGroups (userPk: string, groups: Group[]): Promise<UserAccount> { + return (await ax.patch(`/api/user/${userPk}/change_groups/`, groups)).data +} + export async function deleteSolutionComment (pk: number): Promise<AxiosResponse<void>> { const url = `/api/solution-comment/${pk}/` return ax.delete(url) @@ -221,6 +230,10 @@ export async function fetchUsers (): Promise<UserAccount[]> { return (await ax.get('api/user/')).data } +export async function fetchUser(userPk: string): Promise<UserAccount> { + return (await ax.get(`/api/user/${userPk}`)).data +} + export async function getLabels (): Promise<FeedbackLabel[]> { return (await ax.get('/api/label/')).data } diff --git a/frontend/src/components/student_list/StudentList.vue b/frontend/src/components/student_list/StudentList.vue index 35bf14b9608a91dd097066e669d397896fa561ab..a29fdc40adb7977b5e9781cc2f9b20af2ab9ec4e 100644 --- a/frontend/src/components/student_list/StudentList.vue +++ b/frontend/src/components/student_list/StudentList.vue @@ -228,15 +228,15 @@ export default { return [] }, groups () { - return Assignments.state.groups.slice().sort((a, b) => { - const matches_a = a.name.match(/(\d+)/) - const number_a = Number(matches_a === null ? 0 : matches_a[1]) - - const matches_b = b.name.match(/(\d+)/) - const number_b = Number(matches_b === null ? 0 : matches_b[1]) - - return (number_a<number_b?-1:(number_a>number_b?1:0)) - }) + if (Authentication.isTutor) { + return Authentication.state.user.exerciseGroups + } + else if (Authentication.isReviewer) { + return Assignments.state.groups + } + else { + return [] + } }, }, created () { diff --git a/frontend/src/components/tutor_list/TutorList.vue b/frontend/src/components/tutor_list/TutorList.vue index 2a0fe221b2ee84a8de020c570328034ff5be06a6..26bee4c5c7dc38e3cb6dc927a66f1a12f7bfea88 100644 --- a/frontend/src/components/tutor_list/TutorList.vue +++ b/frontend/src/components/tutor_list/TutorList.vue @@ -34,6 +34,23 @@ <span>Free locked submissions</span> </v-tooltip> </template> + <template #item.exerciseGroups="{ item }"> + <v-select + v-model="item.exerciseGroups" + item-text="name" + item-value="pk" + :items="groups" + label="Set Groups" + single-line + return-object + multiple + chips + dense + hide-details + filled + @change="setExerciseGroups($event, item)" + /> + </template> <template #item.isActive="{ item }"> <v-btn v-if="canRevokeAccess(item.username)" @@ -72,11 +89,12 @@ <script lang="ts"> import Vue from 'vue' import Component from 'vue-class-component' -import { changeActiveForUser } from '@/api' +import { changeActiveForUser, setGroups, fetchUserGroups, fetchUser } from '@/api' import { actions } from '@/store/actions' import { Authentication } from '@/store/modules/authentication' import { TutorOverview } from '@/store/modules/tutor-overview' -import { Tutor, UserAccount } from '@/models' +import { Group, Tutor, UserAccount } from '@/models' +import { Assignments } from '@/store/modules/assignments' import RoleSelect from './RoleSelect.vue' @Component({ components: { RoleSelect } }) @@ -102,6 +120,11 @@ export default class TutorList extends Vue { align: 'right', value: 'reservedSubmissions' }, + { + text: 'Exercise Groups', + align: 'right', + value: 'exerciseGroups' + }, { text: 'Has Access', align: 'right', @@ -114,13 +137,43 @@ export default class TutorList extends Vue { ] get tutors () { - return TutorOverview.state.tutors.map(tutor => { + var tlist = TutorOverview.state.tutors.map(tutor => { + var groups: Group[] = [] + this.userAccountGroups(tutor).then(function(value) { + groups = value // Success! + }, (reason) => { + this.$notify({ + title: 'Error', + text: `Unable to fetch tutors: ${reason}`, + type: 'error' + }) + return [] + }) const reservedSubmissions = TutorOverview.state.activeAssignments[tutor.pk] return { ...tutor, - reservedSubmissions: reservedSubmissions ? reservedSubmissions.length : 0 + reservedSubmissions: reservedSubmissions ? reservedSubmissions.length : 0, } }) + return tlist + } + + get groups () { + return Assignments.state.groups.slice().sort((a, b) => { + const matches_a = a.name.match(/(\d+)/) + const number_a = Number(matches_a === null ? 0 : matches_a[1]) + + const matches_b = b.name.match(/(\d+)/) + const number_b = Number(matches_b === null ? 0 : matches_b[1]) + + return (number_a<number_b?-1:(number_a>number_b?1:0)) + }) + } + + + async userAccountGroups(tutor: Tutor) { + const groups = await (await fetchUser(tutor.pk)).exerciseGroups + return groups } changeActiveStatus (tutor: Tutor) { @@ -135,6 +188,18 @@ export default class TutorList extends Vue { }) } + setExerciseGroups (groups: Group[], tutor: Tutor){ + setGroups(tutor.pk, groups).then(() => { + TutorOverview.getTutors() + }).catch(() => { + this.$notify({ + title: 'Error', + text: `Unable to change exercise-groups of ${tutor.username}`, + type: 'error' + }) + }) + } + deleteAssignmentsOfTutor (tutor: Tutor) { TutorOverview.deleteActiveAssignmentsOfTutor(tutor) } diff --git a/frontend/src/models.ts b/frontend/src/models.ts index 5ef5dfd511d752faea05eb33abaebf7545d0088e..6e29640d2eeb148efbc89a79869040f47375d7ad 100644 --- a/frontend/src/models.ts +++ b/frontend/src/models.ts @@ -785,6 +785,12 @@ export interface Tutor { feedbackValidated?: string /** * + * @type {Group} + * @memberof Tutor + */ + exerciseGroups: Group[] + + /** * @type {string} * @memberof Tutor */