import Vue from 'vue' import * as api from '@/api' import { cartesian, flatten, 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 { getStoreBuilder, BareActionContext } from 'vuex-typex' export interface SubscriptionsState { subscriptions: {[pk: string]: Subscription} currentAssignment?: Assignment loading: boolean } function initialState (): SubscriptionsState { return { subscriptions: {}, currentAssignment: undefined, loading: false } } const mb = getStoreBuilder<RootState>().module('Subscriptions', initialState()) 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 = ['initial', 'validate'] if (Authentication.isReviewer) { stages.push('conflict') } 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) { if (state.currentAssignment && state.currentAssignment.subscription) { return state.subscriptions[state.currentAssignment.subscription] } return undefined }) 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' } } }) type SubscriptionsByStage = {[p in Subscription.FeedbackStageEnum]?: {[k in Subscription.QueryTypeEnum]: Subscription[]}} // TODO Refactor this monstrosity const getSubscriptionsGroupedByTypeGetter = mb.read(function getSubscriptionsGroupedByType (state, getters) { const subscriptionsByType = () => { return { [Subscription.QueryTypeEnum.Random]: [], [Subscription.QueryTypeEnum.Student]: [], [Subscription.QueryTypeEnum.Exam]: [], [Subscription.QueryTypeEnum.SubmissionType]: [] } } let subscriptionsByStage: SubscriptionsByStage = Subscriptions.availableStages.reduce((acc: SubscriptionsByStage, curr: Subscription.FeedbackStageEnum) => { acc[curr] = subscriptionsByType() return acc }, {}) Object.values(state.subscriptions).forEach((subscription: Subscription) => { if (subscriptionsByStage && subscription.feedbackStage && subscription.queryType) { 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 }) 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_CURRENT_ASSIGNMENT (state: SubscriptionsState, assignment?: Assignment): void { state.currentAssignment = assignment } function RESET_STATE (state: SubscriptionsState): void { Object.assign(state, initialState()) subscribeToAll.reset() } 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 } async function changeToSubscription({state}: BareActionContext<SubscriptionsState, RootState>, subscriptionPk: string) { const currAssignment = state.currentAssignment if (currAssignment && currAssignment.subscription == subscriptionPk) { return } if (currAssignment) { await api.deleteAssignment({assignment: currAssignment}) } const newAssignment = await api.createAssignment({subscriptionPk}) Subscriptions.SET_CURRENT_ASSIGNMENT(newAssignment) } async function createNextAssignment() { const activeSubscription = Subscriptions.activeSubscription if (!activeSubscription) { throw new Error("There must be an active Subscription before calling createNextAssignment") } const newAssignment = await api.createAssignment({subscription: activeSubscription}) Subscriptions.SET_CURRENT_ASSIGNMENT(newAssignment) } async function cleanAssignmentsFromSubscriptions ({ state }: BareActionContext<SubscriptionsState, RootState>, excludeActive = true) { Object.values(state.subscriptions).forEach(subscription => { if (!excludeActive || !Subscriptions.activeSubscription || subscription.pk !== Subscriptions.activeSubscription.pk) { if (subscription.assignments) { subscription.assignments.forEach(assignment => { api.deleteAssignment({ assignment }) }) } } }) } async function skipAssignment ({ state }: BareActionContext<SubscriptionsState, RootState>) { if (!state.currentAssignment || !state.currentAssignment.subscription) { throw new Error("skipAssignment can only be called with active assignment") } const newAssignment = await api.createAssignment({subscriptionPk: state.currentAssignment.subscription}) await api.deleteAssignment({assignment: state.currentAssignment }) Subscriptions.SET_CURRENT_ASSIGNMENT(newAssignment) } async function deleteCurrentAssignment ({ state }: BareActionContext<SubscriptionsState, RootState>) { if (!state.currentAssignment) { throw new Error("No active assignment to delete") } await api.deleteAssignment({assignment: state.currentAssignment}) Subscriptions.SET_CURRENT_ASSIGNMENT(undefined) } 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 }) }) } 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() }, SET_SUBSCRIPTIONS: mb.commit(SET_SUBSCRIPTIONS), SET_SUBSCRIPTION: mb.commit(SET_SUBSCRIPTION), SET_CURRENT_ASSIGNMENT: mb.commit(SET_CURRENT_ASSIGNMENT), RESET_STATE: mb.commit(RESET_STATE), subscribeTo: mb.dispatch(subscribeTo), getSubscriptions: mb.dispatch(getSubscriptions), cleanAssignmentsFromSubscriptions: mb.dispatch(cleanAssignmentsFromSubscriptions), changeToSubscription: mb.dispatch(changeToSubscription), createNextAssignment: mb.dispatch(createNextAssignment), skipAssignment: mb.dispatch(skipAssignment), deleteCurrentAssignment: mb.dispatch(deleteCurrentAssignment), subscribeToType: mb.dispatch(subscribeToType), subscribeToAll: mb.dispatch(subscribeToAll, 'subscribeToAll') }