Skip to content

feat: serve course containers from authoring v2 - #39274

Draft
taimoor-ahmed-1 wants to merge 7 commits into
openedx:masterfrom
edly-io:feat/standardize-container-management-api
Draft

taimoor-ahmed-1 wants to merge 7 commits into
openedx:masterfrom
edly-io:feat/standardize-container-management-api

Conversation

@taimoor-ahmed-1

Copy link
Copy Markdown
Contributor

Summary

Standardizes the Container Management area (#39064, part of #38137). The area has one endpoint, the Unit page read GET /api/contentstore/v1/container_handler/{usage_key_string}. This PR adds its successor:

GET /api/authoring/v2/containers/{usage_key}/ (URL name authoring_v2:container_detail)

v1 stays mounted and unchanged. It is marked deprecated: true in the CMS schema and in its docstring.

It applies ADRs 0025–0038 and OEP-69.

Stacked on #39229 (course outline). This PR reuses that PR's CanViewCourse permission. Until #39229 merges, this branch also carries #39229's three commits. Only the last four commits belong to this PR. The branch rebases onto master once #39229 merges.

What a container is. A container is any block in the course outline that has children: a section, a subsection, a unit, or a container inside a unit (split_test, library_content, conditional, a group vertical). This is the same meaning as in the #39229 outline (?usage_key= "a container in the course outline, such as a section, subsection or unit") and in /api/libraries/v2/containers/. The course itself, leaf components, missing blocks and blocks the course does not reach answer 404.

Two representations:

  • Default (view=minimal): a thin, bounded view of the container: id, category, display_name, display_type, course_key, is_unit, unit_id, position and ancestors. Its SQL and Mongo reads depend on how deeply the block is nested, not on how large the course is. unit_id and position are null for a section, a subsection, or a container placed directly in one.
  • view=full: v1's Unit page payload, without user_clipboard and action. It uses v1's field names and shapes, including the Studio page links in prev_url/next_url that the authoring MFE decodes to move between units. It is available for a unit or a container inside a unit; a section or subsection with view=full answers 404.

Why a new version and not a fix in place. Each of these changes the contract for v1's callers, so they ride v2 (ADR 0037):

  • errors use the standard envelope;
  • Bearer tokens, and session-only calls by inactive users, are refused;
  • a missing block is a 404 envelope, not v1's 400 with an empty body;
  • a section, subsection, course or orphan key is a 404 (or, for sections and subsections, a 200 on the default view), not v1's 500;
  • a leaf component key is a 404, not v1's 200;
  • the default response is the thin view;
  • the clipboard is no longer read on GET. On v1 that read can save a CourseOverview row and send IMPORT_COURSE_DETAILS.

Changes

Commit Endpoint(s) Legacy behaviour kept New behaviour
test: share the v2 authoring test helpers — #39229's outline tests unchanged in behaviour Six helpers move verbatim from test_course_outline.py into the existing v2/views/tests/helpers.py, so the new tests do not import from another PR's test module
feat: let CanViewCourse take the course from a usage key /api/authoring/v2/courses/{course_key}/outline/ (unchanged) The course_key branch is untouched On a route with a usage_key, the course is the key's context. A library or other non-course context is 404 before any course lookup
feat: serve course containers from authoring v2 GET /api/authoring/v2/containers/{usage_key}/ (new) v1 route, view and URL name container_handler unchanged The endpoint described above, with its tests
docs: mark the v1 container handler deprecated in favour of the v2 container GET /api/contentstore/v1/container_handler/{usage_key_string} Route, view, responses unchanged deprecated: true in the schema (one exact SUPERSEDED_PATHS entry). The class and get docstrings name the successor and #39128

ADRs applied

  • 0025 serializers — typed output serializers for both views with help_text on every field and explicit CourseContainer* component names; the view parameter validated by its own serializer
  • 0026 permissions — (IsAuthenticated, CanViewCourse), the same courses.view_course check v1 makes inside _get_item_in_course; deny matrix with AuthZ off and on
  • 0027 schema / docs — @extend_schema with summary, description, the view enum, both shapes as a oneOf, an example per shape, every status declared including 406; tagged openedx-platform-sdk
  • 0028 viewsets — applied except router registration: a ViewSet backed by a service module, mounted with path(...) and the shared usage_key converter rather than a DRF Router, as every route in authoring_urls.py is
  • 0029 errors — partial: the envelope on every response this view produces, but two library and platform gaps remain and are pinned by tests: a malformed or i4x:// key gets the CMS HTML 404 at routing, and 405/406 are typed internal
  • 0030 GET idempotence — met for this code (no clipboard read, nothing written or signalled on GET or HEAD), with one inherited write outside it: openedx-authz inserts its PolicyCacheControl row on the first permission check against a database that lacks it, as on v1 and feat: serve the course outline from authoring v2 #39229
  • 0031 merged endpoints — n/a: one read operation; the v1 children route is a different representation, owned by [API] Containers #39063
  • 0032 pagination — n/a: a single resource; the default view has no unbounded list, and the view=full lists are the opt-in Unit page payload
  • 0033 filtering — n/a: no list; the only parameter, view, selects a representation and is validated with an enum
  • 0034 authentication — no authentication_classes: the platform default (JWT and session). Bearer and session-only inactive callers are refused; inactive JWT callers are served
  • 0035 MFE config — n/a: per-container course-authoring data, not site or front-end configuration
  • 0036 nested JSON — the heavy payload is never the default: thin default, view=full by explicit opt-in for the MFE
  • 0037 versioning — partial: v2 module, v1 frozen (docstring-only marker) and deprecated: true in the schema; the DEPR entry is a comment on [DEPR]: Contentstore and enrollment API versions superseded by FC-0118 #39128 until it is folded into the issue body
  • 0038 URL structure — /api/authoring/v2/containers/{usage_key}/: domain API name, plural snake_case collection, shared usage_key converter, trailing slash, version-free URL name; the v1 name container_handler is kept
  • OEP-66 queryset scoping — n/a as a mechanism: not a list, and the data comes from the modulestore with no get_queryset(); the layering is in the matrix
  • OEP-69 conventions — the decisions above as recorded, with the 0028, 0029, 0030 and 0037 departures stated; no governance identifiers in code or schema text

Compliance matrix

Decision Status How it is met, or the concrete reason it is excluded Evidence (test / gate)
0025 met CourseContainerSerializer (default) and CourseContainerFullSerializer (full), with typed nested serializers for ancestors, children, component templates and support legend. help_text on every field. Nullable where the code returns None: display_type (both views), unit_id, position, xblock.display_type, draft_preview_link, published_preview_link, library_content_picker_url, prev_url, next_url, ancestor_xblocks[].children[].url, component_templates[].templates[].boilerplate_name. xblock_info stays a free-form object, as v1's is, because a deployment override can extend it; its keys are documented and pinned per block role ContainerSchemaTest::test_every_field_of_the_read_is_described, …::test_the_read_offers_the_default_and_the_full_shape; ContainerFullViewXBlockInfoKeysTest; hygiene H9
0026 met Every v1 check has a v2 home (Authorization below). CanViewCourse makes the same user_has_course_permission(user, COURSES_VIEW_COURSE, course, READ) call v1 makes. Nobody gains access ContainerAccessLegacyPermissionsTest, ContainerAccessAuthzTest, CanViewCourseFromAUsageKeyTest; each case compared with v1's answer
0027 met Summary, description, view enum with default minimal, oneOf of both shapes, an example per shape (Container, Subsection, UnitPageData), 200/400/401/403/404/406 declared with ErrorResponse for every error. operationId v2_containers_retrieve. Tagged openedx-platform-sdk ContainerSchemaTest::test_the_read_is_published_for_the_sdk, …::test_the_examples_are_valid_bodies_of_their_view
0028 applied except router registration CourseContainerViewSet(StandardizedErrorMixin, viewsets.ViewSet), action retrieve, over v2/container_service.py. A plain ViewSet because the data path is the modulestore. Deviation: mounted with path("containers/<usage_key:usage_key>/", ...as_view({"get": "retrieve"})), because a router's regex routes cannot use the shared path converters; every route in authoring_urls.py does the same ContainerRoutingTest::test_the_address; ContainerQueryCountTest, ContainerQueryCountAuthzTest
0029 partial StandardizedErrorMixin first in the MRO. 401 authn, 403 authz, 404 not-found (unknown course, non-course context, missing block, non-member, full view above unit level), 400 validation for view with errors.view. Messages are fixed strings that never echo the key or the input. Gaps, both pinned by tests that flip when fixed: a malformed or i4x:// key fails at routing and gets the CMS HTML 404; 405 and 406 are typed internal because edx-drf-extensions 10.9.0 does not catalogue MethodNotAllowed and NotAcceptable ContainerDefaultViewTest::test_an_unknown_view_is_refused_with_400; ContainerRoutingTest::test_unparsable_or_deprecated_key_is_404, …::test_unsupported_accept_is_refused_with_406; test_other_methods_are_refused_after_authentication
0030 met for this code; one inherited write v2 never reads the clipboard. Every helper the service calls was read for writes; none saves, signals or emits an event. On GET and HEAD, both views, AuthZ off and on, a unit, a container inside it, a subsection and a section, and a course with prerequisites and special exams: no SQL write, no Mongo write, no Django signal and no Open edX event, apart from the first read on a database without openedx-authz's PolicyCacheControl row, which inserts it (one INSERT, its pre_save/post_save). That write belongs to the permission library, happens on v1 and #39229 too, and is allow-listed by name. v1's clipboard write is pinned as still present on v1 ContainerReadSideEffectsLegacyPermissionsTest, ContainerReadSideEffectsAuthzTest (test_reads_have_no_side_effects, test_reads_of_a_course_with_prerequisites_and_special_exams_have_no_side_effects, test_v1_still_builds_the_clipboard_overview)
0031 excluded One read operation. v1's container/{key}/children is a different representation, owned by #39063; it should mount at containers/{usage_key}/children/ under this member —
0032 excluded A single resource. The default view has no list that grows with the course. The lists in view=full (ancestor_xblocks, course_sequence_ids, sibling units in xblock_info, component_templates) are the Unit page payload under ADR 0036's opt-in exception, not a list endpoint —
0033 excluded No list. view is enum-validated; unknown parameters are ignored ContainerDefaultViewTest::test_unknown_parameters_are_ignored
0034 met No authentication_classes, so DefaultJwtAuthentication + DefaultSessionAuthentication. Frozen v1 keeps (JwtAuthentication, BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser). The sibling v2 views from #39157 (textbooks, video_settings) keep SessionAuthenticationAllowInactiveUser; this view follows #39229 instead. Its only consumer, the authoring MFE's Unit page, authenticates with the JWT cookie, which serves inactive users. Who is affected: an unactivated account (registration signs new accounts in before activation) or inactive global staff calling with a session cookie alone. Reviewers may prefer to align the siblings one way or the other test_an_inactive_session_is_not_authenticated, test_an_inactive_session_with_a_role_is_not_authenticated, test_an_inactive_jwt_caller_who_is_global_staff_may_read, test_an_inactive_jwt_caller_with_a_role_may_read, test_bearer_token_is_refused, test_jwt_caller_may_read; hygiene H4
0035 excluded Not configuration; per-user, per-container, not shared-cached —
0036 met Thin default; view=full only by request. The default view's SQL and Mongo reads are constant across course shapes (walking up the tree still costs CPU in proportion to the course structure, as in split's parent lookup) ContainerDefaultViewTest, test_the_default_view_costs_the_same_on_every_course_shape, ContainerFullViewParityTest
0037 partial New modules in v2/. v1 frozen apart from a docstring-only marker (the syntax tree, docstrings stripped, equals the base). deprecated: true through one exact SUPERSEDED_PATHS entry. No runtime header (frozen route). DEPR: an entry comment on #39128, to be folded into the issue body versions gate; CmsMarkSupersededPathsTest::test_the_container_read_is_marked_and_its_children_routes_are_not; ContainerSchemaTest::test_the_v1_read_is_deprecated_and_the_children_read_is_not
0038 met authoring API name, top-level containers collection (the key is its own opaque key), usage_key converter, trailing slash, container_detail. The slashless address redirects; the collection address without a key is not offered ContainerRoutingTest (test_the_address, test_the_v1_address_is_unchanged, test_the_url_name_is_unique_in_its_namespace, test_the_address_without_its_trailing_slash_redirects_to_it)
OEP-66 mechanism excluded, layering met (1) endpoint access: the two permission classes; (2) record visibility: unchanged from v1, anyone who may view the course sees every block in it, and the default view exposes only the block's own ancestors; (3) user filtering: none, view cannot widen what is visible. Future seam: the modulestore lookup in v2/container_service.py access tests above
OEP-69 met The decisions above, with the departures stated in their rows. No ADR/OEP/PR identifiers or narration in code or schema text hygiene gate PASS (0 FAIL, 0 WARN)

Backward compatibility

  • v1 is unchanged. vertical_block.py gains docstring text only. Its syntax tree with docstrings stripped equals the base. The v1 route, view and URL name cms.djangoapps.contentstore:v1:container_handler are untouched, and v1's error body is pinned (ContainerRoutingTest::test_v1_keeps_its_own_error_body).

  • Legacy tests are not edited. rest_api/v1/views/tests/test_vertical_block.py is not in the diff.

  • Parity. view=full is compared with v1 field by field, through the Django test client, for every member that has a Unit page, and for each of these cases:

    • neighbours across subsections and sections, and across a section with no units;
    • containers inside a unit, and containers that declare their block types;
    • the course's component templates and configured support levels;
    • the library content picker on and off, and tagging on and off;
    • public video sharing on and off;
    • preview links with and without an LMS address;
    • keys pinned to the draft branch and to a course version;
    • a child that cannot be loaded before the unit (position is counted as v1 counts it).

    The only declared differences, verbatim:

    INTENTIONAL_DIFFERENCES = [
        (r"user_clipboard([.\[].*)?", "the clipboard is its own resource at /api/content-staging/v1/clipboard/"),
        (r"action", "v1 echoes its unvalidated action parameter, which v2 does not take"),
    ]

    The comparison fails both on an undeclared difference and on a declared one that does not occur, and is itself mutation-tested (test_the_comparison_fails_when_a_field_changes).

  • Status changes from v1 are each pinned against v1's own answer in ContainerMembershipTest and the access tests:

    Key v1 v2
    missing block (reader) 400, empty body 404
    section, subsection 500 200 default view; 404 for view=full
    course root 500 404
    leaf component 200 404
    orphan unit or section 500 404
    unit of a section the course does not reach 200 404
    library or version-only key not 200 404
  • Query counts. view=full runs v1's SQL statements plus at most the course-existence lookup, and no more Mongo reads than v1. The default view's reads are equal across course shapes.

  • Schema. One new operation and deprecated: true on the v1 operation. docs/cms-openapi.yaml is not edited; the generate_openapi_schemas workflow regenerates it.

In-place edits

File Category Evidence
cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py docstring-only deprecation marker The syntax tree with docstrings stripped equals the base; versions gate ALLOWED
cms/djangoapps/contentstore/rest_api/v2/authoring_urls.py strictly opt-in addition: one import, one route Every existing route resolves as before (ContainerRoutingTest); outline, textbooks and video settings tests untouched
cms/djangoapps/contentstore/rest_api/v2/views/tests/helpers.py test helpers moved in verbatim (additions only, no line removed) versions gate ALLOWED; the moved definitions compare equal to the originals
cms/djangoapps/contentstore/rest_api/v2/views/tests/test_course_outline.py (#39229) imports the moved helpers instead of defining them; one private helper renamed public No test changed; same assertions
cms/djangoapps/contentstore/views/permissions.py (shared) CanViewCourse gains the usage_key branch The course_key branch is unchanged; CanViewCourseFromAUsageKeyTest
cms/lib/spectacular.py, cms/lib/tests/test_spectacular.py one exact SUPERSEDED_PATHS entry and its tests The v1 children routes and the v2 path stay unmarked

New files inside the pre-existing v2/: container_service.py, serializers/containers.py, views/containers.py, views/tests/test_containers.py. v2 imports nothing from rest_api/v1/, so removing v1 cannot break it.

Error-format decision

Versioned. v1 keeps its {"detail": ...} bodies, its empty 400 and its 500s, byte for byte. v2 uses the envelope. The only consumer, the authoring MFE, moves by switching address.

Deprecations

GET /api/contentstore/v1/container_handler/{usage_key_string} is superseded by GET /api/authoring/v2/containers/{usage_key}/?view=full. It is deprecated: true in the CMS schema and its docstrings name the successor. The view is otherwise unchanged: no runtime header. Tracked on the FC-0118 umbrella DEPR ticket, #39128; this area's entry is posted there with the PR. Window per #39128: removed one named release after the release this ships in.

Migration notes for callers:

  • Call /api/authoring/v2/containers/{usage_key}/?view=full, with the trailing slash.
  • Read the clipboard from /api/content-staging/v1/clipboard/ (the authoring MFE already does).
  • action is gone.
  • Errors use the standard envelope.
  • Bearer tokens are refused; use JWT or a session. A session-only call by an inactive user is refused.
  • A leaf component key is a 404: use /api/authoring/v1/xblocks/{usage_key}/ for component data. Library and i4x:// keys are 404.

Authorization

Legacy check Where it lives in v2 Deny-path tests
@view_auth_classes(is_authenticated=True) IsAuthenticated test_anonymous_caller_is_refused_with_401 (both permission modes); test_anonymous_caller_of_an_unknown_course_is_refused_with_401
@login_required on _get_item_in_course covered by IsAuthenticated —
user_has_course_permission(user, COURSES_VIEW_COURSE, course_key, READ) in _get_item_in_course CanViewCourse, same call, same key test_user_without_a_role_is_refused_with_403, test_limited_staff_is_refused, test_staff_of_another_course_is_refused, test_a_role_on_another_course_is_refused, test_other_roles_are_refused, test_legacy_course_staff_without_a_policy_role_is_refused, test_a_custom_course_is_refused_to_everyone, test_a_custom_course_is_refused_to_staff_and_course_staff
has_studio_read_access on the clipboard's source course not served by v2; still enforced at /api/content-staging/v1/clipboard/ —
— (v1 has no guard) non-course context or version-only key → 404 before any course lookup test_a_key_outside_any_course_is_not_found_without_a_course_lookup, test_a_key_naming_only_a_course_version_is_not_found_without_a_course_lookup, test_keys_outside_any_course_are_not_found

Order: 401, then 404 for an unknown course, then 403, then 404 for the block, so a caller without access cannot learn which blocks exist (test_a_missing_block_is_404_for_a_reader_and_403_for_anyone_else). Allowed callers: course staff and instructor, org staff and instructor, global staff and superusers, and with policy-based permissions the roles that grant courses.view_course.

Gate report

Run on the host against #39229's head (53ea5f44a1), which this branch stacks on. No platform environment was available, so four gates were skipped. The test module has not been run locally, and CI is its first run.

Gate Result
versions PASS: 4 ALLOWED (v1/views/vertical_block.py, v2/authoring_urls.py, v2/views/tests/helpers.py, v2/views/tests/test_course_outline.py), 4 ADDED, nothing protected touched (17 shared legacy modules protected)
hygiene PASS (0 FAIL, 0 WARN), 11 files
ruff clean
schema SKIPPED (no platform environment)
urls SKIPPED (no platform environment)
old-tests SKIPPED (no platform environment)
tests SKIPPED (no platform environment)

Follow-ups

  • SDK: the operation carries openedx-platform-sdk, so the next openedx-platform-sdk regeneration gains v2_containers_retrieve.
  • frontend-app-authoring: src/course-unit/data/api.ts can move getVerticalData to /api/authoring/v2/containers/{usage_key}/?view=full.
  • [API] Containers #39063 (Containers): its children endpoint should mount at containers/{usage_key}/children/ under this member.
  • The clipboard endpoint writes on GET. /api/content-staging/v1/clipboard/ can save a CourseOverview row and send IMPORT_COURSE_DETAILS on read, as v1 container_handler did. Issue to be filed.
  • openedx-authz PolicyCacheControl. The first permission check on a fresh database inserts the cache row on a read. Seeding it by migration would make reads write-free. Issue to be filed.
  • JSON 404 for unmatched /api/ addresses. A platform-level fix (CMS handler404 answering with the envelope for /api/ paths) instead of per-area catch-alls. Issue to be filed.
  • edx-drf-extensions: catalogue MethodNotAllowed and NotAcceptable so 405/406 stop being typed internal. Issue to be filed.
  • Authentication consistency across /api/authoring/v2/: textbooks and video_settings keep SessionAuthenticationAllowInactiveUser; the outline and this view use the default. One policy for new versions of such endpoints should be agreed.

Taimoor Ahmed and others added 7 commits October 21, 2026 16:32
The course index existed only at v1, with Bearer auth, a legacy error
body, a slashless address, the whole screen payload as its only shape,
and writes on GET (pre-1900 start dates re-saved, a course overview
built for the clipboard title).

v2 serves GET /api/authoring/v2/courses/{course_key}/outline/. By
default it returns a thin outline tree two levels deep, where every cut
node carries a followable children_url. ?view=full returns v1's screen
payload under v1's names, without the clipboard. Reads write nothing.
Authentication is the platform default, the course-view permission and
its legacy fallback are kept in CanViewCourse, and errors use the
standardized envelope. The v1 course index is marked deprecated in the
CMS schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docstring-only markers naming the v2 successor and DEPR openedx#39128. With
docstrings stripped the module matches the base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mypy checks rest_api/v2/views, and pytz ships no type stubs, so the
import failed make check-types. The standard library's UTC gives the
same timezone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_course, count_mongo_calls, sql_writes, record_effects,
FIRST_LOOKUP_WRITES and the schema conversion helper were defined in the
course outline test module, and the container tests need them too.
Importing one test module from another couples them, and one of the
helpers was private. They move verbatim into v2/views/tests/helpers.py,
the existing module for helpers shared by the v2 authoring endpoint tests,
and the outline tests import them from there; _as_json_schema becomes
as_json_schema. No test changes.
The v2 container resource is addressed by a usage key, not a course key.
CanViewCourse now reads the course from the usage_key URL argument on a
route without course_key, and makes the same courses.view_course check
as before. A usage key whose context is not a course run, such as a
content library block, is answered with 404 before any course lookup,
because the existence lookup fails on such a key. Routes with a
course_key behave exactly as before.
GET /api/contentstore/v1/container_handler/{usage_key_string} serves the
Studio Unit page. It accepts Bearer tokens and inactive users' sessions,
answers errors with {"detail"} bodies, a missing block with an empty 400
and a section, subsection or course key with a 500, always returns the
whole page payload, and reads the user's clipboard on GET, which can save
a CourseOverview row and send IMPORT_COURSE_DETAILS.

GET /api/authoring/v2/containers/{usage_key}/ is its successor. A
container is any block in the course outline with children: a section,
a subsection, a unit or a container inside a unit, as in the course
outline and content libraries APIs. The default view is a thin
representation whose reads depend on nesting depth, not course size.
view=full returns v1's Unit page payload, without user_clipboard and
action, under the same field names, for a unit or a container inside
one. Errors use the standard envelope; authentication is the platform
default; access is IsAuthenticated plus CanViewCourse, the same check v1
makes. Parity tests compare view=full with v1 field by field; the only
declared differences are the clipboard and the action echo.
…ntainer

GET /api/contentstore/v1/container_handler/{usage_key_string} now has a
conforming successor at /api/authoring/v2/containers/{usage_key}/. The
v1 operation is marked deprecated in the CMS schema through an exact
SUPERSEDED_PATHS entry, so the v1 children routes beside it stay current,
and its docstrings name the successor and the deprecation ticket. The
view itself is unchanged.
@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Oct 22, 2026
@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @taimoor-ahmed-1!

This repository is currently maintained by @openedx/wg-maintenance-openedx-platform-oncall.

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.

🔘 Update the status of your PR

Your PR is currently marked as a draft. After completing the steps above, update its status by clicking "Ready for Review", or removing "WIP" from the title, as appropriate.


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.

This branch has not been deployed

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

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Status: Waiting on Author

Development

Successfully merging this pull request may close these issues.

3 participants