Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to sections in ExamPage #12182

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
360 changes: 303 additions & 57 deletions kolibri/core/assets/src/views/AttemptLogList.vue

Large diffs are not rendered by default.

21 changes: 20 additions & 1 deletion kolibri/core/assets/src/views/ExamReport/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
:attemptLogs="attemptLogs"
:selectedQuestionNumber="questionNumber"
:isSurvey="isSurvey"
:sections="sections"
@select="navigateToQuestion"
/>
</template>
Expand All @@ -93,6 +94,7 @@
:attemptLogs="attemptLogs"
:selectedQuestionNumber="questionNumber"
:isSurvey="isSurvey"
:sections="sections"
@select="navigateToQuestion"
/>
<div
Expand All @@ -101,7 +103,7 @@
:class="windowIsSmall ? 'mobile-exercise-container' : ''"
:style="{ backgroundColor: $themeTokens.surface }"
>
<h3>{{ coreString('questionNumberLabel', { questionNumber: questionNumber + 1 }) }}</h3>
<h3>{{ questionNumberInSectionLabel }}</h3>

<div v-if="!isSurvey" data-test="diff-business">
<KCheckbox
Expand Down Expand Up @@ -253,6 +255,12 @@
type: Function,
required: true,
},
// The exam.question_sources value
sections: {
type: Array,
required: true,
default: () => [],
},
// An array of questions in the format:
// {
// exercise_id: <exercise_id>,
Expand Down Expand Up @@ -315,6 +323,17 @@
};
},
computed: {
questionNumberInSectionLabel() {
for (let iSection = 0; iSection < this.sections.length; iSection++) {
const section = this.sections[iSection];
for (let iQuestion = 0; iQuestion < section.questions.length; iQuestion++) {
if (section.questions[iQuestion].item === this.itemId) {
return this.coreString('questionNumberLabel', { questionNumber: iQuestion + 1 });
}
}
}
return '';
},
attemptLogs() {
if (this.isQuiz || this.isSurvey) {
return this.quizAttempts();
Expand Down
97 changes: 48 additions & 49 deletions kolibri/plugins/coach/assets/src/composables/useQuizCreation.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@ function uuidv4() {
return v4().replace(/-/g, '');
}

const { sectionLabel$ } = enhancedQuizManagementStrings;

function displaySectionTitle(section, index) {
return section.section_title === ''
? sectionLabel$({ sectionNumber: index + 1 })
: section.section_title;
}

/** Validators **/
/* objectSpecs expects every property to be available -- but we don't want to have to make an
* object with every property just to validate it. So we use these functions to validate subsets
Expand Down Expand Up @@ -89,6 +81,7 @@ export default function useQuizCreation() {
// The user has removed all resources from the section, so we can clear all questions too
updates.questions = [];
}

if (resource_pool?.length > 0) {
// The resource_pool is being updated
if (originalResourcePool.length === 0) {
Expand All @@ -103,10 +96,11 @@ export default function useQuizCreation() {
// if there weren't resources in the originalResourcePool before.
// ***
updates.questions = selectRandomQuestionsFromResources(
question_count || originalQuestionCount,
question_count || originalQuestionCount || 0,
resource_pool
);
} else {
// We're updating the resource_pool of a section that already had resources
if (question_count === 0) {
updates.questions = [];
} else {
Expand All @@ -125,7 +119,7 @@ export default function useQuizCreation() {
);
if (removedResourceQuestionIds.length !== 0) {
const questionsToKeep = originalQuestions.filter(
q => !removedResourceQuestionIds.includes(q.id)
q => !removedResourceQuestionIds.includes(q.item)
);
const numReplacementsNeeded =
(question_count || originalQuestionCount) - questionsToKeep.length;
Expand All @@ -136,30 +130,18 @@ export default function useQuizCreation() {
}
}
}
} else if (question_count !== originalQuestionCount) {
/**
* Handle edge cases re: questions and question_count changing. When the question_count
* changes, we remove/add questions to match the new count. If questions are deleted, then
* we will update question_count accordingly.
**/

// If the question count changed AND questions have changed, be sure they're the same length
// or we can add questions to match the new question_count
if (question_count < originalQuestionCount) {
// If the question_count is being reduced, we need to remove any questions that are now
// outside the bounds of the new question_count
updates.questions = originalQuestions.slice(0, question_count);
} else if (question_count > originalQuestionCount) {
// If the question_count is being increased, we need to add new questions to the end of the
// questions array
const numQuestionsToAdd = question_count - originalQuestionCount;
const newQuestions = selectRandomQuestionsFromResources(
numQuestionsToAdd,
originalResourcePool,
originalQuestions.map(q => q.id) // Exclude questions we already have to avoid duplicates
);
updates.questions = [...targetSection.questions, ...newQuestions];
}
}
// The resource pool isn't being updated but the question_count is so we need to update them
if (question_count > originalQuestionCount) {
updates.questions = [
...originalQuestions,
...selectRandomQuestionsFromResources(
question_count - originalQuestionCount,
originalResourcePool
),
];
} else if (question_count < originalQuestionCount) {
updates.questions = originalQuestions.slice(0, question_count);
}

set(_quiz, {
Expand Down Expand Up @@ -208,7 +190,11 @@ export default function useQuizCreation() {
exerciseTitles,
questionIdArrays,
Math.floor(Math.random() * 1000),
excludedIds
[
...excludedIds,
// Always exclude the questions that are already in the entire quiz
...get(allQuestionsInQuiz).map(q => q.item),
]
);
}

Expand Down Expand Up @@ -257,6 +243,7 @@ export default function useQuizCreation() {
* Sets the given section_id as the active section ID, however, if the ID is not found or is null
* it will set the activeId to the first section in _quiz.question_sources */
function setActiveSection(section_id = null) {
set(_selectedQuestionIds, []); // Clear the selected questions when changing sections
set(_activeSectionId, section_id);
}

Expand Down Expand Up @@ -306,11 +293,10 @@ export default function useQuizCreation() {

/**
* @returns {Promise<Quiz>}
* @throws {Error} if quiz is not valid
*/
function saveQuiz() {
if (!validateQuiz(get(_quiz))) {
throw new Error(`Quiz is not valid: ${JSON.stringify(get(_quiz))}`);
return Promise.reject(`Quiz is not valid: ${JSON.stringify(get(_quiz))}`);
}

const id = get(_quiz).id;
Expand Down Expand Up @@ -391,7 +377,7 @@ export default function useQuizCreation() {
} else {
set(
_selectedQuestionIds,
get(activeQuestions).map(q => q.id)
get(activeQuestions).map(q => q.item)
);
}
}
Expand Down Expand Up @@ -428,6 +414,9 @@ export default function useQuizCreation() {
const activeSection = computed(() =>
get(allSections).find(s => s.section_id === get(_activeSectionId))
);
const activeSectionIndex = computed(() =>
get(allSections).findIndex(s => isEqual(s.section_title === get(activeSection).section_title))
);
/** @type {ComputedRef<QuizSection[]>} The inactive sections */
const inactiveSections = computed(() =>
get(allSections).filter(s => s.section_id !== get(_activeSectionId))
Expand All @@ -445,15 +434,11 @@ export default function useQuizCreation() {
* exercises */
const activeQuestionsPool = computed(() => {
const pool = get(activeResourcePool);
const numQuestions = pool.reduce(
(count, r) => count + r.assessmentmetadata.assessment_item_ids.length,
0
);
const exerciseIds = pool.map(r => r.exercise_id);
const exerciseTitles = pool.map(r => r.title);
const questionIdArrays = pool.map(r => r.unique_question_ids);
return selectQuestions(
numQuestions,
pool.reduce((acc, r) => acc + r.assessmentmetadata.assessment_item_ids.length, 0),
exerciseIds,
exerciseTitles,
questionIdArrays,
Expand All @@ -468,12 +453,20 @@ export default function useQuizCreation() {
/** @type {ComputedRef<QuizQuestion[]>} Questions in the active section's `resource_pool` that
* are not in `questions` */
const replacementQuestionPool = computed(() => {
const activeQuestionIds = get(activeQuestions).map(q => q.id);
return get(activeQuestionsPool).filter(q => !activeQuestionIds.includes(q.id));
const excludedQuestions = get(allQuestionsInQuiz).map(q => q.item);
return get(activeQuestionsPool).filter(q => !excludedQuestions.includes(q.item));
});
/** @type {ComputedRef<Array>} A list of all channels available which have exercises */
const channels = computed(() => get(_channels));

/** @type {ComputedRef<Array<QuizQuestion>>} A list of all questions in the quiz */
const allQuestionsInQuiz = computed(() => {
return get(allSections).reduce((acc, section) => {
acc = [...acc, ...section.questions];
return acc;
}, []);
});

/** Handling the Select All Checkbox
* See: remove/toggleQuestionFromSelection() & selectAllQuestions() for more */

Expand All @@ -484,7 +477,7 @@ export default function useQuizCreation() {
isEqual(
get(selectedActiveQuestions).sort(),
get(activeQuestions)
.map(q => q.id)
.map(q => q.item)
.sort()
)
);
Expand All @@ -500,7 +493,7 @@ export default function useQuizCreation() {
function deleteActiveSelectedQuestions() {
const { section_id, questions: section_questions } = get(activeSection);
const selectedIds = get(selectedActiveQuestions);
const questions = section_questions.filter(q => !selectedIds.includes(q.id));
const questions = section_questions.filter(q => !selectedIds.includes(q.item));
const question_count = questions.length;
updateSection({
section_id,
Expand Down Expand Up @@ -528,6 +521,7 @@ export default function useQuizCreation() {
return !get(allQuestionsSelected) && !get(noQuestionsSelected);
});

provide('allQuestionsInQuiz', allQuestionsInQuiz);
provide('updateSection', updateSection);
provide('handleReplacement', handleReplacement);
provide('replaceSelectedQuestions', replaceSelectedQuestions);
Expand All @@ -542,6 +536,7 @@ export default function useQuizCreation() {
provide('replacements', replacements);
provide('allSections', allSections);
provide('activeSection', activeSection);
provide('activeSectionIndex', activeSectionIndex);
provide('inactiveSections', inactiveSections);
provide('activeResourcePool', activeResourcePool);
provide('activeResourceMap', activeResourceMap);
Expand Down Expand Up @@ -569,13 +564,13 @@ export default function useQuizCreation() {
clearSelectedQuestions,
addQuestionToSelection,
removeQuestionFromSelection,
displaySectionTitle,

// Computed
channels,
replacements,
quiz,
allSections,
activeSectionIndex,
activeSection,
inactiveSections,
activeResourcePool,
Expand All @@ -589,10 +584,12 @@ export default function useQuizCreation() {
allSectionsEmpty,
allQuestionsSelected,
noQuestionsSelected,
allQuestionsInQuiz,
};
}

export function injectQuizCreation() {
const allQuestionsInQuiz = inject('allQuestionsInQuiz');
const updateSection = inject('updateSection');
const handleReplacement = inject('handleReplacement');
const replaceSelectedQuestions = inject('replaceSelectedQuestions');
Expand All @@ -607,6 +604,7 @@ export function injectQuizCreation() {
const replacements = inject('replacements');
const allSections = inject('allSections');
const activeSection = inject('activeSection');
const activeSectionIndex = inject('activeSectionIndex');
const inactiveSections = inject('inactiveSections');
const activeResourcePool = inject('activeResourcePool');
const activeResourceMap = inject('activeResourceMap');
Expand Down Expand Up @@ -635,15 +633,16 @@ export function injectQuizCreation() {
addQuestionToSelection,
removeQuestionFromSelection,
toggleQuestionInSelection,
displaySectionTitle,

// Computed
allQuestionsSelected,
allQuestionsInQuiz,
selectAllIsIndeterminate,
channels,
replacements,
allSections,
activeSection,
activeSectionIndex,
inactiveSections,
activeResourcePool,
activeResourceMap,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,9 @@ export default {
return function finder({ title, excludeId }) {
return find(getters.exams, exam => {
// Coerce ids to same data type before comparing
String(exam.id) !== String(excludeId) && normalize(exam.title) === normalize(title);
return (
String(exam.id) !== String(excludeId) && normalize(exam.title) === normalize(title)
);
});
};
},
Expand Down
1 change: 1 addition & 0 deletions kolibri/plugins/coach/assets/src/utils/selectQuestions.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export default function selectQuestions(
question_id: uId.split(':')[1],
// TODO See #12127 re: replacing all `id` with `item`
id: uId,
item: uId,
title: exerciseTitles[ri],
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
:exerciseContentNodes="exerciseContentNodes"
:navigateTo="navigateTo"
:questions="questions"
:sections="exam.question_sources"
/>
</KPageContainer>

Expand Down
Loading