diff --git a/backend/core/models.py b/backend/core/models.py index 76b024601d0fb85b549519d734f700d63d1ad2b6..b7c7f3eb1cf114049b95c8d468af138a5d97a228 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -6,11 +6,10 @@ See docstring of the individual models for information on the setup of the database. ''' -from typing import Union, Dict - from collections import OrderedDict from random import randrange, sample from string import ascii_lowercase +from typing import Dict, Union from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractUser diff --git a/backend/core/permissions.py b/backend/core/permissions.py index 29b60731037a3d58727b44826b189de4b8f6217e..564a93c613616dd04aaed7150ccf59f57fa58e39 100644 --- a/backend/core/permissions.py +++ b/backend/core/permissions.py @@ -1,8 +1,8 @@ +from django.http import HttpRequest +from django.views import View from rest_framework import permissions from core.models import Reviewer, Student, Tutor -from django.http import HttpRequest -from django.views import View class IsUserGenericPermission(permissions.BasePermission): diff --git a/backend/core/serializers.py b/backend/core/serializers.py index afccb1bb6212bc3edd35c18680e022f464fb028e..08de11b0c72112eddc615341ba8019ab26d1e72a 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -46,6 +46,17 @@ class StudentSerializer(serializers.ModelSerializer): fields = ('name', 'user', 'exam', 'submissions') +class StudentSerializerForListView(serializers.ModelSerializer): + name = serializers.ReadOnlyField(source='user.fullname') + user = serializers.ReadOnlyField(source='user.username') + exam = serializers.ReadOnlyField(source='exam.module_reference') + submissions = SubmissionSerializer(many=True) + + class Meta: + model = Student + fields = ('name', 'user', 'exam', 'submissions') + + class TutorSerializer(serializers.ModelSerializer): username = serializers.CharField(source='user.username') feedback_count = serializers.IntegerField(source='get_feedback_count', diff --git a/backend/core/tests/test_access_rights.py b/backend/core/tests/test_access_rights.py index ff9857d688f1ebde249371bbabf497c8a0743ad2..bd4627be1b83c91c9196fa2bd4a4a6e1a6df8e3d 100644 --- a/backend/core/tests/test_access_rights.py +++ b/backend/core/tests/test_access_rights.py @@ -4,7 +4,7 @@ from rest_framework.test import (APIRequestFactory, APITestCase, force_authenticate) from core.models import Reviewer -from core.views import StudentApiView +from core.views import StudentSelfApiViewSet from util.factories import GradyUserFactory @@ -21,8 +21,8 @@ class AccessRightsOfStudentAPIViewTests(APITestCase): self.student = self.user_factory.make_student() self.tutor = self.user_factory.make_tutor() self.reviewer = self.user_factory.make_reviewer() - self.request = self.factory.get(reverse('student-page')) - self.view = StudentApiView.as_view() + self.request = self.factory.get(reverse('student_page-list')) + self.view = StudentSelfApiViewSet.as_view({'get': 'retrieve'}) def test_unauthorized_access_denied(self): response = self.view(self.request) diff --git a/backend/core/tests/test_examlist.py b/backend/core/tests/test_examlist.py index 51d55a8b9e9fe2972dd881dfabbafb26035092b9..67b3c84d8bf8965d3c2853fb6f7c6d8c425e6876 100644 --- a/backend/core/tests/test_examlist.py +++ b/backend/core/tests/test_examlist.py @@ -6,11 +6,9 @@ from rest_framework.test import (APIRequestFactory, APITestCase, force_authenticate) from core.models import ExamType -from core.views import ExamListView +from core.views import ExamApiViewSet from util.factories import GradyUserFactory -NUMBER_OF_TUTORS = 7 - class ExamListTest(APITestCase): @@ -20,9 +18,9 @@ class ExamListTest(APITestCase): cls.user_factory = GradyUserFactory() def setUp(self): - self.request = self.factory.get(reverse('exam-list')) + self.request = self.factory.get(reverse('examtype-list')) force_authenticate(self.request, self.user_factory.make_student().user) - self.view = ExamListView.as_view() + self.view = ExamApiViewSet.as_view({'get': 'list'}) self.response = self.view(self.request) def test_can_access_when_authenticated(self): diff --git a/backend/core/tests/test_student_page.py b/backend/core/tests/test_student_page.py index 2b15fb974d92ec1319df33e3abfa017cd0d261ea..166583ad543484da2544ad34830a23cc578796e5 100644 --- a/backend/core/tests/test_student_page.py +++ b/backend/core/tests/test_student_page.py @@ -5,7 +5,7 @@ from rest_framework.test import (APIRequestFactory, APITestCase, from core.models import Reviewer, SubmissionType from core.tests import data_factories -from core.views import StudentApiView +from core.views import StudentSelfApiViewSet class StudentPageTests(APITestCase): @@ -20,8 +20,8 @@ class StudentPageTests(APITestCase): self.student = self.submission.student self.reviewer = Reviewer.objects.create( user=data_factories.make_user(username='reviewer')) - self.request = self.factory.get(reverse('student-page')) - self.view = StudentApiView.as_view() + self.request = self.factory.get(reverse('student_page-list')) + self.view = StudentSelfApiViewSet.as_view({'get': 'retrieve'}) force_authenticate(self.request, user=self.student.user) self.response = self.view(self.request) diff --git a/backend/core/tests/test_tutor_api_endpoints.py b/backend/core/tests/test_tutor_api_endpoints.py index ba21ab11dae10bcef8f918afbb97ed66fe46b9bd..6f65aade5b233b9d0fb15bda177958eaffa6dc2e 100644 --- a/backend/core/tests/test_tutor_api_endpoints.py +++ b/backend/core/tests/test_tutor_api_endpoints.py @@ -7,19 +7,18 @@ import logging as log from unittest import skip -from django.urls import reverse from rest_framework import status +from rest_framework.reverse import reverse from rest_framework.test import (APIClient, APIRequestFactory, APITestCase, force_authenticate) from core.models import Feedback, Reviewer, Tutor -from core.views import TutorCreateView, TutorDetailView, TutorListApiView +from core.views import TutorApiViewSet from util.factories import GradyUserFactory -NUMBER_OF_TUTORS = 7 +NUMBER_OF_TUTORS = 3 -@skip class TutorListTests(APITestCase): @classmethod @@ -32,7 +31,7 @@ class TutorListTests(APITestCase): for _ in range(NUMBER_OF_TUTORS)] self.reviewer = self.user_factory.make_reviewer() self.request = self.factory.get(reverse('tutor-list')) - self.view = TutorListApiView.as_view() + self.view = TutorApiViewSet.as_view({'get': 'list'}) force_authenticate(self.request, user=self.reviewer.user) self.response = self.view(self.request) @@ -67,12 +66,12 @@ class TutorCreateTests(APITestCase): def setUp(self): self.reviewer = self.user_factory.make_reviewer() - self.request = self.factory.post(reverse('tutor-create'), + self.request = self.factory.post(reverse('tutor-list'), {'username': self.USERNAME}) - self.view = TutorCreateView.as_view() + self.view = TutorApiViewSet.as_view({'post': 'create'}) force_authenticate(self.request, user=self.reviewer.user) - self.response = self.view(self.request) + self.response = self.view(self.request, username=self.USERNAME) def test_can_access(self): self.assertEqual(self.response.status_code, status.HTTP_201_CREATED) @@ -80,8 +79,6 @@ class TutorCreateTests(APITestCase): def test_can_create(self): self.assertEqual(Tutor.objects.first().user.username, self.USERNAME) -# @skip("Doesn't work for dubious reasons") - class TutorDetailViewTests(APITestCase): @@ -91,12 +88,12 @@ class TutorDetailViewTests(APITestCase): cls.user_factory = GradyUserFactory() def setUp(self): - self.tutor = self.user_factory.make_tutor(username='fetter.otto') + self.tutor = self.user_factory.make_tutor(username='fetterotto') self.reviewer = self.user_factory.make_reviewer() self.client = APIClient() self.client.force_authenticate(user=self.reviewer.user) - url = reverse('tutor-detail', kwargs={'username': 'fetter.otto'}) + url = reverse('tutor-detail', kwargs={'username': 'fetterotto'}) self.response = self.client.get(url, format='json') def test_can_access(self): diff --git a/backend/core/urls.py b/backend/core/urls.py index 72e447a4c83e6d6b2813a552973dd7a3e36ef3de..f3dde516ff797147d4a777faa79106d18fa87778 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -1,18 +1,19 @@ -from django.conf.urls import url +from django.conf.urls import include, url from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from rest_framework.routers import DefaultRouter from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token from core import views -urlpatterns = [ - url(r'^api/student/$', views.StudentApiView.as_view(), name='student-page'), - - url(r'^api/examlist/$', views.ExamListView.as_view(), name='exam-list'), - - url(r'^api/tutor/$', views.TutorCreateView.as_view(), name='tutor-create'), - url(r'^api/tutor/(?P<username>[\w\d\.\-@_]+)$', views.TutorDetailView.as_view(), name='tutor-detail'), - url(r'^api/tutorlist/$', views.TutorListApiView.as_view(), name='tutor-list'), +# Create a router and register our viewsets with it. +router = DefaultRouter() +router.register(r'examtype', views.ExamApiViewSet) +router.register(r'tutor', views.TutorApiViewSet) +router.register(r'student', views.StudentReviewerApiViewSet) +router.register(r'student-page', views.StudentSelfApiViewSet, base_name='student_page') +urlpatterns = [ + url(r'^api/', include(router.urls)), url(r'^api-token-auth/', obtain_jwt_token), url(r'^api-token-refresh', refresh_jwt_token), ] diff --git a/backend/core/views.py b/backend/core/views.py index 4470a4ab59e360f0ef30fa6c37d1f850e4cf6aac..690d6cfb0b431fabc9b5e1e7f8e25be22bc58df1 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -3,18 +3,20 @@ can be categorized by the permissions they require. All views require a user to be authenticated and most are only accessible by one user group """ import logging -from rest_framework import generics +from rest_framework import mixins, viewsets -from core.models import ExamType, Tutor, Student +from core.models import ExamType, Student, Tutor from core.permissions import IsReviewer, IsStudent -from core.serializers import ExamSerializer, StudentSerializer, TutorSerializer +from core.serializers import (ExamSerializer, StudentSerializer, + StudentSerializerForListView, TutorSerializer) log = logging.getLogger(__name__) -class StudentApiView(generics.RetrieveAPIView): +class StudentSelfApiViewSet(viewsets.ReadOnlyModelViewSet): """ Gets all data that belongs to one student """ permission_classes = (IsStudent,) + queryset = Student.objects.all() serializer_class = StudentSerializer def get_object(self) -> Student: @@ -23,29 +25,28 @@ class StudentApiView(generics.RetrieveAPIView): return self.request.user.student -class TutorListApiView(generics.ListAPIView): - """ A list of all tutors with information about what they corrected """ - permission_classes = (IsReviewer,) - queryset = Tutor.objects.all() - serializer_class = TutorSerializer - - -class TutorCreateView(generics.CreateAPIView): - """ Creates a Tutor instance currently without a password """ - permission_classes = (IsReviewer,) - serializer_class = TutorSerializer - - -class ExamListView(generics.ListAPIView): - """ Gets a list of all exams available. List might be empty """ +class ExamApiViewSet(viewsets.ReadOnlyModelViewSet): + """ Gets a list of an individual exam by Id if provided """ + permissions_classes = (IsReviewer,) queryset = ExamType.objects.all() serializer_class = ExamSerializer -class TutorDetailView(generics.RetrieveAPIView): - """ Gets information of a single tutor by their username """ +class TutorApiViewSet(mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + """ Api endpoint for creating, listing, viewing or deleteing tutors """ permissions_classes = (IsReviewer,) + queryset = Tutor.objects.all() serializer_class = TutorSerializer lookup_field = 'user__username' lookup_url_kwarg = 'username' - queryset = Tutor.objects.all() + + +class StudentReviewerApiViewSet(viewsets.ReadOnlyModelViewSet): + """ Gets a list of all students without individual submissions """ + permission_classes = (IsReviewer,) + queryset = Student.objects.all() + serializer_class = StudentSerializerForListView diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue index 325fa0c94f1e29107e295f3098003aecb0290010..22dd0f091e66c39bb1cb8f73ac2ed657f1f7e970 100644 --- a/frontend/src/components/Login.vue +++ b/frontend/src/components/Login.vue @@ -43,7 +43,7 @@ password: this.credentials.password } this.$store.dispatch('getToken', credentials).then(response => { - this.$router.push('/student/') + this.$router.push('/reviewer/') }) } } diff --git a/frontend/src/components/reviewer/ReviewerPage.vue b/frontend/src/components/reviewer/ReviewerPage.vue new file mode 100644 index 0000000000000000000000000000000000000000..1bcc4a334896da6ba29875885c1b38210c6c898b --- /dev/null +++ b/frontend/src/components/reviewer/ReviewerPage.vue @@ -0,0 +1,57 @@ +<template> + <div> + <v-navigation-drawer persistent stateless value="true"> + <v-toolbar flat> + <v-list class="pa-1"> + <v-list-tile avatar> + <v-list-tile-avatar> + <img src="../../assets/brand.png" /> + </v-list-tile-avatar> + <v-list-tile-content> + <v-list-tile-title class="title" >Grady Menu</v-list-tile-title> + </v-list-tile-content> + </v-list-tile> + </v-list> + </v-toolbar> + <v-divider></v-divider> + <v-list> + <v-list-tile v-for="item in items" :key="item.title" :to="item.to" @click=""> + <v-list-tile-action> + <v-icon>{{ item.icon }}</v-icon> + </v-list-tile-action> + <v-list-tile-content> + <v-list-tile-title>{{ item.title }}</v-list-tile-title> + </v-list-tile-content> + </v-list-tile> + </v-list> + </v-navigation-drawer> + + <p> + Was Geht ab? + </p> + </div> +</template> + +<script> +import ReviewerToolbar from './ReviewerToolbar.vue' + +export default { + components: { + ReviewerToolbar + }, + name: 'reviewer-page', + data () { + return { + drawer: true, + items: [ + {title: 'Student List', to: '/reviewer/student-overview'}, + {title: 'Submission List', to: '/'} + ], + right: null + } + } +} +</script> + +<style lang="css" scoped> +</style> diff --git a/frontend/src/components/reviewer/ReviewerToolbar.vue b/frontend/src/components/reviewer/ReviewerToolbar.vue new file mode 100644 index 0000000000000000000000000000000000000000..8c547490d824b9aa68d6c209f221d3a2ed543ac3 --- /dev/null +++ b/frontend/src/components/reviewer/ReviewerToolbar.vue @@ -0,0 +1,20 @@ +<template> + <v-toolbar> + <v-toolbar-items> + <v-list-tile-avatar> + <img src="../../assets/brand.png"> + </v-list-tile-avatar> + </v-toolbar-items> + <v-toolbar-title>Grady</v-toolbar-title> + <v-spacer></v-spacer> + </v-toolbar> +</template> + +<script> +export default { + name: 'reviewer-toolbar' +} +</script> + +<style scoped> +</style> diff --git a/frontend/src/components/reviewer/StudentListOverview.vue b/frontend/src/components/reviewer/StudentListOverview.vue new file mode 100644 index 0000000000000000000000000000000000000000..08ae4caff8a21e2869f8c85cedc679a99ee451d4 --- /dev/null +++ b/frontend/src/components/reviewer/StudentListOverview.vue @@ -0,0 +1,21 @@ +<template> + <p> + Whack o ! + </p> +</template> + +<script> +export default { + + name: 'StudentListOverview', + + data () { + return { + + } + } +} +</script> + +<style lang="css" scoped> +</style> diff --git a/frontend/src/components/student/StudentNav.vue b/frontend/src/components/student/StudentNav.vue index 6613a097b7116f3935298a0d4db14d817fe5cabd..3b2484e9d207c8f3e87b8ff9d67eaaec93a1e4d9 100644 --- a/frontend/src/components/student/StudentNav.vue +++ b/frontend/src/components/student/StudentNav.vue @@ -1,28 +1,28 @@ <template> - <b-navbar toggleable="md" type="light" variant="light"> - <b-navbar-toggle target="nav_collapse"></b-navbar-toggle> + <v-navbar toggleable="md" type="light" variant="light"> + <v-navbar-toggle target="nav_collapse"></v-navbar-toggle> - <b-navbar-brand> + <v-navbar-brand> <img src="../../assets/brand.png" width="30" class="d-inline-block align-top"> Grady - </b-navbar-brand> + </v-navbar-brand> - <b-collapse is-nav id="nav_collapse"> + <v-collapse is-nav id="nav_collapse"> - <b-navbar-nav id="nav-left"> - <b-nav-item class="active" href="#">Results</b-nav-item> - <b-nav-item href="#">Statistics</b-nav-item> - </b-navbar-nav> + <v-navbar-nav id="nav-left"> + <v-nav-item class="active" href="#">Results</v-nav-item> + <v-nav-item href="#">Statistics</v-nav-item> + </v-navbar-nav> <!-- Right aligned nav items --> - <b-navbar-nav class="ml-auto"> - <b-nav-item>{{ this.$store.state.username }}</b-nav-item> + <v-navbar-nav class="ml-auto"> + <v-nav-item>{{ this.$store.state.username }}</v-nav-item> <router-link to="/"> - <b-button class="btn-dark" @click="logout()" >Signout</b-button> + <v-button class="btn-dark" @click="logout()" >Signout</v-button> </router-link> - </b-navbar-nav> - </b-collapse> - </b-navbar> + </v-navbar-nav> + </v-collapse> + </v-navbar> </template> diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index c29cbc7da105a411223133581fbde9ddec64aace..efb8236906d71e2dc99b1f2f5453851413f6988a 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -2,6 +2,8 @@ import Vue from 'vue' import Router from 'vue-router' import Login from '@/components/Login' import StudentPage from '@/components/student/StudentPage' +import ReviewerPage from '@/components/reviewer/ReviewerPage' +import StudentListOverview from '@/components/reviewer/StudentListOverview' Vue.use(Router) @@ -16,6 +18,16 @@ export default new Router({ path: '/student/', name: 'student-page', component: StudentPage + }, + { + path: '/reviewer/', + name: 'reviewer-page', + component: ReviewerPage + }, + { + path: 'reviewer/student-overview/', + name: 'student-overview', + component: StudentListOverview } ] })