diff --git a/cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py b/cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py index e42c3e2ee397..09799263798a 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py +++ b/cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py @@ -18,9 +18,37 @@ class Meta: ref_name = "authoring_grading.Graders.v0" +class GracePeriodSerializer(serializers.Serializer): + """Serializer for grace period (hours / minutes / seconds).""" + hours = serializers.IntegerField(default=0) + minutes = serializers.IntegerField(default=0) + seconds = serializers.IntegerField(default=0, required=False) + + class Meta: + ref_name = "authoring_grading.GracePeriod.v0" + + class CourseGradingModelSerializer(serializers.Serializer): """ Serializer for course grading model data """ graders = GradersSerializer(many=True, allow_null=True, allow_empty=True) + grade_cutoffs = serializers.DictField( + child=serializers.FloatField(), + required=False, + help_text=( + "Mapping of letter grade to minimum score (0.0–1.0). " + "Required by CourseGradingModel.update_from_json — must be included in every PATCH." + ), + ) + grace_period = GracePeriodSerializer( + allow_null=True, + required=False, + help_text="Grace period duration. Pass null to clear the grace period.", + ) + minimum_grade_credit = serializers.FloatField( + required=False, + allow_null=True, + help_text="Minimum passing score for credit eligibility (0.0–1.0).", + ) class Meta: ref_name = "authoring_grading.CourseGrading.v0" diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_details.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_details.py index eec4dc11f38a..69dfd04c3c67 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_details.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_details.py @@ -29,7 +29,7 @@ class CourseDetailsSerializer(serializers.Serializer): about_sidebar_html = serializers.CharField(allow_null=True, allow_blank=True) banner_image_name = serializers.CharField(allow_blank=True) banner_image_asset_path = serializers.CharField() - certificate_available_date = serializers.DateTimeField() + certificate_available_date = serializers.DateTimeField(allow_null=True) certificates_display_behavior = serializers.CharField(allow_null=True) course_id = serializers.CharField() course_image_asset_path = serializers.CharField(allow_blank=True) diff --git a/cms/djangoapps/contentstore/rest_api/v4/views/home.py b/cms/djangoapps/contentstore/rest_api/v4/views/home.py index 1769edcd173c..6fd7fc2a01ef 100644 --- a/cms/djangoapps/contentstore/rest_api/v4/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v4/views/home.py @@ -1,11 +1,13 @@ """HomeCoursesViewSet for getting courses available to the logged-in user (v4).""" -from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema +from drf_spectacular.openapi import AutoSchema +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema, inline_serializer from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication from edx_rest_framework_extensions.auth.session.authentication import ( SessionAuthenticationAllowInactiveUser, ) from edx_rest_framework_extensions.paginators import DefaultPagination +from rest_framework import serializers as _serializers from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request @@ -18,6 +20,15 @@ from openedx.core.lib.api.mixins import StandardizedErrorMixin +class _HomeCoursesAutoSchema(AutoSchema): + """Custom AutoSchema that treats the 'list' action as a single-object response.""" + + def _is_list_view(self, serializer=None): + if self.view.action == 'list': + return False + return super()._is_list_view(serializer) + + class HomePageCoursesPaginator(DefaultPagination): """ ADR 0032 - standard pagination for the Studio home courses list (v4). @@ -136,6 +147,7 @@ class HomeCoursesViewSet(StandardizedErrorMixin, viewsets.ViewSet): is used instead of the default ``SessionAuthentication``. """ + schema = _HomeCoursesAutoSchema() authentication_classes = (JwtAuthentication, SessionAuthenticationAllowInactiveUser) permission_classes = (IsAuthenticated,) serializer_class = CourseHomeTabSerializerV4 @@ -154,7 +166,24 @@ def get_serializer(self, *args, **kwargs): parameters=_HOME_COURSES_QUERY_PARAMETERS, responses={ 200: OpenApiResponse( - response=CourseHomeTabSerializerV4, + response=inline_serializer( + name="PaginatedV4HomeCoursesResponse", + fields={ + "count": _serializers.IntegerField(help_text="Total number of courses."), + "num_pages": _serializers.IntegerField(help_text="Total number of pages."), + "current_page": _serializers.IntegerField(help_text="Current page number."), + "start": _serializers.IntegerField( + help_text="Zero-based index of the first item on this page." + ), + "next": _serializers.CharField( + allow_null=True, help_text="URL for the next page, or null." + ), + "previous": _serializers.CharField( + allow_null=True, help_text="URL for the previous page, or null." + ), + "results": CourseHomeTabSerializerV4(), + }, + ), description="Paginated course list retrieved successfully.", ), 401: _UNAUTHENTICATED_RESPONSE,