diff --git a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue index a778c34d5740f7850b27dc758c108262bcc0ca2d..7996bbdc78555986f3ba05afe41e54014f3cd8f9 100644 --- a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue +++ b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue @@ -65,7 +65,7 @@ import {mapState, mapGetters} from 'vuex' import {FeedbackSearchOptions} from '@/store/modules/feedback_list/feedback-search-options' import {mapStateToComputedGetterSetter} from '@/util/helpers' import {Authentication} from '@/store/modules/authentication' -import { actions } from '@/store/actions'; +import { actions } from '@/store/actions' export default { name: 'feedback-search-options', diff --git a/frontend/src/components/feedback_list/FeedbackTable.vue b/frontend/src/components/feedback_list/FeedbackTable.vue index 37a460ef18a0503a984dcfdefff8ca4b091b03e2..1925c5f9532a881c0462699837c321a14b7088de 100644 --- a/frontend/src/components/feedback_list/FeedbackTable.vue +++ b/frontend/src/components/feedback_list/FeedbackTable.vue @@ -42,7 +42,7 @@ import {mapState, mapGetters} from 'vuex' import {getObjectValueByPath} from '@/util/helpers' import FeedbackSearchOptions from '@/components/feedback_list/FeedbackSearchOptions' import { FeedbackSearchOptions as OptionsModule } from '@/store/modules/feedback_list/feedback-search-options' -import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table'; +import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table' export default { computed: { diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue index 49c9af1b22a15a7592bc8970e770fb6f43946da3..f759a584983b86f2a9842d9c2146c77573b08640 100644 --- a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue +++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue @@ -56,6 +56,7 @@ <script> import {subNotesMut, subNotesNamespace} from '@/store/modules/submission-notes' import {Authentication} from '@/store/modules/authentication' +import { Subscriptions } from '@/store/modules/subscriptions'; export default { name: 'annotated-submission-bottom-toolbar', @@ -137,7 +138,7 @@ export default { }, skipSubmission () { if (this.skippable) { - this.$store.dispatch('skipAssignment').catch(() => { + Subscriptions.skipAssignment().catch(() => { this.$notify({ title: 'Unable to skip submission', type: 'error' diff --git a/frontend/src/components/subscriptions/SubscriptionCreation.vue b/frontend/src/components/subscriptions/SubscriptionCreation.vue index e41d00abfa0c3e454596ed6cc27c4dd300e126fa..b7d1561ea0de34be32ac6b89de0cc9ebf1fef1f1 100644 --- a/frontend/src/components/subscriptions/SubscriptionCreation.vue +++ b/frontend/src/components/subscriptions/SubscriptionCreation.vue @@ -30,6 +30,7 @@ <script> import {Authentication} from '@/store/modules/authentication' +import { Subscriptions } from '@/store/modules/subscriptions'; const stages = [ { @@ -82,7 +83,7 @@ export default { methods: { subscribe () { this.loading = true - this.$store.dispatch('subscribeTo', { + Subscriptions.subscribeTo({ type: this.type, key: this.key.key, stage: this.stage.stage diff --git a/frontend/src/components/subscriptions/SubscriptionForList.vue b/frontend/src/components/subscriptions/SubscriptionForList.vue index 50619e3b72d5361553097684b3c742deef8d8b41..77c4bf121cf0bb7d7786222598979e48ef6bbe93 100644 --- a/frontend/src/components/subscriptions/SubscriptionForList.vue +++ b/frontend/src/components/subscriptions/SubscriptionForList.vue @@ -20,19 +20,20 @@ <script lang="ts"> import {Vue, Component, Prop} from 'vue-property-decorator' -import {Assignment} from '@/models' +import {Assignment, Subscription} from '@/models' +import { Subscriptions } from '@/store/modules/subscriptions'; @Component export default class SubscriptionForList extends Vue { @Prop({type: String, required: true}) pk!: string - @Prop({type: String, required: true}) feedbackStage!: string - @Prop({type: String, required: true}) queryType!: string + @Prop({type: String, required: true}) feedbackStage!: Subscription.FeedbackStageEnum + @Prop({type: String, required: true}) queryType!: Subscription.QueryTypeEnum @Prop({type: Number, required: true}) available!: number @Prop({type: Array, required: true}) assignments!: Assignment[] @Prop({type: String, default: ''}) queryKey!: string get name () { - return this.$store.getters.resolveSubscriptionKeyToName( + return Subscriptions.resolveSubscriptionKeyToName( {queryKey: this.queryKey, queryType: this.queryType}) } get active () { diff --git a/frontend/src/components/subscriptions/SubscriptionList.vue b/frontend/src/components/subscriptions/SubscriptionList.vue index c31e578caf276d06924d5792d2ee6c28b00ec41c..9db5ed5fde043e17f89ab99b2c1583f6f439e649 100644 --- a/frontend/src/components/subscriptions/SubscriptionList.vue +++ b/frontend/src/components/subscriptions/SubscriptionList.vue @@ -34,6 +34,7 @@ import {actions} from '@/store/actions' import SubscriptionCreation from '@/components/subscriptions/SubscriptionCreation' import SubscriptionForList from '@/components/subscriptions/SubscriptionForList' import SubscriptionsForStage from '@/components/subscriptions/SubscriptionsForStage' +import { Subscriptions } from '@/store/modules/subscriptions' export default { components: { @@ -54,33 +55,26 @@ export default { } }, computed: { - ...mapGetters({ - subscriptions: 'getSubscriptionsGroupedByType', - stages: 'availableStages', - stagesReadable: 'availableStagesReadable' - }), + subscriptions () { return Subscriptions.state.subscriptions }, + stages () { return Subscriptions.availableStages }, + stagesReadable () { return Subscriptions.availableStagesReadable }, showDetail () { return !this.sidebar || (this.sidebar && !UI.state.sideBarCollapsed) } }, methods: { - ...mapActions([ - 'getCurrentAssignment', - 'subscribeToAll', - 'cleanAssignmentsFromSubscriptions' - ]), async getSubscriptions () { this.updating = true - const subscriptions = await this.$store.dispatch('getSubscriptions') + const subscriptions = await Subscriptions.getSubscriptions() this.updating = false return subscriptions } }, created () { - const typesAndSubscriptions = [actions.updateSubmissionTypes(), this.getSubscriptions()] + const typesAndSubscriptions = [actions.updateSubmissionTypes(), Subscriptions.getSubscriptions()] Promise.all(typesAndSubscriptions).then(() => { - this.subscribeToAll() - this.cleanAssignmentsFromSubscriptions() + Subscriptions.subscribeToAll() + Subscriptions.cleanAssignmentsFromSubscriptions() }) } } diff --git a/frontend/src/components/subscriptions/SubscriptionsForStage.vue b/frontend/src/components/subscriptions/SubscriptionsForStage.vue index b6388b7dcf1d3c0a12ff44782d7e1929a4a76eb5..8c044a5240c4ba5aec11ab2c21c53ec9ac1d9784 100644 --- a/frontend/src/components/subscriptions/SubscriptionsForStage.vue +++ b/frontend/src/components/subscriptions/SubscriptionsForStage.vue @@ -14,6 +14,7 @@ <script> import SubscriptionForList from '@/components/subscriptions/SubscriptionForList' +import { Subscriptions } from '@/store/modules/subscriptions'; export default { components: { SubscriptionForList @@ -31,7 +32,7 @@ export default { }, computed: { subscriptions () { - return this.$store.getters.getSubscriptionsGroupedByType[this.stage] + return Subscriptions.getSubscriptionsGroupedByType[this.stage] } } } diff --git a/frontend/src/components/tutor_list/TutorList.vue b/frontend/src/components/tutor_list/TutorList.vue index 1c8f28c3b053c996c9414596dab0daf235d841ac..3b760a08899436eeeb2c81aeed16b15ae847fd24 100644 --- a/frontend/src/components/tutor_list/TutorList.vue +++ b/frontend/src/components/tutor_list/TutorList.vue @@ -31,7 +31,7 @@ <script> import {mapState, mapActions} from 'vuex' import {changeActiveForUser} from '@/api' -import { actions } from '@/store/actions'; +import { actions } from '@/store/actions' export default { name: 'tutor-list', diff --git a/frontend/src/models.ts b/frontend/src/models.ts index c5f59b166079eeb8359b896965b29dd69611fa88..ddf2d43228df82de5198c6e93f1345117d01fd33 100644 --- a/frontend/src/models.ts +++ b/frontend/src/models.ts @@ -635,19 +635,19 @@ export namespace Subscription { * @enum {string} */ export enum QueryTypeEnum { - Random = <any> 'random', - Student = <any> 'student', - Exam = <any> 'exam', - SubmissionType = <any> 'submission_type' + Random = 'random', + Student = 'student', + Exam = 'exam', + SubmissionType = 'submission_type' } /** * @export * @enum {string} */ export enum FeedbackStageEnum { - Creation = <any> 'feedback-creation', - Validation = <any> 'feedback-validation', - ConflictResolution = <any> 'feedback-conflict-resolution' + Creation = 'feedback-creation', + Validation = 'feedback-validation', + ConflictResolution = 'feedback-conflict-resolution' } } @@ -775,9 +775,9 @@ export namespace UserAccount { * @enum {string} */ export enum RoleEnum { - Student = <any> 'Student', - Tutor = <any> 'Tutor', - Reviewer = <any> 'Reviewer' + Student = 'Student', + Tutor = 'Tutor', + Reviewer = 'Reviewer' } } diff --git a/frontend/src/pages/SubscriptionWorkPage.vue b/frontend/src/pages/SubscriptionWorkPage.vue index 8797fdf306c82fe177722b131a90a15f8d853686..ab3a593ac16dec405309ff6280aeaf5be2438177 100644 --- a/frontend/src/pages/SubscriptionWorkPage.vue +++ b/frontend/src/pages/SubscriptionWorkPage.vue @@ -36,12 +36,13 @@ import SubmissionCorrection from '@/components/submission_notes/SubmissionCorrec import SubmissionType from '@/components/SubmissionType' import store from '@/store/store' import SubmissionTests from '@/components/SubmissionTests' -import { subscriptionMuts } from '@/store/modules/subscriptions' +import { Subscriptions } from '@/store/modules/subscriptions' import RouteChangeConfirmation from '@/components/submission_notes/RouteChangeConfirmation' +import { getters } from '@/store/getters' function onRouteEnterOrUpdate (to, from, next) { if (to.name === 'subscription') { - store.dispatch('changeActiveSubscription', to.params['pk']).then(() => { + Subscriptions.changeActiveSubscription(to.params['pk']).then(() => { next() }) } @@ -63,16 +64,16 @@ export default { }, computed: { subscription () { - return this.$store.state.subscriptions.subscriptions[this.$route.params['pk']] + return Subscriptions.state.subscriptions[this.$route.params['pk']] }, currentAssignment () { - return this.$store.state.subscriptions.assignmentQueue[0] + return Subscriptions.state.assignmentQueue[0] }, submission () { return this.currentAssignment.submission }, submissionType () { - return this.$store.state.submissionTypes[this.submission['type']] + return getters.state.submissionTypes[this.submission.type] } }, beforeRouteEnter (to, from, next) { @@ -92,11 +93,11 @@ export default { }, methods: { startWorkOnNextAssignment () { - this.$store.dispatch('getAssignmentsForActiveSubscription', 1).then(([promise]) => { + Subscriptions.getAssignmentsForActiveSubscription(1).then(([promise]) => { promise.then(assignment => { - this.$store.commit(subscriptionMuts.ADD_ASSIGNMENT_TO_QUEUE, assignment) + Subscriptions.ADD_ASSIGNMENT_TO_QUEUE(assignment) }).finally(() => { - this.$store.commit(subscriptionMuts.POP_ASSIGNMENT_FROM_QUEUE) + Subscriptions.POP_ASSIGNMENT_FROM_QUEUE() }) }) } @@ -106,8 +107,8 @@ export default { this.$vuetify.goTo(0, {duration: 200, easing: 'easeInOutCubic'}) if (val === undefined) { this.$router.replace('ended') - this.$store.dispatch('removeActiveSubscription') - this.$store.dispatch('getSubscriptions') + Subscriptions.removeActiveSubscription() + Subscriptions.getSubscriptions() } } } diff --git a/frontend/src/pages/base/FeedbackHistoryPage.vue b/frontend/src/pages/base/FeedbackHistoryPage.vue index 9e890f0bf32867331c6e5d30c626f9dd16c7ccd0..4e0d8b61f210fe0ab4d39437033293ee77a8bd87 100644 --- a/frontend/src/pages/base/FeedbackHistoryPage.vue +++ b/frontend/src/pages/base/FeedbackHistoryPage.vue @@ -12,7 +12,7 @@ </template> <script> -import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table'; +import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table' export default { name: 'feedback-history-page', created () { diff --git a/frontend/src/pages/reviewer/TutorOverviewPage.vue b/frontend/src/pages/reviewer/TutorOverviewPage.vue index e8b7a44c9060ffea4e50037ecdb6a13a2bc3fd8f..e88bc6a0f1509ea72f4474583ffdce76ba6aebd0 100644 --- a/frontend/src/pages/reviewer/TutorOverviewPage.vue +++ b/frontend/src/pages/reviewer/TutorOverviewPage.vue @@ -5,7 +5,7 @@ <script> import store from '@/store/store' import TutorList from '@/components/tutor_list/TutorList' -import { actions } from '@/store/actions'; +import { actions } from '@/store/actions' export default { components: {TutorList}, diff --git a/frontend/src/store/actions.ts b/frontend/src/store/actions.ts index 14a5889c9ef6c164f64daff137c9b28b4fa84126..31250dade5fa3a4c19a212433bf4feaf5320fa15 100644 --- a/frontend/src/store/actions.ts +++ b/frontend/src/store/actions.ts @@ -7,6 +7,8 @@ import { subNotesMut } from "@/store/modules/submission-notes"; import * as api from "@/api"; import router from "@/router/index"; import { RootState } from "@/store/store"; +import { FeedbackTable } from '@/store/modules/feedback_list/feedback-table'; +import { Subscriptions } from '@/store/modules/subscriptions'; async function getExamTypes(context: BareActionContext<RootState, RootState>) { const examTypes = await api.fetchExamTypes({}); @@ -83,6 +85,9 @@ function logout( api.changeActiveForUser(Authentication.state.user.pk, false); } mut.RESET_STATE(); + FeedbackTable.RESET_STATE() + Authentication.RESET_STATE() + Subscriptions.RESET_STATE() commit("submissionNotes/" + subNotesMut.RESET_STATE); Authentication.SET_MESSAGE(message); router.push({ name: "login" }); diff --git a/frontend/src/store/modules/feedback_list/feedback-search-options.ts b/frontend/src/store/modules/feedback_list/feedback-search-options.ts index d73c42dddaa1f9679ecfb9cd8a6685905defaa7d..6a69437c8c5e137d99096d8362e827cf1a76a9bc 100644 --- a/frontend/src/store/modules/feedback_list/feedback-search-options.ts +++ b/frontend/src/store/modules/feedback_list/feedback-search-options.ts @@ -1,6 +1,6 @@ import {Module} from 'vuex' import {RootState} from '@/store/store' -import { getStoreBuilder } from 'vuex-typex'; +import { getStoreBuilder } from 'vuex-typex' export const namespace = 'feedbackSearchOptions' diff --git a/frontend/src/store/modules/feedback_list/feedback-table.ts b/frontend/src/store/modules/feedback_list/feedback-table.ts index 1ea3285a878573b95cf88d59c0bd3346f64fd595..2901b4a2840615ced613ddd014a9e1a56c719f3a 100644 --- a/frontend/src/store/modules/feedback_list/feedback-table.ts +++ b/frontend/src/store/modules/feedback_list/feedback-table.ts @@ -4,12 +4,12 @@ import {Assignment, Feedback, Subscription, SubmissionType} from '@/models' import {Module} from 'vuex' import {RootState} from '@/store/store' import {getters} from '@/store/getters' -import { getStoreBuilder, BareActionContext } from 'vuex-typex'; -import { Authentication } from '@/store/modules/authentication'; +import { getStoreBuilder, BareActionContext } from 'vuex-typex' +import { Authentication } from '@/store/modules/authentication' export interface FeedbackHistoryItem extends Feedback { history?: { - [key in Subscription.FeedbackStageEnum]: { + [key in Subscription.FeedbackStageEnum]?: { owner: string isDone: boolean } @@ -49,7 +49,7 @@ function ADD_ASSIGNMENTS_INFO (state: FeedbackTableState, assignments: Array<Ass } } function SET_FEEDBACK_OF_SUBMISSION_TYPE (state: FeedbackTableState, {feedback, type}: - {feedback: Feedback, type: SubmissionType}) { +{feedback: Feedback, type: SubmissionType}) { if (!feedback.ofSubmission) { throw new Error('Feedback must have ofSubmission present') } diff --git a/frontend/src/store/modules/subscriptions.ts b/frontend/src/store/modules/subscriptions.ts index 12d678d652dacd0fc1d695ffa11a52a56e0d5d4a..3fbfe78a9467b9baba2a54e4a615f8a59c8d4b25 100644 --- a/frontend/src/store/modules/subscriptions.ts +++ b/frontend/src/store/modules/subscriptions.ts @@ -4,20 +4,10 @@ import {cartesian, flatten, handleError, once} from '@/util/helpers' import {Assignment, Subscription} from '@/models' import {ActionContext, Module} from 'vuex' import {RootState} from '@/store/store' -import { Authentication } from '@/store/modules/authentication'; +import { Authentication } from '@/store/modules/authentication' +import { getStoreBuilder, BareActionContext } from 'vuex-typex' -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' -}) - -interface SubscriptionsState { +export interface SubscriptionsState { subscriptions: {[pk: string]: Subscription} assignmentQueue: Array<Assignment> activeSubscriptionPk: string @@ -33,264 +23,279 @@ function initialState (): SubscriptionsState { } } +const mb = getStoreBuilder<RootState>().module('Subscriptions', initialState()) + const MAX_NUMBER_OF_ASSIGNMENTS = 2 -const subscriptionsModule: Module<SubscriptionsState, RootState> = { - state: initialState(), - getters: { - availableTypes (state, getters) { - let types = ['random', 'submission_type'] - if (Authentication.isReviewer) { - types.push('exam') - } - return types - }, - availableStages (state, getters) { - let stages = ['feedback-creation', 'feedback-validation'] - if (Authentication.isReviewer) { - stages.push('feedback-conflict-resolution') - } - return stages - }, - availableStagesReadable (state, getters) { - let stages = ['create', 'validate'] - if (Authentication.isReviewer) { - stages.push('resolve') - } - return stages - }, - availableSubmissionTypeQueryKeys (state, getters, rootState) { - return Object.values(rootState.submissionTypes).map((subType: any) => subType.pk) - }, - availableExamTypeQueryKeys (state, getters, rootState) { - return Object.values(rootState.examTypes).map((examType: any) => examType.pk) - }, - activeSubscription (state) { - return state.subscriptions[state.activeSubscriptionPk] - }, - resolveSubscriptionKeyToName: (state, getters, rootState) => (subscription: Subscription) => { - switch (subscription.queryType) { - case Subscription.QueryTypeEnum.Random: - return 'Active' - case Subscription.QueryTypeEnum.Exam: - return subscription.queryKey - ? rootState.examTypes[subscription.queryKey].moduleReference : 'Exam' - case Subscription.QueryTypeEnum.SubmissionType: - return subscription.queryKey - ? rootState.submissionTypes[subscription.queryKey].name : 'Submission Type' - case Subscription.QueryTypeEnum.Student: - return subscription.queryKey - ? rootState.students[subscription.queryKey].name : 'Student' - } - }, - // TODO Refactor this monstrosity - getSubscriptionsGroupedByType (state, getters) { - const subscriptionsByType = () => { - return { - 'random': [], - 'student': [], - 'exam': [], - 'submission_type': [] - } - } - let subscriptionsByStage = getters.availableStages.reduce((acc: {[p: string]: {[k: string]: Subscription[]}}, - curr: string) => { - acc[curr] = subscriptionsByType() - return acc - }, {}) - Object.values(state.subscriptions).forEach((subscription: any) => { - subscriptionsByStage[subscription.feedbackStage][subscription.queryType].push(subscription) - }) - // sort the resulting arrays in subscriptions lexicographically by their query_keys - const sortSubscriptions = (subscriptionsByType: {[k: string]: Subscription[]}) => Object.values(subscriptionsByType) - .forEach((arr: object[]) => { - if (arr.length > 1 && arr[0].hasOwnProperty('queryKey')) { - 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: any) => { - sortSubscriptions(subscriptionsByType) - }) - return subscriptionsByStage +const stateGetter = mb.state() + +const availableTypesGetter = mb.read(function availableTypes (state, getters) { + let types = [Subscription.QueryTypeEnum.Random, Subscription.QueryTypeEnum.SubmissionType] + if (Authentication.isReviewer) { + types.push(Subscription.QueryTypeEnum.Exam) + } + return types +}) +const availableStagesGetter = mb.read(function availableStages (state, getters) { + let stages = [Subscription.FeedbackStageEnum.Creation, Subscription.FeedbackStageEnum.Validation] + if (Authentication.isReviewer) { + stages.push(Subscription.FeedbackStageEnum.ConflictResolution) + } + return stages +}) +const availableStagesReadableGetter = mb.read(function availableStagesReadable (state, getters) { + let stages = ['create', 'validate'] + if (Authentication.isReviewer) { + stages.push('resolve') + } + return stages +}) +const availableSubmissionTypeQueryKeysGetter = mb.read(function availableSubmissionTypeQueryKeys (state, getters, rootState) { + return Object.values(rootState.submissionTypes).map((subType: any) => subType.pk) +}) +const availableExamTypeQueryKeysGetter = mb.read(function availableExamTypeQueryKeys (state, getters, rootState) { + return Object.values(rootState.examTypes).map((examType: any) => examType.pk) +}) +const activeSubscriptionGetter = mb.read(function activeSubscription (state) { + return state.subscriptions[state.activeSubscriptionPk] +}) +const resolveSubscriptionKeyToNameGetter = mb.read(function resolveSubscriptionKeyToName (state, getters, rootState) { + return (subscription: {queryType: Subscription.QueryTypeEnum, queryKey: string}) => { + switch (subscription.queryType) { + case Subscription.QueryTypeEnum.Random: + return 'Active' + case Subscription.QueryTypeEnum.Exam: + return subscription.queryKey + ? rootState.examTypes[subscription.queryKey].moduleReference : 'Exam' + case Subscription.QueryTypeEnum.SubmissionType: + return subscription.queryKey + ? rootState.submissionTypes[subscription.queryKey].name : 'Submission Type' + case Subscription.QueryTypeEnum.Student: + return subscription.queryKey + ? rootState.students[subscription.queryKey].name : 'Student' } - }, - mutations: { - [subscriptionMuts.SET_SUBSCRIPTIONS] (state, subscriptions: Array<Subscription>): void { - state.subscriptions = subscriptions.reduce((acc: {[pk: string]: Subscription}, curr) => { - acc[curr.pk] = curr - return acc - }, {}) - }, - [subscriptionMuts.SET_SUBSCRIPTION] (state, subscription: Subscription): void { - Vue.set(state.subscriptions, subscription.pk, subscription) - }, - [subscriptionMuts.SET_ACTIVE_SUBSCRIPTION_PK] (state, subscriptionPk: string): void { - state.activeSubscriptionPk = subscriptionPk - }, - [subscriptionMuts.SET_ASSIGNMENT_QUEUE] (state, queue: Array<Assignment>): void { - state.assignmentQueue = queue - }, - [subscriptionMuts.ADD_ASSIGNMENT_TO_QUEUE] (state, assignment: Assignment): void { - state.assignmentQueue.push(assignment) - }, - [subscriptionMuts.POP_ASSIGNMENT_FROM_QUEUE] (state): void { - state.assignmentQueue.shift() - }, - [subscriptionMuts.RESET_STATE] (state): void { - Object.assign(state, initialState()) + } +}) +// TODO Refactor this monstrosity +const getSubscriptionsGroupedByTypeGetter = mb.read(function getSubscriptionsGroupedByType (state, getters) { + const subscriptionsByType = () => { + return { + 'random': [], + 'student': [], + 'exam': [], + 'submission_type': [] } - }, - actions: { - subscribeTo: async function ( - {commit, dispatch, getters}, - {type, key, stage}: - {type: Subscription.QueryTypeEnum, key?: string, stage: Subscription.FeedbackStageEnum}) { - try { - // don't subscribe to type, key, stage combinations if they're already present - let subscription = getters.getSubscriptionsGroupedByType[stage][type].find((elem: Subscription) => { - if (type === Subscription.QueryTypeEnum.Random) { - return true + } + let subscriptionsByStage = getters.availableStages.reduce((acc: {[p: string]: {[k: string]: Subscription[]}}, + curr: string) => { + acc[curr] = subscriptionsByType() + return acc + }, {}) + Object.values(state.subscriptions).forEach((subscription: any) => { + subscriptionsByStage[subscription.feedbackStage][subscription.queryType].push(subscription) + }) + // sort the resulting arrays in subscriptions lexicographically by their query_keys + const sortSubscriptions = (subscriptionsByType: {[k: string]: Subscription[]}) => Object.values(subscriptionsByType) + .forEach((arr: object[]) => { + if (arr.length > 1 && arr[0].hasOwnProperty('queryKey')) { + 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 elem.queryKey === key }) - subscription = subscription || await api.subscribeTo(type, key, stage) - commit(subscriptionMuts.SET_SUBSCRIPTION, subscription) - return subscription - } catch (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) { - 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') + }) + Object.values(subscriptionsByStage).forEach((subscriptionsByType: any) => { + sortSubscriptions(subscriptionsByType) + }) + return subscriptionsByStage +}) + +function SET_SUBSCRIPTIONS (state: SubscriptionsState, subscriptions: Array<Subscription>): void { + state.subscriptions = subscriptions.reduce((acc: {[pk: string]: Subscription}, curr) => { + acc[curr.pk] = curr + return acc + }, {}) +} +function SET_SUBSCRIPTION (state: SubscriptionsState, subscription: Subscription): void { + Vue.set(state.subscriptions, subscription.pk, subscription) +} +function SET_ACTIVE_SUBSCRIPTION_PK (state: SubscriptionsState, subscriptionPk: string): void { + state.activeSubscriptionPk = subscriptionPk +} +function SET_ASSIGNMENT_QUEUE (state: SubscriptionsState, queue: Array<Assignment>): void { + state.assignmentQueue = queue +} +function ADD_ASSIGNMENT_TO_QUEUE (state: SubscriptionsState, assignment: Assignment): void { + state.assignmentQueue.push(assignment) +} +function POP_ASSIGNMENT_FROM_QUEUE (state: SubscriptionsState): void { + state.assignmentQueue.shift() +} +function RESET_STATE (state: SubscriptionsState): void { + Object.assign(state, initialState()) +} + +async function subscribeTo ( + context: BareActionContext<SubscriptionsState, RootState>, + {type, key, stage}: + {type: Subscription.QueryTypeEnum, key?: string, stage: Subscription.FeedbackStageEnum}): Promise<Subscription> { + // don't subscribe to type, key, stage combinations if they're already present + let subscription = Subscriptions.getSubscriptionsGroupedByType[stage][type].find((elem: Subscription) => { + if (type === Subscription.QueryTypeEnum.Random) { + return true + } + return elem.queryKey === key + }) + subscription = subscription || await api.subscribeTo(type, key, stage) + Subscriptions.SET_SUBSCRIPTION(subscription) + return subscription +} +async function getSubscriptions () { + const subscriptions = await api.fetchSubscriptions() + Subscriptions.SET_SUBSCRIPTIONS(subscriptions) + return 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 + */ +async function getAssignmentsForActiveSubscription +(context: BareActionContext<SubscriptionsState, RootState>, numOfAssignments: number): + Promise<Promise<Assignment>[]> { + numOfAssignments = numOfAssignments || MAX_NUMBER_OF_ASSIGNMENTS - context.state.assignmentQueue.length + let assignmentsPromises = [] + for (let i = 0; i < numOfAssignments; i++) { + assignmentsPromises.push(api.createAssignment({subscription: Subscriptions.activeSubscription})) + } + return assignmentsPromises +} +async function deleteAssignment +(context: BareActionContext<SubscriptionsState, RootState>, assignment: Assignment) { + return api.deleteAssignment({assignment}) +} +async function cleanAssignmentsFromSubscriptions +({state}: BareActionContext<SubscriptionsState, RootState>, excludeActive = true) { + Object.values(state.subscriptions).forEach(subscription => { + if (!excludeActive || subscription.pk !== state.activeSubscriptionPk) { + if (subscription.assignments) { + subscription.assignments.forEach(assignment => { + api.deleteAssignment({assignment}) + }) } - }, - async cleanAssignmentsFromSubscriptions ({commit, state, dispatch}, excludeActive = true) { - Object.values(state.subscriptions).forEach(subscription => { - if (!excludeActive || subscription.pk !== state.activeSubscriptionPk) { - if (subscription.assignments) { - subscription.assignments.forEach(assignment => { - api.deleteAssignment({assignment}) - }) - } - } + } + }) +} +async function skipAssignment ({state}: BareActionContext<SubscriptionsState, RootState>) { + Subscriptions.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 Subscriptions.getAssignmentsForActiveSubscription(1) + }).then(([promise]) => { + promise.then((assignment: Assignment) => { + Subscriptions.ADD_ASSIGNMENT_TO_QUEUE(assignment) + Subscriptions.POP_ASSIGNMENT_FROM_QUEUE() }) - }, - 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: 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}) { + }) +} +async function deleteActiveAssignments ({state}: BareActionContext<SubscriptionsState, RootState>) { + Promise.all(state.assignmentQueue.map(assignment => { + Subscriptions.deleteAssignment(assignment) + })) +} +async function changeActiveSubscription ({state}: BareActionContext<SubscriptionsState, RootState>, subscriptionPk = '') { + if (subscriptionPk !== state.activeSubscriptionPk) { + await Subscriptions.deleteActiveAssignments() + Subscriptions.SET_ACTIVE_SUBSCRIPTION_PK(subscriptionPk) + let assignmentsPromises = await Subscriptions.getAssignmentsForActiveSubscription(MAX_NUMBER_OF_ASSIGNMENTS) + let createdAssignments = [] + // TODO refactor this since it's very bad to await promises in for loops + + for (let promise of assignmentsPromises) { 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 = [] - // TODO refactor this since it's very bad to await promises in for loops - 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, '') - }, - // TODO use enums here - async subscribeToType ({commit, state, dispatch, getters}, type: Subscription.QueryTypeEnum) { - switch (type) { - case Subscription.QueryTypeEnum.Random: - return getters.availableStages.map((stage: string) => { - dispatch('subscribeTo', {type, stage}) - }) - case Subscription.QueryTypeEnum.Exam: - if (Authentication.isReviewer) { - const stageKeyCartesian = cartesian( - getters.availableStages, getters.availableExamTypeQueryKeys) - // @ts-ignore - return stageKeyCartesian.map(([stage, key]: [string, string]) => { - dispatch('subscribeTo', {stage, type, key}) - }) - } - return [] - case Subscription.QueryTypeEnum.SubmissionType: - const stageKeyCartesian = cartesian( - getters.availableStages, getters.availableSubmissionTypeQueryKeys) - // @ts-ignore - return stageKeyCartesian.map(([stage, key]: [string, string]) => { - dispatch('subscribeTo', {stage, type, key}) - }) + createdAssignments.push(await promise) + } catch (_) {} + } + Subscriptions.SET_ASSIGNMENT_QUEUE(createdAssignments) + } +} +async function removeActiveSubscription () { + await Subscriptions.deleteActiveAssignments() + Subscriptions.SET_ASSIGNMENT_QUEUE([]) + Subscriptions.SET_ACTIVE_SUBSCRIPTION_PK('') +} +// TODO use enums here +async function subscribeToType +(context: BareActionContext<SubscriptionsState, RootState>, type: Subscription.QueryTypeEnum) { + switch (type) { + case Subscription.QueryTypeEnum.Random: + Subscriptions.availableStages.map((stage: Subscription.FeedbackStageEnum) => { + Subscriptions.subscribeTo({type, stage}) + }) + break + case Subscription.QueryTypeEnum.Exam: + if (Authentication.isReviewer) { + const stageKeyCartesian = cartesian( + Subscriptions.availableStages, Subscriptions.availableExamTypeQueryKeys) + // @ts-ignore + stageKeyCartesian.map(([stage, key]: [Subscription.FeedbackStageEnum, string]) => { + Subscriptions.subscribeTo({stage, type, key}) + }) } - }, - subscribeToAll: once(async ({commit, state, dispatch, getters}: - ActionContext<SubscriptionsState, RootState>) => { - return Promise.all(flatten(getters.availableTypes.map((type: string) => { - return dispatch('subscribeToType', type) - }))) - }) + break + case Subscription.QueryTypeEnum.SubmissionType: + const stageKeyCartesian = cartesian( + Subscriptions.availableStages, Subscriptions.availableSubmissionTypeQueryKeys) + // @ts-ignore + stageKeyCartesian.map(([stage, key]: [Subscription.FeedbackStageEnum, string]) => { + Subscriptions.subscribeTo({stage, type, key}) + }) + break } } +const subscribeToAll = once(async () => { + return Promise.all(flatten(Subscriptions.availableTypes.map((type) => { + return Subscriptions.subscribeToType(type) + }))) +}) + +export const Subscriptions = { + get state () { return stateGetter() }, + get availableTypes () { return availableTypesGetter() }, + get availableStages () { return availableStagesGetter() }, + get availableStagesReadable () { return availableStagesReadableGetter() }, + get availableSubmissionTypeQueryKeys () { return availableSubmissionTypeQueryKeysGetter() }, + get availableExamTypeQueryKeys () { return availableExamTypeQueryKeysGetter() }, + get activeSubscription () { return activeSubscriptionGetter() }, + get resolveSubscriptionKeyToName () { return resolveSubscriptionKeyToNameGetter() }, + get getSubscriptionsGroupedByType () { return getSubscriptionsGroupedByTypeGetter() }, -export default subscriptionsModule + SET_SUBSCRIPTIONS: mb.commit(SET_SUBSCRIPTIONS), + SET_SUBSCRIPTION: mb.commit(SET_SUBSCRIPTION), + SET_ACTIVE_SUBSCRIPTION_PK: mb.commit(SET_ACTIVE_SUBSCRIPTION_PK), + SET_ASSIGNMENT_QUEUE: mb.commit(SET_ASSIGNMENT_QUEUE), + ADD_ASSIGNMENT_TO_QUEUE: mb.commit(ADD_ASSIGNMENT_TO_QUEUE), + POP_ASSIGNMENT_FROM_QUEUE: mb.commit(POP_ASSIGNMENT_FROM_QUEUE), + RESET_STATE: mb.commit(RESET_STATE), + + subscribeTo: mb.dispatch(subscribeTo), + getSubscriptions: mb.dispatch(getSubscriptions), + getAssignmentsForActiveSubscription: mb.dispatch(getAssignmentsForActiveSubscription), + deleteAssignment: mb.dispatch(deleteAssignment), + cleanAssignmentsFromSubscriptions: mb.dispatch(cleanAssignmentsFromSubscriptions), + skipAssignment: mb.dispatch(skipAssignment), + deleteActiveAssignments: mb.dispatch(deleteActiveAssignments), + changeActiveSubscription: mb.dispatch(changeActiveSubscription), + removeActiveSubscription: mb.dispatch(removeActiveSubscription), + subscribeToType: mb.dispatch(subscribeToType), + subscribeToAll: mb.dispatch(subscribeToAll, 'subscribeToAll') +} diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index 761dafdeb3c45454af3c4c4ae17b011e5f5477a9..c208af27350ae75c80c442856a5a43fb902645bc 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -5,12 +5,12 @@ import {getStoreBuilder} from 'vuex-typex' import studentPage from './modules/student-page' import submissionNotes from './modules/submission-notes' -import subscriptions from './modules/subscriptions' import './modules/ui' import './modules/authentication' import './modules/feedback_list/feedback-search-options' import './modules/feedback_list/feedback-table' +import './modules/subscriptions' import './mutations' import './actions' @@ -20,6 +20,7 @@ import './getters' import {UIState} from './modules/ui' import {AuthState} from './modules/authentication' import {FeedbackSearchOptionsState, FeedbackSearchOptions} from './modules/feedback_list/feedback-search-options' +import {Subscriptions, SubscriptionsState} from './modules/subscriptions' import {FeedbackTableState, FeedbackTable} from './modules/feedback_list/feedback-table' import {lastInteraction} from '@/store/plugins/lastInteractionPlugin' @@ -49,7 +50,8 @@ export interface RootState extends RootInitialState{ UI: UIState, Authentication: AuthState, FeedbackSearchOptions: FeedbackSearchOptionsState, - FeedbackTable: FeedbackTableState + FeedbackTable: FeedbackTableState, + Subscriptions: SubscriptionsState } export function initialState (): RootInitialState { @@ -77,8 +79,7 @@ const store = getStoreBuilder<RootState>().vuexStore({ strict: process.env.NODE_ENV === 'development', modules: { studentPage, - submissionNotes, - subscriptions + submissionNotes }, plugins: [ createPersistedState({ @@ -87,7 +88,7 @@ const store = getStoreBuilder<RootState>().vuexStore({ // 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', 'FeedbackSearchOptions', 'subscriptions', + ['UI', 'studentPage', 'submissionNotes', 'FeedbackSearchOptions', 'Subscriptions', 'Authentication.user', 'Authentication.jwtTimeDelta', 'Authentication.tokenCreationTime']) }),