Repository navigation
[BE] Add start-date range filter and outline delivery for accessible courses (Competency Criteria Associations) #669
Description
Repo: openedx-core (CBE sprint-0 backlog). The implementation lands in openedx-platform — see Files to create and modify, below. Tracked in this repo's backlog per team convention: all CBE-program tickets are filed here regardless of which repo the code touches.
Related: #665 (gradeable-subsection-to-competency association), #670 (frontend, Manage & Apply Competencies screen), ticket 5.7 (fetch competency criteria).
Use Case
As a Platform Administrator configuring Competency Criteria associations, I want to fetch the courses I have access to within a specified time range, along with each course's outline (sections and gradeable subsections), so that the Manage & Apply Competencies screen can display those courses and let me drill into their gradeable subsections to attach criteria.
Description
Current state
HomePageCoursesViewV2 (GET /api/contentstore/v2/home/courses, cms/djangoapps/contentstore/rest_api/v2/views/home.py) already lists the courses the current user can access — scoped via get_courses_accessible_to_user() (cms/djangoapps/contentstore/views/course.py) — with pagination and filters for org, search, active_only/archived_only, and order. It has no course-run start-date field and no date-range filter today. Course outlines (sections, subsections, the graded flag) are already served by CourseIndexView (GET /api/contentstore/v1/course_index/{course_id}), the same endpoint Studio's own outline UI uses.
Requested change
Extend HomePageCoursesViewV2 rather than building a new endpoint:
- Add an optional start-date-range filter to the existing accessible-courses listing. The filter matches courses whose course-run start date (
CourseOverview.start) falls within the given range. Courses with no start date always appear, regardless of the range. End-date filtering is not included (a later enhancement, separate SOW). - No change needed for outlines. The frontend ([FE] Competency Management Course Search/View #670) fetches each course's outline lazily, per course, via the existing
CourseIndexViewon drill-in/expand — this ticket does not add outline data to the course-list response.
Explicitly out of scope
- Fetching competency criteria — handled separately by ticket 5.7, cross-referenced against these outlines on the frontend ([FE] Competency Management Course Search/View #670).
- End-date filtering — deferred past MVP.
- Any competency-to-course association — only gradeable-subsection-to-competency association exists ([BE] Create CompetencyCriteria #665).
- Attaching/associating competency criteria to a subsection — that's [BE] Create CompetencyCriteria #665's and 5.7's job, not this read-only listing endpoint.
- The frontend UI ([FE] Competency Management Course Search/View #670) and the create-criterion flow ([BE] Create CompetencyCriteria #665 / the UC5.11 create-and-select UI).
- A per-course "has criteria configured" indicator on the course list — the list stays course-only; no signal is shown before drill-in.
Acceptance Criteria
These scenarios are verifiable via Postman.
Scenario: Fetch accessible courses within a start-date range
Given the requesting user has access to a set of courses via HomePageCoursesViewV2
And some of those courses have a course-run start date within a specified range and some outside it
When GET /api/contentstore/v2/home/courses is called with the start-date range params
Then the response's "results" array includes the accessible courses whose start date falls within the range
And it excludes accessible courses whose start date falls outside the range
And it excludes courses the user cannot access
Scenario: Each returned course's outline is available on drill-in
Given the endpoint returns an accessible course
When the frontend calls CourseIndexView (GET /api/contentstore/v1/course_index/{course_id}) for that course
Then the outline response includes the course's sections and its gradeable subsections (graded: true)
Scenario: No accessible courses in range
Given the user has no accessible courses whose start date falls within the specified range (and none lacking a start date)
When the endpoint is called
Then the response returns "results": [] with a 200 status code
Scenario: Unauthenticated caller
Given a request without valid authentication
When the endpoint is called
Then the response returns a 401 status code
Open Questions
- [non-blocking, owner: architect/implementer] Exact name for the new query params and response field (placeholders used in this ticket:
start_date_after/start_date_beforequery params; the response field is currently unnamed). Confirm before implementation; does not change scope or design.
Technical Details
Data Structures
No new models, serializers, or migrations. Two new optional query params on the existing GET /api/contentstore/v2/home/courses contract: start_date_after, start_date_before (ISO date strings, independently optional and combinable).
Logic
CourseOverview.get_all_courses()(openedx/core/djangoapps/content/course_overviews/models.py:681-717) already implements a null-inclusive OR pattern for its existingactive_onlyparam:Q(end__isnull=True) | Q(end__gte=now). Addstart_date_after/start_date_beforeas sibling parameters on this same method, applyingQ(start__isnull=True) | Q(start__gte=after, start__lte=before)when either is set.CourseOverview.startismodels.DateTimeField(null=True), so theisnullbranch satisfies "courses with no start date still appear." This is the correct integration point:get_all_courses's current filtering is plain-kwarg (filter_={'id__in': ...}passed straight to.filter(**filter_)), which can't express a null-inclusive OR — that capability has to live inside this method's own logic, not bolted on afterward by a caller.get_courses_accessible_to_user(request)(cms/djangoapps/contentstore/views/course.py:998) forwards the new params through toget_all_coursesalongside its existingfilter_={'id__in': ...}call.- The params must be added to
allowed_query_params(cms/djangoapps/contentstore/views/course.py:476), the whitelist that gates which query params actually take effect. Note: this view already documents anorgquery param that is missing from this whitelist and is silently ignored today — a pre-existing, unrelated bug, flagged here so the new params don't suffer the same silent no-op; not this ticket's job to fixorg. HomePageCoursesViewV2.get()(cms/djangoapps/contentstore/rest_api/v2/views/home.py) needs no parsing change — it already forwardsrequestunchanged down the chain. Add the two new params to itsapidocs.string_parameter(...)declarations for schema/doc visibility.- No
openedx-corechange. Outlines use the existingCourseIndexView, fetched lazily per course by the frontend on expand.
Example Resolution Prompt
In openedx-platform, add optional start_date_after/start_date_before params to CourseOverview.get_all_courses() (openedx/core/djangoapps/content/course_overviews/models.py), mirroring the existing active_only param's pattern: when either is set, apply Q(start__isnull=True) | Q(start__gte=after, start__lte=before) to the queryset before other filtering. In cms/djangoapps/contentstore/views/course.py, thread these two params from get_courses_accessible_to_user(request) into the get_all_courses call, and add them to allowed_query_params (~line 476) so they aren't silently dropped (verify this doesn't also require fixing the existing org whitelist gap — that's a separate, pre-existing issue, out of scope here). Add matching apidocs.string_parameter docs to HomePageCoursesViewV2.get() in cms/djangoapps/contentstore/rest_api/v2/views/home.py; no logic change needed there. Add/extend tests covering: in-range match, null-start inclusion, out-of-range exclusion, both-params-absent (unchanged behavior), and that the params actually reach get_all_courses through the whitelist.
Context
HomePageCoursesViewV2:cms/djangoapps/contentstore/rest_api/v2/views/home.pyget_course_context_v2:cms/djangoapps/contentstore/utils.pyget_courses_accessible_to_user:cms/djangoapps/contentstore/views/course.pyCourseOverviewmodel (thestartfield):openedx/core/djangoapps/content/course_overviews/models.pyCourseIndexView(outline, unchanged, already exists):cms/djangoapps/contentstore/rest_api/v1/views/course_index.py- [BE] Create CompetencyCriteria #665, [FE] Competency Management Course Search/View #670, ticket 5.7 — see Related, above.
- Prior investigation ruled out
edx-search'scourse_discovery_search(): its only call site in the platform is the LMS free-textCourseListView(lms/djangoapps/course_api/), the wrong audience and mechanism for this admin workflow, and it carries a runtime dependency on platform-only settings classes (SEARCH_FILTER_GENERATOR/SEARCH_RESULT_PROCESSOR) that this design avoids entirely by not using it. - This ticket proposes extending an existing
openedx-platformendpoint rather than migrating its logic intoopenedx-core. There is a known community direction of migrating code fromopenedx-platformintoopenedx-coreover time; ifget_courses_accessible_to_user/CourseOverview-based listing is ever a migration candidate, community review may prefer migrating first rather than extending in place. Proceeding with the platform-side extension as scoped; this is a risk to be surfaced during community review, not a reason to hold the ticket.
Files to create and modify
New files
None.
Modified files
| File | Nature of modification |
|---|---|
openedx/core/djangoapps/content/course_overviews/models.py |
CourseOverview.get_all_courses: add start_date_after/start_date_before params, null-inclusive Q date-range filter (mirrors existing active_only pattern) |
cms/djangoapps/contentstore/views/course.py |
get_courses_accessible_to_user: thread new params through to get_all_courses; add params to allowed_query_params |
cms/djangoapps/contentstore/rest_api/v2/views/home.py |
HomePageCoursesViewV2: add doc-only query-param declarations |
| Existing test file(s) for the three functions above | Add range/null-inclusion/exclusion/whitelist coverage (exact test file paths to be confirmed by the implementer) |
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
[-][Placeholder for BE] Build endpoint to fetch accessible courses within a time range, with outlines, for Competency Criteria Associations[/-][+][BE] Add start-date range filter and outline delivery for accessible courses (Competency Criteria Associations)[/+]thelmick-unicon commentedon Jul 24, 2026
Need to address timezone issues. Use timezone as it was designed on the course settings page.
thelmick-unicon commentedon Jul 28, 2026
The discussion period for this ticket is now complete, and we appreciate all the feedback. We'll be moving forward with development based on the current ticket pending final Axim sign-off. That said, the conversation doesn't have to stop. If you still have feedback, please continue to add to this ticket. We'll review anything new and determine whether it fits within the current timeline or makes more sense as its own ticket for future work. Thanks again for helping make this better!
@ormsbee This is ready for your review, thank you!
ormsbee commentedon Aug 19, 2026
👍 LGTM.
8 remaining items
feat: add start_date_on_or_after/start_date_on_or_before course filters
feat: add start_date_on_or_after/start_date_on_or_before course filters
feat: add start_date_on_or_after/start_date_on_or_before course filters
dvarenikqaconsultant commentedon Sep 2, 2026
Test Report
Environment: https://apps.master.openedx.io/authoring/home
Test user: test_qa_669@mail.com
Test type: API testing via Postman
Result: all tested cases passed. No bugs found.
Implementation note
The original requirements asked the endpoint to also return courses that have no start date set. During code review, it turned out that a course without a start date isn't really a case that should occur in normal usage, so the implementation was scoped with that in mind. As a result, the endpoint returns only the courses the user has access to, whose start date falls within the range given by the filter, if a filter is provided, or all accessible courses when no filter is given.
Scenario: fetch accessible courses within a start-date range
Test courses:
#1#2#3#4#5Filter combinations tested:
#1,#2,#3,#4,#5#1,#2,#3,#4,#5#1,#2,#3,#4,#5#1,#2,#3,#4,#5#2#2#2,#5#2,#5#1,#2,#3,#4,#5#1,#2,#3,#4,#5#1,#2,#3,#4,#5#1,#2,#3,#4,#5#1#1#1,#3#1,#3#4#4#3,#4,#5#3,#4,#5#1,#2,#3,#4,#5#1,#2,#3,#4,#5#1,#3#1,#3#2,#4,#5#2,#4,#5#2,#5#2,#5Scenario: no accessible courses in range
"count": 0"count": 0"count": 0"count": 0"count": 0"count": 0"count": 0"count": 0Scenario: each returned course's outline is available on drill-in
Tested on course
course-v1:OpenedX+DemoX+DemoCourse:course_structure.child_info.children, each with its ownchild_info.childrenpopulated.graded: true: confirmed on every subsection with a non-nullformat(e.g. "Basic Assessment Tools", "Intermediate Assessment Tools", "lti testing").graded: false: confirmed on every subsection withformat: null.gradedandformat: zero cases ofgraded: truewithformat: null, orgraded: falsewith a non-nullformat, anywhere in the response.Scenario: unauthenticated caller
{"detail": "Invalid token."}{"detail": "Invalid token."}{"detail": "Invalid token."}{"detail": "Invalid token."}{"detail":"Authentication credentials were not provided."}{"detail":"Authentication credentials were not provided."}Scenario: user has no access to the course
Expected result: 403
{"developer_message":"You do not have permission to perform this action."}Actual result: 403
{"developer_message":"You do not have permission to perform this action."}Status: Pass
Additionally tested: invalid dates in filters
Dates that don't exist on the calendar (e.g. February 29, 2027, since it's not a leap year, and September 31, 2026, since September only has 30 days) were also tested in the
start_date_on_or_afterandstart_date_on_or_beforeparameters. In both cases the date-format validation kicks in and the endpoint returns an error:A datetime value such as
2024-01-01T10:00:00was also tested in thestart_date_on_or_afterandstart_date_on_or_beforeparameters, since these filters only accept a date. As expected, the endpoint rejects it with a 400 status and the sameDate has wrong format. Use one of these formats instead: YYYY-MM-DD.error.Access scoping
The environment contains multiple courses beyond the test user's 5 accessible ones, several sharing the same default start date (01-Jan-2040) as courses in the fixture set. Across every filter tested above — including wide ranges that would match that shared date (e.g. the — / 1-Jan-2041 and — / 2-Jan-2040 rows) — the response never returned any course outside the user's own 5 accessible courses. This confirms the date filter narrows within, but never expands, the user's accessible set.
Scenario: date-range filtering is UTC-only (timezone boundary verification)
Verified that the start-date filter behaves correctly at the exact UTC midnight boundary and does not apply any course- or organization-specific timezone conversion. Results below.
Case 1 — boundary inclusivity right at UTC midnight
#1start_date_on_or_before=2027-06-01#2start_date_on_or_before=2027-06-01Course
#1's start time was set to 23:30 rather than 23:45 because the environment's start-time picker only allows 30-minute increments; this doesn't change what the case verifies.Case 2 — filtering is UTC-only, not course/org-timezone-aware (upper-bound side)
Course
#3's start date is 2027-06-01T02:00:00Z. In UTC this falls on June 1, but in a UTC-5 zone (for example, US Eastern) the same instant is still May 31.GETwithstart_date_on_or_after=2027-06-01:get_date_paramparses a plain date and compares it directly againstCourseOverview.startwith no course/org timezone conversion): course#3included.#3excluded, since its local date would be May 31.#3was included. This confirms the filter is UTC-only.Case 3 — same principle, lower-bound side
Course
#4's start date is 2027-06-30T22:00:00Z. In UTC this falls on June 30, but in a UTC+3 zone the same instant is already July 1.GETwithstart_date_on_or_before=2027-06-30:#4included.#4was included, again confirming UTC-only filObservation: swapped range boundaries
One more case was tested where
start_date_on_or_afterandstart_date_on_or_beforeare passed in reverse order (the lower bound is greater than the upper bound), e.g.?start_date_on_or_after=2030-01-01&start_date_on_or_before=2026-01-01. In this case the endpoint returns an empty list with a 200 status instead of a 400 error. This isn't treated as a bug, but it's worth flagging as behavior to be aware of going forward.feat: add course-run scoping to the criteria-groups endpoint (openedx…
feat: add course-run scoping to the criteria-groups endpoint (openedx…
feat: add course-run scoping to the criteria-groups endpoint (openedx…
feat: add course-run scoping to the criteria-groups endpoint (openedx…