-
Notifications
You must be signed in to change notification settings - Fork 2
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
fix: add courserun date validation #3400
base: master
Are you sure you want to change the base?
Conversation
29f8bbf
to
90185d0
Compare
ab801c5
to
7e0b918
Compare
31b782e
to
ff9fb44
Compare
courses/models.py
Outdated
not self.enrollment_end or self.enrollment_end < now | ||
): | ||
raise ValidationError( | ||
"Either start_date or enrollment_end must be in the future." |
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.
"Either start_date or enrollment_end must be in the future." | |
"Either start date or enrollment end must be in the future." |
courses/models.py
Outdated
|
||
if not self.start_date and not self.enrollment_end: | ||
raise ValidationError( | ||
"Either start_date or enrollment_end must be provided." |
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.
"Either start_date or enrollment_end must be provided." | |
"Either start date or enrollment end must be provided." |
if course_run: | ||
if course_run_created: | ||
stats["course_runs_created"].add(course_run.external_course_run_id) | ||
log.info( | ||
f"Created Course Run, title: {external_course.course_title}, external_course_run_id: {course_run.external_course_run_id}" # noqa: G004 | ||
) | ||
elif course_run_updated: | ||
stats["course_runs_updated"].add(course_run.external_course_run_id) | ||
log.info( | ||
f"Updated Course Run, title: {external_course.course_title}, external_course_run_id: {course_run.external_course_run_id}" # noqa: G004 | ||
) | ||
|
||
if course_run_created: | ||
stats["course_runs_created"].add(course_run.external_course_run_id) | ||
log.info( | ||
f"Created Course Run, title: {external_course.course_title}, external_course_run_id: {course_run.external_course_run_id}" # noqa: G004 | ||
) | ||
elif course_run_updated: | ||
stats["course_runs_updated"].add(course_run.external_course_run_id) | ||
log.info( | ||
f"Updated Course Run, title: {external_course.course_title}, external_course_run_id: {course_run.external_course_run_id}" # noqa: G004 | ||
f"Creating or Updating Product and Product Version, course run courseware_id: {course_run.external_course_run_id}, Price: {external_course.price}" # noqa: G004 | ||
) | ||
|
||
log.info( | ||
f"Creating or Updating Product and Product Version, course run courseware_id: {course_run.external_course_run_id}, Price: {external_course.price}" # noqa: G004 | ||
) | ||
|
||
if external_course.price: | ||
product_created, product_version_created = ( | ||
create_or_update_product_and_product_version( | ||
external_course, course_run | ||
if external_course.price: | ||
product_created, product_version_created = ( | ||
create_or_update_product_and_product_version( | ||
external_course, course_run | ||
) | ||
) | ||
) | ||
if product_created: | ||
stats["products_created"].add(course_run.external_course_run_id) | ||
if product_created: |
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.
nit: may be we can move this into separate functions to reduce nested blocks in single 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.
@arslanashraf7 We are not validating the enrollment start date as it is not mentioned in the issue. With the current implementation, I can create a course run with enrollment start greater than enrollment end (both in future).
course_run = CourseRun.objects.create( | ||
external_course_run_id=external_course.course_run_code, | ||
course=course, | ||
title=external_course.course_title, | ||
courseware_id=course_run_courseware_id, | ||
run_tag=external_course.course_run_tag, | ||
start_date=external_course.start_date, | ||
end_date=external_course.end_date, | ||
enrollment_end=external_course.enrollment_end, | ||
live=True, | ||
) | ||
is_created = True | ||
except ValidationError as e: | ||
log.error( | ||
f"Error creating course run for course: {course.readable_id}, course run code: {external_course.course_run_code}, error: {e}" # noqa: G004 | ||
) |
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.
Instead of trying to create a course run with bad data, can we validate the dates like we are validating the end date in update_external_course_runs? We won't need these extra changes and it would be much simpler. What are your thoughts?
courses/models.py
Outdated
@@ -757,14 +757,28 @@ def clean(self): | |||
1. Later than end_date if end_date is set | |||
2. Later than start_date if start_date is set |
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.
We should update docs as per current logic.
At the time of the creation of the ticket, The main goal was to keep the catalog consistent and the catalog course criteria take the start date and enrollment end date into consideration while deciding if the course should be visible on the catalog or not. I do remember discussing this problem of date validation somewhere but don't remember where, or if was it verbal or written at that time. I would defer to @cachob and @pdpinch to make a decision on this. |
225498f
to
953ba0b
Compare
fe39cb1
to
01585ab
Compare
a8b09f1
to
e8adab1
Compare
Can you help me understand this from a systems level? Do we have a history of bad data from the external partners? I understand the validation will raise an error. Will those errors manifest in Sentry? Do we know who will watch out for them and take action? |
9c1e654
to
c7edd99
Compare
52be026
to
935a91d
Compare
@pdpinch This PR mainly adds validations for course runs created manually via Django admin. Data from external vendors is usually correct, but we’ve added extra date checks before creating an external course run to prevent validation-related task failures. Because of this, no validation errors will be raised or logged in Sentry when the task runs, even if the API sends bad data, it will just skip those course runs. Also, validation errors from the clean() method won’t show up in Sentry since Django already handles them and displays them on the form. |
That's fine for the django admin. Silent failures from the external partners' APIs might lead to confusion, but I'm not sure. @cachob maybe be able to comment, although it's probably out of scope for this PR. |
4ad2365
to
1661ed4
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.
Now that we've added validation to prevent courses from being created with invalid dates, we should update our test cases accordingly.
Bypassing validation to create courses without these dates no longer makes sense.
@arslanashraf7 any thoughts ?
elif (not start_date or start_date < now) and ( | ||
not enrollment_end or enrollment_end < now | ||
): |
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.
elif (not start_date or start_date < now) and ( | |
not enrollment_end or enrollment_end < now | |
): | |
elif (start_date and start_date < now) or (enrollment_end and enrollment_end < now): |
mitxpro/utils.py
Outdated
now = now_in_utc() | ||
error_msg = None | ||
|
||
if not start_date and not enrollment_end: |
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.
Its equivalent to:
if not start_date and not enrollment_end: | |
if not (start_date or enrollment_end): |
if not ( | ||
external_course.validate_end_date() | ||
and not bool( | ||
get_courserun_date_errors( | ||
external_course.start_date, | ||
external_course.end_date, | ||
external_course.enrollment_end, | ||
) | ||
) | ||
): |
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.
if not ( | |
external_course.validate_end_date() | |
and not bool( | |
get_courserun_date_errors( | |
external_course.start_date, | |
external_course.end_date, | |
external_course.enrollment_end, | |
) | |
) | |
): | |
if not external_course.validate_end_date() or get_courserun_date_errors( | |
external_course.start_date, | |
external_course.end_date, | |
external_course.enrollment_end, | |
): |
Sometimes we need to test for scenarios when course run is expired (having past dates). For this, I have added the |
mitxpro/utils.py
Outdated
@@ -646,3 +646,52 @@ def strip_datetime(date_str, date_format, date_timezone=None): | |||
|
|||
date_timezone = date_timezone if date_timezone else datetime.UTC | |||
return datetime.datetime.strptime(date_str, date_format).astimezone(date_timezone) | |||
|
|||
|
|||
def get_courserun_date_errors( |
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.
A better name would be validate_courserun_dates
. It can return a tuple of True/False, Message
@@ -339,11 +344,20 @@ def update_external_course_runs(external_courses, keymap): # noqa: C901, PLR091 | |||
stats["course_runs_skipped"].add(external_course.course_run_code) | |||
continue | |||
|
|||
if not external_course.validate_end_date(): | |||
if not ( | |||
external_course.validate_end_date() |
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.
We are validating this in get_courserun_date_errors, Do we need this now?
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.
We are validating this in get_courserun_date_errors, Do we need this now?
I think we are not validating end_date in get_courserun_date_errors. Should we add this validation there as well? Initially the ticket asked validations for start and enrollment_end date and that's why I didn't add the end date validations in get_courserun_date_errors
courses/models.py
Outdated
|
||
if self.end_date and self.expiration_date < self.end_date: | ||
raise ValidationError("Expiration date must be later than end date.") # noqa: EM101 | ||
errorMsg = get_courserun_date_errors( |
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.
errorMsg = get_courserun_date_errors( | |
error_msg = get_courserun_date_errors( |
@pdpinch / @Anas12091101 - I believe with the checks in place that we have for external courses should suffice. I don't see any urgent reason why these should take up Sentry overhead - if ever there is bad data in these endpoints it should be cleaned up by the vendors before they send it over as a best practice. I agree that this is out scope, and this could be dealt with the reporting tools we're looking to develop for our vendor APIs |
1ac7031
to
e270dbf
Compare
dcf3f2a
to
0bd3168
Compare
What are the relevant tickets?
https://github.com/mitodl/hq/issues/5823
Description (What does it do?)
This PR adds the following date validation at course run creation:
Note: This PR also updates the tests which started to break due to validation changes
How can this be tested?
Creating a Course Run via Django Admin:
Either start_date or enrollment_end must be provided.
Either start_date or enrollment_end must be in the future.
End date must be later than start date.
For external courses:
task_sync_external_course_runs
from shell. Now, two scenarios can occur:- If the API data meets the validation rules, the Course Run will be updated.
- Otherwise, the following error will be logged:
Error updating course run for course: course-v1:xPRO+MCPO, course run code: MXP-MCPO-24-12#1, error: e
Error creating course run for course: course-v1:xPRO+MCPO, course run code: MXP-MCPO-24-12#1, error: e
will be logged.