Skip to content
Closed
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
@@ -0,0 +1,74 @@
"""
Tests for the course advanced settings API.
"""
import json

import ddt
from django.urls import reverse
from rest_framework import status

from cms.djangoapps.contentstore.tests.utils import CourseTestCase


@ddt.ddt
class CourseDetailsSettingViewTest(CourseTestCase):
"""
Tests for DetailsSettings API View.
"""

def setUp(self):
super().setUp()
self.url = reverse(
"cms.djangoapps.contentstore:v0:course_details_settings",
kwargs={"course_id": self.course.id},
)

def get_and_check_developer_response(self, response):
"""
Make basic asserting about the presence of an error response, and return the developer response.
"""
content = json.loads(response.content.decode("utf-8"))
assert "developer_message" in content
return content["developer_message"]

def test_permissions_unauthenticated(self):
"""
Test that an error is returned in the absence of auth credentials.
"""
self.client.logout()
response = self.client.get(self.url)
error = self.get_and_check_developer_response(response)
assert error == "Authentication credentials were not provided."

def test_permissions_unauthorized(self):
"""
Test that an error is returned if the user is unauthorised.
"""
client, _ = self.create_non_staff_authed_user_client()
response = client.get(self.url)
error = self.get_and_check_developer_response(response)
assert error == "You do not have permission to perform this action."

def test_get_course_details(self):
"""
Test for get response
"""
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_patch_course_details(self):
"""
Test for patch response
"""
data = {
"start_date": "2030-01-01T00:00:00Z",
"end_date": "2030-01-31T00:00:00Z",
"enrollment_start": "2029-12-01T00:00:00Z",
"enrollment_end": "2030-01-01T00:00:00Z",
"course_title": "Test Course",
"short_description": "This is a test course",
"overview": "This course is for testing purposes",
"intro_video": None
}
response = self.client.patch(self.url, data, content_type='application/json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
13 changes: 12 additions & 1 deletion cms/djangoapps/contentstore/rest_api/v0/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
from django.urls import re_path

from openedx.core.constants import COURSE_ID_PATTERN
from .views import AdvancedCourseSettingsView, CourseTabSettingsView, CourseTabListView, CourseTabReorderView
from .views import (
AdvancedCourseSettingsView,
CourseDetailsSettingsView,
CourseTabSettingsView,
CourseTabListView,
CourseTabReorderView
)

app_name = "v0"

Expand All @@ -28,4 +34,9 @@
CourseTabReorderView.as_view(),
name="course_tab_reorder",
),
re_path(
fr"^details_settings/{COURSE_ID_PATTERN}$",
CourseDetailsSettingsView.as_view(),
name="course_details_settings",
),
]
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/rest_api/v0/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
Views for v0 contentstore API.
"""
from .advanced_settings import AdvancedCourseSettingsView
from .details_settings import CourseDetailsSettingsView
from .tabs import CourseTabSettingsView, CourseTabListView, CourseTabReorderView
76 changes: 76 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v0/views/details_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
""" API Views for course details settings """

import edx_api_doc_tools as apidocs
from django.core.exceptions import ValidationError as DjangoValidationError
from opaque_keys.edx.keys import CourseKey
from rest_framework.request import Request
from rest_framework.views import APIView
from xmodule.modulestore.django import modulestore

from cms.djangoapps.models.settings.encoder import CourseSettingsEncoder
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
from common.djangoapps.util.json_request import JsonResponse, JsonResponseBadRequest, expect_json_in_class_view
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
from openedx.core.djangoapps.models.course_details import CourseDetails

from ....utils import update_course_details


@view_auth_classes(is_authenticated=True)
class CourseDetailsSettingsView(DeveloperErrorViewMixin, APIView):
"""
View for getting and setting the details settings for a course.
"""

@apidocs.schema(
parameters=[
apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"),
],
responses={
401: "The requester is not authenticated.",
403: "The requester cannot access the specified course.",
404: "The requested course does not exist.",
},
)
@expect_json_in_class_view
@verify_course_exists()
def get(self, request: Request, course_id: str):
"""
Get an object containing all the details settings in a course.
"""
course_key = CourseKey.from_string(course_id)
if not has_studio_read_access(request.user, course_key):
self.permission_denied(request)
course_details = CourseDetails.fetch(course_key)
return JsonResponse(
course_details,
# encoder serializes dates, old locations, and instances
encoder=CourseSettingsEncoder
)

@apidocs.schema(
parameters=[
apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"),
],
responses={
401: "The requester is not authenticated.",
403: "The requester cannot access the specified course.",
404: "The requested course does not exist.",
},
)
@expect_json_in_class_view
@verify_course_exists()
def patch(self, request: Request, course_id: str):
"""
Update a course's details settings.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't tell me anything about what settings can be in the patch. What settings can actually be updated by this API endpoint? Is that already documented somewhere and you can just point to that from here? If not, can you provide a list of the values that can be in the patch body? Also, is it possible to write a serializer for the data being output and input for these views? That would solve both the documentation issue and could be used to perform some basic validation on the input.

Copy link
Contributor Author

@pkulkark pkulkark Jul 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@feanil This is meant to allow API access to the existing settings_handler which currently only allow session-based requests. It doesn't change any of its functionality. We added serializers for it in a separate django app (c.f. ref) which consumes this api along with the existing advanced_settings api. But you're right, I think we could still add one here like the CourseAdvancedSettingsSerializer. I'll add it and update here.

"""
course_key = CourseKey.from_string(course_id)
if not has_studio_write_access(request.user, course_key):
self.permission_denied(request)
course_block = modulestore().get_course(course_key)
try:
update_data = update_course_details(request, course_key, request.json, course_block)
except DjangoValidationError as err:
return JsonResponseBadRequest({"error": err.message})

return JsonResponse(update_data, encoder=CourseSettingsEncoder)