You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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).
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: Courses with no start date always appear
Given the requesting user has access to a course that has no course-run start date
When the endpoint is called with any start-date range
Then that course appears in "results" regardless of the range
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_before query 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 existing active_only param: Q(end__isnull=True) | Q(end__gte=now). Add start_date_after/start_date_before as sibling parameters on this same method, applying Q(start__isnull=True) | Q(start__gte=after, start__lte=before) when either is set. CourseOverview.start is models.DateTimeField(null=True), so the isnull branch 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 to get_all_courses alongside its existing filter_={'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 an org query 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 fix org.
HomePageCoursesViewV2.get() (cms/djangoapps/contentstore/rest_api/v2/views/home.py) needs no parsing change — it already forwards request unchanged down the chain. Add the two new params to its apidocs.string_parameter(...) declarations for schema/doc visibility.
No openedx-core change. Outlines use the existing CourseIndexView, 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.
Prior investigation ruled out edx-search's course_discovery_search(): its only call site in the platform is the LMS free-text CourseListView (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-platform endpoint rather than migrating its logic into openedx-core. There is a known community direction of migrating code from openedx-platform into openedx-core over time; if get_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.
Repo:
openedx-core(CBE sprint-0 backlog). The implementation lands inopenedx-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 viaget_courses_accessible_to_user()(cms/djangoapps/contentstore/views/course.py) — with pagination and filters fororg,search,active_only/archived_only, andorder. It has no course-run start-date field and no date-range filter today. Course outlines (sections, subsections, thegradedflag) are already served byCourseIndexView(GET /api/contentstore/v1/course_index/{course_id}), the same endpoint Studio's own outline UI uses.Requested change
Extend
HomePageCoursesViewV2rather than building a new endpoint: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).CourseIndexViewon drill-in/expand — this ticket does not add outline data to the course-list response.Explicitly out of scope
Acceptance Criteria
These scenarios are verifiable via Postman.
Open Questions
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/coursescontract: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.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.openedx-corechange. Outlines use the existingCourseIndexView, fetched lazily per course by the frontend on expand.Example Resolution Prompt
In
openedx-platform, add optionalstart_date_after/start_date_beforeparams toCourseOverview.get_all_courses()(openedx/core/djangoapps/content/course_overviews/models.py), mirroring the existingactive_onlyparam's pattern: when either is set, applyQ(start__isnull=True) | Q(start__gte=after, start__lte=before)to the queryset before other filtering. Incms/djangoapps/contentstore/views/course.py, thread these two params fromget_courses_accessible_to_user(request)into theget_all_coursescall, and add them toallowed_query_params(~line 476) so they aren't silently dropped (verify this doesn't also require fixing the existingorgwhitelist gap — that's a separate, pre-existing issue, out of scope here). Add matchingapidocs.string_parameterdocs toHomePageCoursesViewV2.get()incms/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 reachget_all_coursesthrough 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.pyedx-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.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
openedx/core/djangoapps/content/course_overviews/models.pyCourseOverview.get_all_courses: addstart_date_after/start_date_beforeparams, null-inclusiveQdate-range filter (mirrors existingactive_onlypattern)cms/djangoapps/contentstore/views/course.pyget_courses_accessible_to_user: thread new params through toget_all_courses; add params toallowed_query_paramscms/djangoapps/contentstore/rest_api/v2/views/home.pyHomePageCoursesViewV2: add doc-only query-param declarations