Skip to content

Commit

Permalink
Merge pull request learningequality#11662 from nucleogenesis/feature-…
Browse files Browse the repository at this point in the history
…-quiz-debug-data-improvements

Quiz creation DEBUG data improvements
  • Loading branch information
nucleogenesis authored Dec 21, 2023
2 parents 4e17d42 + 0254fb2 commit 439e564
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 57 deletions.
32 changes: 14 additions & 18 deletions kolibri/plugins/coach/assets/src/composables/quizCreationSpecs.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

/**
* @typedef {Object} QuizResource An object referencing an exercise or topic to be used
* @typedef {Object} QuizExercise An object referencing an exercise or topic to be used
* within the `QuizSeciton.resource_pool` property.
* @property {string} title The resource title
* @property {string} ancestor_id The ID of the parent contentnode
Expand All @@ -17,7 +17,7 @@
* @property {string} kind Exercise or Topic in our case - see: `ContentNodeKinds`
*/

export const QuizResource = {
export const QuizExercise = {
title: {
type: String,
default: '',
Expand All @@ -42,19 +42,6 @@ export const QuizResource = {
type: String,
default: '',
},
};

/**
* @typedef {Object} ExerciseResource A particular exercise that can be selected within a
* quiz. An ExerciseResource here is a QuizResource
* with assessment metadata attached.
* @extends {QuizResource}
* @property {Array} assessment_ids A list of assessment item IDs that are associated with
* this exercise
* @property {string} contentnode The contentnode ID for the Assessment
*/
export const ExerciseResource = {
...QuizResource,
assessment_ids: {
type: Array,
default: () => [],
Expand All @@ -67,7 +54,7 @@ export const ExerciseResource = {

/**
* @typedef {Object} QuizQuestion A particular question in a Quiz - aka an assessment item
* from an ExerciseResource.
* from an QuizExercise.
* @property {string} exercise_id The ID of the resource from which the question originates
* @property {string} question_id A *unique* identifier of this particular question within
* the quiz -- same as the `assessment_item_id`
Expand Down Expand Up @@ -110,7 +97,11 @@ export const QuizQuestion = {
* @property {boolean} learners_see_fixed_order A bool flag indicating whether this
* section is shown in the same order, or
* randomized, to the learners
* @property {ExerciseResource[]} resource_pool An array of contentnode ids indicat
* @property {QuizExercise[]} resource_pool An array of QuizExercise objects from
* which the questions in this section_id
* will be drawn
* @property {QuizQuestion[]} question_pool An array of QuizQuestion objects
* derived from the resource_pool
*/
export const QuizSection = {
section_id: {
Expand Down Expand Up @@ -141,7 +132,12 @@ export const QuizSection = {
resource_pool: {
type: Array,
default: () => [],
spec: ExerciseResource,
spec: QuizExercise,
},
question_pool: {
type: Array,
default: () => [],
spec: QuizQuestion,
},
};

Expand Down
131 changes: 98 additions & 33 deletions kolibri/plugins/coach/assets/src/composables/useQuizCreation.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { v4 as uuidv4 } from 'uuid';
import isEqual from 'lodash/isEqual';
import range from 'lodash/range';
import shuffle from 'lodash/shuffle';
import { enhancedQuizManagementStrings } from 'kolibri-common/strings/enhancedQuizManagementStrings';
import uniq from 'lodash/uniq';
import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
Expand All @@ -9,7 +11,7 @@ import { get, set } from '@vueuse/core';
import { computed, ref, provide, inject } from 'kolibri.lib.vueCompositionApi';
// TODO: Probably move this to this file's local dir
import selectQuestions from '../modules/examCreation/selectQuestions.js';
import { Quiz, QuizSection, QuizQuestion } from './quizCreationSpecs.js';
import { Quiz, QuizSection, QuizQuestion, QuizExercise } from './quizCreationSpecs.js';

/** Validators **/
/* objectSpecs expects every property to be available -- but we don't want to have to make an
Expand All @@ -21,8 +23,8 @@ function validateQuiz(quiz) {
}

/**
* @param {QuizResource} o - The resource to check
* @returns {boolean} - True if the resource is a valid QuizResource
* @param {QuizExercise} o - The resource to check
* @returns {boolean} - True if the resource is a valid QuizExercise
*/
function isExercise(o) {
return o.kind === ContentNodeKinds.EXERCISE;
Expand Down Expand Up @@ -54,40 +56,94 @@ export default function useQuizCreation(DEBUG = false) {
/** @type {ref<Number>} A counter for use in naming new sections */
const _sectionLabelCounter = ref(1);

//--
// Debug Data Generators
//--
function _quizQuestions(num = 5) {
const questions = [];
for (let i = 0; i <= num; i++) {
const overrides = {
title: `Quiz Question ${i}`,
/**
* DEBUG Data
*
* Generates a test quiz with multiple sections. It generates properly shaped QuizExercise type
* and QuizQuestion type objects, but the content is not real.
*
* This should be suitable for all UI testing purposes EXCEPT for resource selection.
* DO NOT use this if you're testing resource selection or want to use real resources.
*/
function _generateTestData() {
if (process.env.NODE_ENV === 'production') {
console.error("You're trying to generate test data in production. Please set DEBUG = false.");
}
/**
* @type {QuizQuestion[]} - dummyQuestions
* Typically this data would be fetched and usable from the useExerciseResources module.
*/
const dummyQuestions = range(1, 100).map(i => {
const questionOverrides = {
exercise_id: uuidv4(),
question_id: uuidv4(),
title: `Question ${i}`,
counter_in_exercise: i,
missing_resource: false,
};
questions.push(objectWithDefaults(overrides, QuizQuestion));
}
return questions;
}
return objectWithDefaults(questionOverrides, QuizQuestion);
});

function _quizSections(num = 5, numQuestions = 5) {
const sections = [];
for (let i = 0; i <= num; i++) {
const overrides = {
// Create some resources that we can put into the section resource_pool arrays
const resources = range(1, 10).map(i => {
// Get a random set of questions to put in this resource -- note here that we're only
// getting the QuizQuestion.question_id, which is what we'll get from the API when fetching
// ContentNodes which are Exercises
const sliceOfQuestions = shuffle(dummyQuestions).splice(0, 5);
const resourceOverrides = {
title: `Resource ${i}`,
content_id: uuidv4(),
kind: ContentNodeKinds.EXERCISE,
is_leaf: true,
id: uuidv4(),
assessment_ids: sliceOfQuestions.map(q => q.question_id),
contentnode: uuidv4(),
};
return objectWithDefaults(resourceOverrides, QuizExercise);
});

const sections = range(1, 5).map(i => {
const resource_pool = shuffle(resources).slice(0, 3);
// We'll reduce the resource_pool down to a list of QuizQuestion typed objects in order to
// imitate what we'll otherwise get from a separate module which will handle the API calls
// Typically the question_pool will be set whenever the resource_pool changes
const question_pool = resource_pool.reduce((acc, resource) => {
acc = [
...acc,
// It may not be immediately clear, but this is where we're getting the QuizQuestion objs
...dummyQuestions.filter(q => resource.assessment_ids.includes(q.question_id)),
];
return acc;
}, []);

// These will be the questions that are currently "in the section" -- that is, the questions
// which could possibly be deleted from the section (which will affect the question_count)
// or replaced with other questions from the question_pool
const questions = question_pool.slice(0, 5);

const sectionOverrides = {
section_id: uuidv4(),
section_title: `Test section ${i}`,
questions: _quizQuestions(numQuestions),
section_title: `Section ${i}`,
description: `Section ${i} description`,
question_count: questions.length,
questions,
resource_pool,
question_pool,
};
sections.push(objectWithDefaults(overrides, QuizSection));
}
return sections;
}

function _generateTestData(numSections = 5, numQuestions = 5) {
const sections = _quizSections(numSections, numQuestions);
return objectWithDefaults(sectionOverrides, QuizSection);
});

/* eslint-disable no-console */
console.log('Generated DEBUG dummyQuestions', dummyQuestions);
console.log('Generated DEBUG resources', resources);
console.log('Generated DEBUG sections', sections);
/* eslint-enable */

// Now we're committing this all ot the _quiz ref from which reactive properties will derive
updateQuiz({ question_sources: sections });
setActiveSection(sections[0].section_id);
}

// ------------------
// Section Management
// ------------------
Expand Down Expand Up @@ -295,6 +351,7 @@ export default function useQuizCreation(DEBUG = false) {
* @params {string} section_id - The section_id whose resource_pool we'll use.
* @returns {QuizQuestion[]}
*/
/*
function _getQuestionsFromSection(section_id) {
const section = get(allSections).find(s => s.section_id === section_id);
if (!section) {
Expand All @@ -304,6 +361,7 @@ export default function useQuizCreation(DEBUG = false) {
return [...acc, ...exercise.questions];
}, []);
}
*/

// Computed properties
/** @type {ComputedRef<Quiz>} The value of _quiz */
Expand All @@ -318,15 +376,15 @@ export default function useQuizCreation(DEBUG = false) {
const inactiveSections = computed(() =>
get(allSections).filter(s => s.section_id !== get(_activeSectionId))
);
/** @type {ComputedRef<QuizResource[]>} The active section's `resource_pool` */
/** @type {ComputedRef<QuizExercise[]>} The active section's `resource_pool` */
const activeResourcePool = computed(() => get(activeSection).resource_pool);
/** @type {ComputedRef<ExerciseResource[]>} The active section's `resource_pool` - that is,
* Exercises from which we will enumerate all
* available questions */
const activeExercisePool = computed(() => get(activeResourcePool).filter(isExercise));
/** @type {ComputedRef<QuizQuestion[]>} All questions in the active section's `resource_pool`
* exercises */
const activeQuestionsPool = computed(() => _getQuestionsFromSection(get(_activeSectionId)));
const activeQuestionsPool = computed(() => []);
/** @type {ComputedRef<QuizQuestion[]>} All questions in the active section's `questions` property
* those which are currently set to be used in the section */
const activeQuestions = computed(() => get(activeSection).questions);
Expand Down Expand Up @@ -400,6 +458,10 @@ export default function useQuizCreation(DEBUG = false) {
provide('activeQuestions', activeQuestions);
provide('selectedActiveQuestions', selectedActiveQuestions);
provide('replacementQuestionPool', replacementQuestionPool);
provide('selectAllQuestions', selectAllQuestions);
provide('deleteActiveSelectedQuestions', deleteActiveSelectedQuestions);
provide('toggleQuestionInSelection', toggleQuestionInSelection);

return {
// Methods
saveQuiz,
Expand All @@ -410,11 +472,8 @@ export default function useQuizCreation(DEBUG = false) {
setActiveSection,
initializeQuiz,
updateQuiz,
deleteActiveSelectedQuestions,
addQuestionToSelection,
removeQuestionFromSelection,
toggleQuestionInSelection,
selectAllQuestions,

// Computed
channels,
Expand Down Expand Up @@ -464,10 +523,15 @@ export function injectQuizCreation() {
const activeQuestions = inject('activeQuestions');
const selectedActiveQuestions = inject('selectedActiveQuestions');
const replacementQuestionPool = inject('replacementQuestionPool');
const selectAllQuestions = inject('selectAllQuestions');
const deleteActiveSelectedQuestions = inject('deleteActiveSelectedQuestions');
const toggleQuestionInSelection = inject('toggleQuestionInSelection');

return {
// Methods
saveQuiz,
deleteActiveSelectedQuestions,
selectAllQuestions,
updateSection,
replaceSelectedQuestions,
addSection,
Expand All @@ -477,6 +541,7 @@ export function injectQuizCreation() {
updateQuiz,
addQuestionToSelection,
removeQuestionFromSelection,
toggleQuestionInSelection,

// Computed
channels,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
updateQuiz,
addQuestionToSelection,
removeQuestionFromSelection,
selectAllQuestions,
// Computed
channels,
Expand Down Expand Up @@ -403,6 +404,7 @@
replaceAction$,
questionList$,
selectAllQuestions,
saveQuiz,
updateSection,
allQuestionsSelected,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</UiAlert>

<KPageContainer
:style="{ ...maxContainerHeight, maxWidth: '1000px', margin: '0 auto' }"
:style="{ maxWidth: '1000px', margin: '0 auto 2em' }"
>

<CreateQuizSection v-if="quizInitialized" />
Expand Down Expand Up @@ -84,9 +84,6 @@
};
},
computed: {
maxContainerHeight() {
return { maxHeight: '1000px' };
},
backRoute() {
return { name: PageNames.EXAMS };
},
Expand Down
4 changes: 2 additions & 2 deletions kolibri/plugins/coach/assets/test/useQuizCreation.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { get } from '@vueuse/core';
import { ChannelResource, ExamResource } from 'kolibri.resources';
import { objectWithDefaults } from 'kolibri.utils.objectSpecs';
import { ExerciseResource, QuizQuestion } from '../src/composables/quizCreationSpecs.js';
import { QuizExercise, QuizQuestion } from '../src/composables/quizCreationSpecs.js';
import useQuizCreation from '../src/composables/useQuizCreation.js';

const {
Expand Down Expand Up @@ -50,7 +50,7 @@ function generateQuestions(num = 0) {
* A helper function to mock an exercise with a given number of questions (for `resource_pool`)
*/
function generateExercise(numQuestions) {
const exercise = objectWithDefaults({ resource_id: 'exercise_1' }, ExerciseResource);
const exercise = objectWithDefaults({ resource_id: 'exercise_1' }, QuizExercise);
exercise.questions = generateQuestions(numQuestions);
return exercise;
}
Expand Down

0 comments on commit 439e564

Please sign in to comment.