Skip to content

Commit

Permalink
updates for validations, section management
Browse files Browse the repository at this point in the history
  • Loading branch information
nucleogenesis committed Jun 6, 2024
1 parent cc9fe58 commit 599460b
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 69 deletions.
96 changes: 47 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 @@ -306,11 +292,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 +376,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 +413,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 +433,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 +452,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 +476,7 @@ export default function useQuizCreation() {
isEqual(
get(selectedActiveQuestions).sort(),
get(activeQuestions)
.map(q => q.id)
.map(q => q.item)
.sort()
)
);
Expand All @@ -500,7 +492,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 +520,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 +535,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 +563,13 @@ export default function useQuizCreation() {
clearSelectedQuestions,
addQuestionToSelection,
removeQuestionFromSelection,
displaySectionTitle,

// Computed
channels,
replacements,
quiz,
allSections,
activeSectionIndex,
activeSection,
inactiveSections,
activeResourcePool,
Expand All @@ -589,10 +583,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 +603,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 +632,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
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
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,18 @@
:selectAllChecked="selectAllChecked"
:selectAllIndeterminate="selectAllIndeterminate"
:contentIsChecked="contentPresentInWorkingResourcePool"
:contentHasCheckbox="hasCheckbox"
:contentHasCheckbox="c => hasCheckbox(c) && unusedQuestionsCount(c) > 0"
:contentCardMessage="selectionMetadata"
:contentCardLink="contentLink"
:loadingMoreState="loadingMore"
@changeselectall="handleSelectAll"
@change_content_card="toggleSelected"
@moreresults="fetchMoreResources"
/>
>
<template #notice="{ content }">
<span style="position: absolute; bottom: 1em;">{{ cardNoticeContent(content) }}</span>
</template>
</ContentCardList>

<div class="bottom-navigation">
<KGrid>
Expand Down Expand Up @@ -146,7 +150,12 @@
// to be added to Quiz Section.
const showBookmarks = computed(() => route.value.query.showBookmarks);
const searchQuery = computed(() => route.value.query.search);
const { updateSection, activeResourcePool, selectAllQuestions } = injectQuizCreation();
const {
updateSection,
activeResourcePool,
selectAllQuestions,
allQuestionsInQuiz,
} = injectQuizCreation();
const showCloseConfirmation = ref(false);
const prevRoute = ref({ name: PageNames.EXAM_CREATION_ROOT });
Expand All @@ -163,6 +172,7 @@
cannotSelectSomeTopicWarning$,
closeConfirmationMessage$,
closeConfirmationTitle$,
questionsUnusedInSection$,
} = enhancedQuizManagementStrings;
// TODO let's not use text for this
Expand Down Expand Up @@ -451,6 +461,7 @@
});
return {
allQuestionsInQuiz,
selectAllChecked,
selectAllIndeterminate,
showSelectAll,
Expand Down Expand Up @@ -480,6 +491,7 @@
sectionSettings$,
selectFromBookmarks$,
numberOfSelectedBookmarks$,
questionsUnusedInSection$,
selectFoldersOrExercises$,
numberOfSelectedResources$,
numberOfResourcesSelected$,
Expand Down Expand Up @@ -541,6 +553,28 @@
}
},
methods: {
unusedQuestionsCount(content) {
if (content.kind === ContentNodeKinds.EXERCISE) {
const questionItems = content.assessmentmetadata.assessment_item_ids.map(
aid => `${content.id}:${aid}`
);
const questionsItemsAlreadyUsed = this.allQuestionsInQuiz
.map(q => q.item)
.filter(i => questionItems.includes(i));
const questionItemsAvailable = questionItems.length - questionsItemsAlreadyUsed.length;
return questionItemsAvailable;
}
return -1;
},
cardNoticeContent(content) {
if (content.kind === ContentNodeKinds.EXERCISE) {
return this.questionsUnusedInSection$({
count: this.unusedQuestionsCount(content),
});
} else {
return '';
}
},
showTopicSizeWarningCard(content) {
return !this.hasCheckbox(content) && content.kind === ContentNodeKinds.TOPIC;
},
Expand Down Expand Up @@ -598,6 +632,7 @@
});
this.$store.dispatch('createSnackbar', this.changesSavedSuccessfully$());
},
// The message put onto the content's card when listed
selectionMetadata(content) {
if (content.kind === ContentNodeKinds.TOPIC) {
const total = content.num_exercises;
Expand All @@ -612,8 +647,9 @@
count: numberOfresourcesSelected,
total: total,
});
} else {
// content is an exercise
}
return '';
},
handleSearchTermChange(searchTerm) {
const query = {
Expand Down
Loading

0 comments on commit 599460b

Please sign in to comment.