-
Notifications
You must be signed in to change notification settings - Fork 3
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
feat: add escalation_email to course exam configurations #221
feat: add escalation_email to course exam configurations #221
Conversation
cfd7c4a
to
1945397
Compare
fe4505a
to
b7563f2
Compare
edx_exams/apps/api/serializers.py
Outdated
""" | ||
Serializer for the CourseExamConfiguration model | ||
""" | ||
provider = serializers.CharField(source='provider.name', allow_null=True) |
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.
I set the source
so that you when you serialize a CourseExamConfiguration
instance, it pulls the provider's name for the provider
field and not the primary key. However, a side effect is that if you deserialize some data (i.e. in the PATCH handler) like the following,
{'provider': 'test', 'escalation_email': '[email protected]'}
you end up with this data.
{'provider': {'name': 'test'}, 'escalation_email': '[email protected]'}
I'm not really sure of a way around this, unless I write two separate serializers - one for GETs and one for PUTs/PATCHEs.
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.
I don't like it, but I have no better solution either. Maybe a quick comment on the class? I feel like this is going to bite someone later.
return course_config.escalation_email | ||
|
||
|
||
def create_or_update_course_exam_configuration(course_id, provider_name, escalation_email): |
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.
I added an API function so that callers don't have to have references to the CourseExamConfiguration
model to call the class method.
@@ -246,46 +253,33 @@ class CourseExamConfigurationsView(ExamsAPIView): | |||
def get(self, request, course_id): | |||
""" | |||
Get exam configuration for a course | |||
|
|||
TODO: This view should use a serializer to ensure the read/write bodies are the same |
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.
I've added a serializer, so I've removed this comment.
edx_exams/apps/api/serializers.py
Outdated
if value is not None: | ||
try: | ||
ProctoringProvider.objects.get(name=value) | ||
# This exception is handled by the Django Rest Framework, so we don't want to use raise from and risk |
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.
Let me know if you disagree here. This is regarding disabling the Pylint rule below.
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.
never ran into this rule. Solution seems fine. But, would raise ValidationError from None
work?
migrations.AddField( | ||
model_name='courseexamconfiguration', | ||
name='escalation_email', | ||
field=models.EmailField(default='', max_length=254), |
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.
I made the decision to set the default of ''
only for existing instances in the database and not for all past and future instances. This is why default=''
is not on the escalation_email
model field.
exam.provider = new_provider | ||
exam.save() | ||
|
||
return len(exams) |
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 original function wasn't actually returning a length, so the count
variable was None
. I've fixed that here.
existing_config.save() | ||
|
||
@classmethod | ||
def _sync_exams_with_new_provider(cls, course_id, new_provider): |
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.
I pulled the exam sync behavior into its own utility function.
156eb50
to
3e7ea8b
Compare
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.
still reviewing but a few comments to start
edx_exams/apps/api/serializers.py
Outdated
if value is not None: | ||
try: | ||
ProctoringProvider.objects.get(name=value) | ||
# This exception is handled by the Django Rest Framework, so we don't want to use raise from and risk |
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.
never ran into this rule. Solution seems fine. But, would raise ValidationError from None
work?
migrations.AlterModelOptions( | ||
name='historicalexam', | ||
options={'get_latest_by': 'history_date', 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical exam'}, | ||
), | ||
migrations.AlterModelOptions( | ||
name='historicalexamattempt', | ||
options={'get_latest_by': 'history_date', 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical exam attempt'}, | ||
), |
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.
huh something does seem off here. Also looking into what might cause this
edx_exams/apps/api/serializers.py
Outdated
""" | ||
Validate that escalalation_email is provided when the provider is provided. | ||
|
||
NOTE: Currently, the LTI-based proctoring providers will require an escalation_email. Because the |
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.
Is this just because of our existing data. If we split the PR to just add the migration for the model field first and than give existing configs a value in stage does that eliminate the need for the empty string default on the model?
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.
Just to be sure we're on the same page, the empty string default is on the serializer. There is no default on the model, but blanks are allowed. Do you mean the default on the serializer? Or the blank on the model?
I changed the model field and forgot to update this comment, so that might be why I was talking about the model here.
If I don't define the escalation_email
serializer field explicitly, the DRF-generated one will look like this.
CourseExamConfigurationSerializer():
provider = CharField(allow_null=True, source='provider.name')
escalation_email = EmailField(allow_blank=True, max_length=254, required=False)
If I remove the explicit escalation_email
serializer field and remove this comment, does that work? I think a more accurate comment is, "Because an escalation email is only required when a proctoring provider is set, we have to enforce this requirement in the validate function."
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.
ah yeah I just got confused by this comment and thought you were referring to the model itself. I'm still fuzzy on what the explicit escalation_email
serializer field is doing for us. The default looks fine to me but I might be missing some nuance here.
edx_exams/apps/core/models.py
Outdated
existing_config.provider = new_provider | ||
|
||
if new_provider is None: | ||
existing_config.escalation_email = '' |
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.
basically same as my earlier comment is this only because of our existing data?
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.
I wanted to avoid a situation where someone sends this data {'provider': None, 'escalation_email': '[email protected]'}
. I believe we should always set the escalation_email
to the empty string if there is no provider. Do you want to leave it up to the client to be sure to do this? Or we could raise a validation error, alternatively. That would be more explicit.
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.
oh this make sense. I misread the condition as if escalation_email is None:
thinking we were doing some Null -> '' conversion.
85168c9
to
6624ee5
Compare
edx_exams/apps/core/models.py
Outdated
# If the provider is set to None, then we must clear the escalation_email, | ||
# even if a non-empty/non-null value is provided. | ||
if provider is None: | ||
escalation_email = '' |
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.
I'm finding that I'm duplicating this validation in api.py
and models.py
. There’s only so much one can do to prevent someone from saving invalid data into the database if the invariant isn’t at the database level, but I think it's okay to have it on the model as well because I cannot guarantee the API will be used, and I don't want to save invalid data into the database.
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.
Some learnings I've taken from this design are the negatives of rigidly following the pattern for view <-> api <-> model. This kind of duplication being one of them.
6624ee5
to
d6cd4ec
Compare
edx_exams/apps/core/models.py
Outdated
# If the provider is set to None, then we must clear the escalation_email, | ||
# even if a non-empty/non-null value is provided. | ||
if provider is None: | ||
escalation_email = '' |
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.
Some learnings I've taken from this design are the negatives of rigidly following the pattern for view <-> api <-> model. This kind of duplication being one of them.
edx_exams/apps/api/serializers.py
Outdated
""" | ||
Serializer for the CourseExamConfiguration model | ||
""" | ||
provider = serializers.CharField(source='provider.name', allow_null=True) |
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.
I don't like it, but I have no better solution either. Maybe a quick comment on the class? I feel like this is going to bite someone later.
bec8966
to
be2e50e
Compare
""" | ||
Serializer for writing to the CourseExamConfiguration model. | ||
|
||
We have separate read and write serializers because the read serializer uses the name field of the provider instance |
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.
Based on an earlier conversation with Zach, I decided it would be better to have two separate serializers than to have to worry about someone reading the data from the serializer improperly. This will be less error prone, and it's explicit that there are two different serializers - one for reading and one for writing. I can revert this to the old code (see my first commit), but I think this is an improvement.
""" | ||
provider = serializers.CharField(allow_null=True) | ||
# The escalation_email is a nullable field, but we require that clients send us an escalation_email. | ||
escalation_email = serializers.EmailField(allow_null=True, allow_blank=False, required=True) |
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.
This is a change.
The original was escalation_email = EmailField(allow_blank=True, max_length=254, required=False)
, which is the default generated by DRF.
The new one is escalation_email = serializers.EmailField(allow_null=True, allow_blank=False, required=True)
.
The two differences are...
- We now allow nulls and forbid blanks. Although this is not Django/DRF best-practice for character based fields, this more closely matches both the existing API on this endpoint, because
provider
is a nullable character field, and the Studio proctoring settings API, which accepts anull
for theescalation_email
. I have setallow_blank=False
so that we only have one representation of the empty value. - This field is now required. I think it's a clearer API contract with the client for this value to always be required. Otherwise, the client is left wondering why a value is required sometimes and not other times. Sending
escalation_email: null
from the client is more explicit.
if provider_name and not escalation_email: | ||
raise serializers.ValidationError('Escalation email is a required field when provider is provided.') | ||
if provider_name is None and escalation_email is not None: | ||
raise serializers.ValidationError('Escalation email must be None when provider is None.') |
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.
This is a new validation error. In the spirit of being more explicit, I've removed the behavior I had originally, which was to set the escalation_email
to None
when the provider
is None
, even when the escalation_email
is not None
(e.g. {'provider': None, 'escalation_email': '[email protected]'}
). The client should supply valid data or receive a 400 error. I don't want to get in the habit of guessing what the client's intentions are when the escalation
is not valid.
if provider_name is not None: | ||
provider = ProctoringProvider.objects.get(name=provider_name) | ||
else: | ||
# If the provider is set to None, then we must clear the escalation_email, |
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.
Despite my comment in the serializers.py
file, I think we still need to enforce this requirement in the Python API.
381a070
to
5eb4b10
Compare
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.
nice improvements, this is a bit easier to understand now 👍
This commit adds an escalation email to course exam configurations. This value is used to specify who learners can contact in the event of issues with or questions about their exam attempts.
5eb4b10
to
2bc4859
Compare
JIRA: COSMO-39
Description: This commit adds an escalation email to course exam configurations. This value is used to specify who learners can contact in the event of issues with or questions about their exam attempts.
Author concerns: None.
Dependencies: None.
Installation instructions: None.
Testing instructions: None.
Merge checklist:
Post merge: