Skip to content

Commit 794c440

Browse files
Faraz32123claude
andauthored
feat: fix OpenAPI schemas for SDK compatibility (#38845)
* feat: fix OpenAPI schemas for SDK compatibility Three changes driven by integration testing with openedx-platform-sdk: 1. authoring_grading serializer — expose grade_cutoffs, grace_period, and minimum_grade_credit as proper typed schema fields. CourseGradingModel.update_from_json requires all three, but they were absent from the serializer so the generated SDK had no typed fields for them, forcing callers to smuggle values in via additional_properties. Adds GracePeriodSerializer (hours / minutes / seconds) and documents that grade_cutoffs and grace_period must be present in every PATCH. 2. course_details serializer — mark certificate_available_date as allow_null=True. The field is legitimately null for many courses, but the missing flag caused the SDK client to crash with fromisoformat(None) when deserialising the response. 3. v4 HomeCoursesViewSet — add _HomeCoursesAutoSchema (same pattern as the existing v3 _HomeAutoSchema). The viewset builds its paginator manually inside list() rather than via pagination_class, so drf-spectacular's _is_list_view() returns True and generates an array schema. Overriding _is_list_view() for the list action and using an inline_serializer makes the schema reflect the actual paginated object shape (count, num_pages, current_page, start, next, previous, results). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: sort imports in v4 home view to satisfy ruff I001 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: wrap long lines in v4 home view inline_serializer fields Fixes pylint C0301 (line-too-long) — lines 175 and 177 exceeded the 120-character limit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8ec46fe commit 794c440

3 files changed

Lines changed: 60 additions & 3 deletions

File tree

cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,37 @@ class Meta:
1818
ref_name = "authoring_grading.Graders.v0"
1919

2020

21+
class GracePeriodSerializer(serializers.Serializer):
22+
"""Serializer for grace period (hours / minutes / seconds)."""
23+
hours = serializers.IntegerField(default=0)
24+
minutes = serializers.IntegerField(default=0)
25+
seconds = serializers.IntegerField(default=0, required=False)
26+
27+
class Meta:
28+
ref_name = "authoring_grading.GracePeriod.v0"
29+
30+
2131
class CourseGradingModelSerializer(serializers.Serializer):
2232
""" Serializer for course grading model data """
2333
graders = GradersSerializer(many=True, allow_null=True, allow_empty=True)
34+
grade_cutoffs = serializers.DictField(
35+
child=serializers.FloatField(),
36+
required=False,
37+
help_text=(
38+
"Mapping of letter grade to minimum score (0.0–1.0). "
39+
"Required by CourseGradingModel.update_from_json — must be included in every PATCH."
40+
),
41+
)
42+
grace_period = GracePeriodSerializer(
43+
allow_null=True,
44+
required=False,
45+
help_text="Grace period duration. Pass null to clear the grace period.",
46+
)
47+
minimum_grade_credit = serializers.FloatField(
48+
required=False,
49+
allow_null=True,
50+
help_text="Minimum passing score for credit eligibility (0.0–1.0).",
51+
)
2452

2553
class Meta:
2654
ref_name = "authoring_grading.CourseGrading.v0"

cms/djangoapps/contentstore/rest_api/v1/serializers/course_details.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class CourseDetailsSerializer(serializers.Serializer):
2929
about_sidebar_html = serializers.CharField(allow_null=True, allow_blank=True)
3030
banner_image_name = serializers.CharField(allow_blank=True)
3131
banner_image_asset_path = serializers.CharField()
32-
certificate_available_date = serializers.DateTimeField()
32+
certificate_available_date = serializers.DateTimeField(allow_null=True)
3333
certificates_display_behavior = serializers.CharField(allow_null=True)
3434
course_id = serializers.CharField()
3535
course_image_asset_path = serializers.CharField(allow_blank=True)

cms/djangoapps/contentstore/rest_api/v4/views/home.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""HomeCoursesViewSet for getting courses available to the logged-in user (v4)."""
22

3-
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
3+
from drf_spectacular.openapi import AutoSchema
4+
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema, inline_serializer
45
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
56
from edx_rest_framework_extensions.auth.session.authentication import (
67
SessionAuthenticationAllowInactiveUser,
78
)
89
from edx_rest_framework_extensions.paginators import DefaultPagination
10+
from rest_framework import serializers as _serializers
911
from rest_framework import viewsets
1012
from rest_framework.permissions import IsAuthenticated
1113
from rest_framework.request import Request
@@ -18,6 +20,15 @@
1820
from openedx.core.lib.api.mixins import StandardizedErrorMixin
1921

2022

23+
class _HomeCoursesAutoSchema(AutoSchema):
24+
"""Custom AutoSchema that treats the 'list' action as a single-object response."""
25+
26+
def _is_list_view(self, serializer=None):
27+
if self.view.action == 'list':
28+
return False
29+
return super()._is_list_view(serializer)
30+
31+
2132
class HomePageCoursesPaginator(DefaultPagination):
2233
"""
2334
ADR 0032 - standard pagination for the Studio home courses list (v4).
@@ -136,6 +147,7 @@ class HomeCoursesViewSet(StandardizedErrorMixin, viewsets.ViewSet):
136147
is used instead of the default ``SessionAuthentication``.
137148
"""
138149

150+
schema = _HomeCoursesAutoSchema()
139151
authentication_classes = (JwtAuthentication, SessionAuthenticationAllowInactiveUser)
140152
permission_classes = (IsAuthenticated,)
141153
serializer_class = CourseHomeTabSerializerV4
@@ -154,7 +166,24 @@ def get_serializer(self, *args, **kwargs):
154166
parameters=_HOME_COURSES_QUERY_PARAMETERS,
155167
responses={
156168
200: OpenApiResponse(
157-
response=CourseHomeTabSerializerV4,
169+
response=inline_serializer(
170+
name="PaginatedV4HomeCoursesResponse",
171+
fields={
172+
"count": _serializers.IntegerField(help_text="Total number of courses."),
173+
"num_pages": _serializers.IntegerField(help_text="Total number of pages."),
174+
"current_page": _serializers.IntegerField(help_text="Current page number."),
175+
"start": _serializers.IntegerField(
176+
help_text="Zero-based index of the first item on this page."
177+
),
178+
"next": _serializers.CharField(
179+
allow_null=True, help_text="URL for the next page, or null."
180+
),
181+
"previous": _serializers.CharField(
182+
allow_null=True, help_text="URL for the previous page, or null."
183+
),
184+
"results": CourseHomeTabSerializerV4(),
185+
},
186+
),
158187
description="Paginated course list retrieved successfully.",
159188
),
160189
401: _UNAUTHENTICATED_RESPONSE,

0 commit comments

Comments
 (0)