Skip to content
Snippets Groups Projects
Commit 03839843 authored by robinwilliam.hundt's avatar robinwilliam.hundt Committed by Dominik Seeger
Browse files

Added LabelStatistics endpoint and Statics overview page in frontend

parent 6f255a08
Branches
Tags
1 merge request!165Resolve "Introducing a labelling system for tutors to mark certain submission"
Showing with 258 additions and 42 deletions
......@@ -4,6 +4,8 @@
*.pot
*.py[co]
.tox/
*.ipynb
.ipynb_checkpoints/
__pycache__
MANIFEST
.coverage
......
......@@ -23,6 +23,7 @@ router.register('assignment', views.AssignmentApiViewSet)
router.register('statistics', views.StatisticsEndpoint, basename='statistics')
router.register('user', views.UserAccountViewSet, basename='user')
router.register('label', views.LabelApiViewSet, basename='label')
router.register('label-statistics', views.LabelStatistics, basename='label-statistics')
schema_view = get_schema_view(
openapi.Info(
......
......@@ -2,4 +2,4 @@ from .feedback import FeedbackApiView, FeedbackCommentApiView # noqa
from .subscription import SubscriptionApiViewSet, AssignmentApiViewSet # noqa
from .common_views import * # noqa
from .export import StudentJSONExport, InstanceExport # noqa
from .label import LabelApiViewSet # noqa
from .label import LabelApiViewSet, LabelStatistics # noqa
......@@ -127,9 +127,11 @@ class SubmissionTypeApiView(viewsets.ReadOnlyModelViewSet):
""" Gets a list or a detail view of a single SubmissionType """
queryset = SubmissionType.objects.all()
serializer_class = SubmissionTypeSerializer
permission_classes = (IsTutorOrReviewer, )
class StatisticsEndpoint(viewsets.ViewSet):
permission_classes = (IsTutorOrReviewer, )
def list(self, request, *args, **kwargs):
first_sub_type = models.SubmissionType.objects.first()
......
import logging
from django.db.models import Case, When, IntegerField, Sum, Q
from rest_framework import mixins, viewsets
from rest_framework.response import Response
from core import models, permissions, serializers
from core.models import SubmissionType, FeedbackLabel
log = logging.getLogger(__name__)
......@@ -14,3 +18,32 @@ class LabelApiViewSet(viewsets.GenericViewSet,
permission_classes = (permissions.IsTutorOrReviewer, )
queryset = models.FeedbackLabel.objects.all()
serializer_class = serializers.LabelSerializer
class LabelStatistics(viewsets.ViewSet):
permission_classes = (permissions.IsTutorOrReviewer, )
def list(self, *args, **kwargs):
# TODO This is horribly ugly and should be killed with fire
# however, i'm unsure whether there is a better way to retrieve the
# information that hits the database less often
labels = FeedbackLabel.objects.all()
counts = list(SubmissionType.objects.annotate(
**{str(label.pk): Sum(
Case(
# if the feedback has a label or there is a visible comment with that
# label add 1 to the count
When(
Q(submissions__feedback__labels=label) |
Q(submissions__feedback__feedback_lines__labels=label) &
Q(submissions__feedback__feedback_lines__visible_to_student=True),
then=1),
output_field=IntegerField(),
default=0
)
) for label in labels}
).values('pk', *[str(label.pk) for label in labels]))
return Response(list(counts))
......@@ -11,7 +11,8 @@ import {
Submission,
SubmissionNoType, SubmissionType,
Subscription,
Tutor, UserAccount, FeedbackLabel
Tutor, UserAccount, LabelStatisticsForSubType,
FeedbackLabel
} from '@/models'
function getInstanceBaseUrl (): string {
......@@ -113,6 +114,11 @@ export async function fetchStatistics (): Promise<Statistics> {
return (await ax.get(url)).data
}
export async function fetchLabelStatistics (): Promise<LabelStatisticsForSubType []> {
const url = '/api/label-statistics'
return (await ax.get(url)).data
}
interface SubscriptionCreatePayload {
queryType: Subscription.QueryTypeEnum
queryKey?: string
......
<template>
<v-card>
<v-card-title class="title">Accumulated Label Statistics</v-card-title>
<v-data-table
:headers=headers
:pagination="pagination"
:loading="loading"
:items="summedLabelCounts"
>
<template slot="items" slot-scope="props">
<td>{{ props.item[0] }}</td>
<td class="text-xs-center">{{ props.item[1] }}</td>
</template>
</v-data-table>
<div v-for="([subType, labelCounts]) in mappedLabelCounts" :key="subType">
<v-card-title class="title">
Statistics for: {{ subType }}
</v-card-title>
<v-data-table
:headers=headers
:pagination="pagination"
:loading="loading"
:items="labelCounts"
>
<template slot="items" slot-scope="props">
<td>{{ props.item[0] }}</td>
<td class="text-xs-center">{{ props.item[1] }}</td>
</template>
</v-data-table>
</div>
</v-card>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import * as api from '@/api'
import { LabelStatisticsForSubType } from '../models';
import { getters } from '../store/getters';
import { FeedbackLabels } from '../store/modules/feedback-labels';
@Component
export default class LabelStatistics extends Vue{
labelStatistics: LabelStatisticsForSubType[] = []
timer = 0
pagination = {
descending: true,
sortBy: '[1]',
rowsPerPage: -1
}
headers = [
{
text: 'Label',
align: 'left',
sortable: true,
value: '[0]'
},
{
text: 'Count',
align: 'center',
sortable: true,
value: '[1]'
}
]
get loading(): boolean {
return this.labelStatistics.length == 0
}
get summedLabelCounts () {
const summedLabelCounts = this.labelStatistics
.reduce((acc: {[labelPk: string]: number}, curr) => {
Object.entries(curr)
.filter(([key, val]) => key !== 'pk')
.forEach(([labelPk, count]: [string, number]) => {
if (!acc[labelPk]) {
acc[labelPk] = 0
}
acc[labelPk] += count
})
return acc
}, {})
// TODO map label pks to names
const mappedLabelCounts = this.mapLabelList(Object
.entries(summedLabelCounts))
return mappedLabelCounts
}
get mappedLabelCounts () {
return this.labelStatistics.map(labelStatistics => {
const labelValues = Object
.entries(labelStatistics)
.filter(([key, val]) => key !== 'pk')
const subTypeName = getters.submissionType(labelStatistics.pk).name
return [subTypeName, this.mapLabelList(labelValues)]
})
}
mapLabelList (labelList: [string, number][]) {
return labelList.map(entry => {
const label = FeedbackLabels.state.labels.find(label => {
return String(label.pk) === entry[0]
})
const labelName = label ? label.name : 'Unknown label'
return [labelName, entry[1]]
})
}
async loadLabelStatistics () {
this.labelStatistics = await api.fetchLabelStatistics()
}
created () {
this.timer = setInterval(() => {
this.loadLabelStatistics()
}, 10 * 1e3)
this.loadLabelStatistics()
}
beforeDestroy () {
clearInterval(this.timer)
}
}
</script>
......@@ -123,7 +123,7 @@ export default {
search: '',
pagination: {
sortBy: 'name',
rowsPerPage: Infinity
rowsPerPage: -1
},
staticHeaders: [
{
......
......@@ -70,12 +70,6 @@ export default class SubscriptionList extends Vue {
return subscriptions
}
mounted() {
this.timer = setInterval(() => {
this.getSubscriptions(true)
}, 30 * 1e3)
}
beforeDestroy() {
clearInterval(this.timer)
}
......@@ -83,6 +77,11 @@ export default class SubscriptionList extends Vue {
created() {
const submissionTypes = actions.updateSubmissionTypes()
const subscriptions = Subscriptions.getSubscriptions()
this.timer = setInterval(() => {
this.getSubscriptions(true)
}, 30 * 1e3)
Promise.all([submissionTypes, subscriptions]).then(() => {
Subscriptions.subscribeToAll()
Subscriptions.cleanAssignmentsFromSubscriptions(true)
......
......@@ -230,6 +230,13 @@ export interface Statistics {
submissionTypeProgress: Array<SubmissionTypeProgress>
}
export interface LabelStatisticsForSubType {
/** Contains the count of the different labels under their pk */
[label_pk: number]: number,
/** The pk of the corresponding SubmissionType */
pk: string
}
/**
*
* @export
......
<template>
<div>
<v-layout row class="ma-2">
<v-flex xs5>
<label-statistics/>
</v-flex>
<v-flex offset-xs1 xs5>
<correction-statistics/>
</v-flex>
</v-layout>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import CorrectionStatistics from '@/components/CorrectionStatistics.vue'
import LabelStatistics from '@/components/LabelStatistics.vue'
@Component({
components: {CorrectionStatistics, LabelStatistics}
})
export default class Statistics extends Vue {
}
</script>
......@@ -51,6 +51,11 @@ export default {
name: 'Feedback History',
icon: 'feedback',
route: '/feedback'
},
{
name: 'Statistics',
icon: 'bar_chart',
route: 'statistics'
}
]
}
......
......@@ -8,6 +8,7 @@ import SubscriptionWorkPage from '@/pages/SubscriptionWorkPage.vue'
import SubscriptionEnded from '@/components/subscriptions/SubscriptionEnded.vue'
import PageNotFound from '@/pages/PageNotFound.vue'
import StartPageSelector from '@/pages/StartPageSelector.vue'
import Statistics from '@/pages/Statistics.vue'
import LayoutSelector from '@/pages/LayoutSelector.vue'
import StudentSubmissionSideView from '@/pages/StudentSubmissionSideView.vue'
import StudentListHelpCard from '@/components/student_list/StudentListHelpCard.vue'
......@@ -91,6 +92,11 @@ const router = new Router({
name: 'subscription-ended',
component: SubscriptionEnded
},
{
path: 'statistics',
name: 'statistics',
component: Statistics
},
{
path: 'feedback',
beforeEnter: tutorOrReviewerOnly,
......
......@@ -7772,11 +7772,6 @@ vue-style-loader@^4.1.0:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
vue-swatches@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/vue-swatches/-/vue-swatches-1.0.3.tgz#84eb23ba99bbbb0d56698f3a8bdb1b340203d33b"
integrity sha512-3J+Nc3bisvhhp0BW0pfTbQvdl3i+dhwoPjoM+2D6R6hW65KNpXOA+sJwcSg2j1Xd6fLcOV7LMb2sz6s4vKSWRg==
vue-template-compiler@^2.5.16:
version "2.5.22"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.22.tgz#c3d3c02c65f1908205c4fbd3b0ef579e51239955"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment