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