Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
class AuthoringGradingView(DeveloperErrorViewMixin, APIView):
"""
View for getting and setting the advanced settings for a course.

Deprecated: use ``PATCH /api/authoring/v3/courses/{course_key}/grading/``
instead. See https://github.com/openedx/openedx-platform/issues/39128.
"""
@extend_schema(
request=CourseGradingModelSerializer,
Expand All @@ -42,6 +45,9 @@ def post(self, request: Request, course_key: CourseKey):
"""
Update a course's grading.

Deprecated: use ``PATCH /api/authoring/v3/courses/{course_key}/grading/``,
which accepts this body unchanged. See https://github.com/openedx/openedx-platform/issues/39128.

**Example Request**

POST /api/contentstore/v0/course_grading/{course_id}
Expand Down
10 changes: 10 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/grading.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
class CourseGradingView(DeveloperErrorViewMixin, APIView):
"""
View for Course Grading policy configuration.

Deprecated: use ``GET`` and ``PATCH /api/authoring/v3/courses/{course_key}/grading/``
instead. See https://github.com/openedx/openedx-platform/issues/39128.
"""

@extend_schema(
Expand All @@ -44,6 +47,10 @@ def get(self, request: Request, course_key: CourseKey):
"""
Get an object containing course grading settings with model.

Deprecated: use ``GET /api/authoring/v3/courses/{course_key}/grading/?view=full``,
which returns the same fields with the grading policy at the top level.
See https://github.com/openedx/openedx-platform/issues/39128.

**Example Request**

GET /api/contentstore/v1/course_grading/{course_id}
Expand Down Expand Up @@ -124,6 +131,9 @@ def post(self, request: Request, course_key: CourseKey):
"""
Update a course's grading.

Deprecated: use ``PATCH /api/authoring/v3/courses/{course_key}/grading/``,
which accepts this body unchanged. See https://github.com/openedx/openedx-platform/issues/39128.

**Example Request**

POST /api/contentstore/v1/course_grading/{course_id}
Expand Down
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/rest_api/v3/authoring_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
),
path(
"courses/<course_key:course_key>/grading/",
AuthoringGradingViewSet.as_view({"patch": "partial_update"}),
AuthoringGradingViewSet.as_view({"get": "retrieve", "patch": "partial_update"}),
name="course_grading",
),
]
83 changes: 83 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v3/grading_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Service layer for the v3 course grading endpoint."""

from django.conf import settings

from cms.djangoapps.contentstore.rest_api.v3.serializers.grading import GRADING_POLICY_FIELDS
from cms.djangoapps.contentstore.utils import get_course_grading
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from openedx.core.djangoapps.credit.api import is_credit_course
from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements
from xmodule.modulestore.django import modulestore


def get_grading_policy(course_key):
"""Return the stored grading policy of the course ``course_key``."""
return CourseGradingModel.fetch(course_key)


def get_full_grading_settings(course_key):
"""
Return the grading policy of ``course_key`` with the context the Studio grading page shows beside it.

The returned mapping holds the four policy fields plus the graded subsections per
assignment type, the default letter grade names, and the credit and proctoring context.
"""
with modulestore().bulk_operations(course_key):
show_credit_eligibility = is_credit_course(course_key) and settings.ENABLE_CREDIT_ELIGIBILITY
context = get_course_grading(course_key)
policy = context["course_details"]
return {
"graders": policy.graders,
"grade_cutoffs": policy.grade_cutoffs,
"grace_period": policy.grace_period,
"minimum_grade_credit": policy.minimum_grade_credit,
"course_assignment_lists": context["course_assignment_lists"],
"default_grade_designations": context["default_grade_designations"],
"is_credit_course": context["is_credit_course"],
"show_credit_eligibility": show_credit_eligibility,
"mfe_proctored_exam_settings_url": context["mfe_proctored_exam_settings_url"],
}


def update_grading_policy(course_key, changes, user):
"""
Apply ``changes`` to the grading policy of ``course_key`` and return the stored policy.

Only the fields present in ``changes`` are written. A body holding all four fields is
applied exactly as a whole-policy update. When ``graders`` is absent the stored graders
are never written back: their weights are fractions that do not survive a round trip
through the percentages the API exchanges, so rewriting them would alter weights
nobody changed. Grade cutoffs are written, and a grading policy change announced,
only when they differ from the stored ones.

The credit requirements of the course are refreshed whenever ``minimum_grade_credit``
is present, whether or not its value changes.
"""
if "minimum_grade_credit" in changes:
update_credit_course_requirements.delay(str(course_key))

if all(field in changes for field in GRADING_POLICY_FIELDS):
return CourseGradingModel.update_from_json(course_key, changes, user)

if "graders" in changes:
# The whole-policy update needs every field. The stored cutoffs leave them
# unchanged, and None leaves the grace period and credit minimum unchanged.
stored_cutoffs = modulestore().get_course(course_key).grade_cutoffs
policy = {
"grade_cutoffs": stored_cutoffs,
"grace_period": None,
"minimum_grade_credit": None,
**changes,
}
return CourseGradingModel.update_from_json(course_key, policy, user)

if "grace_period" in changes:
CourseGradingModel.update_grace_period_from_json(course_key, changes["grace_period"], user)
if "minimum_grade_credit" in changes:
CourseGradingModel.update_minimum_grade_credit_from_json(course_key, changes["minimum_grade_credit"], user)
# Last, so the grading policy change is announced after every other field is stored,
# as the whole-policy update does.
if "grade_cutoffs" in changes:
if modulestore().get_course(course_key).grade_cutoffs != changes["grade_cutoffs"]:
CourseGradingModel.update_cutoffs_from_json(course_key, changes["grade_cutoffs"], user)
return CourseGradingModel.fetch(course_key)
45 changes: 45 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v3/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Permissions for the v3 contentstore API."""

from openedx_authz.constants.permissions import COURSES_EDIT_GRADING_SETTINGS, COURSES_VIEW_GRADING_SETTINGS
from rest_framework.permissions import BasePermission

from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission
from openedx.core.djangoapps.authz.decorators import user_has_course_permission


class _GradingSettingsPermission(BasePermission):
"""
Allow callers holding ``authz_permission`` on the course in the path.

The course comes from the view's ``get_course_key()``, which refuses an
unparsable or unknown course with a 404 before permission is checked. On
courses using policy-based authoring permissions that policy decides;
elsewhere Studio read access to the course is required.
"""

authz_permission = None

def has_permission(self, request, view):
return user_has_course_permission(
request.user,
self.authz_permission,
view.get_course_key(),
LegacyAuthoringPermission.READ,
)


class CanViewGradingSettings(_GradingSettingsPermission):
"""Allow callers who may view the grading settings of the course in the path."""

authz_permission = COURSES_VIEW_GRADING_SETTINGS.identifier


class CanEditGradingSettings(_GradingSettingsPermission):
"""
Allow callers who may edit the grading settings of the course in the path.

Where policy-based permissions are off, Studio read access is enough to
edit, as for every other grading endpoint in Studio.
"""

authz_permission = COURSES_EDIT_GRADING_SETTINGS.identifier
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Serializers for the v3 contentstore API."""
Loading
Loading