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

Channel based quizzes #8212

Merged
merged 23 commits into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
84c6f6f
Add minimal channel based quizzes querying API.
rtibbles Jun 23, 2021
2cb663e
Add minimal channel based quizzes querying API.
rtibbles Jun 23, 2021
9a18e5e
Update NEW QUIZ button to dropdown menu w/same quiz functionality
sairina Jun 25, 2021
e25a048
Merge remote-tracking branch 'upstream/channel_based_quizzes' into ch…
sairina Jun 25, 2021
793626b
Return full content nodes for all level of hierarchy.
rtibbles Jul 7, 2021
3b04415
Added route for channel quiz selection/creation page
sairina Jun 29, 2021
209b0b4
Complete UI on immersive toolbar and bottom app bar
sairina Jun 29, 2021
e68e352
Update showChannelQuizCreationRootPage channelContentList
sairina Jun 30, 2021
4cdb3f3
Fix fetchCollection params
sairina Jun 30, 2021
22bbf0a
Add route to topic selection
sairina Jul 1, 2021
edcf4a8
Add resource selection above content cards
sairina Jul 1, 2021
90411fa
Update selection metadata and when content is selected
sairina Jul 1, 2021
b154aaf
Add channel quiz preview route - preliminary
sairina Jul 2, 2021
c2b7941
Update channel quiz preview route with new channel quiz preview compo…
sairina Jul 2, 2021
25e3dce
Update LessonContentPreviewPage with channel quizzes UI, all except f…
sairina Jul 2, 2021
83a648f
Update UI up to preview page with alternate flow
sairina Jul 7, 2021
ef36cdc
Move logic for preview page into own component for channel quizzes
sairina Jul 8, 2021
50ac507
Fix handleSubmit and fix adding selectedExercises and selectedQuestio…
sairina Jul 15, 2021
bd2e950
Add logic for displaying quiz duration
sairina Jul 15, 2021
4445211
Add functionality to add multiple copies of channel quizzes from work…
sairina Jul 16, 2021
cbc68a2
Update kolibri/plugins/coach/assets/src/modules/examCreation/actions.js
sairina Jul 16, 2021
5f9aa24
Update UI with channel quiz details for user
sairina Jul 19, 2021
ef3ec7a
Fix learners_see_fixed_order vs randomized question order
sairina Jul 20, 2021
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
15 changes: 15 additions & 0 deletions kolibri/core/content/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ class Meta:
fields = ["ids"]


MODALITIES = set(["QUIZ"])


class ContentNodeFilter(IdFilter):
kind = ChoiceFilter(
method="filter_kind",
Expand All @@ -151,12 +154,15 @@ class ContentNodeFilter(IdFilter):
exclude_content_ids = CharFilter(method="filter_exclude_content_ids")
kind_in = CharFilter(method="filter_kind_in")
parent = UUIDFilter("parent")
parent__isnull = BooleanFilter(field_name="parent", lookup_expr="isnull")
include_coach_content = BooleanFilter(method="filter_include_coach_content")
contains_quiz = CharFilter(method="filter_contains_quiz")

class Meta:
model = models.ContentNode
fields = [
"parent",
"parent__isnull",
"prerequisite_for",
"has_prerequisite",
"related",
Expand All @@ -167,6 +173,7 @@ class Meta:
"kind",
"include_coach_content",
"kind_in",
"contains_quiz",
]

def filter_kind(self, queryset, name, value):
Expand Down Expand Up @@ -200,6 +207,14 @@ def filter_include_coach_content(self, queryset, name, value):
return queryset
return queryset.filter(coach_content=False)

def filter_contains_quiz(self, queryset, name, value):
if value:
quizzes = models.ContentNode.objects.filter(
options__contains='"modality": "QUIZ"'
).get_ancestors(include_self=True)
return queryset.filter(pk__in=quizzes.values_list("pk", flat=True))
return queryset


class OptionalPageNumberPagination(ValuesViewsetPageNumberPagination):
"""
Expand Down
3 changes: 3 additions & 0 deletions kolibri/plugins/coach/assets/src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const PageNames = {
NEW_COACH_PAGES: 'NEW_COACH_PAGES',
EXAMS: 'EXAMS',
EXAM_CREATION_ROOT: 'EXAM_CREATION_ROOT',
EXAM_CREATION_CHANNEL_QUIZ: 'EXAM_CREATION_CHANNEL_QUIZ',
EXAM_CREATION_SELECT_CHANNEL_QUIZ_TOPIC: 'EXAM_CREATION_SELECT_CHANNEL_QUIZ_TOPIC',
EXAM_CREATION_CHANNEL_QUIZ_PREVIEW: 'EXAM_CREATION_CHANNEL_QUIZ_PREVIEW',
EXAM_CREATION_TOPIC: 'EXAM_CREATION_TOPIC',
EXAM_CREATION_PREVIEW: 'EXAM_CREATION_PREVIEW',
EXAM_CREATION_SEARCH: 'EXAM_CREATION_SEARCH',
Expand Down
27 changes: 26 additions & 1 deletion kolibri/plugins/coach/assets/src/modules/examCreation/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function fetchAdditionalSearchResults(store, params) {
});
}

export function createExamAndRoute(store, { classId }) {
export function createChannelQuizAndRoute(store, { classId }) {
const exam = {
collection: classId,
title: store.state.title,
Expand All @@ -98,7 +98,23 @@ export function createExamAndRoute(store, { classId }) {
date_archived: null,
date_activated: null,
};
return createExam(store, exam).then(() => {
return router.push({ name: PageNames.EXAMS });
});
}

export function createExamAndRoute(store, { classId }) {
const exam = {
collection: classId,
title: store.state.title,
seed: store.state.seed,
question_count: store.state.selectedQuestions.length,
question_sources: store.state.selectedQuestions,
assignments: [classId],
learners_see_fixed_order: store.state.learnersSeeFixedOrder,
date_archived: null,
date_activated: null,
};
return createExam(store, exam).then(() => {
return router.push({ name: PageNames.EXAMS });
});
Expand Down Expand Up @@ -218,3 +234,12 @@ export function updateSelectedQuestions(store) {
});
});
}

export function fetchChannelQuizzes(parent = null) {
return ContentNodeResource.fetchCollection({
getParams: {
[parent ? 'parent' : 'parent__isnull']: parent ? parent : true,
contains_quiz: true,
},
});
}
86 changes: 84 additions & 2 deletions kolibri/plugins/coach/assets/src/modules/examCreation/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import { ContentNodeResource, ContentNodeSearchResource, ChannelResource } from
import { assessmentMetaDataState } from 'kolibri.coreVue.vuex.mappers';
import router from 'kolibri.coreVue.router';
import { PageNames } from '../../constants';
import { filterAndAnnotateContentList } from './actions';
import { filterAndAnnotateContentList, fetchChannelQuizzes } from './actions';

function showExamCreationPage(store, params) {
const { contentList, pageName, ancestors = [], searchResults = null } = params;

return store.dispatch('loading').then(() => {
store.commit('examCreation/SET_ANCESTORS', ancestors);
store.commit('examCreation/SET_CONTENT_LIST', contentList);
Expand Down Expand Up @@ -44,7 +43,53 @@ export function showExamCreationRootPage(store, params) {
});
});
}
export function showChannelQuizCreationRootPage(store, params) {
return fetchChannelQuizzes().then(channels => {
const channelContentList = channels.map(channel => ({
...channel,
id: channel.id,
title: channel.title,
kind: ContentNodeKinds.CHANNEL,
is_leaf: false,
}));
store.commit('SET_TOOLBAR_ROUTE', {
name: PageNames.EXAMS,
});
return showExamCreationPage(store, {
classId: params.classId,
contentList: channelContentList,
pageName: PageNames.EXAM_CREATION_CHANNEL_QUIZ,
});
});
}
export function showChannelQuizCreationTopicPage(store, params) {
return store.dispatch('loading').then(() => {
const { topicId } = params;
const topicNodePromise = ContentNodeResource.fetchModel({ id: topicId });
const childNodesPromise = ContentNodeResource.fetchCollection({
getParams: {
parent: topicId,
kind_in: [ContentNodeKinds.TOPIC, ContentNodeKinds.EXERCISE],
},
});
const loadRequirements = [topicNodePromise, childNodesPromise];

return Promise.all(loadRequirements).then(([topicNode, childNodes]) => {
return filterAndAnnotateContentList(childNodes).then(contentList => {
store.commit('SET_TOOLBAR_ROUTE', {
name: PageNames.EXAMS,
});

return showExamCreationPage(store, {
classId: params.classId,
contentList,
pageName: PageNames.EXAM_CREATION_SELECT_CHANNEL_QUIZ_TOPIC,
ancestors: [...topicNode.ancestors, topicNode],
});
});
});
});
}
export function showExamCreationTopicPage(store, params) {
return store.dispatch('loading').then(() => {
const { topicId } = params;
Expand Down Expand Up @@ -104,7 +149,44 @@ export function showExamCreationPreviewPage(store, params, query = {}) {
});
});
}
export function showChannelQuizCreationPreviewPage(store, params) {
const { classId, contentId } = params;
return store.dispatch('loading').then(() => {
return Promise.all([_prepChannelQuizContentPreview(store, classId, contentId)])
.then(([contentNode]) => {
store.commit('SET_TOOLBAR_ROUTE', {
name: PageNames.EXAM_CREATION_SELECT_CHANNEL_QUIZ_TOPIC,
params: {
topicId: contentNode.parent,
},
});
store.dispatch('notLoading');
})
.catch(error => {
store.dispatch('notLoading');
return store.dispatch('handleApiError', error);
});
});
}

function _prepChannelQuizContentPreview(store, classId, contentId) {
return ContentNodeResource.fetchModel({ id: contentId }).then(
contentNode => {
const contentMetadata = assessmentMetaDataState(contentNode);
store.commit('SET_TOOLBAR_ROUTE', {});
store.commit('examCreation/SET_CURRENT_CONTENT_NODE', { ...contentNode });
store.commit('examCreation/SET_PREVIEW_STATE', {
questions: contentMetadata.assessmentIds,
completionData: contentMetadata.masteryModel,
});
store.commit('SET_PAGE_NAME', PageNames.EXAM_CREATION_CHANNEL_QUIZ_PREVIEW);
return contentNode;
},
error => {
return store.dispatch('handleApiError', error);
}
);
}
function _prepExamContentPreview(store, classId, contentId) {
return ContentNodeResource.fetchModel({ id: contentId }).then(
contentNode => {
Expand Down
29 changes: 29 additions & 0 deletions kolibri/plugins/coach/assets/src/routes/planExamRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ import {
showExamCreationSearchPage,
showExamCreationQuestionSelectionPage,
showExamCreationPreviewPage,
showChannelQuizCreationRootPage,
showChannelQuizCreationTopicPage,
showChannelQuizCreationPreviewPage,
} from '../modules/examCreation/handlers';
import CreateChannelQuizPage from '../views/plan/CreateExamPage/CreateChannelQuizPage.vue';
import CreateExamPage from '../views/plan/CreateExamPage';
import CreateExamPreview from '../views/plan/CreateExamPage/CreateExamPreview.vue';
import PlanQuizPreviewPage from '../views/plan/PlanQuizPreviewPage';
import CoachExamsPage from '../views/plan/CoachExamsPage';
import { showExamsPage } from '../modules/examsRoot/handlers';
import QuizSummaryPage from '../views/plan/QuizSummaryPage';
import QuizEditDetailsPage from '../views/plan/QuizEditDetailsPage';
import PlanChannelQuizPreviewPage from '../views/plan/CreateExamPage/PlanChannelQuizPreviewPage';

export default [
{
Expand All @@ -35,6 +40,22 @@ export default [
showExamCreationRootPage(store, toRoute.params);
},
},
{
name: PageNames.EXAM_CREATION_CHANNEL_QUIZ,
path: '/:classId/plan/quizzes/new/channel_quiz',
component: CreateChannelQuizPage,
handler: toRoute => {
showChannelQuizCreationRootPage(store, toRoute.params);
},
},
{
name: PageNames.EXAM_CREATION_SELECT_CHANNEL_QUIZ_TOPIC,
path: '/:classId/plan/quizzes/new/channel_quiz/topic/:topicId',
component: CreateChannelQuizPage,
handler: toRoute => {
showChannelQuizCreationTopicPage(store, toRoute.params);
},
},
{
name: PageNames.EXAM_CREATION_TOPIC,
path: '/:classId/plan/quizzes/new/topic/:topicId',
Expand All @@ -59,6 +80,14 @@ export default [
showExamCreationQuestionSelectionPage(store, toRoute, fromRoute);
},
},
{
name: PageNames.EXAM_CREATION_CHANNEL_QUIZ_PREVIEW,
path: '/:classId/plan/quizzes/new/channel_quiz/preview/',
component: PlanChannelQuizPreviewPage,
handler: toRoute => {
showChannelQuizCreationPreviewPage(store, toRoute.params);
},
},
{
name: PageNames.EXAM_CREATION_PREVIEW,
path: '/:classId/plan/quizzes/new/preview/',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@
while the above <KSelect> is hidden
-->
<div>&nbsp;</div>
<KRouterLink
:primary="true"
appearance="raised-button"
:to="newExamRoute"
:text="coachString('newQuizAction')"
/>
<KButtonGroup>
<KDropdownMenu
appearance="raised-button"
:primary="true"
:text="coachString('newQuizAction')"
:options="dropdownOptions"
class="options-btn"
@select="handleSelect"
/>
</KButtonGroup>
</div>
<CoreTable>
<template #headers>
Expand Down Expand Up @@ -203,8 +207,11 @@
}
return this.sortedExams;
},
newExamRoute() {
return { name: PageNames.EXAM_CREATION_ROOT };
dropdownOptions() {
return [
{ label: this.$tr('newQuiz'), value: 'MAKE_NEW_QUIZ' },
{ label: this.$tr('selectQuiz'), value: 'SELECT_QUIZ' },
];
},
},
methods: {
Expand Down Expand Up @@ -248,11 +255,20 @@
this.$store.dispatch('createSnackbar', this.coachString('quizFailedToCloseMessage'));
});
},
handleSelect({ value }) {
const nextRoute = {
MAKE_NEW_QUIZ: PageNames.EXAM_CREATION_ROOT,
SELECT_QUIZ: PageNames.EXAM_CREATION_CHANNEL_QUIZ,
}[value];
this.$router.push(this.$router.getRoute(nextRoute));
},
},
$trs: {
noExams: 'You do not have any quizzes',
noActiveExams: 'No active quizzes',
noInactiveExams: 'No inactive quizzes',
newQuiz: 'Create new quiz',
selectQuiz: 'Select channel quiz',
},
};

Expand Down
Loading