Skip to content

[BE] Course search: accept ISO 8601 datetimes in the start date filter #842

Closed

Description

@dvarenikqaconsultant

User Story

As a Platform Administrator browsing courses on the Competency Management page, I want the start date filter to use the exact boundaries of the days I select in my own time zone, in order to find the courses that Studio shows as starting on those days.

Acceptance Criteria

Scenario: The filter uses the exact start boundary sent by the caller
  Given a course starts at 2027-06-30T22:00:00Z
  When the course list is requested with a start boundary of 2027-07-01T00:00:00+04:00
  Then the course is included in the results

Scenario: The filter uses the exact end boundary sent by the caller
  Given a course starts at 2027-07-01T02:00:00Z
  When the course list is requested with an end boundary of 2027-06-30T23:59:00-04:00
  Then the course is included in the results

Scenario: A course outside the boundaries is excluded
  Given a course starts at 2027-06-30T19:59:00Z
  When the course list is requested with a start boundary of 2027-07-01T00:00:00+04:00
  Then the course is not included in the results

Scenario: Boundaries in UTC and with an offset give the same result
  Given a set of courses with different start dates
  When the course list is requested once with boundaries in UTC ("Z") and once with the same moments written with a time zone offset
  Then both requests return the same courses

Scenario: Only a start boundary or only an end boundary is given
  Given a set of courses with different start dates
  When the course list is requested with only one of the two boundaries
  Then the results are limited on that side only

Scenario: An invalid boundary value is rejected
  Given the course list endpoint
  When a boundary value is not a valid ISO 8601 datetime
  Then the request fails with a validation error that names the invalid parameter

Scenario: A plain calendar date is rejected
  Given the course list endpoint
  When a boundary value is a date without a time, for example 2027-07-01
  Then the request fails with a validation error that names the invalid parameter

Scenario: A datetime without a time zone is rejected
  Given the course list endpoint
  When a boundary value is a datetime without a time zone, for example 2027-07-01T00:00:00
  Then the request fails with a validation error that names the invalid parameter

Description

The course search on the Competency Management page (#670) filters courses by start date through the course list endpoint added in #669. The endpoint accepts only a calendar date and compares it against a UTC day, while Studio shows course start dates in the user's time zone. As a result, a user outside UTC does not find a course by the start date that Studio shows for it (see #). With this change, the front end sends the exact start and end of the selected days in the user's time zone as ISO 8601 datetimes, and the back end filters by those moments.

Technical Details

This section is background and a suggested approach. The User Story and Acceptance Criteria define what must be true when the work is done.

In short

Parsing the boundaries. The start_date_on_or_after and start_date_on_or_before query parameters of GET /api/contentstore/v2/home/courses accept an ISO 8601 datetime with a time zone, either Z or an offset such as +04:00. The parameters no longer accept a plain YYYY-MM-DD date. The back end converts each value to UTC before filtering. A value that isn't an ISO 8601 datetime with a time zone, including a plain date and a datetime without a time zone, fails the request with a 400 validation error.

Filtering. A course matches when its start is at or after the start_date_on_or_after moment and at or before the start_date_on_or_before moment. The back end uses the moments exactly as received and no longer extends the end boundary to the end of a UTC day.

Implementation specifics

  • Parameter parsing: get_date_param in cms/djangoapps/contentstore/api/views/utils.py validates with serializers.DateField, which rejects datetimes. The course list view in cms/djangoapps/contentstore/views/course.py needs a datetime-aware equivalent that returns a UTC-aware datetime and raises a DRF ValidationError (400) for a plain date, a datetime without a time zone, or an unparseable value.
  • Filter logic: CourseOverview.get_all_courses in openedx/core/djangoapps/content/course_overviews/models.py currently sets start__lt = start_date_on_or_before + timedelta(days=1). It becomes start__gte = start_date_on_or_after and start__lte = start_date_on_or_before.
  • API docs: update the parameter descriptions and examples in cms/djangoapps/contentstore/rest_api/v2/views/home.py, which currently document the YYYY-MM-DD format.
  • Tests: cover each Acceptance Criteria scenario, including a boundary exactly equal to a course start (included on both sides), a value with Z, a value with a positive and a negative offset, and the rejected values: an unparseable value, a plain YYYY-MM-DD date, and a datetime without a time zone.
  • Out of scope: the front-end change that sends the boundaries as ISO 8601 datetimes is tracked in #.
Files to create and modify

Modified files

File Nature of modification
cms/djangoapps/contentstore/api/views/utils.py Parse the boundary parameters as ISO 8601 datetimes
cms/djangoapps/contentstore/views/course.py Use the datetime parsing for start_date_on_or_after and start_date_on_or_before
openedx/core/djangoapps/content/course_overviews/models.py Filter by the exact boundary moments in get_all_courses
cms/djangoapps/contentstore/rest_api/v2/views/home.py Update the parameter documentation and examples
Context

Activity

  1. changed the issue type fromtoon Sep 29, 2026
  2. removed theissue type on Sep 29, 2026
  3. moved this from Community Code Review to Ready for QA in Competency Criteria and Student Progresson Oct 9, 2026
  4. dvarenikqaconsultant commented on Oct 10, 2026

    @dvarenikqaconsultant
    Author

    Environment: Master Sandbox (https://apps.master.openedx.io, https://studio.master.openedx.io)
    Implementation tested: openedx/openedx-platform#39163 and openedx/frontend-app-authoring#3278, both merged on 2026-10-09
    Test users: dmitryvarenikqa (staff, has access to all test courses) and test670 (regular account with no course access)
    Test type: API (GET /api/contentstore/v2/home/courses, requests sent from an authenticated Studio browser session) and UI (Competency Management page)
    Result: ✅ All acceptance criteria are met. All 20 test cases passed, and no bugs were found.

    Test data

    I created six courses through Studio and imported a course archive into each one to set an exact start time. Every request below includes search=QA842, so only these courses can appear in the results.

    Course Course key Start (UTC)
    A course-v1:QA842+A+2027 2027-06-30 22:00
    B course-v1:QA842+B+2027 2027-07-01 02:00
    C course-v1:QA842+C+2027 2027-06-30 19:59
    D course-v1:QA842+D+2027 2027-06-30 20:00
    E course-v1:QA842+E+2027 2027-07-01 03:59
    F course-v1:QA842+F+2027 2027-07-01 04:00

    A request without date filters returns all six courses.

    Acceptance criteria scenarios

    Boundary values: S = 2027-07-01T00:00:00+04:00 (equal to 2027-06-30T20:00:00Z) and E = 2027-06-30T23:59:00-04:00 (equal to 2027-07-01T03:59:00Z).

    AC Request parameters Expected Actual Result
    1. Lower bound with offset start_date_on_or_after=S 200, A, B, D, E, F (A must be included) 200, A, B, D, E, F ✅
    2. Upper bound with offset start_date_on_or_before=E 200, A, B, C, D, E (B must be included) 200, A, B, C, D, E ✅
    3. Course outside the bounds is excluded both S and E 200, A, B, D, E (C and F excluded) 200, A, B, D, E ✅
    4. UTC and offset are equivalent 2027-06-30T20:00:00Z and 2027-07-01T03:59:00Z Same set as AC 3 200, A, B, D, E ✅
    5. Only one bound is set Responses of AC 1 and AC 2 No upper limit in AC 1 (F present), no lower limit in AC 2 (C present) F present in AC 1, C present in AC 2 ✅
    6. Unparseable value start_date_on_or_after=not-a-date 400 naming the parameter 400 ✅
    7. Plain date start_date_on_or_after=2027-07-01 400 naming the parameter 400 ✅
    8. Datetime without time zone start_date_on_or_after=2027-07-01T00:00:00 400 naming the parameter 400 ✅

    For AC 6 to AC 8, the response body uses the parameter name as the key and includes the rejected value, for example:

    {"start_date_on_or_after": "'2027-07-01' is not an ISO 8601 datetime with a UTC offset or 'Z', e.g. 2024-01-01T00:00:00+04:00."}

    Additionally tested: API edge cases

    Check Request parameters Expected Actual Result
    Both bounds are inclusive both bounds 2027-06-30T20:00:00Z 200, only D 200, only D ✅
    Upper bound without time zone start_date_on_or_before=2027-07-01T00:00:00 400 keyed by start_date_on_or_before 400 keyed by start_date_on_or_before ✅
    Unencoded + in the offset start_date_on_or_after=2027-07-01T00:00:00+04:00 (sent unencoded) 200, same set as AC 1 200, A, B, D, E, F ✅
    Space instead of T start_date_on_or_after=2027-07-01 00:00:00+04:00 200, same set as AC 1 200, A, B, D, E, F ✅
    Same digits with Z instead of offset start_date_on_or_after=2027-07-01T00:00:00Z 200, B, E, F 200, B, E, F ✅
    Frontend format with milliseconds 2027-07-01T00:00:00.000+04:00 to 2027-07-01T23:59:59.999+04:00 200, A, B, D, E, F 200, A, B, D, E, F ✅
    No authentication valid bound, no session 401, not 400 401 Authentication credentials were not provided. ✅
    User with no course access, invalid value (test670) start_date_on_or_after=2027-07-01 400, not 200 with an empty list 400 keyed by start_date_on_or_after ✅

    With a valid value, the same test670 user gets 200 with an empty list, which is expected for an account with no course access.

    Additionally tested: UI with browser time zones

    Page: /authoring/taxonomy/45/competencies. I selected a competency, searched for QA842, and picked a single day in the Start Date filter. The browser time zone was set to Asia/Tbilisi (UTC+4) and then to America/New_York (UTC-4 in July). For each case I also checked the request the page sends.

    Time zone Selected day Expected courses Actual courses Date parameters sent Result
    Asia/Tbilisi July 1, 2027 A, B, D, E, F A, B, D, E, F 2027-07-01T00:00:00.000+04:00 to 2027-07-01T23:59:59.999+04:00 ✅
    Asia/Tbilisi June 30, 2027 C C 2027-06-30T00:00:00.000+04:00 to 2027-06-30T23:59:59.999+04:00 ✅
    America/New_York July 1, 2027 F F 2027-07-01T00:00:00.000-04:00 to 2027-07-01T23:59:59.999-04:00 ✅
    America/New_York June 30, 2027 A, B, C, D, E A, B, C, D, E 2027-06-30T00:00:00.000-04:00 to 2027-06-30T23:59:59.999-04:00 ✅

    The Tbilisi July 1 case is the original bug scenario: course A starts at 2027-06-30T22:00:00Z, which is July 1 at 02:00 in UTC+4, and it is now found when filtering by July 1.

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

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions