From 1ecc38d2ba846dcde63cfc8c105910adb63113c1 Mon Sep 17 00:00:00 2001 From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de> Date: Mon, 19 Mar 2018 14:39:35 +0100 Subject: [PATCH 1/6] Started subscription overhaul --- frontend/src/api.js | 4 +- .../feedback_list/FeedbackTable.vue | 2 +- .../submission_notes/SubmissionCorrection.vue | 1 - .../AnnotatedSubmissionBottomToolbar.vue | 14 +- .../subscriptions/SubscriptionForList.vue | 39 +-- .../subscriptions/SubscriptionList.vue | 142 ++------- .../subscriptions/SubscriptionType.vue | 91 ------ .../subscriptions/SubscriptionsForStage.vue | 42 +++ frontend/src/pages/PageNotFound.vue | 18 +- frontend/src/pages/SubscriptionWorkPage.vue | 79 ++--- frontend/src/store/actions.js | 75 +---- frontend/src/store/getters.js | 42 --- frontend/src/store/modules/subscriptions.js | 274 ++++++++++++++++++ frontend/src/store/mutations.js | 20 -- frontend/src/store/store.js | 8 +- frontend/src/util/helpers.js | 38 +++ 16 files changed, 461 insertions(+), 428 deletions(-) delete mode 100644 frontend/src/components/subscriptions/SubscriptionType.vue create mode 100644 frontend/src/components/subscriptions/SubscriptionsForStage.vue create mode 100644 frontend/src/store/modules/subscriptions.js diff --git a/frontend/src/api.js b/frontend/src/api.js index ead1f457..b35bce86 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -129,9 +129,9 @@ export async function subscribeTo (type, key, stage) { return (await ax.post('/api/subscription/', data)).data } -export async function createAssignment ({subscription}) { +export async function createAssignment ({subscription = null, subscriptionPk = ''}) { const data = { - subscription: subscription.pk + subscription: subscription ? subscription.pk : subscriptionPk } return (await ax.post(`/api/assignment/`, data)).data } diff --git a/frontend/src/components/feedback_list/FeedbackTable.vue b/frontend/src/components/feedback_list/FeedbackTable.vue index 2ec4b238..02574894 100644 --- a/frontend/src/components/feedback_list/FeedbackTable.vue +++ b/frontend/src/components/feedback_list/FeedbackTable.vue @@ -11,7 +11,7 @@ v-model="search" /> </v-card-title> - <feedback-search-options/> + <feedback-search-options class="mx-3"/> <v-data-table :headers="headers" :items="feedback" diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue index 246b5b05..376a73f8 100644 --- a/frontend/src/components/submission_notes/SubmissionCorrection.vue +++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue @@ -50,7 +50,6 @@ :skippable="assignment !== undefined" :feedback="feedbackObj ? feedbackObj : {}" @submitFeedback="submitFeedback" - @skip="$emit('skip')" /> </base-annotated-submission> </div> diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue index 6ad3b94f..e680ad9f 100644 --- a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue +++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue @@ -4,7 +4,7 @@ <v-btn slot="activator" outline round color="grey darken-2" - @click="$emit('skip')" + @click="skipSubmission" >Skip</v-btn> <span>Skip this submission</span> </v-tooltip> @@ -133,6 +133,18 @@ }, submit () { this.$emit('submitFeedback', {isFinal: this.isFinal}) + }, + skipSubmission () { + if (this.skippable) { + this.$store.dispatch('skipAssignment').catch(() => { + this.$notify({ + title: 'Unable to skip submission', + type: 'error' + }) + }) + } else { + throw new Error("Can't skip submission when skippable is false for AnnotatedSubmissionBottomToolbar.") + } } } } diff --git a/frontend/src/components/subscriptions/SubscriptionForList.vue b/frontend/src/components/subscriptions/SubscriptionForList.vue index 0326bfce..6d2b1ff0 100644 --- a/frontend/src/components/subscriptions/SubscriptionForList.vue +++ b/frontend/src/components/subscriptions/SubscriptionForList.vue @@ -11,20 +11,9 @@ {{name}} </v-list-tile-content> <v-list-tile-action-text> - {{stageMap[feedback_stage]}} + available: {{available}} </v-list-tile-action-text> </v-list-tile> - <v-btn - icon - @click="deactivate" - > - <v-icon - color="grey" - style="font-size: 20px" - > - delete - </v-icon> - </v-btn> </v-layout> </template> @@ -56,22 +45,13 @@ type: String } }, - data () { - return { - stageMap: { - 'feedback-creation': 'create', - 'feedback-validation': 'validate', - 'feedback-conflict-resolution': 'conflict' - } - } - }, computed: { name () { return this.$store.getters.resolveSubscriptionKeyToName( {query_key: this.query_key, query_type: this.query_type}) }, active () { - return this.available || this.assignments.length > 0 + return !!this.available || this.assignments.length > 0 }, subscriptionRoute () { if (this.active) { @@ -79,21 +59,6 @@ } return this.$route.fullPath } - }, - methods: { - deactivate () { - this.$store.dispatch('deactivateSubscription', {pk: this.pk}).then(() => { - if (this.$route.params.pk === this.pk) { - this.$router.push('/') - } - }).catch(err => { - this.$notify({ - title: `Unable to deactivate subscription ${this.name}`, - text: err.msg, - type: 'error' - }) - }) - } } } </script> diff --git a/frontend/src/components/subscriptions/SubscriptionList.vue b/frontend/src/components/subscriptions/SubscriptionList.vue index 4089f6dc..808e0d31 100644 --- a/frontend/src/components/subscriptions/SubscriptionList.vue +++ b/frontend/src/components/subscriptions/SubscriptionList.vue @@ -2,8 +2,8 @@ <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)"> - Your subscriptions + <v-toolbar-title v-if="!sidebar || (sidebar && !sideBarCollapsed)" style="min-width: fit-content;"> + Tasks </v-toolbar-title> <v-spacer/> <v-btn icon @click="getSubscriptions"> @@ -16,41 +16,28 @@ /> </v-btn> </v-toolbar> - <v-list :dense="sidebar" v-if="!sidebar || (sidebar && !sideBarCollapsed)"> - <div v-for="item in subscriptionTypes" :key="item.type"> - <subscription-type - v-bind="item" - :is-empty-subscription-type="subscriptions[item.type].length === 0" - :possible-subscription-keys="possibleKeys[item.type]" - @toggleExpand="item.expanded = !item.expanded" - > - <div v-if="examTypesLoaded && submissionTypesLoaded"> - <div v-for="subscription in subscriptions[item.type]"> - <subscription-for-list - v-if="subscription.assignments.length > 0 || - !(subscription.deactivated || subscription.remaining === 0)" - :key="subscription.pk" - v-bind="subscription" - > - </subscription-for-list> - </div> - </div> - </subscription-type> - </div> - </v-list> + <v-tabs grow color="teal lighten-1" v-model="selectedStage"> + <v-tab v-for="(item, i) in stagesReadable" :key="i"> + {{item}} + </v-tab> + <v-tab-item v-for="(stage, i) in stages" :key="i"> + <subscriptions-for-stage :stage="stage"/> + </v-tab-item> + </v-tabs> </v-card> </template> + <script> import {mapGetters, mapActions, mapState} from 'vuex' import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation' - import SubscriptionType from '@/components/subscriptions/SubscriptionType' import SubscriptionForList from '@/components/subscriptions/SubscriptionForList' + import SubscriptionsForStage from '@/components/subscriptions/SubscriptionsForStage' export default { components: { + SubscriptionsForStage, SubscriptionForList, - SubscriptionType, SubscriptionCreation}, name: 'subscription-list', props: { @@ -61,48 +48,8 @@ }, data () { return { - subscriptionCreateMenu: {}, - submissionTypesLoaded: false, - examTypesLoaded: false, - updating: false, - subscriptionTypes: [ - { - name: 'Random', - type: 'random', - description: 'Random submissions of all types.', - expanded: true - }, - { - name: 'Exam', - type: 'exam', - description: 'Just submissions for the specified exam.', - expanded: true, - createPermission: () => { - return this.$store.getters.isReviewer - }, - viewPermission: () => { - return this.$store.getters.isReviewer - } - }, - { - name: 'Submission Type', - type: 'submission_type', - description: 'Just submissions for the specified type.', - expanded: true - }, - { - name: 'Student', - type: 'student', - description: 'Submissions of single students.', - expanded: true, - createPermission: () => { - return false - }, - viewPermission: () => { - return this.$store.getters.isReviewer - } - } - ] + selectedStage: null, + updating: false } }, computed: { @@ -110,59 +57,32 @@ sideBarCollapsed: state => state.ui.sideBarCollapsed }), ...mapGetters({ - subscriptions: 'getSubscriptionsGroupedByType' - }), - possibleKeys () { - const submissionTypes = Object.entries(this.$store.state.submissionTypes).map(([id, type]) => { - return {text: type.name, key: type.pk} - }) - const examTypes = Object.entries(this.$store.state.examTypes).map(([id, type]) => { - return {text: type['module_reference'], key: type.pk} - }) - return { - submission_type: submissionTypes, - exam: examTypes - } - } + subscriptions: 'getSubscriptionsGroupedByType', + stages: 'availableStages', + stagesReadable: 'availableStagesReadable' + }) }, methods: { ...mapActions([ 'updateSubmissionTypes', 'getCurrentAssignment', - 'getExamTypes' + 'getExamTypes', + 'subscribeToAll', + 'cleanAssignmentsFromSubscriptions' ]), - getSubscriptions () { + async getSubscriptions () { this.updating = true - 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']}) - } + const subscriptions = await this.$store.dispatch('getSubscriptions') + this.updating = false + return subscriptions } }, created () { - if (Object.keys(this.$store.state.subscriptions).length === 0) { - this.getSubscriptions() - } - if (Object.keys(this.$store.state.submissionTypes).length === 0) { - this.updateSubmissionTypes().then(() => { this.submissionTypesLoaded = true }) - } else { - this.submissionTypesLoaded = true - } - if (Object.keys(this.$store.state.examTypes).length === 0 && - this.$store.getters.isReviewer) { - this.getExamTypes().then(() => { this.examTypesLoaded = true }) - } else { - this.examTypesLoaded = true - } + const typesAndSubscriptions = [this.updateSubmissionTypes(), this.getSubscriptions()] + Promise.all(typesAndSubscriptions).then(() => { + this.subscribeToAll() + this.cleanAssignmentsFromSubscriptions() + }) } } </script> diff --git a/frontend/src/components/subscriptions/SubscriptionType.vue b/frontend/src/components/subscriptions/SubscriptionType.vue deleted file mode 100644 index cb735799..00000000 --- a/frontend/src/components/subscriptions/SubscriptionType.vue +++ /dev/null @@ -1,91 +0,0 @@ -<template> - <div> - <v-list-tile v-if="viewPermission()"> - <v-list-tile-content> - <v-list-tile-title> - {{ name }} - </v-list-tile-title> - <v-list-tile-sub-title> - {{ description }} - </v-list-tile-sub-title> - </v-list-tile-content> - <v-list-tile-action v-if="!isEmptySubscriptionType"> - <v-btn icon @click="$emit('toggleExpand')"> - <v-icon v-if="expanded">keyboard_arrow_up</v-icon> - <v-icon v-else>keyboard_arrow_down</v-icon> - </v-btn> - </v-list-tile-action> - <v-list-tile-action - v-if="createPermission()" - > - <v-menu - offset-x - :min-width="500" - :close-on-content-click="false" - :nudge-width="200" - v-model="subscriptionCreateMenu" - > - <v-btn small flat icon slot="activator"> - <v-icon>add</v-icon> - </v-btn> - <subscription-creation - :title="name" - :type="type" - :keyItems="possibleSubscriptionKeys" - /> - </v-menu> - </v-list-tile-action> - </v-list-tile> - <slot v-if="expanded"></slot> - </div> -</template> - -<script> - import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation' - - export default { - components: {SubscriptionCreation}, - name: 'subscription-type', - props: { - name: { - type: String, - required: true - }, - type: { - type: String, - required: true - }, - description: { - type: String - }, - expanded: { - type: Boolean, - default: true - }, - isEmptySubscriptionType: { - type: Boolean, - required: true - }, - createPermission: { - type: Function, - default: () => true - }, - viewPermission: { - type: Function, - default: () => true - }, - possibleSubscriptionKeys: { - type: Array - } - }, - data () { - return { - subscriptionCreateMenu: {} - } - } - } -</script> - -<style scoped> - -</style> diff --git a/frontend/src/components/subscriptions/SubscriptionsForStage.vue b/frontend/src/components/subscriptions/SubscriptionsForStage.vue new file mode 100644 index 00000000..548c3ae2 --- /dev/null +++ b/frontend/src/components/subscriptions/SubscriptionsForStage.vue @@ -0,0 +1,42 @@ +<template> + <v-list :dense="dense"> + <div> + <div v-for="subscription in subscriptions['submission_type']"> + <subscription-for-list + :key="subscription.pk" + v-bind="subscription" + > + </subscription-for-list> + </div> + </div> + </v-list> +</template> + +<script> + import SubscriptionForList from '@/components/subscriptions/SubscriptionForList' + export default { + components: { + SubscriptionForList + }, + name: 'subscriptions-for-stage', + props: { + stage: { + type: String, + required: true + }, + dense: { + type: Boolean, + default: false + } + }, + computed: { + subscriptions () { + return this.$store.getters.getSubscriptionsGroupedByType[this.stage] + } + } + } +</script> + +<style scoped> + +</style> diff --git a/frontend/src/pages/PageNotFound.vue b/frontend/src/pages/PageNotFound.vue index ba712e1f..83653ae8 100644 --- a/frontend/src/pages/PageNotFound.vue +++ b/frontend/src/pages/PageNotFound.vue @@ -1,7 +1,18 @@ <template> <v-container fill-height> <v-layout align-center justify-center> - <h1>Ooops, something went wrong. There is nothing here.</h1> + <v-card dark width="80%" height="80%"> + <v-card-title style="font-size: 350%"> + The content you're requesting is not available in your country. + </v-card-title> + <v-divider class="px-5"/> + <v-flex xs10 offset-xs2> + <v-card-text class="no-content-text"> + <v-icon size="200px" color="deep-orange accent-4">play_circle_outline</v-icon> + <span style="font-size: xx-large">We're sorry about that ¯\_(ツ)_/¯</span> + </v-card-text> + </v-flex> + </v-card> </v-layout> </v-container> </template> @@ -13,5 +24,8 @@ </script> <style scoped> - + .no-content-text { + position: absolute; + top: 40%; + } </style> diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue index 0449d1a6..fca62801 100644 --- a/frontend/src/pages/SubscriptionWorkPage.vue +++ b/frontend/src/pages/SubscriptionWorkPage.vue @@ -2,19 +2,20 @@ <v-layout row wrap > - <v-flex xs12 md6 class="sub-correction"> - <submission-correction - :assignment="currentAssignment" - :key="subscription.pk" - @feedbackCreated="startWorkOnNextAssignment" - @skip="skipAssignment" - class="ma-4 autofocus" - /> - <submission-tests - :tests="submission.tests" - :expand="true" - class="mx-4" - /> + <v-flex xs12 md6> + <div class="sub-correction"> + <submission-correction + :assignment="currentAssignment" + :key="subscription.pk" + @feedbackCreated="startWorkOnNextAssignment" + class="ma-4 autofocus" + /> + <submission-tests + :tests="submission.tests" + :expand="true" + class="mx-4" + /> + </div> </v-flex> <v-flex md6> @@ -33,23 +34,14 @@ import SubmissionCorrection from '@/components/submission_notes/SubmissionCorrection' import SubmissionType from '@/components/SubmissionType' import store from '@/store/store' - import {mut} from '@/store/mutations' import SubmissionTests from '@/components/SubmissionTests' + import { subscriptionMuts } from '@/store/modules/subscriptions' function onRouteEnterOrUpdate (to, from, next) { if (to.name === 'subscription') { - let subscription = store.state.subscriptions[to.params['pk']] - if (subscription['assignments'].length === 0) { - store.dispatch('getAssignmentForSubscription', {subscription}).then(() => { - next() - }) - store.dispatch('getAssignmentForSubscription', {subscription}) - } else if (subscription['assignments'].length === 1) { - store.dispatch('getAssignmentForSubscription', {subscription}) - next() - } else { + store.dispatch('changeActiveSubscription', to.params['pk']).then(() => { next() - } + }) } } @@ -67,10 +59,10 @@ }, computed: { subscription () { - return this.$store.state.subscriptions[this.$route.params['pk']] + return this.$store.state.subscriptions.subscriptions[this.$route.params['pk']] }, currentAssignment () { - return this.subscription['assignments'][0] + return this.$store.state.subscriptions.assignmentQueue[0] }, submission () { return this.currentAssignment.submission @@ -85,30 +77,19 @@ beforeRouteUpdate (to, from, next) { onRouteEnterOrUpdate(to, from, next) }, + beforeRouteLeave (to, from, next) { + if (to.name !== 'subscription') { + next() + this.$store.dispatch('removeActiveSubscription') + } + }, methods: { - prefetchAssignment () { - this.$store.dispatch('getAssignmentForSubscription', {subscription: this.subscription}).catch(() => { - this.$notify({ - title: 'Last submission here!', - text: 'This will be your last submission to correct for this subscription.', - type: 'warning' - }) - }) - }, startWorkOnNextAssignment () { - this.$store.commit(mut.DELETE_ASSIGNMENT_FROM_SUBSCRIPTION_QUEUE, { - subscription: this.subscription - }) - this.prefetchAssignment() - }, - skipAssignment () { - this.$store.dispatch('deleteAssignment', {assignment: this.currentAssignment}).then(() => { - this.startWorkOnNextAssignment() - }).catch(err => { - this.$notify({ - title: "Couldn't skip this submission", - text: err.msg, - type: 'error' + this.$store.dispatch('getAssignmentsForActiveSubscription', 1).then(([promise]) => { + promise.then(assignment => { + this.$store.commit(subscriptionMuts.ADD_ASSIGNMENT_TO_QUEUE, assignment) + }).finally(() => { + this.$store.commit(subscriptionMuts.POP_ASSIGNMENT_FROM_QUEUE) }) }) } diff --git a/frontend/src/store/actions.js b/frontend/src/store/actions.js index e9556598..9bf5e08d 100644 --- a/frontend/src/store/actions.js +++ b/frontend/src/store/actions.js @@ -1,60 +1,20 @@ -import {mut} from './mutations' -import {authMut} from '@/store/modules/authentication' -import {subNotesMut} from '@/store/modules/submission-notes' +import { mut } from './mutations' +import { authMut } from '@/store/modules/authentication' +import { subNotesMut } from '@/store/modules/submission-notes' import * as api from '@/api' import router from '@/router/index' - -function handleError (err, dispatch, fallbackMsg) { - if (err.response) { - if (err.response.status === 401) { - dispatch('logout', "You've been logged out") - } else { - throw new Error(err.response.data) - } - } else { - if (fallbackMsg) { - throw new Error(fallbackMsg) - } - } -} +import { handleError } from '@/util/helpers' const actions = { - async getSubscriptions ({commit, dispatch}) { - try { - const subscriptions = await api.fetchSubscriptions() - commit(mut.SET_SUBSCRIPTIONS, subscriptions) - return subscriptions - } catch (err) { - handleError(err, dispatch, 'Unable to fetch subscriptions') - } - }, async getExamTypes ({commit, dispatch}) { try { const examTypes = await api.fetchExamType({}) commit(mut.SET_EXAM_TYPES, examTypes) } catch (err) { - handleError(err, dispatch, 'Unable to fetch exam mut') - } - }, - async subscribeTo ({commit, dispatch}, {type, key, stage}) { - try { - const subscription = await api.subscribeTo(type, key, stage) - commit(mut.SET_SUBSCRIPTION, subscription) - return subscription - } catch (err) { - handleError(err, dispatch, 'Subscribing unsuccessful') - } - }, - async deactivateSubscription ({commit, dispatch}, {subscription, pk}) { - try { - const subscriptionPk = subscription ? subscription.pk : pk - await api.deactivateSubscription({pk: subscriptionPk}) - commit(mut.DELETE_SUBSCRIPTION, {pk: subscriptionPk}) - } catch (err) { - handleError(err, dispatch, 'Unable to deactivate subscription') + handleError(err, dispatch, 'Unable to fetch exam types') } }, - async updateSubmissionTypes ({commit, dispatch}, fields) { + async updateSubmissionTypes ({commit, dispatch}, fields = []) { try { const submissionTypes = await api.fetchSubmissionTypes(fields) submissionTypes.forEach(type => { @@ -64,26 +24,6 @@ const actions = { handleError(err, dispatch, 'Unable to get submission types') } }, - async getAssignmentForSubscription ({commit, state, dispatch}, {subscription}) { - if (subscription.assignments.length < 2) { - try { - const assignment = await api.createAssignment({subscription}) - commit(mut.ADD_ASSIGNMENT_TO_SUBSCRIPTION_QUEUE, {assignment}) - return assignment - } catch (err) { - handleError(err, dispatch, "Couldn't fetch assignment") - } - } else { - return subscription.assignments[0] - } - }, - async deleteAssignment ({commit, state, dispatch}, {assignment}) { - try { - return await api.deleteAssignment({assignment}) - } catch (err) { - handleError(err, dispatch, 'Unable to delete assignment') - } - }, async getStudents ({commit, dispatch}, opt = {studentPks: [], fields: []}) { try { if (opt.studentPks.length === 0) { @@ -162,10 +102,11 @@ const actions = { } }, logout ({ commit }, message = '') { - commit(mut.RESET_STATE) + commit('RESET_STATE') commit('submissionNotes/' + subNotesMut.RESET_STATE) commit(authMut.SET_MESSAGE, message) router.push({name: 'login'}) + router.go() } } diff --git a/frontend/src/store/getters.js b/frontend/src/store/getters.js index c44ef362..5663746c 100644 --- a/frontend/src/store/getters.js +++ b/frontend/src/store/getters.js @@ -1,51 +1,9 @@ const getters = { - getSubscriptionsGroupedByType (state, getters) { - let subscriptions = { - 'random': [], - 'student': [], - 'exam': [], - 'submission_type': [] - } - Object.entries(state.subscriptions).forEach(([id, subscription]) => { - subscriptions[subscription.query_type].push(subscription) - }) - // sort the resulting arrays in subscriptions lexicographically by their query_keys - Object.entries(subscriptions).forEach(([id, 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 - } - }) - } - }) - return subscriptions - }, corrected (state) { return state.statistics.submission_type_progress.every(progress => { return progress.percentage === 100 }) }, - resolveSubscriptionKeyToName: state => subscription => { - if (subscription.query_type === 'random') { - return 'Active' - } else if (subscription.query_type === 'exam') { - const examType = state.examTypes[subscription.query_key] - return examType ? examType.module_reference : 'Exam' - } else if (subscription.query_type === 'submission_type') { - const submissionType = state.submissionTypes[subscription.query_key] - return submissionType ? submissionType.name : 'Submission Type' - } else if (subscription.query_type === 'student') { - const studentName = state.students[subscription.query_key] - return studentName ? studentName.name : 'Student' - } - }, getSubmission: state => pk => { return state.submissions[pk] }, diff --git a/frontend/src/store/modules/subscriptions.js b/frontend/src/store/modules/subscriptions.js new file mode 100644 index 00000000..531ea809 --- /dev/null +++ b/frontend/src/store/modules/subscriptions.js @@ -0,0 +1,274 @@ +import Vue from 'vue' +import * as api from '@/api' +import {handleError, flatten, cartesian, once} from '@/util/helpers' + +export const subscriptionMuts = Object.freeze({ + SET_SUBSCRIPTIONS: 'SET_SUBSCRIPTIONS', + SET_SUBSCRIPTION: 'SET_SUBSCRIPTION', + SET_LOADING: 'SET_LOADING', + SET_ACTIVE_SUBSCRIPTION_PK: 'SET_ACTIVE_SUBSCRIPTION_PK', + SET_ASSIGNMENT_QUEUE: 'SET_ASSIGNMENT_QUEUE', + ADD_ASSIGNMENT_TO_QUEUE: 'ADD_ASSIGNMENT_TO_QUEUE', + POP_ASSIGNMENT_FROM_QUEUE: 'POP_ASSIGNMENT_FROM_QUEUE', + RESET_STATE: 'RESET_STATE' +}) + +function initialState () { + return { + subscriptions: {}, + assignmentQueue: [], + activeSubscriptionPk: '', + loading: false + } +} + +const MAX_NUMBER_OF_ASSIGNMENTS = 2 + +// noinspection JSCommentMatchesSignature +const subscriptions = { + state: initialState(), + getters: { + availableTypes (state, getters) { + let types = ['random', 'submission_type'] + if (getters.isReviewer) { + types.push('exam') + } + return types + }, + availableStages (state, getters) { + let stages = ['feedback-creation', 'feedback-validation'] + if (getters.isReviewer) { + stages.push('feedback-conflict-resolution') + } + return stages + }, + availableStagesReadable (state, getters) { + let stages = ['create', 'validate'] + if (getters.isReviewer) { + stages.push('resolve') + } + return stages + }, + availableSubmissionTypeQueryKeys (state, getters, rootState) { + return Object.values(rootState.submissionTypes).map(subType => subType.pk) + }, + availableExamTypeQueryKeys (state, getters, rootState) { + return Object.values(rootState.examTypes).map(examType => examType.pk) + }, + activeSubscription (state) { + return state.subscriptions[state.activeSubscriptionPk] + }, + resolveSubscriptionKeyToName: (state, getters, rootState) => subscription => { + switch (subscription.query_type) { + case 'random': + return 'Active' + case 'exam': + const examType = rootState.examTypes[subscription.query_key] + return examType ? examType.module_reference : 'Exam' + case 'submission_type': + const submissionType = rootState.submissionTypes[subscription.query_key] + return submissionType ? submissionType.name : 'Submission Type' + case 'student': + const studentName = rootState.students[subscription.query_key] + return studentName ? studentName.name : 'Student' + } + }, + getSubscriptionsGroupedByType (state, getters) { + const subscriptionsByType = () => { + return { + 'random': [], + 'student': [], + 'exam': [], + 'submission_type': [] + } + } + let subscriptionsByStage = getters.availableStages.reduce((acc, curr) => { + acc[curr] = subscriptionsByType() + return acc + }, {}) + Object.values(state.subscriptions).forEach(subscription => { + 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 + } + }) + } + }) + Object.values(subscriptionsByStage).forEach(subscriptionsByType => { + sortSubscriptions(subscriptionsByType) + }) + return subscriptionsByStage + } + }, + mutations: { + [subscriptionMuts.SET_SUBSCRIPTIONS] (state, subscriptions) { + state.subscriptions = subscriptions.reduce((acc, curr) => { + acc[curr['pk']] = curr + return acc + }, {}) + }, + [subscriptionMuts.SET_SUBSCRIPTION] (state, subscription) { + Vue.set(state.subscriptions, subscription.pk, subscription) + }, + [subscriptionMuts.SET_ACTIVE_SUBSCRIPTION_PK] (state, subscriptionPk) { + state.activeSubscriptionPk = subscriptionPk + }, + [subscriptionMuts.SET_ASSIGNMENT_QUEUE] (state, queue) { + state.assignmentQueue = queue + }, + [subscriptionMuts.ADD_ASSIGNMENT_TO_QUEUE] (state, assignment) { + state.assignmentQueue.push(assignment) + }, + [subscriptionMuts.POP_ASSIGNMENT_FROM_QUEUE] (state) { + state.assignmentQueue.shift() + }, + [subscriptionMuts.RESET_STATE] (state) { + Object.assign(state, initialState()) + } + }, + actions: { + subscribeTo: async function ({commit, dispatch, getters}, {type, key, stage}) { + try { + // don't subscribe to type, key, stage combinations if they're already present + let subscription = getters.getSubscriptionsGroupedByType[stage][type].find(elem => { + if (type === 'random') { + return true + } + return elem.query_key === key + }) + subscription = subscription || await api.subscribeTo(type, key, stage) + commit(subscriptionMuts.SET_SUBSCRIPTION, subscription) + return subscription + } catch (err) { + console.log(err) + handleError(err, dispatch, 'Subscribing unsuccessful') + } + }, + async getSubscriptions ({commit, dispatch}) { + try { + const subscriptions = await api.fetchSubscriptions() + commit(subscriptionMuts.SET_SUBSCRIPTIONS, subscriptions) + return subscriptions + } catch (err) { + console.log(err) + handleError(err, dispatch, 'Unable to fetch subscriptions') + } + }, + /** + * Creates as many assignments as needed to reach MAX_NUMBER_OF_ASSIGNMENTS + * @param numOfAssignments Use to override default behaviour of creating MAX_NUMBER_OF_ASSIGNMENTS - assignmentQueue.length assignments + * @returns {Promise<[Promise]>} returns Promise of Array of Promises of assignments + */ + async getAssignmentsForActiveSubscription ({commit, state, dispatch, getters}, numOfAssignments) { + numOfAssignments = numOfAssignments || MAX_NUMBER_OF_ASSIGNMENTS - state.assignmentQueue.length + let assignmentsPromises = [] + for (let i = 0; i < numOfAssignments; i++) { + assignmentsPromises.push(api.createAssignment({subscription: getters.activeSubscription})) + } + return assignmentsPromises + }, + async deleteAssignment ({commit, state, dispatch}, assignment) { + try { + return await api.deleteAssignment({assignment}) + } catch (err) { + handleError(err, dispatch, 'Unable to delete assignment') + } + }, + async cleanAssignmentsFromSubscriptions ({commit, state, dispatch}, excludeActive = true) { + Object.values(state.subscriptions).forEach(subscription => { + if (!excludeActive || subscription.pk !== state.activeSubscriptionPk) { + subscription.assignments.forEach(assignment => { + api.deleteAssignment({assignment}) + }) + } + }) + }, + async skipAssignment ({commit, state, dispatch}) { + try { + dispatch('deleteAssignment', state.assignmentQueue[0]) + .then(() => { + // pass numOfAssignments = 1 to create 1 new assignment although maybe two are already in the queue, + // this is needed because otherwise the current assignment in the comp. might be unknown for a period + // which will result get incorrectly interpreted as a an ended subscription + return dispatch('getAssignmentsForActiveSubscription', 1) + }).then(([promise]) => { + promise.then(assignment => { + commit(subscriptionMuts.ADD_ASSIGNMENT_TO_QUEUE, assignment) + commit(subscriptionMuts.POP_ASSIGNMENT_FROM_QUEUE) + }) + }) + } catch (err) { + handleError(err, dispatch, 'Unable to skip assignment') + } + }, + async deleteActiveAssignments ({commit, state, dispatch}) { + try { + Promise.all(state.assignmentQueue.map(assignment => { + return dispatch('deleteAssignment', assignment) + })) + } catch (err) { + handleError(err, dispatch, 'Unable to remove assignments.') + } + }, + async changeActiveSubscription ({commit, state, dispatch}, subscriptionPk = '') { + if (subscriptionPk !== state.activeSubscriptionPk) { + await dispatch('deleteActiveAssignments') + commit(subscriptionMuts.SET_ACTIVE_SUBSCRIPTION_PK, subscriptionPk) + let assignmentsPromises = await dispatch('getAssignmentsForActiveSubscription', MAX_NUMBER_OF_ASSIGNMENTS) + let createdAssignments = [] + for (let promise of assignmentsPromises) { + try { + createdAssignments.push(await promise) + } catch (_) {} + } + commit(subscriptionMuts.SET_ASSIGNMENT_QUEUE, createdAssignments) + } + }, + async removeActiveSubscription ({commit, state, dispatch}) { + await dispatch('deleteActiveAssignments') + commit(subscriptionMuts.SET_ASSIGNMENT_QUEUE, []) + commit(subscriptionMuts.SET_ACTIVE_SUBSCRIPTION_PK, '') + }, + async subscribeToType ({commit, state, dispatch, getters}, type) { + switch (type) { + case 'random': + return getters.availableStages.map(stage => { + dispatch('subscribeTo', {type, stage}) + }) + case 'exam': + if (getters.isReviewer) { + const stageKeyCartesian = cartesian( + getters.availableStages, getters.availableExamTypeQueryKeys) + return stageKeyCartesian.map(([stage, key]) => { + dispatch('subscribeTo', {stage, type, key}) + }) + } + return [] + case 'submission_type': + const stageKeyCartesian = cartesian( + getters.availableStages, getters.availableSubmissionTypeQueryKeys) + return stageKeyCartesian.map(([stage, key]) => { + dispatch('subscribeTo', {stage, type, key}) + }) + } + }, + subscribeToAll: once(async ({commit, state, dispatch, getters}) => { + return Promise.all(flatten(getters.availableTypes.map(type => { + return dispatch('subscribeToType', type) + }))) + }) + } +} + +export default subscriptions diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.js index 60a963a8..2e4c4081 100644 --- a/frontend/src/store/mutations.js +++ b/frontend/src/store/mutations.js @@ -3,12 +3,6 @@ import Vue from 'vue' import {initialState} from '@/store/store' export const mut = Object.freeze({ - ADD_ASSIGNMENT_TO_SUBSCRIPTION_QUEUE: 'ADD_ASSIGNMENT_TO_SUBSCRIPTION_QUEUE', - DELETE_ASSIGNMENT_FROM_SUBSCRIPTION_QUEUE: 'DELETE_ASSIGNMENT_FROM_SUBSCRIPTION_QUEUE', - SET_ASSIGNMENT: 'SET_ASSIGNMENT', - SET_SUBSCRIPTIONS: 'SET_SUBSCRIPTIONS', - SET_SUBSCRIPTION: 'SET_SUBSCRIPTION', - DELETE_SUBSCRIPTION: 'DELETE_SUBSCRIPTION', SET_LAST_INTERACTION: 'SET_LAST_INTERACTION', SET_EXAM_TYPES: 'SET_EXAM_TYPES', SET_STUDENTS: 'SET_STUDENTS', @@ -32,9 +26,6 @@ const mutations = { return acc }, {}) }, - [mut.SET_ASSIGNMENT] (state, assignment) { - Vue.set(state.assignments, assignment.pk, assignment) - }, [mut.SET_SUBSCRIPTIONS] (state, subscriptions) { state.subscriptions = subscriptions.reduce((acc, curr) => { acc[curr['pk']] = curr @@ -56,9 +47,6 @@ const mutations = { } } }, - [mut.DELETE_SUBSCRIPTION] (state, {pk}) { - Vue.delete(state.subscriptions, pk) - }, [mut.SET_STUDENTS] (state, students) { state.students = students.reduce((acc, curr) => { acc[curr.pk] = mapStudent(curr, state.studentMap) @@ -117,14 +105,6 @@ const mutations = { [mut.UPDATE_SUBMISSION] (state, {submissionPk, payload, key}) { Vue.set(state.submissions[submissionPk], key, payload) }, - [mut.ADD_ASSIGNMENT_TO_SUBSCRIPTION_QUEUE] (state, {assignment}) { - let subscription = state.subscriptions[assignment.subscription] - subscription['assignments'].push(assignment) - }, - [mut.DELETE_ASSIGNMENT_FROM_SUBSCRIPTION_QUEUE] (state, {subscription}) { - subscription.assignments.shift() - }, - [mut.SET_LAST_INTERACTION] (state) { state.lastAppInteraction = Date.now() }, diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 2d0b8af1..588aa4e2 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -6,6 +6,7 @@ import studentPage from './modules/student-page' import submissionNotes from './modules/submission-notes' import authentication from './modules/authentication' import ui from './modules/ui' +import subscriptions from './modules/subscriptions' import feedbackSearchOptions from './modules/feedback_list/feedback-search-options' import actions from './actions' @@ -22,8 +23,6 @@ export function initialState () { submissionTypes: {}, submissions: {}, feedback: {}, - subscriptions: {}, - assignments: {}, students: {}, studentMap: {}, // is used to map obfuscated student data back to the original statistics: { @@ -42,6 +41,7 @@ const store = new Vuex.Store({ studentPage, submissionNotes, ui, + subscriptions, feedbackSearchOptions }, plugins: [ @@ -51,8 +51,8 @@ const store = new Vuex.Store({ // authentication.token is manually saved since using it with this plugin caused issues // when manually reloading the page paths: Object.keys(initialState()).concat( - ['ui', 'studentPage', 'submissionNotes', 'authentication.username', - 'authentication.userRole', 'authentication.jwtTimeDelta', + ['ui', 'studentPage', 'submissionNotes', 'feedbackSearchOptions', 'subscriptions', + 'authentication.username', 'authentication.userRole', 'authentication.jwtTimeDelta', 'authentication.tokenCreationTime']) }), lastInteraction], diff --git a/frontend/src/util/helpers.js b/frontend/src/util/helpers.js index 221bf042..f25491c1 100644 --- a/frontend/src/util/helpers.js +++ b/frontend/src/util/helpers.js @@ -56,3 +56,41 @@ export function mapStateToComputedGetterSetter ({namespace = '', pathPrefix = '' return acc }, {}) } + +// thanks to rsp +// 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) { + return b ? cartesian(cartesianHelper(a, b), ...c) : a +} + +// flatten an array +export function flatten (list) { + return list.reduce( + (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] + ) +} + +export function once (fn, context) { + let result + return function () { + if (!result) { + result = fn.apply(context || this, arguments) + } + return result + } +} + +export function handleError (err, dispatch, fallbackMsg) { + if (err.response) { + if (err.response.status === 401) { + dispatch('logout', 'You\'ve been logged out') + } else { + throw err + } + } else { + if (fallbackMsg) { + throw new Error(fallbackMsg) + } + } +} -- GitLab From fd4d81943d56b6cdbfa5b92a5bad3837d562ba3f Mon Sep 17 00:00:00 2001 From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de> Date: Thu, 5 Apr 2018 01:39:13 +0200 Subject: [PATCH 2/6] Desc. and Solution are now always open by default --- frontend/src/pages/StudentSubmissionSideView.vue | 2 +- frontend/src/pages/SubscriptionWorkPage.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/StudentSubmissionSideView.vue b/frontend/src/pages/StudentSubmissionSideView.vue index 6085f04d..acabb60d 100644 --- a/frontend/src/pages/StudentSubmissionSideView.vue +++ b/frontend/src/pages/StudentSubmissionSideView.vue @@ -13,7 +13,7 @@ v-bind="submissionType" :key="submissionType.pk" :reverse="true" - :expandedByDefault="{ Description: false, Solution: false }" + :expandedByDefault="{ Description: true, Solution: true }" class="mt-1" /> </div> diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue index fca62801..96d555ff 100644 --- a/frontend/src/pages/SubscriptionWorkPage.vue +++ b/frontend/src/pages/SubscriptionWorkPage.vue @@ -23,7 +23,7 @@ v-bind="submissionType" :key="submissionType.pk" :reverse="true" - :expandedByDefault="{ Description: false, Solution: true }" + :expandedByDefault="{ Description: true, Solution: true }" class="mt-4 mr-4" /> </v-flex> -- GitLab From 35babfa674d3bad8b3aa3c4c5bbc5231c77dbf7c Mon Sep 17 00:00:00 2001 From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de> Date: Thu, 5 Apr 2018 03:46:31 +0200 Subject: [PATCH 3/6] Implemented RouteChangeConfirmation when unsaved feedback --- .../RouteChangeConfirmation.vue | 57 +++++++++++++++++++ .../submission_notes/SubmissionCorrection.vue | 9 ++- .../submission_notes/base/FeedbackComment.vue | 2 +- .../src/pages/StudentSubmissionSideView.vue | 17 +++++- frontend/src/pages/SubscriptionWorkPage.vue | 16 ++++-- frontend/src/router/index.js | 1 + .../src/store/modules/submission-notes.js | 13 +++-- 7 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/submission_notes/RouteChangeConfirmation.vue diff --git a/frontend/src/components/submission_notes/RouteChangeConfirmation.vue b/frontend/src/components/submission_notes/RouteChangeConfirmation.vue new file mode 100644 index 00000000..3df4f48c --- /dev/null +++ b/frontend/src/components/submission_notes/RouteChangeConfirmation.vue @@ -0,0 +1,57 @@ +<template> + <v-dialog + v-model="dialog" + max-width="fit-content" + > + <v-card> + <v-card-title class="title"> + Are you sure? + </v-card-title> + <v-card-text> + Not submitted feedback will be lost! + </v-card-text> + <v-card-actions> + <v-btn flat outline color="red lighten-1" @click="changeRoute">Change page</v-btn> + <v-btn flat outline @click="dialog = false">Stay here</v-btn> + </v-card-actions> + </v-card> + </v-dialog> +</template> + +<script> + export default { + name: 'route-change-confirmation', + props: { + nextRoute: { + type: Function, + default: null + } + }, + data () { + return { + dialog: false + } + }, + methods: { + changeRoute () { + this.nextRoute() + this.dialog = false + } + }, + watch: { + nextRoute (newVal, oldVal) { + if (newVal !== oldVal && this.$store.getters['submissionNotes/workInProgress']) { + console.log('here') + this.dialog = true + } else { + console.log('there') + this.nextRoute() + } + } + } + } +</script> + +<style scoped> + +</style> diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue index 376a73f8..34e72856 100644 --- a/frontend/src/components/submission_notes/SubmissionCorrection.vue +++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue @@ -65,9 +65,11 @@ import BaseAnnotatedSubmission from '@/components/submission_notes/base/BaseAnnotatedSubmission' import SubmissionLine from '@/components/submission_notes/base/SubmissionLine' import {subNotesMut, subNotesNamespace} from '@/store/modules/submission-notes' + import RouteChangeConfirmation from '@/components/submission_notes/RouteChangeConfirmation' export default { components: { + RouteChangeConfirmation, SubmissionLine, BaseAnnotatedSubmission, AnnotatedSubmissionBottomToolbar, @@ -78,7 +80,7 @@ data () { return { loading: false, - feedbackShortPollInterval: undefined + feedbackShortPollInterval: null } }, props: { @@ -108,7 +110,8 @@ 'isReviewer', 'getSubmission', 'getFeedback', - 'getSubmissionType' + 'getSubmissionType', + 'workInProgress' ]), submission () { return this.$store.getters['submissionNotes/submission'] @@ -158,7 +161,7 @@ } }, watch: { - assignment: function () { + assignment: function (newVar, oldVar) { this.init() }, submissionWithoutAssignment: function () { diff --git a/frontend/src/components/submission_notes/base/FeedbackComment.vue b/frontend/src/components/submission_notes/base/FeedbackComment.vue index af3a997f..bfd4b946 100644 --- a/frontend/src/components/submission_notes/base/FeedbackComment.vue +++ b/frontend/src/components/submission_notes/base/FeedbackComment.vue @@ -74,7 +74,7 @@ }, computed: { ...mapState({ - markedForDeletion: state => state.submissionNotes.commentsMarkedForDeletetion, + markedForDeletion: state => state.submissionNotes.commentsMarkedForDeletion, darkMode: state => state.ui.darkMode }), parsedCreated () { diff --git a/frontend/src/pages/StudentSubmissionSideView.vue b/frontend/src/pages/StudentSubmissionSideView.vue index acabb60d..505ee8f8 100644 --- a/frontend/src/pages/StudentSubmissionSideView.vue +++ b/frontend/src/pages/StudentSubmissionSideView.vue @@ -1,10 +1,11 @@ <template> <div> + <route-change-confirmation :next-route="nextRoute"/> <submission-correction :submission-without-assignment="submission" :feedback="submission.feedback" @feedbackCreated="refresh" - ></submission-correction> + /> <submission-tests :tests="submission.tests" class="mt-4" @@ -25,6 +26,7 @@ import SubmissionCorrection from '@/components/submission_notes/SubmissionCorrection' import SubmissionTests from '@/components/SubmissionTests' import SubmissionType from '@/components/SubmissionType' + import RouteChangeConfirmation from '@/components/submission_notes/RouteChangeConfirmation' function onRouteEnterOrUpdate (to, from, next) { const toIsSubmissionSideView = to.matched.some(route => route.meta.submissionSideView) @@ -53,10 +55,16 @@ export default { components: { + RouteChangeConfirmation, SubmissionType, SubmissionTests, SubmissionCorrection}, name: 'student-submission-side-view', + data () { + return { + nextRoute: null + } + }, computed: { submissionPk () { return this.$route.params['submissionPk'] @@ -82,7 +90,12 @@ onRouteEnterOrUpdate(to, from, next) }, beforeRouteUpdate (to, from, next) { - onRouteEnterOrUpdate(to, from, next) + this.nextRoute = () => { + onRouteEnterOrUpdate(to, from, next) + } + }, + beforeRouteLeave (to, from, next) { + this.nextRoute = next } } </script> diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue index 96d555ff..9e472cd0 100644 --- a/frontend/src/pages/SubscriptionWorkPage.vue +++ b/frontend/src/pages/SubscriptionWorkPage.vue @@ -2,6 +2,7 @@ <v-layout row wrap > + <route-change-confirmation :next-route="nextRoute"/> <v-flex xs12 md6> <div class="sub-correction"> <submission-correction @@ -36,6 +37,7 @@ import store from '@/store/store' import SubmissionTests from '@/components/SubmissionTests' import { subscriptionMuts } from '@/store/modules/subscriptions' + import RouteChangeConfirmation from '@/components/submission_notes/RouteChangeConfirmation' function onRouteEnterOrUpdate (to, from, next) { if (to.name === 'subscription') { @@ -47,6 +49,7 @@ export default { components: { + RouteChangeConfirmation, SubmissionTests, SubmissionType, SubmissionCorrection @@ -54,7 +57,8 @@ name: 'subscription-work-page', data () { return { - subscriptionActive: true + subscriptionActive: true, + nextRoute: null } }, computed: { @@ -75,12 +79,15 @@ onRouteEnterOrUpdate(to, from, next) }, beforeRouteUpdate (to, from, next) { - onRouteEnterOrUpdate(to, from, next) + this.nextRoute = () => { + onRouteEnterOrUpdate(to, from, next) + } }, beforeRouteLeave (to, from, next) { - if (to.name !== 'subscription') { + if (to.name === 'subscription-ended') { next() - this.$store.dispatch('removeActiveSubscription') + } else { + this.nextRoute = next } }, methods: { @@ -98,6 +105,7 @@ currentAssignment (val) { if (val === undefined) { this.$router.replace('ended') + this.$store.dispatch('removeActiveSubscription') this.$store.dispatch('getSubscriptions') } } diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index f4af722d..15701cfe 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -80,6 +80,7 @@ const router = new Router({ }, { path: 'subscription/ended', + name: 'subscription-ended', component: SubscriptionEnded }, { diff --git a/frontend/src/store/modules/submission-notes.js b/frontend/src/store/modules/submission-notes.js index 82008d4f..08669ce0 100644 --- a/frontend/src/store/modules/submission-notes.js +++ b/frontend/src/store/modules/submission-notes.js @@ -41,7 +41,7 @@ function initialState () { score: null, feedback_lines: {} }, - commentsMarkedForDeletetion: {} + commentsMarkedForDeletion: {} } } @@ -69,6 +69,11 @@ const submissionNotes = { score: state => { return state.updatedFeedback.score !== null ? state.updatedFeedback.score : state.origFeedback.score }, + workInProgress: state => { + const openEditor = Object.values(state.ui.showEditorOnLine).reduce((acc, curr) => acc || curr, false) + const feedbackWritten = Object.entries(state.updatedFeedback.feedback_lines).length > 0 + return openEditor || feedbackWritten + }, isFeedbackCreation: state => { return !state.origFeedback['feedback_stage_for_user'] || state.origFeedback['feedback_stage_for_user'] === 'feedback-creation' @@ -101,10 +106,10 @@ const submissionNotes = { Vue.set(state.ui.showEditorOnLine, lineNo, !state.ui.showEditorOnLine[lineNo]) }, [subNotesMut.MARK_COMMENT_FOR_DELETION]: function (state, comment) { - Vue.set(state.commentsMarkedForDeletetion, comment.pk, comment) + Vue.set(state.commentsMarkedForDeletion, comment.pk, comment) }, [subNotesMut.UN_MARK_COMMENT_FOR_DELETION]: function (state, comment) { - Vue.delete(state.commentsMarkedForDeletetion, comment.pk) + Vue.delete(state.commentsMarkedForDeletion, comment.pk) }, [subNotesMut.RESET_UPDATED_FEEDBACK]: function (state) { state.updatedFeedback = initialState().updatedFeedback @@ -116,7 +121,7 @@ const submissionNotes = { actions: { deleteComments: async function ({state}) { return Promise.all( - Object.values(state.commentsMarkedForDeletetion).map(comment => { + Object.values(state.commentsMarkedForDeletion).map(comment => { return api.deleteComment(comment) }) ) -- GitLab From 9b7e5fca71e23635b3f81fd6f68cd3163093f67c Mon Sep 17 00:00:00 2001 From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de> Date: Thu, 5 Apr 2018 03:52:13 +0200 Subject: [PATCH 4/6] Fixed double visible to student icon bug --- .../src/components/submission_notes/SubmissionCorrection.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue index 34e72856..49cdd72f 100644 --- a/frontend/src/components/submission_notes/SubmissionCorrection.vue +++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue @@ -17,6 +17,7 @@ <feedback-comment v-for="(comment, index) in origFeedback[lineNo]" v-bind="comment" + :visible_to_student="updatedFeedback[lineNo] ? false : comment.visible_to_student" :line-no="lineNo" :key="index" :deletable="comment.of_tutor === user || isReviewer" -- GitLab From cfa815dd88e6a0574d945b98344b60b1f06ff97f Mon Sep 17 00:00:00 2001 From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de> Date: Thu, 5 Apr 2018 04:14:51 +0200 Subject: [PATCH 5/6] Scrolling to top on correction page when submitting --- frontend/src/pages/SubscriptionWorkPage.vue | 41 +++++++++++---------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue index 9e472cd0..940be057 100644 --- a/frontend/src/pages/SubscriptionWorkPage.vue +++ b/frontend/src/pages/SubscriptionWorkPage.vue @@ -4,29 +4,29 @@ > <route-change-confirmation :next-route="nextRoute"/> <v-flex xs12 md6> - <div class="sub-correction"> - <submission-correction - :assignment="currentAssignment" - :key="subscription.pk" - @feedbackCreated="startWorkOnNextAssignment" - class="ma-4 autofocus" - /> - <submission-tests - :tests="submission.tests" - :expand="true" - class="mx-4" - /> - </div> + <submission-correction + :assignment="currentAssignment" + :key="subscription.pk" + @feedbackCreated="startWorkOnNextAssignment" + class="ma-4 autofocus" + /> + <submission-tests + :tests="submission.tests" + :expand="true" + class="mx-4" + /> </v-flex> <v-flex md6> - <submission-type - v-bind="submissionType" - :key="submissionType.pk" - :reverse="true" - :expandedByDefault="{ Description: true, Solution: true }" - class="mt-4 mr-4" - /> + <div class="sub-correction"> + <submission-type + v-bind="submissionType" + :key="submissionType.pk" + :reverse="true" + :expandedByDefault="{ Description: true, Solution: true }" + class="mt-4 mr-4" + /> + </div> </v-flex> </v-layout> </template> @@ -103,6 +103,7 @@ }, watch: { currentAssignment (val) { + this.$vuetify.goTo(0, {duration: 200, easing: 'easeInOutCubic'}) if (val === undefined) { this.$router.replace('ended') this.$store.dispatch('removeActiveSubscription') -- GitLab From e0ac3842d5ca3e03dc1d14d51b14a89b94902cc9 Mon Sep 17 00:00:00 2001 From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de> Date: Thu, 5 Apr 2018 12:24:31 +0200 Subject: [PATCH 6/6] Added registration option on login page --- frontend/src/api.js | 14 ++++- frontend/src/components/RegisterDialog.vue | 59 ++++++++++++++++++++++ frontend/src/pages/Login.vue | 14 +++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/RegisterDialog.vue diff --git a/frontend/src/api.js b/frontend/src/api.js index b35bce86..b35d6811 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -13,9 +13,19 @@ function getInstanceBaseUrl () { } let ax = axios.create({ - baseURL: getInstanceBaseUrl(), - headers: {'Authorization': 'JWT ' + sessionStorage.getItem('token')} + baseURL: getInstanceBaseUrl() + // headers: {'Authorization': 'JWT ' + sessionStorage.getItem('token')} }) +{ + let token = sessionStorage.getItem('token') + if (token) { + ax.defaults.headers['Authorization'] = `JWT ${token}` + } +} + +export async function registerTutor (credentials) { + return ax.post('/api/tutor/register/', credentials) +} export async function fetchJWT (credentials) { const token = (await ax.post('/api/get-token/', credentials)).data.token diff --git a/frontend/src/components/RegisterDialog.vue b/frontend/src/components/RegisterDialog.vue new file mode 100644 index 00000000..8c2425da --- /dev/null +++ b/frontend/src/components/RegisterDialog.vue @@ -0,0 +1,59 @@ +<template> + <v-card> + <v-card-title class="title"> + Register + </v-card-title> + <v-card-text> + <v-text-field + label="Username" + required + autofocus + v-model="credentials.username" + /> + <v-text-field + label="Password" + required + type="password" + v-model="credentials.password" + /> + </v-card-text> + <v-card-actions class="justify-center"> + <v-btn flat :loading="loading" @click="register">submit</v-btn> + </v-card-actions> + </v-card> +</template> + +<script> + import { registerTutor } from '@/api' + + export default { + name: 'register-dialog', + data () { + return { + credentials: { + username: '', + password: '' + }, + loading: false + } + }, + methods: { + register () { + this.loading = true + registerTutor(this.credentials).then(() => { + this.$emit('registered', this.credentials) + }).catch(() => { + this.$notify({ + title: 'Unable to register', + text: "Couldn't register a tutor account.", + type: 'error' + }) + }).finally(() => { this.loading = false }) + } + } + } +</script> + +<style scoped> + +</style> diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue index 5ae6b8e2..13fd519f 100644 --- a/frontend/src/pages/Login.vue +++ b/frontend/src/pages/Login.vue @@ -1,6 +1,9 @@ <template> <v-container fill-height> <v-layout align-center justify-center> + <v-dialog v-model="registerDialog" max-width="fit-content" class="pa-4"> + <register-dialog @registered="registered($event)"/> + </v-dialog> <v-flex text-xs-center xs8 sm6 md4 lg2> <img v-if="production" :src="productionBrandUrl"/> <img v-else src="../assets/brand.png"/> @@ -27,6 +30,7 @@ type="password" required /> + <v-btn @click="registerDialog = true">register</v-btn> <v-btn :loading="loading" type="submit" color="primary">Access</v-btn> </v-form> </v-flex> @@ -37,8 +41,11 @@ <script> import {mapActions, mapState} from 'vuex' + import RegisterDialog from '@/components/RegisterDialog' + import { authMut } from '@/store/modules/authentication' export default { + components: {RegisterDialog}, name: 'grady-login', data () { return { @@ -46,6 +53,7 @@ username: '', password: '' }, + registerDialog: false, loading: false } }, @@ -76,6 +84,12 @@ this.getJWTTimeDelta() this.loading = false }).catch(() => { this.loading = false }) + }, + registered (credentials) { + this.registerDialog = false + this.credentials.username = credentials.username + this.credentials.password = credentials.password + this.$store.commit(authMut.SET_MESSAGE, 'Your account is being activated. Please wait.') } } } -- GitLab