From 41e8ec6fc1a960b419d2de44f16ef95a2708df15 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Thu, 11 Jun 2026 21:43:39 -0500 Subject: [PATCH 01/21] Exercise: Migrate legacy exercise tool to Vue --- assets/vue/router/exercise.js | 68 + assets/vue/router/index.js | 2 + assets/vue/services/exerciseService.js | 179 + .../exercise/ExerciseConfigurationView.vue | 863 +++++ .../vue/views/exercise/ExerciseListView.vue | 598 +++ .../vue/views/exercise/ExercisePlayerView.vue | 1204 ++++++ .../exercise/ExerciseQuestionEditorView.vue | 3426 +++++++++++++++++ .../exercise/ExerciseQuestionSelectorView.vue | 1008 +++++ .../vue/views/exercise/ExerciseReportView.vue | 608 +++ .../vue/views/exercise/ExerciseResultView.vue | 1008 +++++ public/main/exercise/answer.class.php | 6 +- public/main/exercise/exercise.class.php | 8 +- .../Exercise/ExerciseConfiguration.php | 242 ++ .../ApiResource/Exercise/ExerciseList.php | 79 + .../ApiResource/Exercise/ExerciseQuestion.php | 85 + .../Exercise/ExerciseQuestionAction.php | 71 + .../Exercise/ExerciseQuestionEditor.php | 313 ++ .../ApiResource/Exercise/ExerciseRuntime.php | 98 + .../Exercise/ExerciseRuntimeAnswer.php | 91 + .../Exercise/ExerciseRuntimeAttempt.php | 117 + .../Exercise/ExerciseRuntimeAttemptDelete.php | 64 + .../Exercise/ExerciseRuntimeCorrection.php | 85 + .../Exercise/ExerciseRuntimeFinish.php | 83 + .../Exercise/ExerciseRuntimeReport.php | 82 + .../Exercise/ExerciseRuntimeResult.php | 90 + .../Exercise/ExerciseRuntimeUploadAnswer.php | 114 + ...erciseRuntimeAttemptFileDownloadAction.php | 212 + src/CoreBundle/Controller/IndexController.php | 1 + .../ExerciseConfigurationProcessor.php | 510 +++ .../ExerciseConfigurationProvider.php | 422 ++ .../State/Exercise/ExerciseListProvider.php | 410 ++ .../ExerciseQuestionActionProcessor.php | 508 +++ .../ExerciseQuestionEditorProcessor.php | 2551 ++++++++++++ .../ExerciseQuestionEditorProvider.php | 1449 +++++++ .../Exercise/ExerciseQuestionProvider.php | 838 ++++ .../ExerciseRuntimeAnswerProcessor.php | 690 ++++ .../ExerciseRuntimeAttemptDeleteProcessor.php | 185 + .../ExerciseRuntimeAttemptProcessor.php | 547 +++ .../ExerciseRuntimeCorrectionProcessor.php | 365 ++ .../ExerciseRuntimeFinishProcessor.php | 1077 ++++++ .../Exercise/ExerciseRuntimeProvider.php | 990 +++++ .../ExerciseRuntimeReportProvider.php | 382 ++ .../ExerciseRuntimeResultProvider.php | 1267 ++++++ .../ExerciseRuntimeUploadAnswerProcessor.php | 548 +++ src/CoreBundle/Tool/Exercise.php | 2 +- src/CourseBundle/Entity/CQuizQuestion.php | 7 + 46 files changed, 23545 insertions(+), 8 deletions(-) create mode 100644 assets/vue/router/exercise.js create mode 100644 assets/vue/services/exerciseService.js create mode 100644 assets/vue/views/exercise/ExerciseConfigurationView.vue create mode 100644 assets/vue/views/exercise/ExerciseListView.vue create mode 100644 assets/vue/views/exercise/ExercisePlayerView.vue create mode 100644 assets/vue/views/exercise/ExerciseQuestionEditorView.vue create mode 100644 assets/vue/views/exercise/ExerciseQuestionSelectorView.vue create mode 100644 assets/vue/views/exercise/ExerciseReportView.vue create mode 100644 assets/vue/views/exercise/ExerciseResultView.vue create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseConfiguration.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseList.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseQuestion.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseQuestionAction.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseQuestionEditor.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseRuntime.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseRuntimeAnswer.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseRuntimeAttempt.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseRuntimeAttemptDelete.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseRuntimeCorrection.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseRuntimeFinish.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseRuntimeReport.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseRuntimeResult.php create mode 100644 src/CoreBundle/ApiResource/Exercise/ExerciseRuntimeUploadAnswer.php create mode 100644 src/CoreBundle/Controller/Api/ExerciseRuntimeAttemptFileDownloadAction.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseConfigurationProcessor.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseConfigurationProvider.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseListProvider.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseQuestionActionProcessor.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseQuestionEditorProcessor.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseQuestionEditorProvider.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseQuestionProvider.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseRuntimeAnswerProcessor.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseRuntimeAttemptDeleteProcessor.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseRuntimeAttemptProcessor.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseRuntimeCorrectionProcessor.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseRuntimeFinishProcessor.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseRuntimeProvider.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseRuntimeReportProvider.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseRuntimeResultProvider.php create mode 100644 src/CoreBundle/State/Exercise/ExerciseRuntimeUploadAnswerProcessor.php diff --git a/assets/vue/router/exercise.js b/assets/vue/router/exercise.js new file mode 100644 index 00000000000..06467aa58a0 --- /dev/null +++ b/assets/vue/router/exercise.js @@ -0,0 +1,68 @@ +export default { + path: "/resources/exercise/:node/", + meta: { + requiresAuth: true, + showBreadcrumb: true, + tool: "exercise", + breadcrumb: "Exercises", + }, + name: "exercise", + component: () => import("../components/layout/SimpleRouterViewLayout.vue"), + redirect: { name: "ExerciseList" }, + children: [ + { + name: "ExerciseList", + path: "", + meta: { breadcrumb: "Exercises" }, + component: () => import("../views/exercise/ExerciseListView.vue"), + }, + { + name: "ExerciseCreate", + path: "create", + meta: { breadcrumb: "Create exercise" }, + component: () => import("../views/exercise/ExerciseConfigurationView.vue"), + }, + { + name: "ExerciseEdit", + path: ":exerciseId/edit", + meta: { breadcrumb: "Edit exercise" }, + component: () => import("../views/exercise/ExerciseConfigurationView.vue"), + }, + { + name: "ExercisePlayer", + path: ":exerciseId/player", + meta: { breadcrumb: "Exercise player" }, + component: () => import("../views/exercise/ExercisePlayerView.vue"), + }, + { + name: "ExerciseResult", + path: ":exerciseId/result/:attemptId", + meta: { breadcrumb: "Exercise result" }, + component: () => import("../views/exercise/ExerciseResultView.vue"), + }, + { + name: "ExerciseReport", + path: ":exerciseId/report", + meta: { breadcrumb: "Learner score" }, + component: () => import("../views/exercise/ExerciseReportView.vue"), + }, + { + name: "ExerciseQuestions", + path: ":exerciseId/questions", + meta: { breadcrumb: "Questions" }, + component: () => import("../views/exercise/ExerciseQuestionSelectorView.vue"), + }, + { + name: "ExerciseQuestionCreate", + path: ":exerciseId/questions/create/:questionType", + meta: { breadcrumb: "Create question" }, + component: () => import("../views/exercise/ExerciseQuestionEditorView.vue"), + }, + { + name: "ExerciseQuestionEdit", + path: ":exerciseId/questions/:questionId/edit", + meta: { breadcrumb: "Edit question" }, + component: () => import("../views/exercise/ExerciseQuestionEditorView.vue"), + }, + ], +} diff --git a/assets/vue/router/index.js b/assets/vue/router/index.js index 14465a9cd2c..16b2ba4c2f8 100644 --- a/assets/vue/router/index.js +++ b/assets/vue/router/index.js @@ -26,6 +26,7 @@ import assignments from "./assignments" import links from "./links" import forum from "./forum" import survey from "./survey" +import exercise from "./exercise" import glossary from "./glossary" import attendance from "./attendance" import lpRoutes from "./lp" @@ -381,6 +382,7 @@ const router = createRouter({ links, forum, survey, + exercise, glossary, attendance, lpRoutes, diff --git a/assets/vue/services/exerciseService.js b/assets/vue/services/exerciseService.js new file mode 100644 index 00000000000..79017d2fba3 --- /dev/null +++ b/assets/vue/services/exerciseService.js @@ -0,0 +1,179 @@ +import baseService from "./baseService" + +function cleanParams(params = {}) { + const query = {} + + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null && String(value) !== "") { + query[key] = value + } + } + + return query +} + +function buildQueryString(params = {}) { + const query = new URLSearchParams() + + for (const [key, value] of Object.entries(cleanParams(params))) { + query.set(key, String(value)) + } + + const queryString = query.toString() + + return queryString ? `?${queryString}` : "" +} + +function exerciseRequestConfig(config = {}) { + return { + skipCourseContext: true, + ...config, + } +} + +export default { + async getExerciseList(params = {}) { + return await baseService.get("/api/exercise/list", cleanParams(params), exerciseRequestConfig()) + }, + + async getExerciseConfiguration(params = {}, exerciseId = null) { + const endpoint = exerciseId ? `/api/exercise/configuration/${exerciseId}` : "/api/exercise/configuration" + + return await baseService.get(endpoint, cleanParams(params), exerciseRequestConfig()) + }, + + + async getExerciseQuestions(params = {}, exerciseId) { + return await baseService.get(`/api/exercise/questions/${exerciseId}`, cleanParams(params), exerciseRequestConfig()) + }, + + async getExerciseRuntime(params = {}, exerciseId) { + return await baseService.get(`/api/exercise/runtime/${exerciseId}`, cleanParams(params), exerciseRequestConfig()) + }, + + async startExerciseAttempt(payload = {}, params = {}, exerciseId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async saveExerciseRuntimeAnswer(payload = {}, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/answer${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + + async uploadExerciseRuntimeAnswer(formData, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.postForm( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/upload-answer${queryString}`, + formData, + exerciseRequestConfig(), + ) + }, + + async finishExerciseRuntimeAttempt(payload = {}, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/finish${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async getExerciseRuntimeResult(params = {}, exerciseId, attemptId) { + return await baseService.get( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/result`, + cleanParams(params), + exerciseRequestConfig(), + ) + }, + + async getExerciseRuntimeReport(params = {}, exerciseId) { + return await baseService.get( + `/api/exercise/runtime/${exerciseId}/attempts`, + cleanParams(params), + exerciseRequestConfig(), + ) + }, + + async deleteExerciseRuntimeAttempt(payload = {}, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/delete${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async saveExerciseRuntimeCorrection(payload = {}, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/correction${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async getExerciseQuestionEditor(params = {}, exerciseId, questionId = null, questionType = null) { + const queryParams = { ...params } + + if (questionType !== null && questionType !== undefined) { + queryParams.type = questionType + } + + const endpoint = questionId + ? `/api/exercise/questions/${exerciseId}/editor/${questionId}` + : `/api/exercise/questions/${exerciseId}/editor` + + return await baseService.get(endpoint, cleanParams(queryParams), exerciseRequestConfig()) + }, + + async saveExerciseQuestion(payload, params = {}, exerciseId, questionId = null) { + const queryString = buildQueryString(params) + const endpoint = questionId + ? `/api/exercise/questions/${exerciseId}/editor/${questionId}${queryString}` + : `/api/exercise/questions/${exerciseId}/editor${queryString}` + + return await baseService.post(endpoint, payload, {}, exerciseRequestConfig()) + }, + + async saveExerciseQuestionAction(payload, params = {}, exerciseId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/questions/${exerciseId}/action${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async saveExerciseConfiguration(payload, params = {}, exerciseId = null) { + const queryString = buildQueryString(params) + + if (exerciseId) { + return await baseService.put(`/api/exercise/configuration/${exerciseId}${queryString}`, payload, exerciseRequestConfig()) + } + + return await baseService.post(`/api/exercise/configuration${queryString}`, payload, {}, exerciseRequestConfig()) + }, +} diff --git a/assets/vue/views/exercise/ExerciseConfigurationView.vue b/assets/vue/views/exercise/ExerciseConfigurationView.vue new file mode 100644 index 00000000000..bb59aa339a0 --- /dev/null +++ b/assets/vue/views/exercise/ExerciseConfigurationView.vue @@ -0,0 +1,863 @@ + + + diff --git a/assets/vue/views/exercise/ExerciseListView.vue b/assets/vue/views/exercise/ExerciseListView.vue new file mode 100644 index 00000000000..c5f5b624aba --- /dev/null +++ b/assets/vue/views/exercise/ExerciseListView.vue @@ -0,0 +1,598 @@ + + + + + diff --git a/assets/vue/views/exercise/ExercisePlayerView.vue b/assets/vue/views/exercise/ExercisePlayerView.vue new file mode 100644 index 00000000000..65ec05ce5f8 --- /dev/null +++ b/assets/vue/views/exercise/ExercisePlayerView.vue @@ -0,0 +1,1204 @@ +