From fac4373b34eb6fa2cf0ef83407b18fa474a745e8 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Fri, 22 Dec 2017 22:02:18 +0100
Subject: [PATCH] Bug Fixes and StudentSubmissionPage

Restructured front end code into components and pages
Components should be as dumb and generic as possible. Pages should dispatch actions, pass props to components etc,
Student page now gets submission and submissiontyp from api and displays those to the student
Added information which submissions have been viewed
---
 core/permissions.py                           |   2 +-
 core/serializers.py                           |  53 ++++++--
 core/tests/test_student_page.py               | 113 ++++++++++++++---
 core/tests/test_submissiontypeview.py         |  39 +++++-
 core/urls.py                                  |   2 +
 core/views.py                                 |  11 +-
 frontend/src/App.vue                          |   3 +
 .../src/components/{base => }/BaseLayout.vue  |  14 ++-
 frontend/src/components/SubmissionType.vue    |  54 ++++++++
 .../components/student/ExamInformation.vue    |   6 +-
 .../components/student/SubmissionDetail.vue   |  17 ---
 .../src/components/student/SubmissionList.vue |  26 ++--
 .../submission_notes/AnnotatedSubmission.vue  |  50 ++++----
 .../submission_notes/FeedbackComment.vue      |   5 +-
 frontend/src/{components => pages}/Login.vue  |   0
 .../reviewer/ReviewerPage.vue                 |   0
 .../reviewer/ReviewerToolbar.vue              |   0
 .../reviewer/StudentListOverview.vue          |   0
 .../student/StudentLayout.vue                 |  50 +++++---
 .../student/StudentPage.vue                   |  22 ++--
 .../pages/student/StudentSubmissionPage.vue   |  48 ++++++++
 frontend/src/router/index.js                  |  32 ++---
 frontend/src/store/modules/student-page.js    |  76 ++++++++++--
 .../src/store/modules/submission-notes.js     | 116 +++++++-----------
 frontend/src/store/store.js                   |   8 +-
 .../test/unit/specs/SubmissionList.spec.js    |  26 ++--
 util/factories.py                             |   3 +-
 27 files changed, 516 insertions(+), 260 deletions(-)
 rename frontend/src/components/{base => }/BaseLayout.vue (88%)
 create mode 100644 frontend/src/components/SubmissionType.vue
 delete mode 100644 frontend/src/components/student/SubmissionDetail.vue
 rename frontend/src/{components => pages}/Login.vue (100%)
 rename frontend/src/{components => pages}/reviewer/ReviewerPage.vue (100%)
 rename frontend/src/{components => pages}/reviewer/ReviewerToolbar.vue (100%)
 rename frontend/src/{components => pages}/reviewer/StudentListOverview.vue (100%)
 rename frontend/src/{components => pages}/student/StudentLayout.vue (50%)
 rename frontend/src/{components => pages}/student/StudentPage.vue (59%)
 create mode 100644 frontend/src/pages/student/StudentSubmissionPage.vue

diff --git a/core/permissions.py b/core/permissions.py
index bb478558..211ed2fb 100644
--- a/core/permissions.py
+++ b/core/permissions.py
@@ -22,7 +22,7 @@ class IsUserGenericPermission(permissions.BasePermission):
         )
 
         user = request.user
-        is_authorized = user.is_authenticated() and any(isinstance(
+        is_authorized = user.is_authenticated and any(isinstance(
             user.get_associated_user(), models) for models in self.models)
 
         if not is_authorized:
diff --git a/core/serializers.py b/core/serializers.py
index 44b91717..74db00cf 100644
--- a/core/serializers.py
+++ b/core/serializers.py
@@ -4,7 +4,7 @@ from drf_dynamic_fields import DynamicFieldsMixin
 from rest_framework import serializers
 
 from core.models import (ExamType, Feedback, Student, Submission,
-                         SubmissionType, Tutor)
+                         SubmissionType, Test, Tutor)
 from util.factories import GradyUserFactory
 
 log = logging.getLogger(__name__)
@@ -13,7 +13,19 @@ user_factory = GradyUserFactory()
 
 class DynamicFieldsModelSerializer(DynamicFieldsMixin,
                                    serializers.ModelSerializer):
-    pass
+    def __init__(self, *args, **kwargs):
+        # Don't pass the 'fields' arg up to the superclass
+        fields = kwargs.pop('fields', None)
+
+        # Instantiate the superclass normally
+        super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
+
+        if fields is not None:
+            # Drop any fields that are not specified in the `fields` argument.
+            allowed = set(fields)
+            existing = set(self.fields.keys())
+            for field_name in existing - allowed:
+                self.fields.pop(field_name)
 
 
 class ExamSerializer(DynamicFieldsModelSerializer):
@@ -31,35 +43,50 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
         fields = ('text', 'score')
 
 
+class TestSerializer(DynamicFieldsModelSerializer):
+
+    class Meta:
+        model = Test
+        fields = ('name', 'label', 'annotation')
+
+
 class SubmissionTypeSerializer(DynamicFieldsModelSerializer):
+    fullScore = serializers.IntegerField(source='full_score')
 
     class Meta:
         model = SubmissionType
-        fields = ('name', 'full_score', 'description', 'solution')
+        fields = ('id', 'name', 'fullScore', 'description', 'solution')
 
 
 class SubmissionSerializer(DynamicFieldsModelSerializer):
-    feedback = serializers.ReadOnlyField(source='feedback.text')
-    score = serializers.ReadOnlyField(source='feedback.score')
-    type_id = serializers.ReadOnlyField(source='type.id')
-    type_name = serializers.ReadOnlyField(source='type.name')
-    full_score = serializers.ReadOnlyField(source='type.full_score')
+    type = SubmissionTypeSerializer()
+    feedback = FeedbackSerializer()
+    tests = TestSerializer(many=True)
 
     class Meta:
         model = Submission
-        fields = ('type_id', 'type_name', 'text',
-                  'feedback', 'score', 'full_score')
+        fields = ('type', 'text', 'feedback', 'tests')
+
+
+class SubmissionListSerializer(DynamicFieldsModelSerializer):
+    type = SubmissionTypeSerializer(fields=('id', 'name', 'fullScore'))
+    # TODO change this according to new feedback model
+    feedback = FeedbackSerializer(fields=('score',))
+
+    class Meta:
+        model = Submission
+        fields = ('type', 'feedback')
 
 
 class StudentSerializer(DynamicFieldsModelSerializer):
     name = serializers.ReadOnlyField(source='user.fullname')
-    user = serializers.ReadOnlyField(source='user.username')
+    matrikel_no = serializers.ReadOnlyField(source='user.matrikel_no')
     exam = ExamSerializer()
-    submissions = SubmissionSerializer(many=True)
+    submissions = SubmissionListSerializer(many=True)
 
     class Meta:
         model = Student
-        fields = ('name', 'user', 'exam', 'submissions')
+        fields = ('name', 'user', 'matrikel_no', 'exam', 'submissions')
 
 
 class SubmissionNoTextFieldsSerializer(DynamicFieldsModelSerializer):
diff --git a/core/tests/test_student_page.py b/core/tests/test_student_page.py
index 8bd9f349..2af05f2b 100644
--- a/core/tests/test_student_page.py
+++ b/core/tests/test_student_page.py
@@ -3,7 +3,7 @@ from rest_framework.test import (APIRequestFactory, APITestCase,
                                  force_authenticate)
 
 from core.models import SubmissionType
-from core.views import StudentSelfApiView
+from core.views import StudentSelfApiView, StudentSelfSubmissionsApiView
 from util.factories import make_test_data
 
 
@@ -57,7 +57,6 @@ class StudentPageTests(APITestCase):
 
         self.request = self.factory.get(reverse('student-page'))
         self.view = StudentSelfApiView.as_view()
-
         force_authenticate(self.request, user=self.student.user)
         self.response = self.view(self.request)
 
@@ -69,10 +68,6 @@ class StudentPageTests(APITestCase):
         self.assertEqual(
             self.response.data['name'], self.student.user.fullname)
 
-    def test_student_contains_associated_user(self):
-        self.assertEqual(
-            self.response.data['user'], self.student.user.username)
-
     def test_all_student_submissions_are_loded(self):
         self.assertEqual(len(self.submission_list),
                          SubmissionType.objects.count())
@@ -98,34 +93,116 @@ class StudentPageTests(APITestCase):
     # Tests concerning submission data
     def test_a_student_submissions_contains_type_name(self):
         self.assertEqual(
-            self.submission_list_first_entry['type_name'],
+            self.submission_list_first_entry['type']['name'],
             self.student.submissions.first().type.name)
 
     def test_a_student_submissions_contains_type_id(self):
         self.assertEqual(
-            self.submission_list_first_entry['type_id'],
+            self.submission_list_first_entry['type']['id'],
             self.student.submissions.first().type.id)
 
-    def test_submission_data_contains_text(self):
+    def test_submission_data_contains_full_score(self):
         self.assertEqual(
-            self.submission_list_first_entry['text'],
-            self.student.submissions.first().text)
+            self.submission_list_first_entry['type']['fullScore'],
+            self.student.submissions.first().type.full_score)
 
-    def test_submission_data_contains_feedback(self):
+    def test_submission_data_contains_feedback_score(self):
         self.assertEqual(
-            self.submission_list_first_entry['feedback'],
-            self.student.submissions.first().feedback.text)
+            self.submission_list_first_entry['feedback']['score'],
+            self.student.submissions.first().feedback.score)
+
+    # We don't want a matriculation number here
+    def test_matriculation_number_is_not_send(self):
+        self.assertNotIn('matrikel_no', self.submission_list_first_entry)
+
+
+class StudentSelfSubmissionsTests(APITestCase):
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.factory = APIRequestFactory()
+
+    def setUp(self):
+        self.test_data = make_test_data(data_dict={
+            'submission_types': [{
+                'name': 'problem01',
+                'full_score': 10,
+                'description': 'Very hard',
+                'solution': 'Impossible!'
+            }],
+            'students': [{
+                'username': 'user01',
+            }],
+            'tutors': [{
+                'username': 'tutor01'
+            }],
+            'submissions': [{
+                'user': 'user01',
+                'type': 'problem01',
+                'text': 'Too hard for me ;-(',
+                'feedback': {
+                    'of_tutor': 'tutor01',
+                    'text': 'Very bad!',
+                    'score': 3
+                }
+            }]
+        })
+
+        self.student = self.test_data['students'][0]
+        self.tutor = self.test_data['tutors'][0]
+        self.submission = self.test_data['submissions'][0]
+        self.feedback = self.submission.feedback
+
+        self.request = self.factory.get(reverse('student-submissions'))
+        self.view = StudentSelfSubmissionsApiView.as_view()
+
+        force_authenticate(self.request, user=self.student.user)
+        self.response = self.view(self.request)
 
-    def test_submission_data_contains_score(self):
+        self.submission_list = self.response.data
+        self.submission_list_first_entry = self.submission_list[0]
+
+    # Tests concerning submission data
+    def test_a_student_submissions_contains_type_name(self):
         self.assertEqual(
-            self.submission_list_first_entry['score'],
-            self.student.submissions.first().feedback.score)
+            self.submission_list_first_entry['type']['name'],
+            self.student.submissions.first().type.name)
+
+    def test_a_student_submissions_contains_type_id(self):
+        self.assertEqual(
+            self.submission_list_first_entry['type']['id'],
+            self.student.submissions.first().type.id)
 
     def test_submission_data_contains_full_score(self):
         self.assertEqual(
-            self.submission_list_first_entry['full_score'],
+            self.submission_list_first_entry['type']['fullScore'],
             self.student.submissions.first().type.full_score)
 
+    def test_submission_data_contains_description(self):
+        self.assertEqual(
+            self.submission_list_first_entry['type']['description'],
+            self.student.submissions.first().type.description)
+
+    def test_submission_data_contains_solution(self):
+        self.assertEqual(
+            self.submission_list_first_entry['type']['solution'],
+            self.student.submissions.first().type.solution)
+
+    def test_submission_data_contains_text(self):
+        self.assertEqual(
+            self.submission_list_first_entry['text'],
+            self.student.submissions.first().text)
+
+    def test_submission_data_contains_feedback_score(self):
+        self.assertEqual(
+            self.submission_list_first_entry['feedback']['score'],
+            self.student.submissions.first().feedback.score)
+
+    def test_submission_data_contains_feedback_text(self):
+        self.assertEqual(
+            self.submission_list_first_entry['feedback']['text'],
+            self.student.submissions.first().feedback.text)
+
     # We don't want a matriculation number here
     def test_matriculation_number_is_not_send(self):
         self.assertNotIn('matrikel_no', self.submission_list_first_entry)
diff --git a/core/tests/test_submissiontypeview.py b/core/tests/test_submissiontypeview.py
index c1c7f4b0..0bd370da 100644
--- a/core/tests/test_submissiontypeview.py
+++ b/core/tests/test_submissiontypeview.py
@@ -10,7 +10,7 @@ from core.views import SubmissionTypeApiView
 from util.factories import GradyUserFactory
 
 
-class SubmissionTypeViewTest(APITestCase):
+class SubmissionTypeViewTestList(APITestCase):
 
     @classmethod
     def setUpTestData(cls):
@@ -37,10 +37,41 @@ class SubmissionTypeViewTest(APITestCase):
         self.assertEqual('Hard question', self.response.data[0]['name'])
 
     def test_get_full_score(self):
-        self.assertEqual(20, self.response.data[0]['full_score'])
+        self.assertEqual(20, self.response.data[0]['fullScore'])
+
+
+class SubmissionTypeViewTestRetrieve(APITestCase):
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.factory = APIRequestFactory()
+        cls.user_factory = GradyUserFactory()
+
+    def setUp(self):
+        self.request = self.factory.get('/api/submissiontype/')
+        SubmissionType.objects.create(name='Hard question',
+                                      full_score=20,
+                                      description='Whatever')
+        self.pk = SubmissionType.objects.first().pk
+        force_authenticate(self.request,
+                           self.user_factory.make_reviewer().user)
+        self.view = SubmissionTypeApiView.as_view({'get': 'retrieve'})
+        self.response = self.view(self.request, pk=self.pk)
+
+    def test_can_access_when_authenticated(self):
+        self.assertEqual(self.response.status_code, status.HTTP_200_OK)
+
+    def test_get_id(self):
+        self.assertEqual(self.pk, self.response.data['id'])
+
+    def test_get_sumbission_type_name(self):
+        self.assertEqual('Hard question', self.response.data['name'])
+
+    def test_get_full_score(self):
+        self.assertEqual(20, self.response.data['fullScore'])
 
     def test_get_descritpion(self):
-        self.assertEqual('Whatever', self.response.data[0]['description'])
+        self.assertEqual('Whatever', self.response.data['description'])
 
     def test_there_is_no_solution_to_nothing(self):
-        self.assertEqual('', self.response.data[0]['solution'])
+        self.assertEqual('', self.response.data['solution'])
diff --git a/core/urls.py b/core/urls.py
index cb66e7d2..2f2e47e6 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -17,6 +17,8 @@ router.register(r'tutor', views.TutorApiViewSet)
 regular_views_urlpatterns = [
     url(r'student-page', views.StudentSelfApiView.as_view(),
         name='student-page'),
+    url(r'student-submissions', views.StudentSelfSubmissionsApiView.as_view(),
+        name='student-submissions'),
     url(r'user-role', views.get_user_role, name='user-role'),
     url(r'jwt-time-delta', views.get_jwt_expiration_delta,
         name='jwt-time-delta')
diff --git a/core/views.py b/core/views.py
index 6d5a9595..2b87315b 100644
--- a/core/views.py
+++ b/core/views.py
@@ -10,7 +10,8 @@ from core.models import ExamType, Student, SubmissionType, Tutor
 from core.permissions import IsReviewer, IsStudent
 from core.serializers import (ExamSerializer, StudentSerializer,
                               StudentSerializerForListView,
-                              SubmissionTypeSerializer, TutorSerializer)
+                              SubmissionSerializer, SubmissionTypeSerializer,
+                              TutorSerializer)
 
 
 @api_view()
@@ -35,6 +36,14 @@ class StudentSelfApiView(generics.RetrieveAPIView):
         return self.request.user.student
 
 
+class StudentSelfSubmissionsApiView(generics.ListAPIView):
+    permission_classes = (IsStudent, )
+    serializer_class = SubmissionSerializer
+
+    def get_queryset(self):
+        return self.request.user.student.submissions
+
+
 class ExamApiViewSet(viewsets.ReadOnlyModelViewSet):
     """ Gets a list of an individual exam by Id if provided """
     permission_classes = (IsReviewer,)
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 47893229..0e0910b3 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -15,4 +15,7 @@
 </script>
 
 <style>
+  #app {
+    font-family: Roboto, sans-serif;
+  }
 </style>
diff --git a/frontend/src/components/base/BaseLayout.vue b/frontend/src/components/BaseLayout.vue
similarity index 88%
rename from frontend/src/components/base/BaseLayout.vue
rename to frontend/src/components/BaseLayout.vue
index 5daac11f..3f18a986 100644
--- a/frontend/src/components/base/BaseLayout.vue
+++ b/frontend/src/components/BaseLayout.vue
@@ -5,7 +5,7 @@
       clipped
       app
       permanent
-      :mini-variant.sync="mini"
+      :mini-variant="mini"
     >
       <v-toolbar flat>
         <v-list>
@@ -26,7 +26,7 @@
           </v-list-tile>
         </v-list>
       </v-toolbar>
-        <slot name="navigation"></slot>
+        <slot name="sidebar-content"></slot>
     </v-navigation-drawer>
     <v-toolbar
       app
@@ -38,7 +38,7 @@
     >
       <v-toolbar-title>
         <v-avatar>
-          <img src="../../assets/brand.png">
+          <img src="../assets/brand.png">
         </v-avatar>
       </v-toolbar-title>
       <span class="pl-2 grady-speak">{{ gradySpeak }}</span>
@@ -48,7 +48,7 @@
       <v-btn color="blue darken-1" to="/" @click.native="logout">Logout</v-btn>
     </v-toolbar>
     <v-content>
-      <slot></slot>
+      <router-view></router-view>
     </v-content>
   </div>
 </template>
@@ -67,7 +67,6 @@
         'gradySpeak'
       ]),
       ...mapState([
-        'examInstance',
         'username',
         'userRole'
       ])
@@ -76,6 +75,11 @@
       ...mapActions([
         'logout'
       ])
+    },
+    watch: {
+      mini: function () {
+        this.$emit('sidebarMini', this.mini)
+      }
     }
   }
 </script>
diff --git a/frontend/src/components/SubmissionType.vue b/frontend/src/components/SubmissionType.vue
new file mode 100644
index 00000000..5827cbb6
--- /dev/null
+++ b/frontend/src/components/SubmissionType.vue
@@ -0,0 +1,54 @@
+<template>
+  <v-container>
+    <h2 class="mb-2">{{ name }} - Full score: {{ fullScore }}</h2>
+    <v-expansion-panel expand>
+      <v-expansion-panel-content
+      v-for="(item, key, i) in {Description: description, Solution:solution}" 
+      :key="i"
+      :value="expandedByDefault[key]">
+      <div slot="header">{{ key }}</div>
+        <v-card color="grey lighten-4">
+          <v-card-text>
+            {{ item }}
+          </v-card-text>
+        </v-card>
+      </v-expansion-panel-content>
+    </v-expansion-panel>
+  </v-container>
+</template>
+
+
+<script>
+  export default {
+    name: 'submission-type',
+    props: {
+      name: {
+        type: String,
+        required: true
+      },
+      description: {
+        type: String,
+        required: true
+      },
+      solution: {
+        type: String,
+        required: true
+      },
+      fullScore: {
+        type: Number,
+        required: true
+      },
+      expandedByDefault: {
+        type: Object,
+        default: function () {
+          return {
+            Description: true,
+            Solution: true
+          }
+        },
+        required: false
+      }
+    }
+  }
+</script>
+
diff --git a/frontend/src/components/student/ExamInformation.vue b/frontend/src/components/student/ExamInformation.vue
index 817cd58c..21795f05 100644
--- a/frontend/src/components/student/ExamInformation.vue
+++ b/frontend/src/components/student/ExamInformation.vue
@@ -5,12 +5,12 @@
         <th>Modul</th>
         <td>{{ exam.module_reference }}</td>
       </tr>
-      <tr v-if="!exam.pass_only">
+      <tr>
         <th>Pass score</th>
         <td>{{ exam.pass_score }}</td>
       </tr>
-      <tr v-else>
-        <th>Pass only!</th>
+      <tr v-if="exam.passOnly">
+        <th>Pass only exam!</th>
       </tr>
       <tr>
         <th>Total score</th>
diff --git a/frontend/src/components/student/SubmissionDetail.vue b/frontend/src/components/student/SubmissionDetail.vue
deleted file mode 100644
index 1a757b46..00000000
--- a/frontend/src/components/student/SubmissionDetail.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-<template>
-  <v-layout>
-
-    <annotated-submission class="ma-3" :editable="false"></annotated-submission>
-  </v-layout>
-</template>
-
-
-<script>
-  import AnnotatedSubmission from '../submission_notes/AnnotatedSubmission'
-  export default {
-    components: {
-      AnnotatedSubmission
-    },
-    name: 'submission-detail'
-  }
-</script>
diff --git a/frontend/src/components/student/SubmissionList.vue b/frontend/src/components/student/SubmissionList.vue
index 3954d899..7ef34f95 100644
--- a/frontend/src/components/student/SubmissionList.vue
+++ b/frontend/src/components/student/SubmissionList.vue
@@ -7,10 +7,10 @@
       item-key="type"
     >
       <template slot="items" slot-scope="props">
-        <td>{{ props.item.type_name }}</td>
-        <td class="text-xs-right">{{ props.item.score }}</td>
-        <td class="text-xs-right">{{ props.item.full_score }}</td>
-        <td class="text-xs-right"><v-btn :to="`submission/${props.item.type_id}`" color="orange lighten-2">View</v-btn></td>
+        <td>{{ props.item.type.name }}</td>
+        <td class="text-xs-right">{{ props.item.feedback.score }}</td>
+        <td class="text-xs-right">{{ props.item.type.fullScore }}</td>
+        <td class="text-xs-right"><v-btn :to="`submission/${props.item.type.id}`" color="orange lighten-2"><v-icon>chevron_right</v-icon></v-btn></td>
       </template>
     </v-data-table>
     <v-alert color="info" value="true">
@@ -29,22 +29,17 @@
           {
             text: 'Task',
             align: 'left',
-            value: 'type'
+            value: 'type',
+            sortable: false
           },
           {
             text: 'Score',
-            value: 'score'
+            value: 'feedback.score'
           },
           {
             text: 'Maximum Score',
-            value: 'full_score'
+            value: 'type.fullScore'
           }
-        ],
-
-        fields: [
-          { key: 'type', sortable: true },
-          { key: 'score', label: 'Score', sortable: true },
-          { key: 'full_score', sortable: true }
         ]
       }
     },
@@ -56,11 +51,10 @@
     },
     computed: {
       sumScore () {
-        console.log(this.submissions)
-        return this.submissions.map(a => a.score).reduce((a, b) => a + b)
+        return this.submissions.map(a => a.feedback.score).reduce((a, b) => a + b)
       },
       sumFullScore () {
-        return this.submissions.map(a => a.full_score).reduce((a, b) => a + b)
+        return this.submissions.map(a => a.type.fullScore).reduce((a, b) => a + b)
       },
       pointRatio () {
         return ((this.sumScore / this.sumFullScore) * 100).toFixed(2)
diff --git a/frontend/src/components/submission_notes/AnnotatedSubmission.vue b/frontend/src/components/submission_notes/AnnotatedSubmission.vue
index 774d55a8..d5c24b1b 100644
--- a/frontend/src/components/submission_notes/AnnotatedSubmission.vue
+++ b/frontend/src/components/submission_notes/AnnotatedSubmission.vue
@@ -1,11 +1,8 @@
 <template>
-  <table>
+  <table class="elevation-1">
     <tr v-for="(code, index) in submission" :key="index">
       <td class="line-number-cell">
-        <!--<v-tooltip left close-delay="20" color="transparent" content-class="comment-icon">-->
-          <v-btn block class="line-number-btn" slot="activator" @click="toggleEditorOnLine(index)">{{ index }}</v-btn>
-          <!--<v-icon small color="indigo accent-3" class="comment-icon">comment</v-icon>-->
-        <!--</v-tooltip>-->
+        <v-btn block class="line-number-btn" @click="toggleEditorOnLine(index)">{{ index }}</v-btn>
       </td>
       <td>
         <pre class="prettyprint"><code class="lang-c"> {{ code }}</code></pre>
@@ -26,7 +23,6 @@
 
 
 <script>
-  import {mapGetters, mapState} from 'vuex'
   import CommentForm from '@/components/submission_notes/FeedbackForm.vue'
   import FeedbackComment from '@/components/submission_notes/FeedbackComment.vue'
 
@@ -36,24 +32,34 @@
       CommentForm},
     name: 'annotated-submission',
     props: {
+      rawSubmission: {
+        type: String,
+        required: true
+      },
+      score: {
+        type: Number,
+        required: true
+      },
+      feedback: {
+        type: Object,
+        required: true
+      },
       editable: {
         type: Boolean,
         default: false
       }
     },
-    beforeCreate () {
-      this.$store.dispatch('getFeedback', 0)
-      this.$store.dispatch('getSubmission', 0)
-    },
-    computed: {
-      ...mapState({
-        feedback: state => state.submissionNotes.feedback
-      }),
-      ...mapGetters(['submission'])
-    },
     data: function () {
       return {
-        showEditorOnLine: { }
+        showEditorOnLine: {}
+      }
+    },
+    computed: {
+      submission () {
+        return this.rawSubmission.split('\n').reduce((acc, cur, index) => {
+          acc[index + 1] = cur
+          return acc
+        }, {})
       }
     },
     methods: {
@@ -75,13 +81,8 @@
     border-collapse: collapse;
   }
 
-  td {
-    /*white-space: nowrap;*/
-    /*border: 1px solid green;*/
-  }
 
   .line-number-cell {
-    /*padding-left: 50px;*/
     vertical-align: top;
   }
 
@@ -101,9 +102,4 @@
     min-width: fit-content;
     margin: 0;
   }
-
-  .comment-icon {
-    border: 0;
-  }
-
 </style>
diff --git a/frontend/src/components/submission_notes/FeedbackComment.vue b/frontend/src/components/submission_notes/FeedbackComment.vue
index a63b4de6..637fce73 100644
--- a/frontend/src/components/submission_notes/FeedbackComment.vue
+++ b/frontend/src/components/submission_notes/FeedbackComment.vue
@@ -40,12 +40,11 @@
     margin: 20px 10px 10px 10px;
     padding: 5px;
     background-color: #F3F3F3;
-    border-radius: 5px;
-    border: 5px solid #3D8FC1;
+    border-radius: 0px;
+    border: 2px solid #3D8FC1;
   }
 
   .body .message {
-    font-family: Roboto, sans-serif;
     min-height: 30px;
     border-radius: 3px;
     font-size: 14px;
diff --git a/frontend/src/components/Login.vue b/frontend/src/pages/Login.vue
similarity index 100%
rename from frontend/src/components/Login.vue
rename to frontend/src/pages/Login.vue
diff --git a/frontend/src/components/reviewer/ReviewerPage.vue b/frontend/src/pages/reviewer/ReviewerPage.vue
similarity index 100%
rename from frontend/src/components/reviewer/ReviewerPage.vue
rename to frontend/src/pages/reviewer/ReviewerPage.vue
diff --git a/frontend/src/components/reviewer/ReviewerToolbar.vue b/frontend/src/pages/reviewer/ReviewerToolbar.vue
similarity index 100%
rename from frontend/src/components/reviewer/ReviewerToolbar.vue
rename to frontend/src/pages/reviewer/ReviewerToolbar.vue
diff --git a/frontend/src/components/reviewer/StudentListOverview.vue b/frontend/src/pages/reviewer/StudentListOverview.vue
similarity index 100%
rename from frontend/src/components/reviewer/StudentListOverview.vue
rename to frontend/src/pages/reviewer/StudentListOverview.vue
diff --git a/frontend/src/components/student/StudentLayout.vue b/frontend/src/pages/student/StudentLayout.vue
similarity index 50%
rename from frontend/src/components/student/StudentLayout.vue
rename to frontend/src/pages/student/StudentLayout.vue
index e7f12d7e..28bcabe7 100644
--- a/frontend/src/components/student/StudentLayout.vue
+++ b/frontend/src/pages/student/StudentLayout.vue
@@ -1,9 +1,12 @@
 <template>
-  <base-layout>
+  <base-layout @sidebarMini="mini = $event">
+
     <template  slot="header">
       {{ module_reference }}
     </template>
-    <v-list dense slot="navigation">
+
+    <v-list dense slot="sidebar-content">
+
       <v-list-tile exact v-for="(item, i) in generalNavItems" :key="i" :to="item.route">
         <v-list-tile-action>
           <v-icon>{{ item.icon }}</v-icon>
@@ -14,30 +17,38 @@
           </v-list-tile-title>
         </v-list-tile-content>
       </v-list-tile>
+
       <v-divider></v-divider>
-    <v-list-tile exact v-for="(item, i) in submissionNavItems" :key="i" :to="item.route">
-      <v-list-tile-action>
-        <v-icon>assignment</v-icon>
-      </v-list-tile-action>
-      <v-list-tile-content>
-        <v-list-tile-title>
-          {{ item.name }}
-        </v-list-tile-title>
-      </v-list-tile-content>
-    </v-list-tile>
+        <v-card color="grey lighten-2" v-if="!mini">
+          <v-card-title primary-title>
+            <exam-information :exam="exam"></exam-information>
+          </v-card-title>
+        </v-card>
+      <v-list-tile exact v-for="(item, i) in submissionNavItems" :key="i" :to="item.route">
+        <v-list-tile-action>
+          <v-icon v-if="!visited[item.id]">assignment</v-icon>
+          <v-icon v-else>check</v-icon>
+        </v-list-tile-action>
+        <v-list-tile-content>
+          <v-list-tile-title>
+            {{ item.name }}
+          </v-list-tile-title>
+        </v-list-tile-content>
+      </v-list-tile>
     </v-list>
-    <router-view></router-view>
   </base-layout>
 </template>
 
 <script>
   import { mapState } from 'vuex'
-  import BaseLayout from '../base/BaseLayout'
+  import BaseLayout from '@/components/BaseLayout'
+  import ExamInformation from '@/components/student/ExamInformation'
   export default {
-    components: {BaseLayout},
+    components: {BaseLayout, ExamInformation},
     name: 'student-layout',
     data () {
       return {
+        mini: false,
         generalNavItems: [
           {
             name: 'Overview',
@@ -55,13 +66,16 @@
     computed: {
       ...mapState({
         module_reference: state => state.studentPage.exam.module_reference,
-        submissions: state => state.studentPage.submissions
+        submissions: state => state.studentPage.submissionsForList,
+        exam: state => state.studentPage.exam,
+        visited: state => state.studentPage.visited
       }),
       submissionNavItems: function () {
         return this.submissions.map((sub, index) => {
           return {
-            name: sub.type_name,
-            route: `/student/submission/${sub.type_id}`
+            name: sub.type.name,
+            id: sub.type.id,
+            route: `/student/submission/${sub.type.id}`
           }
         })
       }
diff --git a/frontend/src/components/student/StudentPage.vue b/frontend/src/pages/student/StudentPage.vue
similarity index 59%
rename from frontend/src/components/student/StudentPage.vue
rename to frontend/src/pages/student/StudentPage.vue
index 1bcb1fc7..f42887fa 100644
--- a/frontend/src/components/student/StudentPage.vue
+++ b/frontend/src/pages/student/StudentPage.vue
@@ -1,12 +1,8 @@
 <template>
     <v-container fluid>
       <v-layout justify center>
-        <v-flex md3>
-          <h2>Exam Overview</h2>
-          <exam-information v-if="!loading" :exam="exam"></exam-information>
-        </v-flex>
-        <template v-if="!loading">
-          <v-flex md7 offset-md1>
+        <template v-if="loaded">
+          <v-flex md10 mt-5 offset-xs1>
             <h2>Submissions of {{ studentName }}</h2>
             <submission-list :submissions="submissions"></submission-list>
           </v-flex>
@@ -19,8 +15,8 @@
 <script>
   import {mapState} from 'vuex'
   import StudentLayout from './StudentLayout.vue'
-  import SubmissionList from './SubmissionList.vue'
-  import ExamInformation from './ExamInformation.vue'
+  import SubmissionList from '@/components/student/SubmissionList.vue'
+  import ExamInformation from '@/components/student/ExamInformation.vue'
 
   export default {
     components: {
@@ -29,14 +25,18 @@
       StudentLayout},
     name: 'student-page',
     created: function () {
-      this.$store.dispatch('getStudentData')
+      if (!this.loaded) {
+        this.$store.dispatch('getStudentData').then(() => {
+          this.$store.dispatch('getStudentSubmissions')
+        })
+      }
     },
     computed: {
       ...mapState({
         studentName: state => state.studentPage.studentName,
         exam: state => state.studentPage.exam,
-        submissions: state => state.studentPage.submissions,
-        loading: state => state.studentPage.loading
+        submissions: state => state.studentPage.submissionsForList,
+        loaded: state => state.studentPage.loaded
       })
     }
   }
diff --git a/frontend/src/pages/student/StudentSubmissionPage.vue b/frontend/src/pages/student/StudentSubmissionPage.vue
new file mode 100644
index 00000000..1d13fc4c
--- /dev/null
+++ b/frontend/src/pages/student/StudentSubmissionPage.vue
@@ -0,0 +1,48 @@
+<template>
+  <v-container flex>
+    <v-layout>
+      <v-flex xs-12 sm-6 md-6 ma-5>
+        <annotated-submission 
+        :rawSubmission="rawSubmission"
+        :score="score"
+        :feedback="{}">
+        </annotated-submission>
+      </v-flex>
+      <v-flex xs-12 sm-6 md-6>
+        <submission-type
+        v-bind="submissionType">
+        </submission-type>
+      </v-flex>
+    </v-layout>
+  </v-container>
+</template>
+
+
+<script>
+  import { mapState } from 'vuex'
+  import AnnotatedSubmission from '@/components/submission_notes/AnnotatedSubmission'
+  import SubmissionType from '@/components/SubmissionType'
+  export default {
+    name: 'student-submission-page',
+    components: {AnnotatedSubmission, SubmissionType},
+    computed: {
+      id: function () {
+        return this.$route.params.id
+      },
+      ...mapState({
+        rawSubmission: function (state) { return state.studentPage.submissionData[this.id].text },
+        score: function (state) { return state.studentPage.submissionData[this.id].feedback.score },
+        submissionType: function (state) { return state.studentPage.submissionData[this.id].type }
+        // feedback: function (state) { return state.studentPage.submissionData[this.$route.params.id].feedback.text }
+      })
+    },
+    mounted: function () {
+      this.$store.commit('SET_VISITED', { index: this.id, visited: true })
+    },
+    updated: function () {
+      if (this.id) {
+        this.$store.commit('SET_VISITED', { index: this.id, visited: true })
+      }
+    }
+  }
+</script>
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index 849fbf66..9ff1b839 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -1,14 +1,12 @@
 import Vue from 'vue'
 import Router from 'vue-router'
 import store from '../store/store'
-import Login from '@/components/Login'
-import StudentPage from '@/components/student/StudentPage'
-import StudentLayout from '@/components/student/StudentLayout'
-import SubmissionDetail from '@/components/student/SubmissionDetail'
-import ReviewerPage from '@/components/reviewer/ReviewerPage'
-import StudentListOverview from '@/components/reviewer/StudentListOverview'
-import BaseLayout from '@/components/base/BaseLayout'
-import AnnotatedSubmission from '@/components/submission_notes/AnnotatedSubmission'
+import Login from '@/pages/Login'
+import StudentPage from '@/pages/student/StudentPage'
+import StudentSubmissionPage from '@/pages/student/StudentSubmissionPage'
+import StudentLayout from '@/pages/student/StudentLayout'
+import ReviewerPage from '@/pages/reviewer/ReviewerPage'
+import StudentListOverview from '@/pages/reviewer/StudentListOverview'
 
 Vue.use(Router)
 
@@ -29,7 +27,7 @@ const router = new Router({
         },
         {
           path: 'submission/:id',
-          component: SubmissionDetail
+          component: StudentSubmissionPage
         }
       ]
 
@@ -43,28 +41,16 @@ const router = new Router({
       path: 'reviewer/student-overview/',
       name: 'student-overview',
       component: StudentListOverview
-    },
-    {
-      path: '/base/',
-      name: 'base-layout',
-      component: BaseLayout
-    },
-    {
-      path: '/notes/',
-      name: 'annotated-submission',
-      component: AnnotatedSubmission
     }
   ]
 })
 
 router.beforeEach((to, from, next) => {
-  if (to.path === '/') {
+  if (to.path === '/' || from.path === '/') {
     next()
   } else {
-    const now = new Date()
+    const now = Date.now()
     if (now - store.state.logInTime > store.state.jwtTimeDelta * 1000) {
-      console.log(now)
-      console.log(store.state.logInTime)
       store.dispatch('logout').then(() => {
         store.commit('API_FAIL', 'You\'ve been logged out due to inactivity')
         next('/')
diff --git a/frontend/src/store/modules/student-page.js b/frontend/src/store/modules/student-page.js
index ce6cc28b..45a5b34f 100644
--- a/frontend/src/store/modules/student-page.js
+++ b/frontend/src/store/modules/student-page.js
@@ -4,9 +4,10 @@ const studentPage = {
   state: {
     studentName: '',
     exam: {},
-    submissionTypes: [],
-    submissions: [],
-    loading: true
+    submissionsForList: [],
+    submissionData: {},
+    visited: {},
+    loaded: false
   },
   mutations: {
     'SET_STUDENT_NAME': function (state, name) {
@@ -18,26 +19,81 @@ const studentPage = {
     'SET_SUBMISSION_TYPES': function (state, submissionTypes) {
       state.submissionTypes = submissionTypes
     },
-    'SET_SUBMISSIONS': function (state, submissions) {
-      state.submissions = submissions
+    'SET_SUBMISSIONS_FOR_LIST': function (state, submissions) {
+      state.submissionsForList = submissions
     },
-    'SET_LOADING': function (state, loading) {
-      state.loading = loading
+    /**
+     * Reduces the array submissionData returned by the /api/student-submissions
+     * into an object where the keys are the SubmissionType id's and the values
+     * the former array elements. This is done to have direct access to the data
+     * via the SubmissionType id.
+     */
+    'SET_FULL_SUBMISSION_DATA': function (state, submissionData) {
+      state.submissionData = submissionData.reduce((acc, cur, index) => {
+        acc[cur.type.id] = cur
+        return acc
+      }, {})
+    },
+    'SET_VISITED': function (state, visited) {
+      state.visited = { ...state.visited, [visited.index]: visited.visited }
+    },
+    'SET_LOADED': function (state, loaded) {
+      state.loaded = loaded
     }
   },
   actions: {
 
     getStudentData (context) {
-      context.commit('SET_LOADING', true)
+      context.commit('SET_LOADED', false)
       ax.get('api/student-page/').then(response => {
         const data = response.data
         context.commit('SET_STUDENT_NAME', data.name)
         context.commit('SET_EXAM', data.exam)
-        context.commit('SET_SUBMISSIONS', data.submissions)
-        context.commit('SET_LOADING', false)
+        context.commit('SET_SUBMISSIONS_FOR_LIST', data.submissions)
+        context.commit('SET_LOADED', true)
       })
+    },
+
+    async getStudentSubmissions (context) {
+      const response = await ax.get('/api/student-submissions')
+      context.commit('SET_FULL_SUBMISSION_DATA', response.data)
     }
   }
 }
 
+// const mockSubmission = '//Procedural Programming technique shows creation of Pascal\'s Triangl\n' +
+//   '#include <iostream>\n' +
+//   '#include <iomanip>\n' +
+//   '\n' +
+//   'using namespace std;\n' +
+//   '\n' +
+//   '\n' +
+//   'int** comb(int** a , int row , int col)\n' +
+//   '{\n' +
+//   '   int mid = col/2;\n' +
+//   '        //clear matrix\n' +
+//   '         for( int i = 0 ; i < row ; i++)\n' +
+//   '         for( int j = 0 ; j < col ; j++)\n' +
+//   '                a[i][j] = 0;\n' +
+//   '                a[0][mid] = 1; //put 1 in the middle of first row\n' +
+//   '    //build up Pascal\'s Triangle matrix\n' +
+//   '     for( int i = 1 ; i < row ; i++)\n' +
+//   '        {\n' +
+//   '          for( int j = 1 ; j < col - 1 ; j++)\n' +
+//   '               a[i][j] = a[i-1][j-1] + a[i-1][j+1];\n' +
+//   '        }\n' +
+//   '   return a;\n' +
+//   '}\n' +
+//   'void disp(int** ptr, int row, int col)\n' +
+//   '{\n' +
+//   '  cout << endl << endl;\n' +
+//   '    for ( int i = 0 ; i < row ; i++)\n' +
+//   '        {\n' +
+//   '        for ( int j = 0 ; j < col ; j++)\n'
+
+// const mockFeedback = {
+//   '1': 'Youre STUPID',
+//   '4': 'Very much so'
+// }
+
 export default studentPage
diff --git a/frontend/src/store/modules/submission-notes.js b/frontend/src/store/modules/submission-notes.js
index cde09fa0..10a47e24 100644
--- a/frontend/src/store/modules/submission-notes.js
+++ b/frontend/src/store/modules/submission-notes.js
@@ -1,76 +1,44 @@
-import Vue from 'vue'
+// import Vue from 'vue'
 
-const mockSubmission = '//Procedural Programming technique shows creation of Pascal\'s Triangl\n' +
-  '#include <iostream>\n' +
-  '#include <iomanip>\n' +
-  'using namespace std;\n' +
-  'int** comb(int** a , int row , int col)\n' +
-  '{\n' +
-  '   int mid = col/2;\n' +
-  '        //clear matrix\n' +
-  '         for( int i = 0 ; i < row ; i++)\n' +
-  '         for( int j = 0 ; j < col ; j++)\n' +
-  '                a[i][j] = 0;\n' +
-  '                a[0][mid] = 1; //put 1 in the middle of first row\n' +
-  '    //build up Pascal\'s Triangle matrix\n' +
-  '     for( int i = 1 ; i < row ; i++)\n' +
-  '        {\n' +
-  '          for( int j = 1 ; j < col - 1 ; j++)\n' +
-  '               a[i][j] = a[i-1][j-1] + a[i-1][j+1];\n' +
-  '        }\n' +
-  '   return a;\n' +
-  '}\n' +
-  'void disp(int** ptr, int row, int col)\n' +
-  '{\n' +
-  '  cout << endl << endl;\n' +
-  '    for ( int i = 0 ; i < row ; i++)\n' +
-  '        {\n' +
-  '        for ( int j = 0 ; j < col ; j++)\n'
+// const submissionNotes = {
+//   state: {
+//     rawSubmission: '',
+//     feedback: {}
+//   },
+//   getters: {
+//     // reduce the string rawSubmission into an object where the keys are the
+//     // line indexes starting at one and the values the corresponding submission line
+//     // this makes iterating over the submission much more pleasant
+//     submission: state => {
+//       return state.rawSubmission.split('\n').reduce((acc, cur, index) => {
+//         acc[index + 1] = cur
+//         return acc
+//       }, {})
+//     }
+//   },
+//   mutations: {
+//     'SET_RAW_SUBMISSION': function (state, submission) {
+//       state.rawSubmission = mockSubmission
+//     },
+//     'SET_FEEDBACK': function (state, feedback) {
+//       state.feedback = feedback
+//     },
+//     'UPDATE_FEEDBACK': function (state, feedback) {
+//       Vue.set(state.feedback, feedback.lineIndex, feedback.content)
+//     }
+//   },
+//   actions: {
+//     // TODO remove mock data
+//     getSubmission (context, submissionId) {
+//       context.commit('SET_RAW_SUBMISSION', mockSubmission)
+//     },
+//     getFeedback (context, feedbackId) {
+//       context.commit('SET_FEEDBACK', mockFeedback)
+//     },
+//     updateFeedback (context, lineIndex, feedbackContent) {
+//       context.commit('UPDATE_FEEDBACK', lineIndex, feedbackContent)
+//     }
+//   }
+// }
 
-const mockFeedback = {
-  '1': 'Youre STUPID',
-  '4': 'Very much so'
-}
-
-const submissionNotes = {
-  state: {
-    rawSubmission: '',
-    feedback: {}
-  },
-  getters: {
-    // reduce the string rawSubmission into an object where the keys are the
-    // line indexes starting at one and the values the corresponding submission line
-    // this makes iterating over the submission much more pleasant
-    submission: state => {
-      return state.rawSubmission.split('\n').reduce((acc, cur, index) => {
-        acc[index + 1] = cur
-        return acc
-      }, {})
-    }
-  },
-  mutations: {
-    'SET_RAW_SUBMISSION': function (state, submission) {
-      state.rawSubmission = mockSubmission
-    },
-    'SET_FEEDBACK': function (state, feedback) {
-      state.feedback = feedback
-    },
-    'UPDATE_FEEDBACK': function (state, feedback) {
-      Vue.set(state.feedback, feedback.lineIndex, feedback.content)
-    }
-  },
-  actions: {
-    // TODO remove mock data
-    getSubmission (context, submissionId) {
-      context.commit('SET_RAW_SUBMISSION', mockSubmission)
-    },
-    getFeedback (context, feedbackId) {
-      context.commit('SET_FEEDBACK', mockFeedback)
-    },
-    updateFeedback (context, lineIndex, feedbackContent) {
-      context.commit('UPDATE_FEEDBACK', lineIndex, feedbackContent)
-    }
-  }
-}
-
-export default submissionNotes
+// export default submissionNotes
diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js
index e9c47976..0eb0f5fb 100644
--- a/frontend/src/store/store.js
+++ b/frontend/src/store/store.js
@@ -3,22 +3,20 @@ import Vue from 'vue'
 import ax from './api'
 
 import gradySays from './grady_speak'
-import submissionNotes from './modules/submission-notes'
 import studentPage from './modules/student-page'
 
 Vue.use(Vuex)
 
 const store = new Vuex.Store({
   modules: {
-    submissionNotes,
     studentPage
   },
   state: {
     token: sessionStorage.getItem('jwtToken'),
     loggedIn: !!sessionStorage.getItem('jwtToken'),
-    logInTime: sessionStorage.getItem('logInTime'),
+    logInTime: Number(sessionStorage.getItem('logInTime')),
     username: sessionStorage.getItem('username'),
-    jwtTimeDelta: sessionStorage.getItem('jwtTimeDelta'),
+    jwtTimeDelta: Number(sessionStorage.getItem('jwtTimeDelta')),
     userRole: sessionStorage.getItem('userRole'),
     error: ''
   },
@@ -36,7 +34,7 @@ const store = new Vuex.Store({
       state.logInTime = Date.now()
       ax.defaults.headers['Authorization'] = 'JWT ' + token
       sessionStorage.setItem('jwtToken', token)
-      sessionStorage.setItem('logInTime', state.logInTime)
+      sessionStorage.setItem('logInTime', String(state.logInTime))
     },
     'SET_JWT_TIME_DELTA': function (state, timeDelta) {
       state.jwtTimeDelta = timeDelta
diff --git a/frontend/test/unit/specs/SubmissionList.spec.js b/frontend/test/unit/specs/SubmissionList.spec.js
index 8e855486..59542a0f 100644
--- a/frontend/test/unit/specs/SubmissionList.spec.js
+++ b/frontend/test/unit/specs/SubmissionList.spec.js
@@ -4,18 +4,24 @@ import SubmissionList from '@/components/student/SubmissionList'
 describe('SubmissionList.vue', () => {
   it('tests the SubmissionList for students', () => {
     const data = [{
-      'type': 'Aufgabe 01',
-      'text': 'I dont know the answer.',
-      'feedback': 'I am very disappointed.',
-      'score': 5,
-      'full_score': 14
+      type: {
+        name: 'Aufgabe 01',
+        fullScore: 14
+      },
+      feedback: {
+        text: 'I am very disappointed.',
+        score: 5
+      }
     },
     {
-      'type': 'Aufgabe 01',
-      'text': 'A very good solution, indeed',
-      'feedback': 'I am still very disappointed.',
-      'score': 7,
-      'full_score': 10
+      type: {
+        name: 'Aufgabe 02',
+        fullScore: 10
+      },
+      feedback: {
+        text: 'I am still very disappointed.',
+        score: 7
+      }
     }]
 
     const Constructor = Vue.extend(SubmissionList)
diff --git a/util/factories.py b/util/factories.py
index 63da6ecf..f90e649c 100644
--- a/util/factories.py
+++ b/util/factories.py
@@ -134,7 +134,8 @@ def make_submission_types(submission_types=[], **kwargs):
 def make_students(students=[], **kwargs):
     return [GradyUserFactory().make_student(
         username=student['username'],
-        exam=ExamType.objects.get(module_reference=student['exam']),
+        exam=ExamType.objects.get(module_reference=student['exam']) if
+        'exam' in student else None,
         password=student.get('password', None)
     ) for student in students]
 
-- 
GitLab