From 1ecc38d2ba846dcde63cfc8c105910adb63113c1 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Mon, 19 Mar 2018 14:39:35 +0100
Subject: [PATCH 1/6] Started subscription overhaul

---
 frontend/src/api.js                           |   4 +-
 .../feedback_list/FeedbackTable.vue           |   2 +-
 .../submission_notes/SubmissionCorrection.vue |   1 -
 .../AnnotatedSubmissionBottomToolbar.vue      |  14 +-
 .../subscriptions/SubscriptionForList.vue     |  39 +--
 .../subscriptions/SubscriptionList.vue        | 142 ++-------
 .../subscriptions/SubscriptionType.vue        |  91 ------
 .../subscriptions/SubscriptionsForStage.vue   |  42 +++
 frontend/src/pages/PageNotFound.vue           |  18 +-
 frontend/src/pages/SubscriptionWorkPage.vue   |  79 ++---
 frontend/src/store/actions.js                 |  75 +----
 frontend/src/store/getters.js                 |  42 ---
 frontend/src/store/modules/subscriptions.js   | 274 ++++++++++++++++++
 frontend/src/store/mutations.js               |  20 --
 frontend/src/store/store.js                   |   8 +-
 frontend/src/util/helpers.js                  |  38 +++
 16 files changed, 461 insertions(+), 428 deletions(-)
 delete mode 100644 frontend/src/components/subscriptions/SubscriptionType.vue
 create mode 100644 frontend/src/components/subscriptions/SubscriptionsForStage.vue
 create mode 100644 frontend/src/store/modules/subscriptions.js

diff --git a/frontend/src/api.js b/frontend/src/api.js
index ead1f457..b35bce86 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -129,9 +129,9 @@ export async function subscribeTo (type, key, stage) {
   return (await ax.post('/api/subscription/', data)).data
 }
 
-export async function createAssignment ({subscription}) {
+export async function createAssignment ({subscription = null, subscriptionPk = ''}) {
   const data = {
-    subscription: subscription.pk
+    subscription: subscription ? subscription.pk : subscriptionPk
   }
   return (await ax.post(`/api/assignment/`, data)).data
 }
diff --git a/frontend/src/components/feedback_list/FeedbackTable.vue b/frontend/src/components/feedback_list/FeedbackTable.vue
index 2ec4b238..02574894 100644
--- a/frontend/src/components/feedback_list/FeedbackTable.vue
+++ b/frontend/src/components/feedback_list/FeedbackTable.vue
@@ -11,7 +11,7 @@
         v-model="search"
       />
     </v-card-title>
-    <feedback-search-options/>
+    <feedback-search-options class="mx-3"/>
     <v-data-table
       :headers="headers"
       :items="feedback"
diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue
index 246b5b05..376a73f8 100644
--- a/frontend/src/components/submission_notes/SubmissionCorrection.vue
+++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue
@@ -50,7 +50,6 @@
         :skippable="assignment !== undefined"
         :feedback="feedbackObj ? feedbackObj : {}"
         @submitFeedback="submitFeedback"
-        @skip="$emit('skip')"
       />
     </base-annotated-submission>
   </div>
diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
index 6ad3b94f..e680ad9f 100644
--- a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
+++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
@@ -4,7 +4,7 @@
       <v-btn
         slot="activator"
         outline round color="grey darken-2"
-        @click="$emit('skip')"
+        @click="skipSubmission"
       >Skip</v-btn>
       <span>Skip this submission</span>
     </v-tooltip>
@@ -133,6 +133,18 @@
       },
       submit () {
         this.$emit('submitFeedback', {isFinal: this.isFinal})
+      },
+      skipSubmission () {
+        if (this.skippable) {
+          this.$store.dispatch('skipAssignment').catch(() => {
+            this.$notify({
+              title: 'Unable to skip submission',
+              type: 'error'
+            })
+          })
+        } else {
+          throw new Error("Can't skip submission when skippable is false for AnnotatedSubmissionBottomToolbar.")
+        }
       }
     }
   }
diff --git a/frontend/src/components/subscriptions/SubscriptionForList.vue b/frontend/src/components/subscriptions/SubscriptionForList.vue
index 0326bfce..6d2b1ff0 100644
--- a/frontend/src/components/subscriptions/SubscriptionForList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionForList.vue
@@ -11,20 +11,9 @@
         {{name}}
       </v-list-tile-content>
       <v-list-tile-action-text>
-        {{stageMap[feedback_stage]}}
+        available: {{available}}
       </v-list-tile-action-text>
     </v-list-tile>
-    <v-btn
-      icon
-      @click="deactivate"
-    >
-      <v-icon
-        color="grey"
-        style="font-size: 20px"
-      >
-        delete
-      </v-icon>
-    </v-btn>
   </v-layout>
 </template>
 
@@ -56,22 +45,13 @@
         type: String
       }
     },
-    data () {
-      return {
-        stageMap: {
-          'feedback-creation': 'create',
-          'feedback-validation': 'validate',
-          'feedback-conflict-resolution': 'conflict'
-        }
-      }
-    },
     computed: {
       name () {
         return this.$store.getters.resolveSubscriptionKeyToName(
           {query_key: this.query_key, query_type: this.query_type})
       },
       active () {
-        return this.available || this.assignments.length > 0
+        return !!this.available || this.assignments.length > 0
       },
       subscriptionRoute () {
         if (this.active) {
@@ -79,21 +59,6 @@
         }
         return this.$route.fullPath
       }
-    },
-    methods: {
-      deactivate () {
-        this.$store.dispatch('deactivateSubscription', {pk: this.pk}).then(() => {
-          if (this.$route.params.pk === this.pk) {
-            this.$router.push('/')
-          }
-        }).catch(err => {
-          this.$notify({
-            title: `Unable to deactivate subscription ${this.name}`,
-            text: err.msg,
-            type: 'error'
-          })
-        })
-      }
     }
   }
 </script>
diff --git a/frontend/src/components/subscriptions/SubscriptionList.vue b/frontend/src/components/subscriptions/SubscriptionList.vue
index 4089f6dc..808e0d31 100644
--- a/frontend/src/components/subscriptions/SubscriptionList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionList.vue
@@ -2,8 +2,8 @@
   <v-card>
     <v-toolbar color="teal" :dense="sidebar">
       <v-toolbar-side-icon><v-icon>assignment</v-icon></v-toolbar-side-icon>
-      <v-toolbar-title v-if="!sidebar || (sidebar && !sideBarCollapsed)">
-        Your subscriptions
+      <v-toolbar-title v-if="!sidebar || (sidebar && !sideBarCollapsed)" style="min-width: fit-content;">
+        Tasks
       </v-toolbar-title>
       <v-spacer/>
       <v-btn icon @click="getSubscriptions">
@@ -16,41 +16,28 @@
         />
       </v-btn>
     </v-toolbar>
-    <v-list :dense="sidebar" v-if="!sidebar || (sidebar && !sideBarCollapsed)">
-      <div v-for="item in subscriptionTypes" :key="item.type">
-        <subscription-type
-          v-bind="item"
-          :is-empty-subscription-type="subscriptions[item.type].length === 0"
-          :possible-subscription-keys="possibleKeys[item.type]"
-          @toggleExpand="item.expanded = !item.expanded"
-        >
-          <div v-if="examTypesLoaded && submissionTypesLoaded">
-            <div v-for="subscription in subscriptions[item.type]">
-              <subscription-for-list
-                v-if="subscription.assignments.length > 0 ||
-                !(subscription.deactivated || subscription.remaining === 0)"
-                :key="subscription.pk"
-                v-bind="subscription"
-              >
-              </subscription-for-list>
-            </div>
-          </div>
-        </subscription-type>
-      </div>
-    </v-list>
+    <v-tabs grow color="teal lighten-1" v-model="selectedStage">
+      <v-tab v-for="(item, i) in stagesReadable" :key="i">
+        {{item}}
+      </v-tab>
+      <v-tab-item v-for="(stage, i) in stages" :key="i">
+        <subscriptions-for-stage :stage="stage"/>
+      </v-tab-item>
+    </v-tabs>
   </v-card>
 </template>
 
+
 <script>
   import {mapGetters, mapActions, mapState} from 'vuex'
   import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation'
-  import SubscriptionType from '@/components/subscriptions/SubscriptionType'
   import SubscriptionForList from '@/components/subscriptions/SubscriptionForList'
+  import SubscriptionsForStage from '@/components/subscriptions/SubscriptionsForStage'
 
   export default {
     components: {
+      SubscriptionsForStage,
       SubscriptionForList,
-      SubscriptionType,
       SubscriptionCreation},
     name: 'subscription-list',
     props: {
@@ -61,48 +48,8 @@
     },
     data () {
       return {
-        subscriptionCreateMenu: {},
-        submissionTypesLoaded: false,
-        examTypesLoaded: false,
-        updating: false,
-        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,
-            createPermission: () => {
-              return this.$store.getters.isReviewer
-            },
-            viewPermission: () => {
-              return this.$store.getters.isReviewer
-            }
-          },
-          {
-            name: 'Submission Type',
-            type: 'submission_type',
-            description: 'Just submissions for the specified type.',
-            expanded: true
-          },
-          {
-            name: 'Student',
-            type: 'student',
-            description: 'Submissions of single students.',
-            expanded: true,
-            createPermission: () => {
-              return false
-            },
-            viewPermission: () => {
-              return this.$store.getters.isReviewer
-            }
-          }
-        ]
+        selectedStage: null,
+        updating: false
       }
     },
     computed: {
@@ -110,59 +57,32 @@
         sideBarCollapsed: state => state.ui.sideBarCollapsed
       }),
       ...mapGetters({
-        subscriptions: 'getSubscriptionsGroupedByType'
-      }),
-      possibleKeys () {
-        const submissionTypes = Object.entries(this.$store.state.submissionTypes).map(([id, type]) => {
-          return {text: type.name, key: type.pk}
-        })
-        const examTypes = Object.entries(this.$store.state.examTypes).map(([id, type]) => {
-          return {text: type['module_reference'], key: type.pk}
-        })
-        return {
-          submission_type: submissionTypes,
-          exam: examTypes
-        }
-      }
+        subscriptions: 'getSubscriptionsGroupedByType',
+        stages: 'availableStages',
+        stagesReadable: 'availableStagesReadable'
+      })
     },
     methods: {
       ...mapActions([
         'updateSubmissionTypes',
         'getCurrentAssignment',
-        'getExamTypes'
+        'getExamTypes',
+        'subscribeToAll',
+        'cleanAssignmentsFromSubscriptions'
       ]),
-      getSubscriptions () {
+      async getSubscriptions () {
         this.updating = true
-        this.$store.dispatch('getSubscriptions').then(() => {
-          this.getStudentNames()
-        }).finally(() => {
-          this.updating = false
-        })
-      },
-      getStudentNames () {
-        if (this.subscriptions.student.length > 0 && this.$store.getters.isReviewer) {
-          const studentPks = this.subscriptions.student.map(subscription => {
-            return subscription.query_key
-          }).filter(key => key)
-          this.$store.dispatch('getStudents', {studentPks, fields: ['name']})
-        }
+        const subscriptions = await this.$store.dispatch('getSubscriptions')
+        this.updating = false
+        return subscriptions
       }
     },
     created () {
-      if (Object.keys(this.$store.state.subscriptions).length === 0) {
-        this.getSubscriptions()
-      }
-      if (Object.keys(this.$store.state.submissionTypes).length === 0) {
-        this.updateSubmissionTypes().then(() => { this.submissionTypesLoaded = true })
-      } else {
-        this.submissionTypesLoaded = true
-      }
-      if (Object.keys(this.$store.state.examTypes).length === 0 &&
-        this.$store.getters.isReviewer) {
-        this.getExamTypes().then(() => { this.examTypesLoaded = true })
-      } else {
-        this.examTypesLoaded = true
-      }
+      const typesAndSubscriptions = [this.updateSubmissionTypes(), this.getSubscriptions()]
+      Promise.all(typesAndSubscriptions).then(() => {
+        this.subscribeToAll()
+        this.cleanAssignmentsFromSubscriptions()
+      })
     }
   }
 </script>
diff --git a/frontend/src/components/subscriptions/SubscriptionType.vue b/frontend/src/components/subscriptions/SubscriptionType.vue
deleted file mode 100644
index cb735799..00000000
--- a/frontend/src/components/subscriptions/SubscriptionType.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<template>
-  <div>
-    <v-list-tile v-if="viewPermission()">
-      <v-list-tile-content>
-        <v-list-tile-title>
-          {{ name }}
-        </v-list-tile-title>
-        <v-list-tile-sub-title>
-          {{ description }}
-        </v-list-tile-sub-title>
-      </v-list-tile-content>
-      <v-list-tile-action v-if="!isEmptySubscriptionType">
-        <v-btn icon @click="$emit('toggleExpand')">
-          <v-icon v-if="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="createPermission()"
-      >
-        <v-menu
-          offset-x
-          :min-width="500"
-          :close-on-content-click="false"
-          :nudge-width="200"
-          v-model="subscriptionCreateMenu"
-        >
-          <v-btn small flat icon slot="activator">
-            <v-icon>add</v-icon>
-          </v-btn>
-          <subscription-creation
-            :title="name"
-            :type="type"
-            :keyItems="possibleSubscriptionKeys"
-          />
-        </v-menu>
-      </v-list-tile-action>
-    </v-list-tile>
-    <slot v-if="expanded"></slot>
-  </div>
-</template>
-
-<script>
-  import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation'
-
-  export default {
-    components: {SubscriptionCreation},
-    name: 'subscription-type',
-    props: {
-      name: {
-        type: String,
-        required: true
-      },
-      type: {
-        type: String,
-        required: true
-      },
-      description: {
-        type: String
-      },
-      expanded: {
-        type: Boolean,
-        default: true
-      },
-      isEmptySubscriptionType: {
-        type: Boolean,
-        required: true
-      },
-      createPermission: {
-        type: Function,
-        default: () => true
-      },
-      viewPermission: {
-        type: Function,
-        default: () => true
-      },
-      possibleSubscriptionKeys: {
-        type: Array
-      }
-    },
-    data () {
-      return {
-        subscriptionCreateMenu: {}
-      }
-    }
-  }
-</script>
-
-<style scoped>
-
-</style>
diff --git a/frontend/src/components/subscriptions/SubscriptionsForStage.vue b/frontend/src/components/subscriptions/SubscriptionsForStage.vue
new file mode 100644
index 00000000..548c3ae2
--- /dev/null
+++ b/frontend/src/components/subscriptions/SubscriptionsForStage.vue
@@ -0,0 +1,42 @@
+<template>
+  <v-list :dense="dense">
+    <div>
+      <div v-for="subscription in subscriptions['submission_type']">
+        <subscription-for-list
+          :key="subscription.pk"
+          v-bind="subscription"
+        >
+        </subscription-for-list>
+      </div>
+    </div>
+  </v-list>
+</template>
+
+<script>
+  import SubscriptionForList from '@/components/subscriptions/SubscriptionForList'
+  export default {
+    components: {
+      SubscriptionForList
+    },
+    name: 'subscriptions-for-stage',
+    props: {
+      stage: {
+        type: String,
+        required: true
+      },
+      dense: {
+        type: Boolean,
+        default: false
+      }
+    },
+    computed: {
+      subscriptions () {
+        return this.$store.getters.getSubscriptionsGroupedByType[this.stage]
+      }
+    }
+  }
+</script>
+
+<style scoped>
+
+</style>
diff --git a/frontend/src/pages/PageNotFound.vue b/frontend/src/pages/PageNotFound.vue
index ba712e1f..83653ae8 100644
--- a/frontend/src/pages/PageNotFound.vue
+++ b/frontend/src/pages/PageNotFound.vue
@@ -1,7 +1,18 @@
 <template>
   <v-container fill-height>
     <v-layout align-center justify-center>
-      <h1>Ooops, something went wrong. There is nothing here.</h1>
+      <v-card dark width="80%" height="80%">
+        <v-card-title style="font-size: 350%">
+          The content you're requesting is not available in your country.
+        </v-card-title>
+        <v-divider class="px-5"/>
+        <v-flex xs10 offset-xs2>
+          <v-card-text class="no-content-text">
+            <v-icon size="200px" color="deep-orange accent-4">play_circle_outline</v-icon>
+            <span style="font-size: xx-large">We're sorry about that ¯\_(ツ)_/¯</span>
+          </v-card-text>
+        </v-flex>
+      </v-card>
     </v-layout>
   </v-container>
 </template>
@@ -13,5 +24,8 @@
 </script>
 
 <style scoped>
-
+  .no-content-text {
+    position: absolute;
+    top: 40%;
+  }
 </style>
diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue
index 0449d1a6..fca62801 100644
--- a/frontend/src/pages/SubscriptionWorkPage.vue
+++ b/frontend/src/pages/SubscriptionWorkPage.vue
@@ -2,19 +2,20 @@
   <v-layout
     row wrap
   >
-    <v-flex xs12 md6 class="sub-correction">
-      <submission-correction
-        :assignment="currentAssignment"
-        :key="subscription.pk"
-        @feedbackCreated="startWorkOnNextAssignment"
-        @skip="skipAssignment"
-        class="ma-4 autofocus"
-      />
-      <submission-tests
-        :tests="submission.tests"
-        :expand="true"
-        class="mx-4"
-      />
+    <v-flex xs12 md6>
+      <div class="sub-correction">
+        <submission-correction
+          :assignment="currentAssignment"
+          :key="subscription.pk"
+          @feedbackCreated="startWorkOnNextAssignment"
+          class="ma-4 autofocus"
+        />
+        <submission-tests
+          :tests="submission.tests"
+          :expand="true"
+          class="mx-4"
+        />
+      </div>
     </v-flex>
 
     <v-flex md6>
@@ -33,23 +34,14 @@
   import SubmissionCorrection from '@/components/submission_notes/SubmissionCorrection'
   import SubmissionType from '@/components/SubmissionType'
   import store from '@/store/store'
-  import {mut} from '@/store/mutations'
   import SubmissionTests from '@/components/SubmissionTests'
+  import { subscriptionMuts } from '@/store/modules/subscriptions'
 
   function onRouteEnterOrUpdate (to, from, next) {
     if (to.name === 'subscription') {
-      let subscription = store.state.subscriptions[to.params['pk']]
-      if (subscription['assignments'].length === 0) {
-        store.dispatch('getAssignmentForSubscription', {subscription}).then(() => {
-          next()
-        })
-        store.dispatch('getAssignmentForSubscription', {subscription})
-      } else if (subscription['assignments'].length === 1) {
-        store.dispatch('getAssignmentForSubscription', {subscription})
-        next()
-      } else {
+      store.dispatch('changeActiveSubscription', to.params['pk']).then(() => {
         next()
-      }
+      })
     }
   }
 
@@ -67,10 +59,10 @@
     },
     computed: {
       subscription () {
-        return this.$store.state.subscriptions[this.$route.params['pk']]
+        return this.$store.state.subscriptions.subscriptions[this.$route.params['pk']]
       },
       currentAssignment () {
-        return this.subscription['assignments'][0]
+        return this.$store.state.subscriptions.assignmentQueue[0]
       },
       submission () {
         return this.currentAssignment.submission
@@ -85,30 +77,19 @@
     beforeRouteUpdate (to, from, next) {
       onRouteEnterOrUpdate(to, from, next)
     },
+    beforeRouteLeave (to, from, next) {
+      if (to.name !== 'subscription') {
+        next()
+        this.$store.dispatch('removeActiveSubscription')
+      }
+    },
     methods: {
-      prefetchAssignment () {
-        this.$store.dispatch('getAssignmentForSubscription', {subscription: this.subscription}).catch(() => {
-          this.$notify({
-            title: 'Last submission here!',
-            text: 'This will be your last submission to correct for this subscription.',
-            type: 'warning'
-          })
-        })
-      },
       startWorkOnNextAssignment () {
-        this.$store.commit(mut.DELETE_ASSIGNMENT_FROM_SUBSCRIPTION_QUEUE, {
-          subscription: this.subscription
-        })
-        this.prefetchAssignment()
-      },
-      skipAssignment () {
-        this.$store.dispatch('deleteAssignment', {assignment: this.currentAssignment}).then(() => {
-          this.startWorkOnNextAssignment()
-        }).catch(err => {
-          this.$notify({
-            title: "Couldn't skip this submission",
-            text: err.msg,
-            type: 'error'
+        this.$store.dispatch('getAssignmentsForActiveSubscription', 1).then(([promise]) => {
+          promise.then(assignment => {
+            this.$store.commit(subscriptionMuts.ADD_ASSIGNMENT_TO_QUEUE, assignment)
+          }).finally(() => {
+            this.$store.commit(subscriptionMuts.POP_ASSIGNMENT_FROM_QUEUE)
           })
         })
       }
diff --git a/frontend/src/store/actions.js b/frontend/src/store/actions.js
index e9556598..9bf5e08d 100644
--- a/frontend/src/store/actions.js
+++ b/frontend/src/store/actions.js
@@ -1,60 +1,20 @@
-import {mut} from './mutations'
-import {authMut} from '@/store/modules/authentication'
-import {subNotesMut} from '@/store/modules/submission-notes'
+import { mut } from './mutations'
+import { authMut } from '@/store/modules/authentication'
+import { subNotesMut } from '@/store/modules/submission-notes'
 import * as api from '@/api'
 import router from '@/router/index'
-
-function handleError (err, dispatch, fallbackMsg) {
-  if (err.response) {
-    if (err.response.status === 401) {
-      dispatch('logout', "You've been logged out")
-    } else {
-      throw new Error(err.response.data)
-    }
-  } else {
-    if (fallbackMsg) {
-      throw new Error(fallbackMsg)
-    }
-  }
-}
+import { handleError } from '@/util/helpers'
 
 const actions = {
-  async getSubscriptions ({commit, dispatch}) {
-    try {
-      const subscriptions = await api.fetchSubscriptions()
-      commit(mut.SET_SUBSCRIPTIONS, subscriptions)
-      return subscriptions
-    } catch (err) {
-      handleError(err, dispatch, 'Unable to fetch subscriptions')
-    }
-  },
   async getExamTypes ({commit, dispatch}) {
     try {
       const examTypes = await api.fetchExamType({})
       commit(mut.SET_EXAM_TYPES, examTypes)
     } catch (err) {
-      handleError(err, dispatch, 'Unable to fetch exam mut')
-    }
-  },
-  async subscribeTo ({commit, dispatch}, {type, key, stage}) {
-    try {
-      const subscription = await api.subscribeTo(type, key, stage)
-      commit(mut.SET_SUBSCRIPTION, subscription)
-      return subscription
-    } catch (err) {
-      handleError(err, dispatch, 'Subscribing unsuccessful')
-    }
-  },
-  async deactivateSubscription ({commit, dispatch}, {subscription, pk}) {
-    try {
-      const subscriptionPk = subscription ? subscription.pk : pk
-      await api.deactivateSubscription({pk: subscriptionPk})
-      commit(mut.DELETE_SUBSCRIPTION, {pk: subscriptionPk})
-    } catch (err) {
-      handleError(err, dispatch, 'Unable to deactivate subscription')
+      handleError(err, dispatch, 'Unable to fetch exam types')
     }
   },
-  async updateSubmissionTypes ({commit, dispatch}, fields) {
+  async updateSubmissionTypes ({commit, dispatch}, fields = []) {
     try {
       const submissionTypes = await api.fetchSubmissionTypes(fields)
       submissionTypes.forEach(type => {
@@ -64,26 +24,6 @@ const actions = {
       handleError(err, dispatch, 'Unable to get submission types')
     }
   },
-  async getAssignmentForSubscription ({commit, state, dispatch}, {subscription}) {
-    if (subscription.assignments.length < 2) {
-      try {
-        const assignment = await api.createAssignment({subscription})
-        commit(mut.ADD_ASSIGNMENT_TO_SUBSCRIPTION_QUEUE, {assignment})
-        return assignment
-      } catch (err) {
-        handleError(err, dispatch, "Couldn't fetch assignment")
-      }
-    } else {
-      return subscription.assignments[0]
-    }
-  },
-  async deleteAssignment ({commit, state, dispatch}, {assignment}) {
-    try {
-      return await api.deleteAssignment({assignment})
-    } catch (err) {
-      handleError(err, dispatch, 'Unable to delete assignment')
-    }
-  },
   async getStudents ({commit, dispatch}, opt = {studentPks: [], fields: []}) {
     try {
       if (opt.studentPks.length === 0) {
@@ -162,10 +102,11 @@ const actions = {
     }
   },
   logout ({ commit }, message = '') {
-    commit(mut.RESET_STATE)
+    commit('RESET_STATE')
     commit('submissionNotes/' + subNotesMut.RESET_STATE)
     commit(authMut.SET_MESSAGE, message)
     router.push({name: 'login'})
+    router.go()
   }
 }
 
diff --git a/frontend/src/store/getters.js b/frontend/src/store/getters.js
index c44ef362..5663746c 100644
--- a/frontend/src/store/getters.js
+++ b/frontend/src/store/getters.js
@@ -1,51 +1,9 @@
 const getters = {
-  getSubscriptionsGroupedByType (state, getters) {
-    let subscriptions = {
-      'random': [],
-      'student': [],
-      'exam': [],
-      'submission_type': []
-    }
-    Object.entries(state.subscriptions).forEach(([id, subscription]) => {
-      subscriptions[subscription.query_type].push(subscription)
-    })
-    // sort the resulting arrays in subscriptions lexicographically by their query_keys
-    Object.entries(subscriptions).forEach(([id, arr]) => {
-      if (arr.length > 1 && arr[0].hasOwnProperty('query_key')) {
-        arr.sort((subA, subB) => {
-          const subALower = getters.resolveSubscriptionKeyToName(subA).toLowerCase()
-          const subBLower = getters.resolveSubscriptionKeyToName(subB).toLowerCase()
-          if (subALower < subBLower) {
-            return -1
-          } else if (subALower > subBLower) {
-            return 1
-          } else {
-            return 0
-          }
-        })
-      }
-    })
-    return subscriptions
-  },
   corrected (state) {
     return state.statistics.submission_type_progress.every(progress => {
       return progress.percentage === 100
     })
   },
-  resolveSubscriptionKeyToName: state => subscription => {
-    if (subscription.query_type === 'random') {
-      return 'Active'
-    } else if (subscription.query_type === 'exam') {
-      const examType = state.examTypes[subscription.query_key]
-      return examType ? examType.module_reference : 'Exam'
-    } else if (subscription.query_type === 'submission_type') {
-      const submissionType = state.submissionTypes[subscription.query_key]
-      return submissionType ? submissionType.name : 'Submission Type'
-    } else if (subscription.query_type === 'student') {
-      const studentName = state.students[subscription.query_key]
-      return studentName ? studentName.name : 'Student'
-    }
-  },
   getSubmission: state => pk => {
     return state.submissions[pk]
   },
diff --git a/frontend/src/store/modules/subscriptions.js b/frontend/src/store/modules/subscriptions.js
new file mode 100644
index 00000000..531ea809
--- /dev/null
+++ b/frontend/src/store/modules/subscriptions.js
@@ -0,0 +1,274 @@
+import Vue from 'vue'
+import * as api from '@/api'
+import {handleError, flatten, cartesian, once} from '@/util/helpers'
+
+export const subscriptionMuts = Object.freeze({
+  SET_SUBSCRIPTIONS: 'SET_SUBSCRIPTIONS',
+  SET_SUBSCRIPTION: 'SET_SUBSCRIPTION',
+  SET_LOADING: 'SET_LOADING',
+  SET_ACTIVE_SUBSCRIPTION_PK: 'SET_ACTIVE_SUBSCRIPTION_PK',
+  SET_ASSIGNMENT_QUEUE: 'SET_ASSIGNMENT_QUEUE',
+  ADD_ASSIGNMENT_TO_QUEUE: 'ADD_ASSIGNMENT_TO_QUEUE',
+  POP_ASSIGNMENT_FROM_QUEUE: 'POP_ASSIGNMENT_FROM_QUEUE',
+  RESET_STATE: 'RESET_STATE'
+})
+
+function initialState () {
+  return {
+    subscriptions: {},
+    assignmentQueue: [],
+    activeSubscriptionPk: '',
+    loading: false
+  }
+}
+
+const MAX_NUMBER_OF_ASSIGNMENTS = 2
+
+// noinspection JSCommentMatchesSignature
+const subscriptions = {
+  state: initialState(),
+  getters: {
+    availableTypes (state, getters) {
+      let types = ['random', 'submission_type']
+      if (getters.isReviewer) {
+        types.push('exam')
+      }
+      return types
+    },
+    availableStages (state, getters) {
+      let stages = ['feedback-creation', 'feedback-validation']
+      if (getters.isReviewer) {
+        stages.push('feedback-conflict-resolution')
+      }
+      return stages
+    },
+    availableStagesReadable (state, getters) {
+      let stages = ['create', 'validate']
+      if (getters.isReviewer) {
+        stages.push('resolve')
+      }
+      return stages
+    },
+    availableSubmissionTypeQueryKeys (state, getters, rootState) {
+      return Object.values(rootState.submissionTypes).map(subType => subType.pk)
+    },
+    availableExamTypeQueryKeys (state, getters, rootState) {
+      return Object.values(rootState.examTypes).map(examType => examType.pk)
+    },
+    activeSubscription (state) {
+      return state.subscriptions[state.activeSubscriptionPk]
+    },
+    resolveSubscriptionKeyToName: (state, getters, rootState) => subscription => {
+      switch (subscription.query_type) {
+        case 'random':
+          return 'Active'
+        case 'exam':
+          const examType = rootState.examTypes[subscription.query_key]
+          return examType ? examType.module_reference : 'Exam'
+        case 'submission_type':
+          const submissionType = rootState.submissionTypes[subscription.query_key]
+          return submissionType ? submissionType.name : 'Submission Type'
+        case 'student':
+          const studentName = rootState.students[subscription.query_key]
+          return studentName ? studentName.name : 'Student'
+      }
+    },
+    getSubscriptionsGroupedByType (state, getters) {
+      const subscriptionsByType = () => {
+        return {
+          'random': [],
+          'student': [],
+          'exam': [],
+          'submission_type': []
+        }
+      }
+      let subscriptionsByStage = getters.availableStages.reduce((acc, curr) => {
+        acc[curr] = subscriptionsByType()
+        return acc
+      }, {})
+      Object.values(state.subscriptions).forEach(subscription => {
+        subscriptionsByStage[subscription.feedback_stage][subscription.query_type].push(subscription)
+      })
+      // sort the resulting arrays in subscriptions lexicographically by their query_keys
+      const sortSubscriptions = subscriptionsByType => Object.values(subscriptionsByType).forEach(arr => {
+        if (arr.length > 1 && arr[0].hasOwnProperty('query_key')) {
+          arr.sort((subA, subB) => {
+            const subALower = getters.resolveSubscriptionKeyToName(subA).toLowerCase()
+            const subBLower = getters.resolveSubscriptionKeyToName(subB).toLowerCase()
+            if (subALower < subBLower) {
+              return -1
+            } else if (subALower > subBLower) {
+              return 1
+            } else {
+              return 0
+            }
+          })
+        }
+      })
+      Object.values(subscriptionsByStage).forEach(subscriptionsByType => {
+        sortSubscriptions(subscriptionsByType)
+      })
+      return subscriptionsByStage
+    }
+  },
+  mutations: {
+    [subscriptionMuts.SET_SUBSCRIPTIONS] (state, subscriptions) {
+      state.subscriptions = subscriptions.reduce((acc, curr) => {
+        acc[curr['pk']] = curr
+        return acc
+      }, {})
+    },
+    [subscriptionMuts.SET_SUBSCRIPTION] (state, subscription) {
+      Vue.set(state.subscriptions, subscription.pk, subscription)
+    },
+    [subscriptionMuts.SET_ACTIVE_SUBSCRIPTION_PK] (state, subscriptionPk) {
+      state.activeSubscriptionPk = subscriptionPk
+    },
+    [subscriptionMuts.SET_ASSIGNMENT_QUEUE] (state, queue) {
+      state.assignmentQueue = queue
+    },
+    [subscriptionMuts.ADD_ASSIGNMENT_TO_QUEUE] (state, assignment) {
+      state.assignmentQueue.push(assignment)
+    },
+    [subscriptionMuts.POP_ASSIGNMENT_FROM_QUEUE] (state) {
+      state.assignmentQueue.shift()
+    },
+    [subscriptionMuts.RESET_STATE] (state) {
+      Object.assign(state, initialState())
+    }
+  },
+  actions: {
+    subscribeTo: async function ({commit, dispatch, getters}, {type, key, stage}) {
+      try {
+        // don't subscribe to type, key, stage combinations if they're already present
+        let subscription = getters.getSubscriptionsGroupedByType[stage][type].find(elem => {
+          if (type === 'random') {
+            return true
+          }
+          return elem.query_key === key
+        })
+        subscription = subscription || await api.subscribeTo(type, key, stage)
+        commit(subscriptionMuts.SET_SUBSCRIPTION, subscription)
+        return subscription
+      } catch (err) {
+        console.log(err)
+        handleError(err, dispatch, 'Subscribing unsuccessful')
+      }
+    },
+    async getSubscriptions ({commit, dispatch}) {
+      try {
+        const subscriptions = await api.fetchSubscriptions()
+        commit(subscriptionMuts.SET_SUBSCRIPTIONS, subscriptions)
+        return subscriptions
+      } catch (err) {
+        console.log(err)
+        handleError(err, dispatch, 'Unable to fetch subscriptions')
+      }
+    },
+    /**
+     * Creates as many assignments as needed to reach MAX_NUMBER_OF_ASSIGNMENTS
+     * @param numOfAssignments Use to override default behaviour of creating MAX_NUMBER_OF_ASSIGNMENTS - assignmentQueue.length assignments
+     * @returns {Promise<[Promise]>} returns Promise of Array of Promises of assignments
+     */
+    async getAssignmentsForActiveSubscription ({commit, state, dispatch, getters}, numOfAssignments) {
+      numOfAssignments = numOfAssignments || MAX_NUMBER_OF_ASSIGNMENTS - state.assignmentQueue.length
+      let assignmentsPromises = []
+      for (let i = 0; i < numOfAssignments; i++) {
+        assignmentsPromises.push(api.createAssignment({subscription: getters.activeSubscription}))
+      }
+      return assignmentsPromises
+    },
+    async deleteAssignment ({commit, state, dispatch}, assignment) {
+      try {
+        return await api.deleteAssignment({assignment})
+      } catch (err) {
+        handleError(err, dispatch, 'Unable to delete assignment')
+      }
+    },
+    async cleanAssignmentsFromSubscriptions ({commit, state, dispatch}, excludeActive = true) {
+      Object.values(state.subscriptions).forEach(subscription => {
+        if (!excludeActive || subscription.pk !== state.activeSubscriptionPk) {
+          subscription.assignments.forEach(assignment => {
+            api.deleteAssignment({assignment})
+          })
+        }
+      })
+    },
+    async skipAssignment ({commit, state, dispatch}) {
+      try {
+        dispatch('deleteAssignment', state.assignmentQueue[0])
+          .then(() => {
+            // pass numOfAssignments = 1 to create 1 new assignment although maybe two are already in the queue,
+            // this is needed because otherwise the current assignment in the comp. might be unknown for a period
+            // which will result get incorrectly interpreted as a an ended subscription
+            return dispatch('getAssignmentsForActiveSubscription', 1)
+          }).then(([promise]) => {
+            promise.then(assignment => {
+              commit(subscriptionMuts.ADD_ASSIGNMENT_TO_QUEUE, assignment)
+              commit(subscriptionMuts.POP_ASSIGNMENT_FROM_QUEUE)
+            })
+          })
+      } catch (err) {
+        handleError(err, dispatch, 'Unable to skip assignment')
+      }
+    },
+    async deleteActiveAssignments ({commit, state, dispatch}) {
+      try {
+        Promise.all(state.assignmentQueue.map(assignment => {
+          return dispatch('deleteAssignment', assignment)
+        }))
+      } catch (err) {
+        handleError(err, dispatch, 'Unable to remove assignments.')
+      }
+    },
+    async changeActiveSubscription ({commit, state, dispatch}, subscriptionPk = '') {
+      if (subscriptionPk !== state.activeSubscriptionPk) {
+        await dispatch('deleteActiveAssignments')
+        commit(subscriptionMuts.SET_ACTIVE_SUBSCRIPTION_PK, subscriptionPk)
+        let assignmentsPromises = await dispatch('getAssignmentsForActiveSubscription', MAX_NUMBER_OF_ASSIGNMENTS)
+        let createdAssignments = []
+        for (let promise of assignmentsPromises) {
+          try {
+            createdAssignments.push(await promise)
+          } catch (_) {}
+        }
+        commit(subscriptionMuts.SET_ASSIGNMENT_QUEUE, createdAssignments)
+      }
+    },
+    async removeActiveSubscription ({commit, state, dispatch}) {
+      await dispatch('deleteActiveAssignments')
+      commit(subscriptionMuts.SET_ASSIGNMENT_QUEUE, [])
+      commit(subscriptionMuts.SET_ACTIVE_SUBSCRIPTION_PK, '')
+    },
+    async subscribeToType ({commit, state, dispatch, getters}, type) {
+      switch (type) {
+        case 'random':
+          return getters.availableStages.map(stage => {
+            dispatch('subscribeTo', {type, stage})
+          })
+        case 'exam':
+          if (getters.isReviewer) {
+            const stageKeyCartesian = cartesian(
+              getters.availableStages, getters.availableExamTypeQueryKeys)
+            return stageKeyCartesian.map(([stage, key]) => {
+              dispatch('subscribeTo', {stage, type, key})
+            })
+          }
+          return []
+        case 'submission_type':
+          const stageKeyCartesian = cartesian(
+            getters.availableStages, getters.availableSubmissionTypeQueryKeys)
+          return stageKeyCartesian.map(([stage, key]) => {
+            dispatch('subscribeTo', {stage, type, key})
+          })
+      }
+    },
+    subscribeToAll: once(async ({commit, state, dispatch, getters}) => {
+      return Promise.all(flatten(getters.availableTypes.map(type => {
+        return dispatch('subscribeToType', type)
+      })))
+    })
+  }
+}
+
+export default subscriptions
diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.js
index 60a963a8..2e4c4081 100644
--- a/frontend/src/store/mutations.js
+++ b/frontend/src/store/mutations.js
@@ -3,12 +3,6 @@ import Vue from 'vue'
 import {initialState} from '@/store/store'
 
 export const mut = Object.freeze({
-  ADD_ASSIGNMENT_TO_SUBSCRIPTION_QUEUE: 'ADD_ASSIGNMENT_TO_SUBSCRIPTION_QUEUE',
-  DELETE_ASSIGNMENT_FROM_SUBSCRIPTION_QUEUE: 'DELETE_ASSIGNMENT_FROM_SUBSCRIPTION_QUEUE',
-  SET_ASSIGNMENT: 'SET_ASSIGNMENT',
-  SET_SUBSCRIPTIONS: 'SET_SUBSCRIPTIONS',
-  SET_SUBSCRIPTION: 'SET_SUBSCRIPTION',
-  DELETE_SUBSCRIPTION: 'DELETE_SUBSCRIPTION',
   SET_LAST_INTERACTION: 'SET_LAST_INTERACTION',
   SET_EXAM_TYPES: 'SET_EXAM_TYPES',
   SET_STUDENTS: 'SET_STUDENTS',
@@ -32,9 +26,6 @@ const mutations = {
       return acc
     }, {})
   },
-  [mut.SET_ASSIGNMENT] (state, assignment) {
-    Vue.set(state.assignments, assignment.pk, assignment)
-  },
   [mut.SET_SUBSCRIPTIONS] (state, subscriptions) {
     state.subscriptions = subscriptions.reduce((acc, curr) => {
       acc[curr['pk']] = curr
@@ -56,9 +47,6 @@ const mutations = {
       }
     }
   },
-  [mut.DELETE_SUBSCRIPTION] (state, {pk}) {
-    Vue.delete(state.subscriptions, pk)
-  },
   [mut.SET_STUDENTS] (state, students) {
     state.students = students.reduce((acc, curr) => {
       acc[curr.pk] = mapStudent(curr, state.studentMap)
@@ -117,14 +105,6 @@ const mutations = {
   [mut.UPDATE_SUBMISSION] (state, {submissionPk, payload, key}) {
     Vue.set(state.submissions[submissionPk], key, payload)
   },
-  [mut.ADD_ASSIGNMENT_TO_SUBSCRIPTION_QUEUE] (state, {assignment}) {
-    let subscription = state.subscriptions[assignment.subscription]
-    subscription['assignments'].push(assignment)
-  },
-  [mut.DELETE_ASSIGNMENT_FROM_SUBSCRIPTION_QUEUE] (state, {subscription}) {
-    subscription.assignments.shift()
-  },
-
   [mut.SET_LAST_INTERACTION] (state) {
     state.lastAppInteraction = Date.now()
   },
diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js
index 2d0b8af1..588aa4e2 100644
--- a/frontend/src/store/store.js
+++ b/frontend/src/store/store.js
@@ -6,6 +6,7 @@ import studentPage from './modules/student-page'
 import submissionNotes from './modules/submission-notes'
 import authentication from './modules/authentication'
 import ui from './modules/ui'
+import subscriptions from './modules/subscriptions'
 import feedbackSearchOptions from './modules/feedback_list/feedback-search-options'
 
 import actions from './actions'
@@ -22,8 +23,6 @@ export function initialState () {
     submissionTypes: {},
     submissions: {},
     feedback: {},
-    subscriptions: {},
-    assignments: {},
     students: {},
     studentMap: {},  // is used to map obfuscated student data back to the original
     statistics: {
@@ -42,6 +41,7 @@ const store = new Vuex.Store({
     studentPage,
     submissionNotes,
     ui,
+    subscriptions,
     feedbackSearchOptions
   },
   plugins: [
@@ -51,8 +51,8 @@ const store = new Vuex.Store({
       // authentication.token is manually saved since using it with this plugin caused issues
       // when manually reloading the page
       paths: Object.keys(initialState()).concat(
-        ['ui', 'studentPage', 'submissionNotes', 'authentication.username',
-          'authentication.userRole', 'authentication.jwtTimeDelta',
+        ['ui', 'studentPage', 'submissionNotes', 'feedbackSearchOptions', 'subscriptions',
+          'authentication.username', 'authentication.userRole', 'authentication.jwtTimeDelta',
           'authentication.tokenCreationTime'])
     }),
     lastInteraction],
diff --git a/frontend/src/util/helpers.js b/frontend/src/util/helpers.js
index 221bf042..f25491c1 100644
--- a/frontend/src/util/helpers.js
+++ b/frontend/src/util/helpers.js
@@ -56,3 +56,41 @@ export function mapStateToComputedGetterSetter ({namespace = '', pathPrefix = ''
     return acc
   }, {})
 }
+
+// thanks to rsp
+// https://stackoverflow.com/questions/12303989/cartesian-product-of-multiple-arrays-in-javascript/43053803#43053803
+let cartesianHelper = (a, b) => [].concat(...a.map(a => b.map(b => [].concat(a, b))))
+export function cartesian (a, b, ...c) {
+  return b ? cartesian(cartesianHelper(a, b), ...c) : a
+}
+
+// flatten an array
+export function flatten (list) {
+  return list.reduce(
+    (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
+  )
+}
+
+export function once (fn, context) {
+  let result
+  return function () {
+    if (!result) {
+      result = fn.apply(context || this, arguments)
+    }
+    return result
+  }
+}
+
+export function handleError (err, dispatch, fallbackMsg) {
+  if (err.response) {
+    if (err.response.status === 401) {
+      dispatch('logout', 'You\'ve been logged out')
+    } else {
+      throw err
+    }
+  } else {
+    if (fallbackMsg) {
+      throw new Error(fallbackMsg)
+    }
+  }
+}
-- 
GitLab


From fd4d81943d56b6cdbfa5b92a5bad3837d562ba3f Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Thu, 5 Apr 2018 01:39:13 +0200
Subject: [PATCH 2/6] Desc. and Solution are now always open by default

---
 frontend/src/pages/StudentSubmissionSideView.vue | 2 +-
 frontend/src/pages/SubscriptionWorkPage.vue      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/frontend/src/pages/StudentSubmissionSideView.vue b/frontend/src/pages/StudentSubmissionSideView.vue
index 6085f04d..acabb60d 100644
--- a/frontend/src/pages/StudentSubmissionSideView.vue
+++ b/frontend/src/pages/StudentSubmissionSideView.vue
@@ -13,7 +13,7 @@
       v-bind="submissionType"
       :key="submissionType.pk"
       :reverse="true"
-      :expandedByDefault="{ Description: false, Solution: false }"
+      :expandedByDefault="{ Description: true, Solution: true }"
       class="mt-1"
     />
   </div>
diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue
index fca62801..96d555ff 100644
--- a/frontend/src/pages/SubscriptionWorkPage.vue
+++ b/frontend/src/pages/SubscriptionWorkPage.vue
@@ -23,7 +23,7 @@
         v-bind="submissionType"
         :key="submissionType.pk"
         :reverse="true"
-        :expandedByDefault="{ Description: false, Solution: true }"
+        :expandedByDefault="{ Description: true, Solution: true }"
         class="mt-4 mr-4"
       />
     </v-flex>
-- 
GitLab


From 35babfa674d3bad8b3aa3c4c5bbc5231c77dbf7c Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Thu, 5 Apr 2018 03:46:31 +0200
Subject: [PATCH 3/6] Implemented RouteChangeConfirmation when unsaved feedback

---
 .../RouteChangeConfirmation.vue               | 57 +++++++++++++++++++
 .../submission_notes/SubmissionCorrection.vue |  9 ++-
 .../submission_notes/base/FeedbackComment.vue |  2 +-
 .../src/pages/StudentSubmissionSideView.vue   | 17 +++++-
 frontend/src/pages/SubscriptionWorkPage.vue   | 16 ++++--
 frontend/src/router/index.js                  |  1 +
 .../src/store/modules/submission-notes.js     | 13 +++--
 7 files changed, 101 insertions(+), 14 deletions(-)
 create mode 100644 frontend/src/components/submission_notes/RouteChangeConfirmation.vue

diff --git a/frontend/src/components/submission_notes/RouteChangeConfirmation.vue b/frontend/src/components/submission_notes/RouteChangeConfirmation.vue
new file mode 100644
index 00000000..3df4f48c
--- /dev/null
+++ b/frontend/src/components/submission_notes/RouteChangeConfirmation.vue
@@ -0,0 +1,57 @@
+<template>
+    <v-dialog
+      v-model="dialog"
+      max-width="fit-content"
+    >
+      <v-card>
+        <v-card-title class="title">
+          Are you sure?
+        </v-card-title>
+        <v-card-text>
+          Not submitted feedback will be lost!
+        </v-card-text>
+        <v-card-actions>
+          <v-btn flat outline color="red lighten-1" @click="changeRoute">Change page</v-btn>
+          <v-btn flat outline @click="dialog = false">Stay here</v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+</template>
+
+<script>
+  export default {
+    name: 'route-change-confirmation',
+    props: {
+      nextRoute: {
+        type: Function,
+        default: null
+      }
+    },
+    data () {
+      return {
+        dialog: false
+      }
+    },
+    methods: {
+      changeRoute () {
+        this.nextRoute()
+        this.dialog = false
+      }
+    },
+    watch: {
+      nextRoute (newVal, oldVal) {
+        if (newVal !== oldVal && this.$store.getters['submissionNotes/workInProgress']) {
+          console.log('here')
+          this.dialog = true
+        } else {
+          console.log('there')
+          this.nextRoute()
+        }
+      }
+    }
+  }
+</script>
+
+<style scoped>
+
+</style>
diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue
index 376a73f8..34e72856 100644
--- a/frontend/src/components/submission_notes/SubmissionCorrection.vue
+++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue
@@ -65,9 +65,11 @@
   import BaseAnnotatedSubmission from '@/components/submission_notes/base/BaseAnnotatedSubmission'
   import SubmissionLine from '@/components/submission_notes/base/SubmissionLine'
   import {subNotesMut, subNotesNamespace} from '@/store/modules/submission-notes'
+  import RouteChangeConfirmation from '@/components/submission_notes/RouteChangeConfirmation'
 
   export default {
     components: {
+      RouteChangeConfirmation,
       SubmissionLine,
       BaseAnnotatedSubmission,
       AnnotatedSubmissionBottomToolbar,
@@ -78,7 +80,7 @@
     data () {
       return {
         loading: false,
-        feedbackShortPollInterval: undefined
+        feedbackShortPollInterval: null
       }
     },
     props: {
@@ -108,7 +110,8 @@
         'isReviewer',
         'getSubmission',
         'getFeedback',
-        'getSubmissionType'
+        'getSubmissionType',
+        'workInProgress'
       ]),
       submission () {
         return this.$store.getters['submissionNotes/submission']
@@ -158,7 +161,7 @@
       }
     },
     watch: {
-      assignment: function () {
+      assignment: function (newVar, oldVar) {
         this.init()
       },
       submissionWithoutAssignment: function () {
diff --git a/frontend/src/components/submission_notes/base/FeedbackComment.vue b/frontend/src/components/submission_notes/base/FeedbackComment.vue
index af3a997f..bfd4b946 100644
--- a/frontend/src/components/submission_notes/base/FeedbackComment.vue
+++ b/frontend/src/components/submission_notes/base/FeedbackComment.vue
@@ -74,7 +74,7 @@
     },
     computed: {
       ...mapState({
-        markedForDeletion: state => state.submissionNotes.commentsMarkedForDeletetion,
+        markedForDeletion: state => state.submissionNotes.commentsMarkedForDeletion,
         darkMode: state => state.ui.darkMode
       }),
       parsedCreated () {
diff --git a/frontend/src/pages/StudentSubmissionSideView.vue b/frontend/src/pages/StudentSubmissionSideView.vue
index acabb60d..505ee8f8 100644
--- a/frontend/src/pages/StudentSubmissionSideView.vue
+++ b/frontend/src/pages/StudentSubmissionSideView.vue
@@ -1,10 +1,11 @@
 <template>
   <div>
+    <route-change-confirmation :next-route="nextRoute"/>
     <submission-correction
       :submission-without-assignment="submission"
       :feedback="submission.feedback"
       @feedbackCreated="refresh"
-    ></submission-correction>
+    />
     <submission-tests
       :tests="submission.tests"
       class="mt-4"
@@ -25,6 +26,7 @@
   import SubmissionCorrection from '@/components/submission_notes/SubmissionCorrection'
   import SubmissionTests from '@/components/SubmissionTests'
   import SubmissionType from '@/components/SubmissionType'
+  import RouteChangeConfirmation from '@/components/submission_notes/RouteChangeConfirmation'
 
   function onRouteEnterOrUpdate (to, from, next) {
     const toIsSubmissionSideView = to.matched.some(route => route.meta.submissionSideView)
@@ -53,10 +55,16 @@
 
   export default {
     components: {
+      RouteChangeConfirmation,
       SubmissionType,
       SubmissionTests,
       SubmissionCorrection},
     name: 'student-submission-side-view',
+    data () {
+      return {
+        nextRoute: null
+      }
+    },
     computed: {
       submissionPk () {
         return this.$route.params['submissionPk']
@@ -82,7 +90,12 @@
       onRouteEnterOrUpdate(to, from, next)
     },
     beforeRouteUpdate (to, from, next) {
-      onRouteEnterOrUpdate(to, from, next)
+      this.nextRoute = () => {
+        onRouteEnterOrUpdate(to, from, next)
+      }
+    },
+    beforeRouteLeave (to, from, next) {
+      this.nextRoute = next
     }
   }
 </script>
diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue
index 96d555ff..9e472cd0 100644
--- a/frontend/src/pages/SubscriptionWorkPage.vue
+++ b/frontend/src/pages/SubscriptionWorkPage.vue
@@ -2,6 +2,7 @@
   <v-layout
     row wrap
   >
+    <route-change-confirmation :next-route="nextRoute"/>
     <v-flex xs12 md6>
       <div class="sub-correction">
         <submission-correction
@@ -36,6 +37,7 @@
   import store from '@/store/store'
   import SubmissionTests from '@/components/SubmissionTests'
   import { subscriptionMuts } from '@/store/modules/subscriptions'
+  import RouteChangeConfirmation from '@/components/submission_notes/RouteChangeConfirmation'
 
   function onRouteEnterOrUpdate (to, from, next) {
     if (to.name === 'subscription') {
@@ -47,6 +49,7 @@
 
   export default {
     components: {
+      RouteChangeConfirmation,
       SubmissionTests,
       SubmissionType,
       SubmissionCorrection
@@ -54,7 +57,8 @@
     name: 'subscription-work-page',
     data () {
       return {
-        subscriptionActive: true
+        subscriptionActive: true,
+        nextRoute: null
       }
     },
     computed: {
@@ -75,12 +79,15 @@
       onRouteEnterOrUpdate(to, from, next)
     },
     beforeRouteUpdate (to, from, next) {
-      onRouteEnterOrUpdate(to, from, next)
+      this.nextRoute = () => {
+        onRouteEnterOrUpdate(to, from, next)
+      }
     },
     beforeRouteLeave (to, from, next) {
-      if (to.name !== 'subscription') {
+      if (to.name === 'subscription-ended') {
         next()
-        this.$store.dispatch('removeActiveSubscription')
+      } else {
+        this.nextRoute = next
       }
     },
     methods: {
@@ -98,6 +105,7 @@
       currentAssignment (val) {
         if (val === undefined) {
           this.$router.replace('ended')
+          this.$store.dispatch('removeActiveSubscription')
           this.$store.dispatch('getSubscriptions')
         }
       }
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index f4af722d..15701cfe 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -80,6 +80,7 @@ const router = new Router({
         },
         {
           path: 'subscription/ended',
+          name: 'subscription-ended',
           component: SubscriptionEnded
         },
         {
diff --git a/frontend/src/store/modules/submission-notes.js b/frontend/src/store/modules/submission-notes.js
index 82008d4f..08669ce0 100644
--- a/frontend/src/store/modules/submission-notes.js
+++ b/frontend/src/store/modules/submission-notes.js
@@ -41,7 +41,7 @@ function initialState () {
       score: null,
       feedback_lines: {}
     },
-    commentsMarkedForDeletetion: {}
+    commentsMarkedForDeletion: {}
   }
 }
 
@@ -69,6 +69,11 @@ const submissionNotes = {
     score: state => {
       return state.updatedFeedback.score !== null ? state.updatedFeedback.score : state.origFeedback.score
     },
+    workInProgress: state => {
+      const openEditor = Object.values(state.ui.showEditorOnLine).reduce((acc, curr) => acc || curr, false)
+      const feedbackWritten = Object.entries(state.updatedFeedback.feedback_lines).length > 0
+      return openEditor || feedbackWritten
+    },
     isFeedbackCreation: state => {
       return !state.origFeedback['feedback_stage_for_user'] ||
         state.origFeedback['feedback_stage_for_user'] === 'feedback-creation'
@@ -101,10 +106,10 @@ const submissionNotes = {
       Vue.set(state.ui.showEditorOnLine, lineNo, !state.ui.showEditorOnLine[lineNo])
     },
     [subNotesMut.MARK_COMMENT_FOR_DELETION]: function (state, comment) {
-      Vue.set(state.commentsMarkedForDeletetion, comment.pk, comment)
+      Vue.set(state.commentsMarkedForDeletion, comment.pk, comment)
     },
     [subNotesMut.UN_MARK_COMMENT_FOR_DELETION]: function (state, comment) {
-      Vue.delete(state.commentsMarkedForDeletetion, comment.pk)
+      Vue.delete(state.commentsMarkedForDeletion, comment.pk)
     },
     [subNotesMut.RESET_UPDATED_FEEDBACK]: function (state) {
       state.updatedFeedback = initialState().updatedFeedback
@@ -116,7 +121,7 @@ const submissionNotes = {
   actions: {
     deleteComments: async function ({state}) {
       return Promise.all(
-        Object.values(state.commentsMarkedForDeletetion).map(comment => {
+        Object.values(state.commentsMarkedForDeletion).map(comment => {
           return api.deleteComment(comment)
         })
       )
-- 
GitLab


From 9b7e5fca71e23635b3f81fd6f68cd3163093f67c Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Thu, 5 Apr 2018 03:52:13 +0200
Subject: [PATCH 4/6] Fixed double visible to student icon bug

---
 .../src/components/submission_notes/SubmissionCorrection.vue     | 1 +
 1 file changed, 1 insertion(+)

diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue
index 34e72856..49cdd72f 100644
--- a/frontend/src/components/submission_notes/SubmissionCorrection.vue
+++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue
@@ -17,6 +17,7 @@
                 <feedback-comment
                   v-for="(comment, index) in origFeedback[lineNo]"
                   v-bind="comment"
+                  :visible_to_student="updatedFeedback[lineNo] ? false : comment.visible_to_student"
                   :line-no="lineNo"
                   :key="index"
                   :deletable="comment.of_tutor === user || isReviewer"
-- 
GitLab


From cfa815dd88e6a0574d945b98344b60b1f06ff97f Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Thu, 5 Apr 2018 04:14:51 +0200
Subject: [PATCH 5/6] Scrolling to top on correction page when submitting

---
 frontend/src/pages/SubscriptionWorkPage.vue | 41 +++++++++++----------
 1 file changed, 21 insertions(+), 20 deletions(-)

diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue
index 9e472cd0..940be057 100644
--- a/frontend/src/pages/SubscriptionWorkPage.vue
+++ b/frontend/src/pages/SubscriptionWorkPage.vue
@@ -4,29 +4,29 @@
   >
     <route-change-confirmation :next-route="nextRoute"/>
     <v-flex xs12 md6>
-      <div class="sub-correction">
-        <submission-correction
-          :assignment="currentAssignment"
-          :key="subscription.pk"
-          @feedbackCreated="startWorkOnNextAssignment"
-          class="ma-4 autofocus"
-        />
-        <submission-tests
-          :tests="submission.tests"
-          :expand="true"
-          class="mx-4"
-        />
-      </div>
+      <submission-correction
+        :assignment="currentAssignment"
+        :key="subscription.pk"
+        @feedbackCreated="startWorkOnNextAssignment"
+        class="ma-4 autofocus"
+      />
+      <submission-tests
+        :tests="submission.tests"
+        :expand="true"
+        class="mx-4"
+      />
     </v-flex>
 
     <v-flex md6>
-      <submission-type
-        v-bind="submissionType"
-        :key="submissionType.pk"
-        :reverse="true"
-        :expandedByDefault="{ Description: true, Solution: true }"
-        class="mt-4 mr-4"
-      />
+      <div class="sub-correction">
+        <submission-type
+          v-bind="submissionType"
+          :key="submissionType.pk"
+          :reverse="true"
+          :expandedByDefault="{ Description: true, Solution: true }"
+          class="mt-4 mr-4"
+        />
+      </div>
     </v-flex>
   </v-layout>
 </template>
@@ -103,6 +103,7 @@
     },
     watch: {
       currentAssignment (val) {
+        this.$vuetify.goTo(0, {duration: 200, easing: 'easeInOutCubic'})
         if (val === undefined) {
           this.$router.replace('ended')
           this.$store.dispatch('removeActiveSubscription')
-- 
GitLab


From e0ac3842d5ca3e03dc1d14d51b14a89b94902cc9 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Thu, 5 Apr 2018 12:24:31 +0200
Subject: [PATCH 6/6] Added registration option on login page

---
 frontend/src/api.js                        | 14 ++++-
 frontend/src/components/RegisterDialog.vue | 59 ++++++++++++++++++++++
 frontend/src/pages/Login.vue               | 14 +++++
 3 files changed, 85 insertions(+), 2 deletions(-)
 create mode 100644 frontend/src/components/RegisterDialog.vue

diff --git a/frontend/src/api.js b/frontend/src/api.js
index b35bce86..b35d6811 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -13,9 +13,19 @@ function getInstanceBaseUrl () {
 }
 
 let ax = axios.create({
-  baseURL: getInstanceBaseUrl(),
-  headers: {'Authorization': 'JWT ' + sessionStorage.getItem('token')}
+  baseURL: getInstanceBaseUrl()
+  // headers: {'Authorization': 'JWT ' + sessionStorage.getItem('token')}
 })
+{
+  let token = sessionStorage.getItem('token')
+  if (token) {
+    ax.defaults.headers['Authorization'] = `JWT ${token}`
+  }
+}
+
+export async function registerTutor (credentials) {
+  return ax.post('/api/tutor/register/', credentials)
+}
 
 export async function fetchJWT (credentials) {
   const token = (await ax.post('/api/get-token/', credentials)).data.token
diff --git a/frontend/src/components/RegisterDialog.vue b/frontend/src/components/RegisterDialog.vue
new file mode 100644
index 00000000..8c2425da
--- /dev/null
+++ b/frontend/src/components/RegisterDialog.vue
@@ -0,0 +1,59 @@
+<template>
+  <v-card>
+    <v-card-title class="title">
+      Register
+    </v-card-title>
+    <v-card-text>
+      <v-text-field
+        label="Username"
+        required
+        autofocus
+        v-model="credentials.username"
+      />
+      <v-text-field
+        label="Password"
+        required
+        type="password"
+        v-model="credentials.password"
+      />
+    </v-card-text>
+    <v-card-actions class="justify-center">
+      <v-btn flat :loading="loading" @click="register">submit</v-btn>
+    </v-card-actions>
+  </v-card>
+</template>
+
+<script>
+  import { registerTutor } from '@/api'
+
+  export default {
+    name: 'register-dialog',
+    data () {
+      return {
+        credentials: {
+          username: '',
+          password: ''
+        },
+        loading: false
+      }
+    },
+    methods: {
+      register () {
+        this.loading = true
+        registerTutor(this.credentials).then(() => {
+          this.$emit('registered', this.credentials)
+        }).catch(() => {
+          this.$notify({
+            title: 'Unable to register',
+            text: "Couldn't register a tutor account.",
+            type: 'error'
+          })
+        }).finally(() => { this.loading = false })
+      }
+    }
+  }
+</script>
+
+<style scoped>
+
+</style>
diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue
index 5ae6b8e2..13fd519f 100644
--- a/frontend/src/pages/Login.vue
+++ b/frontend/src/pages/Login.vue
@@ -1,6 +1,9 @@
 <template>
   <v-container fill-height>
     <v-layout align-center justify-center>
+      <v-dialog v-model="registerDialog" max-width="fit-content" class="pa-4">
+        <register-dialog @registered="registered($event)"/>
+      </v-dialog>
       <v-flex text-xs-center xs8 sm6 md4 lg2>
         <img v-if="production" :src="productionBrandUrl"/>
         <img v-else src="../assets/brand.png"/>
@@ -27,6 +30,7 @@
             type="password"
             required
           />
+          <v-btn @click="registerDialog = true">register</v-btn>
           <v-btn :loading="loading" type="submit" color="primary">Access</v-btn>
         </v-form>
       </v-flex>
@@ -37,8 +41,11 @@
 
 <script>
   import {mapActions, mapState} from 'vuex'
+  import RegisterDialog from '@/components/RegisterDialog'
+  import { authMut } from '@/store/modules/authentication'
 
   export default {
+    components: {RegisterDialog},
     name: 'grady-login',
     data () {
       return {
@@ -46,6 +53,7 @@
           username: '',
           password: ''
         },
+        registerDialog: false,
         loading: false
       }
     },
@@ -76,6 +84,12 @@
           this.getJWTTimeDelta()
           this.loading = false
         }).catch(() => { this.loading = false })
+      },
+      registered (credentials) {
+        this.registerDialog = false
+        this.credentials.username = credentials.username
+        this.credentials.password = credentials.password
+        this.$store.commit(authMut.SET_MESSAGE, 'Your account is being activated. Please wait.')
       }
     }
   }
-- 
GitLab