Skip to content
Snippets Groups Projects
Commit bf0799ed authored by robinwilliam.hundt's avatar robinwilliam.hundt
Browse files

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
parent d25eb108
No related branches found
No related tags found
1 merge request!66Feedback shortPolling / statistics / +minor fixes
Pipeline #
Showing
with 274 additions and 35 deletions
......@@ -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')
......
......@@ -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):
......
......@@ -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')
})
......
......@@ -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
......
<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>
......@@ -36,12 +36,16 @@
props: {
tests: {
type: Array,
default: []
default: () => []
},
expand: {
type: Boolean,
default: false
}
},
data () {
return {
expanded: false
expanded: this.expand
}
}
}
......
......@@ -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">
......
......@@ -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>
......
......@@ -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(() => {
......
......@@ -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>
<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>
......@@ -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>
......
......@@ -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 () {
......
......@@ -32,10 +32,12 @@
},
{
text: '# created',
align: 'right',
value: 'feedback_created'
},
{
text: '# validated',
align: 'right',
value: 'feedback_validated'
}
]
......
......@@ -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>
......
......@@ -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>
<template>
<tutor-list></tutor-list>
<tutor-list class="ma-2 elevation-1"></tutor-list>
</template>
<script>
......
<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>
......
......@@ -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',
......
......@@ -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)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment