From bf0799eddbd7575b24ff9f0f20901736af309627 Mon Sep 17 00:00:00 2001
From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de>
Date: Sun, 18 Feb 2018 11:05:01 +0100
Subject: [PATCH] Feedback shortPolling / statistics / +minor fixes

Student names in Subscription list for reviewer

Short polling on orig Feedback

Refresh button student list

FeedbackComment visiblity indication

SubscriptionEnded page

Right side of student overview is sticky

Statistics overview
---
 core/serializers/feedback.py                  |  5 +-
 core/serializers/subscription.py              |  6 ++-
 core/views/common_views.py                    |  2 +-
 frontend/src/api.js                           | 21 ++++++++
 .../src/components/CorrectionStatistics.vue   | 52 +++++++++++++++++++
 frontend/src/components/SubmissionTests.vue   |  8 ++-
 frontend/src/components/SubmissionType.vue    |  2 +-
 .../components/student_list/StudentList.vue   | 35 ++++++++-----
 .../submission_notes/SubmissionCorrection.vue | 16 +++++-
 .../submission_notes/base/FeedbackComment.vue | 30 +++++++++--
 .../subscriptions/SubscriptionEnded.vue       | 32 ++++++++++++
 .../subscriptions/SubscriptionForList.vue     |  2 +-
 .../subscriptions/SubscriptionList.vue        | 14 ++++-
 .../src/components/tutor_list/TutorList.vue   |  2 +
 frontend/src/pages/SubscriptionWorkPage.vue   | 20 +++++++
 .../pages/reviewer/StudentOverviewPage.vue    |  9 +++-
 .../src/pages/reviewer/TutorOverviewPage.vue  |  2 +-
 frontend/src/pages/tutor/TutorStartPage.vue   |  8 ++-
 frontend/src/router/index.js                  |  5 ++
 frontend/src/store/actions.js                 | 38 ++++++++++++--
 frontend/src/store/mutations.js               | 23 +++++++-
 frontend/src/store/store.js                   |  3 +-
 22 files changed, 298 insertions(+), 37 deletions(-)
 create mode 100644 frontend/src/components/CorrectionStatistics.vue
 create mode 100644 frontend/src/components/subscriptions/SubscriptionEnded.vue

diff --git a/core/serializers/feedback.py b/core/serializers/feedback.py
index 4fbbab3c..3f7c1335 100644
--- a/core/serializers/feedback.py
+++ b/core/serializers/feedback.py
@@ -141,7 +141,10 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
 
         has_full_score = score == submission.type.full_score
         has_feedback_lines = ('feedback_lines' in data and
-                              len(data['feedback_lines']) > 0)
+                              len(data['feedback_lines']) > 0 or
+                              self.instance is not None and
+                              self.instance.feedback_lines.count() > 0)
+
         if not has_full_score and not has_feedback_lines:
             raise serializers.ValidationError(
                 'Sorry, you have to explain why this does not get full score')
diff --git a/core/serializers/subscription.py b/core/serializers/subscription.py
index 90ae0e2e..adf3a54a 100644
--- a/core/serializers/subscription.py
+++ b/core/serializers/subscription.py
@@ -2,17 +2,19 @@ from rest_framework import serializers
 
 from core.models import (Submission, SubmissionSubscription,
                          TutorSubmissionAssignment)
-from core.serializers import DynamicFieldsModelSerializer, FeedbackSerializer
+from core.serializers import (DynamicFieldsModelSerializer, FeedbackSerializer,
+                              TestSerializer)
 
 
 class SubmissionAssignmentSerializer(DynamicFieldsModelSerializer):
     text = serializers.ReadOnlyField()
     type_pk = serializers.ReadOnlyField(source='type.pk')
     full_score = serializers.ReadOnlyField(source='type.full_score')
+    tests = TestSerializer(many=True, read_only=True)
 
     class Meta:
         model = Submission
-        fields = ('pk', 'type_pk', 'text', 'full_score')
+        fields = ('pk', 'type_pk', 'text', 'full_score', 'tests')
 
 
 class AssignmentSerializer(DynamicFieldsModelSerializer):
diff --git a/core/views/common_views.py b/core/views/common_views.py
index da6ab82c..cb109ffc 100644
--- a/core/views/common_views.py
+++ b/core/views/common_views.py
@@ -100,7 +100,7 @@ class StatisticsEndpoint(viewsets.ViewSet):
 
             'submission_type_progress':
                 models.SubmissionType.get_annotated_feedback_count().values(
-                    'feedback_count', 'pk', 'percentage')
+                    'feedback_count', 'pk', 'percentage', 'name')
         })
 
 
diff --git a/frontend/src/api.js b/frontend/src/api.js
index 7ddcfb34..cf303a22 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -57,6 +57,14 @@ export async function fetchAllStudents (fields = []) {
   return (await ax.get(url)).data
 }
 
+export async function fetchStudent ({pk, fields = []}) {
+  const url = addFieldsToUrl({
+    url: `/api/student/${pk}/`,
+    fields
+  })
+  return (await ax.get(url)).data
+}
+
 export async function fetchAllTutors (fields = []) {
   const url = addFieldsToUrl({
     url: '/api/tutor/',
@@ -86,6 +94,11 @@ export async function fetchAllFeedback (fields = []) {
   return (await ax.get(url)).data
 }
 
+export async function fetchFeedback ({ofSubmission}) {
+  const url = `/api/feedback/${ofSubmission}/`
+  return (await ax.get(url)).data
+}
+
 export async function fetchExamType ({examPk, fields = []}) {
   const url = addFieldsToUrl({
     url: `/api/examtype/${examPk !== undefined ? examPk + '/' : ''}`,
@@ -93,6 +106,14 @@ export async function fetchExamType ({examPk, fields = []}) {
   return (await ax.get(url)).data
 }
 
+export async function fetchStatistics (opt = {fields: []}) {
+  const url = addFieldsToUrl({
+    url: '/api/statistics/',
+    fields: opt.fields
+  })
+  return (await ax.get(url)).data
+}
+
 export async function subscribeTo (type, key, stage) {
   let data = {
     query_type: type
diff --git a/frontend/src/components/CorrectionStatistics.vue b/frontend/src/components/CorrectionStatistics.vue
new file mode 100644
index 00000000..20f94423
--- /dev/null
+++ b/frontend/src/components/CorrectionStatistics.vue
@@ -0,0 +1,52 @@
+<template>
+    <v-card class="py-2">
+      <v-card-title>
+        <span class="title">Statistics</span>
+      </v-card-title>
+      <ul class="inline-list mx-3 mb-4">
+        <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}}</span></li>
+      </ul>
+      <div v-for="(progress, index) in statistics.submission_type_progress" :key="index">
+        <v-card-title class="py-0">
+          {{progress.name}}
+        </v-card-title>
+        <div class="mx-3">
+          <v-progress-linear
+            v-model="progress.percentage"
+            :color="progress.precentage === 100 ? 'green' : 'blue'"
+          ></v-progress-linear>
+        </div>
+      </div>
+    </v-card>
+</template>
+
+<script>
+  export default {
+    name: 'correction-statistics',
+    data () {
+      return {
+        loaded: false
+      }
+    },
+    computed: {
+      statistics () {
+        return this.$store.state.statistics
+      }
+    },
+    created () {
+      this.$store.dispatch('getStatistics').then(() => { this.loaded = true })
+    }
+  }
+</script>
+
+<style scoped>
+  .inline-list li {
+    display: inline;
+    margin: 0px 5px;
+  }
+  .inline-list span {
+    font-weight: bolder;
+  }
+</style>
diff --git a/frontend/src/components/SubmissionTests.vue b/frontend/src/components/SubmissionTests.vue
index 8474e7e7..ea1c8731 100644
--- a/frontend/src/components/SubmissionTests.vue
+++ b/frontend/src/components/SubmissionTests.vue
@@ -36,12 +36,16 @@
     props: {
       tests: {
         type: Array,
-        default: []
+        default: () => []
+      },
+      expand: {
+        type: Boolean,
+        default: false
       }
     },
     data () {
       return {
-        expanded: false
+        expanded: this.expand
       }
     }
   }
diff --git a/frontend/src/components/SubmissionType.vue b/frontend/src/components/SubmissionType.vue
index 2e3b41ee..aa1fbd49 100644
--- a/frontend/src/components/SubmissionType.vue
+++ b/frontend/src/components/SubmissionType.vue
@@ -7,7 +7,7 @@
           v-for="(item, i) in typeItems"
           :key="i"
           :value="expandedByDefault[item.title]">
-          <div slot="header">{{ item.title }}</div>
+          <div slot="header"><b>{{ item.title }}</b></div>
           <v-card
             v-if="item.title === 'Description'"
             color="grey lighten-4">
diff --git a/frontend/src/components/student_list/StudentList.vue b/frontend/src/components/student_list/StudentList.vue
index a4d7de1a..ea229948 100644
--- a/frontend/src/components/student_list/StudentList.vue
+++ b/frontend/src/components/student_list/StudentList.vue
@@ -12,12 +12,16 @@
         hide-details
         v-model="search"
       ></v-text-field>
+      <v-card-actions>
+        <v-btn icon @click="refresh"><v-icon>refresh</v-icon></v-btn>
+      </v-card-actions>
     </v-card-title>
     <v-data-table
       :headers="dynamicHeaders"
       :items="studentListItems"
       :search="search"
       :pagination.sync="pagination"
+      :loading="loading"
       item-key="name"
       hide-actions
     >
@@ -95,6 +99,7 @@
     name: 'student-list',
     data () {
       return {
+        loading: true,
         search: '',
         pagination: {
           sortBy: 'name',
@@ -135,17 +140,19 @@
         return headers
       },
       studentListItems () {
-        return this.students.map(student => {
-          return {
-            pk: student.pk,
-            user: student.user,
-            exam: student.exam,
-            name: student.name,
-            matrikel_no: student.matrikel_no,
-            ...this.reduceArrToDict(student.submissions, 'type'),
-            total: this.sumSubmissionScores(student.submissions)
-          }
-        })
+        if (!this.loading) {
+          return Object.values(this.students).map(student => {
+            return {
+              pk: student.pk,
+              user: student.user,
+              exam: student.exam,
+              name: student.name,
+              matrikel_no: student.matrikel_no,
+              ...this.reduceArrToDict(student.submissions, 'type'),
+              total: this.sumSubmissionScores(student.submissions)
+            }
+          })
+        }
       }
     },
     methods: {
@@ -180,10 +187,14 @@
           this.pagination.sortBy = column
           this.pagination.descending = false
         }
+      },
+      refresh () {
+        this.loading = true
+        this.getStudents().then(() => { this.loading = false })
       }
     },
     created () {
-      this.getStudents()
+      this.getStudents().then(() => { this.loading = false })
     }
   }
 </script>
diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue
index 91ebed2d..cffeef17 100644
--- a/frontend/src/components/submission_notes/SubmissionCorrection.vue
+++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue
@@ -75,7 +75,8 @@
     name: 'submission-correction',
     data () {
       return {
-        loading: false
+        loading: false,
+        feedbackShortPollInterval: undefined
       }
     },
     props: {
@@ -139,6 +140,15 @@
           this.loading = false
         })
       },
+      shortPollOrigFeedback () {
+        this.feedbackShortPollInterval = setInterval(() => {
+          if (this.feedbackObj && this.feedbackObj.of_submission) {
+            this.$store.dispatch('getFeedback', {ofSubmission: this.feedbackObj.of_submission}).then(feedback => {
+              this.$store.commit(subNotesNamespace(subNotesMut.SET_ORIG_FEEDBACK), feedback)
+            })
+          }
+        }, 5e3)
+      },
       init () {
         this.$store.commit(subNotesNamespace(subNotesMut.RESET_STATE))
         this.$store.commit(subNotesNamespace(subNotesMut.SET_SUBMISSION), this.submissionObj)
@@ -161,6 +171,10 @@
     },
     created () {
       this.init()
+      this.shortPollOrigFeedback()
+    },
+    beforeDestroy () {
+      clearInterval(this.feedbackShortPollInterval)
     },
     mounted () {
       this.$nextTick(() => {
diff --git a/frontend/src/components/submission_notes/base/FeedbackComment.vue b/frontend/src/components/submission_notes/base/FeedbackComment.vue
index b4d52a18..4672a2c6 100644
--- a/frontend/src/components/submission_notes/base/FeedbackComment.vue
+++ b/frontend/src/components/submission_notes/base/FeedbackComment.vue
@@ -4,13 +4,28 @@
       <span class="tip tip-up" :style="{borderBottomColor: borderColor}"></span>
       <span v-if="of_tutor" class="of-tutor">Of tutor: {{of_tutor}}</span>
       <span class="comment-created">{{parsedCreated}}</span>
+      <div class="visibility-icon">
+        <v-tooltip top v-if="visible_to_student" size="20px">
+          <v-icon
+            slot="activator"
+            size="20px"
+          >visibility</v-icon>
+          <span>Will be visible to student</span>
+        </v-tooltip>
+        <v-tooltip top v-else>
+          <v-icon
+            slot="activator"
+            size="20px">visibility_off</v-icon>
+          <span>Won't be visible to student</span>
+        </v-tooltip>
+      </div>
       <div class="message">{{text}}</div>
       <v-btn
         flat icon
         class="delete-button"
         v-if="deletable"
         @click.stop="$emit('delete')"
-      ><v-icon color="grey darken-1">delete_forever</v-icon></v-btn>
+      ><v-icon color="grey darken-1" size="20px">delete_forever</v-icon></v-btn>
     </div>
   </div>
 </template>
@@ -36,6 +51,10 @@
         type: Boolean,
         default: false
       },
+      visible_to_student: {
+        type: Boolean,
+        default: true
+      },
       borderColor: {
         type: String,
         default: '#3D8FC1'
@@ -88,8 +107,8 @@
   }
   .delete-button {
     position: absolute;
-    bottom: -10px;
-    right: 0px;
+    bottom: -20px;
+    left: -50px;
   }
   .comment-created {
     position: absolute;
@@ -103,4 +122,9 @@
     top: -20px;
     left: 50px;
   }
+  .visibility-icon {
+    position: absolute;
+    top: -4px;
+    left: -34px;
+  }
 </style>
diff --git a/frontend/src/components/subscriptions/SubscriptionEnded.vue b/frontend/src/components/subscriptions/SubscriptionEnded.vue
new file mode 100644
index 00000000..0a773e2f
--- /dev/null
+++ b/frontend/src/components/subscriptions/SubscriptionEnded.vue
@@ -0,0 +1,32 @@
+<template>
+  <v-card class="mx-auto center-page">
+    <v-card-title class="title">
+      It seems like your subscription has (temporarily) ended.
+    </v-card-title>
+    <v-card-text>
+      If you've been validating feedback or resolving conflicts those subscriptions might become active again.<br/>
+      If that happens they'll become clickable in the sidebar.
+    </v-card-text>
+    <v-card-actions class="text-xs-center">
+      <v-btn to="/home">
+        Overview
+      </v-btn>
+      <v-btn to="/feedback">
+        Feedback History
+      </v-btn>
+    </v-card-actions>
+  </v-card>
+</template>
+
+<script>
+  export default {
+    name: 'subscription-ended'
+  }
+</script>
+
+<style scoped>
+  .center-page {
+    width: fit-content;
+    top: 30vh;
+  }
+</style>
diff --git a/frontend/src/components/subscriptions/SubscriptionForList.vue b/frontend/src/components/subscriptions/SubscriptionForList.vue
index 2057cd92..4cbe731f 100644
--- a/frontend/src/components/subscriptions/SubscriptionForList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionForList.vue
@@ -6,7 +6,7 @@
       style="width: 100%"
     >
       <v-list-tile-content
-        :class="{inactiveSubscription: !active}"
+        :class="{'inactive-subscription': !active}"
         class="ml-3">
         {{name}}
       </v-list-tile-content>
diff --git a/frontend/src/components/subscriptions/SubscriptionList.vue b/frontend/src/components/subscriptions/SubscriptionList.vue
index aa409e97..1aba1a7b 100644
--- a/frontend/src/components/subscriptions/SubscriptionList.vue
+++ b/frontend/src/components/subscriptions/SubscriptionList.vue
@@ -94,7 +94,7 @@
             description: 'Submissions of single students.',
             expanded: true,
             createPermission: () => {
-              return this.$store.getters.isReviewer
+              return false
             },
             viewPermission: () => {
               return this.$store.getters.isReviewer
@@ -131,9 +131,19 @@
       ]),
       getSubscriptions () {
         this.updating = true
-        this.$store.dispatch('getSubscriptions').finally(() => {
+        this.$store.dispatch('getSubscriptions').then(() => {
+          this.getStudentNames()
+        }).finally(() => {
           this.updating = false
         })
+      },
+      getStudentNames () {
+        if (this.subscriptions.student.length > 0 && this.$store.getters.isReviewer) {
+          const studentPks = this.subscriptions.student.map(subscription => {
+            return subscription.query_key
+          }).filter(key => key)
+          this.$store.dispatch('getStudents', {studentPks, fields: ['name']})
+        }
       }
     },
     created () {
diff --git a/frontend/src/components/tutor_list/TutorList.vue b/frontend/src/components/tutor_list/TutorList.vue
index bbc69b7f..a5f0e9b3 100644
--- a/frontend/src/components/tutor_list/TutorList.vue
+++ b/frontend/src/components/tutor_list/TutorList.vue
@@ -32,10 +32,12 @@
           },
           {
             text: '# created',
+            align: 'right',
             value: 'feedback_created'
           },
           {
             text: '# validated',
+            align: 'right',
             value: 'feedback_validated'
           }
         ]
diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue
index 8e0c6b2b..0a25e0d0 100644
--- a/frontend/src/pages/SubscriptionWorkPage.vue
+++ b/frontend/src/pages/SubscriptionWorkPage.vue
@@ -10,6 +10,11 @@
         @skip="skipAssignment"
         class="ma-4 autofocus"
       />
+      <submission-tests
+        :tests="submission.tests"
+        :expand="true"
+        class="mx-4"
+      />
     </v-flex>
 
     <v-flex md6>
@@ -29,6 +34,7 @@
   import SubmissionType from '@/components/SubmissionType'
   import store from '@/store/store'
   import {mut} from '@/store/mutations'
+  import SubmissionTests from '@/components/SubmissionTests'
 
   function onRouteEnterOrUpdate (to, from, next) {
     if (to.name === 'subscription') {
@@ -46,10 +52,16 @@
 
   export default {
     components: {
+      SubmissionTests,
       SubmissionType,
       SubmissionCorrection
     },
     name: 'subscription-work-page',
+    data () {
+      return {
+        subscriptionActive: true
+      }
+    },
     computed: {
       subscription () {
         return this.$store.state.subscriptions[this.$route.params['pk']]
@@ -97,6 +109,14 @@
           })
         })
       }
+    },
+    watch: {
+      currentAssignment (val) {
+        if (val === undefined) {
+          this.$router.replace('ended')
+          this.$store.dispatch('getSubscriptions')
+        }
+      }
     }
   }
 </script>
diff --git a/frontend/src/pages/reviewer/StudentOverviewPage.vue b/frontend/src/pages/reviewer/StudentOverviewPage.vue
index 8b0936fb..9a61a211 100644
--- a/frontend/src/pages/reviewer/StudentOverviewPage.vue
+++ b/frontend/src/pages/reviewer/StudentOverviewPage.vue
@@ -3,7 +3,7 @@
       <v-flex xs6>
         <student-list class="ma-1"></student-list>
       </v-flex>
-      <v-flex xs6 style="height: 100%;">
+      <v-flex xs6 class="right-view">
         <router-view></router-view>
       </v-flex>
     </v-layout>
@@ -19,5 +19,10 @@
 </script>
 
 <style scoped>
-
+  .right-view {
+    position: sticky;
+    top: 80px;
+    overflow-y: scroll;
+    height: 90vh;
+  }
 </style>
diff --git a/frontend/src/pages/reviewer/TutorOverviewPage.vue b/frontend/src/pages/reviewer/TutorOverviewPage.vue
index 88109335..4d6e3b45 100644
--- a/frontend/src/pages/reviewer/TutorOverviewPage.vue
+++ b/frontend/src/pages/reviewer/TutorOverviewPage.vue
@@ -1,5 +1,5 @@
 <template>
-  <tutor-list></tutor-list>
+  <tutor-list class="ma-2 elevation-1"></tutor-list>
 </template>
 
 <script>
diff --git a/frontend/src/pages/tutor/TutorStartPage.vue b/frontend/src/pages/tutor/TutorStartPage.vue
index 1971e267..5230e7ee 100644
--- a/frontend/src/pages/tutor/TutorStartPage.vue
+++ b/frontend/src/pages/tutor/TutorStartPage.vue
@@ -1,13 +1,17 @@
 <template>
-    <v-flex lg3>
+    <v-flex xs5>
+      <correction-statistics class="ma-4"></correction-statistics>
     </v-flex>
 </template>
 
 <script>
   import SubscriptionList from '@/components/subscriptions/SubscriptionList'
+  import CorrectionStatistics from '@/components/CorrectionStatistics'
 
   export default {
-    components: {SubscriptionList},
+    components: {
+      CorrectionStatistics,
+      SubscriptionList},
     name: 'tutor-start-page'
   }
 </script>
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index eb40cae5..cba803b7 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -5,6 +5,7 @@ import StudentSubmissionPage from '@/pages/student/StudentSubmissionPage'
 import StudentOverviewPage from '@/pages/reviewer/StudentOverviewPage'
 import TutorOverviewPage from '@/pages/reviewer/TutorOverviewPage'
 import SubscriptionWorkPage from '@/pages/SubscriptionWorkPage'
+import SubscriptionEnded from '@/components/subscriptions/SubscriptionEnded'
 import PageNotFound from '@/pages/PageNotFound'
 import StartPageSelector from '@/pages/StartPageSelector'
 import LayoutSelector from '@/pages/LayoutSelector'
@@ -77,6 +78,10 @@ const router = new Router({
           name: 'home',
           component: StartPageSelector
         },
+        {
+          path: 'subscription/ended',
+          component: SubscriptionEnded
+        },
         {
           path: 'subscription/:pk',
           name: 'subscription',
diff --git a/frontend/src/store/actions.js b/frontend/src/store/actions.js
index 56b4207e..9b65d33f 100644
--- a/frontend/src/store/actions.js
+++ b/frontend/src/store/actions.js
@@ -23,6 +23,7 @@ const actions = {
     try {
       const subscriptions = await api.fetchSubscriptions()
       commit(mut.SET_SUBSCRIPTIONS, subscriptions)
+      return subscriptions
     } catch (err) {
       handleError(err, dispatch, 'Unable to fetch subscriptions')
     }
@@ -83,12 +84,24 @@ const actions = {
       handleError(err, dispatch, 'Unable to delete assignment')
     }
   },
-  async getStudents ({commit, dispatch}) {
+  async getStudents ({commit, dispatch}, opt = {studentPks: [], fields: []}) {
     try {
-      const students = await api.fetchAllStudents()
-      commit(mut.SET_STUDENTS, students)
-      return students
+      if (opt.studentPks.length === 0) {
+        const students = await api.fetchAllStudents()
+        commit(mut.SET_STUDENTS, students)
+        return students
+      } else {
+        const students = await Promise.all(
+          opt.studentPks.map(pk => api.fetchStudent({
+            pk,
+            fields: opt.fields
+          }))
+        )
+        students.forEach(student => commit(mut.SET_STUDENT, student))
+        return students
+      }
     } catch (err) {
+      console.log(err)
       handleError(err, dispatch, 'Unable to fetch student data')
     }
   },
@@ -109,6 +122,15 @@ const actions = {
       handleError(err, dispatch, 'Unable to fetch feedback history')
     }
   },
+  async getFeedback ({commit, dispatch}, {ofSubmission}) {
+    try {
+      const feedback = await api.fetchFeedback({ofSubmission})
+      commit(mut.SET_FEEDBACK, feedback)
+      return feedback
+    } catch (err) {
+      handleError(err, dispatch, `Unable to fetch feedback ${ofSubmission}`)
+    }
+  },
   async getSubmissionFeedbackTest ({commit, dispatch}, {pk}) {
     try {
       const submission = await api.fetchSubmissionFeedbackTests({pk})
@@ -117,6 +139,14 @@ const actions = {
       handleError(err, dispatch, 'Unable to fetch submission')
     }
   },
+  async getStatistics ({commit, dispatch}, opt) {
+    try {
+      const statistics = await api.fetchStatistics(opt)
+      commit(mut.SET_STATISTICS, statistics)
+    } catch (err) {
+      handleError(err, dispatch, 'Unable to fetch statistics')
+    }
+  },
   logout ({ commit }, message = '') {
     commit(mut.RESET_STATE)
     commit('submissionNotes/' + subNotesMut.RESET_STATE)
diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.js
index 176db5d7..0fadd7f7 100644
--- a/frontend/src/store/mutations.js
+++ b/frontend/src/store/mutations.js
@@ -12,9 +12,12 @@ export const mut = Object.freeze({
   SET_LAST_INTERACTION: 'SET_LAST_INTERACTION',
   SET_EXAM_TYPES: 'SET_EXAM_TYPES',
   SET_STUDENTS: 'SET_STUDENTS',
+  SET_STUDENT: 'SET_STUDENT',
   SET_TUTORS: 'SET_TUTORS',
   SET_SUBMISSION: 'SET_SUBMISSION',
   SET_ALL_FEEDBACK: 'SET_ALL_FEEDBACK',
+  SET_STATISTICS: 'SET_STATISTICS',
+  SET_FEEDBACK: 'SET_FEEDBACK',
   MAP_FEEDBACK_OF_SUBMISSION_TYPE: 'MAP_FEEDBACK_OF_SUBMISSION_TYPE',
   UPDATE_SUBMISSION_TYPE: 'UPDATE_SUBMISSION_TYPE',
   RESET_STATE: 'RESET_STATE'
@@ -40,7 +43,16 @@ const mutations = {
     state.subscriptions[pk].deactivated = true
   },
   [mut.SET_STUDENTS] (state, students) {
-    state.students = students
+    state.students = students.reduce((acc, curr) => {
+      acc[curr.pk] = curr
+      return acc
+    }, {})
+  },
+  [mut.SET_STUDENT] (state, student) {
+    Vue.set(state.students, student.pk, {
+      ...state.students[student.pk],
+      ...student
+    })
   },
   [mut.SET_TUTORS] (state, tutors) {
     state.tutors = tutors
@@ -54,6 +66,15 @@ const mutations = {
       return acc
     }, {})
   },
+  [mut.SET_FEEDBACK] (state, feedback) {
+    Vue.set(state.feedback, feedback.pk, feedback)
+  },
+  [mut.SET_STATISTICS] (state, statistics) {
+    state.statistics = {
+      ...state.statistics,
+      ...statistics
+    }
+  },
   [mut.MAP_FEEDBACK_OF_SUBMISSION_TYPE] (state) {
     Object.values(state.feedback).forEach(feedback => {
       const submissionType = state.submissionTypes[feedback['of_submission_type']]
diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js
index 97ff061c..5fe804e1 100644
--- a/frontend/src/store/store.js
+++ b/frontend/src/store/store.js
@@ -23,7 +23,8 @@ export function initialState () {
     feedback: {},
     subscriptions: {},
     assignments: {},
-    students: [],
+    students: {},
+    statistics: {},
     tutors: []
   }
 }
-- 
GitLab