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

Add score in practice quiz #12564

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
5 changes: 5 additions & 0 deletions kolibri/core/assets/src/mixins/commonCoreStrings.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,11 @@ export const coreStrings = createTranslator('CommonCoreStrings', {
context:
"A coach can view learner progress in Kolibri.\n\nFor example, in the Coach > Reports section under the 'Progress' column they can see how many learners have started a lesson, or if a learner needs help.\n",
},
questionsLeftLabel: {
message: '{remaining, number} questions left',
context:
"Indicates how many questions a learner has left to answer in a quiz. For example, it will show something like '5 questions left' or '1 question left' when a learner is taking a quiz.",
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah - one blocking note here. I missed this was part of the diff in my initial review and thought it was pre-existing when I saw this.

We'll need to use ICU syntax for the word "questions" here which allows translators to translate the various ways of expressing an amount of something. A similar string is defined here in the QuizCard component which demonstrates how this works where we can say "when the value of questionsLeft is one then the word is question otherwise, use questions".

You could probably just copy that string directly into the ProgressBar component - note how you'll add a $tr: {} object to that component, then add the string inside of that object there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! ✅

questionNumberLabel: {
message: 'Question { questionNumber, number }',
context: 'Indicates the question number in a quiz that a learner could be taking.',
Expand Down
44 changes: 42 additions & 2 deletions kolibri/core/content/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
from kolibri.core.lessons.models import Lesson
from kolibri.core.logger.models import ContentSessionLog
from kolibri.core.logger.models import ContentSummaryLog
from kolibri.core.logger.models import MasteryLog
from kolibri.core.query import SQSum
from kolibri.core.utils.pagination import ValuesViewsetCursorPagination
from kolibri.core.utils.pagination import ValuesViewsetLimitOffsetPagination
Expand Down Expand Up @@ -1680,7 +1681,13 @@ class ContentNodeProgressViewset(TreeQueryMixin, BaseValuesViewset, ListModelMix
# that the pagination object generated by the ContentNodeViewset
# will be used to make subsequent page requests.
pagination_class = OptionalPagination
values = ("content_id", "progress")
values = (
"content_id",
"progress",
"num_question_answered",
"num_question_answered_correctly",
"total_questions",
)

def get_queryset(self):
user = self.request.user
Expand All @@ -1707,7 +1714,40 @@ def generate_response(self, request, queryset):
content_id__in=queryset.exclude(kind=content_kinds.TOPIC).values_list(
"content_id", flat=True
),
).values(*self.values)
)
.annotate(
num_question_answered=Subquery(
MasteryLog.objects.filter(
user=self.request.user,
summarylog__content_id=OuterRef("content_id"),
)
.annotate(
num_question_answered=Count("attemptlogs"),
)
.values("num_question_answered")
.order_by("-end_timestamp")[:1]
),
num_question_answered_correctly=Subquery(
MasteryLog.objects.filter(
user=self.request.user,
summarylog__content_id=OuterRef("content_id"),
)
.annotate(
num_question_answered_correctly=Count(
"attemptlogs",
filter=Q(attemptlogs__correct=1),
),
)
.values("num_question_answered_correctly")
.order_by("-end_timestamp")[:1]
),
total_questions=Subquery(
models.ContentNode.objects.filter(
content_id=OuterRef("content_id"),
).values("assessmentmetadata__number_of_assessments")[:1]
),
)
.values(*self.values)
)
return Response(logs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ContentNodeProgressResource } from 'kolibri.resources';

// The reactive is defined in the outer scope so it can be used as a shared store
const contentNodeProgressMap = reactive({});
const contentNodeQuizProgressMap = reactive({});

export function setContentNodeProgress(progress) {
// Avoid setting stale progress data - assume that progress increases monotonically
Expand All @@ -19,6 +20,13 @@ export function setContentNodeProgress(progress) {
progress.progress > contentNodeProgressMap[progress.content_id]
) {
set(contentNodeProgressMap, progress.content_id, progress.progress);
// this should have been conditional
set(contentNodeQuizProgressMap, progress.content_id, {
progress: progress.progress,
num_question_answered: progress.num_question_answered,
num_question_answered_correctly: progress.num_question_answered_correctly,
total_questions: progress.total_questions,
});
}
}

Expand Down Expand Up @@ -69,5 +77,6 @@ export default function useContentNodeProgress() {
fetchContentNodeProgress,
fetchContentNodeTreeProgress,
contentNodeProgressMap,
contentNodeQuizProgressMap,
};
}
67 changes: 58 additions & 9 deletions kolibri/plugins/learn/assets/src/views/ProgressBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,39 @@
:aria-valuenow="progress * 100"
>
<p
v-if="completed"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will add an extraneous <p> tag in the case that it this is not a quiz and not completed. We should retain the v-if but change it to:

v-if="completed || isQuiz"

class="completion-label"
:style="{ color: $themePalette.grey.v_800 }"
>
<ProgressIcon
:progress="progress"
class="completion-icon"
/>
{{ coreString('completedLabel') }}
<template v-if="isQuiz">
<template v-if="progress < 1">
<ProgressIcon
:progress="progress"
class="completion-icon"
/>
{{
coreString('questionsLeftLabel', {
remaining: remainingQuestions,
})
}}
</template>
<template v-else>
<ProgressIcon
:progress="progress"
class="completion-icon"
/>
{{ coreString('scoreLabel') }} {{ $formatNumber(score, { style: 'percent' }) }}
</template>
</template>
<template v-else>
<ProgressIcon
:progress="progress"
class="completion-icon"
/>
{{ coreString('completedLabel') }}
</template>
</p>
<KLinearLoader
v-if="progress && !completed"
v-if="!isQuiz && progress && !completed"
class="k-linear-loader"
:delay="false"
:progress="progress * 100"
Expand All @@ -36,6 +57,7 @@

import ProgressIcon from 'kolibri.coreVue.components.ProgressIcon';
import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
import lodashGet from 'lodash/get';
import useContentNodeProgress from '../composables/useContentNodeProgress';

/**
Expand All @@ -51,8 +73,8 @@
},
mixins: [commonCoreStrings],
setup() {
const { contentNodeProgressMap } = useContentNodeProgress();
return { contentNodeProgressMap };
const { contentNodeProgressMap, contentNodeQuizProgressMap } = useContentNodeProgress();
return { contentNodeProgressMap, contentNodeQuizProgressMap };
},
props: {
// eslint-disable-next-line kolibri/vue-no-unused-properties
Expand All @@ -62,7 +84,34 @@
},
},
computed: {
isQuiz() {
return lodashGet(this.contentNode, ['options', 'modality'], false) === 'QUIZ';
},
score() {
if (this.isQuiz) {
const quizProgress =
this.contentNodeQuizProgressMap[this.contentNode && this.contentNode.content_id];
if (quizProgress.total_questions) {
return quizProgress.num_question_answered_correctly / quizProgress.total_questions;
}
}
},
remainingQuestions() {
if (this.isQuiz) {
const quizProgress =
this.contentNodeQuizProgressMap[this.contentNode && this.contentNode.content_id];
if (quizProgress.total_questions) {
return quizProgress.total_questions - quizProgress.num_question_answered;
}
}
},
progress() {
if (this.isQuiz) {
return (
this.contentNodeQuizProgressMap[this.contentNode && this.contentNode.content_id]
?.progress || 0
);
}
return this.contentNodeProgressMap[this.contentNode && this.contentNode.content_id] || 0;
},
completed() {
Expand Down
Loading