-
Notifications
You must be signed in to change notification settings - Fork 750
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 quiz report visibility control for coaches #13064
base: develop
Are you sure you want to change the base?
Changes from 7 commits
09301e8
3b64d66
e39ccdb
39bb753
fa607e3
972728c
272a948
80645ac
41f8f75
39a71e1
d026a02
c4fca87
995180c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Generated by Django 3.2.25 on 2025-02-07 15:17 | ||
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(default=True), | ||
), | ||
migrations.AddField( | ||
model_name="exam", | ||
name="instant_report_visibility", | ||
field=models.BooleanField(default=True), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(default=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we may want to be careful about exactly how we add this field - my specific thought is in relation to the migration this will produce on KDP. When we add a Django model field with a default value, this can result in a very long running migration, if the table is large. The way to avoid this is to allow the field to be nullable We create the migration with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is a good point. It would be simpler and safer to allow the field to be null and return a |
||
|
||
def __str__(self): | ||
return self.title | ||
|
||
|
@@ -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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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 }" | ||
|
@@ -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'; | ||
|
@@ -146,6 +173,7 @@ | |
}, | ||
mixins: [commonCoreStrings], | ||
setup() { | ||
const { windowIsSmall } = useKResponsiveWindow(); | ||
const { | ||
recipientsLabel$, | ||
descriptionLabel$, | ||
|
@@ -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: { | ||
|
@@ -217,6 +256,7 @@ | |
formIsSubmitted: false, | ||
showServerError: false, | ||
showTitleError: false, | ||
instantReportVisibility: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be dynamic rather than hard-set? For example, when I open an exam for editing, should this be set to the value previously saved to the exam? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oi, just realized that you've set it in mounted - curious if you tried assigning it to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, the only thing is if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've just read that the Nullish coalescing operator (??) is mainly supported on newer browsers so I've decided to set it as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update: I went with setting Setting it as: This field is set to true by default in |
||
}; | ||
}, | ||
computed: { | ||
|
@@ -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() { | ||
|
@@ -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(() => { | ||
|
@@ -308,6 +365,9 @@ | |
} | ||
}, | ||
}, | ||
mounted() { | ||
this.instantReportVisibility = this.assignment.instant_report_visibility; | ||
}, | ||
methods: { | ||
submitData() { | ||
this.showServerError = false; | ||
|
@@ -406,13 +466,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; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only thing I can think of here, is that we should probably return
True
for this if the backend value isnull
- that will save the frontend from having to worry about the nullability of the field, and leave it entirely as a backend concern (as we only really introduced it for the purposes of migration).