diff --git a/kolibri/core/assets/src/exams/utils.js b/kolibri/core/assets/src/exams/utils.js index d1f3a8d7324..a59f1e571f2 100644 --- a/kolibri/core/assets/src/exams/utils.js +++ b/kolibri/core/assets/src/exams/utils.js @@ -114,7 +114,7 @@ export function revertV3toV2(questionSources) { /** * @param {object} exam - an exam object of any question_sources version - * @returns V2 formatted question_sources + * @returns V3 formatted question_sources */ export function convertExamQuestionSourcesToV3(exam, extraArgs = {}) { if (exam.data_model_version !== 3) { diff --git a/kolibri/plugins/learn/assets/src/modules/examViewer/handlers.js b/kolibri/plugins/learn/assets/src/modules/examViewer/handlers.js index 6096f798fda..9ec709b726a 100644 --- a/kolibri/plugins/learn/assets/src/modules/examViewer/handlers.js +++ b/kolibri/plugins/learn/assets/src/modules/examViewer/handlers.js @@ -1,6 +1,6 @@ import { ContentNodeResource, ExamResource } from 'kolibri.resources'; import samePageCheckGenerator from 'kolibri.utils.samePageCheckGenerator'; -import { convertExamQuestionSources } from 'kolibri.utils.exams'; +import { convertExamQuestionSourcesToV3 } from 'kolibri.utils.exams'; import shuffled from 'kolibri.utils.shuffled'; import { ClassesPageNames } from '../../constants'; import { LearnerClassroomResource } from '../../apiResources'; @@ -30,10 +30,19 @@ export function showExam(store, params, alreadyOnQuiz) { store.commit('classAssignments/SET_CURRENT_CLASSROOM', classroom); let contentPromise; - if (exam.question_sources.length) { + let allExerciseIds = []; + if (exam.data_version == 3) { + allExerciseIds = exam.question_sources.reduce((acc, section) => { + acc = [...acc, ...section.questions.map(q => q.exercise_id)]; + return acc; + }, []); + } else { + allExerciseIds = exam.question_sources.map(q => q.exercise_id); + } + if (allExerciseIds.length) { contentPromise = ContentNodeResource.fetchCollection({ getParams: { - ids: exam.question_sources.map(item => item.exercise_id), + ids: allExerciseIds, }, }); } else { @@ -43,26 +52,36 @@ export function showExam(store, params, alreadyOnQuiz) { contentNodes => { if (shouldResolve()) { // If necessary, convert the question source info - let questions = convertExamQuestionSources(exam, { contentNodes }); + const question_sources = convertExamQuestionSourcesToV3(exam, { contentNodes }); // When necessary, randomize the questions for the learner. // Seed based on the user ID so they see a consistent order each time. - if (!exam.learners_see_fixed_order) { - questions = shuffled(questions, store.state.core.session.user_id); - } + question_sources.forEach(section => { + if (!section.learners_see_fixed_order) { + section.questions = shuffled( + section.questions, + store.state.core.session.user_id + ); + } + }); + + const allQuestions = question_sources.reduce((acc, section) => { + acc = [...acc, ...section.questions]; + return acc; + }, []); // Exam is drawing solely on malformed exercise data, best to quit now - if (questions.some(question => !question.question_id)) { + if (allQuestions.some(question => !question.question_id)) { store.dispatch( 'handleError', `This quiz cannot be displayed:\nQuestion sources: ${JSON.stringify( - questions + allQuestions )}\nExam: ${JSON.stringify(exam)}` ); return; } // Illegal question number! - else if (questionNumber >= questions.length) { + else if (questionNumber >= allQuestions.length) { store.dispatch( 'handleError', `Question number ${questionNumber} is not valid for this quiz` @@ -76,15 +95,15 @@ export function showExam(store, params, alreadyOnQuiz) { contentNodeMap[node.id] = node; } - for (const question of questions) { + for (const question of allQuestions) { question.missing = !contentNodeMap[question.exercise_id]; } - + exam.question_sources = question_sources; store.commit('examViewer/SET_STATE', { contentNodeMap, exam, questionNumber, - questions, + questions: allQuestions, }); store.commit('CORE_SET_PAGE_LOADING', false); store.commit('CORE_SET_ERROR', null); diff --git a/kolibri/plugins/learn/assets/src/views/ExamPage/index.vue b/kolibri/plugins/learn/assets/src/views/ExamPage/index.vue index d12fd203a05..ff1fca75c04 100644 --- a/kolibri/plugins/learn/assets/src/views/ExamPage/index.vue +++ b/kolibri/plugins/learn/assets/src/views/ExamPage/index.vue @@ -5,168 +5,258 @@ :appBarTitle="exam.title || ''" > - - - -
- -
-

{{ coreString('timeSpentLabel') }}

-
- +
+ + + +
+ + +
+
+ + + + + +
+ + + + + +
-

- {{ learnString('suggestedTime') }} -

- +
+
+ +
+ + +

+ {{ $tr('question', { num: questionNumber + 1, total: exam.question_count }) }} +

+ -
- - - - -
- - -
- - -

- {{ $tr('question', { num: questionNumber + 1, total: exam.question_count }) }} -

- - -
- - - - - {{ $tr('nextQuestion') }} - - - - - {{ $tr('previousQuestion') }} - - - - -
+ +
+
+ + + + + + + {{ $tr('previousQuestion') }} + + + + + +
- {{ answeredText }} -
- -
- {{ $tr('unableToSubmit') }} -
-
-
- - - -
-
{{ answeredText }} +
+ {{ $tr('unableToSubmit') }} +
- -
- {{ $tr('unableToSubmit') }} -
-
- - - + + + + +
+ + + + +
+
+ + +
+ -

{{ $tr('areYouSure') }}

{{ $tr('unanswered', { numLeft: questionsUnanswered } ) }}

+

{{ $tr('areYouSure') }}

+ + + + + + {{ coreString('cancelAction') }} + + + + + + {{ $tr('tryAgain') }} + + +
@@ -179,11 +269,7 @@ import isEqual from 'lodash/isEqual'; import debounce from 'lodash/debounce'; import BottomAppBar from 'kolibri.coreVue.components.BottomAppBar'; - import UiAlert from 'kolibri-design-system/lib/keen/UiAlert'; - import UiIconButton from 'kolibri.coreVue.components.UiIconButton'; import useKResponsiveWindow from 'kolibri-design-system/lib/useKResponsiveWindow'; - import SuggestedTime from 'kolibri.coreVue.components.SuggestedTime'; - import TimeDuration from 'kolibri.coreVue.components.TimeDuration'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; import ImmersivePage from 'kolibri.coreVue.components.ImmersivePage'; import ResourceSyncingUiAlert from '../ResourceSyncingUiAlert'; @@ -202,11 +288,7 @@ }, components: { AnswerHistory, - UiAlert, - UiIconButton, BottomAppBar, - TimeDuration, - SuggestedTime, ImmersivePage, ResourceSyncingUiAlert, }, @@ -236,6 +318,7 @@ data() { return { submitModalOpen: false, + isDescriptionVisible: false, // Note this time is only used to calculate the time spent on a // question, it is not used to generate any timestamps. startTime: Date.now(), @@ -327,11 +410,6 @@ // https://github.com/vuejs/vue/issues/2870#issuecomment-219096773 return debounce(this.setAndSaveCurrentExamAttemptLog, 500); }, - bottomBarLayoutDirection() { - // Allows contents to be displayed visually in reverse-order, - // but semantically in correct order. - return this.isRtl ? 'ltr' : 'rtl'; - }, layoutDirReset() { // Overrides bottomBarLayoutDirection reversal return this.isRtl ? 'rtl' : 'ltr'; @@ -398,6 +476,7 @@ } return this.updateContentSession(data) + .then(() => { if (close) { this.stopTrackingProgress(); @@ -460,6 +539,9 @@ } this.submitModalOpen = !this.submitModalOpen; }, + toggleDescription() { + this.isDescriptionVisible = !this.isDescriptionVisible; + }, finishExam() { this.saveAnswer(true).then(() => { this.$router.push(this.backPageLink); @@ -468,7 +550,7 @@ }, $trs: { submitExam: { - message: 'Submit quiz', + message: 'Submit Quiz', context: 'Action that learner takes to submit their quiz answers so that the coach can review them.', }, @@ -507,6 +589,10 @@ context: 'Indicates that a learner cannot submit the quiz because they are not able to see all the questions.', }, + tryAgain: { + message: 'Try Again', + context: 'Indicates that quiz can only be submitted with all questions answered.', + }, }, }; @@ -517,7 +603,7 @@ .answered { display: inline-block; - margin-right: 8px; + margin-right: 700px; margin-left: 8px; white-space: nowrap; } @@ -550,7 +636,7 @@ .left-align { position: absolute; - left: 16px; + left: 10px; display: inline-block; } @@ -567,4 +653,45 @@ overflow-y: hidden; } + .number-of-questions { + font-size: 1em; + font-weight: 400; + text-align: center; + } + + .spacing-items { + padding: 0.5em; + } + + .quiz-title { + font-size: 14px; + font-weight: 700; + } + + .icon-size { + font-size: 1.5em; + } + + .btn-size { + width: 100%; + margin-top: 1em; + } + + .fixed-element { + position: fixed; + right: 20px; + bottom: 20px; + } + + .centered-text { + text-align: center; + } + + .remove-btn-style { + width: 100%; + padding: 0; + background-color: transparent; + border: 0; + } +