Skip to content

feat: standardize the Grades API as v2 at /api/grade/v2/ - #39172

Open
taimoor-ahmed-1 wants to merge 6 commits into
openedx:masterfrom
edly-io:feat/standardize-grades-api
Open

taimoor-ahmed-1 wants to merge 6 commits into
openedx:masterfrom
edly-io:feat/standardize-grades-api

Conversation

@taimoor-ahmed-1

@taimoor-ahmed-1 taimoor-ahmed-1 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR standardizes the Grades functional area (#39057, umbrella #38137) as a new version, /api/grade/v2/, following the API standardization ADRs docs/decisions/0025 to 0038, OEP-66 and OEP-69. /api/grades/v1/ stays mounted, unchanged in behaviour, and marked deprecated.

Why a new version. Nothing in v1 was standardized, and every endpoint needs contract changes that existing callers would notice:

  • errors use the standard envelope;
  • cursor pagination becomes page pagination with the seven-field envelope;
  • Bearer authentication is dropped;
  • addresses change: a singular API name, plural collections, username and opaque keys instead of numeric ids, and no verbs in paths;
  • 200 {"success": false} becomes a real 404;
  • bulk overrides are validated as a whole batch.

Changes like these go in a new version, not the current one.

Builds on #39078 (URL structure), which has merged. This branch is rebased onto master and uses #39078's register_url_converters(), schema path settings and LMS deprecation hook, adding no parallel ones. Every commit on the branch belongs to this PR.

Change table

Commit v2 endpoint(s) v1 source New behaviour
shared infrastructure mount, schema filter, pagination schema, domain error types, permissions, JSON 404 catch-alls n/a /api/grade/v2/ and /api/grades/v1/ are admitted to the LMS schema; all nine v1 operations are marked deprecated
course grades GET course_grades/, GET course_grades/{username},{course_key}/ courses/, courses/{course_id}/, section_grades_breakdown/ The breakdown is ?view=full, for global staff only (as E8). Rows are scoped: staff see all, a restricted token sees its org and user filters, everyone else sees their own
gradebook entries GET courses/{course_key}/gradebook_entries/[{username}/] gradebook/{course_id}/ Access is checked before existence and the flag. ?view=minimal. Invalid filter values give 400 (v1 gave 500)
overrides POST courses/{course_key}/subsection_grade_overrides/ gradebook/{course_id}/bulk-update JSON only, at most 100 items. If any item is invalid, nothing is stored and errors are keyed like overrides[1].username
subsection grades GET subsection_grades/{username},{usage_key}/, .../override_history_records/ subsection/{subsection_id}/ CCX coaches admitted (v1 refuses them); the learner must be enrolled; an unavailable subsection gives 404
grading policy GET courses/{course_key}/grading_policy/ policy/courses/{course_id}/, gradebook/{course_id}/grading-info One resource. The default view keeps grading-info's access; ?view=minimal keeps the policy endpoint's access and content. A read never creates a course overview
submission histories GET courses/{course_key}/submission_histories/ submission_history/{course_id}/ Page pagination; problem data only with ?view=full; an unknown course gives 404
v1 deprecation markers none all nine Docstring lines only

ADRs applied

  • 0025 serializers - separate request and response serializers, help_text on every field, nullable and view-dependent fields marked, and a grade_v2.* ref_name on each
  • 0026 permissions - every v1 check is mapped to a permission class, checked per view, and compared against v1 caller by caller
  • 0027 schema / docs - @extend_schema and an explicit operation id on all nine operations, with examples for the batch override. The only v2 generator warning is "could not resolve authenticator" for the platform's default authentication classes, the same as Enrollment v2 today (library follow-up)
  • 0028 viewsets - views, services and serializers are separated, with GenericViewSets or ViewSets on explicit path() routes, but not router-registered. A DRF router names routes course_grade-list, cannot carry the {username},{course_key} composite or the key converters, and publishes an extra {id} operation. The per-row query growth is pinned below v1's
  • 0029 errors - the standard envelope everywhere; v1's 200 success:false becomes 404; no exception text in any body. Until the library catalogues them, a malformed JSON body (400), 405, 406 and 415 report type internal (library follow-up); tests pin this and flip when the pin moves
  • 0030 GET idempotence - met for N7, N8, N9 and the N3/N4 row reads. Three first-read writes are kept from v1 and pinned by tests: gradebook entries copy the course's cohort settings; a computed subsection grade creates the learner's anonymous-ID row; a computed grade (N6, and staff-only ?view=full) can assign an uncohorted learner to a cohort, writing two cohort tables and sending COHORT_MEMBERSHIP_CHANGED and two tracking events. The tracking issue is not filed yet
  • 0031 merged endpoints - the area has no action family; bulk-update becomes a POST to a noun. The two consolidations (breakdown into course grades, grading-info with policy) are view variants that keep each legacy check for the data it guarded, as caller-by-caller comparison tests show
  • 0032 pagination - the seven-field envelope, 10 per page, 100 max, on every list
  • 0033 filtering - FilterSets with typed, documented parameters, an ordering allow-list with an id tie-break, and no parameter aliases in v2
  • 0034 authentication - the platform default on every v2 view; Bearer and allow-inactive dropped
  • 0035 MFE config - n/a, no Grades endpoint serves front-end or site configuration
  • 0036 nested JSON - ?view=full or ?view=minimal where a representation is heavy; view-dependent fields are optional in the schema; other values give 400
  • 0037 versioning - v2 is added, and v1 is frozen (docstring markers only) and deprecated in the schema. Still owed before merge: the DEPR issue (not filed yet; the eight v1 markers link a placeholder that will be replaced), a named removal release, and the migration guide below published there
  • 0038 URL structure - singular API name, plural collections, comma composites, opaque-key converters, required trailing slashes, unique snake_case names, and JSON 404s for every unmatched address under the mount
  • OEP-66 queryset scoping - a real policy on course grades, implementing the library's ScopingPolicy protocol; FullScopePolicy on the other lists; filters only narrow
  • OEP-69 conventions - no governance identifiers in code or schema text, and no default-auth declarations. Two local stopgaps have library follow-ups: the pagination response schema and the JSON 404 catch-all view

Compliance matrix

Decision Status How it is met, or why it is excluded Evidence
0025 met Request and response serializers differ on N5; help_text on every field; allow_null where values can be null (for example display_name on an unnamed subsection); fields dropped by a view are required=False test_schema.py::test_override_request_body_differs_from_its_response, test_gradebook_list_is_the_page_envelope (required {username, percent}), test_grading_policy_operation, GradingPolicyUnnamedSubsectionTest
0026 met Every v1 check maps to a class (Authorization below); access runs before existence, flag and freeze checks CourseGradeFullViewAccessTest, CourseGradeFullViewParityTest, RestrictedTokenUserFilterParityTest, GradingPolicyAccessTest, GradingPolicyCcxAccessTest, HasGradebookAccess* in test_infrastructure.py, deny tests in every endpoint module
0027 met Full extend_schema; explicit operation ids; request and error examples on N5; the default-authenticator warning is the only v2 warning test_every_operation_id_is_the_declared_one, test_override_examples, test_v2_generates_no_warning_but_the_default_authentication_one, schema gate
0028 partial Layers separated; ViewSet types match the data path; not router-registered (reason above) query-count tests: test_full_list_grows_by_a_fixed_count_per_row, test_list_grows_by_one_query_per_row, *QueryCountTest
0029 met, with library caveat Envelope everywhere; domain types grades/writable-gradebook-disabled, grades/grades-frozen, grades/subsection-unavailable; no exception text assert_error_envelope across modules; test_failure_while_storing_records_nothing; test_failure_while_recomputing_keeps_every_override_and_answers_500; UnmatchedAddressTest; the 400/405/406/415 internal pins
0030 not met for three declared writes Pinned as v1 behaviour; every other read path proven write-free, HEAD included, on both databases *ReadOnlyTest, GradebookColdCohortSettingsTest, ComputedGradeAnonymousIdTest, CourseGradeCohortAssignmentTest, the cohort no-write tests on N3/N4/N7/N8/N9, test_course_without_an_overview_is_not_created_one
0031 met No action family; consolidations keep each legacy check per view CourseGradeFullViewParityTest, assert_views_follow_v1 (N8, with fields per view)
0032 met GradePagination(DefaultPagination) adds only the response schema CourseGradePagingTest, GradebookRowTest.test_paging, schema tests
0033 met FilterSets; excluded_course_roles published as a repeatable array and cohort_id as an integer; unknown ordering gives 400 CourseGradeFilterTest, GradebookFilterTest, test_gradebook_filter_parameter_types
0034 met No authentication_classes on any v2 view test_deactivated_account_*, restricted-token tests on N5 and N9, hygiene H4 clean
0035 n/a No configuration endpoint none
0036 met ?view=full on N1/N2 (staff) and N9; ?view=minimal on N3/N4 and N8 *RepresentationTest, test_minimal_view_reads_the_course_one_level_deep
0037 partial v1 docstring-only; nine v1 operations deprecated: true; DEPR issue, release and guide owed versions gate; syntax-tree comparison (docstrings blanked) identical; test_every_legacy_operation_is_deprecated
0038 met Plan URL table as built; 15 routes checked GradeV2UrlNameTest, GradeV2ResolutionTest, SupersededAddressTest, test_no_route_is_shadowed_by_a_not_found_route, UnmatchedAddressTest, reverse() literal tests, urls gate
OEP-66 met CourseGradeScopingPolicy (staff all, restricted token by org and first user filter as v1, else own rows); FullScopePolicy elsewhere CourseGradeVisibilityTest, RestrictedTokenListTest, test_filters_do_not_widen_a_learners_view
OEP-69 met See the box; hygiene H3 flags CourseGradeScopingPolicy by name, but it implements the library's protocol rather than copying a library class hygiene gate (explained below)

Backward compatibility

  • Versions gate: PASS. v1 changes are class-docstring lines only, and the syntax trees match the base with docstrings blanked. The unversioned legacy files (rest_api/urls.py, rest_api/serializers.py, apps.py, the shared view utils, and the enrollment form and throttle) are protected.
  • URLs gate: PASS. Every base address and URL name still resolves to the same view.
  • Schema gate: PASS, 0 breaking changes. v1 was not in the LMS schema before: its nine operations are newly published, deprecated, and bring 30 generator warnings plus one operation-id collision (api_grades_v1_courses_retrieve, _2), listed under Gate report. So the schema gate is not evidence that v1 is unchanged; the syntax-tree comparison and the untouched v1 tests are.
  • v1 tests: 193, untouched and green.
  • Parity tests compare v1 and v2 on one fixture and fail on any undeclared difference, or on a declared one that doesn't occur. The declared differences are the migration guide below.

Migration guide (v1 → v2)

Everywhere

  • JWT or session instead of Bearer.
  • Page pagination (count, num_pages, current_page, start, next, previous, results; page, page_size up to 100) instead of the cursor.
  • Errors use the standard envelope.
  • Database ids are not published (user_id, id, grade_id, history_user_id); learners are named by username.
  • Parameters: course_id → course_key, module_id/subsection_id → usage_key, assignment → assignment_usage_key, history_record_limit → page_size.

Course grades (E1, E2, E8 → N1, N2)

  • E8's current_grade (0 to 100) becomes percent (0 to 1). This is a unit change, not only a rename.
  • section_breakdown[].sequential_id becomes usage_key, and is null on entries that name no subsection.
  • The breakdown needs ?view=full, for global staff only. Rows carry email and letter_grade with or without it.
  • A single-learner read returns an object, not a one-element array.
  • A single-learner read no longer hides a master's-track learner's email.
  • Row order is by enrollment id; E8 grouped a page by course.
  • A restricted token needs grades:read and an org filter to list rows; a restricted token for a staff service user no longer gets E8's breakdown.
  • Statuses:
    • reading another learner gives 404 (v1: 403);
    • listing with no course gives your own rows (v1: 403);
    • an unknown course gives an empty page (v1: 404).
  • A row whose grade cannot be computed is left out of results but still counted in count, as in v1.

Gradebook entries (E4 → N3, N4)

  • total_users_count and filtered_users_count are removed; count is the filtered total. For "N of M learners", make a second call without filters and page_size=1.
  • A cohort_id naming another course's cohort, or no cohort, gives an empty page (v1: 404).
  • cohort_id=1.5 and other invalid filter values give 400 (v1: 500 or ignored).
  • A missing course gives 403 to callers without access and 404 to callers with it.
  • A deactivated session gives 401 (v1: 403).

Overrides (E5 → N5)

  • Body {"overrides": [{username, usage_key, earned_all_override, possible_all_override, earned_graded_override, possible_graded_override, comment}]}, JSON only, at most 100 items.
  • If any item is invalid, nothing is stored: 400, with errors keyed like overrides[1].username. v1 applied the valid items and returned 202 or 422.
  • The usage key must belong to the course in the address. comment is at most 300 characters.
  • If grade recalculation fails after the overrides are stored, the response is 500 and the overrides stay stored; sending the same batch again recalculates them.

Subsection grades (E7 → N6, N7)

  • The history moves to override_history_records/, paginated, newest first.
  • An unavailable subsection gives 404 grades/subsection-unavailable (v1: 200 success:false).
  • The learner must be enrolled. CCX coaches are admitted.

Grading policy (E3, E6 → N8)

  • E6's can_see_bulk_management becomes bulk_management_enabled, and assignment_types is a list, not a dict.
  • E3's content is ?view=minimal: assignment_type/count/dropped become type/min_count/drop_count, and it adds grade_cutoffs and each type's short_label.
  • E6's all parameter is dropped: it changed load depth, not output.
  • A course with no overview is still served, and no overview is created.

Submission histories (E9 → N9)

  • user → username, location → usage_key, name → display_name, submission_history → submissions.
  • Per-row course_id and course_name are removed; the course is the address.
  • Problem data only with ?view=full.
  • An unknown course gives 404.
  • The nested problems[] and submissions[] are not bounded, as in v1; the page bounds learners only.

In-place edits

File Category Evidence
lms/djangoapps/grades/rest_api/v1/views.py docstring-only deprecation marker syntax trees identical with docstrings blanked; v1 tests untouched
lms/djangoapps/grades/rest_api/v1/gradebook_views.py docstring-only deprecation marker same

Shared service files, not frozen:

  • lms/urls.py: the mount.
  • lms/lib/spectacular.py: the filter admits grades, and the existing hook marks v1 deprecated.
  • lms/lib/tests/test_spectacular.py.

drf-spectacular prefers method docstrings, so the successor text reaches the schema description of one v1 operation; all nine carry deprecated: true.

Error-format decision

Versioned. v1 keeps its bodies, and v2 uses the envelope. v1's main caller is frontend-app-gradebook, which migrates on its own schedule.

Deprecations

All nine /api/grades/v1/ operations are deprecated: true in the LMS schema, and each v1 view's docstring names its successor. The DEPR issue is not filed yet; the v1 docstring markers link a placeholder that is replaced once it exists. Both mounts stay for at least one named release, to be named in the DEPR issue.

Authorization

v1 check v2
E1/E2 JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS, scope grades:read COURSE_GRADE_READ_ACCESS (endpoint), plus CourseGradeScopingPolicy (rows: staff all, restricted token by org and first user filter, else own)
E8 IsStaff, plus the middleware's NotJwtRestrictedApplication HasGradeBreakdownAccess on ?view=full: IsAdminUser & NotJwtRestrictedApplication
E4/E5/E6 course_author_access_required (with #39145's CCX branch) HasGradebookAccess (the CCX check in the shared lms/djangoapps/grades/rest_api/helpers.py; v1 keeps its own copy until it is removed)
E4/E5 verify_course_exists, verify_writable_gradebook_enabled CourseExists, WritableGradebookEnabled, after access
E5 are_grades_frozen GradesNotFrozen, last
E7 inline has_course_author_access (no CCX branch) HasGradebookAccess, which gains CCX, plus enrollment of the named learner
E3 inline has_access('staff') COURSE_GRADING_POLICY_READ_ACCESS = IsAuthenticated & (HasCourseStaffAccess | HasGradebookAccess), plus HasGradebookAccessUnlessMinimal so the default view keeps E6's access
E9 IsStaff, throttle, can_disable_rate_limit IsAdminUser, same throttle and switch

Deny-path tests cover anonymous callers, learners, staff of another course, CCX outsiders, restricted tokens with and without scope, deactivated sessions, and, for callers without access, whether a course exists or which flags are set.

Gate report

run_gates.sh --base <#39078 rebased onto master> --service lms --prefix /api/grade/v2/, run in a Tutor dev LMS container (Python 3.12, edx-drf-extensions 10.9.0). No gate was skipped.

These results are from before the rebase onto master and the review fixes, and have not been re-run since. After them, on the host: the versions gate (base master) gives PASS with only the two docstring-only v1 files allowed, hygiene gives the same 1 H3 FAIL and 1 H3 WARN explained below, ruff is clean on the changed files, and lms/lib/tests/test_spectacular.py passes (8 tests). The Grades v2 and v1 suites need the full LMS test environment and are left to CI.

versions   PASS  v1 edits are docstring-only; the protected legacy files are untouched
hygiene    FAIL  4 H1 on Tutor's container-only envs/tutor settings (not in this diff);
                 1 H3 on CourseGradeScopingPolicy (see notes); 1 H3 WARN on GradePagination
schema     PASS  0 BREAKING, 46 WARN, 18 INFO
urls       PASS  0 FAIL, 0 WARN
old-tests  PASS  193 passed (grades v1, unmodified)
tests      PASS  539 passed (grades v2, LMS spectacular, enrollments v2), -Wd

The schema gate's 46 warnings:

  • 14 are the default-authenticator warning on the nine v2 operations;
  • 30 are v1's generator warnings (no serializers on frozen v1 views);
  • 1 is the v1 operation-id collision, since fixed: /api/grades/v1/courses/{course_id}/ is now suffixed _legacy, like the colliding Enrollment v2 address;
  • 1 is v1's bulk-update having no documented request body.

Hygiene notes.

  • H3 flags CourseGradeScopingPolicy by name. It implements the library's ScopingPolicy protocol through ScopedQuerysetMixin, which is the intended use.
  • H3 warns on GradePagination, the documented stopgap.
  • In a Tutor container, H1 also reports Tutor's bind-mounted envs/tutor settings. Those are untracked and not in this diff.

Query counts (v1 → v2, same fixture)

Read v1 v2
N1 list 17 13
N1 ?view=full at 5 / 10 rows 50 / 85 (+7 per row) 46 / 76 (+6 per row)
N2 11 9
N3 at 5 / 10 rows 33 / 43 (+2 per row) 27 / 32 (+1 per row, the authorization policy-version read)
N4 17 15
N5 (one item) 48 49
N6 / N7 12 9 / 10
N8 default / minimal 12 / 9 14 / 9
N9 223 14

v1 cached the gradebook's unfiltered user count for an hour. v2 counts the filtered page on every request, and the staff cross-course N1 list counts all active enrollments.

Follow-ups

  • edx-drf-extensions:
    • a pagination response schema;
    • cataloguing the DRF built-in errors (parse, 405, 406, 415);
    • schema extensions for the default authentication classes;
    • the error handler logging tracebacks and rolling back ATOMIC_REQUESTS when it converts an exception to a 500;
    • nested per-item validation errors;
    • a JSON 404 catch-all view;
    • turning restricted-token org and user filters into a queryset filter.
  • Issues:
    • read-path writes (cohort settings, anonymous ID, cohort assignment), not filed yet;
    • the v1 subsection endpoint's CCX gap (cc feat: change in endpoint to support CCX id #39145's author);
    • frontend-app-gradebook calling bulk-update/history/, which matches no route.
  • SDK: course grades and grading policy carry the openedx-platform-sdk tag, so the SDK needs regenerating.
  • v1 removal: the CCX predicate and the problem-listing helper are already shared (rest_api/helpers.py), so v2 code imports nothing from v1. What remains is in tests: the test mixin, and the v1 patch targets in the tests that compare v1 with v2.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Sep 30, 2026
@openedx-webhooks

openedx-webhooks commented Sep 30, 2026 •

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.

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.

@github-project-automation github-project-automation Bot moved this to Needs Triage in Contributions Sep 30, 2026
@taimoor-ahmed-1
taimoor-ahmed-1 force-pushed the feat/standardize-grades-api branch from d08b9e0 to 2f62d22 Compare October 7, 2026 07:02
@taimoor-ahmed-1
taimoor-ahmed-1 marked this pull request as ready for review October 7, 2026 09:48
@taimoor-ahmed-1
taimoor-ahmed-1 requested review from a team, feanil and kdmccormick as code owners October 7, 2026 09:48

@feanil feanil left a comment

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.

This PR is pretty hard to read with all the other changes from the other PR in the file changes list, can you make the other PR the base or use PR stacks to make it easier to review?

Comment thread lms/lib/spectacular.py Outdated
# The one legacy address that differs from its conforming twin only by the
# trailing slash, so both tokenize to the same operationId. The other legacy
# addresses already have distinct ids and keep them.
COLLIDING_LEGACY_PATH = "/api/enrollment/v2/enrollments"

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.

The nine v1 operations are newly published here, and GET /api/grades/v1/courses/ and GET /api/grades/v1/courses/{course_id}/ tokenize to the same operationId. drf-spectacular breaks the tie in registration order, so the second one is published as api_grades_v1_courses_retrieve_2. That is the same collision the Enrollment v2 pair has.

Add /api/grades/v1/courses/{course_id}/ to the paths LmsAutoSchema.get_operation_id suffixes with _legacy, and update the comment above it. I generated the LMS schema with that change and the ids come out as api_grades_v1_courses_retrieve and api_grades_v1_courses_retrieve_legacy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks — fixed in 5b83e8d. /api/grades/v1/courses/{course_id}/ is now in COLLIDING_LEGACY_PATHS, so LmsAutoSchema suffixes it _legacy, and the comment above it covers both collisions. A new test in lms/lib/tests/test_spectacular.py generates the schema for the two routes and pins api_grades_v1_courses_retrieve / api_grades_v1_courses_retrieve_legacy; against the previous hook it reproduces api_grades_v1_courses_retrieve_2.

from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.grades.api import is_writable_gradebook_enabled
from lms.djangoapps.grades.grade_utils import are_grades_frozen
from lms.djangoapps.grades.rest_api.v1.gradebook_views import _has_ccx_gradebook_access

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.

nit: _has_ccx_gradebook_access is a private name in the v1 views, and services.py::submission_history_rows also calls SubmissionHistoryView.get_problem_blocks. Not a blocker but while you're here, you could move both to a shared module now so v1 can be removed without touching v2.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 16a7bbb: both now live in an unversioned lms/djangoapps/grades/rest_api/helpers.py (has_ccx_gradebook_access, get_problem_blocks), and v2 code imports nothing from v1. I left v1's own copies in place rather than making v1 import the shared ones, because v1 is frozen and its tests patch v1.gradebook_views.get_course_by_id; a true move would have meant editing a v1 test. The duplication ends when v1 is removed. The v2 tests that compare v1 and v2 on a CCX course now patch both lookups.

@taimoor-ahmed-1 taimoor-ahmed-1 self-assigned this Oct 21, 2026
Taimoor Ahmed and others added 6 commits October 21, 2026 16:21
Admit /api/grade/v2/ and /api/grades/v1/ to the LMS OpenAPI document,
and mark every /api/grades/v1/ operation deprecated through the
existing post-processing hook instead of adding a second one.
Grades v1 had no standardized endpoint: bare error bodies, cursor
pagination, Bearer auth, numeric-id addresses, verb paths and
200 {"success": false}. Fixing any of it changes the contract, so v2
is a new version and v1 stays mounted as it is.

Nine operations replace the nine v1 routes: course grades (with the
staff-only section breakdown as ?view=full), gradebook entries,
all-or-nothing subsection grade overrides, subsection grades and their
override history, the grading policy, and submission histories. Every
v1 permission check maps to a permission class, and tests compare v2
with v1 caller by caller, CCX included. Parity tests compare v1 and v2
responses and fail on any undeclared difference.
Docstring-only markers on the nine v1 views, naming each successor.
No statement, decorator or import changes; the syntax trees match
the base with docstrings blanked.
pylint's missing-function-docstring check flagged three helpers in the
v2 grade tests.
GET /api/grades/v1/courses/ and GET /api/grades/v1/courses/{course_id}/
tokenize to the same operationId, so drf-spectacular published the second
as api_grades_v1_courses_retrieve_2, depending on registration order. The
detail address is now suffixed _legacy, as the colliding Enrollment v2
address already is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ades versions

Grades v2 imported _has_ccx_gradebook_access from the v1 gradebook views
and called SubmissionHistoryView.get_problem_blocks, so removing v1 would
have meant editing v2. Both now live in lms/djangoapps/grades/rest_api/helpers.py,
which v2 imports.

v1 keeps its own copies, unchanged, because it is frozen until it is
removed. The tests that compare v1 with v2 on a CCX course patch both
lookups of the master course.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@taimoor-ahmed-1
taimoor-ahmed-1 force-pushed the feat/standardize-grades-api branch from e66d32d to 16a7bbb Compare October 21, 2026 11:29
@taimoor-ahmed-1

Copy link
Copy Markdown
Contributor Author

@feanil thanks for the review. #39078 has merged, so I rebased this branch onto master: the diff now holds only the Grades changes (the four original commits, plus two for your inline comments, 5b83e8d and 16a7bbb). The description is updated to match, including a note that the gate results predate the rebase. Could you take another look?

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: Needs Triage

Development

Successfully merging this pull request may close these issues.

3 participants