diff --git a/.gitignore b/.gitignore
index 94f4e488ebf83d4527182546e3ee8cb3fd9af4a9..9eba662cd6b951fc8dcec32caa0c0b29f9bca3e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,7 @@ public/
 *.sublime-*
 .idea/
 .vscode/
+anon-export/
 
 # node
 node_modules
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 76b05a06c9e304bb955c52235792595912933c54..241d7579edecffbc2434ced41aad3dc717d40eec 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -64,6 +64,7 @@ test_flake8:
 
 test_frontend:
   <<: *test_definition_frontend
+  when: manual
   stage: test
   script:
     - yarn install
diff --git a/core/models.py b/core/models.py
index 4ffd5bcf9f92c6ef6534aceaaaf1b8208b3f6749..413c51023afafc6045a4a2dbb86c79191ee29924 100644
--- a/core/models.py
+++ b/core/models.py
@@ -435,7 +435,7 @@ class GeneralTaskSubscription(models.Model):
         RANDOM: '__any',
         STUDENT_QUERY: 'student__student_id',
         EXAM_TYPE_QUERY: 'student__examtype__module_reference',
-        SUBMISSION_TYPE_QUERY: 'type__title',
+        SUBMISSION_TYPE_QUERY: 'type__name',
     }
 
     QUERY_CHOICE = (
diff --git a/core/serializers.py b/core/serializers.py
index 5dd096095b358040d9c8d2ecf57da30de27f39a4..64c50f5a73bb1292df3055b6c31b0108797adf8f 100644
--- a/core/serializers.py
+++ b/core/serializers.py
@@ -3,7 +3,6 @@ import logging
 from django.core.exceptions import ObjectDoesNotExist
 from drf_dynamic_fields import DynamicFieldsMixin
 from rest_framework import serializers
-from rest_framework.validators import UniqueValidator
 
 from core import models
 from core.models import (ExamType, Feedback, GeneralTaskSubscription,
@@ -45,7 +44,7 @@ class ExamSerializer(DynamicFieldsModelSerializer):
 
     class Meta:
         model = ExamType
-        fields = ('module_reference', 'total_score',
+        fields = ('pk', 'module_reference', 'total_score',
                   'pass_score', 'pass_only',)
 
 
@@ -62,16 +61,10 @@ class FeedbackForSubmissionLineSerializer(serializers.BaseSerializer):
 
 
 class FeedbackSerializer(DynamicFieldsModelSerializer):
-    assignment_id = serializers.UUIDField(write_only=True)
+    assignment_pk = serializers.UUIDField(write_only=True)
     feedback_lines = FeedbackForSubmissionLineSerializer(
         required=False
     )
-    isFinal = serializers.BooleanField(source="is_final", required=False)
-    ofSubmission = serializers.PrimaryKeyRelatedField(
-        source='of_submission',
-        required=False,
-        queryset=Submission.objects.all(),
-        validators=[UniqueValidator(queryset=Feedback.objects.all()), ])
 
     def create(self, validated_data) -> Feedback:
         feedback = Feedback.objects.create(
@@ -96,13 +89,13 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
 
     def validate(self, data):
         log.debug(data)
-        assignment_id = data.pop('assignment_id')
+        assignment_pk = data.pop('assignment_pk')
         score = data.get('score')
         is_final = data.get('is_final', False)
 
         try:
             assignment = TutorSubmissionAssignment.objects.get(
-                assignment_id=assignment_id)
+                pk=assignment_pk)
         except ObjectDoesNotExist as err:
             raise serializers.ValidationError('No assignment for given id.')
 
@@ -133,21 +126,20 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
 
     class Meta:
         model = Feedback
-        fields = ('assignment_id', 'isFinal', 'score',
-                  'ofSubmission', 'feedback_lines')
+        fields = ('pk', 'assignment_pk', 'is_final', 'score',
+                  'of_submission', 'feedback_lines')
+        read_only_fields = ('of_submission', )
 
 
 class FeedbackCommentSerializer(serializers.ModelSerializer):
-
-    ofTutor = serializers.StringRelatedField(source='of_tutor.username')
-    isFinalComment = serializers.BooleanField(source='is_final')
+    of_tutor = serializers.StringRelatedField(source='of_tutor.username')
 
     def to_internal_value(self, data):
         return data
 
     class Meta:
         model = models.FeedbackComment
-        fields = ('text', 'ofTutor', 'created', 'isFinalComment')
+        fields = ('text', 'of_tutor', 'created', 'is_final')
         read_only_fields = ('created',)
 
 
@@ -155,22 +147,21 @@ class TestSerializer(DynamicFieldsModelSerializer):
 
     class Meta:
         model = Test
-        fields = ('name', 'label', 'annotation')
+        fields = ('pk', 'name', 'label', 'annotation')
 
 
 class SubmissionTypeListSerializer(DynamicFieldsModelSerializer):
-    fullScore = serializers.IntegerField(source='full_score')
 
     class Meta:
         model = SubmissionType
-        fields = ('id', 'name', 'fullScore')
+        fields = ('pk', 'name', 'full_score')
 
 
 class SubmissionTypeSerializer(SubmissionTypeListSerializer):
 
     class Meta:
         model = SubmissionType
-        fields = ('id', 'name', 'fullScore', 'description', 'solution')
+        fields = ('pk', 'name', 'full_score', 'description', 'solution')
 
 
 class SubmissionSerializer(DynamicFieldsModelSerializer):
@@ -180,17 +171,17 @@ class SubmissionSerializer(DynamicFieldsModelSerializer):
 
     class Meta:
         model = Submission
-        fields = ('type', 'text', 'feedback', 'tests')
+        fields = ('pk', 'type', 'text', 'feedback', 'tests')
 
 
 class SubmissionListSerializer(DynamicFieldsModelSerializer):
-    type = SubmissionTypeListSerializer(fields=('id', 'name', 'fullScore'))
+    type = SubmissionTypeListSerializer(fields=('pk', 'name', 'full_score'))
     # TODO change this according to new feedback model
     feedback = FeedbackSerializer(fields=('score',))
 
     class Meta:
         model = Submission
-        fields = ('type', 'feedback')
+        fields = ('pk', 'type', 'feedback')
 
 
 class StudentInfoSerializer(DynamicFieldsModelSerializer):
@@ -201,17 +192,17 @@ class StudentInfoSerializer(DynamicFieldsModelSerializer):
 
     class Meta:
         model = StudentInfo
-        fields = ('name', 'user', 'matrikel_no', 'exam', 'submissions')
+        fields = ('pk', 'name', 'user', 'matrikel_no', 'exam', 'submissions')
 
 
 class SubmissionNoTextFieldsSerializer(DynamicFieldsModelSerializer):
     score = serializers.ReadOnlyField(source='feedback.score')
     type = serializers.ReadOnlyField(source='type.name')
-    fullScore = serializers.ReadOnlyField(source='type.full_score')
+    full_score = serializers.ReadOnlyField(source='type.full_score')
 
     class Meta:
         model = Submission
-        fields = ('type', 'score', 'fullScore')
+        fields = ('pk', 'type', 'score', 'full_score')
 
 
 class StudentInfoSerializerForListView(DynamicFieldsModelSerializer):
@@ -222,7 +213,7 @@ class StudentInfoSerializerForListView(DynamicFieldsModelSerializer):
 
     class Meta:
         model = StudentInfo
-        fields = ('name', 'user', 'exam', 'submissions')
+        fields = ('pk', 'name', 'user', 'exam', 'submissions')
 
 
 class TutorSerializer(DynamicFieldsModelSerializer):
@@ -236,26 +227,26 @@ class TutorSerializer(DynamicFieldsModelSerializer):
 
     class Meta:
         model = UserAccount
-        fields = ('username', 'done_assignments_count')
+        fields = ('pk', 'username', 'done_assignments_count')
 
 
 class AssignmentSerializer(DynamicFieldsModelSerializer):
-    submission_id = serializers.ReadOnlyField(
-        source='submission.submission_id')
+    submission_pk = serializers.ReadOnlyField(
+        source='submission.pk')
 
     class Meta:
         model = TutorSubmissionAssignment
-        fields = ('assignment_id', 'submission_id', 'is_done',)
+        fields = ('pk', 'submission_pk', 'is_done',)
 
 
 class SubmissionAssignmentSerializer(DynamicFieldsModelSerializer):
     text = serializers.ReadOnlyField()
-    typeId = serializers.ReadOnlyField(source='type.id')
-    fullScore = serializers.ReadOnlyField(source='type.full_score')
+    type_pk = serializers.ReadOnlyField(source='type.pk')
+    full_score = serializers.ReadOnlyField(source='type.full_score')
 
     class Meta:
         model = Submission
-        fields = ('submission_id', 'typeId', 'text', 'fullScore')
+        fields = ('pk', 'type_pk', 'text', 'full_score')
 
 
 class AssignmentDetailSerializer(DynamicFieldsModelSerializer):
@@ -264,7 +255,7 @@ class AssignmentDetailSerializer(DynamicFieldsModelSerializer):
 
     class Meta:
         model = TutorSubmissionAssignment
-        fields = ('assignment_id', 'feedback', 'submission', 'is_done',)
+        fields = ('pk', 'feedback', 'submission', 'is_done',)
 
 
 class SubscriptionSerializer(DynamicFieldsModelSerializer):
@@ -300,7 +291,7 @@ class SubscriptionSerializer(DynamicFieldsModelSerializer):
     class Meta:
         model = GeneralTaskSubscription
         fields = (
-            'subscription_id',
+            'pk',
             'owner',
             'query_type',
             'query_key',
diff --git a/core/tests/test_feedback.py b/core/tests/test_feedback.py
index 75c22ab9b42d88106894b0eb74c64750589dcce1..2512b6bc98659484f8f16e120bdf7863551c026d 100644
--- a/core/tests/test_feedback.py
+++ b/core/tests/test_feedback.py
@@ -69,8 +69,8 @@ class FeedbackRetrieveTestCase(APITestCase):
         self.assertIn(2, self.data['feedback_lines'])
 
     def test_if_feedback_contains_final(self):
-        self.assertIn('isFinal', self.data)
-        self.assertIsNotNone(self.data['isFinal'])
+        self.assertIn('is_final', self.data)
+        self.assertIsNotNone(self.data['is_final'])
 
     def test_if_comment_contains_text(self):
         self.assertIn('text', self.data['feedback_lines'][1][0])
@@ -82,15 +82,15 @@ class FeedbackRetrieveTestCase(APITestCase):
         self.assertIsNotNone(self.data['feedback_lines'][1][0]['created'])
 
     def test_if_comment_has_tutor(self):
-        self.assertIn('ofTutor', self.data['feedback_lines'][1][0])
+        self.assertIn('of_tutor', self.data['feedback_lines'][1][0])
         self.assertEqual(
             self.tutor.username,
-            self.data['feedback_lines'][1][0]['ofTutor'])
+            self.data['feedback_lines'][1][0]['of_tutor'])
 
     def test_if_comment_has_final(self):
-        self.assertIn('isFinalComment', self.data['feedback_lines'][1][0])
+        self.assertIn('is_final', self.data['feedback_lines'][1][0])
         self.assertIsNotNone(
-            self.data['feedback_lines'][1][0]['isFinalComment'])
+            self.data['feedback_lines'][1][0]['is_final'])
 
 
 class FeedbackCreateTestCase(APITestCase):
@@ -123,8 +123,8 @@ class FeedbackCreateTestCase(APITestCase):
         # to the max Score for this submission
         data = {
             'score': 10,
-            'isFinal': False,
-            'assignment_id': self.assignment.assignment_id
+            'is_final': False,
+            'assignment_pk': self.assignment.pk
 
         }
         self.assertEqual(Feedback.objects.count(), 0)
@@ -135,8 +135,8 @@ class FeedbackCreateTestCase(APITestCase):
     def test_cannot_create_feedback_with_score_higher_than_max(self):
         data = {
             'score': 101,
-            'isFinal': False,
-            'assignment_id': self.assignment.assignment_id
+            'is_final': False,
+            'assignment_pk': self.assignment.pk
         }
         self.assertEqual(Feedback.objects.count(), 0)
         response = self.client.post(self.url, data, format='json')
@@ -146,8 +146,8 @@ class FeedbackCreateTestCase(APITestCase):
     def test_cannot_create_feedback_with_score_less_than_zero(self):
         data = {
             'score': -1,
-            'isFinal': False,
-            'assignment_id': self.assignment.assignment_id
+            'is_final': False,
+            'assignment_pk': self.assignment.pk
         }
         self.assertEqual(Feedback.objects.count(), 0)
         response = self.client.post(self.url, data, format='json')
@@ -157,8 +157,8 @@ class FeedbackCreateTestCase(APITestCase):
     def test_check_score_is_set_accordingly(self):
         data = {
             'score': 5,
-            'isFinal': False,
-            'assignment_id': self.assignment.assignment_id
+            'is_final': False,
+            'assignment_pk': self.assignment.pk
         }
         self.client.post(self.url, data, format='json')
         object_score = self.sub.feedback.score
@@ -167,8 +167,8 @@ class FeedbackCreateTestCase(APITestCase):
     def test_can_create_feedback_with_comment(self):
         data = {
             'score': 0,
-            'isFinal': False,
-            'assignment_id': self.assignment.assignment_id,
+            'is_final': False,
+            'assignment_pk': self.assignment.pk,
             'feedback_lines': {
                 '5': {
                     'text': 'Nice meth!'
@@ -183,8 +183,8 @@ class FeedbackCreateTestCase(APITestCase):
     def test_feedback_comment_is_created_correctly(self):
         data = {
             'score': 0,
-            'isFinal': False,
-            'assignment_id': self.assignment.assignment_id,
+            'is_final': False,
+            'assignment_pk': self.assignment.pk,
             'feedback_lines': {
                 '5': {
                     'text': 'Nice meth!'
@@ -202,8 +202,8 @@ class FeedbackCreateTestCase(APITestCase):
     def test_can_create_multiple_feedback_comments(self):
         data = {
             'score': 0,
-            'isFinal': False,
-            'assignment_id': self.assignment.assignment_id,
+            'is_final': False,
+            'assignment_pk': self.assignment.pk,
             'feedback_lines': {
                 '5': {
                     'text': 'Nice meth!'
diff --git a/core/tests/test_student_page.py b/core/tests/test_student_page.py
index a4a86a37e9f77c853b4555bedc061634002f4178..52d09daf1e1e2a6e931670c2a63951f42959b9e8 100644
--- a/core/tests/test_student_page.py
+++ b/core/tests/test_student_page.py
@@ -104,12 +104,12 @@ class StudentPageTests(APITestCase):
 
     def test_a_student_submissions_contains_type_id(self):
         self.assertEqual(
-            self.submission_list_first_entry['type']['id'],
+            self.submission_list_first_entry['type']['pk'],
             self.student_info.submissions.first().type.id)
 
     def test_submission_data_contains_full_score(self):
         self.assertEqual(
-            self.submission_list_first_entry['type']['fullScore'],
+            self.submission_list_first_entry['type']['full_score'],
             self.student_info.submissions.first().type.full_score)
 
     def test_submission_data_contains_feedback_score(self):
@@ -182,12 +182,12 @@ class StudentSelfSubmissionsTests(APITestCase):
 
     def test_a_student_submissions_contains_type_id(self):
         self.assertEqual(
-            self.submission_list_first_entry['type']['id'],
-            self.student_info.submissions.first().type.id)
+            self.submission_list_first_entry['type']['pk'],
+            self.student_info.submissions.first().type.pk)
 
     def test_submission_data_contains_full_score(self):
         self.assertEqual(
-            self.submission_list_first_entry['type']['fullScore'],
+            self.submission_list_first_entry['type']['full_score'],
             self.student_info.submissions.first().type.full_score)
 
     def test_submission_data_contains_description(self):
@@ -202,7 +202,7 @@ class StudentSelfSubmissionsTests(APITestCase):
 
     def test_submission_data_contains_final_status(self):
         self.assertEqual(
-            self.submission_list_first_entry['feedback']['isFinal'],
+            self.submission_list_first_entry['feedback']['is_final'],
             self.student_info.submissions.first().feedback.is_final)
 
     def test_submission_data_contains_feedback_score(self):
diff --git a/core/tests/test_student_reviewer_viewset.py b/core/tests/test_student_reviewer_viewset.py
index ddc317546e63aad954f6dea37a24a93c9b53a71f..9d22d7c1f24e4bc8fec393f2bdb6eb06d187ede7 100644
--- a/core/tests/test_student_reviewer_viewset.py
+++ b/core/tests/test_student_reviewer_viewset.py
@@ -76,4 +76,4 @@ class StudentPageTests(APITestCase):
     def test_submissions_full_score_is_included(self):
         print(self.response.data[0]['submissions'][0])
         self.assertEqual(self.student.submissions.first().type.full_score,
-                         self.response.data[0]['submissions'][0]['fullScore'])
+                         self.response.data[0]['submissions'][0]['full_score'])
diff --git a/core/tests/test_submissiontypeview.py b/core/tests/test_submissiontypeview.py
index 713cdb9072c47c370c6d1bc12a3ba3c2e239fc56..cbb68f07bf4812ba9d9004dae176922ce5a9b58e 100644
--- a/core/tests/test_submissiontypeview.py
+++ b/core/tests/test_submissiontypeview.py
@@ -37,7 +37,7 @@ class SubmissionTypeViewTestList(APITestCase):
         self.assertEqual('Hard question', self.response.data[0]['name'])
 
     def test_get_full_score(self):
-        self.assertEqual(20, self.response.data[0]['fullScore'])
+        self.assertEqual(20, self.response.data[0]['full_score'])
 
 
 class SubmissionTypeViewTestRetrieve(APITestCase):
@@ -62,13 +62,13 @@ class SubmissionTypeViewTestRetrieve(APITestCase):
         self.assertEqual(self.response.status_code, status.HTTP_200_OK)
 
     def test_get_id(self):
-        self.assertEqual(self.pk, self.response.data['id'])
+        self.assertEqual(self.pk, self.response.data['pk'])
 
     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'])
+        self.assertEqual(20, self.response.data['full_score'])
 
     def test_get_descritpion(self):
         self.assertEqual('Whatever', self.response.data['description'])
diff --git a/core/tests/test_subscription_assignment_service.py b/core/tests/test_subscription_assignment_service.py
index f11b23194a803973949878ed3f33b66c6d8e24fb..01d418e0b075fa7a14e2e100acc473d7b9a027af 100644
--- a/core/tests/test_subscription_assignment_service.py
+++ b/core/tests/test_subscription_assignment_service.py
@@ -160,15 +160,15 @@ class TestApiEndpoints(APITestCase):
 
         response_subs = client.post(
             '/api/subscription/', {'query_type': 'random'})
-        subscription_id = response_subs.data['subscription_id']
-        assignment_id = response_subs.data['assignments'][0]['assignment_id']
+        subscription_id = response_subs.data['pk']
+        assignment_pk = response_subs.data['assignments'][0]['pk']
 
         response = client.get(
             f'/api/subscription/{subscription_id}/assignments/current/')
 
         self.assertEqual(1, len(response_subs.data['assignments']))
-        self.assertEqual(assignment_id,
-                         response.data['assignment_id'])
+        self.assertEqual(assignment_pk,
+                         response.data['pk'])
 
         response_next = client.get(
             f'/api/subscription/{subscription_id}/assignments/next/')
@@ -176,7 +176,7 @@ class TestApiEndpoints(APITestCase):
             client.get(f'/api/subscription/{subscription_id}/')
 
         self.assertEqual(2, len(response_detail_subs.data['assignments']))
-        self.assertNotEqual(assignment_id, response_next.data['assignment_id'])
+        self.assertNotEqual(assignment_pk, response_next.data['pk'])
 
     def test_subscription_can_assign_to_student(self):
         client = APIClient()
@@ -207,11 +207,11 @@ class TestApiEndpoints(APITestCase):
 
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
-        assignment_id = response.data['assignments'][0]['assignment_id']
+        assignment_pk = response.data['assignments'][0]['pk']
         response = client.post(
             f'/api/feedback/', {
                 "score": 23,
-                "assignment_id": assignment_id,
+                "assignment_pk": assignment_pk,
                 "feedback_lines": {
                     2: {"text": "< some string >"},
                     3: {"text": "< some string >"}
@@ -232,7 +232,7 @@ class TestApiEndpoints(APITestCase):
 
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
-        subscription_id = response.data['subscription_id']
+        subscription_id = response.data['pk']
         response = client.get(
             f'/api/subscription/{subscription_id}/assignments/current/')
 
@@ -240,7 +240,7 @@ class TestApiEndpoints(APITestCase):
         submission_id_in_database = models.Feedback.objects.filter(
             is_final=False).first().of_submission.submission_id
         submission_id_in_response = \
-            response.data['submission']['submission_id']
+            response.data['submission']['pk']
 
         self.assertEqual(
             str(submission_id_in_database),
diff --git a/core/views.py b/core/views.py
index 165498d777c6d380f8643bdf9abd64f4defdef9c..34f785ade209238416074da9ab7f7fea20cdc1c6 100644
--- a/core/views.py
+++ b/core/views.py
@@ -163,10 +163,17 @@ class SubscriptionApiViewSet(
         serializer.is_valid(raise_exception=True)
         subscription = serializer.save()
 
-        if subscription.query_type == GeneralTaskSubscription.STUDENT_QUERY:
-            subscription.reserve_all_assignments_for_a_student()
-        else:
-            subscription.get_oldest_unfinished_assignment()
+        try:
+            if subscription.query_type == \
+                    GeneralTaskSubscription.STUDENT_QUERY:
+                subscription.reserve_all_assignments_for_a_student()
+            else:
+                subscription.get_oldest_unfinished_assignment()
+        except models.SubscriptionEnded as err:
+            return Response(
+                {'Error': 'This subscription has no available submissions'},
+                status.HTTP_410_GONE
+            )
 
         headers = self.get_success_headers(serializer.data)
         return Response(serializer.data,
diff --git a/frontend/package.json b/frontend/package.json
index 03ef1b1cd9bf1edda3018db02a2008091e8256ac..fa17670cfdb2314460bb91225986a1cb35df00d2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -18,9 +18,11 @@
     "material-design-icons": "^3.0.1",
     "v-clipboard": "^1.0.4",
     "vue": "^2.5.2",
+    "vue-notification": "^1.3.6",
     "vue-router": "^3.0.1",
     "vuetify": "^0.17.3",
-    "vuex": "^3.0.1"
+    "vuex": "^3.0.1",
+    "vuex-persistedstate": "^2.4.2"
   },
   "devDependencies": {
     "autoprefixer": "^7.1.2",
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 0e0910b3cbff0ccb4a7884cf6d4abd3832648c76..accd086237c26b89aece8e056ca52f046f447998 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,15 +1,93 @@
 <template>
   <div id="app">
     <v-app>
+      <notifications/>
       <router-view/>
+      <v-dialog
+        persistent
+        width="fit-content"
+        v-model="logoutDialog"
+      >
+        <v-card>
+          <v-card-title class="headline">
+            You'll be logged out!
+          </v-card-title>
+          <v-card-text>
+            Due to inactivity you'll be logged out in a couple of moments.<br/>
+            Any unsaved work will be lost.
+            Click Continue to stay logged in.
+          </v-card-text>
+          <v-card-actions>
+            <v-btn flat color="grey lighten-0"
+              @click="logout"
+            >Logout now</v-btn>
+            <v-spacer/>
+            <v-btn flat color="blue darken-2"
+              @click="continueWork"
+            >Continue</v-btn>
+          </v-card-actions>
+        </v-card>
+      </v-dialog>
     </v-app>
   </div>
 </template>
 
 <script>
+  import {mapState} from 'vuex'
   export default {
     name: 'app',
-    components: {
+    data () {
+      return {
+        timer: 0,
+        logoutDialog: false
+      }
+    },
+    computed: {
+      ...mapState([
+        'lastAppInteraction'
+      ]),
+      ...mapState({
+        tokenCreationTime: state => state.authentication.tokenCreationTime,
+        refreshingToken: state => state.authentication.refreshingToken,
+        jwtTimeDelta: state => state.authentication.jwtTimeDelta
+      })
+    },
+    methods: {
+      logout () {
+        this.logoutDialog = false
+        this.$store.dispatch('logout')
+      },
+      continueWork () {
+        this.$store.dispatch('refreshJWT')
+        this.logoutDialog = false
+      }
+    },
+    watch: {
+      lastAppInteraction: function (val) {
+        const timeSinceLastRefresh = Date.now() - this.tokenCreationTime
+        const timeDelta = this.jwtTimeDelta
+        // refresh jwt if it's older than 20% of his maximum age
+        if (timeDelta > 0 && timeSinceLastRefresh > timeDelta * 0.2 &&
+          !this.refreshingToken) {
+          this.$store.dispatch('refreshJWT')
+        }
+      }
+    },
+    mounted () {
+      const oneAndHalfMinute = 90 * 1e3
+      this.timer = setInterval(() => {
+        if (this.$route.path !== '/') {
+          if (Date.now() > this.tokenCreationTime + this.jwtTimeDelta) {
+            this.logoutDialog = false
+            this.$store.dispatch('logout', "You've been logged out due to inactivity.")
+          } else if (Date.now() + oneAndHalfMinute > this.tokenCreationTime + this.jwtTimeDelta) {
+            this.logoutDialog = true
+          }
+        }
+      }, 5 * 1e3)
+    },
+    beforeDestroy () {
+      clearInterval(this.timer)
     }
   }
 </script>
diff --git a/frontend/src/api.js b/frontend/src/api.js
new file mode 100644
index 0000000000000000000000000000000000000000..90ecdd6acc45f06ee2ef0d8ac48bfd643de4e6b6
--- /dev/null
+++ b/frontend/src/api.js
@@ -0,0 +1,86 @@
+import axios from 'axios'
+
+// import store from '@/store/store'
+
+let ax = axios.create({
+  baseURL: 'http://localhost:8000/',
+  headers: {'Authorization': 'JWT ' + sessionStorage.getItem('token')}
+})
+
+export async function fetchJWT (credentials) {
+  const token = (await ax.post('/api-token-auth/', credentials)).data.token
+  ax.defaults.headers['Authorization'] = `JWT ${token}`
+  return token
+}
+
+export async function refreshJWT (token) {
+  const newToken = (await ax.post('/api-token-refresh/', {token})).data.token
+  ax.defaults.headers['Authorization'] = `JWT ${newToken}`
+  return token
+}
+
+export async function fetchJWTTimeDelta () {
+  return (await ax.get('/api/jwt-time-delta/')).data.timeDelta
+}
+
+export async function fetchUserRole () {
+  return (await ax.get('/api/user-role/')).data.role
+}
+
+export async function fetchStudentSelfData () {
+  return (await ax.get('/api/student-page/')).data
+}
+
+export async function fetchStudentSubmissions () {
+  return (await ax.get('/api/student-submissions/')).data
+}
+
+export async function fetchSubscriptions () {
+  return (await ax.get('/api/subscription/')).data
+}
+
+export async function fetchSubscription (subscriptionPk) {
+  return (await ax.get(`/api/subscription/${subscriptionPk}`)).data
+}
+
+export async function subscribeTo (type, key, stage) {
+  let data = {
+    query_type: type
+  }
+
+  if (key) {
+    data.query_key = key
+  }
+  if (stage) {
+    data.feedback_stage = stage
+  }
+
+  return (await ax.post('/api/subscription/', data)).data
+}
+
+export async function fetchCurrentAssignment (subscriptionPk) {
+  return (await ax.get(`/api/subscription/${subscriptionPk}/assignments/current/`)).data
+}
+
+export async function fetchNextAssignment (subscriptionPk) {
+  return (await ax.get(`/api/subscription/${subscriptionPk}/assignments/next/`)).data
+}
+
+export async function submitFeedbackForAssignment (feedback, assignmentPk) {
+  const data = {
+    ...feedback,
+    assignment_pk: assignmentPk
+  }
+
+  return (await ax.post('/api/feedback/', data)).data
+}
+
+export async function fetchSubmissionTypes (fields = []) {
+  let url = '/api/submissiontype/'
+  if (fields.length > 0) {
+    url += '?fields=pk,' + fields
+  }
+  return (await ax.get(url)).data
+}
+
+export default ax
diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue
index 3f18a9866469951978cbc9420c4b34edfd9c3184..851b49a050abd129fbdd2d15e53accaec4b82107 100644
--- a/frontend/src/components/BaseLayout.vue
+++ b/frontend/src/components/BaseLayout.vue
@@ -7,7 +7,7 @@
       permanent
       :mini-variant="mini"
     >
-      <v-toolbar flat>
+      <v-toolbar>
         <v-list>
           <v-list-tile>
             <v-list-tile-action v-if="mini">
@@ -15,10 +15,12 @@
                 <v-icon>chevron_right</v-icon>
               </v-btn>
             </v-list-tile-action>
-            <v-list-tile-content class="title">
+            <v-list-tile-content
+              class="title"
+            >
               <slot name="header"></slot>
             </v-list-tile-content>
-            <v-list-tile-action>
+            <v-list-tile-action v-if="!mini">
               <v-btn icon @click.native.stop="mini = !mini">
                 <v-icon>chevron_left</v-icon>
               </v-btn>
@@ -54,7 +56,8 @@
 </template>
 
 <script>
-  import { mapActions, mapGetters, mapState } from 'vuex'
+  import { mapGetters, mapState } from 'vuex'
+
   export default {
     name: 'base-layout',
     data () {
@@ -66,15 +69,15 @@
       ...mapGetters([
         'gradySpeak'
       ]),
-      ...mapState([
-        'username',
-        'userRole'
-      ])
+      ...mapState({
+        username: state => state.authentication.username,
+        userRole: state => state.authentication.userRole
+      })
     },
     methods: {
-      ...mapActions([
-        'logout'
-      ])
+      logout () {
+        this.$store.dispatch('logout')
+      }
     },
     watch: {
       mini: function () {
@@ -92,4 +95,8 @@
   .grady-toolbar {
     font-weight: bold;
   }
+
+  .title {
+    color: gray;
+  }
 </style>
diff --git a/frontend/src/components/SubmissionType.vue b/frontend/src/components/SubmissionType.vue
index 6cf35fce198a4f61e0369575a7669f5c7990767b..c5bd057cb08fb08f17f19f919b4519953cab9d92 100644
--- a/frontend/src/components/SubmissionType.vue
+++ b/frontend/src/components/SubmissionType.vue
@@ -1,19 +1,31 @@
 <template>
   <v-container>
-    <h2 class="mb-2">{{ name }} - Full score: {{ fullScore }}</h2>
-    <v-expansion-panel expand>
-      <v-expansion-panel-content
-      v-for="(item, i) in typeItems"
-      :key="i"
-      :value="expandedByDefault[item.title]">
-      <div slot="header">{{ item.title }}</div>
-        <v-card color="grey lighten-4">
-          <v-card-text>
-            {{ item.text }}
-          </v-card-text>
-        </v-card>
-      </v-expansion-panel-content>
-    </v-expansion-panel>
+    <v-layout column>
+      <span class="title mb-2">{{ name }} - Full score: {{ full_score }}</span>
+      <v-expansion-panel expand>
+        <v-expansion-panel-content
+          v-for="(item, i) in typeItems"
+          :key="i"
+          :value="expandedByDefault[item.title]">
+          <div slot="header">{{ item.title }}</div>
+          <v-card
+            v-if="item.title === 'Description'"
+            color="grey lighten-4">
+            <v-card-text class="ml-2">
+            <span
+              v-html="item.text"
+            ></span>
+            </v-card-text>
+          </v-card>
+          <v-flex v-else-if="item.title === 'Solution'">
+          <pre
+            class="prettyprint elevation-2 solution-code"
+            :class="language"
+          >{{item.text}}</pre>
+          </v-flex>
+        </v-expansion-panel-content>
+      </v-expansion-panel>
+    </v-layout>
   </v-container>
 </template>
 
@@ -34,10 +46,14 @@
         type: String,
         required: true
       },
-      fullScore: {
+      full_score: {
         type: Number,
         required: true
       },
+      language: {
+        type: String,
+        default: 'lang-c'
+      },
       reverse: {
         type: Boolean,
         default: false
@@ -70,7 +86,17 @@
           return items
         }
       }
+    },
+    mounted () {
+      window.PR.prettyPrint()
     }
   }
 </script>
 
+<style scoped>
+  .solution-code {
+    border-width: 0px;
+    white-space: pre-wrap;
+  }
+</style>
+
diff --git a/frontend/src/components/student/SubmissionList.vue b/frontend/src/components/student/SubmissionList.vue
index 451404f910874998df123c66d144528d2e11c4fb..916563d5d09bdbbf2a7dae9fc1fc0add52e5ec48 100644
--- a/frontend/src/components/student/SubmissionList.vue
+++ b/frontend/src/components/student/SubmissionList.vue
@@ -9,8 +9,8 @@
       <template slot="items" slot-scope="props">
         <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="`/student/submission/${props.item.type.id}`" color="orange lighten-2"><v-icon>chevron_right</v-icon></v-btn></td>
+        <td class="text-xs-right">{{ props.item.type.full_score }}</td>
+        <td class="text-xs-right"><v-btn :to="`/student/submission/${props.item.type.pk}`" color="orange lighten-2"><v-icon>chevron_right</v-icon></v-btn></td>
       </template>
     </v-data-table>
     <v-alert color="info" value="true">
@@ -38,7 +38,7 @@
           },
           {
             text: 'Maximum Score',
-            value: 'type.fullScore'
+            value: 'type.full_score'
           }
         ]
       }
@@ -54,10 +54,15 @@
         return this.submissions.map(a => a.feedback.score).reduce((a, b) => a + b)
       },
       sumFullScore () {
-        return this.submissions.map(a => a.type.fullScore).reduce((a, b) => a + b)
+        return this.submissions.map(a => a.type.full_score).reduce((a, b) => a + b)
       },
       pointRatio () {
         return ((this.sumScore / this.sumFullScore) * 100).toFixed(2)
+      },
+      students () {
+        this.items.forEach(item => {
+
+        })
       }
     }
   }
diff --git a/frontend/src/components/submission_notes/AnnotatedSubmission.vue b/frontend/src/components/submission_notes/AnnotatedSubmission.vue
deleted file mode 100644
index a3fdb504bf11319d0313c9858c57ec129c7231db..0000000000000000000000000000000000000000
--- a/frontend/src/components/submission_notes/AnnotatedSubmission.vue
+++ /dev/null
@@ -1,127 +0,0 @@
-<template>
-  <v-container>
-    <annotated-submission-top-toolbar
-      v-if="isTutor || isReviewer"
-      class="mb-1 elevation-1"
-      :submission="rawSubmission"
-    />
-    <table class="elevation-1">
-      <tr v-for="(code, index) in submission" :key="index">
-        <td class="line-number-cell">
-          <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>
-          <feedback-comment
-            v-if="feedback[index] && !showEditorOnLine[index]"
-            @click.native="toggleEditorOnLine(index)">{{ feedback[index] }}
-          </feedback-comment>
-          <comment-form
-            v-if="showEditorOnLine[index] && editable"
-            @collapseFeedbackForm="showEditorOnLine[index] = false"
-            :feedback="feedback[index]"
-            :index="index">
-          </comment-form>
-        </td>
-      </tr>
-    </table>
-    <annotated-submission-bottom-toolbar
-      v-if="isTutor || isReviewer"
-      class="mt-1 elevation-1"
-    />
-  </v-container>
-</template>
-
-
-<script>
-  import { mapGetters } from 'vuex'
-  import CommentForm from '@/components/submission_notes/FeedbackForm.vue'
-  import FeedbackComment from '@/components/submission_notes/FeedbackComment.vue'
-  import AnnotatedSubmissionTopToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar'
-  import AnnotatedSubmissionBottomToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar'
-
-  export default {
-    components: {
-      AnnotatedSubmissionBottomToolbar,
-      AnnotatedSubmissionTopToolbar,
-      FeedbackComment,
-      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
-      }
-    },
-    data: function () {
-      return {
-        showEditorOnLine: {}
-      }
-    },
-    computed: {
-      submission () {
-        return this.rawSubmission.split('\n').reduce((acc, cur, index) => {
-          acc[index + 1] = cur
-          return acc
-        }, {})
-      },
-      ...mapGetters([
-        'isStudent',
-        'isTutor',
-        'isReviewer'
-      ])
-    },
-    methods: {
-      toggleEditorOnLine (lineIndex) {
-        this.$set(this.showEditorOnLine, lineIndex, !this.showEditorOnLine[lineIndex])
-      }
-    },
-    mounted () {
-      window.PR.prettyPrint()
-    }
-  }
-</script>
-
-
-<style scoped>
-
-  table {
-    table-layout: auto;
-    border-collapse: collapse;
-    width: 100%;
-  }
-
-
-  .line-number-cell {
-    vertical-align: top;
-  }
-
-  pre.prettyprint {
-    padding: 0;
-    border: 0;
-  }
-
-  code {
-    width: 100%;
-    box-shadow: None;
-  }
-
-
-  .line-number-btn {
-    height: fit-content;
-    min-width: fit-content;
-    margin: 0;
-  }
-</style>
diff --git a/frontend/src/components/submission_notes/FeedbackComment.vue b/frontend/src/components/submission_notes/FeedbackComment.vue
deleted file mode 100644
index 8af24d6e8b8d011967f105b11f2e69d8f15511ed..0000000000000000000000000000000000000000
--- a/frontend/src/components/submission_notes/FeedbackComment.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<template>
-  <div class="dialogbox">
-    <div class="body">
-      <span class="tip tip-up"></span>
-      <div class="message">
-        <slot></slot>
-      </div>
-    </div>
-  </div>
-</template>
-
-
-<script>
-  export default {
-    name: 'feedback-comment'
-  }
-</script>
-
-
-<style scoped>
-  .tip {
-    width: 0px;
-    height: 0px;
-    position: absolute;
-    background: transparent;
-    border: 10px solid #3D8FC1;
-  }
-
-  .tip-up {
-    top: -22px; /* Same as body margin top + border */
-    left: 10px;
-    border-right-color: transparent;
-    border-left-color: transparent;
-    border-top-color: transparent;
-  }
-
-  .dialogbox .body {
-    position: relative;
-    height: auto;
-    margin: 20px 10px 10px 10px;
-    padding: 5px;
-    background-color: #F3F3F3;
-    border-radius: 0px;
-    border: 2px solid #3D8FC1;
-  }
-
-  .body .message {
-    min-height: 30px;
-    border-radius: 3px;
-    font-size: 14px;
-    line-height: 1.5;
-  }
-</style>
diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a7ba1a5fdc5f438f552d7aabe1ceda6c1ebdd981
--- /dev/null
+++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue
@@ -0,0 +1,152 @@
+<template>
+  <v-container>
+    <base-annotated-submission>
+      <annotated-submission-top-toolbar
+        class="mb-1 elevation-1"
+        slot="header"
+      />
+      <template slot="table-content">
+        <tr v-for="(code, lineNo) in submission" :key="lineNo">
+          <submission-line
+            :code="code"
+            :line-no="lineNo"
+            @toggleEditor="toggleEditorOnLine(lineNo)"
+          >
+            <template>
+              <feedback-comment
+                v-if="origFeedback[lineNo]"
+                v-for="(comment, index) in origFeedback[lineNo]"
+                v-bind="comment"
+                :key="index"
+                @click.native="toggleEditorOnLine(lineNo, comment)"
+              />
+            </template>
+            <feedback-comment
+              v-if="updatedFeedback[lineNo]"
+              borderColor="orange"
+              v-bind="updatedFeedback[lineNo]"
+              :deletable="true"
+              @click.native="toggleEditorOnLine(lineNo, updatedFeedback[lineNo])"
+              @delete="deleteFeedback(lineNo)"
+            />
+            <comment-form
+              v-if="showEditorOnLine[lineNo]"
+              :feedback="selectedComment[lineNo].text"
+              :lineNo="lineNo"
+              @collapseFeedbackForm="toggleEditorOnLine(lineNo)"
+              @submitFeedback=""
+            >
+            </comment-form>
+          </submission-line>
+        </tr>
+      </template>
+      <annotated-submission-bottom-toolbar
+        class="mt-1 elevation-1"
+        slot="footer"
+        :loading="loading"
+        :fullScore="submissionObj['full_score']"
+        @submitFeedback="submitFeedback"
+      />
+    </base-annotated-submission>
+  </v-container>
+</template>
+
+
+<script>
+  import { mapState, mapGetters } from 'vuex'
+  import CommentForm from '@/components/submission_notes/base/CommentForm.vue'
+  import FeedbackComment from '@/components/submission_notes/base/FeedbackComment.vue'
+  import AnnotatedSubmissionTopToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar'
+  import AnnotatedSubmissionBottomToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar'
+  import BaseAnnotatedSubmission from '@/components/submission_notes/base/BaseAnnotatedSubmission'
+  import SubmissionLine from '@/components/submission_notes/base/SubmissionLine'
+
+  export default {
+    components: {
+      SubmissionLine,
+      BaseAnnotatedSubmission,
+      AnnotatedSubmissionBottomToolbar,
+      AnnotatedSubmissionTopToolbar,
+      FeedbackComment,
+      CommentForm},
+    name: 'submission-correction',
+    data () {
+      return {
+        loading: false
+      }
+    },
+    props: {
+      assignment: {
+        type: Object
+      },
+      submissionWithoutAssignment: {
+        type: Object
+      },
+      feedback: {
+        type: Object
+      }
+    },
+    computed: {
+      ...mapState({
+        showEditorOnLine: state => state.submissionNotes.ui.showEditorOnLine,
+        selectedComment: state => state.submissionNotes.ui.selectedCommentOnLine,
+        origFeedback: state => state.submissionNotes.orig.feedbackLines,
+        updatedFeedback: state => state.submissionNotes.updated.feedbackLines
+      }),
+      ...mapGetters([
+        'isStudent',
+        'isTutor',
+        'isReviewer',
+        'getSubmission',
+        'getFeedback',
+        'getSubmissionType'
+      ]),
+      submission () {
+        return this.$store.getters['submissionNotes/submission']
+      },
+      submissionObj () {
+        return this.assignment ? this.assignment.submission : this.submissionWithoutAssignment
+      },
+      feedbackObj () {
+        return this.assignment ? this.assignment.feedback : this.feedback
+      }
+    },
+    methods: {
+      deleteFeedback (lineNo) {
+        this.$store.commit('submissionNotes/DELETE_FEEDBACK_LINE', lineNo)
+      },
+      toggleEditorOnLine (lineNo, comment = '') {
+        this.$store.commit('submissionNotes/TOGGLE_EDITOR_ON_LINE', {lineNo, comment})
+      },
+      submitFeedback () {
+        this.loading = true
+        this.$store.dispatch('submissionNotes/submitFeedback', this.assignment).then(() => {
+          this.$store.commit('submissionNotes/RESET_STATE')
+          this.$emit('feedbackCreated')
+        }).catch(err => {
+          this.$notify({
+            title: 'Feedback creation Error!',
+            text: err.message,
+            type: 'error'
+          })
+        }).finally(() => {
+          this.loading = false
+        })
+      },
+      init () {
+        this.$store.commit('submissionNotes/RESET_STATE')
+        this.$store.commit('submissionNotes/SET_RAW_SUBMISSION', this.submissionObj.text)
+        this.$store.commit('submissionNotes/SET_ORIG_FEEDBACK', this.feedbackObj)
+        window.PR.prettyPrint()
+      }
+    },
+    mounted () {
+      this.init()
+    }
+  }
+</script>
+
+
+<style scoped>
+
+</style>
diff --git a/frontend/src/components/submission_notes/base/BaseAnnotatedSubmission.vue b/frontend/src/components/submission_notes/base/BaseAnnotatedSubmission.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c79b1e55847613c52c3a2967e7831e65a5b92943
--- /dev/null
+++ b/frontend/src/components/submission_notes/base/BaseAnnotatedSubmission.vue
@@ -0,0 +1,23 @@
+<template>
+  <div>
+    <slot name="header"/>
+    <table class="submission-table elevation-1">
+      <slot name="table-content"/>
+    </table>
+    <slot name="footer"/>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'base-annotated-submission'
+  }
+</script>
+
+<style scoped>
+  .submission-table {
+    table-layout: auto;
+    border-collapse: collapse;
+    width: 100%;
+  }
+</style>
diff --git a/frontend/src/components/submission_notes/FeedbackForm.vue b/frontend/src/components/submission_notes/base/CommentForm.vue
similarity index 78%
rename from frontend/src/components/submission_notes/FeedbackForm.vue
rename to frontend/src/components/submission_notes/base/CommentForm.vue
index 403d001547cdbb3476ab9402bb90f92b6602f4d7..73c3aff18fda4c410253709752098f777834f47a 100644
--- a/frontend/src/components/submission_notes/FeedbackForm.vue
+++ b/frontend/src/components/submission_notes/base/CommentForm.vue
@@ -23,8 +23,14 @@
   export default {
     name: 'comment-form',
     props: {
-      feedback: String,
-      index: String
+      feedback: {
+        type: String,
+        default: ''
+      },
+      lineNo: {
+        type: String,
+        required: true
+      }
     },
     data () {
       return {
@@ -41,9 +47,11 @@
         this.$emit('collapseFeedbackForm')
       },
       submitFeedback () {
-        this.$store.dispatch('updateFeedback', {
-          lineIndex: this.index,
-          content: this.currentFeedback
+        this.$store.commit('submissionNotes/UPDATE_FEEDBACK_LINE', {
+          lineNo: this.lineNo,
+          comment: {
+            text: this.currentFeedback
+          }
         })
         this.collapseTextField()
       },
diff --git a/frontend/src/components/submission_notes/base/FeedbackComment.vue b/frontend/src/components/submission_notes/base/FeedbackComment.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c467023f210d27da8fb0274bec64a4d1da87afc9
--- /dev/null
+++ b/frontend/src/components/submission_notes/base/FeedbackComment.vue
@@ -0,0 +1,105 @@
+<template>
+  <div class="dialog-box">
+    <div class="body elevation-1" :style="{borderColor: borderColor}">
+      <span class="tip tip-up" :style="{borderBottomColor: borderColor}"></span>
+      <span v-if="of_tutor" class="of-tutor">Of tutor: {{of_tutor}}</span>
+      <span class="comment-created">{{parsedCreated}}</span>
+      <div class="message">{{text}}</div>
+      <v-btn
+        flat icon
+        class="delete-button"
+        v-if="deletable"
+        @click.stop="$emit('delete')"
+      ><v-icon color="grey darken-1">delete_forever</v-icon></v-btn>
+    </div>
+  </div>
+</template>
+
+
+<script>
+  export default {
+    name: 'feedback-comment',
+    props: {
+      text: {
+        type: String,
+        required: true
+      },
+      created: {
+        type: String,
+        required: false
+      },
+      of_tutor: {
+        type: String,
+        required: false
+      },
+      deletable: {
+        type: Boolean,
+        default: false
+      },
+      borderColor: {
+        type: String,
+        default: '#3D8FC1'
+      }
+    },
+    computed: {
+      parsedCreated () {
+        if (this.created) {
+          return new Date(this.created).toLocaleString()
+        } else {
+          return 'Just now'
+        }
+      }
+    }
+  }
+</script>
+
+
+<style scoped>
+  .tip {
+    width: 0px;
+    height: 0px;
+    position: absolute;
+    background: transparent;
+    border: 10px solid;
+  }
+  .tip-up {
+    top: -22px; /* Same as body margin top + border */
+    left: 10px;
+    border-right-color: transparent;
+    border-left-color: transparent;
+    border-top-color: transparent;
+  }
+  .dialog-box .body {
+    position: relative;
+    height: auto;
+    margin: 20px 10px 10px 10px;
+    padding: 5px;
+    background-color: #F3F3F3;
+    border-radius: 0px;
+    border: 2px solid;
+  }
+  .body .message {
+    min-height: 30px;
+    border-radius: 3px;
+    font-size: 14px;
+    line-height: 1.5;
+    white-space: pre-wrap;
+  }
+  .delete-button {
+    position: absolute;
+    bottom: -10px;
+    right: 0px;
+  }
+  .comment-created {
+    position: absolute;
+    font-size: 10px;
+    right: 4px;
+    top: -20px;
+  }
+  .of-tutor {
+    position: absolute;
+    font-size: 13px;
+    top: -20px;
+    left: 50px;
+  }
+</style>
diff --git a/frontend/src/components/submission_notes/base/SubmissionLine.vue b/frontend/src/components/submission_notes/base/SubmissionLine.vue
new file mode 100644
index 0000000000000000000000000000000000000000..78f5dd15dd212bf3fb4e066861bd687f819ab408
--- /dev/null
+++ b/frontend/src/components/submission_notes/base/SubmissionLine.vue
@@ -0,0 +1,73 @@
+<template>
+    <div>
+      <td class="line-number-cell">
+        <v-btn
+          block
+          class="line-number-btn"
+          @click="toggleEditor"
+        >
+          {{ lineNo }}
+        </v-btn>
+      </td>
+      <td class="code-cell-content pl-2">
+        <pre class="prettyprint" :class="codeLanguage">{{ code }}</pre>
+        <slot/>
+      </td>
+    </div>
+</template>
+
+<script>
+  export default {
+    name: 'submission-line',
+    props: {
+      lineNo: {
+        type: String,
+        required: true
+      },
+      code: {
+        type: String,
+        required: true
+      },
+      codeLanguage: {
+        type: String,
+        default: 'lang-c'
+      }
+    },
+    methods: {
+      toggleEditor () {
+        this.$emit('toggleEditor')
+      }
+    },
+    mounted () {
+      window.PR.prettyPrint()
+    }
+  }
+</script>
+
+<style scoped>
+  .line-number-cell {
+    vertical-align: top;
+  }
+
+  pre.prettyprint {
+    padding: 0;
+    border: 0;
+    white-space: pre-wrap;
+  }
+
+  .code-cell-content {
+    width: 100%;
+  }
+
+  code {
+    width: 100%;
+    box-shadow: None;
+  }
+
+
+  .line-number-btn {
+    height: fit-content;
+    min-width: 50px;
+    margin: 0;
+  }
+</style>
diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
index 887661e20f30dba96dd787bdd441f2a21225cec7..eb81ab6007d64456db8f9b5081112928da2347a8 100644
--- a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
+++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
@@ -15,8 +15,24 @@
       @input="validateScore"
       @change="validateScore"
     />
+    <span>&nbsp;/ {{fullScore}}</span>
+    <v-btn
+      outline round flat
+      @click="score = 0"
+      color="red lighten-1"
+      class="score-button">0</v-btn>
+    <v-btn
+      outline round flat
+      @click="score = fullScore"
+      color="blue darken-3"
+      class="score-button">{{fullScore}}</v-btn>
     <v-tooltip top>
-      <v-btn color="success" slot="activator">Submit<v-icon>chevron_right</v-icon></v-btn>
+      <v-btn
+        color="success"
+        slot="activator"
+        :loading="loading"
+        @click="submit"
+      >Submit<v-icon>chevron_right</v-icon></v-btn>
       <span>Submit and continue</span>
     </v-tooltip>
   </v-toolbar>
@@ -27,21 +43,48 @@
     name: 'annotated-submission-bottom-toolbar',
     data () {
       return {
-        score: 42,
-        mockMax: 50,
         scoreError: ''
-
+      }
+    },
+    props: {
+      fullScore: {
+        type: Number,
+        required: true
+      },
+      loading: {
+        type: Boolean,
+        required: true
+      }
+    },
+    computed: {
+      score: {
+        get: function () {
+          return this.$store.getters['submissionNotes/score']
+        },
+        set: function (score) {
+          this.$store.commit('submissionNotes/UPDATE_FEEDBACK_SCORE', Number(score))
+        }
       }
     },
     methods: {
+      emitScoreError (error, duration) {
+        this.scoreError = error
+        setTimeout(() => { this.scoreError = '' }, duration)
+      },
       validateScore () {
         if (this.score < 0) {
           this.score = 0
-          this.scoreError = 'Score must be 0 or greater.'
-        } else if (this.score > this.mockMax) {
-          this.score = this.mockMax
-          this.scoreError = `Score must be less or equal to ${this.mockMax}`
+          this.emitScoreError('Score must be 0 or greater.', 2000)
+        } else if (this.score > this.fullScore) {
+          this.score = this.fullScore
+          this.emitScoreError(`Score must be less or equal to ${this.fullScore}`, 2000)
+        } else {
+          return true
         }
+        return false
+      },
+      submit () {
+        this.$emit('submitFeedback')
       }
     }
   }
@@ -61,4 +104,7 @@
   .score-alert {
     max-height: 40px;
   }
+  .score-button {
+    min-width: 0px;
+  }
 </style>
diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar.vue
index 845c66087bfc655afda1515b039fe2d2b99b05c3..fc0ed47213b594e229235a9dbf16cf88c260ce71 100644
--- a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar.vue
+++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar.vue
@@ -9,31 +9,45 @@
       max-width="fit-content"
       v-model="helpDialog"
     >
-      <correction-help-card></correction-help-card>
+      <correction-help-card/>
     </v-dialog>
-    <v-spacer></v-spacer>
+    <span class="title">Student submission</span>
+    <v-spacer/>
     <v-tooltip top>
-      <v-btn icon slot="activator" v-clipboard="submission"><v-icon>content_copy</v-icon></v-btn>
-      <span>Copy to clipboard</span>
+      <v-btn
+        icon slot="activator"
+        @click="copyToClipboard"
+      ><v-icon>content_copy</v-icon></v-btn>
+      <span>{{copyMessage}}</span>
     </v-tooltip>
   </v-toolbar>
 </template>
 
 <script>
   import CorrectionHelpCard from '@/components/submission_notes/CorrectionHelpCard'
+  import { mapState } from 'vuex'
 
   export default {
     components: {CorrectionHelpCard},
     name: 'annotated-submission-top-toolbar',
-    props: {
-      submission: {
-        type: String,
-        required: true
-      }
-    },
     data () {
       return {
-        helpDialog: false
+        helpDialog: false,
+        copyMessage: 'Copy to clipboard'
+      }
+    },
+    computed: {
+      ...mapState({
+        submission: state => state.submissionNotes.orig.rawSubmission
+      })
+    },
+    methods: {
+      copyToClipboard () {
+        this.$clipboard(this.submission)
+        this.copyMessage = 'Copied!'
+        setTimeout(() => {
+          this.copyMessage = 'Copy to clipboard'
+        }, 2500)
       }
     }
   }
diff --git a/frontend/src/components/subscriptions/SubscriptionCreation.vue b/frontend/src/components/subscriptions/SubscriptionCreation.vue
new file mode 100644
index 0000000000000000000000000000000000000000..acd8d3c48d24345fd23074d4f4d1b964eae5ab7c
--- /dev/null
+++ b/frontend/src/components/subscriptions/SubscriptionCreation.vue
@@ -0,0 +1,93 @@
+<template>
+  <v-card>
+    <v-card-text>
+      <v-card-title>
+        <h3>Subscribe to {{ title }}</h3>
+      </v-card-title>
+      <v-select
+        v-if="keyItems"
+        v-model="key"
+        :items="keyItems"
+        :label="`Select your desired type of ${title}`"
+      />
+      <v-select
+        v-model="stage"
+        return-object
+        :items="possibleStages"
+        label="Select your desired feedback stage"
+      />
+      <v-card-actions>
+        <v-spacer/>
+        <v-btn
+          flat
+          @click="subscribe"
+          :loading="loading"
+        >Subscribe</v-btn>
+      </v-card-actions>
+    </v-card-text>
+  </v-card>
+</template>
+
+<script>
+  export default {
+    name: 'subscription-creation',
+    data () {
+      return {
+        key: '',
+        stage: '',
+        loading: false
+      }
+    },
+    props: {
+      title: {
+        type: String,
+        required: true
+      },
+      type: {
+        type: String,
+        required: true
+      },
+      keyItems: {
+        type: Array
+      }
+    },
+    computed: {
+      possibleStages () {
+        let stages = [
+          {
+            text: 'Initial Feedback',
+            type: 'feedback-creation'
+          },
+          {
+            text: 'Feedback validation',
+            type: 'feedback-validation'
+          }
+        ]
+        if (this.$store.getters.isReviewer) {
+          stages.push({
+            text: 'Conflict resolution',
+            type: 'feedback-conflict-resolution'
+          })
+        }
+        return stages
+      }
+    },
+    methods: {
+      subscribe () {
+        this.loading = true
+        console.log(this.stage.type)
+        this.$store.dispatch('subscribeTo', {
+          type: this.type,
+          key: this.key.text,
+          stage: this.stage.type
+        }).then(() => {
+          this.loading = false
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped>
+
+</style>
diff --git a/frontend/src/components/subscriptions/SubscriptionList.vue b/frontend/src/components/subscriptions/SubscriptionList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..55ee2cb14aeca24d99d3432a1c5991b540b1b23c
--- /dev/null
+++ b/frontend/src/components/subscriptions/SubscriptionList.vue
@@ -0,0 +1,139 @@
+<template>
+  <v-card>
+    <v-toolbar color="teal">
+      <v-toolbar-title>
+        Your subscriptions
+      </v-toolbar-title>
+    </v-toolbar>
+    <v-list>
+      <div v-for="item in subscriptionTypes" :key="item.type">
+        <v-list-tile>
+          <v-list-tile-content>
+            <v-list-tile-title>
+              {{ item.name }}
+            </v-list-tile-title>
+            <v-list-tile-sub-title>
+              {{ item.description }}
+            </v-list-tile-sub-title>
+          </v-list-tile-content>
+          <v-list-tile-action v-if="subscriptions[item.type].length > 0">
+            <v-btn icon @click="item.expanded = !item.expanded">
+              <v-icon v-if="item.expanded">keyboard_arrow_up</v-icon>
+              <v-icon v-else>keyboard_arrow_down</v-icon>
+            </v-btn>
+          </v-list-tile-action>
+          <v-list-tile-action
+            v-if="!item.hasOwnProperty('permission') || item.permission()"
+          >
+            <v-menu
+              offset-x
+              :min-width="500"
+              :close-on-content-click="false"
+              :nudge-width="200"
+              v-model="subscriptionCreateMenu[item.type]"
+            >
+              <v-btn small flat icon slot="activator">
+                <v-icon>add</v-icon>
+              </v-btn>
+              <subscription-creation
+                :title="item.name"
+                :type="item.type"
+                :keyItems="possibleKeys[item.type]"
+              />
+            </v-menu>
+          </v-list-tile-action>
+        </v-list-tile>
+        <v-list-tile
+          v-if="subscriptions[item.type].length > 0 && item.expanded"
+          v-for="subscription in subscriptions[item.type]"
+          :key="subscription.pk"
+          @click="workOnSubscription(subscription)"
+        >
+          <v-list-tile-content class="ml-3">
+            {{subscription.query_key ? subscription.query_key : 'Active'}}
+          </v-list-tile-content>
+        </v-list-tile>
+      </div>
+    </v-list>
+  </v-card>
+</template>
+
+<script>
+  import {mapGetters, mapActions} from 'vuex'
+  import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation'
+  export default {
+    components: {SubscriptionCreation},
+    name: 'subscription-list',
+    data () {
+      return {
+        subscriptionCreateMenu: {},
+
+        subscriptionTypes: [
+          {
+            name: 'Random',
+            type: 'random',
+            description: 'Random submissions of all types.',
+            expanded: true
+          },
+          {
+            name: 'Exam',
+            type: 'exam',
+            description: 'Just submissions for the specified exam.',
+            expanded: true
+          },
+          {
+            name: 'Submission Type',
+            type: 'submission_type',
+            description: 'Just submissions for the specified type.',
+            expanded: true
+          },
+          {
+            name: 'Student',
+            type: 'student',
+            description: 'The submissions of a student.',
+            expanded: true,
+            permission: () => {
+              return this.$store.getters.isReviewer
+            }
+          }
+        ]
+      }
+    },
+    computed: {
+      ...mapGetters({
+        subscriptions: 'getSubscriptionsGroupedByType'
+      }),
+      possibleKeys () {
+        const submissionTypes = Object.entries(this.$store.state.submissionTypes).map(([id, type]) => {
+          return {text: type.name}
+        })
+        return {
+          submission_type: submissionTypes
+        }
+      }
+    },
+    methods: {
+      ...mapActions([
+        'getSubscriptions',
+        'updateSubmissionTypes',
+        'getCurrentAssignment'
+      ]),
+      workOnSubscription (subscription) {
+        this.$router.push(`tutor/subscription/${subscription['pk']}`)
+      }
+    },
+    created () {
+      if (Object.keys(this.$store.state.subscriptions).length === 0) {
+        this.getSubscriptions()
+      }
+      if (Object.keys(this.$store.state.submissionTypes).length === 0) {
+        this.updateSubmissionTypes(['name'])
+      }
+    }
+  }
+</script>
+
+
+<style scoped>
+
+</style>
diff --git a/frontend/src/main.js b/frontend/src/main.js
index a92f0660684df1256f3e73c16d89dddb59e9d021..5cdc9a464716e5fb36589c76c6bb6aa114bd2113 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -5,6 +5,7 @@ import App from './App'
 import router from './router'
 import store from './store/store'
 import Vuetify from 'vuetify'
+import Notifications from 'vue-notification'
 import Cliboard from 'v-clipboard'
 
 import 'vuetify/dist/vuetify.min.css'
@@ -14,6 +15,7 @@ import 'google-code-prettify/bin/prettify.min.css'
 
 Vue.use(Vuetify)
 Vue.use(Cliboard)
+Vue.use(Notifications)
 
 Vue.config.productionTip = false
 
diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue
index a5b5d1b40e1d1d9fa53a1814a9396b15dc060e47..3b7b36216a3b7729ab8b31c493e233554daaa289 100644
--- a/frontend/src/pages/Login.vue
+++ b/frontend/src/pages/Login.vue
@@ -1,16 +1,16 @@
 <template>
       <v-container fill-height>
         <v-layout align-center justify-center>
-          <v-flex text-xs-center md4 lg2>
+          <v-flex text-xs-center xs8 sm6 md4 lg2>
             <img src="../assets/brand.png"/>
             <h3 class="pt-3">Log in</h3>
             <v-alert
               outline
-              v-if="error"
+              v-if="msg"
               color="error"
               :value="true"
               transition="fade-transition"
-            >{{ error }}</v-alert>
+            >{{ msg }}</v-alert>
             <p v-else>But I corrected them, sir.</p>
             <v-form
               @submit="submit">
@@ -48,21 +48,21 @@
       }
     },
     computed: {
-      ...mapState([
-        'error',
-        'userRole'
-      ])
+      ...mapState({
+        msg: state => state.authentication.message,
+        userRole: state => state.authentication.userRole
+      })
     },
     methods: {
       ...mapActions([
-        'getJWTToken',
+        'getJWT',
         'getExamModule',
         'getUserRole',
         'getJWTTimeDelta'
       ]),
       submit () {
         this.loading = true
-        this.getJWTToken(this.credentials).then(() => {
+        this.getJWT(this.credentials).then(() => {
           this.getUserRole().then(() => {
             switch (this.userRole) {
               case 'Student': this.$router.push('/student')
diff --git a/frontend/src/pages/SubmissionCorrectionPage.vue b/frontend/src/pages/SubmissionCorrectionPage.vue
deleted file mode 100644
index 386e82100baf48e383030bc1995cc8f491974fc7..0000000000000000000000000000000000000000
--- a/frontend/src/pages/SubmissionCorrectionPage.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<template>
-  <v-layout row wrap>
-    <v-flex xs12 md6>
-      <annotated-submission
-        :rawSubmission="mockSubmission"
-        :feedback="mockFeedback"
-        :score="mockScore"
-        :editable="true"
-        class="ma-4 autofocus"
-      />
-    </v-flex>
-
-    <v-flex md6>
-      <submission-type
-        v-bind="mockSubType"
-        :reverse="true"
-        :expandedByDefault="{ Description: false, Solution: true }"
-      />
-    </v-flex>
-  </v-layout>
-</template>
-
-<script>
-  import AnnotatedSubmission from '@/components/submission_notes/AnnotatedSubmission'
-  import SubmissionType from '@/components/SubmissionType'
-
-  export default {
-    components: {
-      SubmissionType,
-      AnnotatedSubmission},
-    name: 'submission-correction-page',
-    data () {
-      return {
-        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',
-        mockFeedback: {
-          1: 'Youre STUPID',
-          4: 'Very much so'
-        },
-        mockScore: 42,
-        mockSubType: {
-          description: 'Space suits meet with devastation! The vogon dies disconnection like an intelligent dosi.',
-          solution: 'The volume is a remarkable sinner.',
-          name: 'Seas stutter from graces like wet clouds.',
-          fullScore: 42
-        }
-      }
-    }
-  }
-</script>
-
-<style scoped>
-
-</style>
diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue
new file mode 100644
index 0000000000000000000000000000000000000000..77d7e7635667a4a07efd4c68f8bbad18d1b5491b
--- /dev/null
+++ b/frontend/src/pages/SubscriptionWorkPage.vue
@@ -0,0 +1,90 @@
+<template>
+  <v-layout
+    v-if="loaded"
+    row wrap
+  >
+    <v-flex xs12 md6>
+      <submission-correction
+        :assignment="currentAssignment"
+        @feedbackCreated="startWorkOnNextAssignment"
+        class="ma-4 autofocus"
+      />
+    </v-flex>
+
+    <v-flex md6>
+      <submission-type
+        v-bind="submissionType"
+        :reverse="true"
+        :expandedByDefault="{ Description: false, Solution: true }"
+      />
+    </v-flex>
+  </v-layout>
+</template>
+
+<script>
+  import SubmissionCorrection from '@/components/submission_notes/SubmissionCorrection'
+  import SubmissionType from '@/components/SubmissionType'
+
+  export default {
+    components: {
+      SubmissionType,
+      SubmissionCorrection},
+    name: 'subscription-work-page',
+    data () {
+      return {
+        currentAssignment: {},
+        nextAssignment: {},
+        loaded: false
+      }
+    },
+    computed: {
+      subscription () {
+        return this.$store.state.subscriptions[this.$route.params['pk']]
+      },
+      submission () {
+        return this.loaded ? this.currentAssignment.submission : {}
+      },
+      submissionType () {
+        return this.loaded ? this.$store.state.submissionTypes[this.submission['type_pk']] : {}
+      }
+    },
+    methods: {
+      prefetchAssignment () {
+        this.$store.dispatch('getNextAssignment', this.subscription['pk']).then(assignment => {
+          this.nextAssignment = assignment
+        }).catch(err => {
+          this.nextAssignment = null
+          if (err.statusCode === 410) {
+            this.$notify({
+              title: 'Last submission here!',
+              text: 'This will be your last submission to correct for this subscription.',
+              type: 'warning'
+            })
+          }
+        })
+      },
+      startWorkOnNextAssignment () {
+        this.currentAssignment = this.nextAssignment
+        this.prefetchAssignment()
+      }
+    },
+    created () {
+      if (!this.subscription.currentAssignment) {
+        this.$store.dispatch('getCurrentAssignment', this.subscription['pk']).then(assignment => {
+          this.currentAssignment = assignment
+          this.loaded = true
+        }).catch(err => {
+          console.log('Unable to fetch current Assignment. Err:' + err)
+        })
+        this.prefetchAssignment()
+      } else {
+        this.currentAssignment = this.subscription.currentAssignment
+        this.loaded = true
+      }
+    }
+  }
+</script>
+
+<style scoped>
+
+</style>
diff --git a/frontend/src/pages/student/StudentLayout.vue b/frontend/src/pages/student/StudentLayout.vue
index c20f0943815c5c3298e3a5e86d6fb7a2655b0902..825661306946e09427019a3ca6c583d8754e93a2 100644
--- a/frontend/src/pages/student/StudentLayout.vue
+++ b/frontend/src/pages/student/StudentLayout.vue
@@ -20,11 +20,11 @@
 
       <v-divider></v-divider>
 
-        <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>
+            <exam-information
+              :exam="exam"
+              v-if="!mini"
+              class="elevation-1 exam-info ma-1"
+            />
       <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>
@@ -75,11 +75,15 @@
         return this.submissions.map((sub, index) => {
           return {
             name: sub.type.name,
-            id: sub.type.id,
-            route: `/student/submission/${sub.type.id}`
+            id: sub.type.pk,
+            route: `/student/submission/${sub.type.pk}`
           }
         })
       }
     }
   }
 </script>
+
+<style scoped>
+
+</style>
diff --git a/frontend/src/pages/student/StudentSubmissionPage.vue b/frontend/src/pages/student/StudentSubmissionPage.vue
index 1d13fc4ccad88fb4198b5e72e052678827878f75..45d46bcda72ed80d6eb74bc0466b6c4f0757948a 100644
--- a/frontend/src/pages/student/StudentSubmissionPage.vue
+++ b/frontend/src/pages/student/StudentSubmissionPage.vue
@@ -1,14 +1,36 @@
 <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-layout row wrap>
+      <v-flex lg6 md12 mt-5>
+        <base-annotated-submission>
+          <v-toolbar
+            dense
+            slot="header"
+            class="mb-1 elevation-1"
+          >
+            <v-btn flat color="info" @click="showFeedback = !showFeedback">
+              <div v-if="showFeedback"> Hide Feedback</div>
+              <div v-else> Show Feedback</div>
+            </v-btn>
+
+            <v-spacer/>
+
+            <h2>Score: {{score}} / {{submissionType.full_score}}</h2>
+          </v-toolbar>
+          <template slot="table-content">
+            <tr v-for="(code, lineNo) in submission" :key="lineNo">
+              <submission-line :code="code" :lineNo="lineNo"/>
+              <feedback-comment
+                v-if="feedback[lineNo] && showFeedback"
+                v-for="(comment, index) in feedback[lineNo]"
+                v-bind="comment"
+                :key="index"
+              />
+            </tr>
+          </template>
+        </base-annotated-submission>
       </v-flex>
-      <v-flex xs-12 sm-6 md-6>
+      <v-flex lg6 md12>
         <submission-type
         v-bind="submissionType">
         </submission-type>
@@ -19,30 +41,53 @@
 
 
 <script>
-  import { mapState } from 'vuex'
-  import AnnotatedSubmission from '@/components/submission_notes/AnnotatedSubmission'
+  import { mapState, mapGetters } from 'vuex'
+  import AnnotatedSubmission from '@/components/submission_notes/SubmissionCorrection'
   import SubmissionType from '@/components/SubmissionType'
+  import BaseAnnotatedSubmission from '@/components/submission_notes/base/BaseAnnotatedSubmission'
+  import SubmissionLine from '@/components/submission_notes/base/SubmissionLine'
+  import FeedbackComment from '@/components/submission_notes/base/FeedbackComment'
   export default {
     name: 'student-submission-page',
-    components: {AnnotatedSubmission, SubmissionType},
+    components: {
+      FeedbackComment,
+      SubmissionLine,
+      BaseAnnotatedSubmission,
+      AnnotatedSubmission,
+      SubmissionType},
+    data () {
+      return {
+        showFeedback: true
+      }
+    },
     computed: {
       id: function () {
         return this.$route.params.id
       },
+      ...mapGetters([
+        'submission'
+      ]),
       ...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 }
+        submissionType: function (state) { return state.studentPage.submissionData[this.id].type },
+        feedback: function (state) {
+          return state.studentPage.submissionData[this.$route.params.id].feedback.feedback_lines
+        }
       })
     },
-    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 })
+    methods: {
+      onRouteMountOrUpdate (routeId) {
+        this.$store.commit('SET_VISITED', { index: routeId, visited: true })
+        this.$store.commit('SET_RAW_SUBMISSION',
+          this.$store.state.studentPage.submissionData[this.id].text)
       }
+    },
+    mounted () {
+      this.onRouteMountOrUpdate(this.id)
+    },
+    beforeRouteUpdate (to, from, next) {
+      this.onRouteMountOrUpdate(to.params.id)
+      next()
     }
   }
 </script>
diff --git a/frontend/src/pages/tutor/TutorLayout.vue b/frontend/src/pages/tutor/TutorLayout.vue
index b5641050361515af4df61a078b8510628a3521d7..3966da439a62c1e13949700a570cc93ac4b15da8 100644
--- a/frontend/src/pages/tutor/TutorLayout.vue
+++ b/frontend/src/pages/tutor/TutorLayout.vue
@@ -2,7 +2,7 @@
   <base-layout @sidebarMini="mini = $event">
 
     <template slot="header">
-      Collapse
+      Grady
     </template>
 
     <v-list dense slot="sidebar-content">
diff --git a/frontend/src/pages/tutor/TutorStartPage.vue b/frontend/src/pages/tutor/TutorStartPage.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1a98f924261130743f62ecc22cf241c28f96e462
--- /dev/null
+++ b/frontend/src/pages/tutor/TutorStartPage.vue
@@ -0,0 +1,21 @@
+<template>
+    <v-flex lg3>
+      <subscription-list/>
+    </v-flex>
+</template>
+
+<script>
+  import SubscriptionList from '@/components/subscriptions/SubscriptionList'
+
+  export default {
+    components: {SubscriptionList},
+    name: 'tutor-start-page',
+    mounted () {
+      this.$store.dispatch('updateSubmissionTypes')
+    }
+  }
+</script>
+
+<style scoped>
+
+</style>
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index babc257c19f2cb17155ebb0f15c1f313335216dd..b3769bf03fc43cf44f9c2e52b7ece97b51eea7fa 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -1,12 +1,13 @@
 import Vue from 'vue'
 import Router from 'vue-router'
-import store from '../store/store'
+// import store from '@/store/store'
 import Login from '@/pages/Login'
 import TutorLayout from '@/pages/tutor/TutorLayout'
+import TutorStartPage from '@/pages/tutor/TutorStartPage'
 import StudentPage from '@/pages/student/StudentPage'
 import StudentLayout from '@/pages/student/StudentLayout'
 import StudentSubmissionPage from '@/pages/student/StudentSubmissionPage'
-import SubmissionCorrectionPage from '@/pages/SubmissionCorrectionPage'
+import SubscriptionWorkPage from '@/pages/SubscriptionWorkPage'
 import ReviewerPage from '@/pages/reviewer/ReviewerPage'
 import StudentListOverview from '@/pages/reviewer/StudentListOverview'
 
@@ -38,8 +39,12 @@ const router = new Router({
       component: TutorLayout,
       children: [
         {
-          path: 'assignment/',
-          component: SubmissionCorrectionPage
+          path: '',
+          component: TutorStartPage
+        },
+        {
+          path: 'subscription/:pk',
+          component: SubscriptionWorkPage
         }
       ]
     },
@@ -56,21 +61,4 @@ const router = new Router({
   ]
 })
 
-router.beforeEach((to, from, next) => {
-  if (to.path === '/' || from.path === '/') {
-    next()
-  } else {
-    const now = Date.now()
-    if (now - store.state.logInTime > store.state.jwtTimeDelta * 1000) {
-      store.dispatch('logout').then(() => {
-        store.commit('API_FAIL', 'You\'ve been logged out due to inactivity')
-        next('/')
-      })
-    } else {
-      store.dispatch('refreshJWTToken')
-      next()
-    }
-  }
-})
-
 export default router
diff --git a/frontend/src/store/actions.js b/frontend/src/store/actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..f162396762c2c7245a36331eca6c1e9ad0192192
--- /dev/null
+++ b/frontend/src/store/actions.js
@@ -0,0 +1,65 @@
+import {types} from './mutations'
+import * as api from '@/api'
+import router from '@/router/index'
+
+const actions = {
+  async getSubscriptions ({ commit }) {
+    try {
+      const subscriptions = await api.fetchSubscriptions()
+      commit(types.SET_SUBSCRIPTIONS, subscriptions)
+    } catch (e) {
+      console.log(e)
+    }
+  },
+  async subscribeTo ({ commit }, {type, key, stage}) {
+    try {
+      const subscription = await api.subscribeTo(type, key, stage)
+      commit(types.SET_SUBSCRIPTION, subscription)
+    } catch (e) {
+      console.log(e)
+    }
+  },
+  async updateSubmissionTypes ({ commit }, fields) {
+    try {
+      const submissionTypes = await api.fetchSubmissionTypes(fields)
+      submissionTypes.forEach(type => {
+        commit(types.UPDATE_SUBMISSION_TYPE, type)
+      })
+    } catch (e) {
+      console.log(e)
+    }
+  },
+  async getCurrentAssignment ({ commit }, subscriptionPk) {
+    try {
+      const assignment = await api.fetchCurrentAssignment(subscriptionPk)
+      commit(types.UPDATE_ASSIGNMENT, {
+        assignment,
+        subscriptionPk,
+        key: 'currentAssignment'
+      })
+      return assignment
+    } catch (e) {
+      console.log(e)
+    }
+  },
+  async getNextAssignment ({ commit }, subscriptionPk) {
+    try {
+      const assignment = await api.fetchNextAssignment(subscriptionPk)
+      commit(types.UPDATE_ASSIGNMENT, {
+        assignment,
+        subscriptionPk,
+        key: 'nextAssignment'
+      })
+      return assignment
+    } catch (e) {
+      console.log(e)
+    }
+  },
+  logout ({ commit }, message = '') {
+    commit(types.RESET_STATE)
+    commit('SET_MESSAGE', message)
+    router.push('/')
+  }
+}
+
+export default actions
diff --git a/frontend/src/store/api.js b/frontend/src/store/api.js
deleted file mode 100644
index c1e523645706a4f2c07c2282906e138186c67d4f..0000000000000000000000000000000000000000
--- a/frontend/src/store/api.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import axios from 'axios'
-
-let ax = axios.create({
-  baseURL: 'http://localhost:8000/',
-  headers: {'Authorization': 'JWT ' + sessionStorage.getItem('jwtToken')}
-})
-
-export default ax
diff --git a/frontend/src/store/getters.js b/frontend/src/store/getters.js
new file mode 100644
index 0000000000000000000000000000000000000000..e00a18775e08e17dcc568f6b0ab9cf1f59554876
--- /dev/null
+++ b/frontend/src/store/getters.js
@@ -0,0 +1,25 @@
+const getters = {
+  getSubscriptionsGroupedByType (state) {
+    let subscriptions = {
+      'random': [],
+      'student': [],
+      'exam': [],
+      'submission_type': []
+    }
+    Object.entries(state.subscriptions).forEach(([id, submission]) => {
+      subscriptions[submission.query_type].push(submission)
+    })
+    return subscriptions
+  },
+  getSubmission: state => pk => {
+    return state.submissions[pk]
+  },
+  getFeedback: state => pk => {
+    return state.feedback[pk]
+  },
+  getSubmissionType: state => pk => {
+    return state.submissionTypes[pk]
+  }
+}
+
+export default getters
diff --git a/frontend/src/store/lastInteractionPlugin.js b/frontend/src/store/lastInteractionPlugin.js
new file mode 100644
index 0000000000000000000000000000000000000000..766a0db22e1920b4e3b02372ceb6f1d04e9416d2
--- /dev/null
+++ b/frontend/src/store/lastInteractionPlugin.js
@@ -0,0 +1,9 @@
+import {types} from '@/store/mutations'
+
+export function lastInteraction (store) {
+  store.subscribe((mutation, state) => {
+    if (mutation.type !== types.SET_LAST_INTERACTION) {
+      store.commit(types.SET_LAST_INTERACTION)
+    }
+  })
+}
diff --git a/frontend/src/store/modules/authentication.js b/frontend/src/store/modules/authentication.js
new file mode 100644
index 0000000000000000000000000000000000000000..2b5137251eef0e11bc9376e91ebd25d757ee6ec8
--- /dev/null
+++ b/frontend/src/store/modules/authentication.js
@@ -0,0 +1,110 @@
+import {fetchJWT, fetchJWTTimeDelta, fetchUserRole, refreshJWT} from '@/api'
+import gradySays from '../grady_speak'
+
+function initialState () {
+  return {
+    token: sessionStorage.getItem('token'),
+    tokenCreationTime: 0,
+    refreshingToken: false,
+    username: '',
+    jwtTimeDelta: 0,
+    userRole: '',
+    message: ''
+  }
+}
+
+const authentication = {
+  state: {
+    ...initialState()
+  },
+  getters: {
+    gradySpeak: () => {
+      return gradySays[Math.floor(Math.random() * gradySays.length)]
+    },
+    isStudent: state => {
+      return state.userRole === 'Student'
+    },
+    isTutor: state => {
+      return state.userRole === 'Tutor'
+    },
+    isReviewer: state => {
+      return state.userRole === 'Reviewer'
+    }
+  },
+  mutations: {
+    'SET_MESSAGE': function (state, message) {
+      state.message = message
+    },
+    'SET_JWT_TOKEN': function (state, token) {
+      sessionStorage.setItem('token', token)
+      state.token = token
+      state.tokenCreationTime = Date.now()
+    },
+    'SET_JWT_TIME_DELTA': function (state, timeDelta) {
+      state.jwtTimeDelta = timeDelta
+    },
+    'SET_USERNAME': function (state, username) {
+      state.username = username
+    },
+    'SET_USER_ROLE': function (state, userRole) {
+      state.userRole = userRole
+    },
+    'RESET_STATE': function (state) {
+      sessionStorage.setItem('token', '')
+      Object.assign(state, initialState())
+    },
+    'SET_REFRESHING_TOKEN': function (state, refreshing) {
+      state.refreshingToken = refreshing
+    }
+  },
+  actions: {
+    async getJWT (context, credentials) {
+      try {
+        const token = await fetchJWT(credentials)
+        context.commit('SET_USERNAME', credentials.username)
+        context.commit('SET_JWT_TOKEN', token)
+      } catch (error) {
+        console.log(error)
+        if (error.response) {
+          const errorMsg = 'Unable to log in with provided credentials.'
+          context.commit('SET_MESSAGE', errorMsg)
+          throw errorMsg
+        } else {
+          const errorMsg = 'Cannot reach server.'
+          context.commit('SET_MESSAGE', errorMsg)
+          throw errorMsg
+        }
+      }
+    },
+    async refreshJWT ({state, commit, dispatch}) {
+      commit('SET_REFRESHING_TOKEN', true)
+      try {
+        const token = await refreshJWT(state.token)
+        commit('SET_JWT_TOKEN', token)
+      } catch (err) {
+        dispatch('logout')
+      } finally {
+        commit('SET_REFRESHING_TOKEN', false)
+      }
+    },
+    async getUserRole ({commit}) {
+      try {
+        const userRole = await fetchUserRole()
+        commit('SET_USER_ROLE', userRole)
+      } catch (err) {
+        commit('SET_MESSAGE', "You've been logged out.")
+      }
+    },
+    async getJWTTimeDelta ({commit}) {
+      try {
+        const delta = await fetchJWTTimeDelta()
+        // multiply by 1000 to convert to ms
+        commit('SET_JWT_TIME_DELTA', delta * 1000)
+      } catch (err) {
+        console.log(err)
+      }
+    }
+  }
+}
+
+export default authentication
diff --git a/frontend/src/store/modules/student-page.js b/frontend/src/store/modules/student-page.js
index 45a5b34f3d678182a596e039f83d48aaf83d98bd..01da389ac82ba48731525c618183eb459e96af8a 100644
--- a/frontend/src/store/modules/student-page.js
+++ b/frontend/src/store/modules/student-page.js
@@ -1,4 +1,4 @@
-import ax from '../api'
+import {fetchStudentSelfData, fetchStudentSubmissions} from '../../api'
 
 const studentPage = {
   state: {
@@ -30,7 +30,7 @@ const studentPage = {
      */
     'SET_FULL_SUBMISSION_DATA': function (state, submissionData) {
       state.submissionData = submissionData.reduce((acc, cur, index) => {
-        acc[cur.type.id] = cur
+        acc[cur.type.pk] = cur
         return acc
       }, {})
     },
@@ -43,57 +43,36 @@ const studentPage = {
   },
   actions: {
 
-    getStudentData (context) {
-      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_FOR_LIST', data.submissions)
+    async getStudentData (context) {
+      try {
+        const studentData = await fetchStudentSelfData()
+        context.commit('SET_STUDENT_NAME', studentData.name)
+        context.commit('SET_EXAM', studentData.exam)
+        context.commit('SET_SUBMISSIONS_FOR_LIST', studentData.submissions)
         context.commit('SET_LOADED', true)
-      })
+      } catch (e) {
+        this.$notify({
+          title: 'API Fail',
+          text: 'Unable to fetch student data',
+          type: 'error'
+        })
+        console.log(e)
+      }
     },
 
     async getStudentSubmissions (context) {
-      const response = await ax.get('/api/student-submissions')
-      context.commit('SET_FULL_SUBMISSION_DATA', response.data)
+      try {
+        const submissions = await fetchStudentSubmissions()
+        context.commit('SET_FULL_SUBMISSION_DATA', submissions)
+      } catch (e) {
+        this.$notify({
+          title: 'API Fail',
+          text: 'Unable to fetch student submissions',
+          type: 'error'
+        })
+      }
     }
   }
 }
 
-// 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 10a47e24d2471d5d27e39900a0d8002c228f8007..e80dbb7d8389a7328056dfb4788baf58afa43e09 100644
--- a/frontend/src/store/modules/submission-notes.js
+++ b/frontend/src/store/modules/submission-notes.js
@@ -1,44 +1,85 @@
-// import Vue from 'vue'
+import Vue from 'vue'
+import * as api from '@/api'
 
-// 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)
-//     }
-//   }
-// }
+function initialState () {
+  return {
+    assignment: '',
+    ui: {
+      showEditorOnLine: {},
+      selectedCommentOnLine: {}
+    },
+    orig: {
+      rawSubmission: '',
+      score: null,
+      feedbackLines: {}
+    },
+    updated: {
+      score: null,
+      feedbackLines: {}
+    }
+  }
+}
 
-// export default submissionNotes
+const submissionNotes = {
+  namespaced: true,
+  state: {
+    ...initialState()
+  },
+  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.orig.rawSubmission.split('\n').reduce((acc, cur, index) => {
+        acc[index + 1] = cur
+        return acc
+      }, {})
+    },
+    score: state => {
+      return state.updated.score !== null ? state.updated.score : state.orig.score
+    }
+  },
+  mutations: {
+    'SET_RAW_SUBMISSION': function (state, submission) {
+      state.orig.rawSubmission = submission
+    },
+    'SET_ORIG_FEEDBACK': function (state, feedback) {
+      if (feedback) {
+        state.orig.feedbackLines = feedback['feedback_lines'] ? feedback['feedback_lines'] : {}
+        state.orig.score = feedback.score
+      }
+    },
+    'UPDATE_FEEDBACK_LINE': function (state, feedback) {
+      Vue.set(state.updated.feedbackLines, feedback.lineNo, feedback.comment)
+    },
+    'UPDATE_FEEDBACK_SCORE': function (state, score) {
+      state.updated.score = score
+    },
+    'DELETE_FEEDBACK_LINE': function (state, lineNo) {
+      Vue.delete(state.updated.feedbackLines, lineNo)
+    },
+    'TOGGLE_EDITOR_ON_LINE': function (state, {lineNo, comment}) {
+      Vue.set(state.ui.selectedCommentOnLine, lineNo, comment)
+      Vue.set(state.ui.showEditorOnLine, lineNo, !state.ui.showEditorOnLine[lineNo])
+    },
+    'RESET_STATE': function (state) {
+      Object.assign(state, initialState())
+    }
+  },
+  actions: {
+    'submitFeedback': async function ({state}, assignment) {
+      let feedback = {}
+      if (Object.keys(state.updated.feedbackLines).length > 0) {
+        feedback['feedback_lines'] = state.updated.feedbackLines
+      }
+      if (state.orig.score === null && state.updated.score === null) {
+        throw new Error('You need to give a score.')
+      } else if (state.updated.score !== null) {
+        feedback['score'] = state.updated.score
+      }
+      return api.submitFeedbackForAssignment(feedback, assignment['pk'])
+    }
+  }
+}
+
+export default submissionNotes
diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.js
new file mode 100644
index 0000000000000000000000000000000000000000..c7f1c544d327455b6c4509f58b3effc4b319e424
--- /dev/null
+++ b/frontend/src/store/mutations.js
@@ -0,0 +1,58 @@
+import Vue from 'vue'
+
+import {initialState} from '@/store/store'
+
+export const types = {
+  SET_ASSIGNMENT: 'SET_ASSIGNMENT',
+  SET_SUBSCRIPTIONS: 'SET_SUBSCRIPTIONS',
+  SET_SUBSCRIPTION: 'SET_SUBSCRIPTION',
+  UPDATE_SUBMISSION_TYPE: 'UPDATE_SUBMISSION_TYPE',
+  UPDATE_ASSIGNMENT: 'UPDATE_ASSIGNMENT',
+  UPDATE_NEXT_ASSIGNMENT: 'UPDATE_NEXT_ASSIGNMENT',
+  RESET_STATE: 'RESET_STATE',
+  SET_LAST_INTERACTION: 'SET_LAST_INTERACTION'
+}
+
+const mutations = {
+  [types.SET_ASSIGNMENT] (state, assignment) {
+    Vue.set(state.assignments, assignment.pk, assignment)
+  },
+  [types.SET_SUBSCRIPTIONS] (state, subscriptions) {
+    state.subscriptions = subscriptions.reduce((acc, curr) => {
+      acc[curr['pk']] = curr
+      return acc
+    }, {})
+  },
+  [types.SET_SUBSCRIPTION] (state, subscription) {
+    Vue.set(state.subscriptions, subscription.pk, subscription)
+  },
+  [types.UPDATE_SUBMISSION_TYPE] (state, submissionType) {
+    const updatedSubmissionType = {
+      ...state.submissionTypes[submissionType.pk],
+      ...submissionType
+    }
+    Vue.set(state.submissionTypes, submissionType.pk, updatedSubmissionType)
+  },
+  [types.UPDATE_ASSIGNMENT] (state, {key, assignment, subscriptionPk}) {
+    const submission = assignment.submission
+    const feedback = assignment.feedback
+    let updatedAssignment = {
+      ...state.assignments[assignment.pk],
+      ...assignment
+    }
+    if (feedback) {
+      Vue.set(state.feedback, feedback.pk, feedback)
+    }
+    Vue.set(state.assignments, assignment.pk, updatedAssignment)
+    Vue.set(state.submissions, submission.pk, submission)
+    Vue.set(state.subscriptions[subscriptionPk], key, updatedAssignment)
+  },
+  [types.RESET_STATE] (state) {
+    Object.assign(state, initialState())
+  },
+  [types.SET_LAST_INTERACTION] (state) {
+    state.lastAppInteraction = Date.now()
+  }
+}
+
+export default mutations
diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js
index 5b60e7706279674a107aa04d7e88472cb855661f..4e1d15819682fb5acb109cbbc95fc79fe47691c0 100644
--- a/frontend/src/store/store.js
+++ b/frontend/src/store/store.js
@@ -1,103 +1,53 @@
 import Vuex from 'vuex'
 import Vue from 'vue'
-import ax from './api'
+import createPersistedState from 'vuex-persistedstate'
 
-import gradySays from './grady_speak'
 import studentPage from './modules/student-page'
+import submissionNotes from './modules/submission-notes'
+import authentication from './modules/authentication'
+
+import actions from './actions'
+import getters from './getters'
+import mutations from '@/store/mutations'
+import {lastInteraction} from '@/store/lastInteractionPlugin'
 
 Vue.use(Vuex)
 
+export function initialState () {
+  return {
+    lastAppInteraction: Date.now(),
+    currentTime: Date.now(),
+    examTypes: {},
+    submissionTypes: {},
+    submissions: {},
+    feedback: {},
+    subscriptions: {},
+    assignments: {}
+  }
+}
+
 const store = new Vuex.Store({
+  // TODO only enable this in dev and not in deployment (use env variable)
+  strict: true,
   modules: {
-    studentPage
+    authentication,
+    studentPage,
+    submissionNotes
   },
+  plugins: [createPersistedState({
+    storage: window.sessionStorage,
+    // authentication.token is manually saved since using it with this plugin caused issues
+    // when manually reloading the page
+    paths: Object.keys(initialState()).concat(
+      ['studentPage', 'submissionNotes', 'authentication.username', 'authentication.userRole',
+        'authentication.jwtTimeDelta'])
+  }),
+    lastInteraction],
+  actions,
+  getters,
+  mutations,
   state: {
-    token: sessionStorage.getItem('jwtToken'),
-    loggedIn: !!sessionStorage.getItem('jwtToken'),
-    logInTime: Number(sessionStorage.getItem('logInTime')),
-    username: sessionStorage.getItem('username'),
-    jwtTimeDelta: Number(sessionStorage.getItem('jwtTimeDelta')),
-    userRole: sessionStorage.getItem('userRole'),
-    error: ''
-  },
-  getters: {
-    gradySpeak: () => {
-      return gradySays[Math.floor(Math.random() * gradySays.length)]
-    },
-    isStudent: state => {
-      return state.userRole === 'Student'
-    },
-    isTutor: state => {
-      return state.userRole === 'Tutor'
-    },
-    isReviewer: state => {
-      return state.userRole === 'Reviewer'
-    }
-  },
-  mutations: {
-    'API_FAIL': function (state, error) {
-      state.error = error
-    },
-    'SET_JWT_TOKEN': function (state, token) {
-      state.token = token
-      state.logInTime = Date.now()
-      ax.defaults.headers['Authorization'] = 'JWT ' + token
-      sessionStorage.setItem('jwtToken', token)
-      sessionStorage.setItem('logInTime', String(state.logInTime))
-    },
-    'SET_JWT_TIME_DELTA': function (state, timeDelta) {
-      state.jwtTimeDelta = timeDelta
-      sessionStorage.setItem('jwtTimeDelta', timeDelta)
-    },
-    'LOGIN': function (state, username) {
-      state.loggedIn = true
-      state.username = username
-      sessionStorage.setItem('username', username)
-    },
-    'LOGOUT': function (state) {
-      state.loggedIn = false
-    },
-    'SET_USER_ROLE': function (state, userRole) {
-      state.userRole = userRole
-      sessionStorage.setItem('userRole', userRole)
-    }
-  },
-  actions: {
-    async getJWTToken (context, credentials) {
-      try {
-        const response = await ax.post('api-token-auth/', credentials)
-        context.commit('LOGIN', credentials.username)
-        context.commit('SET_JWT_TOKEN', response.data.token)
-      } catch (error) {
-        if (error.response) {
-          const errorMsg = 'Unable to log in with provided credentials.'
-          context.commit('API_FAIL', errorMsg)
-          throw errorMsg
-        } else {
-          const errorMsg = 'Cannot reach server.'
-          context.commit('API_FAIL', errorMsg)
-          throw errorMsg
-        }
-      }
-    },
-    refreshJWTToken (context) {
-      ax.post('/api-token-refresh/', {token: context.state.token}).then(response => {
-        context.commit('SET_JWT_TOKEN', response.data.token)
-      })
-    },
-    getJWTTimeDelta (context) {
-      ax.get('api/jwt-time-delta/').then(response => {
-        context.commit('SET_JWT_TIME_DELTA', response.data.timeDelta)
-      })
-    },
-    async getUserRole (context) {
-      const response = await ax.get('api/user-role/')
-      context.commit('SET_USER_ROLE', response.data.role)
-    },
-    logout (store) {
-      store.commit('LOGOUT')
-      store.commit('SET_JWT_TOKEN', '')
-    }
+    ...initialState()
   }
 })
 
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 1098822b3ab11afca7cf48c304034ec91d5014fb..326369ad893c3735fe86e1c252134901371610c0 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1711,6 +1711,10 @@ deep-is@~0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
 
+deepmerge@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.0.1.tgz#25c1c24f110fb914f80001b925264dd77f3f4312"
+
 defined@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
@@ -5214,6 +5218,10 @@ shelljs@^0.7.5, shelljs@^0.7.6:
     interpret "^1.0.0"
     rechoir "^0.6.2"
 
+shvl@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shvl/-/shvl-1.2.0.tgz#5e2de474c68b8430602689a7d35100ad2cb33fec"
+
 signal-exit@^3.0.0, signal-exit@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -5797,6 +5805,10 @@ vary@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
 
+velocity-animate@^1.5.0:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/velocity-animate/-/velocity-animate-1.5.1.tgz#606837047bab8fbfb59a636d1d82ecc3f7bd71a6"
+
 vendors@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22"
@@ -5841,6 +5853,12 @@ vue-loader@^13.3.0:
     vue-style-loader "^3.0.0"
     vue-template-es2015-compiler "^1.6.0"
 
+vue-notification@^1.3.6:
+  version "1.3.6"
+  resolved "https://registry.yarnpkg.com/vue-notification/-/vue-notification-1.3.6.tgz#f11f825a3d9858ef17f22d4a72e9e6d383d97bbf"
+  dependencies:
+    velocity-animate "^1.5.0"
+
 vue-router@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"
@@ -5871,6 +5889,13 @@ vuetify@^0.17.3:
   version "0.17.3"
   resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-0.17.3.tgz#66280c5532b12d80c0ce75f4574d1d5a8c2955b9"
 
+vuex-persistedstate@^2.4.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-2.4.2.tgz#a8caf63b07ce4bdff6d82b29634c051ead382bf3"
+  dependencies:
+    deepmerge "^2.0.1"
+    shvl "^1.1.1"
+
 vuex@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"
diff --git a/grady/settings/default.py b/grady/settings/default.py
index 3be7ad7365ab77bb0dcb8c5aa7095b1a0f4c4e72..47b0df73549d71f62dd11179b167962d25f308d2 100644
--- a/grady/settings/default.py
+++ b/grady/settings/default.py
@@ -143,7 +143,7 @@ REST_FRAMEWORK = {
 }
 
 JWT_AUTH = {
-    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=600),
+    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=6000),
     'JWT_ALLOW_REFRESH': True,
 }
 
diff --git a/util/factories.py b/util/factories.py
index 5582b6dd15945294218c1e518daad07e121c27c2..655f9720dd57c54380de01d662b59e4c2505b7c5 100644
--- a/util/factories.py
+++ b/util/factories.py
@@ -76,7 +76,7 @@ class GradyUserFactory:
             role=role,
             defaults=kwargs)
 
-        if created:
+        if created or password is not None:
             password = self.make_password() if password is None else password
             user.set_password(password)
             user.save()
@@ -128,7 +128,8 @@ def make_students(students=[], **kwargs):
     return [GradyUserFactory().make_student(
         username=student['username'],
         exam=ExamType.objects.get(
-            module_reference=student['exam']) if 'exam' in student else None
+            module_reference=student['exam']) if 'exam' in student else None,
+        password=student.get('password')
     ) for student in students]
 
 
@@ -232,6 +233,31 @@ def init_test_instance():
                     'exam': 'Test Exam 01',
                     'password': 'p'
                 },
+                {
+                    'username': 'student03',
+                    'exam': 'Test Exam 01',
+                    'password': 'p'
+                },
+                {
+                    'username': 'student04',
+                    'exam': 'Test Exam 01',
+                    'password': 'p'
+                },
+                {
+                    'username': 'student05',
+                    'exam': 'Test Exam 01',
+                    'password': 'p'
+                },
+                {
+                    'username': 'student06',
+                    'exam': 'Test Exam 01',
+                    'password': 'p'
+                },
+                {
+                    'username': 'student07',
+                    'exam': 'Test Exam 01',
+                    'password': 'p'
+                },
             ],
             'tutors': [
                 {
@@ -344,5 +370,128 @@ def init_test_instance():
                         }
                     }
                 },
+
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '01. Sort this or that',
+                    'user': 'student02',
+                },
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '02. Merge this or that or maybe even this',
+                    'user': 'student02',
+                },
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '03. This one exists for the sole purpose to test',
+                    'user': 'student02',
+                },
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '01. Sort this or that',
+                    'user': 'student03',
+                },
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '02. Merge this or that or maybe even this',
+                    'user': 'student03',
+                },
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '03. This one exists for the sole purpose to test',
+                    'user': 'student03',
+                },
+
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '01. Sort this or that',
+                    'user': 'student04',
+                },
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '02. Merge this or that or maybe even this',
+                    'user': 'student04',
+                },
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '03. This one exists for the sole purpose to test',
+                    'user': 'student04',
+                },
+
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '01. Sort this or that',
+                    'user': 'student05',
+                },
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '02. Merge this or that or maybe even this',
+                    'user': 'student05',
+                },
+                {
+                    'text': 'function blabl\n'
+                            '   on multi lines\n'
+                            '       for blabla in bla:\n'
+                            '   arrrgh\n'
+                            '       asasxasx\n'
+                            '           lorem ipsum und so\n',
+                    'type': '03. This one exists for the sole purpose to test',
+                    'user': 'student05',
+                },
             ]}
     )