From 9306fdeb0b17e110d49b96f1c567c3638997acd5 Mon Sep 17 00:00:00 2001 From: janmax <j.michal@stud.uni-goettingen.de> Date: Sun, 26 Nov 2017 20:47:25 +0100 Subject: [PATCH] Refactored all views to ViewSets. Has the following benefits * no url configuration needed simply register viewset with router * now using DefaultRouter, meaning api root is now browsable * merged some views * makes it easier to include api schema later * Ran isort and updated docstring --- backend/core/models.py | 3 +- backend/core/permissions.py | 4 +- backend/core/serializers.py | 11 ++++ backend/core/tests/test_access_rights.py | 6 +- backend/core/tests/test_examlist.py | 8 +-- backend/core/tests/test_student_page.py | 6 +- .../core/tests/test_tutor_api_endpoints.py | 21 +++---- backend/core/urls.py | 19 ++++--- backend/core/views.py | 45 ++++++++------- frontend/src/components/Login.vue | 2 +- .../src/components/reviewer/ReviewerPage.vue | 57 +++++++++++++++++++ .../components/reviewer/ReviewerToolbar.vue | 20 +++++++ .../reviewer/StudentListOverview.vue | 21 +++++++ .../src/components/student/StudentNav.vue | 30 +++++----- frontend/src/router/index.js | 12 ++++ 15 files changed, 191 insertions(+), 74 deletions(-) create mode 100644 frontend/src/components/reviewer/ReviewerPage.vue create mode 100644 frontend/src/components/reviewer/ReviewerToolbar.vue create mode 100644 frontend/src/components/reviewer/StudentListOverview.vue diff --git a/backend/core/models.py b/backend/core/models.py index 76b02460..b7c7f3eb 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 29b60731..564a93c6 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 afccb1bb..08de11b0 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 ff9857d6..bd4627be 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 51d55a8b..67b3c84d 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 2b15fb97..166583ad 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 ba21ab11..6f65aade 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 72e447a4..f3dde516 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 4470a4ab..690d6cfb 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 325fa0c9..22dd0f09 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 00000000..1bcc4a33 --- /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 00000000..8c547490 --- /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 00000000..08ae4caf --- /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 6613a097..3b2484e9 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 c29cbc7d..efb82369 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 } ] }) -- GitLab