Skip to content

feat: [FC-7879] implement get_user_dates() method to get all user dates for a course#326 - #346

Open
kyrylo-kh wants to merge 6 commits into
openedx:masterfrom
raccoongang:rg/axm-dates-implement-service-layer-for-date-resolution
Open

feat: [FC-7879] implement get_user_dates() method to get all user dates for a course#326#346
kyrylo-kh wants to merge 6 commits into
openedx:masterfrom
raccoongang:rg/axm-dates-implement-service-layer-for-date-resolution

Conversation

@kyrylo-kh

Copy link
Copy Markdown
Member

NOTE: Depends on #343

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 are datetime objects.

  • Prioritizes user-specific overrides (UserDate) over default content dates (ContentDate with DatePolicy).

  • Supports optional filtering by:

    • block_types (e.g., sequential, vertical)
    • block_keys (UsageKey or string list)
    • date_types (due, start, etc.)

Features

  • Handles missing schedules and skips invalid or inactive dates gracefully.
  • Fully supports both string and object inputs for course and block keys.
  • Optimized with prefetching to minimize query overhead.

Testing

Extensive test suite verifies:

  • Correct merging of user and default dates
  • All filtering options
  • Multiple overrides with the latest one selected
  • Handling of edge cases like empty datasets or missing metadata

@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @kyrylo-kh!

This repository is currently maintained by @openedx/openedx-unmaintained.

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 approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To 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:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where 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:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Feb 9, 2026
@github-project-automation github-project-automation Bot moved this to Needs Triage in Contributions Feb 9, 2026
@kyrylo-kh
kyrylo-kh requested a review from e0d February 9, 2026 18:23
@mphilbrick211 mphilbrick211 moved this from Needs Triage to Waiting on Author in Contributions Feb 9, 2026
@mphilbrick211 mphilbrick211 added the FC Relates to an Axim Funded Contribution project label Feb 9, 2026
Comment thread edx_when/tests/test_api.py Outdated
Comment thread edx_when/api.py Outdated
dates[key] = content_date.user_overrides[0].actual_date
else:
try:
dates[key] = content_date.policy.actual_date()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that this call is without arguments, compared to this:

dates[key] = cdate.policy.actual_date(schedule, end_datetime, cutoff_datetime)

Has this been tested with self-paced courses?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread edx_when/api.py
return schedules


def get_user_dates(course_id, user_id, block_types=None, block_keys=None, date_types=None):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that makes sense.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread edx_when/tests/test_api.py Outdated
@mphilbrick211 mphilbrick211 moved this from Waiting on Author to In Eng Review in Contributions Feb 25, 2026
@kyrylo-kh
kyrylo-kh force-pushed the rg/axm-dates-implement-service-layer-for-date-resolution branch from dbb6a9c to 08ca4c1 Compare March 9, 2026 22:00
@kyrylo-kh
kyrylo-kh force-pushed the rg/axm-dates-implement-service-layer-for-date-resolution branch from b4852bb to 4a63da5 Compare March 12, 2026 00:09
@kyrylo-kh
kyrylo-kh force-pushed the rg/axm-dates-implement-service-layer-for-date-resolution branch 3 times, most recently from 7750058 to ab07abb Compare March 23, 2026 21:57
@kyrylo-kh
kyrylo-kh force-pushed the rg/axm-dates-implement-service-layer-for-date-resolution branch from ab07abb to 6fbc0cd Compare March 25, 2026 21:31
@kyrylo-kh
kyrylo-kh force-pushed the rg/axm-dates-implement-service-layer-for-date-resolution branch from 6fbc0cd to b86734d Compare March 25, 2026 21:50
Comment thread edx_when/api.py
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Guard against no schedule/ MissingScheduleError as in _resolve_policy_dates above

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added 5285469

Comment thread edx_when/api.py Outdated

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Is there a guard against shifting the date past a course end date?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 farhaanbukhsh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kyrylo-kh thank you for the work here, I have added some more comments please have a look.

Comment thread edx_when/api.py Outdated
Comment on lines +656 to +659
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an atomic operation because if on of the the operation raises errors, others will still go on. What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread edx_when/api.py
"""
course_id = _ensure_key(CourseKey, course_id)

content_dates_query = models.ContentDate.objects.filter(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread edx_when/api.py Outdated
)

schedule = get_schedule_for_user(user_id, course_id)
end_datetime, cutoff_datetime = _get_end_dates_from_content_dates(content_dates)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the above answer if I have relative date enable this function can have undesirable behaviour, I have two scenarios in mind.

  1. 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.

  2. 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'])

@GlugovGrGlib GlugovGrGlib Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup that makes more sense

@GlugovGrGlib GlugovGrGlib left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread edx_when/api.py Outdated
return end_datetime, cutoff_datetime


def _resolve_policy_dates(content_dates, schedule=None, end_datetime=None, cutoff_datetime=None):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread edx_when/api.py Outdated
)

schedule = get_schedule_for_user(user_id, course_id)
end_datetime, cutoff_datetime = _get_end_dates_from_content_dates(content_dates)

@GlugovGrGlib GlugovGrGlib Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread edx_when/api.py Outdated
Comment on lines +656 to +659
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread edx_when/api.py Outdated
return dates


def get_existing_due_locations(course_key):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@itsjeyd itsjeyd added the waiting on author PR author needs to resolve review requests, answer questions, fix tests, etc. label Jun 25, 2026
@kyrylo-kh

Copy link
Copy Markdown
Member Author

Thanks for your review, @GlugovGrGlib @farhaanbukhsh
Please, take a look at changes 5bb4dae

@farhaanbukhsh

Copy link
Copy Markdown
Member

@kyrylo-kh can you please fix the CI

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FC Relates to an Axim Funded Contribution project open-source-contribution PR author is not from Axim or 2U waiting on author PR author needs to resolve review requests, answer questions, fix tests, etc.

Projects

Status: In Eng Review

Development

Successfully merging this pull request may close these issues.

8 participants