Skip to content

Commit

Permalink
Merge pull request #13064 from LianaHarris360/quiz-report-visibility-…
Browse files Browse the repository at this point in the history
…control

Add quiz report visibility control for coaches
  • Loading branch information
marcellamaki authored Feb 27, 2025
2 parents 91fcbcb + 926edc0 commit 907f31d
Show file tree
Hide file tree
Showing 15 changed files with 259 additions and 12 deletions.
8 changes: 7 additions & 1 deletion kolibri/core/exams/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class ExamViewset(ValuesViewset):
"creator",
"data_model_version",
"learners_see_fixed_order",
"instant_report_visibility",
"date_created",
)

Expand All @@ -82,7 +83,12 @@ class ExamViewset(ValuesViewset):

draft_values = common_values + ("assignments", "learner_ids")

field_map = {"assignments": "assignment_collections"}
field_map = {
"assignments": "assignment_collections",
"instant_report_visibility": lambda x: True
if x["instant_report_visibility"] is None
else x["instant_report_visibility"],
}

def get_draft_queryset(self):
return models.DraftExam.objects.all()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 3.2.25 on 2025-02-20 17:37
from django.db import migrations
from django.db import models


class Migration(migrations.Migration):

dependencies = [
("exams", "0009_alter_exam_date_created"),
]

operations = [
migrations.AddField(
model_name="draftexam",
name="instant_report_visibility",
field=models.BooleanField(null=True),
),
migrations.AddField(
model_name="exam",
name="instant_report_visibility",
field=models.BooleanField(null=True),
),
migrations.AlterField(
model_name="draftexam",
name="instant_report_visibility",
field=models.BooleanField(default=True, null=True),
),
migrations.AlterField(
model_name="exam",
name="instant_report_visibility",
field=models.BooleanField(default=True, null=True),
),
]
5 changes: 5 additions & 0 deletions kolibri/core/exams/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ class Meta:
"""
data_model_version = models.SmallIntegerField(default=3)

# If True, learners have instant access to exam reports after submission.
# Otherwise, reports are visible only after the coach ends the exam.
instant_report_visibility = models.BooleanField(null=True, default=True)

def __str__(self):
return self.title

Expand Down Expand Up @@ -209,6 +213,7 @@ def to_exam(self):
collection=self.collection,
creator=self.creator,
data_model_version=self.data_model_version,
instant_report_visibility=self.instant_report_visibility,
date_created=self.date_created,
)
return exam
Expand Down
5 changes: 5 additions & 0 deletions kolibri/core/exams/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class Meta:
"archive",
"assignments",
"learners_see_fixed_order",
"instant_report_visibility",
"learner_ids",
"draft",
)
Expand Down Expand Up @@ -270,6 +271,10 @@ def update(self, instance, validated_data): # noqa
instance.learners_see_fixed_order = validated_data.pop(
"learners_see_fixed_order", instance.learners_see_fixed_order
)
instance.instant_report_visibility = validated_data.pop(
"instant_report_visibility",
instance.instant_report_visibility,
)
if not instance_is_draft:
# Update the non-draft specific fields
instance.active = validated_data.pop("active", instance.active)
Expand Down
12 changes: 12 additions & 0 deletions kolibri/core/exams/test/test_exam_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def setUpTestData(cls):
}
],
"learners_see_fixed_order": False,
"instant_report_visibility": True,
}
],
)
Expand All @@ -98,6 +99,7 @@ def make_basic_exam(self):
"draft": self.draft,
"collection": self.classroom.id,
"learners_see_fixed_order": False,
"instant_report_visibility": True,
"question_sources": sections,
"assignments": [],
}
Expand Down Expand Up @@ -421,6 +423,7 @@ def test_retrieve_exam(self):
"creator",
"data_model_version",
"learners_see_fixed_order",
"instant_report_visibility",
"date_created",
]:
self.assertIn(field, response.data)
Expand All @@ -433,6 +436,7 @@ def test_post_exam_v2_model_fails(self):
"active": True,
"collection": self.classroom.id,
"learners_see_fixed_order": False,
"instant_report_visibility": True,
"question_sources": [],
"assignments": [],
"date_activated": None,
Expand Down Expand Up @@ -503,6 +507,14 @@ def test_admin_can_update_learner_sees_fixed_order(self):
self.assertEqual(response.status_code, 200)
self.assertExamExists(id=self.exam.id, learners_see_fixed_order=True)

def test_admin_can_update_instant_report_visibility(self):
self.login_as_admin()
response = self.patch_updated_exam(
self.exam.id, {"instant_report_visibility": False}
)
self.assertEqual(response.status_code, 200)
self.assertExamExists(id=self.exam.id, instant_report_visibility=False)


class ExamAPITestCase(BaseExamTest, APITestCase):
class_object = models.Exam
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,9 @@ export const Quiz = {
type: Boolean,
default: true,
},
// Default to quiz reports being visible immediately after learner submits quiz
instant_report_visibility: {
type: Boolean,
default: true,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const fieldsToSave = [
'learner_ids',
'collection',
'learners_see_fixed_order',
'instant_report_visibility',
'draft',
'active',
'archive',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function examState(exam) {
questionSources: exam.question_sources,
assignments: exam.assignments,
learnersSeeFixedOrder: exam.learners_see_fixed_order,
instantReportVisibility: exam.instant_report_visibility,
dataModelVersion: exam.data_model_version,
seed: exam.seed,
};
Expand Down
29 changes: 28 additions & 1 deletion kolibri/plugins/coach/assets/src/views/common/QuizStatus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
<span> {{ $tr('reportVisibleToLearnersLabel') }} </span>
<StatusElapsedTime
v-if="exam.active"
:date="examDateOpened"
:date="exam.instant_report_visibility ? examDateOpened : examDateArchived"
actionType="madeVisible"
style="font-weight: normal"
/>
Expand Down Expand Up @@ -158,6 +158,28 @@
</KGridItem>
</div>

<!-- Report Visibility -->
<div
v-if="!exam.archive"
class="status-item"
>
<KGridItem
class="status-label"
:layout4="{ span: 4 }"
:layout8="{ span: 4 }"
:layout12="layout12Label"
>
<span>{{ coachString('reportVisibilityLabel') }}</span>
</KGridItem>
<KGridItem
:layout4="{ span: 4 }"
:layout8="{ span: 4 }"
:layout12="layout12Value"
>
<span>{{ reportVisibilityStatus }}</span>
</KGridItem>
</div>

<!-- Class name -->
<div class="status-item">
<KGridItem
Expand Down Expand Up @@ -394,6 +416,11 @@
return null;
}
},
reportVisibilityStatus() {
return this.exam.instant_report_visibility
? this.coachString('afterLearnerSubmitsQuizLabel')
: this.coachString('afterCoachEndsQuizLabel');
},
layout12Label() {
return { span: this.$isPrint ? 3 : 12 };
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@

<KGridItem
:layout4="{ span: 3 }"
:layout8="{ span: 7 }"
:layout12="{ span: 11 }"
:layout8="{ span: assignmentIsQuiz ? 3 : 7 }"
:layout12="{ span: assignmentIsQuiz ? 5 : 11 }"
>
<KTextbox
ref="titleField"
Expand All @@ -47,11 +47,37 @@
@keydown.enter="submitData"
/>
</KGridItem>
<KGridItem
:layout4="{ span: 1 }"
:layout8="{ span: 1 }"
:layout12="{ span: 1 }"
/>
<template v-if="assignmentIsQuiz">
<KGridItem
:layout4="{ span: 1 }"
:layout8="{ span: 1, alignment: 'left' }"
:layout12="{ span: 1, alignment: 'left' }"
>
<KIcon
icon="circleCheckmark"
:class="windowIsSmall ? 'style-icon' : 'checkmark-style-icon'"
/>
</KGridItem>
<KGridItem
:layout4="{ span: 3 }"
:layout8="{ span: 3 }"
:layout12="{ span: 5 }"
>
<KSelect
:label="reportVisibilityLabel$()"
:options="reportVisibilityOptions"
:value="reportVisibilityValue"
:help="
instantReportVisibility
? afterLearnerSubmitsQuizDescription$()
: afterCoachEndsQuizDescription$()
"
:style="windowIsSmall ? 'margin-left: -1em' : 'margin-left: -3em'"
class="visibility-score-select"
@change="option => (instantReportVisibility = option.value)"
/>
</KGridItem>
</template>
<KGridItem
:layout4="{ span: 3 }"
:layout8="{ span: 7 }"
Expand Down Expand Up @@ -132,6 +158,7 @@
import UiAlert from 'kolibri-design-system/lib/keen/UiAlert';
import BottomAppBar from 'kolibri/components/BottomAppBar';
import commonCoreStrings from 'kolibri/uiText/commonCoreStrings';
import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow';
import { coachStrings } from '../../common/commonCoachStrings';
import RecipientSelector from './RecipientSelector';
import SidePanelRecipientsSelector from './SidePanelRecipientsSelector';
Expand All @@ -146,6 +173,7 @@
},
mixins: [commonCoreStrings],
setup() {
const { windowIsSmall } = useKResponsiveWindow();
const {
recipientsLabel$,
descriptionLabel$,
Expand All @@ -154,15 +182,26 @@
saveQuizError$,
quizDuplicateTitleError$,
lessonDuplicateTitleError$,
reportVisibilityLabel$,
afterLearnerSubmitsQuizLabel$,
afterCoachEndsQuizLabel$,
afterLearnerSubmitsQuizDescription$,
afterCoachEndsQuizDescription$,
} = coachStrings;
return {
windowIsSmall,
recipientsLabel$,
descriptionLabel$,
titleLabel$,
saveLessonError$,
saveQuizError$,
quizDuplicateTitleError$,
lessonDuplicateTitleError$,
reportVisibilityLabel$,
afterLearnerSubmitsQuizLabel$,
afterCoachEndsQuizLabel$,
afterLearnerSubmitsQuizDescription$,
afterCoachEndsQuizDescription$,
};
},
props: {
Expand Down Expand Up @@ -217,6 +256,7 @@
formIsSubmitted: false,
showServerError: false,
showTitleError: false,
instantReportVisibility: this.assignment.instant_report_visibility,
};
},
computed: {
Expand Down Expand Up @@ -284,8 +324,22 @@
assignments: this.selectedCollectionIds,
active: this.activeIsSelected,
learner_ids: this.adHocLearners,
instant_report_visibility: this.instantReportVisibility,
};
},
reportVisibilityOptions() {
return [
{ label: this.afterLearnerSubmitsQuizLabel$(), value: true },
{ label: this.afterCoachEndsQuizLabel$(), value: false },
];
},
reportVisibilityValue() {
return (
this.reportVisibilityOptions.find(
option => option.value === this.instantReportVisibility,
) || {}
);
},
},
watch: {
title() {
Expand All @@ -300,6 +354,9 @@
adHocLearners() {
this.$emit('update', { learner_ids: this.adHocLearners });
},
instantReportVisibility() {
this.$emit('update', { instant_report_visibility: this.instantReportVisibility });
},
submitObject() {
if (this.showServerError) {
this.$nextTick(() => {
Expand Down Expand Up @@ -406,13 +463,35 @@
margin-left: -1em;
}
/deep/ .ui-select-feedback {
background: #ffffff !important;
}
/deep/ .ui-select-label {
background: #f5f5f5;
border-bottom-color: #666666;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.visibility-score-select {
border-bottom: 0 !important;
}
.style-icon {
width: 2em;
height: 2em;
margin-top: 0.5em;
margin-left: 1em;
}
.checkmark-style-icon {
width: 2em;
height: 2em;
margin-top: 0.5em;
margin-left: -1em;
}
fieldset {
padding: 0;
margin: 24px 0;
Expand Down
Loading

0 comments on commit 907f31d

Please sign in to comment.