From 995f2488991c5262448611c7a5667d20a06d2214 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Tue, 15 May 2018 22:39:46 +0200
Subject: [PATCH 01/15] fix in stats comp when mean was null

---
 frontend/src/components/CorrectionStatistics.vue | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/frontend/src/components/CorrectionStatistics.vue b/frontend/src/components/CorrectionStatistics.vue
index 2bd836a5..c6d877b2 100644
--- a/frontend/src/components/CorrectionStatistics.vue
+++ b/frontend/src/components/CorrectionStatistics.vue
@@ -7,7 +7,11 @@
         <ul class="inline-list mx-3">
           <li>Submissions per student: <span>{{statistics.submissions_per_student}}</span></li>
           <li>Submissions per type: <span>{{statistics.submissions_per_type}}</span></li>
-          <li>Curr. mean score: <span>{{statistics.current_mean_score.toFixed(2)}}</span></li>
+          <li>Curr. mean score:
+            <span>
+              {{statistics.current_mean_score === null ? 'N.A.' : statistics.current_mean_score.toFixed(2)}}
+            </span>
+          </li>
         </ul>
         <v-divider class="mx-2 my-2"></v-divider>
         <div v-for="(progress, index) in statistics.submission_type_progress" :key="index">
-- 
GitLab


From 77eeea971d7c6ab509db0b5a26c8ef682cd2e6db Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Tue, 15 May 2018 22:59:56 +0200
Subject: [PATCH 02/15] Changes in store helper/baselayout/subscription

---
 frontend/src/components/BaseLayout.vue        |  84 ++++++-------
 .../subscriptions/SubscriptionList.vue        | 113 +++++++++---------
 frontend/src/util/helpers.js                  |   4 +-
 3 files changed, 103 insertions(+), 98 deletions(-)

diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue
index c2974f03..9ab1ba03 100644
--- a/frontend/src/components/BaseLayout.vue
+++ b/frontend/src/components/BaseLayout.vue
@@ -84,53 +84,53 @@
 </template>
 
 <script>
-import { mapGetters, mapState } from 'vuex'
-import {uiMut} from '@/store/modules/ui'
-import { createComputedGetterSetter } from '@/util/helpers'
-import UserOptions from '@/components/UserOptions'
-export default {
-  name: 'base-layout',
-  components: {UserOptions},
-  computed: {
-    ...mapGetters([
-      'gradySpeak'
-    ]),
-    ...mapState({
-      username: state => state.authentication.user.username,
-      userRole: state => state.authentication.user.role
-    }),
-    darkMode: createComputedGetterSetter({
-      path: 'ui.darkMode',
-      mutation: uiMut.SET_DARK_MODE
-    }),
-    darkModeUnlocked: createComputedGetterSetter({
-      path: 'ui.darkModeUnlocked',
-      mutation: uiMut.SET_DARK_MODE_UNLOCKED
-    }),
-    mini: {
-      get: function () {
-        return this.$store.state.ui.sideBarCollapsed
+  import { mapGetters, mapState } from 'vuex'
+  import {uiMut} from '@/store/modules/ui'
+  import { mapStateToComputedGetterSetter } from '@/util/helpers'
+  export default {
+    name: 'base-layout',
+    computed: {
+      ...mapGetters([
+        'gradySpeak'
+      ]),
+      ...mapState({
+        username: state => state.authentication.username,
+        userRole: state => state.authentication.userRole
+      }),
+      ...mapStateToComputedGetterSetter({
+        pathPrefix: 'ui',
+        items: [
+          {
+            name: 'darkMode',
+            mutation: uiMut.SET_DARK_MODE
+          },
+          {
+            name: 'darkModeUnlocked',
+            mutation: uiMut.SET_DARK_MODE_UNLOCKED
+          },
+          {
+            name: 'mini',
+            path: 'sideBarCollapsed',
+            mutation: uiMut.SET_SIDEBAR_COLLAPSED
+          }
+        ]
+      }),
+      production () {
+        return process.env.NODE_ENV === 'production'
       },
-      set: function (collapsed) {
-        this.$store.commit(uiMut.SET_SIDEBAR_COLLAPSED, collapsed)
+      productionBrandUrl () {
+        return `https://${window.location.host}/static/img/brand.png`
       }
     },
-    production () {
-      return process.env.NODE_ENV === 'production'
-    },
-    productionBrandUrl () {
-      return `https://${window.location.host}/static/img/brand.png`
-    }
-  },
-  methods: {
-    logout () {
-      this.$store.dispatch('logout')
-    },
-    logFeedbackClick () {
-      this.darkModeUnlocked = true
+    methods: {
+      logout () {
+        this.$store.dispatch('logout')
+      },
+      logFeedbackClick () {
+        this.darkModeUnlocked = true
+      }
     }
   }
-}
 </script>
 
 <style scoped>
diff --git a/frontend/src/components/subscriptions/SubscriptionList.vue b/frontend/src/components/subscriptions/SubscriptionList.vue
index 4ce05b34..cd63a0c0 100644
--- a/frontend/src/components/subscriptions/SubscriptionList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionList.vue
@@ -2,7 +2,7 @@
   <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)" style="min-width: fit-content;">
+      <v-toolbar-title v-if="showDetail" style="min-width: fit-content;">
         Tasks
       </v-toolbar-title>
       <v-spacer/>
@@ -16,7 +16,7 @@
         />
       </v-btn>
     </v-toolbar>
-    <v-tabs grow color="teal lighten-1" v-model="selectedStage">
+    <v-tabs grow color="teal lighten-1" v-model="selectedStage" v-if="showDetail">
       <v-tab v-for="(item, i) in stagesReadable" :key="i">
         {{item}}
       </v-tab>
@@ -28,62 +28,65 @@
 </template>
 
 <script>
-import {mapGetters, mapActions, mapState} from 'vuex'
-import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation'
-import SubscriptionForList from '@/components/subscriptions/SubscriptionForList'
-import SubscriptionsForStage from '@/components/subscriptions/SubscriptionsForStage'
+  import {mapGetters, mapActions, mapState} from 'vuex'
+  import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation'
+  import SubscriptionForList from '@/components/subscriptions/SubscriptionForList'
+  import SubscriptionsForStage from '@/components/subscriptions/SubscriptionsForStage'
 
-export default {
-  components: {
-    SubscriptionsForStage,
-    SubscriptionForList,
-    SubscriptionCreation},
-  name: 'subscription-list',
-  props: {
-    sidebar: {
-      type: Boolean,
-      default: false
+  export default {
+    components: {
+      SubscriptionsForStage,
+      SubscriptionForList,
+      SubscriptionCreation},
+    name: 'subscription-list',
+    props: {
+      sidebar: {
+        type: Boolean,
+        default: false
+      }
+    },
+    data () {
+      return {
+        selectedStage: null,
+        updating: false
+      }
+    },
+    computed: {
+      ...mapState({
+        sideBarCollapsed: state => state.ui.sideBarCollapsed
+      }),
+      ...mapGetters({
+        subscriptions: 'getSubscriptionsGroupedByType',
+        stages: 'availableStages',
+        stagesReadable: 'availableStagesReadable'
+      }),
+      showDetail () {
+        return !this.sidebar || (this.sidebar && !this.sideBarCollapsed)
+      }
+    },
+    methods: {
+      ...mapActions([
+        'updateSubmissionTypes',
+        'getCurrentAssignment',
+        'getExamTypes',
+        'subscribeToAll',
+        'cleanAssignmentsFromSubscriptions'
+      ]),
+      async getSubscriptions () {
+        this.updating = true
+        const subscriptions = await this.$store.dispatch('getSubscriptions')
+        this.updating = false
+        return subscriptions
+      }
+    },
+    created () {
+      const typesAndSubscriptions = [this.updateSubmissionTypes(), this.getSubscriptions()]
+      Promise.all(typesAndSubscriptions).then(() => {
+        this.subscribeToAll()
+        this.cleanAssignmentsFromSubscriptions()
+      })
     }
-  },
-  data () {
-    return {
-      selectedStage: null,
-      updating: false
-    }
-  },
-  computed: {
-    ...mapState({
-      sideBarCollapsed: state => state.ui.sideBarCollapsed
-    }),
-    ...mapGetters({
-      subscriptions: 'getSubscriptionsGroupedByType',
-      stages: 'availableStages',
-      stagesReadable: 'availableStagesReadable'
-    })
-  },
-  methods: {
-    ...mapActions([
-      'updateSubmissionTypes',
-      'getCurrentAssignment',
-      'getExamTypes',
-      'subscribeToAll',
-      'cleanAssignmentsFromSubscriptions'
-    ]),
-    async getSubscriptions () {
-      this.updating = true
-      const subscriptions = await this.$store.dispatch('getSubscriptions')
-      this.updating = false
-      return subscriptions
-    }
-  },
-  created () {
-    const typesAndSubscriptions = [this.updateSubmissionTypes(), this.getSubscriptions()]
-    Promise.all(typesAndSubscriptions).then(() => {
-      this.subscribeToAll()
-      this.cleanAssignmentsFromSubscriptions()
-    })
   }
-}
 </script>
 
 <style scoped>
diff --git a/frontend/src/util/helpers.js b/frontend/src/util/helpers.js
index c18c724f..a582d317 100644
--- a/frontend/src/util/helpers.js
+++ b/frontend/src/util/helpers.js
@@ -51,7 +51,9 @@ export function createComputedGetterSetter ({path, mutation, namespace}) {
  */
 export function mapStateToComputedGetterSetter ({namespace = '', pathPrefix = '', items = []}) {
   return items.reduce((acc, curr) => {
-    let path = pathPrefix ? `${pathPrefix}.${curr.path}` : curr.path
+    // if no path is give, use name
+    const itemPath = curr.path || curr.name
+    const path = pathPrefix ? `${pathPrefix}.${itemPath}` : itemPath
     acc[curr.name] = createComputedGetterSetter({...curr, path, namespace})
     return acc
   }, {})
-- 
GitLab


From 1c790526da1cc8e5e2deec7dc8086bd8dbfc6e36 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Tue, 15 May 2018 23:23:06 +0200
Subject: [PATCH 03/15] WelcomeJumbotron now hidable

---
 frontend/src/components/WelcomeJumbotron.vue | 37 ++++++++++++++++----
 frontend/src/store/modules/ui.js             |  9 +++--
 2 files changed, 37 insertions(+), 9 deletions(-)

diff --git a/frontend/src/components/WelcomeJumbotron.vue b/frontend/src/components/WelcomeJumbotron.vue
index d3d9151f..4c140e5c 100644
--- a/frontend/src/components/WelcomeJumbotron.vue
+++ b/frontend/src/components/WelcomeJumbotron.vue
@@ -1,5 +1,10 @@
 <template>
-    <v-jumbotron :gradient="gradient" dark class="elevation-10">
+    <v-jumbotron :gradient="gradient" dark class="elevation-10" v-if="showJumbotron">
+      <v-btn @click="hide" icon class="hide-btn">
+        <v-icon>
+          close
+        </v-icon>
+      </v-btn>
       <v-container fill-height>
         <v-layout align-center>
           <v-flex>
@@ -26,14 +31,28 @@
 </template>
 
 <script>
-export default {
-  name: 'welcome-jumbotron',
-  data () {
-    return {
-      gradient: 'to bottom, #1A237E, #5753DD'
+  import { createComputedGetterSetter } from '@/util/helpers'
+  import { uiMut } from '@/store/modules/ui'
+
+  export default {
+    name: 'welcome-jumbotron',
+    data () {
+      return {
+        gradient: 'to bottom, #1A237E, #5753DD'
+      }
+    },
+    computed: {
+      showJumbotron: createComputedGetterSetter({
+        path: 'ui.showJumbotron',
+        mutation: uiMut.SET_SHOW_JUMBOTRON
+      })
+    },
+    methods: {
+      hide () {
+        this.showJumbotron = false
+      }
     }
   }
-}
 </script>
 
 <style scoped>
@@ -41,4 +60,8 @@ export default {
     color: lightgrey;
     text-decoration: none;
   }
+  .hide-btn {
+    position: absolute;
+    right: 0px;
+  }
 </style>
diff --git a/frontend/src/store/modules/ui.js b/frontend/src/store/modules/ui.js
index df08d683..2cecc601 100644
--- a/frontend/src/store/modules/ui.js
+++ b/frontend/src/store/modules/ui.js
@@ -3,14 +3,16 @@ function initialState () {
   return {
     sideBarCollapsed: false,
     darkMode: false,
-    darkModeUnlocked: false
+    darkModeUnlocked: false,
+    showJumbotron: true
   }
 }
 
 export const uiMut = Object.freeze({
   SET_SIDEBAR_COLLAPSED: 'SET_SIDEBAR_COLLAPSED',
   SET_DARK_MODE: 'SET_DARK_MODE',
-  SET_DARK_MODE_UNLOCKED: 'SET_DARK_MODE_UNLOCKED'
+  SET_DARK_MODE_UNLOCKED: 'SET_DARK_MODE_UNLOCKED',
+  SET_SHOW_JUMBOTRON: 'SET_SHOW_JUMBOTRON'
 })
 
 const ui = {
@@ -24,6 +26,9 @@ const ui = {
     },
     [uiMut.SET_DARK_MODE_UNLOCKED] (state, val) {
       state.darkModeUnlocked = val
+    },
+    [uiMut.SET_SHOW_JUMBOTRON] (state, val) {
+      state.showJumbotron = val
     }
   }
 }
-- 
GitLab


From b479780bd483dc8784504b2a71165c3ad599e757 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Sun, 5 Aug 2018 12:33:32 +0200
Subject: [PATCH 04/15] Fixed some button positioning

---
 frontend/src/components/BaseLayout.vue        |  91 +++++++-------
 frontend/src/components/WelcomeJumbotron.vue  |  43 ++++---
 .../submission_notes/base/FeedbackComment.vue |   7 +-
 .../subscriptions/SubscriptionList.vue        | 112 +++++++++---------
 4 files changed, 127 insertions(+), 126 deletions(-)

diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue
index 9ab1ba03..87a14851 100644
--- a/frontend/src/components/BaseLayout.vue
+++ b/frontend/src/components/BaseLayout.vue
@@ -84,53 +84,56 @@
 </template>
 
 <script>
-  import { mapGetters, mapState } from 'vuex'
-  import {uiMut} from '@/store/modules/ui'
-  import { mapStateToComputedGetterSetter } from '@/util/helpers'
-  export default {
-    name: 'base-layout',
-    computed: {
-      ...mapGetters([
-        'gradySpeak'
-      ]),
-      ...mapState({
-        username: state => state.authentication.username,
-        userRole: state => state.authentication.userRole
-      }),
-      ...mapStateToComputedGetterSetter({
-        pathPrefix: 'ui',
-        items: [
-          {
-            name: 'darkMode',
-            mutation: uiMut.SET_DARK_MODE
-          },
-          {
-            name: 'darkModeUnlocked',
-            mutation: uiMut.SET_DARK_MODE_UNLOCKED
-          },
-          {
-            name: 'mini',
-            path: 'sideBarCollapsed',
-            mutation: uiMut.SET_SIDEBAR_COLLAPSED
-          }
-        ]
-      }),
-      production () {
-        return process.env.NODE_ENV === 'production'
-      },
-      productionBrandUrl () {
-        return `https://${window.location.host}/static/img/brand.png`
-      }
+import { mapGetters, mapState } from 'vuex'
+import {uiMut} from '@/store/modules/ui'
+import { mapStateToComputedGetterSetter } from '@/util/helpers'
+import UserOptions from '@/components/UserOptions'
+
+export default {
+  name: 'base-layout',
+  components: {UserOptions},
+  computed: {
+    ...mapGetters([
+      'gradySpeak'
+    ]),
+    ...mapState({
+      username: state => state.authentication.user.username,
+      userRole: state => state.authentication.user.role
+    }),
+    ...mapStateToComputedGetterSetter({
+      pathPrefix: 'ui',
+      items: [
+        {
+          name: 'darkMode',
+          mutation: uiMut.SET_DARK_MODE
+        },
+        {
+          name: 'darkModeUnlocked',
+          mutation: uiMut.SET_DARK_MODE_UNLOCKED
+        },
+        {
+          name: 'mini',
+          path: 'sideBarCollapsed',
+          mutation: uiMut.SET_SIDEBAR_COLLAPSED
+        }
+      ]
+    }),
+    production () {
+      return process.env.NODE_ENV === 'production'
+    },
+    productionBrandUrl () {
+      return `https://${window.location.host}/static/img/brand.png`
+    }
+  },
+  methods: {
+    logout () {
+      this.$store.dispatch('logout')
     },
-    methods: {
-      logout () {
-        this.$store.dispatch('logout')
-      },
-      logFeedbackClick () {
-        this.darkModeUnlocked = true
-      }
+    logFeedbackClick () {
+      this.darkModeUnlocked = true
     }
   }
+}
 </script>
 
 <style scoped>
diff --git a/frontend/src/components/WelcomeJumbotron.vue b/frontend/src/components/WelcomeJumbotron.vue
index 4c140e5c..687274a6 100644
--- a/frontend/src/components/WelcomeJumbotron.vue
+++ b/frontend/src/components/WelcomeJumbotron.vue
@@ -1,6 +1,6 @@
 <template>
     <v-jumbotron :gradient="gradient" dark class="elevation-10" v-if="showJumbotron">
-      <v-btn @click="hide" icon class="hide-btn">
+      <v-btn @click="hide" icon class="hide-btn" absolute>
         <v-icon>
           close
         </v-icon>
@@ -31,28 +31,28 @@
 </template>
 
 <script>
-  import { createComputedGetterSetter } from '@/util/helpers'
-  import { uiMut } from '@/store/modules/ui'
+import { createComputedGetterSetter } from '@/util/helpers'
+import { uiMut } from '@/store/modules/ui'
 
-  export default {
-    name: 'welcome-jumbotron',
-    data () {
-      return {
-        gradient: 'to bottom, #1A237E, #5753DD'
-      }
-    },
-    computed: {
-      showJumbotron: createComputedGetterSetter({
-        path: 'ui.showJumbotron',
-        mutation: uiMut.SET_SHOW_JUMBOTRON
-      })
-    },
-    methods: {
-      hide () {
-        this.showJumbotron = false
-      }
+export default {
+  name: 'welcome-jumbotron',
+  data () {
+    return {
+      gradient: 'to bottom, #1A237E, #5753DD'
+    }
+  },
+  computed: {
+    showJumbotron: createComputedGetterSetter({
+      path: 'ui.showJumbotron',
+      mutation: uiMut.SET_SHOW_JUMBOTRON
+    })
+  },
+  methods: {
+    hide () {
+      this.showJumbotron = false
     }
   }
+}
 </script>
 
 <style scoped>
@@ -61,7 +61,6 @@
     text-decoration: none;
   }
   .hide-btn {
-    position: absolute;
-    right: 0px;
+    right: 0;
   }
 </style>
diff --git a/frontend/src/components/submission_notes/base/FeedbackComment.vue b/frontend/src/components/submission_notes/base/FeedbackComment.vue
index 93591485..c45d38a9 100644
--- a/frontend/src/components/submission_notes/base/FeedbackComment.vue
+++ b/frontend/src/components/submission_notes/base/FeedbackComment.vue
@@ -21,7 +21,7 @@
       </div>
       <div class="message">{{text}}</div>
       <v-btn
-        flat icon
+        flat icon absolute
         class="delete-button"
         v-if="deletable"
         @click.stop="toggleDeleteComment"
@@ -144,9 +144,8 @@ export default {
     white-space: pre-wrap;
   }
   .delete-button {
-    position: absolute;
-    bottom: -20px;
-    left: -50px;
+    bottom: -15px;
+    left: -42px;
   }
   .comment-created {
     position: absolute;
diff --git a/frontend/src/components/subscriptions/SubscriptionList.vue b/frontend/src/components/subscriptions/SubscriptionList.vue
index cd63a0c0..2e8b7f72 100644
--- a/frontend/src/components/subscriptions/SubscriptionList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionList.vue
@@ -28,65 +28,65 @@
 </template>
 
 <script>
-  import {mapGetters, mapActions, mapState} from 'vuex'
-  import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation'
-  import SubscriptionForList from '@/components/subscriptions/SubscriptionForList'
-  import SubscriptionsForStage from '@/components/subscriptions/SubscriptionsForStage'
+import {mapGetters, mapActions, mapState} from 'vuex'
+import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation'
+import SubscriptionForList from '@/components/subscriptions/SubscriptionForList'
+import SubscriptionsForStage from '@/components/subscriptions/SubscriptionsForStage'
 
-  export default {
-    components: {
-      SubscriptionsForStage,
-      SubscriptionForList,
-      SubscriptionCreation},
-    name: 'subscription-list',
-    props: {
-      sidebar: {
-        type: Boolean,
-        default: false
-      }
-    },
-    data () {
-      return {
-        selectedStage: null,
-        updating: false
-      }
-    },
-    computed: {
-      ...mapState({
-        sideBarCollapsed: state => state.ui.sideBarCollapsed
-      }),
-      ...mapGetters({
-        subscriptions: 'getSubscriptionsGroupedByType',
-        stages: 'availableStages',
-        stagesReadable: 'availableStagesReadable'
-      }),
-      showDetail () {
-        return !this.sidebar || (this.sidebar && !this.sideBarCollapsed)
-      }
-    },
-    methods: {
-      ...mapActions([
-        'updateSubmissionTypes',
-        'getCurrentAssignment',
-        'getExamTypes',
-        'subscribeToAll',
-        'cleanAssignmentsFromSubscriptions'
-      ]),
-      async getSubscriptions () {
-        this.updating = true
-        const subscriptions = await this.$store.dispatch('getSubscriptions')
-        this.updating = false
-        return subscriptions
-      }
-    },
-    created () {
-      const typesAndSubscriptions = [this.updateSubmissionTypes(), this.getSubscriptions()]
-      Promise.all(typesAndSubscriptions).then(() => {
-        this.subscribeToAll()
-        this.cleanAssignmentsFromSubscriptions()
-      })
+export default {
+  components: {
+    SubscriptionsForStage,
+    SubscriptionForList,
+    SubscriptionCreation},
+  name: 'subscription-list',
+  props: {
+    sidebar: {
+      type: Boolean,
+      default: false
     }
+  },
+  data () {
+    return {
+      selectedStage: null,
+      updating: false
+    }
+  },
+  computed: {
+    ...mapState({
+      sideBarCollapsed: state => state.ui.sideBarCollapsed
+    }),
+    ...mapGetters({
+      subscriptions: 'getSubscriptionsGroupedByType',
+      stages: 'availableStages',
+      stagesReadable: 'availableStagesReadable'
+    }),
+    showDetail () {
+      return !this.sidebar || (this.sidebar && !this.sideBarCollapsed)
+    }
+  },
+  methods: {
+    ...mapActions([
+      'updateSubmissionTypes',
+      'getCurrentAssignment',
+      'getExamTypes',
+      'subscribeToAll',
+      'cleanAssignmentsFromSubscriptions'
+    ]),
+    async getSubscriptions () {
+      this.updating = true
+      const subscriptions = await this.$store.dispatch('getSubscriptions')
+      this.updating = false
+      return subscriptions
+    }
+  },
+  created () {
+    const typesAndSubscriptions = [this.updateSubmissionTypes(), this.getSubscriptions()]
+    Promise.all(typesAndSubscriptions).then(() => {
+      this.subscribeToAll()
+      this.cleanAssignmentsFromSubscriptions()
+    })
   }
+}
 </script>
 
 <style scoped>
-- 
GitLab


From 1a025710b6d8493aae2c733cb604f17b66e01265 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Sun, 5 Aug 2018 12:37:08 +0200
Subject: [PATCH 05/15] Fixed static files in prod

---
 frontend/vue.config.js    | 1 +
 grady/settings/default.py | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/frontend/vue.config.js b/frontend/vue.config.js
index 78bc4df2..6c2485df 100644
--- a/frontend/vue.config.js
+++ b/frontend/vue.config.js
@@ -3,6 +3,7 @@ const path = require('path')
 const projectRoot = path.resolve(__dirname)
 
 module.exports = {
+  assetsDir: 'static',
   configureWebpack: {
     resolve: {
       alias: {
diff --git a/grady/settings/default.py b/grady/settings/default.py
index 8099ca89..eb220958 100644
--- a/grady/settings/default.py
+++ b/grady/settings/default.py
@@ -116,7 +116,7 @@ STATICFILES_FINDERS = (
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
 )
 STATICFILES_DIRS = (
-    'frontend/dist/',
+    'frontend/dist/static',
 )
 
 
-- 
GitLab


From 3ccd714822138361370b8de759d67780ca4204f4 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Sun, 5 Aug 2018 14:06:29 +0200
Subject: [PATCH 06/15] Added localhost to webpack devserver allowed hosts

---
 frontend/vue.config.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/frontend/vue.config.js b/frontend/vue.config.js
index 6c2485df..29c09438 100644
--- a/frontend/vue.config.js
+++ b/frontend/vue.config.js
@@ -4,6 +4,9 @@ const projectRoot = path.resolve(__dirname)
 
 module.exports = {
   assetsDir: 'static',
+  devServer: {
+    allowedHosts: ['localhost']
+  },
   configureWebpack: {
     resolve: {
       alias: {
-- 
GitLab


From 7ab2e07359692767df7c994a3c5eb063c159dc5f Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Sun, 5 Aug 2018 14:07:04 +0200
Subject: [PATCH 07/15] Tutormanager migration

---
 core/migrations/0010_auto_20180805_1139.py | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)
 create mode 100644 core/migrations/0010_auto_20180805_1139.py

diff --git a/core/migrations/0010_auto_20180805_1139.py b/core/migrations/0010_auto_20180805_1139.py
new file mode 100644
index 00000000..622d7e5a
--- /dev/null
+++ b/core/migrations/0010_auto_20180805_1139.py
@@ -0,0 +1,22 @@
+# Generated by Django 2.1 on 2018-08-05 11:39
+
+import core.models
+import django.contrib.auth.models
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0009_auto_20180320_2335'),
+    ]
+
+    operations = [
+        migrations.AlterModelManagers(
+            name='useraccount',
+            managers=[
+                ('objects', django.contrib.auth.models.UserManager()),
+                ('tutors', core.models.TutorManager()),
+            ],
+        ),
+    ]
-- 
GitLab


From 8fe74889c5cb811959e10aa142bb94275f6deeab Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Sun, 5 Aug 2018 20:34:48 +0200
Subject: [PATCH 08/15] Fixed some button positioning / dialog widths

---
 frontend/src/components/AutoLogout.vue                        | 2 +-
 frontend/src/components/submission_notes/base/CommentForm.vue | 4 ++--
 .../src/components/submission_notes/base/FeedbackComment.vue  | 2 +-
 frontend/src/pages/Login.vue                                  | 2 +-
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/frontend/src/components/AutoLogout.vue b/frontend/src/components/AutoLogout.vue
index 76e69552..3c29cf12 100644
--- a/frontend/src/components/AutoLogout.vue
+++ b/frontend/src/components/AutoLogout.vue
@@ -1,7 +1,7 @@
 <template>
   <v-dialog
     persistent
-    width="fit-content"
+    max-width="30%"
     v-model="logoutDialog"
   >
     <v-card>
diff --git a/frontend/src/components/submission_notes/base/CommentForm.vue b/frontend/src/components/submission_notes/base/CommentForm.vue
index a8c1949d..603f4398 100644
--- a/frontend/src/components/submission_notes/base/CommentForm.vue
+++ b/frontend/src/components/submission_notes/base/CommentForm.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <v-text-field
+    <v-textarea
       name="feedback-input"
       label="Please provide your feedback here"
       v-model="currentFeedback"
@@ -8,7 +8,7 @@
       @keyup.esc="collapseTextField"
       @focus="selectInput($event)"
       rows="2"
-      textarea
+      outline
       autofocus
       auto-grow
       hide-details
diff --git a/frontend/src/components/submission_notes/base/FeedbackComment.vue b/frontend/src/components/submission_notes/base/FeedbackComment.vue
index c45d38a9..5b29b56c 100644
--- a/frontend/src/components/submission_notes/base/FeedbackComment.vue
+++ b/frontend/src/components/submission_notes/base/FeedbackComment.vue
@@ -144,7 +144,7 @@ export default {
     white-space: pre-wrap;
   }
   .delete-button {
-    bottom: -15px;
+    bottom: -12px;
     left: -42px;
   }
   .comment-created {
diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue
index 972b08b5..0988645e 100644
--- a/frontend/src/pages/Login.vue
+++ b/frontend/src/pages/Login.vue
@@ -1,7 +1,7 @@
 <template>
   <v-container fill-height>
     <v-layout align-center justify-center>
-      <v-dialog v-model="registerDialog" max-width="fit-content" class="pa-4">
+      <v-dialog v-model="registerDialog" class="pa-4" max-width="30%">
         <register-dialog @registered="registered($event)"/>
       </v-dialog>
       <v-flex text-xs-center xs8 sm6 md4 lg2>
-- 
GitLab


From ff996d22b6be648be0537e13ef6b01939722ce5a Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Sun, 5 Aug 2018 21:34:44 +0200
Subject: [PATCH 09/15] Fixed Export button closes #114

---
 frontend/src/components/DataExport.vue | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/frontend/src/components/DataExport.vue b/frontend/src/components/DataExport.vue
index 2b3c5868..451fd950 100644
--- a/frontend/src/components/DataExport.vue
+++ b/frontend/src/components/DataExport.vue
@@ -163,8 +163,4 @@ export default {
 </script>
 
 <style scoped>
-  #export-link {
-    color: #000;
-    text-decoration: none;
-  }
 </style>
-- 
GitLab


From 200750722241590cff650161b4d23728a856c00a Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Sun, 5 Aug 2018 21:35:44 +0200
Subject: [PATCH 10/15] Started typing js files

---
 frontend/@types/v-clipboard/index.d.ts |  1 +
 frontend/src/main.ts                   |  4 ++--
 frontend/src/router/index.ts           | 18 ++++++++++--------
 frontend/src/store/getters.js          |  2 +-
 frontend/tsconfig.json                 |  8 ++------
 5 files changed, 16 insertions(+), 17 deletions(-)
 create mode 100644 frontend/@types/v-clipboard/index.d.ts

diff --git a/frontend/@types/v-clipboard/index.d.ts b/frontend/@types/v-clipboard/index.d.ts
new file mode 100644
index 00000000..f2dd4459
--- /dev/null
+++ b/frontend/@types/v-clipboard/index.d.ts
@@ -0,0 +1 @@
+declare module 'v-clipboard';
\ No newline at end of file
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 5576736f..ad81436e 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -4,13 +4,13 @@ import router from './router/index'
 import store from './store/store'
 import Vuetify from 'vuetify'
 import Notifications from 'vue-notification'
-import Cliboard from 'v-clipboard'
+import Clipboard from 'v-clipboard'
 
 import 'vuetify/dist/vuetify.min.css'
 import 'highlight.js/styles/atom-one-light.css'
 
 Vue.use(Vuetify)
-Vue.use(Cliboard)
+Vue.use(Clipboard)
 Vue.use(Notifications)
 
 Vue.config.productionTip = false
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index e780522f..25b1aeee 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -1,5 +1,5 @@
 import Vue from 'vue'
-import Router from 'vue-router'
+import Router, {RawLocation, Route, NavigationGuard} from 'vue-router'
 import Login from '@/pages/Login.vue'
 import StudentSubmissionPage from '@/pages/student/StudentSubmissionPage.vue'
 import StudentOverviewPage from '@/pages/reviewer/StudentOverviewPage.vue'
@@ -19,7 +19,9 @@ import store from '@/store/store'
 
 Vue.use(Router)
 
-function denyAccess (next, redirect) {
+type rerouteFunc = (to?: RawLocation | false | ((vm: Vue) => any) | void) => void
+
+function denyAccess (next: rerouteFunc, redirect: Route) {
   next(redirect.path)
   VueInstance.$notify({
     title: 'Access denied',
@@ -28,23 +30,23 @@ function denyAccess (next, redirect) {
   })
 }
 
-function tutorOrReviewerOnly (to, from, next) {
+let tutorOrReviewerOnly: NavigationGuard = function (to, from, next) {
   if (store.getters.isTutorOrReviewer) {
     next()
   } else {
-    denyAccess(next, from.path)
+    denyAccess(next, from)
   }
 }
 
-function reviewerOnly (to, from, next) {
+let reviewerOnly: NavigationGuard = function (to, from, next) {
   if (store.getters.isReviewer) {
     next()
   } else {
-    denyAccess(next, from.path)
+    denyAccess(next, from)
   }
 }
 
-function studentOnly (to, from, next) {
+let studentOnly: NavigationGuard = function (to, from, next) {
   if (store.getters.isStudent) {
     next()
   } else {
@@ -52,7 +54,7 @@ function studentOnly (to, from, next) {
   }
 }
 
-function checkLoggedIn (to, from, next) {
+let checkLoggedIn: NavigationGuard = function (to, from, next) {
   if (store.getters.isLoggedIn) {
     next()
   } else {
diff --git a/frontend/src/store/getters.js b/frontend/src/store/getters.js
index 5663746c..3cdc0cbe 100644
--- a/frontend/src/store/getters.js
+++ b/frontend/src/store/getters.js
@@ -1,7 +1,7 @@
 const getters = {
   corrected (state) {
     return state.statistics.submission_type_progress.every(progress => {
-      return progress.percentage === 100
+      return progress.feedback_final === progress.submission_count
     })
   },
   getSubmission: state => pk => {
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index c1700466..c19146b1 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -2,7 +2,7 @@
   "compilerOptions": {
     "target": "esnext",
     "module": "esnext",
-    "strict": false,
+    "strict": true,
     "jsx": "preserve",
     "importHelpers": true,
     "moduleResolution": "node",
@@ -10,11 +10,6 @@
     "esModuleInterop": true,
     "sourceMap": true,
     "baseUrl": ".",
-    "types": [
-      "node",
-      "mocha",
-      "chai"
-    ],
     "paths": {
       "@/*": [
         "src/*"
@@ -28,6 +23,7 @@
     ]
   },
   "include": [
+    "@types/",
     "src/**/*.ts",
     "src/**/*.tsx",
     "src/**/*.vue",
-- 
GitLab


From f59e38392cbce9ab6d49c69a745e2a85ff9784c8 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Sun, 5 Aug 2018 23:00:43 +0200
Subject: [PATCH 11/15] Renamed store files to .ts

---
 frontend/src/{api.js => api.ts}                                   | 0
 frontend/src/store/{actions.js => actions.ts}                     | 0
 frontend/src/store/{getters.js => getters.ts}                     | 0
 frontend/src/store/{grady_speak.js => grady_speak.ts}             | 0
 .../src/store/modules/{authentication.js => authentication.ts}    | 0
 .../{feedback-search-options.js => feedback-search-options.ts}    | 0
 .../feedback_list/{feedback-table.js => feedback-table.ts}        | 0
 frontend/src/store/modules/{student-page.js => student-page.ts}   | 0
 .../store/modules/{submission-notes.js => submission-notes.ts}    | 0
 frontend/src/store/modules/{subscriptions.js => subscriptions.ts} | 0
 frontend/src/store/modules/{ui.js => ui.ts}                       | 0
 frontend/src/store/{mutations.js => mutations.ts}                 | 0
 .../{lastInteractionPlugin.js => lastInteractionPlugin.ts}        | 0
 frontend/src/util/{helpers.js => helpers.ts}                      | 0
 14 files changed, 0 insertions(+), 0 deletions(-)
 rename frontend/src/{api.js => api.ts} (100%)
 rename frontend/src/store/{actions.js => actions.ts} (100%)
 rename frontend/src/store/{getters.js => getters.ts} (100%)
 rename frontend/src/store/{grady_speak.js => grady_speak.ts} (100%)
 rename frontend/src/store/modules/{authentication.js => authentication.ts} (100%)
 rename frontend/src/store/modules/feedback_list/{feedback-search-options.js => feedback-search-options.ts} (100%)
 rename frontend/src/store/modules/feedback_list/{feedback-table.js => feedback-table.ts} (100%)
 rename frontend/src/store/modules/{student-page.js => student-page.ts} (100%)
 rename frontend/src/store/modules/{submission-notes.js => submission-notes.ts} (100%)
 rename frontend/src/store/modules/{subscriptions.js => subscriptions.ts} (100%)
 rename frontend/src/store/modules/{ui.js => ui.ts} (100%)
 rename frontend/src/store/{mutations.js => mutations.ts} (100%)
 rename frontend/src/store/plugins/{lastInteractionPlugin.js => lastInteractionPlugin.ts} (100%)
 rename frontend/src/util/{helpers.js => helpers.ts} (100%)

diff --git a/frontend/src/api.js b/frontend/src/api.ts
similarity index 100%
rename from frontend/src/api.js
rename to frontend/src/api.ts
diff --git a/frontend/src/store/actions.js b/frontend/src/store/actions.ts
similarity index 100%
rename from frontend/src/store/actions.js
rename to frontend/src/store/actions.ts
diff --git a/frontend/src/store/getters.js b/frontend/src/store/getters.ts
similarity index 100%
rename from frontend/src/store/getters.js
rename to frontend/src/store/getters.ts
diff --git a/frontend/src/store/grady_speak.js b/frontend/src/store/grady_speak.ts
similarity index 100%
rename from frontend/src/store/grady_speak.js
rename to frontend/src/store/grady_speak.ts
diff --git a/frontend/src/store/modules/authentication.js b/frontend/src/store/modules/authentication.ts
similarity index 100%
rename from frontend/src/store/modules/authentication.js
rename to frontend/src/store/modules/authentication.ts
diff --git a/frontend/src/store/modules/feedback_list/feedback-search-options.js b/frontend/src/store/modules/feedback_list/feedback-search-options.ts
similarity index 100%
rename from frontend/src/store/modules/feedback_list/feedback-search-options.js
rename to frontend/src/store/modules/feedback_list/feedback-search-options.ts
diff --git a/frontend/src/store/modules/feedback_list/feedback-table.js b/frontend/src/store/modules/feedback_list/feedback-table.ts
similarity index 100%
rename from frontend/src/store/modules/feedback_list/feedback-table.js
rename to frontend/src/store/modules/feedback_list/feedback-table.ts
diff --git a/frontend/src/store/modules/student-page.js b/frontend/src/store/modules/student-page.ts
similarity index 100%
rename from frontend/src/store/modules/student-page.js
rename to frontend/src/store/modules/student-page.ts
diff --git a/frontend/src/store/modules/submission-notes.js b/frontend/src/store/modules/submission-notes.ts
similarity index 100%
rename from frontend/src/store/modules/submission-notes.js
rename to frontend/src/store/modules/submission-notes.ts
diff --git a/frontend/src/store/modules/subscriptions.js b/frontend/src/store/modules/subscriptions.ts
similarity index 100%
rename from frontend/src/store/modules/subscriptions.js
rename to frontend/src/store/modules/subscriptions.ts
diff --git a/frontend/src/store/modules/ui.js b/frontend/src/store/modules/ui.ts
similarity index 100%
rename from frontend/src/store/modules/ui.js
rename to frontend/src/store/modules/ui.ts
diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.ts
similarity index 100%
rename from frontend/src/store/mutations.js
rename to frontend/src/store/mutations.ts
diff --git a/frontend/src/store/plugins/lastInteractionPlugin.js b/frontend/src/store/plugins/lastInteractionPlugin.ts
similarity index 100%
rename from frontend/src/store/plugins/lastInteractionPlugin.js
rename to frontend/src/store/plugins/lastInteractionPlugin.ts
diff --git a/frontend/src/util/helpers.js b/frontend/src/util/helpers.ts
similarity index 100%
rename from frontend/src/util/helpers.js
rename to frontend/src/util/helpers.ts
-- 
GitLab


From 9de3c6843cd9f9ef2dca317d88e952dfc5f6baf6 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Mon, 6 Aug 2018 14:22:15 +0200
Subject: [PATCH 12/15] Started added typings in store files

---
 frontend/src/api.ts                           | 39 +++++++-------
 .../feedback_list/FeedbackSearchOptions.vue   |  6 ---
 .../RouteChangeConfirmation.vue               |  2 -
 frontend/src/store/modules/authentication.ts  | 53 +++++++++++++------
 frontend/src/util/helpers.ts                  | 46 +++++++++++-----
 frontend/tsconfig.json                        |  4 +-
 frontend/vue.config.js                        |  3 +-
 7 files changed, 94 insertions(+), 59 deletions(-)

diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index d5965da6..029722cd 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -1,6 +1,7 @@
 import axios from 'axios'
+import {Credentials} from "@/store/modules/authentication";
 
-function addFieldsToUrl ({url, fields = []}) {
+function addFieldsToUrl ({url, fields = []}: {url: string, fields?: string[]}) {
   return fields.length > 0 ? url + '?fields=pk,' + fields : url
 }
 
@@ -22,27 +23,27 @@ let ax = axios.create({
   }
 }
 
-export async function registerTutor (credentials) {
+export async function registerTutor (credentials: Credentials) {
   return ax.post('/api/tutor/register/', credentials)
 }
 
-export async function fetchJWT (credentials) {
-  const token = (await ax.post('/api/get-token/', credentials)).data.token
+export async function fetchJWT (credentials: Credentials): Promise<string> {
+  const token: string = (await ax.post('/api/get-token/', credentials)).data.token
   ax.defaults.headers['Authorization'] = `JWT ${token}`
   return token
 }
 
-export async function refreshJWT (token) {
-  const newToken = (await ax.post('/api/refresh-token/', {token})).data.token
+export async function refreshJWT (token: string): Promise<string> {
+  const newToken: string = (await ax.post('/api/refresh-token/', {token})).data.token
   ax.defaults.headers['Authorization'] = `JWT ${newToken}`
   return newToken
 }
 
-export async function fetchJWTTimeDelta () {
+export async function fetchJWTTimeDelta (): Promise<number> {
   return (await ax.get('/api/jwt-time-delta/')).data.timeDelta
 }
 
-export async function fetchUserRole () {
+export async function fetchUserRole (): Promise<string> {
   return (await ax.get('/api/user-role/')).data.role
 }
 
@@ -54,11 +55,11 @@ export async function fetchStudentSubmissions () {
   return (await ax.get('/api/student-submissions/')).data
 }
 
-export async function fetchSubmissionFeedbackTests ({pk}) {
+export async function fetchSubmissionFeedbackTests ({pk}: {pk: string}) {
   return (await ax.get(`/api/submission/${pk}/`)).data
 }
 
-export async function fetchAllStudents (fields = []) {
+export async function fetchAllStudents (fields: string[] = []) {
   const url = addFieldsToUrl({
     url: '/api/student/',
     fields
@@ -66,7 +67,8 @@ export async function fetchAllStudents (fields = []) {
   return (await ax.get(url)).data
 }
 
-export async function fetchStudent ({pk, fields = []}) {
+export async function fetchStudent ({pk, fields = []}:
+                                      {pk: string, fields?: string[]}) {
   const url = addFieldsToUrl({
     url: `/api/student/${pk}/`,
     fields
@@ -74,7 +76,7 @@ export async function fetchStudent ({pk, fields = []}) {
   return (await ax.get(url)).data
 }
 
-export async function fetchAllTutors (fields = []) {
+export async function fetchAllTutors (fields: string[] = []) {
   const url = addFieldsToUrl({
     url: '/api/tutor/',
     fields
@@ -86,16 +88,16 @@ export async function fetchSubscriptions () {
   return (await ax.get('/api/subscription/')).data
 }
 
-export async function deactivateSubscription ({pk}) {
+export async function deactivateSubscription ({pk}: {pk: string}) {
   const url = `/api/subscription/${pk}/`
   return (await ax.delete(url)).data
 }
 
-export async function fetchSubscription (subscriptionPk) {
+export async function fetchSubscription (subscriptionPk: string) {
   return (await ax.get(`/api/subscription/${subscriptionPk}/`)).data
 }
 
-export async function fetchAllFeedback (fields = []) {
+export async function fetchAllFeedback (fields: string[] = []) {
   const url = addFieldsToUrl({
     url: '/api/feedback/',
     fields
@@ -108,7 +110,8 @@ export async function fetchFeedback ({ofSubmission}) {
   return (await ax.get(url)).data
 }
 
-export async function fetchExamType ({examPk, fields = []}) {
+export async function fetchExamType ({examPk, fields = []}:
+                                       {examPk: string, fields?: string[]}) {
   const url = addFieldsToUrl({
     url: `/api/examtype/${examPk !== undefined ? examPk + '/' : ''}`,
     fields})
@@ -189,7 +192,7 @@ export async function deactivateAllStudentAccess () {
   return ax.post('/api/student/deactivate/')
 }
 
-export async function changePassword (userPk, data) {
+export async function changePassword (userPk: string, data) {
   return ax.patch(`/api/user/${userPk}/change_password/`, data)
 }
 
@@ -197,7 +200,7 @@ export async function getOwnUser () {
   return (await ax.get('/api/user/me/')).data
 }
 
-export async function changeActiveForUser (userPk, active) {
+export async function changeActiveForUser (userPk: string, active: boolean) {
   return (await ax.patch(`/api/user/${userPk}/change_active/`, {'is_active': active})).data
 }
 
diff --git a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
index 03e7d84f..c5ec661f 100644
--- a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
+++ b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue
@@ -88,32 +88,26 @@ export default {
       items: [
         {
           name: 'showFinal',
-          path: 'showFinal',
           mutation: feedbackSearchOptsMut.SET_SHOW_FINAL
         },
         {
           name: 'searchOtherUserComments',
-          path: 'searchOtherUserComments',
           mutation: feedbackSearchOptsMut.SET_SEARCH_OTHER_USER_COMMENTS
         },
         {
           name: 'caseSensitive',
-          path: 'caseSensitive',
           mutation: feedbackSearchOptsMut.SET_CASE_SENSITIVE
         },
         {
           name: 'useRegex',
-          path: 'useRegex',
           mutation: feedbackSearchOptsMut.SET_USE_REGEX
         },
         {
           name: 'filterByTutors',
-          path: 'filterByTutors',
           mutation: feedbackSearchOptsMut.SET_FILTER_BY_TUTORS
         },
         {
           name: 'filterByStage',
-          path: 'filterByStage',
           mutation: feedbackSearchOptsMut.SET_FILTER_BY_STAGE
         }
 
diff --git a/frontend/src/components/submission_notes/RouteChangeConfirmation.vue b/frontend/src/components/submission_notes/RouteChangeConfirmation.vue
index ce5a4b26..362980ee 100644
--- a/frontend/src/components/submission_notes/RouteChangeConfirmation.vue
+++ b/frontend/src/components/submission_notes/RouteChangeConfirmation.vue
@@ -41,10 +41,8 @@ export default {
   watch: {
     nextRoute (newVal, oldVal) {
       if (newVal !== oldVal && this.$store.getters['submissionNotes/workInProgress']) {
-        console.log('here')
         this.dialog = true
       } else {
-        console.log('there')
         this.nextRoute()
       }
     }
diff --git a/frontend/src/store/modules/authentication.ts b/frontend/src/store/modules/authentication.ts
index ce8bbb04..b0101d5d 100644
--- a/frontend/src/store/modules/authentication.ts
+++ b/frontend/src/store/modules/authentication.ts
@@ -1,9 +1,28 @@
 import * as api from '@/api'
 import gradySays from '../grady_speak'
+import {ActionContext, Module} from "vuex";
 
-function initialState () {
+export interface Credentials {
+    username: string,
+    password: string
+}
+
+interface AuthState {
+    token: string,
+    lastTokenRefreshTry: number,
+    refreshingToken: boolean,
+    jwtTimeDelta: number,
+    message: string,
+    user: {
+        pk: string,
+        username: string,
+        role: string,
+        is_admin: boolean
+    }
+}
+function initialState (): AuthState {
   return {
-    token: sessionStorage.getItem('token'),
+    token: sessionStorage.getItem('token') || '',
     lastTokenRefreshTry: Date.now(),
     refreshingToken: false,
     jwtTimeDelta: 0,
@@ -12,7 +31,7 @@ function initialState () {
       pk: '',
       username: '',
       role: '',
-      is_admin: ''
+      is_admin: false
     }
   }
 }
@@ -27,53 +46,53 @@ export const authMut = Object.freeze({
   SET_REFRESHING_TOKEN: 'SET_REFRESHING_TOKEN'
 })
 
-const authentication = {
+const authentication: Module<AuthState, any> = {
   state: initialState(),
   getters: {
     gradySpeak: () => {
       return gradySays[Math.floor(Math.random() * gradySays.length)]
     },
-    isStudent: state => {
+    isStudent: (state: AuthState) => {
       return state.user.role === 'Student'
     },
-    isTutor: state => {
+    isTutor: (state: AuthState)=> {
       return state.user.role === 'Tutor'
     },
-    isReviewer: state => {
+    isReviewer: (state: AuthState)=> {
       return state.user.role === 'Reviewer'
     },
-    isTutorOrReviewer: (state, getters) => {
+    isTutorOrReviewer: (state: AuthState, getters) => {
       return getters.isTutor || getters.isReviewer
     },
-    isLoggedIn: state => !!state.token
+    isLoggedIn: (state: AuthState) => !!state.token
   },
   mutations: {
-    [authMut.SET_MESSAGE] (state, message) {
+    [authMut.SET_MESSAGE] (state: AuthState, message: string) {
       state.message = message
     },
-    [authMut.SET_JWT_TOKEN] (state, token) {
+    [authMut.SET_JWT_TOKEN] (state: AuthState, token: string) {
       sessionStorage.setItem('token', token)
       state.token = token
     },
-    [authMut.SET_JWT_TIME_DELTA] (state, timeDelta) {
+    [authMut.SET_JWT_TIME_DELTA] (state: AuthState, timeDelta: number) {
       state.jwtTimeDelta = timeDelta
     },
-    [authMut.SET_USER] (state, user) {
+    [authMut.SET_USER] (state: AuthState, user) {
       state.user = user
     },
-    [authMut.SET_REFRESHING_TOKEN] (state, refreshing) {
+    [authMut.SET_REFRESHING_TOKEN] (state: AuthState, refreshing: boolean) {
       state.refreshingToken = refreshing
     },
-    [authMut.SET_LAST_TOKEN_REFRESH_TRY] (state) {
+    [authMut.SET_LAST_TOKEN_REFRESH_TRY] (state: AuthState) {
       state.lastTokenRefreshTry = Date.now()
     },
-    [authMut.RESET_STATE] (state) {
+    [authMut.RESET_STATE] (state: AuthState) {
       sessionStorage.setItem('token', '')
       Object.assign(state, initialState())
     }
   },
   actions: {
-    async getJWT (context, credentials) {
+    async getJWT (context: ActionContext<AuthState, any>, credentials: Credentials) {
       try {
         const token = await api.fetchJWT(credentials)
         context.commit(authMut.SET_JWT_TOKEN, token)
diff --git a/frontend/src/util/helpers.ts b/frontend/src/util/helpers.ts
index a582d317..73e8fc49 100644
--- a/frontend/src/util/helpers.ts
+++ b/frontend/src/util/helpers.ts
@@ -1,11 +1,12 @@
+import vueInstance from '@/main.ts'
 
-export function nameSpacer (namespace) {
-  return function (commitType) {
+export function nameSpacer (namespace: string) {
+  return function (commitType: string) {
     return namespace + commitType
   }
 }
 
-export function getObjectValueByPath (obj, path) {
+export function getObjectValueByPath (obj: any, path: string): any {
   // credit: http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key#comment55278413_6491621
   if (!path || path.constructor !== String) return
   path = path.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties
@@ -22,6 +23,11 @@ export function getObjectValueByPath (obj, path) {
   return obj
 }
 
+interface GetSetPair {
+    get: () => any,
+    set: (val: object) => void
+}
+
 /**
  * Use this method to generate a computed property accessing the store for a Vue instance.
  * The get method will return the value at this.$store.state.<path>.
@@ -31,17 +37,29 @@ export function getObjectValueByPath (obj, path) {
  * @param namespace to prepend the mutation type with
  * @returns {*}
  */
-export function createComputedGetterSetter ({path, mutation, namespace}) {
+export function createComputedGetterSetter (
+    {path, mutation, namespace}:
+        {path: string, mutation: string, namespace:string}): GetSetPair {
   return {
-    get () {
-      return getObjectValueByPath(this.$store.state, path)
+    get (): any {
+      return getObjectValueByPath(vueInstance.$store.state, path)
     },
-    set (val) {
-      this.$store.commit(`${namespace ? namespace + '/' : ''}${mutation}`, val)
+    set (val: object): void {
+      vueInstance.$store.commit(`${namespace ? namespace + '/' : ''}${mutation}`, val)
     }
   }
 }
 
+interface StateMapperItem {
+    name: string,
+    mutation: string,
+    path?: string
+}
+
+interface MappedState {
+    [key: string]: GetSetPair
+}
+
 /**
  * Returns an object of generated computed getter/setter pairs.
  * Can be used to quickly bind a stores state and corresponding setters to a vue component
@@ -49,12 +67,14 @@ export function createComputedGetterSetter ({path, mutation, namespace}) {
  * @param pathPrefix if set, all items path will be prepended by the path prefix
  * @param items array that contains objects {name, path, mutation}
  */
-export function mapStateToComputedGetterSetter ({namespace = '', pathPrefix = '', items = []}) {
-  return items.reduce((acc, curr) => {
+export function mapStateToComputedGetterSetter (
+    {namespace = '', pathPrefix = '', items = []}:
+        {namespace: string, pathPrefix: string, items: StateMapperItem[]}): MappedState {
+  return items.reduce((acc: MappedState, curr) => {
     // if no path is give, use name
     const itemPath = curr.path || curr.name
     const path = pathPrefix ? `${pathPrefix}.${itemPath}` : itemPath
-    acc[curr.name] = createComputedGetterSetter({...curr, path, namespace})
+    acc[curr.name] = createComputedGetterSetter({mutation: curr.mutation, path, namespace})
     return acc
   }, {})
 }
@@ -67,13 +87,13 @@ export function cartesian (a, b, ...c) {
 }
 
 // flatten an array
-export function flatten (list) {
+export function flatten (list: any[]): any[] {
   return list.reduce(
     (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
   )
 }
 
-export function objectifyArray (arr, key = 'pk') {
+export function objectifyArray<T> (arr: T[], key = 'pk'): {[key: string]: T} {
   return arr.reduce((acc, curr) => {
     acc[curr[key]] = curr
     return acc
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index c19146b1..c800f3ea 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -2,7 +2,7 @@
   "compilerOptions": {
     "target": "esnext",
     "module": "esnext",
-    "strict": true,
+    "strict": false,
     "jsx": "preserve",
     "importHelpers": true,
     "moduleResolution": "node",
@@ -16,7 +16,7 @@
       ]
     },
     "lib": [
-      "es2015",
+      "es2017",
       "dom",
       "dom.iterable",
       "scripthost"
diff --git a/frontend/vue.config.js b/frontend/vue.config.js
index 29c09438..d3212539 100644
--- a/frontend/vue.config.js
+++ b/frontend/vue.config.js
@@ -5,7 +5,8 @@ const projectRoot = path.resolve(__dirname)
 module.exports = {
   assetsDir: 'static',
   devServer: {
-    allowedHosts: ['localhost']
+    allowedHosts: ['localhost'],
+    host: 'localhost'
   },
   configureWebpack: {
     resolve: {
-- 
GitLab


From d0091de4afd88a0b024c557ef03096beca6e9afa Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Mon, 6 Aug 2018 15:31:50 +0200
Subject: [PATCH 13/15] Disabled staging

---
 .gitlab-ci.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3e28a168..74e005ab 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -104,6 +104,7 @@ pages:
 .staging_template: &staging_definition
    stage: staging
    image: docker:latest
+   when: manual
    only:
      - master
    before_script:
-- 
GitLab


From 068dacc22bbc06435e567e01c6e0f253449c64dc Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Mon, 6 Aug 2018 15:55:52 +0200
Subject: [PATCH 14/15] Reactivated staging

---
 .gitlab-ci.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 74e005ab..3e28a168 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -104,7 +104,6 @@ pages:
 .staging_template: &staging_definition
    stage: staging
    image: docker:latest
-   when: manual
    only:
      - master
    before_script:
-- 
GitLab


From 2015309fba1bab6e2dda749723e9846cdeca6cc8 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Mon, 6 Aug 2018 17:19:00 +0200
Subject: [PATCH 15/15] Fixed a bunch possible bugs...

---
 frontend/src/api.ts                           | 18 ++++++---
 frontend/src/store/actions.ts                 |  6 +--
 frontend/src/store/modules/authentication.ts  |  8 ++--
 .../modules/feedback_list/feedback-table.ts   |  2 +-
 .../src/store/modules/submission-notes.ts     |  4 +-
 frontend/src/store/modules/subscriptions.ts   | 39 ++++++++++---------
 frontend/src/store/mutations.ts               | 21 ----------
 frontend/src/util/helpers.ts                  | 11 +++---
 8 files changed, 49 insertions(+), 60 deletions(-)

diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index 029722cd..670854ca 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -1,5 +1,5 @@
 import axios from 'axios'
-import {Credentials} from "@/store/modules/authentication";
+import {Credentials} from '@/store/modules/authentication'
 
 function addFieldsToUrl ({url, fields = []}: {url: string, fields?: string[]}) {
   return fields.length > 0 ? url + '?fields=pk,' + fields : url
@@ -68,7 +68,7 @@ export async function fetchAllStudents (fields: string[] = []) {
 }
 
 export async function fetchStudent ({pk, fields = []}:
-                                      {pk: string, fields?: string[]}) {
+{pk: string, fields?: string[]}) {
   const url = addFieldsToUrl({
     url: `/api/student/${pk}/`,
     fields
@@ -111,7 +111,7 @@ export async function fetchFeedback ({ofSubmission}) {
 }
 
 export async function fetchExamType ({examPk, fields = []}:
-                                       {examPk: string, fields?: string[]}) {
+{examPk?: string, fields?: string[]}) {
   const url = addFieldsToUrl({
     url: `/api/examtype/${examPk !== undefined ? examPk + '/' : ''}`,
     fields})
@@ -126,8 +126,16 @@ export async function fetchStatistics (opt = {fields: []}) {
   return (await ax.get(url)).data
 }
 
-export async function subscribeTo (type, key, stage) {
-  let data = {
+interface SubscriptionPayload {
+  /* eslint-disable */
+  query_type: string,
+  query_key?: string,
+  feedback_stage?: string
+  /* eslint-enable */
+}
+
+export async function subscribeTo (type: string, key: string, stage: string) {
+  let data: SubscriptionPayload = {
     query_type: type
   }
 
diff --git a/frontend/src/store/actions.ts b/frontend/src/store/actions.ts
index b6ce7879..47ca46fc 100644
--- a/frontend/src/store/actions.ts
+++ b/frontend/src/store/actions.ts
@@ -53,9 +53,9 @@ const actions = {
       handleError(err, dispatch, 'Unable to fetch tutor data')
     }
   },
-  async getAllFeedback ({commit, dispatch}, fields = []) {
+  async getAllFeedback ({commit, dispatch}, fields: string[] = []) {
     try {
-      const feedback = await api.fetchAllFeedback({fields})
+      const feedback = await api.fetchAllFeedback(fields)
       commit(mut.SET_ALL_FEEDBACK, feedback)
       commit(mut.MAP_FEEDBACK_OF_SUBMISSION_TYPE)
     } catch (err) {
@@ -110,7 +110,7 @@ const actions = {
     commit('submissionNotes/' + subNotesMut.RESET_STATE)
     commit(authMut.SET_MESSAGE, message)
     router.push({name: 'login'})
-    router.go()
+    router.go(0)
   }
 }
 
diff --git a/frontend/src/store/modules/authentication.ts b/frontend/src/store/modules/authentication.ts
index b0101d5d..9e0c95e7 100644
--- a/frontend/src/store/modules/authentication.ts
+++ b/frontend/src/store/modules/authentication.ts
@@ -1,6 +1,6 @@
 import * as api from '@/api'
 import gradySays from '../grady_speak'
-import {ActionContext, Module} from "vuex";
+import {ActionContext, Module} from 'vuex'
 
 export interface Credentials {
     username: string,
@@ -17,7 +17,7 @@ interface AuthState {
         pk: string,
         username: string,
         role: string,
-        is_admin: boolean
+        is_admin: boolean //eslint-disable-line
     }
 }
 function initialState (): AuthState {
@@ -55,10 +55,10 @@ const authentication: Module<AuthState, any> = {
     isStudent: (state: AuthState) => {
       return state.user.role === 'Student'
     },
-    isTutor: (state: AuthState)=> {
+    isTutor: (state: AuthState) => {
       return state.user.role === 'Tutor'
     },
-    isReviewer: (state: AuthState)=> {
+    isReviewer: (state: AuthState) => {
       return state.user.role === 'Reviewer'
     },
     isTutorOrReviewer: (state: AuthState, getters) => {
diff --git a/frontend/src/store/modules/feedback_list/feedback-table.ts b/frontend/src/store/modules/feedback_list/feedback-table.ts
index 35e526ac..4ea766a8 100644
--- a/frontend/src/store/modules/feedback_list/feedback-table.ts
+++ b/frontend/src/store/modules/feedback_list/feedback-table.ts
@@ -41,7 +41,7 @@ const feedbackTable = {
   actions: {
     mapFeedbackHistOfSubmissionType ({getters, commit, state}) {
       for (const feedback of Object.values(state.feedbackHist)) {
-        const type = getters.getSubmissionType(feedback.of_submission_type)
+        const type = getters.getSubmissionType((feedback as any).of_submission_type)
         commit(feedbackTableMut.SET_FEEDBACK_OF_SUBMISSION_TYPE, {feedback, type})
       }
     },
diff --git a/frontend/src/store/modules/submission-notes.ts b/frontend/src/store/modules/submission-notes.ts
index 08669ce0..cd02557f 100644
--- a/frontend/src/store/modules/submission-notes.ts
+++ b/frontend/src/store/modules/submission-notes.ts
@@ -122,7 +122,7 @@ const submissionNotes = {
     deleteComments: async function ({state}) {
       return Promise.all(
         Object.values(state.commentsMarkedForDeletion).map(comment => {
-          return api.deleteComment(comment)
+          return api.deleteComment((comment as any))
         })
       )
     },
@@ -143,7 +143,7 @@ const submissionNotes = {
       if (!state.hasOrigFeedback) {
         return api.submitFeedbackForAssignment({feedback})
       } else {
-        feedback.pk = state.origFeedback.pk
+        feedback['pk']= state.origFeedback.pk
         return api.submitUpdatedFeedback({feedback})
       }
     }
diff --git a/frontend/src/store/modules/subscriptions.ts b/frontend/src/store/modules/subscriptions.ts
index 531ea809..588867f0 100644
--- a/frontend/src/store/modules/subscriptions.ts
+++ b/frontend/src/store/modules/subscriptions.ts
@@ -50,10 +50,10 @@ const subscriptions = {
       return stages
     },
     availableSubmissionTypeQueryKeys (state, getters, rootState) {
-      return Object.values(rootState.submissionTypes).map(subType => subType.pk)
+      return Object.values(rootState.submissionTypes).map((subType: any) => subType.pk)
     },
     availableExamTypeQueryKeys (state, getters, rootState) {
-      return Object.values(rootState.examTypes).map(examType => examType.pk)
+      return Object.values(rootState.examTypes).map((examType: any) => examType.pk)
     },
     activeSubscription (state) {
       return state.subscriptions[state.activeSubscriptionPk]
@@ -86,25 +86,26 @@ const subscriptions = {
         acc[curr] = subscriptionsByType()
         return acc
       }, {})
-      Object.values(state.subscriptions).forEach(subscription => {
+      Object.values(state.subscriptions).forEach((subscription: any) => {
         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
-            }
-          })
-        }
-      })
+      const sortSubscriptions = subscriptionsByType => Object.values(subscriptionsByType)
+        .forEach((arr: object[]) => {
+          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)
       })
@@ -186,7 +187,7 @@ const subscriptions = {
       }
     },
     async cleanAssignmentsFromSubscriptions ({commit, state, dispatch}, excludeActive = true) {
-      Object.values(state.subscriptions).forEach(subscription => {
+      Object.values(state.subscriptions).forEach((subscription: any) => {
         if (!excludeActive || subscription.pk !== state.activeSubscriptionPk) {
           subscription.assignments.forEach(assignment => {
             api.deleteAssignment({assignment})
diff --git a/frontend/src/store/mutations.ts b/frontend/src/store/mutations.ts
index 2e4c4081..1c9e80ad 100644
--- a/frontend/src/store/mutations.ts
+++ b/frontend/src/store/mutations.ts
@@ -26,27 +26,6 @@ const mutations = {
       return acc
     }, {})
   },
-  [mut.SET_SUBSCRIPTIONS] (state, subscriptions) {
-    state.subscriptions = subscriptions.reduce((acc, curr) => {
-      acc[curr['pk']] = curr
-      return acc
-    }, {})
-    for (let subscription of Object.values(subscriptions)) {
-      for (let assignment of subscription.assignments) {
-        if (assignment.feedback) {
-          Vue.set(assignment.feedback, 'feedback_stage_for_user', subscription.feedback_stage)
-        }
-      }
-    }
-  },
-  [mut.SET_SUBSCRIPTION] (state, subscription) {
-    Vue.set(state.subscriptions, subscription.pk, subscription)
-    for (let assignment of subscription.assignments) {
-      if (assignment.feedback) {
-        Vue.set(assignment.feedback, 'feedback_stage_for_user', subscription.feedback_stage)
-      }
-    }
-  },
   [mut.SET_STUDENTS] (state, students) {
     state.students = students.reduce((acc, curr) => {
       acc[curr.pk] = mapStudent(curr, state.studentMap)
diff --git a/frontend/src/util/helpers.ts b/frontend/src/util/helpers.ts
index 73e8fc49..1927f50a 100644
--- a/frontend/src/util/helpers.ts
+++ b/frontend/src/util/helpers.ts
@@ -38,8 +38,8 @@ interface GetSetPair {
  * @returns {*}
  */
 export function createComputedGetterSetter (
-    {path, mutation, namespace}:
-        {path: string, mutation: string, namespace:string}): GetSetPair {
+  {path, mutation, namespace}:
+  {path: string, mutation: string, namespace:string}): GetSetPair {
   return {
     get (): any {
       return getObjectValueByPath(vueInstance.$store.state, path)
@@ -68,8 +68,8 @@ interface MappedState {
  * @param items array that contains objects {name, path, mutation}
  */
 export function mapStateToComputedGetterSetter (
-    {namespace = '', pathPrefix = '', items = []}:
-        {namespace: string, pathPrefix: string, items: StateMapperItem[]}): MappedState {
+  {namespace = '', pathPrefix = '', items = []}:
+  {namespace: string, pathPrefix: string, items: StateMapperItem[]}): MappedState {
   return items.reduce((acc: MappedState, curr) => {
     // if no path is give, use name
     const itemPath = curr.path || curr.name
@@ -83,6 +83,7 @@ export function mapStateToComputedGetterSetter (
 // 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) {
+  // @ts-ignore can be ignored since cartesian is only recursively called if b si truthy
   return b ? cartesian(cartesianHelper(a, b), ...c) : a
 }
 
@@ -100,7 +101,7 @@ export function objectifyArray<T> (arr: T[], key = 'pk'): {[key: string]: T} {
   }, {})
 }
 
-export function once (fn, context) {
+export function once (fn: Function, context?: object) {
   let result
   return function () {
     if (!result) {
-- 
GitLab