Skip to content

Commit

Permalink
Merge pull request #11434 from nucleogenesis/feature--quizrootpolish
Browse files Browse the repository at this point in the history
Finishing up the Quiz Root Page
  • Loading branch information
marcellamaki authored Dec 1, 2023
2 parents 170b74f + 1a65873 commit 1ef0b57
Show file tree
Hide file tree
Showing 11 changed files with 581 additions and 170 deletions.
1 change: 1 addition & 0 deletions kolibri/core/assets/src/views/sortable/DragContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
handleStart() {
// handle cancelation of drags
// document.addEventListener('keyup', this.triggerMouseUpOnESC);
this.$emit('dragStart');
},
handleStop(event) {
const { oldIndex, newIndex } = event.data;
Expand Down
2 changes: 1 addition & 1 deletion kolibri/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"js-cookie": "^3.0.5",
"knuth-shuffle-seeded": "^1.0.6",
"kolibri-constants": "0.2.0",
"kolibri-design-system": "https://github.com/learningequality/kolibri-design-system#v2.0.0-beta1",
"kolibri-design-system": "https://github.com/learningequality/kolibri-design-system#0ed2f274b1bc3808218a4d3f526c181b96b32c6d",
"lockr": "0.8.5",
"lodash": "^4.17.21",
"loglevel": "^1.8.1",
Expand Down
141 changes: 124 additions & 17 deletions kolibri/plugins/coach/assets/src/composables/useQuizCreation.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import isEqual from 'lodash/isEqual';
import { enhancedQuizManagementStrings } from 'kolibri-common/strings/enhancedQuizManagementStrings';
import uniq from 'lodash/uniq';
import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
Expand All @@ -8,7 +9,7 @@ import { get, set } from '@vueuse/core';
import { computed, ref } from 'kolibri.lib.vueCompositionApi';
// TODO: Probably move this to this file's local dir
import selectQuestions from '../modules/examCreation/selectQuestions.js';
import { Quiz, QuizSection } from './quizCreationSpecs.js';
import { Quiz, QuizSection, QuizQuestion } from './quizCreationSpecs.js';

/** Validators **/
/* objectSpecs expects every property to be available -- but we don't want to have to make an
Expand All @@ -30,7 +31,7 @@ function isExercise(o) {
/**
* Composable function presenting primary interface for Quiz Creation
*/
export default () => {
export default (DEBUG = false) => {
// -----------
// Local state
// -----------
Expand All @@ -43,16 +44,50 @@ export default () => {
* The section that is currently selected for editing */
const _activeSectionId = ref(null);

/** @type {ref<QuizQuestion[]>}
* The questions that are currently selected for action in the active section */
const _selectedQuestions = ref([]);
/** @type {ref<String[]>}
* The question_ids that are currently selected for action in the active section */
const _selectedQuestionIds = ref([]);

/** @type {ref<Array>} A list of all channels available which have exercises */
const _channels = ref([]);

/** @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}`,
question_id: uuidv4(),
};
questions.push(objectWithDefaults(overrides, QuizQuestion));
}
return questions;
}

function _quizSections(num = 5, numQuestions = 5) {
const sections = [];
for (let i = 0; i <= num; i++) {
const overrides = {
section_id: uuidv4(),
section_title: `Test section ${i}`,
questions: _quizQuestions(numQuestions),
};
sections.push(objectWithDefaults(overrides, QuizSection));
}
return sections;
}

function _generateTestData(numSections = 5, numQuestions = 5) {
const sections = _quizSections(numSections, numQuestions);
updateQuiz({ question_sources: sections });
setActiveSection(sections[0].section_id);
}

// ------------------
// Section Management
// ------------------
Expand Down Expand Up @@ -103,10 +138,10 @@ export default () => {
/**
* @param {QuizQuestion[]} newQuestions
* @affects _quiz - Updates the active section's `questions` property
* @affects _selectedQuestions - Clears this back to an empty array
* @affects _selectedQuestionIds - Clears this back to an empty array
* @throws {TypeError} if newQuestions is not a valid array of QuizQuestions
* Updates the active section's `questions` property with the given newQuestions, and clears
* _selectedQuestions from it. Then it resets _selectedQuestions to an empty array */
* _selectedQuestionIds from it. Then it resets _selectedQuestionIds to an empty array */
// TODO WRITE THIS FUNCTION
function replaceSelectedQuestions(newQuestions) {
return newQuestions;
Expand Down Expand Up @@ -162,8 +197,12 @@ export default () => {
* use */
function initializeQuiz() {
set(_quiz, objectWithDefaults({}, Quiz));
const newSection = addSection();
setActiveSection(newSection.section_id);
if (DEBUG) {
_generateTestData();
} else {
const newSection = addSection();
setActiveSection(newSection.section_id);
}
_fetchChannels();
}

Expand Down Expand Up @@ -195,21 +234,41 @@ export default () => {
// --------------------------------

/** @param {QuizQuestion} question
* @affects _selectedQuestions - Adds question to _selectedQuestions if it isn't there already */
* @affects _selectedQuestionIds - Adds question to _selectedQuestionIds if it isn't
* there already */
function addQuestionToSelection(question_id) {
set(_selectedQuestions, uniq([...get(_selectedQuestions), question_id]));
set(_selectedQuestionIds, uniq([...get(_selectedQuestionIds), question_id]));
}

/**
* @param {QuizQuestion} question
* @affects _selectedQuestions - Removes question from _selectedQuestions if it is there */
* @affects _selectedQuestionIds - Removes question from _selectedQuestionIds if it is there */
function removeQuestionFromSelection(question_id) {
set(
_selectedQuestions,
get(_selectedQuestions).filter(id => id !== question_id)
_selectedQuestionIds,
get(_selectedQuestionIds).filter(id => id !== question_id)
);
}

function toggleQuestionInSelection(question_id) {
if (get(_selectedQuestionIds).includes(question_id)) {
removeQuestionFromSelection(question_id);
} else {
addQuestionToSelection(question_id);
}
}

function selectAllQuestions() {
if (get(allQuestionsSelected)) {
set(_selectedQuestionIds, []);
} else {
set(
_selectedQuestionIds,
get(activeQuestions).map(q => q.question_id)
);
}
}

/**
* @affects _channels - Fetches all channels with exercises and sets them to _channels */
function _fetchChannels() {
Expand Down Expand Up @@ -271,15 +330,56 @@ export default () => {
/** @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);
/** @type {ComputedRef<QuizQuestion[]>} All questions the user has selected for the active
* section */
const selectedActiveQuestions = computed(() => get(_selectedQuestions));
/** @type {ComputedRef<String[]>} All question_ids the user has selected for the active section */
const selectedActiveQuestions = computed(() => get(_selectedQuestionIds));
/** @type {ComputedRef<QuizQuestion[]>} Questions in the active section's `resource_pool` that
* are not in `questions` */
const replacementQuestionPool = computed(() => {});
/** @type {ComputedRef<Array>} A list of all channels available which have exercises */
const channels = computed(() => get(_channels));

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

/** @type {ComputedRef<Boolean>} Whether all active questions are selected */
const allQuestionsSelected = computed(() => {
return isEqual(
get(selectedActiveQuestions).sort(),
get(activeQuestions)
.map(q => q.question_id)
.sort()
);
});

/**
* Deletes and clears the selected questions from the active section
*/
function deleteActiveSelectedQuestions() {
const { section_id, questions } = get(activeSection);
const selectedIds = get(selectedActiveQuestions);
const newQuestions = questions.filter(q => !selectedIds.includes(q.question_id));
updateSection({ section_id, questions: newQuestions });
set(_selectedQuestionIds, []);
}

const noQuestionsSelected = computed(() => get(selectedActiveQuestions).length === 0);
/** @type {ComputedRef<String>} The label that should be shown alongside the "Select all" checkbox
*/
const selectAllLabel = computed(() => {
if (get(noQuestionsSelected)) {
const { selectAllLabel$ } = enhancedQuizManagementStrings;
return selectAllLabel$();
} else {
const { numberOfSelectedQuestions$ } = enhancedQuizManagementStrings;
return numberOfSelectedQuestions$({ count: get(selectedActiveQuestions).length });
}
});

/** @type {ComputedRef<Boolean>} Whether the select all checkbox should be indeterminate */
const selectAllIsIndeterminate = computed(() => {
return !get(allQuestionsSelected) && !get(noQuestionsSelected);
});

return {
// Methods
saveQuiz,
Expand All @@ -290,8 +390,11 @@ export default () => {
setActiveSection,
initializeQuiz,
updateQuiz,
deleteActiveSelectedQuestions,
addQuestionToSelection,
removeQuestionFromSelection,
toggleQuestionInSelection,
selectAllQuestions,

// Computed
channels,
Expand All @@ -304,5 +407,9 @@ export default () => {
activeQuestions,
selectedActiveQuestions,
replacementQuestionPool,
selectAllIsIndeterminate,
selectAllLabel,
allQuestionsSelected,
noQuestionsSelected,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
name="list"
class="wrapper"
>
<slot
name="top"
:expandAll="expandAll"
:collapseAll="collapseAll"
></slot>
<slot
:toggleItemState="toggleItemState"
:isItemExpanded="isItemExpanded"
Expand All @@ -26,7 +31,18 @@
expandedItemIds: [],
};
},
watch: {
expandedItemIds() {
this.$emit('toggled', this.expandedItemIds);
},
},
methods: {
expandAll(ids = []) {
this.expandedItemIds = ids;
},
collapseAll() {
this.expandedItemIds = [];
},
toggleItemState(id) {
const index = this.expandedItemIds.indexOf(id);
if (index === -1) {
Expand All @@ -43,6 +59,7 @@
const index = this.expandedItemIds.indexOf(id);
this.expandedItemIds.splice(index, 1);
}
this.$emit('toggled', this.expandedItemIds);
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
<slot
:id="id"
name="content"
:answers="title"
>
</slot>
</div>
Expand All @@ -32,7 +31,7 @@
required: true,
},
id: {
type: Number,
type: String,
required: true,
},
},
Expand Down
Loading

0 comments on commit 1ef0b57

Please sign in to comment.