From 38320f8433427111c6cfb2bfaaa70aec5b989053 Mon Sep 17 00:00:00 2001 From: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com> Date: Wed, 29 May 2024 14:54:00 -0400 Subject: [PATCH] feat: grading rest api for authoring api (#34854) * feat: grading rest api for authoring api * fix: doc string api path --- .../rest_api/v0/serializers/__init__.py | 1 + .../v0/serializers/authoring_grading.py | 20 +++++ .../contentstore/rest_api/v0/urls.py | 9 +- .../rest_api/v0/views/__init__.py | 1 + .../rest_api/v0/views/authoring_grading.py | 88 +++++++++++++++++++ 5 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py create mode 100644 cms/djangoapps/contentstore/rest_api/v0/views/authoring_grading.py diff --git a/cms/djangoapps/contentstore/rest_api/v0/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v0/serializers/__init__.py index e90409ca864a..33931a4a199a 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v0/serializers/__init__.py @@ -3,6 +3,7 @@ """ from .advanced_settings import AdvancedSettingsFieldSerializer, CourseAdvancedSettingsSerializer from .assets import AssetSerializer +from .authoring_grading import CourseGradingModelSerializer from .tabs import CourseTabSerializer, CourseTabUpdateSerializer, TabIDLocatorSerializer from .transcripts import TranscriptSerializer, YoutubeTranscriptCheckSerializer, YoutubeTranscriptUploadSerializer from .xblock import XblockSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py b/cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py new file mode 100644 index 000000000000..e3dd070573aa --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v0/serializers/authoring_grading.py @@ -0,0 +1,20 @@ +""" +API Serializers for course grading +""" + +from rest_framework import serializers + + +class GradersSerializer(serializers.Serializer): + """ Serializer for graders """ + type = serializers.CharField() + min_count = serializers.IntegerField() + drop_count = serializers.IntegerField() + short_label = serializers.CharField(required=False, allow_null=True, allow_blank=True) + weight = serializers.IntegerField() + id = serializers.IntegerField() + + +class CourseGradingModelSerializer(serializers.Serializer): + """ Serializer for course grading model data """ + graders = GradersSerializer(many=True, allow_null=True, allow_empty=True) diff --git a/cms/djangoapps/contentstore/rest_api/v0/urls.py b/cms/djangoapps/contentstore/rest_api/v0/urls.py index 023285471036..cc1e13b0929c 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v0/urls.py @@ -7,6 +7,7 @@ from .views import ( AdvancedCourseSettingsView, + AuthoringGradingView, CourseTabSettingsView, CourseTabListView, CourseTabReorderView, @@ -46,8 +47,8 @@ ), # Authoring API - re_path( - r'^heartbeat$', APIHeartBeatView.as_view(), name='heartbeat' + path( + 'heartbeat', APIHeartBeatView.as_view(), name='heartbeat' ), re_path( fr'^file_assets/{settings.COURSE_ID_PATTERN}$', @@ -61,6 +62,10 @@ fr'^videos/encodings/{settings.COURSE_ID_PATTERN}$', authoring_videos.VideoEncodingsDownloadView.as_view(), name='cms_api_videos_encodings' ), + re_path( + fr'grading/{settings.COURSE_ID_PATTERN}', + AuthoringGradingView.as_view(), name='cms_api_update_grading' + ), path( 'videos/features', authoring_videos.VideoFeaturesView.as_view(), name='cms_api_videos_features' diff --git a/cms/djangoapps/contentstore/rest_api/v0/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v0/views/__init__.py index 85b7b7d93ba7..00d22a1ea715 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v0/views/__init__.py @@ -2,6 +2,7 @@ Views for v0 contentstore API. """ from .advanced_settings import AdvancedCourseSettingsView +from .authoring_grading import AuthoringGradingView from .tabs import CourseTabSettingsView, CourseTabListView, CourseTabReorderView from .transcripts import TranscriptView, YoutubeTranscriptCheckView, YoutubeTranscriptUploadView from .api_heartbeat import APIHeartBeatView diff --git a/cms/djangoapps/contentstore/rest_api/v0/views/authoring_grading.py b/cms/djangoapps/contentstore/rest_api/v0/views/authoring_grading.py new file mode 100644 index 000000000000..ef965bc3d4c8 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v0/views/authoring_grading.py @@ -0,0 +1,88 @@ +""" API Views for course advanced settings """ + +import edx_api_doc_tools as apidocs +from opaque_keys.edx.keys import CourseKey +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from cms.djangoapps.models.settings.course_grading import CourseGradingModel +from common.djangoapps.student.auth import has_studio_read_access +from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements +from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes +from ..serializers import CourseGradingModelSerializer + + +@view_auth_classes(is_authenticated=True) +class AuthoringGradingView(DeveloperErrorViewMixin, APIView): + """ + View for getting and setting the advanced settings for a course. + """ + @apidocs.schema( + body=CourseGradingModelSerializer, + parameters=[ + apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"), + ], + responses={ + 200: CourseGradingModelSerializer, + 401: "The requester is not authenticated.", + 403: "The requester cannot access the specified course.", + 404: "The requested course does not exist.", + }, + ) + @verify_course_exists() + def post(self, request: Request, course_id: str): + """ + Update a course's grading. + + **Example Request** + + POST /api/contentstore/v0/course_grading/{course_id} + + **POST Parameters** + + The data sent for a post request should follow next object. + Here is an example request data that updates the ``course_grading`` + + ```json + { + "graders": [ + { + "type": "Homework", + "min_count": 1, + "drop_count": 0, + "short_label": "", + "weight": 100, + "id": 0 + } + ], + "grade_cutoffs": { + "A": 0.75, + "B": 0.63, + "C": 0.57, + "D": 0.5 + }, + "grace_period": { + "hours": 12, + "minutes": 0 + }, + "minimum_grade_credit": 0.7, + "is_credit_course": true + } + ``` + + **Response Values** + + If the request is successful, an HTTP 200 "OK" response is returned, + """ + course_key = CourseKey.from_string(course_id) + + if not has_studio_read_access(request.user, course_key): + self.permission_denied(request) + + if 'minimum_grade_credit' in request.data: + update_credit_course_requirements.delay(str(course_key)) + + updated_data = CourseGradingModel.update_from_json(course_key, request.data, request.user) + serializer = CourseGradingModelSerializer(updated_data) + return Response(serializer.data)