feat: [FC-7879] implement get_user_dates() method to get all user dates for a course#326 - #346
Conversation
|
Thanks for the pull request, @kyrylo-kh! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
| dates[key] = content_date.user_overrides[0].actual_date | ||
| else: | ||
| try: | ||
| dates[key] = content_date.policy.actual_date() |
There was a problem hiding this comment.
I notice that this call is without arguments, compared to this:
Line 251 in ab2ee71
Has this been tested with self-paced courses?
There was a problem hiding this comment.
Currently get_user_dates only resolves absolute policy dates here. Relative dates are skipped (MissingScheduleError is caught). So self-paced (relative) policy dates aren’t returned by this API
There was a problem hiding this comment.
What does this mean from a user perspective? Are we not providing user dates at all for self-paced courses or is the expectation that the caller make a different call for that information?
There was a problem hiding this comment.
The bug was that get_user_dates was calling actual_date() with no arguments. Since relative dates need a schedule to compute (they're stored as offsets like "due in 14 days"), the call always raised MissingScheduleError and was silently skipped. So for self-paced enrolled users, only absolute dates and user overrides were returned - relative due dates were completely absent.
| return schedules | ||
|
|
||
|
|
||
| def get_user_dates(course_id, user_id, block_types=None, block_keys=None, date_types=None): |
There was a problem hiding this comment.
I'm trying to grok how this function and [get_dates_for_course()](https://github.com/openedx/edx-when/blob/ab2ee71af64ee6ef720083586eb26e1f58a748d9/edx_when/api.py#L163) relate. Given that there is some repeated code, is there a cleaner refactoring that's possible?
There was a problem hiding this comment.
get_user_dates is a narrow, filterable API for one user. get_dates_for_cours is the full path with caching, subsection filtering, and relative dates. The shared part is that loop that turns content dates into policy dates and applies user overrides. A cleaner approach would be to extract that into a small helper and have both call it (and pass schedule/end/cutoff from the helper so self-paced works in both). Would you like to have such helper function in this PR?
dbb6a9c to
08ca4c1
Compare
b4852bb to
4a63da5
Compare
7750058 to
ab07abb
Compare
ab07abb to
6fbc0cd
Compare
6fbc0cd to
b86734d
Compare
| end_datetime, cutoff_datetime = _get_end_dates_from_content_dates(content_dates) | ||
| dates = _resolve_policy_dates(content_dates, schedule, end_datetime, cutoff_datetime) | ||
|
|
||
| for content_date in content_dates: |
There was a problem hiding this comment.
Suggestion: Guard against no schedule/ MissingScheduleError as in _resolve_policy_dates above
|
|
||
| for content_date in content_dates: | ||
| if content_date.user_overrides: | ||
| dates[(content_date.location, content_date.field)] = content_date.user_overrides[0].actual_date |
There was a problem hiding this comment.
Question: Is there a guard against shifting the date past a course end date?
There was a problem hiding this comment.
No guard, by design - matches canonical get_dates_for_course, which applies overrides via bare userdate.actual_date without end-date capping. Only policy relative dates get capped. Overrides = explicit instructor extensions, allowed past course end
farhaanbukhsh
left a comment
There was a problem hiding this comment.
@kyrylo-kh thank you for the work here, I have added some more comments please have a look.
| course_key = _ensure_key(CourseKey, course_key) | ||
| for assignment in assignments: | ||
| if assignment.date is not None: | ||
| set_date_for_block(course_key, assignment.block_key, 'due', assignment.date) |
There was a problem hiding this comment.
This should be an atomic operation because if on of the the operation raises errors, others will still go on. What do you think?
There was a problem hiding this comment.
It seems to be handled in the follow-up PR already - https://github.com/openedx/edx-when/pull/347/changes#diff-c3d98af45b99fefbf88d009b7b89c13a1fdea947cb57f3523c1af0c5fa8c4899R595.
Maybe in this case, this method can be completely removed from the current PR @kyrylo-kh?
| """ | ||
| course_id = _ensure_key(CourseKey, course_id) | ||
|
|
||
| content_dates_query = models.ContentDate.objects.filter( |
There was a problem hiding this comment.
What about when edx_when.api._are_relative_dates_enabled is true/false, are we expected to return relative deadlines? I was looking into (relative_dates tests)[https://github.com/openedx/edx-when/blob/master/tests/test_api.py#L363], probably we should add this function there as well?
| ) | ||
|
|
||
| schedule = get_schedule_for_user(user_id, course_id) | ||
| end_datetime, cutoff_datetime = _get_end_dates_from_content_dates(content_dates) |
There was a problem hiding this comment.
Depending on the above answer if I have relative date enable this function can have undesirable behaviour, I have two scenarios in mind.
-
When a relative due date would fall after the course end date, the API should cap
it at the course end date. But this function wouldn't do that instead will return the relative due date. -
For self-paced courses, if the learner started too late, due dates should become
unavailable (None) based on the course cutoff logic.
I have tried some tests
def test_due_filter_still_applies_course_end_cap(self):
due_block = make_block_id(self.course_key)
end_block = make_block_id(self.course_key, block_type='course')
end_date = datetime(2023, 1, 5)
models.ContentDate.objects.create(
course_id=self.course_key,
location=due_block,
field='due',
active=True,
policy=models.DatePolicy.objects.create(rel_date=timedelta(days=10)),
block_type='sequential'
)
models.ContentDate.objects.create(
course_id=self.course_key,
location=end_block,
field='end',
active=True,
policy=models.DatePolicy.objects.create(abs_date=end_date),
block_type='course'
)
self._make_enrollment_with_schedule(start_date=datetime(2023, 1, 1), created=datetime(2023, 1, 1))
result = api.get_user_dates(self.course_key, self.user.id, date_types=['due'])
assert result == {(due_block, 'due'): end_date}
def test_due_filter_still_applies_course_cutoff(self):
due_block = make_block_id(self.course_key)
end_block = make_block_id(self.course_key, block_type='course')
end_date = datetime(2023, 1, 20)
models.ContentDate.objects.create(
course_id=self.course_key,
location=due_block,
field='due',
active=True,
policy=models.DatePolicy.objects.create(rel_date=timedelta(days=10)),
block_type='sequential'
)
models.ContentDate.objects.create(
course_id=self.course_key,
location=end_block,
field='end',
active=True,
policy=models.DatePolicy.objects.create(abs_date=end_date),
block_type='course'
)
self._make_enrollment_with_schedule(start_date=datetime(2023, 1, 1), created=datetime(2023, 1, 15))
result = api.get_user_dates(self.course_key, self.user.id, date_types=['due'])There was a problem hiding this comment.
This is a nice catch. Except relative dates case, _get_end_dates_from_content_dates is returning not expected result in case any of the filters are passed, as this method require a course block to correctly calculate end date.
Probably it's necessary to get end date and due directly from DB via ContentDate and then calculate cutoff.
GlugovGrGlib
left a comment
There was a problem hiding this comment.
This looks good, but the current issue around usage of the relative dates feature flag and _get_end_dates_from_content_dates worth addressing before the merge.
| return end_datetime, cutoff_datetime | ||
|
|
||
|
|
||
| def _resolve_policy_dates(content_dates, schedule=None, end_datetime=None, cutoff_datetime=None): |
There was a problem hiding this comment.
Based on the previous discussion I suggest reusing this helper function inside def get_dates_for_course (lines 270 - 278) as well.
This requires building a policies map inside a helper, and also pass course_id as an additional parameter to normalize cdate using cdate.location.map_into_course(course_id) as in get_dates_for_course case, but makes more sense in keeping it as a helper method.
Also in case this is done, please update a docstring to describe arguments and new return value.
| ) | ||
|
|
||
| schedule = get_schedule_for_user(user_id, course_id) | ||
| end_datetime, cutoff_datetime = _get_end_dates_from_content_dates(content_dates) |
There was a problem hiding this comment.
This is a nice catch. Except relative dates case, _get_end_dates_from_content_dates is returning not expected result in case any of the filters are passed, as this method require a course block to correctly calculate end date.
Probably it's necessary to get end date and due directly from DB via ContentDate and then calculate cutoff.
| course_key = _ensure_key(CourseKey, course_key) | ||
| for assignment in assignments: | ||
| if assignment.date is not None: | ||
| set_date_for_block(course_key, assignment.block_key, 'due', assignment.date) |
There was a problem hiding this comment.
It seems to be handled in the follow-up PR already - https://github.com/openedx/edx-when/pull/347/changes#diff-c3d98af45b99fefbf88d009b7b89c13a1fdea947cb57f3523c1af0c5fa8c4899R595.
Maybe in this case, this method can be completely removed from the current PR @kyrylo-kh?
| return dates | ||
|
|
||
|
|
||
| def get_existing_due_locations(course_key): |
There was a problem hiding this comment.
I suggest improving the function naming, as the current one is a little confusing.
get_locations_with_due_dates or get_blocks_with_due_dates might suit better
|
Thanks for your review, @GlugovGrGlib @farhaanbukhsh |
|
@kyrylo-kh can you please fix the CI |
Description
This PR adds a new API function
get_user_dates(course_id, user_id)to return a comprehensive map of dates for a given course and user.Functionality
Returns a dictionary where keys are
(block_key, field)tuples and values aredatetimeobjects.Prioritizes user-specific overrides (
UserDate) over default content dates (ContentDatewithDatePolicy).Supports optional filtering by:
block_types(e.g.,sequential,vertical)block_keys(UsageKey or string list)date_types(due,start, etc.)Features
Testing
Extensive test suite verifies: