From b1cfeeb531617b18f5696d3c4e8fd811a3f777f8 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Thu, 27 Sep 2018 16:26:49 +0200
Subject: [PATCH] type safe subscriptions module

---
 .../feedback_list/FeedbackSearchOptions.vue   |   2 +-
 .../feedback_list/FeedbackTable.vue           |   2 +-
 .../AnnotatedSubmissionBottomToolbar.vue      |   3 +-
 .../subscriptions/SubscriptionCreation.vue    |   3 +-
 .../subscriptions/SubscriptionForList.vue     |   9 +-
 .../subscriptions/SubscriptionList.vue        |  22 +-
 .../subscriptions/SubscriptionsForStage.vue   |   3 +-
 .../src/components/tutor_list/TutorList.vue   |   2 +-
 frontend/src/models.ts                        |  20 +-
 frontend/src/pages/SubscriptionWorkPage.vue   |  21 +-
 .../src/pages/base/FeedbackHistoryPage.vue    |   2 +-
 .../src/pages/reviewer/TutorOverviewPage.vue  |   2 +-
 frontend/src/store/actions.ts                 |   5 +
 .../feedback_list/feedback-search-options.ts  |   2 +-
 .../modules/feedback_list/feedback-table.ts   |   8 +-
 frontend/src/store/modules/subscriptions.ts   | 525 +++++++++---------
 frontend/src/store/store.ts                   |  11 +-
 17 files changed, 326 insertions(+), 316 deletions(-)

diff --git a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
index a778c34d..7996bbdc 100644
--- a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
+++ b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
@@ -65,7 +65,7 @@ import {mapState, mapGetters} from 'vuex'
 import {FeedbackSearchOptions} from '@/store/modules/feedback_list/feedback-search-options'
 import {mapStateToComputedGetterSetter} from '@/util/helpers'
 import {Authentication} from '@/store/modules/authentication'
-import { actions } from '@/store/actions';
+import { actions } from '@/store/actions'
 
 export default {
   name: 'feedback-search-options',
diff --git a/frontend/src/components/feedback_list/FeedbackTable.vue b/frontend/src/components/feedback_list/FeedbackTable.vue
index 37a460ef..1925c5f9 100644
--- a/frontend/src/components/feedback_list/FeedbackTable.vue
+++ b/frontend/src/components/feedback_list/FeedbackTable.vue
@@ -42,7 +42,7 @@ import {mapState, mapGetters} from 'vuex'
 import {getObjectValueByPath} from '@/util/helpers'
 import FeedbackSearchOptions from '@/components/feedback_list/FeedbackSearchOptions'
 import { FeedbackSearchOptions as OptionsModule } from '@/store/modules/feedback_list/feedback-search-options'
-import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table';
+import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table'
 
 export default {
   computed: {
diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
index 49c9af1b..f759a584 100644
--- a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
+++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue
@@ -56,6 +56,7 @@
 <script>
 import {subNotesMut, subNotesNamespace} from '@/store/modules/submission-notes'
 import {Authentication} from '@/store/modules/authentication'
+import { Subscriptions } from '@/store/modules/subscriptions';
 
 export default {
   name: 'annotated-submission-bottom-toolbar',
@@ -137,7 +138,7 @@ export default {
     },
     skipSubmission () {
       if (this.skippable) {
-        this.$store.dispatch('skipAssignment').catch(() => {
+        Subscriptions.skipAssignment().catch(() => {
           this.$notify({
             title: 'Unable to skip submission',
             type: 'error'
diff --git a/frontend/src/components/subscriptions/SubscriptionCreation.vue b/frontend/src/components/subscriptions/SubscriptionCreation.vue
index e41d00ab..b7d1561e 100644
--- a/frontend/src/components/subscriptions/SubscriptionCreation.vue
+++ b/frontend/src/components/subscriptions/SubscriptionCreation.vue
@@ -30,6 +30,7 @@
 
 <script>
 import {Authentication} from '@/store/modules/authentication'
+import { Subscriptions } from '@/store/modules/subscriptions';
 
 const stages = [
   {
@@ -82,7 +83,7 @@ export default {
   methods: {
     subscribe () {
       this.loading = true
-      this.$store.dispatch('subscribeTo', {
+      Subscriptions.subscribeTo({
         type: this.type,
         key: this.key.key,
         stage: this.stage.stage
diff --git a/frontend/src/components/subscriptions/SubscriptionForList.vue b/frontend/src/components/subscriptions/SubscriptionForList.vue
index 50619e3b..77c4bf12 100644
--- a/frontend/src/components/subscriptions/SubscriptionForList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionForList.vue
@@ -20,19 +20,20 @@
 
 <script lang="ts">
 import {Vue, Component, Prop} from 'vue-property-decorator'
-import {Assignment} from '@/models'
+import {Assignment, Subscription} from '@/models'
+import { Subscriptions } from '@/store/modules/subscriptions';
 
 @Component
 export default class SubscriptionForList extends Vue {
   @Prop({type: String, required: true}) pk!: string
-  @Prop({type: String, required: true}) feedbackStage!: string
-  @Prop({type: String, required: true}) queryType!: string
+  @Prop({type: String, required: true}) feedbackStage!: Subscription.FeedbackStageEnum
+  @Prop({type: String, required: true}) queryType!: Subscription.QueryTypeEnum
   @Prop({type: Number, required: true}) available!: number
   @Prop({type: Array, required: true}) assignments!: Assignment[]
   @Prop({type: String, default: ''}) queryKey!: string
 
   get name () {
-    return this.$store.getters.resolveSubscriptionKeyToName(
+    return Subscriptions.resolveSubscriptionKeyToName(
       {queryKey: this.queryKey, queryType: this.queryType})
   }
   get active () {
diff --git a/frontend/src/components/subscriptions/SubscriptionList.vue b/frontend/src/components/subscriptions/SubscriptionList.vue
index c31e578c..9db5ed5f 100644
--- a/frontend/src/components/subscriptions/SubscriptionList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionList.vue
@@ -34,6 +34,7 @@ import {actions} from '@/store/actions'
 import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation'
 import SubscriptionForList from '@/components/subscriptions/SubscriptionForList'
 import SubscriptionsForStage from '@/components/subscriptions/SubscriptionsForStage'
+import { Subscriptions } from '@/store/modules/subscriptions'
 
 export default {
   components: {
@@ -54,33 +55,26 @@ export default {
     }
   },
   computed: {
-    ...mapGetters({
-      subscriptions: 'getSubscriptionsGroupedByType',
-      stages: 'availableStages',
-      stagesReadable: 'availableStagesReadable'
-    }),
+    subscriptions () { return Subscriptions.state.subscriptions },
+    stages () { return Subscriptions.availableStages },
+    stagesReadable () { return Subscriptions.availableStagesReadable },
     showDetail () {
       return !this.sidebar || (this.sidebar && !UI.state.sideBarCollapsed)
     }
   },
   methods: {
-    ...mapActions([
-      'getCurrentAssignment',
-      'subscribeToAll',
-      'cleanAssignmentsFromSubscriptions'
-    ]),
     async getSubscriptions () {
       this.updating = true
-      const subscriptions = await this.$store.dispatch('getSubscriptions')
+      const subscriptions = await Subscriptions.getSubscriptions()
       this.updating = false
       return subscriptions
     }
   },
   created () {
-    const typesAndSubscriptions = [actions.updateSubmissionTypes(), this.getSubscriptions()]
+    const typesAndSubscriptions = [actions.updateSubmissionTypes(), Subscriptions.getSubscriptions()]
     Promise.all(typesAndSubscriptions).then(() => {
-      this.subscribeToAll()
-      this.cleanAssignmentsFromSubscriptions()
+      Subscriptions.subscribeToAll()
+      Subscriptions.cleanAssignmentsFromSubscriptions()
     })
   }
 }
diff --git a/frontend/src/components/subscriptions/SubscriptionsForStage.vue b/frontend/src/components/subscriptions/SubscriptionsForStage.vue
index b6388b7d..8c044a52 100644
--- a/frontend/src/components/subscriptions/SubscriptionsForStage.vue
+++ b/frontend/src/components/subscriptions/SubscriptionsForStage.vue
@@ -14,6 +14,7 @@
 
 <script>
 import SubscriptionForList from '@/components/subscriptions/SubscriptionForList'
+import { Subscriptions } from '@/store/modules/subscriptions';
 export default {
   components: {
     SubscriptionForList
@@ -31,7 +32,7 @@ export default {
   },
   computed: {
     subscriptions () {
-      return this.$store.getters.getSubscriptionsGroupedByType[this.stage]
+      return Subscriptions.getSubscriptionsGroupedByType[this.stage]
     }
   }
 }
diff --git a/frontend/src/components/tutor_list/TutorList.vue b/frontend/src/components/tutor_list/TutorList.vue
index 1c8f28c3..3b760a08 100644
--- a/frontend/src/components/tutor_list/TutorList.vue
+++ b/frontend/src/components/tutor_list/TutorList.vue
@@ -31,7 +31,7 @@
 <script>
 import {mapState, mapActions} from 'vuex'
 import {changeActiveForUser} from '@/api'
-import { actions } from '@/store/actions';
+import { actions } from '@/store/actions'
 
 export default {
   name: 'tutor-list',
diff --git a/frontend/src/models.ts b/frontend/src/models.ts
index c5f59b16..ddf2d432 100644
--- a/frontend/src/models.ts
+++ b/frontend/src/models.ts
@@ -635,19 +635,19 @@ export namespace Subscription {
      * @enum {string}
      */
     export enum QueryTypeEnum {
-        Random = <any> 'random',
-        Student = <any> 'student',
-        Exam = <any> 'exam',
-        SubmissionType = <any> 'submission_type'
+        Random = 'random',
+        Student = 'student',
+        Exam = 'exam',
+        SubmissionType = 'submission_type'
     }
     /**
      * @export
      * @enum {string}
      */
     export enum FeedbackStageEnum {
-        Creation = <any> 'feedback-creation',
-        Validation = <any> 'feedback-validation',
-        ConflictResolution = <any> 'feedback-conflict-resolution'
+        Creation = 'feedback-creation',
+        Validation = 'feedback-validation',
+        ConflictResolution = 'feedback-conflict-resolution'
     }
 }
 
@@ -775,9 +775,9 @@ export namespace UserAccount {
      * @enum {string}
      */
     export enum RoleEnum {
-        Student = <any> 'Student',
-        Tutor = <any> 'Tutor',
-        Reviewer = <any> 'Reviewer'
+        Student = 'Student',
+        Tutor = 'Tutor',
+        Reviewer = 'Reviewer'
     }
 }
 
diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue
index 8797fdf3..ab3a593a 100644
--- a/frontend/src/pages/SubscriptionWorkPage.vue
+++ b/frontend/src/pages/SubscriptionWorkPage.vue
@@ -36,12 +36,13 @@ import SubmissionCorrection from '@/components/submission_notes/SubmissionCorrec
 import SubmissionType from '@/components/SubmissionType'
 import store from '@/store/store'
 import SubmissionTests from '@/components/SubmissionTests'
-import { subscriptionMuts } from '@/store/modules/subscriptions'
+import { Subscriptions } from '@/store/modules/subscriptions'
 import RouteChangeConfirmation from '@/components/submission_notes/RouteChangeConfirmation'
+import { getters } from '@/store/getters'
 
 function onRouteEnterOrUpdate (to, from, next) {
   if (to.name === 'subscription') {
-    store.dispatch('changeActiveSubscription', to.params['pk']).then(() => {
+    Subscriptions.changeActiveSubscription(to.params['pk']).then(() => {
       next()
     })
   }
@@ -63,16 +64,16 @@ export default {
   },
   computed: {
     subscription () {
-      return this.$store.state.subscriptions.subscriptions[this.$route.params['pk']]
+      return Subscriptions.state.subscriptions[this.$route.params['pk']]
     },
     currentAssignment () {
-      return this.$store.state.subscriptions.assignmentQueue[0]
+      return Subscriptions.state.assignmentQueue[0]
     },
     submission () {
       return this.currentAssignment.submission
     },
     submissionType () {
-      return this.$store.state.submissionTypes[this.submission['type']]
+      return getters.state.submissionTypes[this.submission.type]
     }
   },
   beforeRouteEnter (to, from, next) {
@@ -92,11 +93,11 @@ export default {
   },
   methods: {
     startWorkOnNextAssignment () {
-      this.$store.dispatch('getAssignmentsForActiveSubscription', 1).then(([promise]) => {
+      Subscriptions.getAssignmentsForActiveSubscription(1).then(([promise]) => {
         promise.then(assignment => {
-          this.$store.commit(subscriptionMuts.ADD_ASSIGNMENT_TO_QUEUE, assignment)
+          Subscriptions.ADD_ASSIGNMENT_TO_QUEUE(assignment)
         }).finally(() => {
-          this.$store.commit(subscriptionMuts.POP_ASSIGNMENT_FROM_QUEUE)
+          Subscriptions.POP_ASSIGNMENT_FROM_QUEUE()
         })
       })
     }
@@ -106,8 +107,8 @@ export default {
       this.$vuetify.goTo(0, {duration: 200, easing: 'easeInOutCubic'})
       if (val === undefined) {
         this.$router.replace('ended')
-        this.$store.dispatch('removeActiveSubscription')
-        this.$store.dispatch('getSubscriptions')
+        Subscriptions.removeActiveSubscription()
+        Subscriptions.getSubscriptions()
       }
     }
   }
diff --git a/frontend/src/pages/base/FeedbackHistoryPage.vue b/frontend/src/pages/base/FeedbackHistoryPage.vue
index 9e890f0b..4e0d8b61 100644
--- a/frontend/src/pages/base/FeedbackHistoryPage.vue
+++ b/frontend/src/pages/base/FeedbackHistoryPage.vue
@@ -12,7 +12,7 @@
 </template>
 
 <script>
-import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table';
+import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table'
 export default {
   name: 'feedback-history-page',
   created () {
diff --git a/frontend/src/pages/reviewer/TutorOverviewPage.vue b/frontend/src/pages/reviewer/TutorOverviewPage.vue
index e8b7a44c..e88bc6a0 100644
--- a/frontend/src/pages/reviewer/TutorOverviewPage.vue
+++ b/frontend/src/pages/reviewer/TutorOverviewPage.vue
@@ -5,7 +5,7 @@
 <script>
 import store from '@/store/store'
 import TutorList from '@/components/tutor_list/TutorList'
-import { actions } from '@/store/actions';
+import { actions } from '@/store/actions'
 
 export default {
   components: {TutorList},
diff --git a/frontend/src/store/actions.ts b/frontend/src/store/actions.ts
index 14a5889c..31250dad 100644
--- a/frontend/src/store/actions.ts
+++ b/frontend/src/store/actions.ts
@@ -7,6 +7,8 @@ import { subNotesMut } from "@/store/modules/submission-notes";
 import * as api from "@/api";
 import router from "@/router/index";
 import { RootState } from "@/store/store";
+import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table';
+import { Subscriptions } from '@/store/modules/subscriptions';
 
 async function getExamTypes(context: BareActionContext<RootState, RootState>) {
   const examTypes = await api.fetchExamTypes({});
@@ -83,6 +85,9 @@ function logout(
     api.changeActiveForUser(Authentication.state.user.pk, false);
   }
   mut.RESET_STATE();
+  FeedbackTable.RESET_STATE()
+  Authentication.RESET_STATE()
+  Subscriptions.RESET_STATE()
   commit("submissionNotes/" + subNotesMut.RESET_STATE);
   Authentication.SET_MESSAGE(message);
   router.push({ name: "login" });
diff --git a/frontend/src/store/modules/feedback_list/feedback-search-options.ts b/frontend/src/store/modules/feedback_list/feedback-search-options.ts
index d73c42dd..6a69437c 100644
--- a/frontend/src/store/modules/feedback_list/feedback-search-options.ts
+++ b/frontend/src/store/modules/feedback_list/feedback-search-options.ts
@@ -1,6 +1,6 @@
 import {Module} from 'vuex'
 import {RootState} from '@/store/store'
-import { getStoreBuilder } from 'vuex-typex';
+import { getStoreBuilder } from 'vuex-typex'
 
 export const namespace = 'feedbackSearchOptions'
 
diff --git a/frontend/src/store/modules/feedback_list/feedback-table.ts b/frontend/src/store/modules/feedback_list/feedback-table.ts
index 1ea3285a..2901b4a2 100644
--- a/frontend/src/store/modules/feedback_list/feedback-table.ts
+++ b/frontend/src/store/modules/feedback_list/feedback-table.ts
@@ -4,12 +4,12 @@ import {Assignment, Feedback, Subscription, SubmissionType} from '@/models'
 import {Module} from 'vuex'
 import {RootState} from '@/store/store'
 import {getters} from '@/store/getters'
-import { getStoreBuilder, BareActionContext } from 'vuex-typex';
-import { Authentication } from '@/store/modules/authentication';
+import { getStoreBuilder, BareActionContext } from 'vuex-typex'
+import { Authentication } from '@/store/modules/authentication'
 
 export interface FeedbackHistoryItem extends Feedback {
   history?: {
-    [key in Subscription.FeedbackStageEnum]: {
+    [key in Subscription.FeedbackStageEnum]?: {
       owner: string
       isDone: boolean
     }
@@ -49,7 +49,7 @@ function ADD_ASSIGNMENTS_INFO (state: FeedbackTableState, assignments: Array<Ass
   }
 }
 function SET_FEEDBACK_OF_SUBMISSION_TYPE (state: FeedbackTableState, {feedback, type}:
-  {feedback: Feedback, type: SubmissionType}) {
+{feedback: Feedback, type: SubmissionType}) {
   if (!feedback.ofSubmission) {
     throw new Error('Feedback must have ofSubmission present')
   }
diff --git a/frontend/src/store/modules/subscriptions.ts b/frontend/src/store/modules/subscriptions.ts
index 12d678d6..3fbfe78a 100644
--- a/frontend/src/store/modules/subscriptions.ts
+++ b/frontend/src/store/modules/subscriptions.ts
@@ -4,20 +4,10 @@ import {cartesian, flatten, handleError, once} from '@/util/helpers'
 import {Assignment, Subscription} from '@/models'
 import {ActionContext, Module} from 'vuex'
 import {RootState} from '@/store/store'
-import { Authentication } from '@/store/modules/authentication';
+import { Authentication } from '@/store/modules/authentication'
+import { getStoreBuilder, BareActionContext } from 'vuex-typex'
 
-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'
-})
-
-interface SubscriptionsState {
+export interface SubscriptionsState {
   subscriptions: {[pk: string]: Subscription}
   assignmentQueue: Array<Assignment>
   activeSubscriptionPk: string
@@ -33,264 +23,279 @@ function initialState (): SubscriptionsState {
   }
 }
 
+const mb = getStoreBuilder<RootState>().module('Subscriptions', initialState())
+
 const MAX_NUMBER_OF_ASSIGNMENTS = 2
 
-const subscriptionsModule: Module<SubscriptionsState, RootState> = {
-  state: initialState(),
-  getters: {
-    availableTypes (state, getters) {
-      let types = ['random', 'submission_type']
-      if (Authentication.isReviewer) {
-        types.push('exam')
-      }
-      return types
-    },
-    availableStages (state, getters) {
-      let stages = ['feedback-creation', 'feedback-validation']
-      if (Authentication.isReviewer) {
-        stages.push('feedback-conflict-resolution')
-      }
-      return stages
-    },
-    availableStagesReadable (state, getters) {
-      let stages = ['create', 'validate']
-      if (Authentication.isReviewer) {
-        stages.push('resolve')
-      }
-      return stages
-    },
-    availableSubmissionTypeQueryKeys (state, getters, rootState) {
-      return Object.values(rootState.submissionTypes).map((subType: any) => subType.pk)
-    },
-    availableExamTypeQueryKeys (state, getters, rootState) {
-      return Object.values(rootState.examTypes).map((examType: any) => examType.pk)
-    },
-    activeSubscription (state) {
-      return state.subscriptions[state.activeSubscriptionPk]
-    },
-    resolveSubscriptionKeyToName: (state, getters, rootState) => (subscription: Subscription) => {
-      switch (subscription.queryType) {
-        case Subscription.QueryTypeEnum.Random:
-          return 'Active'
-        case Subscription.QueryTypeEnum.Exam:
-          return subscription.queryKey
-            ? rootState.examTypes[subscription.queryKey].moduleReference : 'Exam'
-        case Subscription.QueryTypeEnum.SubmissionType:
-          return subscription.queryKey
-            ? rootState.submissionTypes[subscription.queryKey].name : 'Submission Type'
-        case Subscription.QueryTypeEnum.Student:
-          return subscription.queryKey
-            ? rootState.students[subscription.queryKey].name : 'Student'
-      }
-    },
-    // TODO Refactor this monstrosity
-    getSubscriptionsGroupedByType (state, getters) {
-      const subscriptionsByType = () => {
-        return {
-          'random': [],
-          'student': [],
-          'exam': [],
-          'submission_type': []
-        }
-      }
-      let subscriptionsByStage = getters.availableStages.reduce((acc: {[p: string]: {[k: string]: Subscription[]}},
-        curr: string) => {
-        acc[curr] = subscriptionsByType()
-        return acc
-      }, {})
-      Object.values(state.subscriptions).forEach((subscription: any) => {
-        subscriptionsByStage[subscription.feedbackStage][subscription.queryType].push(subscription)
-      })
-      // sort the resulting arrays in subscriptions lexicographically by their query_keys
-      const sortSubscriptions = (subscriptionsByType: {[k: string]: Subscription[]}) => Object.values(subscriptionsByType)
-        .forEach((arr: object[]) => {
-          if (arr.length > 1 && arr[0].hasOwnProperty('queryKey')) {
-            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: any) => {
-        sortSubscriptions(subscriptionsByType)
-      })
-      return subscriptionsByStage
+const stateGetter = mb.state()
+
+const availableTypesGetter = mb.read(function availableTypes (state, getters) {
+  let types = [Subscription.QueryTypeEnum.Random, Subscription.QueryTypeEnum.SubmissionType]
+  if (Authentication.isReviewer) {
+    types.push(Subscription.QueryTypeEnum.Exam)
+  }
+  return types
+})
+const availableStagesGetter = mb.read(function availableStages (state, getters) {
+  let stages = [Subscription.FeedbackStageEnum.Creation, Subscription.FeedbackStageEnum.Validation]
+  if (Authentication.isReviewer) {
+    stages.push(Subscription.FeedbackStageEnum.ConflictResolution)
+  }
+  return stages
+})
+const availableStagesReadableGetter = mb.read(function availableStagesReadable (state, getters) {
+  let stages = ['create', 'validate']
+  if (Authentication.isReviewer) {
+    stages.push('resolve')
+  }
+  return stages
+})
+const availableSubmissionTypeQueryKeysGetter = mb.read(function availableSubmissionTypeQueryKeys (state, getters, rootState) {
+  return Object.values(rootState.submissionTypes).map((subType: any) => subType.pk)
+})
+const availableExamTypeQueryKeysGetter = mb.read(function availableExamTypeQueryKeys (state, getters, rootState) {
+  return Object.values(rootState.examTypes).map((examType: any) => examType.pk)
+})
+const activeSubscriptionGetter = mb.read(function activeSubscription (state) {
+  return state.subscriptions[state.activeSubscriptionPk]
+})
+const resolveSubscriptionKeyToNameGetter = mb.read(function resolveSubscriptionKeyToName (state, getters, rootState) {
+  return (subscription: {queryType: Subscription.QueryTypeEnum, queryKey: string}) => {
+    switch (subscription.queryType) {
+      case Subscription.QueryTypeEnum.Random:
+        return 'Active'
+      case Subscription.QueryTypeEnum.Exam:
+        return subscription.queryKey
+          ? rootState.examTypes[subscription.queryKey].moduleReference : 'Exam'
+      case Subscription.QueryTypeEnum.SubmissionType:
+        return subscription.queryKey
+          ? rootState.submissionTypes[subscription.queryKey].name : 'Submission Type'
+      case Subscription.QueryTypeEnum.Student:
+        return subscription.queryKey
+          ? rootState.students[subscription.queryKey].name : 'Student'
     }
-  },
-  mutations: {
-    [subscriptionMuts.SET_SUBSCRIPTIONS] (state, subscriptions: Array<Subscription>): void {
-      state.subscriptions = subscriptions.reduce((acc: {[pk: string]: Subscription}, curr) => {
-        acc[curr.pk] = curr
-        return acc
-      }, {})
-    },
-    [subscriptionMuts.SET_SUBSCRIPTION] (state, subscription: Subscription): void {
-      Vue.set(state.subscriptions, subscription.pk, subscription)
-    },
-    [subscriptionMuts.SET_ACTIVE_SUBSCRIPTION_PK] (state, subscriptionPk: string): void {
-      state.activeSubscriptionPk = subscriptionPk
-    },
-    [subscriptionMuts.SET_ASSIGNMENT_QUEUE] (state, queue: Array<Assignment>): void {
-      state.assignmentQueue = queue
-    },
-    [subscriptionMuts.ADD_ASSIGNMENT_TO_QUEUE] (state, assignment: Assignment): void {
-      state.assignmentQueue.push(assignment)
-    },
-    [subscriptionMuts.POP_ASSIGNMENT_FROM_QUEUE] (state): void {
-      state.assignmentQueue.shift()
-    },
-    [subscriptionMuts.RESET_STATE] (state): void {
-      Object.assign(state, initialState())
+  }
+})
+// TODO Refactor this monstrosity
+const getSubscriptionsGroupedByTypeGetter = mb.read(function getSubscriptionsGroupedByType (state, getters) {
+  const subscriptionsByType = () => {
+    return {
+      'random': [],
+      'student': [],
+      'exam': [],
+      'submission_type': []
     }
-  },
-  actions: {
-    subscribeTo: async function (
-      {commit, dispatch, getters},
-      {type, key, stage}:
-      {type: Subscription.QueryTypeEnum, key?: string, stage: Subscription.FeedbackStageEnum}) {
-      try {
-        // don't subscribe to type, key, stage combinations if they're already present
-        let subscription = getters.getSubscriptionsGroupedByType[stage][type].find((elem: Subscription) => {
-          if (type === Subscription.QueryTypeEnum.Random) {
-            return true
+  }
+  let subscriptionsByStage = getters.availableStages.reduce((acc: {[p: string]: {[k: string]: Subscription[]}},
+    curr: string) => {
+    acc[curr] = subscriptionsByType()
+    return acc
+  }, {})
+  Object.values(state.subscriptions).forEach((subscription: any) => {
+    subscriptionsByStage[subscription.feedbackStage][subscription.queryType].push(subscription)
+  })
+  // sort the resulting arrays in subscriptions lexicographically by their query_keys
+  const sortSubscriptions = (subscriptionsByType: {[k: string]: Subscription[]}) => Object.values(subscriptionsByType)
+    .forEach((arr: object[]) => {
+      if (arr.length > 1 && arr[0].hasOwnProperty('queryKey')) {
+        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 elem.queryKey === key
         })
-        subscription = subscription || await api.subscribeTo(type, key, stage)
-        commit(subscriptionMuts.SET_SUBSCRIPTION, subscription)
-        return subscription
-      } catch (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) {
-        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')
+    })
+  Object.values(subscriptionsByStage).forEach((subscriptionsByType: any) => {
+    sortSubscriptions(subscriptionsByType)
+  })
+  return subscriptionsByStage
+})
+
+function SET_SUBSCRIPTIONS (state: SubscriptionsState, subscriptions: Array<Subscription>): void {
+  state.subscriptions = subscriptions.reduce((acc: {[pk: string]: Subscription}, curr) => {
+    acc[curr.pk] = curr
+    return acc
+  }, {})
+}
+function SET_SUBSCRIPTION (state: SubscriptionsState, subscription: Subscription): void {
+  Vue.set(state.subscriptions, subscription.pk, subscription)
+}
+function SET_ACTIVE_SUBSCRIPTION_PK (state: SubscriptionsState, subscriptionPk: string): void {
+  state.activeSubscriptionPk = subscriptionPk
+}
+function SET_ASSIGNMENT_QUEUE (state: SubscriptionsState, queue: Array<Assignment>): void {
+  state.assignmentQueue = queue
+}
+function ADD_ASSIGNMENT_TO_QUEUE (state: SubscriptionsState, assignment: Assignment): void {
+  state.assignmentQueue.push(assignment)
+}
+function POP_ASSIGNMENT_FROM_QUEUE (state: SubscriptionsState): void {
+  state.assignmentQueue.shift()
+}
+function RESET_STATE (state: SubscriptionsState): void {
+  Object.assign(state, initialState())
+}
+
+async function subscribeTo (
+  context: BareActionContext<SubscriptionsState, RootState>,
+  {type, key, stage}:
+  {type: Subscription.QueryTypeEnum, key?: string, stage: Subscription.FeedbackStageEnum}): Promise<Subscription> {
+  // don't subscribe to type, key, stage combinations if they're already present
+  let subscription = Subscriptions.getSubscriptionsGroupedByType[stage][type].find((elem: Subscription) => {
+    if (type === Subscription.QueryTypeEnum.Random) {
+      return true
+    }
+    return elem.queryKey === key
+  })
+  subscription = subscription || await api.subscribeTo(type, key, stage)
+  Subscriptions.SET_SUBSCRIPTION(subscription)
+  return subscription
+}
+async function getSubscriptions () {
+  const subscriptions = await api.fetchSubscriptions()
+  Subscriptions.SET_SUBSCRIPTIONS(subscriptions)
+  return 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
+ */
+async function getAssignmentsForActiveSubscription
+(context: BareActionContext<SubscriptionsState, RootState>, numOfAssignments: number):
+  Promise<Promise<Assignment>[]> {
+  numOfAssignments = numOfAssignments || MAX_NUMBER_OF_ASSIGNMENTS - context.state.assignmentQueue.length
+  let assignmentsPromises = []
+  for (let i = 0; i < numOfAssignments; i++) {
+    assignmentsPromises.push(api.createAssignment({subscription: Subscriptions.activeSubscription}))
+  }
+  return assignmentsPromises
+}
+async function deleteAssignment
+(context: BareActionContext<SubscriptionsState, RootState>, assignment: Assignment) {
+  return api.deleteAssignment({assignment})
+}
+async function cleanAssignmentsFromSubscriptions
+({state}: BareActionContext<SubscriptionsState, RootState>, excludeActive = true) {
+  Object.values(state.subscriptions).forEach(subscription => {
+    if (!excludeActive || subscription.pk !== state.activeSubscriptionPk) {
+      if (subscription.assignments) {
+        subscription.assignments.forEach(assignment => {
+          api.deleteAssignment({assignment})
+        })
       }
-    },
-    async cleanAssignmentsFromSubscriptions ({commit, state, dispatch}, excludeActive = true) {
-      Object.values(state.subscriptions).forEach(subscription => {
-        if (!excludeActive || subscription.pk !== state.activeSubscriptionPk) {
-          if (subscription.assignments) {
-            subscription.assignments.forEach(assignment => {
-              api.deleteAssignment({assignment})
-            })
-          }
-        }
+    }
+  })
+}
+async function skipAssignment ({state}: BareActionContext<SubscriptionsState, RootState>) {
+  Subscriptions.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 Subscriptions.getAssignmentsForActiveSubscription(1)
+    }).then(([promise]) => {
+      promise.then((assignment: Assignment) => {
+        Subscriptions.ADD_ASSIGNMENT_TO_QUEUE(assignment)
+        Subscriptions.POP_ASSIGNMENT_FROM_QUEUE()
       })
-    },
-    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: 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}) {
+    })
+}
+async function deleteActiveAssignments ({state}: BareActionContext<SubscriptionsState, RootState>) {
+  Promise.all(state.assignmentQueue.map(assignment => {
+    Subscriptions.deleteAssignment(assignment)
+  }))
+}
+async function changeActiveSubscription ({state}: BareActionContext<SubscriptionsState, RootState>, subscriptionPk = '') {
+  if (subscriptionPk !== state.activeSubscriptionPk) {
+    await Subscriptions.deleteActiveAssignments()
+    Subscriptions.SET_ACTIVE_SUBSCRIPTION_PK(subscriptionPk)
+    let assignmentsPromises = await Subscriptions.getAssignmentsForActiveSubscription(MAX_NUMBER_OF_ASSIGNMENTS)
+    let createdAssignments = []
+    // TODO refactor this since it's very bad to await promises in for loops
+
+    for (let promise of assignmentsPromises) {
       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 = []
-        // TODO refactor this since it's very bad to await promises in for loops
-        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, '')
-    },
-    // TODO use enums here
-    async subscribeToType ({commit, state, dispatch, getters}, type: Subscription.QueryTypeEnum) {
-      switch (type) {
-        case Subscription.QueryTypeEnum.Random:
-          return getters.availableStages.map((stage: string) => {
-            dispatch('subscribeTo', {type, stage})
-          })
-        case Subscription.QueryTypeEnum.Exam:
-          if (Authentication.isReviewer) {
-            const stageKeyCartesian = cartesian(
-              getters.availableStages, getters.availableExamTypeQueryKeys)
-            // @ts-ignore
-            return stageKeyCartesian.map(([stage, key]: [string, string]) => {
-              dispatch('subscribeTo', {stage, type, key})
-            })
-          }
-          return []
-        case Subscription.QueryTypeEnum.SubmissionType:
-          const stageKeyCartesian = cartesian(
-            getters.availableStages, getters.availableSubmissionTypeQueryKeys)
-          // @ts-ignore
-          return stageKeyCartesian.map(([stage, key]: [string, string]) => {
-            dispatch('subscribeTo', {stage, type, key})
-          })
+        createdAssignments.push(await promise)
+      } catch (_) {}
+    }
+    Subscriptions.SET_ASSIGNMENT_QUEUE(createdAssignments)
+  }
+}
+async function removeActiveSubscription () {
+  await Subscriptions.deleteActiveAssignments()
+  Subscriptions.SET_ASSIGNMENT_QUEUE([])
+  Subscriptions.SET_ACTIVE_SUBSCRIPTION_PK('')
+}
+// TODO use enums here
+async function subscribeToType
+(context: BareActionContext<SubscriptionsState, RootState>, type: Subscription.QueryTypeEnum) {
+  switch (type) {
+    case Subscription.QueryTypeEnum.Random:
+      Subscriptions.availableStages.map((stage: Subscription.FeedbackStageEnum) => {
+        Subscriptions.subscribeTo({type, stage})
+      })
+      break
+    case Subscription.QueryTypeEnum.Exam:
+      if (Authentication.isReviewer) {
+        const stageKeyCartesian = cartesian(
+          Subscriptions.availableStages, Subscriptions.availableExamTypeQueryKeys)
+        // @ts-ignore
+        stageKeyCartesian.map(([stage, key]: [Subscription.FeedbackStageEnum, string]) => {
+          Subscriptions.subscribeTo({stage, type, key})
+        })
       }
-    },
-    subscribeToAll: once(async ({commit, state, dispatch, getters}:
-    ActionContext<SubscriptionsState, RootState>) => {
-      return Promise.all(flatten(getters.availableTypes.map((type: string) => {
-        return dispatch('subscribeToType', type)
-      })))
-    })
+      break
+    case Subscription.QueryTypeEnum.SubmissionType:
+      const stageKeyCartesian = cartesian(
+        Subscriptions.availableStages, Subscriptions.availableSubmissionTypeQueryKeys)
+      // @ts-ignore
+      stageKeyCartesian.map(([stage, key]: [Subscription.FeedbackStageEnum, string]) => {
+        Subscriptions.subscribeTo({stage, type, key})
+      })
+      break
   }
 }
+const subscribeToAll = once(async () => {
+  return Promise.all(flatten(Subscriptions.availableTypes.map((type) => {
+    return Subscriptions.subscribeToType(type)
+  })))
+})
+
+export const Subscriptions = {
+  get state () { return stateGetter() },
+  get availableTypes () { return availableTypesGetter() },
+  get availableStages () { return availableStagesGetter() },
+  get availableStagesReadable () { return availableStagesReadableGetter() },
+  get availableSubmissionTypeQueryKeys () { return availableSubmissionTypeQueryKeysGetter() },
+  get availableExamTypeQueryKeys () { return availableExamTypeQueryKeysGetter() },
+  get activeSubscription () { return activeSubscriptionGetter() },
+  get resolveSubscriptionKeyToName () { return resolveSubscriptionKeyToNameGetter() },
+  get getSubscriptionsGroupedByType () { return getSubscriptionsGroupedByTypeGetter() },
 
-export default subscriptionsModule
+  SET_SUBSCRIPTIONS: mb.commit(SET_SUBSCRIPTIONS),
+  SET_SUBSCRIPTION: mb.commit(SET_SUBSCRIPTION),
+  SET_ACTIVE_SUBSCRIPTION_PK: mb.commit(SET_ACTIVE_SUBSCRIPTION_PK),
+  SET_ASSIGNMENT_QUEUE: mb.commit(SET_ASSIGNMENT_QUEUE),
+  ADD_ASSIGNMENT_TO_QUEUE: mb.commit(ADD_ASSIGNMENT_TO_QUEUE),
+  POP_ASSIGNMENT_FROM_QUEUE: mb.commit(POP_ASSIGNMENT_FROM_QUEUE),
+  RESET_STATE: mb.commit(RESET_STATE),
+
+  subscribeTo: mb.dispatch(subscribeTo),
+  getSubscriptions: mb.dispatch(getSubscriptions),
+  getAssignmentsForActiveSubscription: mb.dispatch(getAssignmentsForActiveSubscription),
+  deleteAssignment: mb.dispatch(deleteAssignment),
+  cleanAssignmentsFromSubscriptions: mb.dispatch(cleanAssignmentsFromSubscriptions),
+  skipAssignment: mb.dispatch(skipAssignment),
+  deleteActiveAssignments: mb.dispatch(deleteActiveAssignments),
+  changeActiveSubscription: mb.dispatch(changeActiveSubscription),
+  removeActiveSubscription: mb.dispatch(removeActiveSubscription),
+  subscribeToType: mb.dispatch(subscribeToType),
+  subscribeToAll: mb.dispatch(subscribeToAll, 'subscribeToAll')
+}
diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts
index 761dafde..c208af27 100644
--- a/frontend/src/store/store.ts
+++ b/frontend/src/store/store.ts
@@ -5,12 +5,12 @@ import {getStoreBuilder} from 'vuex-typex'
 
 import studentPage from './modules/student-page'
 import submissionNotes from './modules/submission-notes'
-import subscriptions from './modules/subscriptions'
 
 import './modules/ui'
 import './modules/authentication'
 import './modules/feedback_list/feedback-search-options'
 import './modules/feedback_list/feedback-table'
+import './modules/subscriptions'
 
 import './mutations'
 import './actions'
@@ -20,6 +20,7 @@ import './getters'
 import {UIState} from './modules/ui'
 import {AuthState} from './modules/authentication'
 import {FeedbackSearchOptionsState, FeedbackSearchOptions} from './modules/feedback_list/feedback-search-options'
+import {Subscriptions, SubscriptionsState} from './modules/subscriptions'
 import {FeedbackTableState, FeedbackTable} from './modules/feedback_list/feedback-table'
 
 import {lastInteraction} from '@/store/plugins/lastInteractionPlugin'
@@ -49,7 +50,8 @@ export interface RootState extends RootInitialState{
   UI: UIState,
   Authentication: AuthState,
   FeedbackSearchOptions: FeedbackSearchOptionsState,
-  FeedbackTable: FeedbackTableState
+  FeedbackTable: FeedbackTableState,
+  Subscriptions: SubscriptionsState
 }
 
 export function initialState (): RootInitialState {
@@ -77,8 +79,7 @@ const store = getStoreBuilder<RootState>().vuexStore({
   strict: process.env.NODE_ENV === 'development',
   modules: {
     studentPage,
-    submissionNotes,
-    subscriptions
+    submissionNotes
   },
   plugins: [
     createPersistedState({
@@ -87,7 +88,7 @@ const store = getStoreBuilder<RootState>().vuexStore({
       // 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', 'FeedbackSearchOptions', 'subscriptions',
+        ['UI', 'studentPage', 'submissionNotes', 'FeedbackSearchOptions', 'Subscriptions',
           'Authentication.user', 'Authentication.jwtTimeDelta',
           'Authentication.tokenCreationTime'])
     }),
-- 
GitLab