diff --git a/core/migrations/0010_auto_20180805_1139.py b/core/migrations/0010_auto_20180805_1139.py new file mode 100644 index 0000000000000000000000000000000000000000..622d7e5a33788fcdd9993fc4149c0f164e553500 --- /dev/null +++ b/core/migrations/0010_auto_20180805_1139.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1 on 2018-08-05 11:39 + +import core.models +import django.contrib.auth.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_auto_20180320_2335'), + ] + + operations = [ + migrations.AlterModelManagers( + name='useraccount', + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ('tutors', core.models.TutorManager()), + ], + ), + ] diff --git a/frontend/@types/v-clipboard/index.d.ts b/frontend/@types/v-clipboard/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..f2dd4459b9daac2e8b5cffd0b18fd0fca3741346 --- /dev/null +++ b/frontend/@types/v-clipboard/index.d.ts @@ -0,0 +1 @@ +declare module 'v-clipboard'; \ No newline at end of file diff --git a/frontend/src/api.js b/frontend/src/api.ts similarity index 72% rename from frontend/src/api.js rename to frontend/src/api.ts index d5965da6100864e0ef0960842eafa1cbb0e0c8e0..670854cab4becc9b07050862cbe91601ceb228b8 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.ts @@ -1,6 +1,7 @@ import axios from 'axios' +import {Credentials} from '@/store/modules/authentication' -function addFieldsToUrl ({url, fields = []}) { +function addFieldsToUrl ({url, fields = []}: {url: string, fields?: string[]}) { return fields.length > 0 ? url + '?fields=pk,' + fields : url } @@ -22,27 +23,27 @@ let ax = axios.create({ } } -export async function registerTutor (credentials) { +export async function registerTutor (credentials: Credentials) { return ax.post('/api/tutor/register/', credentials) } -export async function fetchJWT (credentials) { - const token = (await ax.post('/api/get-token/', credentials)).data.token +export async function fetchJWT (credentials: Credentials): Promise<string> { + const token: string = (await ax.post('/api/get-token/', credentials)).data.token ax.defaults.headers['Authorization'] = `JWT ${token}` return token } -export async function refreshJWT (token) { - const newToken = (await ax.post('/api/refresh-token/', {token})).data.token +export async function refreshJWT (token: string): Promise<string> { + const newToken: string = (await ax.post('/api/refresh-token/', {token})).data.token ax.defaults.headers['Authorization'] = `JWT ${newToken}` return newToken } -export async function fetchJWTTimeDelta () { +export async function fetchJWTTimeDelta (): Promise<number> { return (await ax.get('/api/jwt-time-delta/')).data.timeDelta } -export async function fetchUserRole () { +export async function fetchUserRole (): Promise<string> { return (await ax.get('/api/user-role/')).data.role } @@ -54,11 +55,11 @@ export async function fetchStudentSubmissions () { return (await ax.get('/api/student-submissions/')).data } -export async function fetchSubmissionFeedbackTests ({pk}) { +export async function fetchSubmissionFeedbackTests ({pk}: {pk: string}) { return (await ax.get(`/api/submission/${pk}/`)).data } -export async function fetchAllStudents (fields = []) { +export async function fetchAllStudents (fields: string[] = []) { const url = addFieldsToUrl({ url: '/api/student/', fields @@ -66,7 +67,8 @@ export async function fetchAllStudents (fields = []) { return (await ax.get(url)).data } -export async function fetchStudent ({pk, fields = []}) { +export async function fetchStudent ({pk, fields = []}: +{pk: string, fields?: string[]}) { const url = addFieldsToUrl({ url: `/api/student/${pk}/`, fields @@ -74,7 +76,7 @@ export async function fetchStudent ({pk, fields = []}) { return (await ax.get(url)).data } -export async function fetchAllTutors (fields = []) { +export async function fetchAllTutors (fields: string[] = []) { const url = addFieldsToUrl({ url: '/api/tutor/', fields @@ -86,16 +88,16 @@ export async function fetchSubscriptions () { return (await ax.get('/api/subscription/')).data } -export async function deactivateSubscription ({pk}) { +export async function deactivateSubscription ({pk}: {pk: string}) { const url = `/api/subscription/${pk}/` return (await ax.delete(url)).data } -export async function fetchSubscription (subscriptionPk) { +export async function fetchSubscription (subscriptionPk: string) { return (await ax.get(`/api/subscription/${subscriptionPk}/`)).data } -export async function fetchAllFeedback (fields = []) { +export async function fetchAllFeedback (fields: string[] = []) { const url = addFieldsToUrl({ url: '/api/feedback/', fields @@ -108,7 +110,8 @@ export async function fetchFeedback ({ofSubmission}) { return (await ax.get(url)).data } -export async function fetchExamType ({examPk, fields = []}) { +export async function fetchExamType ({examPk, fields = []}: +{examPk?: string, fields?: string[]}) { const url = addFieldsToUrl({ url: `/api/examtype/${examPk !== undefined ? examPk + '/' : ''}`, fields}) @@ -123,8 +126,16 @@ export async function fetchStatistics (opt = {fields: []}) { return (await ax.get(url)).data } -export async function subscribeTo (type, key, stage) { - let data = { +interface SubscriptionPayload { + /* eslint-disable */ + query_type: string, + query_key?: string, + feedback_stage?: string + /* eslint-enable */ +} + +export async function subscribeTo (type: string, key: string, stage: string) { + let data: SubscriptionPayload = { query_type: type } @@ -189,7 +200,7 @@ export async function deactivateAllStudentAccess () { return ax.post('/api/student/deactivate/') } -export async function changePassword (userPk, data) { +export async function changePassword (userPk: string, data) { return ax.patch(`/api/user/${userPk}/change_password/`, data) } @@ -197,7 +208,7 @@ export async function getOwnUser () { return (await ax.get('/api/user/me/')).data } -export async function changeActiveForUser (userPk, active) { +export async function changeActiveForUser (userPk: string, active: boolean) { return (await ax.patch(`/api/user/${userPk}/change_active/`, {'is_active': active})).data } diff --git a/frontend/src/components/AutoLogout.vue b/frontend/src/components/AutoLogout.vue index 76e695523d527f4eab20a1ba765072f1e164051e..3c29cf12caafb3c05f6213771a7f1543fccf08c5 100644 --- a/frontend/src/components/AutoLogout.vue +++ b/frontend/src/components/AutoLogout.vue @@ -1,7 +1,7 @@ <template> <v-dialog persistent - width="fit-content" + max-width="30%" v-model="logoutDialog" > <v-card> diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue index c2974f03186e933cb08a8fa625a8a9ecf80607f3..87a1485174e18ffbc677a134398a15598d774210 100644 --- a/frontend/src/components/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -86,8 +86,9 @@ <script> import { mapGetters, mapState } from 'vuex' import {uiMut} from '@/store/modules/ui' -import { createComputedGetterSetter } from '@/util/helpers' +import { mapStateToComputedGetterSetter } from '@/util/helpers' import UserOptions from '@/components/UserOptions' + export default { name: 'base-layout', components: {UserOptions}, @@ -99,22 +100,24 @@ export default { username: state => state.authentication.user.username, userRole: state => state.authentication.user.role }), - darkMode: createComputedGetterSetter({ - path: 'ui.darkMode', - mutation: uiMut.SET_DARK_MODE - }), - darkModeUnlocked: createComputedGetterSetter({ - path: 'ui.darkModeUnlocked', - mutation: uiMut.SET_DARK_MODE_UNLOCKED + ...mapStateToComputedGetterSetter({ + pathPrefix: 'ui', + items: [ + { + name: 'darkMode', + mutation: uiMut.SET_DARK_MODE + }, + { + name: 'darkModeUnlocked', + mutation: uiMut.SET_DARK_MODE_UNLOCKED + }, + { + name: 'mini', + path: 'sideBarCollapsed', + mutation: uiMut.SET_SIDEBAR_COLLAPSED + } + ] }), - mini: { - get: function () { - return this.$store.state.ui.sideBarCollapsed - }, - set: function (collapsed) { - this.$store.commit(uiMut.SET_SIDEBAR_COLLAPSED, collapsed) - } - }, production () { return process.env.NODE_ENV === 'production' }, diff --git a/frontend/src/components/CorrectionStatistics.vue b/frontend/src/components/CorrectionStatistics.vue index 2bd836a52b442bd13c847943ca5c8d3d55891b35..c6d877b2f5178f103822e64f39fdfdb5fbdc8d75 100644 --- a/frontend/src/components/CorrectionStatistics.vue +++ b/frontend/src/components/CorrectionStatistics.vue @@ -7,7 +7,11 @@ <ul class="inline-list mx-3"> <li>Submissions per student: <span>{{statistics.submissions_per_student}}</span></li> <li>Submissions per type: <span>{{statistics.submissions_per_type}}</span></li> - <li>Curr. mean score: <span>{{statistics.current_mean_score.toFixed(2)}}</span></li> + <li>Curr. mean score: + <span> + {{statistics.current_mean_score === null ? 'N.A.' : statistics.current_mean_score.toFixed(2)}} + </span> + </li> </ul> <v-divider class="mx-2 my-2"></v-divider> <div v-for="(progress, index) in statistics.submission_type_progress" :key="index"> diff --git a/frontend/src/components/DataExport.vue b/frontend/src/components/DataExport.vue index 2b3c58681fc4570599fab5b4d2360c922f931758..451fd950666d535aa5e4bb83a7a07bd4e88735fa 100644 --- a/frontend/src/components/DataExport.vue +++ b/frontend/src/components/DataExport.vue @@ -163,8 +163,4 @@ export default { </script> <style scoped> - #export-link { - color: #000; - text-decoration: none; - } </style> diff --git a/frontend/src/components/WelcomeJumbotron.vue b/frontend/src/components/WelcomeJumbotron.vue index d3d9151f280e4672958841fbc519fb2a9f6cea97..687274a6f14f988c3199c0982f10b60da4e9cb66 100644 --- a/frontend/src/components/WelcomeJumbotron.vue +++ b/frontend/src/components/WelcomeJumbotron.vue @@ -1,5 +1,10 @@ <template> - <v-jumbotron :gradient="gradient" dark class="elevation-10"> + <v-jumbotron :gradient="gradient" dark class="elevation-10" v-if="showJumbotron"> + <v-btn @click="hide" icon class="hide-btn" absolute> + <v-icon> + close + </v-icon> + </v-btn> <v-container fill-height> <v-layout align-center> <v-flex> @@ -26,12 +31,26 @@ </template> <script> +import { createComputedGetterSetter } from '@/util/helpers' +import { uiMut } from '@/store/modules/ui' + export default { name: 'welcome-jumbotron', data () { return { gradient: 'to bottom, #1A237E, #5753DD' } + }, + computed: { + showJumbotron: createComputedGetterSetter({ + path: 'ui.showJumbotron', + mutation: uiMut.SET_SHOW_JUMBOTRON + }) + }, + methods: { + hide () { + this.showJumbotron = false + } } } </script> @@ -41,4 +60,7 @@ export default { color: lightgrey; text-decoration: none; } + .hide-btn { + right: 0; + } </style> diff --git a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue index 03e7d84f650080e083e1c9bfd2a11dba814c10dc..c5ec661f53854323718c3f67843e79355b9c4756 100644 --- a/frontend/src/components/feedback_list/FeedbackSearchOptions.vue +++ b/frontend/src/components/feedback_list/FeedbackSearchOptions.vue @@ -88,32 +88,26 @@ export default { items: [ { name: 'showFinal', - path: 'showFinal', mutation: feedbackSearchOptsMut.SET_SHOW_FINAL }, { name: 'searchOtherUserComments', - path: 'searchOtherUserComments', mutation: feedbackSearchOptsMut.SET_SEARCH_OTHER_USER_COMMENTS }, { name: 'caseSensitive', - path: 'caseSensitive', mutation: feedbackSearchOptsMut.SET_CASE_SENSITIVE }, { name: 'useRegex', - path: 'useRegex', mutation: feedbackSearchOptsMut.SET_USE_REGEX }, { name: 'filterByTutors', - path: 'filterByTutors', mutation: feedbackSearchOptsMut.SET_FILTER_BY_TUTORS }, { name: 'filterByStage', - path: 'filterByStage', mutation: feedbackSearchOptsMut.SET_FILTER_BY_STAGE } diff --git a/frontend/src/components/submission_notes/RouteChangeConfirmation.vue b/frontend/src/components/submission_notes/RouteChangeConfirmation.vue index ce5a4b26ded02686037e41fa744996fae411e8a8..362980ee6d7a14ad660374a0f8f25a67d6424e48 100644 --- a/frontend/src/components/submission_notes/RouteChangeConfirmation.vue +++ b/frontend/src/components/submission_notes/RouteChangeConfirmation.vue @@ -41,10 +41,8 @@ export default { watch: { nextRoute (newVal, oldVal) { if (newVal !== oldVal && this.$store.getters['submissionNotes/workInProgress']) { - console.log('here') this.dialog = true } else { - console.log('there') this.nextRoute() } } diff --git a/frontend/src/components/submission_notes/base/CommentForm.vue b/frontend/src/components/submission_notes/base/CommentForm.vue index a8c1949df1446b1eb9f0757a7f52ade8c6437439..603f4398ff7b973165c873b1812c844dbc50a699 100644 --- a/frontend/src/components/submission_notes/base/CommentForm.vue +++ b/frontend/src/components/submission_notes/base/CommentForm.vue @@ -1,6 +1,6 @@ <template> <div> - <v-text-field + <v-textarea name="feedback-input" label="Please provide your feedback here" v-model="currentFeedback" @@ -8,7 +8,7 @@ @keyup.esc="collapseTextField" @focus="selectInput($event)" rows="2" - textarea + outline autofocus auto-grow hide-details diff --git a/frontend/src/components/submission_notes/base/FeedbackComment.vue b/frontend/src/components/submission_notes/base/FeedbackComment.vue index 935914856e729f745b3109104d6d0724d7033502..5b29b56c4b888872efd4ec093d36f79113d94525 100644 --- a/frontend/src/components/submission_notes/base/FeedbackComment.vue +++ b/frontend/src/components/submission_notes/base/FeedbackComment.vue @@ -21,7 +21,7 @@ </div> <div class="message">{{text}}</div> <v-btn - flat icon + flat icon absolute class="delete-button" v-if="deletable" @click.stop="toggleDeleteComment" @@ -144,9 +144,8 @@ export default { white-space: pre-wrap; } .delete-button { - position: absolute; - bottom: -20px; - left: -50px; + bottom: -12px; + left: -42px; } .comment-created { position: absolute; diff --git a/frontend/src/components/subscriptions/SubscriptionList.vue b/frontend/src/components/subscriptions/SubscriptionList.vue index 4ce05b34206cd19bf26c11647770840247088887..2e8b7f72b1a704b90fea1c8bd582e3bd2aa0b371 100644 --- a/frontend/src/components/subscriptions/SubscriptionList.vue +++ b/frontend/src/components/subscriptions/SubscriptionList.vue @@ -2,7 +2,7 @@ <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)" style="min-width: fit-content;"> + <v-toolbar-title v-if="showDetail" style="min-width: fit-content;"> Tasks </v-toolbar-title> <v-spacer/> @@ -16,7 +16,7 @@ /> </v-btn> </v-toolbar> - <v-tabs grow color="teal lighten-1" v-model="selectedStage"> + <v-tabs grow color="teal lighten-1" v-model="selectedStage" v-if="showDetail"> <v-tab v-for="(item, i) in stagesReadable" :key="i"> {{item}} </v-tab> @@ -59,7 +59,10 @@ export default { subscriptions: 'getSubscriptionsGroupedByType', stages: 'availableStages', stagesReadable: 'availableStagesReadable' - }) + }), + showDetail () { + return !this.sidebar || (this.sidebar && !this.sideBarCollapsed) + } }, methods: { ...mapActions([ diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 5576736f60685a98f689a1ee5caea62c9a2c278a..ad81436ec767936f2e0d2118a82e08287f7d99f8 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -4,13 +4,13 @@ import router from './router/index' import store from './store/store' import Vuetify from 'vuetify' import Notifications from 'vue-notification' -import Cliboard from 'v-clipboard' +import Clipboard from 'v-clipboard' import 'vuetify/dist/vuetify.min.css' import 'highlight.js/styles/atom-one-light.css' Vue.use(Vuetify) -Vue.use(Cliboard) +Vue.use(Clipboard) Vue.use(Notifications) Vue.config.productionTip = false diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue index 972b08b5086bbb1d9ffb3e37f7ac240751bebd23..0988645e495fe611959f06788d662156c367236b 100644 --- a/frontend/src/pages/Login.vue +++ b/frontend/src/pages/Login.vue @@ -1,7 +1,7 @@ <template> <v-container fill-height> <v-layout align-center justify-center> - <v-dialog v-model="registerDialog" max-width="fit-content" class="pa-4"> + <v-dialog v-model="registerDialog" class="pa-4" max-width="30%"> <register-dialog @registered="registered($event)"/> </v-dialog> <v-flex text-xs-center xs8 sm6 md4 lg2> diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index e780522f3cb6e3a2723bb573f3b59c3304a3e7ec..25b1aeee5b8f2fa7288587de7e8c2e923231c0d9 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,5 +1,5 @@ import Vue from 'vue' -import Router from 'vue-router' +import Router, {RawLocation, Route, NavigationGuard} from 'vue-router' import Login from '@/pages/Login.vue' import StudentSubmissionPage from '@/pages/student/StudentSubmissionPage.vue' import StudentOverviewPage from '@/pages/reviewer/StudentOverviewPage.vue' @@ -19,7 +19,9 @@ import store from '@/store/store' Vue.use(Router) -function denyAccess (next, redirect) { +type rerouteFunc = (to?: RawLocation | false | ((vm: Vue) => any) | void) => void + +function denyAccess (next: rerouteFunc, redirect: Route) { next(redirect.path) VueInstance.$notify({ title: 'Access denied', @@ -28,23 +30,23 @@ function denyAccess (next, redirect) { }) } -function tutorOrReviewerOnly (to, from, next) { +let tutorOrReviewerOnly: NavigationGuard = function (to, from, next) { if (store.getters.isTutorOrReviewer) { next() } else { - denyAccess(next, from.path) + denyAccess(next, from) } } -function reviewerOnly (to, from, next) { +let reviewerOnly: NavigationGuard = function (to, from, next) { if (store.getters.isReviewer) { next() } else { - denyAccess(next, from.path) + denyAccess(next, from) } } -function studentOnly (to, from, next) { +let studentOnly: NavigationGuard = function (to, from, next) { if (store.getters.isStudent) { next() } else { @@ -52,7 +54,7 @@ function studentOnly (to, from, next) { } } -function checkLoggedIn (to, from, next) { +let checkLoggedIn: NavigationGuard = function (to, from, next) { if (store.getters.isLoggedIn) { next() } else { diff --git a/frontend/src/store/actions.js b/frontend/src/store/actions.ts similarity index 96% rename from frontend/src/store/actions.js rename to frontend/src/store/actions.ts index b6ce78791497a13300e2b9eb1f9728530193d0e0..47ca46fc750f2160e3a009bd6ebc70784d4d6323 100644 --- a/frontend/src/store/actions.js +++ b/frontend/src/store/actions.ts @@ -53,9 +53,9 @@ const actions = { handleError(err, dispatch, 'Unable to fetch tutor data') } }, - async getAllFeedback ({commit, dispatch}, fields = []) { + async getAllFeedback ({commit, dispatch}, fields: string[] = []) { try { - const feedback = await api.fetchAllFeedback({fields}) + const feedback = await api.fetchAllFeedback(fields) commit(mut.SET_ALL_FEEDBACK, feedback) commit(mut.MAP_FEEDBACK_OF_SUBMISSION_TYPE) } catch (err) { @@ -110,7 +110,7 @@ const actions = { commit('submissionNotes/' + subNotesMut.RESET_STATE) commit(authMut.SET_MESSAGE, message) router.push({name: 'login'}) - router.go() + router.go(0) } } diff --git a/frontend/src/store/getters.js b/frontend/src/store/getters.ts similarity index 84% rename from frontend/src/store/getters.js rename to frontend/src/store/getters.ts index 5663746c1d79ac91d22c6b369f3455a9e46ed99b..3cdc0cbe2850114a1e33d632a759cd4a2da74a4c 100644 --- a/frontend/src/store/getters.js +++ b/frontend/src/store/getters.ts @@ -1,7 +1,7 @@ const getters = { corrected (state) { return state.statistics.submission_type_progress.every(progress => { - return progress.percentage === 100 + return progress.feedback_final === progress.submission_count }) }, getSubmission: state => pk => { diff --git a/frontend/src/store/grady_speak.js b/frontend/src/store/grady_speak.ts similarity index 100% rename from frontend/src/store/grady_speak.js rename to frontend/src/store/grady_speak.ts diff --git a/frontend/src/store/modules/authentication.js b/frontend/src/store/modules/authentication.ts similarity index 68% rename from frontend/src/store/modules/authentication.js rename to frontend/src/store/modules/authentication.ts index ce8bbb045e98a85a10ec70e979ce41f116b25b11..9e0c95e7234e7b27895120c20f2982382f2e8d50 100644 --- a/frontend/src/store/modules/authentication.js +++ b/frontend/src/store/modules/authentication.ts @@ -1,9 +1,28 @@ import * as api from '@/api' import gradySays from '../grady_speak' +import {ActionContext, Module} from 'vuex' -function initialState () { +export interface Credentials { + username: string, + password: string +} + +interface AuthState { + token: string, + lastTokenRefreshTry: number, + refreshingToken: boolean, + jwtTimeDelta: number, + message: string, + user: { + pk: string, + username: string, + role: string, + is_admin: boolean //eslint-disable-line + } +} +function initialState (): AuthState { return { - token: sessionStorage.getItem('token'), + token: sessionStorage.getItem('token') || '', lastTokenRefreshTry: Date.now(), refreshingToken: false, jwtTimeDelta: 0, @@ -12,7 +31,7 @@ function initialState () { pk: '', username: '', role: '', - is_admin: '' + is_admin: false } } } @@ -27,53 +46,53 @@ export const authMut = Object.freeze({ SET_REFRESHING_TOKEN: 'SET_REFRESHING_TOKEN' }) -const authentication = { +const authentication: Module<AuthState, any> = { state: initialState(), getters: { gradySpeak: () => { return gradySays[Math.floor(Math.random() * gradySays.length)] }, - isStudent: state => { + isStudent: (state: AuthState) => { return state.user.role === 'Student' }, - isTutor: state => { + isTutor: (state: AuthState) => { return state.user.role === 'Tutor' }, - isReviewer: state => { + isReviewer: (state: AuthState) => { return state.user.role === 'Reviewer' }, - isTutorOrReviewer: (state, getters) => { + isTutorOrReviewer: (state: AuthState, getters) => { return getters.isTutor || getters.isReviewer }, - isLoggedIn: state => !!state.token + isLoggedIn: (state: AuthState) => !!state.token }, mutations: { - [authMut.SET_MESSAGE] (state, message) { + [authMut.SET_MESSAGE] (state: AuthState, message: string) { state.message = message }, - [authMut.SET_JWT_TOKEN] (state, token) { + [authMut.SET_JWT_TOKEN] (state: AuthState, token: string) { sessionStorage.setItem('token', token) state.token = token }, - [authMut.SET_JWT_TIME_DELTA] (state, timeDelta) { + [authMut.SET_JWT_TIME_DELTA] (state: AuthState, timeDelta: number) { state.jwtTimeDelta = timeDelta }, - [authMut.SET_USER] (state, user) { + [authMut.SET_USER] (state: AuthState, user) { state.user = user }, - [authMut.SET_REFRESHING_TOKEN] (state, refreshing) { + [authMut.SET_REFRESHING_TOKEN] (state: AuthState, refreshing: boolean) { state.refreshingToken = refreshing }, - [authMut.SET_LAST_TOKEN_REFRESH_TRY] (state) { + [authMut.SET_LAST_TOKEN_REFRESH_TRY] (state: AuthState) { state.lastTokenRefreshTry = Date.now() }, - [authMut.RESET_STATE] (state) { + [authMut.RESET_STATE] (state: AuthState) { sessionStorage.setItem('token', '') Object.assign(state, initialState()) } }, actions: { - async getJWT (context, credentials) { + async getJWT (context: ActionContext<AuthState, any>, credentials: Credentials) { try { const token = await api.fetchJWT(credentials) context.commit(authMut.SET_JWT_TOKEN, token) diff --git a/frontend/src/store/modules/feedback_list/feedback-search-options.js b/frontend/src/store/modules/feedback_list/feedback-search-options.ts similarity index 100% rename from frontend/src/store/modules/feedback_list/feedback-search-options.js rename to frontend/src/store/modules/feedback_list/feedback-search-options.ts diff --git a/frontend/src/store/modules/feedback_list/feedback-table.js b/frontend/src/store/modules/feedback_list/feedback-table.ts similarity index 96% rename from frontend/src/store/modules/feedback_list/feedback-table.js rename to frontend/src/store/modules/feedback_list/feedback-table.ts index 35e526ac8befd77a62e77e5ab6b78d74cb3c7dca..4ea766a8009ccf177b7c464fa246545e2ade6e85 100644 --- a/frontend/src/store/modules/feedback_list/feedback-table.js +++ b/frontend/src/store/modules/feedback_list/feedback-table.ts @@ -41,7 +41,7 @@ const feedbackTable = { actions: { mapFeedbackHistOfSubmissionType ({getters, commit, state}) { for (const feedback of Object.values(state.feedbackHist)) { - const type = getters.getSubmissionType(feedback.of_submission_type) + const type = getters.getSubmissionType((feedback as any).of_submission_type) commit(feedbackTableMut.SET_FEEDBACK_OF_SUBMISSION_TYPE, {feedback, type}) } }, diff --git a/frontend/src/store/modules/student-page.js b/frontend/src/store/modules/student-page.ts similarity index 100% rename from frontend/src/store/modules/student-page.js rename to frontend/src/store/modules/student-page.ts diff --git a/frontend/src/store/modules/submission-notes.js b/frontend/src/store/modules/submission-notes.ts similarity index 98% rename from frontend/src/store/modules/submission-notes.js rename to frontend/src/store/modules/submission-notes.ts index 08669ce051587f7b08620ac603d668d863dfddf3..cd02557f0009049f8381fe55c6e660e4cd2a8bfd 100644 --- a/frontend/src/store/modules/submission-notes.js +++ b/frontend/src/store/modules/submission-notes.ts @@ -122,7 +122,7 @@ const submissionNotes = { deleteComments: async function ({state}) { return Promise.all( Object.values(state.commentsMarkedForDeletion).map(comment => { - return api.deleteComment(comment) + return api.deleteComment((comment as any)) }) ) }, @@ -143,7 +143,7 @@ const submissionNotes = { if (!state.hasOrigFeedback) { return api.submitFeedbackForAssignment({feedback}) } else { - feedback.pk = state.origFeedback.pk + feedback['pk']= state.origFeedback.pk return api.submitUpdatedFeedback({feedback}) } } diff --git a/frontend/src/store/modules/subscriptions.js b/frontend/src/store/modules/subscriptions.ts similarity index 91% rename from frontend/src/store/modules/subscriptions.js rename to frontend/src/store/modules/subscriptions.ts index 531ea8093b91a03d64fb41f56960d1eea10a007e..588867f0defa7321fd29e461fa3e2d611db0cad6 100644 --- a/frontend/src/store/modules/subscriptions.js +++ b/frontend/src/store/modules/subscriptions.ts @@ -50,10 +50,10 @@ const subscriptions = { return stages }, availableSubmissionTypeQueryKeys (state, getters, rootState) { - return Object.values(rootState.submissionTypes).map(subType => subType.pk) + return Object.values(rootState.submissionTypes).map((subType: any) => subType.pk) }, availableExamTypeQueryKeys (state, getters, rootState) { - return Object.values(rootState.examTypes).map(examType => examType.pk) + return Object.values(rootState.examTypes).map((examType: any) => examType.pk) }, activeSubscription (state) { return state.subscriptions[state.activeSubscriptionPk] @@ -86,25 +86,26 @@ const subscriptions = { acc[curr] = subscriptionsByType() return acc }, {}) - Object.values(state.subscriptions).forEach(subscription => { + Object.values(state.subscriptions).forEach((subscription: any) => { 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 - } - }) - } - }) + const sortSubscriptions = subscriptionsByType => Object.values(subscriptionsByType) + .forEach((arr: object[]) => { + 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) }) @@ -186,7 +187,7 @@ const subscriptions = { } }, async cleanAssignmentsFromSubscriptions ({commit, state, dispatch}, excludeActive = true) { - Object.values(state.subscriptions).forEach(subscription => { + Object.values(state.subscriptions).forEach((subscription: any) => { if (!excludeActive || subscription.pk !== state.activeSubscriptionPk) { subscription.assignments.forEach(assignment => { api.deleteAssignment({assignment}) diff --git a/frontend/src/store/modules/ui.js b/frontend/src/store/modules/ui.ts similarity index 70% rename from frontend/src/store/modules/ui.js rename to frontend/src/store/modules/ui.ts index df08d683c22f997f988e7ba764f57654f9b54e41..2cecc601d8df26034b9d98da0fc3547a44d4ce84 100644 --- a/frontend/src/store/modules/ui.js +++ b/frontend/src/store/modules/ui.ts @@ -3,14 +3,16 @@ function initialState () { return { sideBarCollapsed: false, darkMode: false, - darkModeUnlocked: false + darkModeUnlocked: false, + showJumbotron: true } } export const uiMut = Object.freeze({ SET_SIDEBAR_COLLAPSED: 'SET_SIDEBAR_COLLAPSED', SET_DARK_MODE: 'SET_DARK_MODE', - SET_DARK_MODE_UNLOCKED: 'SET_DARK_MODE_UNLOCKED' + SET_DARK_MODE_UNLOCKED: 'SET_DARK_MODE_UNLOCKED', + SET_SHOW_JUMBOTRON: 'SET_SHOW_JUMBOTRON' }) const ui = { @@ -24,6 +26,9 @@ const ui = { }, [uiMut.SET_DARK_MODE_UNLOCKED] (state, val) { state.darkModeUnlocked = val + }, + [uiMut.SET_SHOW_JUMBOTRON] (state, val) { + state.showJumbotron = val } } } diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.ts similarity index 79% rename from frontend/src/store/mutations.js rename to frontend/src/store/mutations.ts index 2e4c4081fd4253ef781e21b22e62589baefdc798..1c9e80ad6460bd0813b5e60872df2403f5f74508 100644 --- a/frontend/src/store/mutations.js +++ b/frontend/src/store/mutations.ts @@ -26,27 +26,6 @@ const mutations = { return acc }, {}) }, - [mut.SET_SUBSCRIPTIONS] (state, subscriptions) { - state.subscriptions = subscriptions.reduce((acc, curr) => { - acc[curr['pk']] = curr - return acc - }, {}) - for (let subscription of Object.values(subscriptions)) { - for (let assignment of subscription.assignments) { - if (assignment.feedback) { - Vue.set(assignment.feedback, 'feedback_stage_for_user', subscription.feedback_stage) - } - } - } - }, - [mut.SET_SUBSCRIPTION] (state, subscription) { - Vue.set(state.subscriptions, subscription.pk, subscription) - for (let assignment of subscription.assignments) { - if (assignment.feedback) { - Vue.set(assignment.feedback, 'feedback_stage_for_user', subscription.feedback_stage) - } - } - }, [mut.SET_STUDENTS] (state, students) { state.students = students.reduce((acc, curr) => { acc[curr.pk] = mapStudent(curr, state.studentMap) diff --git a/frontend/src/store/plugins/lastInteractionPlugin.js b/frontend/src/store/plugins/lastInteractionPlugin.ts similarity index 100% rename from frontend/src/store/plugins/lastInteractionPlugin.js rename to frontend/src/store/plugins/lastInteractionPlugin.ts diff --git a/frontend/src/util/helpers.js b/frontend/src/util/helpers.ts similarity index 60% rename from frontend/src/util/helpers.js rename to frontend/src/util/helpers.ts index c18c724fc35c10616f5a168b7628d7adc1a1fc83..1927f50accbc2661ba4cd552c8be1db8711540ea 100644 --- a/frontend/src/util/helpers.js +++ b/frontend/src/util/helpers.ts @@ -1,11 +1,12 @@ +import vueInstance from '@/main.ts' -export function nameSpacer (namespace) { - return function (commitType) { +export function nameSpacer (namespace: string) { + return function (commitType: string) { return namespace + commitType } } -export function getObjectValueByPath (obj, path) { +export function getObjectValueByPath (obj: any, path: string): any { // credit: http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key#comment55278413_6491621 if (!path || path.constructor !== String) return path = path.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties @@ -22,6 +23,11 @@ export function getObjectValueByPath (obj, path) { return obj } +interface GetSetPair { + get: () => any, + set: (val: object) => void +} + /** * Use this method to generate a computed property accessing the store for a Vue instance. * The get method will return the value at this.$store.state.<path>. @@ -31,17 +37,29 @@ export function getObjectValueByPath (obj, path) { * @param namespace to prepend the mutation type with * @returns {*} */ -export function createComputedGetterSetter ({path, mutation, namespace}) { +export function createComputedGetterSetter ( + {path, mutation, namespace}: + {path: string, mutation: string, namespace:string}): GetSetPair { return { - get () { - return getObjectValueByPath(this.$store.state, path) + get (): any { + return getObjectValueByPath(vueInstance.$store.state, path) }, - set (val) { - this.$store.commit(`${namespace ? namespace + '/' : ''}${mutation}`, val) + set (val: object): void { + vueInstance.$store.commit(`${namespace ? namespace + '/' : ''}${mutation}`, val) } } } +interface StateMapperItem { + name: string, + mutation: string, + path?: string +} + +interface MappedState { + [key: string]: GetSetPair +} + /** * Returns an object of generated computed getter/setter pairs. * Can be used to quickly bind a stores state and corresponding setters to a vue component @@ -49,10 +67,14 @@ export function createComputedGetterSetter ({path, mutation, namespace}) { * @param pathPrefix if set, all items path will be prepended by the path prefix * @param items array that contains objects {name, path, mutation} */ -export function mapStateToComputedGetterSetter ({namespace = '', pathPrefix = '', items = []}) { - return items.reduce((acc, curr) => { - let path = pathPrefix ? `${pathPrefix}.${curr.path}` : curr.path - acc[curr.name] = createComputedGetterSetter({...curr, path, namespace}) +export function mapStateToComputedGetterSetter ( + {namespace = '', pathPrefix = '', items = []}: + {namespace: string, pathPrefix: string, items: StateMapperItem[]}): MappedState { + return items.reduce((acc: MappedState, curr) => { + // if no path is give, use name + const itemPath = curr.path || curr.name + const path = pathPrefix ? `${pathPrefix}.${itemPath}` : itemPath + acc[curr.name] = createComputedGetterSetter({mutation: curr.mutation, path, namespace}) return acc }, {}) } @@ -61,24 +83,25 @@ export function mapStateToComputedGetterSetter ({namespace = '', pathPrefix = '' // 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) { + // @ts-ignore can be ignored since cartesian is only recursively called if b si truthy return b ? cartesian(cartesianHelper(a, b), ...c) : a } // flatten an array -export function flatten (list) { +export function flatten (list: any[]): any[] { return list.reduce( (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] ) } -export function objectifyArray (arr, key = 'pk') { +export function objectifyArray<T> (arr: T[], key = 'pk'): {[key: string]: T} { return arr.reduce((acc, curr) => { acc[curr[key]] = curr return acc }, {}) } -export function once (fn, context) { +export function once (fn: Function, context?: object) { let result return function () { if (!result) { diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index c1700466f6d20ad84535501ff813e85d1d5cc680..c800f3ea8e347d2dbec47be5b999bd78f2e119d3 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -10,24 +10,20 @@ "esModuleInterop": true, "sourceMap": true, "baseUrl": ".", - "types": [ - "node", - "mocha", - "chai" - ], "paths": { "@/*": [ "src/*" ] }, "lib": [ - "es2015", + "es2017", "dom", "dom.iterable", "scripthost" ] }, "include": [ + "@types/", "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", diff --git a/frontend/vue.config.js b/frontend/vue.config.js index 78bc4df2ae1e5ca245453ca7b8c389cee5e513c8..d321253997a1fc75cc8b2ed0b9aabd624ded3303 100644 --- a/frontend/vue.config.js +++ b/frontend/vue.config.js @@ -3,6 +3,11 @@ const path = require('path') const projectRoot = path.resolve(__dirname) module.exports = { + assetsDir: 'static', + devServer: { + allowedHosts: ['localhost'], + host: 'localhost' + }, configureWebpack: { resolve: { alias: { diff --git a/grady/settings/default.py b/grady/settings/default.py index 8099ca8962d5f00b7cefae7ca566d27a2b51332d..eb220958dc8b6186385e41cc9dc0fc3a49a010ae 100644 --- a/grady/settings/default.py +++ b/grady/settings/default.py @@ -116,7 +116,7 @@ STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.AppDirectoriesFinder', ) STATICFILES_DIRS = ( - 'frontend/dist/', + 'frontend/dist/static', )