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
92 changes: 89 additions & 3 deletions cms/djangoapps/contentstore/rest_api/v1/views/course_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
import edx_api_doc_tools as apidocs
from django.core.exceptions import ValidationError
from opaque_keys.edx.keys import CourseKey
from openedx_authz.constants.permissions import (
COURSES_EDIT_DETAILS,
COURSES_EDIT_SCHEDULE,
COURSES_VIEW_SCHEDULE_AND_DETAILS,
)
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from common.djangoapps.student.auth import has_studio_read_access
from common.djangoapps.util.json_request import JsonResponseBadRequest
from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission
from openedx.core.djangoapps.authz.decorators import user_has_course_permission
from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
from xmodule.modulestore.django import modulestore
Expand All @@ -17,6 +23,62 @@
from ..serializers import CourseDetailsSerializer


def _classify_update(payload: dict, course_key: CourseKey) -> tuple[bool, bool]:
"""
Determine whether the payload is updating schedule fields, detail fields, or both
for the course identified by course_key.

Returns:
(is_schedule_update, is_details_update)
"""
schedule_fields = frozenset({"start_date", "end_date", "enrollment_start", "enrollment_end"})

course_details = CourseDetails.fetch(course_key)

is_schedule_update = False
is_details_update = False

serializer = CourseDetailsSerializer()

for field, payload_value in payload.items():
# Early exit for efficiency
if is_schedule_update and is_details_update:
break

# Ignore unknown fields if needed
if field not in serializer.fields:
continue

current_value = getattr(course_details, field, None)

# Check schedule fields
if field in schedule_fields:
if is_schedule_update:
# Already classified as schedule update, no need to check again
continue
try:
# Convert payload value to internal value for accurate comparison
# on date fields
if payload_value is not None:
payload_value = serializer.fields[field].to_internal_value(payload_value)
except ValidationError as exc:
raise ValidationError(
f"Invalid date format for field {field}: {payload_value}"
) from exc

if payload_value != current_value:
is_schedule_update = True
else:
# Any non-schedule field counts as details update
if is_details_update:
# Already classified as details update, no need to check again
continue
if payload_value != current_value:
is_details_update = True

return is_schedule_update, is_details_update


@view_auth_classes(is_authenticated=True)
class CourseDetailsView(DeveloperErrorViewMixin, APIView):
"""
Expand Down Expand Up @@ -99,7 +161,12 @@ def get(self, request: Request, course_id: str):
```
"""
course_key = CourseKey.from_string(course_id)
if not has_studio_read_access(request.user, course_key):
if not user_has_course_permission(
request.user,
COURSES_VIEW_SCHEDULE_AND_DETAILS.identifier,
course_key,
LegacyAuthoringPermission.READ
):
self.permission_denied(request)

course_details = CourseDetails.fetch(course_key)
Expand Down Expand Up @@ -142,7 +209,26 @@ def put(self, request: Request, course_id: str):
along with all the course's details similar to a ``GET`` request.
"""
course_key = CourseKey.from_string(course_id)
if not has_studio_read_access(request.user, course_key):
is_schedule_update, is_details_update = _classify_update(request.data, course_key)

if not is_schedule_update and not is_details_update:
# No updatable fields provided in the request
is_details_update = True # To trigger permission check and return 403 if user cannot edit details

if is_schedule_update and not user_has_course_permission(
request.user,
COURSES_EDIT_SCHEDULE.identifier,
course_key,
LegacyAuthoringPermission.READ
):
self.permission_denied(request)

if is_details_update and not user_has_course_permission(
request.user,
COURSES_EDIT_DETAILS.identifier,
course_key,
LegacyAuthoringPermission.READ
):
self.permission_denied(request)

course_block = modulestore().get_course(course_key)
Expand Down
11 changes: 9 additions & 2 deletions cms/djangoapps/contentstore/rest_api/v1/views/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import edx_api_doc_tools as apidocs
from django.conf import settings
from opaque_keys.edx.keys import CourseKey
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from common.djangoapps.student.auth import has_studio_read_access
from lms.djangoapps.certificates.api import can_show_certificate_available_date_field
from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission
from openedx.core.djangoapps.authz.decorators import user_has_course_permission
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
from xmodule.modulestore.django import modulestore

Expand Down Expand Up @@ -99,7 +101,12 @@ def get(self, request: Request, course_id: str):
```
"""
course_key = CourseKey.from_string(course_id)
if not has_studio_read_access(request.user, course_key):
if not user_has_course_permission(
request.user,
COURSES_VIEW_COURSE.identifier,
course_key,
LegacyAuthoringPermission.READ
):
self.permission_denied(request)

with modulestore().bulk_operations(course_key):
Expand Down
Loading
Loading