Skip to content

Commit 1ef0b57

Browse files
authored
Merge pull request #11434 from nucleogenesis/feature--quizrootpolish
Finishing up the Quiz Root Page
2 parents 170b74f + 1a65873 commit 1ef0b57

File tree

11 files changed

+581
-170
lines changed

11 files changed

+581
-170
lines changed

kolibri/core/assets/src/views/sortable/DragContainer.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
handleStart() {
5757
// handle cancelation of drags
5858
// document.addEventListener('keyup', this.triggerMouseUpOnESC);
59+
this.$emit('dragStart');
5960
},
6061
handleStop(event) {
6162
const { oldIndex, newIndex } = event.data;

kolibri/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"js-cookie": "^3.0.5",
2222
"knuth-shuffle-seeded": "^1.0.6",
2323
"kolibri-constants": "0.2.0",
24-
"kolibri-design-system": "https://github.com/learningequality/kolibri-design-system#v2.0.0-beta1",
24+
"kolibri-design-system": "https://github.com/learningequality/kolibri-design-system#0ed2f274b1bc3808218a4d3f526c181b96b32c6d",
2525
"lockr": "0.8.5",
2626
"lodash": "^4.17.21",
2727
"loglevel": "^1.8.1",

kolibri/plugins/coach/assets/src/composables/useQuizCreation.js

Lines changed: 124 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { v4 as uuidv4 } from 'uuid';
2+
import isEqual from 'lodash/isEqual';
23
import { enhancedQuizManagementStrings } from 'kolibri-common/strings/enhancedQuizManagementStrings';
34
import uniq from 'lodash/uniq';
45
import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
@@ -8,7 +9,7 @@ import { get, set } from '@vueuse/core';
89
import { computed, ref } from 'kolibri.lib.vueCompositionApi';
910
// TODO: Probably move this to this file's local dir
1011
import selectQuestions from '../modules/examCreation/selectQuestions.js';
11-
import { Quiz, QuizSection } from './quizCreationSpecs.js';
12+
import { Quiz, QuizSection, QuizQuestion } from './quizCreationSpecs.js';
1213

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

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

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

5354
/** @type {ref<Number>} A counter for use in naming new sections */
5455
const _sectionLabelCounter = ref(1);
5556

57+
//--
58+
// Debug Data Generators
59+
//--
60+
function _quizQuestions(num = 5) {
61+
const questions = [];
62+
for (let i = 0; i <= num; i++) {
63+
const overrides = {
64+
title: `Quiz Question ${i}`,
65+
question_id: uuidv4(),
66+
};
67+
questions.push(objectWithDefaults(overrides, QuizQuestion));
68+
}
69+
return questions;
70+
}
71+
72+
function _quizSections(num = 5, numQuestions = 5) {
73+
const sections = [];
74+
for (let i = 0; i <= num; i++) {
75+
const overrides = {
76+
section_id: uuidv4(),
77+
section_title: `Test section ${i}`,
78+
questions: _quizQuestions(numQuestions),
79+
};
80+
sections.push(objectWithDefaults(overrides, QuizSection));
81+
}
82+
return sections;
83+
}
84+
85+
function _generateTestData(numSections = 5, numQuestions = 5) {
86+
const sections = _quizSections(numSections, numQuestions);
87+
updateQuiz({ question_sources: sections });
88+
setActiveSection(sections[0].section_id);
89+
}
90+
5691
// ------------------
5792
// Section Management
5893
// ------------------
@@ -103,10 +138,10 @@ export default () => {
103138
/**
104139
* @param {QuizQuestion[]} newQuestions
105140
* @affects _quiz - Updates the active section's `questions` property
106-
* @affects _selectedQuestions - Clears this back to an empty array
141+
* @affects _selectedQuestionIds - Clears this back to an empty array
107142
* @throws {TypeError} if newQuestions is not a valid array of QuizQuestions
108143
* Updates the active section's `questions` property with the given newQuestions, and clears
109-
* _selectedQuestions from it. Then it resets _selectedQuestions to an empty array */
144+
* _selectedQuestionIds from it. Then it resets _selectedQuestionIds to an empty array */
110145
// TODO WRITE THIS FUNCTION
111146
function replaceSelectedQuestions(newQuestions) {
112147
return newQuestions;
@@ -162,8 +197,12 @@ export default () => {
162197
* use */
163198
function initializeQuiz() {
164199
set(_quiz, objectWithDefaults({}, Quiz));
165-
const newSection = addSection();
166-
setActiveSection(newSection.section_id);
200+
if (DEBUG) {
201+
_generateTestData();
202+
} else {
203+
const newSection = addSection();
204+
setActiveSection(newSection.section_id);
205+
}
167206
_fetchChannels();
168207
}
169208

@@ -195,21 +234,41 @@ export default () => {
195234
// --------------------------------
196235

197236
/** @param {QuizQuestion} question
198-
* @affects _selectedQuestions - Adds question to _selectedQuestions if it isn't there already */
237+
* @affects _selectedQuestionIds - Adds question to _selectedQuestionIds if it isn't
238+
* there already */
199239
function addQuestionToSelection(question_id) {
200-
set(_selectedQuestions, uniq([...get(_selectedQuestions), question_id]));
240+
set(_selectedQuestionIds, uniq([...get(_selectedQuestionIds), question_id]));
201241
}
202242

203243
/**
204244
* @param {QuizQuestion} question
205-
* @affects _selectedQuestions - Removes question from _selectedQuestions if it is there */
245+
* @affects _selectedQuestionIds - Removes question from _selectedQuestionIds if it is there */
206246
function removeQuestionFromSelection(question_id) {
207247
set(
208-
_selectedQuestions,
209-
get(_selectedQuestions).filter(id => id !== question_id)
248+
_selectedQuestionIds,
249+
get(_selectedQuestionIds).filter(id => id !== question_id)
210250
);
211251
}
212252

253+
function toggleQuestionInSelection(question_id) {
254+
if (get(_selectedQuestionIds).includes(question_id)) {
255+
removeQuestionFromSelection(question_id);
256+
} else {
257+
addQuestionToSelection(question_id);
258+
}
259+
}
260+
261+
function selectAllQuestions() {
262+
if (get(allQuestionsSelected)) {
263+
set(_selectedQuestionIds, []);
264+
} else {
265+
set(
266+
_selectedQuestionIds,
267+
get(activeQuestions).map(q => q.question_id)
268+
);
269+
}
270+
}
271+
213272
/**
214273
* @affects _channels - Fetches all channels with exercises and sets them to _channels */
215274
function _fetchChannels() {
@@ -271,15 +330,56 @@ export default () => {
271330
/** @type {ComputedRef<QuizQuestion[]>} All questions in the active section's `questions` property
272331
* those which are currently set to be used in the section */
273332
const activeQuestions = computed(() => get(activeSection).questions);
274-
/** @type {ComputedRef<QuizQuestion[]>} All questions the user has selected for the active
275-
* section */
276-
const selectedActiveQuestions = computed(() => get(_selectedQuestions));
333+
/** @type {ComputedRef<String[]>} All question_ids the user has selected for the active section */
334+
const selectedActiveQuestions = computed(() => get(_selectedQuestionIds));
277335
/** @type {ComputedRef<QuizQuestion[]>} Questions in the active section's `resource_pool` that
278336
* are not in `questions` */
279337
const replacementQuestionPool = computed(() => {});
280338
/** @type {ComputedRef<Array>} A list of all channels available which have exercises */
281339
const channels = computed(() => get(_channels));
282340

341+
/** Handling the Select All Checkbox
342+
* See: remove/toggleQuestionFromSelection() & selectAllQuestions() for more */
343+
344+
/** @type {ComputedRef<Boolean>} Whether all active questions are selected */
345+
const allQuestionsSelected = computed(() => {
346+
return isEqual(
347+
get(selectedActiveQuestions).sort(),
348+
get(activeQuestions)
349+
.map(q => q.question_id)
350+
.sort()
351+
);
352+
});
353+
354+
/**
355+
* Deletes and clears the selected questions from the active section
356+
*/
357+
function deleteActiveSelectedQuestions() {
358+
const { section_id, questions } = get(activeSection);
359+
const selectedIds = get(selectedActiveQuestions);
360+
const newQuestions = questions.filter(q => !selectedIds.includes(q.question_id));
361+
updateSection({ section_id, questions: newQuestions });
362+
set(_selectedQuestionIds, []);
363+
}
364+
365+
const noQuestionsSelected = computed(() => get(selectedActiveQuestions).length === 0);
366+
/** @type {ComputedRef<String>} The label that should be shown alongside the "Select all" checkbox
367+
*/
368+
const selectAllLabel = computed(() => {
369+
if (get(noQuestionsSelected)) {
370+
const { selectAllLabel$ } = enhancedQuizManagementStrings;
371+
return selectAllLabel$();
372+
} else {
373+
const { numberOfSelectedQuestions$ } = enhancedQuizManagementStrings;
374+
return numberOfSelectedQuestions$({ count: get(selectedActiveQuestions).length });
375+
}
376+
});
377+
378+
/** @type {ComputedRef<Boolean>} Whether the select all checkbox should be indeterminate */
379+
const selectAllIsIndeterminate = computed(() => {
380+
return !get(allQuestionsSelected) && !get(noQuestionsSelected);
381+
});
382+
283383
return {
284384
// Methods
285385
saveQuiz,
@@ -290,8 +390,11 @@ export default () => {
290390
setActiveSection,
291391
initializeQuiz,
292392
updateQuiz,
393+
deleteActiveSelectedQuestions,
293394
addQuestionToSelection,
294395
removeQuestionFromSelection,
396+
toggleQuestionInSelection,
397+
selectAllQuestions,
295398

296399
// Computed
297400
channels,
@@ -304,5 +407,9 @@ export default () => {
304407
activeQuestions,
305408
selectedActiveQuestions,
306409
replacementQuestionPool,
410+
selectAllIsIndeterminate,
411+
selectAllLabel,
412+
allQuestionsSelected,
413+
noQuestionsSelected,
307414
};
308415
};

kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionContainer.vue

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
name="list"
77
class="wrapper"
88
>
9+
<slot
10+
name="top"
11+
:expandAll="expandAll"
12+
:collapseAll="collapseAll"
13+
></slot>
914
<slot
1015
:toggleItemState="toggleItemState"
1116
:isItemExpanded="isItemExpanded"
@@ -26,7 +31,18 @@
2631
expandedItemIds: [],
2732
};
2833
},
34+
watch: {
35+
expandedItemIds() {
36+
this.$emit('toggled', this.expandedItemIds);
37+
},
38+
},
2939
methods: {
40+
expandAll(ids = []) {
41+
this.expandedItemIds = ids;
42+
},
43+
collapseAll() {
44+
this.expandedItemIds = [];
45+
},
3046
toggleItemState(id) {
3147
const index = this.expandedItemIds.indexOf(id);
3248
if (index === -1) {
@@ -43,6 +59,7 @@
4359
const index = this.expandedItemIds.indexOf(id);
4460
this.expandedItemIds.splice(index, 1);
4561
}
62+
this.$emit('toggled', this.expandedItemIds);
4663
},
4764
},
4865
};

kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionItem.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
<slot
1414
:id="id"
1515
name="content"
16-
:answers="title"
1716
>
1817
</slot>
1918
</div>
@@ -32,7 +31,7 @@
3231
required: true,
3332
},
3433
id: {
35-
type: Number,
34+
type: String,
3635
required: true,
3736
},
3837
},

0 commit comments

Comments
 (0)