Skip to content

Commit

Permalink
Merge pull request #15 from nelc/and/backport_course_settings_api
Browse files Browse the repository at this point in the history
feat: Create DRF for course settings and course details views out of …
  • Loading branch information
andrey-canon authored Apr 26, 2024
2 parents c76ea2a + 2e6a5e3 commit 15fc4df
Show file tree
Hide file tree
Showing 16 changed files with 837 additions and 168 deletions.
42 changes: 42 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Common mixins for module.
"""
import json
from unittest.mock import patch

from rest_framework import status


class PermissionAccessMixin:
"""
Mixin for testing permission access for views.
"""

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)
self.assertEqual(error, "Authentication credentials were not provided.")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

@patch.dict('django.conf.settings.FEATURES', {'DISABLE_ADVANCED_SETTINGS': True})
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)
self.assertEqual(error, "You do not have permission to perform this action.")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
10 changes: 10 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Serializers for v1 contentstore API.
"""
from .settings import CourseSettingsSerializer
from .course_details import CourseDetailsSerializer
from .proctoring import (
LimitedProctoredExamSettingsSerializer,
ProctoredExamConfigurationSerializer,
ProctoredExamSettingsSerializer,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
API Serializers for course details
"""

from rest_framework import serializers

from openedx.core.lib.api.serializers import CourseKeyField


class InstructorInfoSerializer(serializers.Serializer):
""" Serializer for instructor info """
name = serializers.CharField(allow_blank=True)
title = serializers.CharField(allow_blank=True)
organization = serializers.CharField(allow_blank=True)
image = serializers.CharField(allow_blank=True)
bio = serializers.CharField(allow_blank=True)


class InstructorsSerializer(serializers.Serializer):
""" Serializer for instructors """
instructors = InstructorInfoSerializer(many=True, allow_empty=True)


class CourseDetailsSerializer(serializers.Serializer):
""" Serializer for course details """
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()
certificates_display_behavior = serializers.CharField(allow_null=True)
course_id = serializers.CharField()
course_image_asset_path = serializers.CharField(allow_blank=True)
course_image_name = serializers.CharField(allow_blank=True)
description = serializers.CharField(allow_blank=True)
duration = serializers.CharField(allow_blank=True)
effort = serializers.CharField(allow_null=True, allow_blank=True)
end_date = serializers.DateTimeField(allow_null=True)
enrollment_end = serializers.DateTimeField(allow_null=True)
enrollment_start = serializers.DateTimeField(allow_null=True)
entrance_exam_enabled = serializers.CharField(allow_blank=True)
entrance_exam_id = serializers.CharField(allow_blank=True)
entrance_exam_minimum_score_pct = serializers.CharField(allow_blank=True)
instructor_info = InstructorsSerializer()
intro_video = serializers.CharField(allow_null=True)
language = serializers.CharField(allow_null=True)
learning_info = serializers.ListField(child=serializers.CharField(allow_blank=True))
license = serializers.CharField(allow_null=True)
org = serializers.CharField()
overview = serializers.CharField(allow_blank=True)
pre_requisite_courses = serializers.ListField(child=CourseKeyField())
run = serializers.CharField()
self_paced = serializers.BooleanField()
short_description = serializers.CharField(allow_blank=True)
start_date = serializers.DateTimeField()
subtitle = serializers.CharField(allow_blank=True)
syllabus = serializers.CharField(allow_null=True)
title = serializers.CharField(allow_blank=True)
video_thumbnail_image_asset_path = serializers.CharField()
video_thumbnail_image_name = serializers.CharField(allow_blank=True)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
API Serializers for Contentstore
API Serializers for proctoring
"""

from rest_framework import serializers
Expand Down
44 changes: 44 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/serializers/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
API Serializers for course settings
"""

from rest_framework import serializers

from openedx.core.lib.api.serializers import CourseKeyField


class PossiblePreRequisiteCourseSerializer(serializers.Serializer):
""" Serializer for possible pre requisite course """
course_key = CourseKeyField()
display_name = serializers.CharField()
lms_link = serializers.CharField()
number = serializers.CharField()
org = serializers.CharField()
rerun_link = serializers.CharField()
run = serializers.CharField()
url = serializers.CharField()


class CourseSettingsSerializer(serializers.Serializer):
""" Serializer for course settings """
about_page_editable = serializers.BooleanField()
can_show_certificate_available_date_field = serializers.BooleanField()
course_display_name = serializers.CharField()
course_display_name_with_default = serializers.CharField()
credit_eligibility_enabled = serializers.BooleanField()
credit_requirements = serializers.DictField(required=False)
enable_extended_course_details = serializers.BooleanField()
enrollment_end_editable = serializers.BooleanField()
is_credit_course = serializers.BooleanField()
is_entrance_exams_enabled = serializers.BooleanField()
is_prerequisite_courses_enabled = serializers.BooleanField()
language_options = serializers.ListField(child=serializers.ListField(child=serializers.CharField()))
lms_link_for_about_page = serializers.URLField()
marketing_enabled = serializers.BooleanField()
mfe_proctored_exam_settings_url = serializers.CharField(required=False, allow_null=True, allow_blank=True)
possible_pre_requisite_courses = PossiblePreRequisiteCourseSerializer(required=False, many=True)
short_description_editable = serializers.BooleanField()
show_min_grade_warning = serializers.BooleanField()
sidebar_html_enabled = serializers.BooleanField()
upgrade_deadline = serializers.DateTimeField(allow_null=True)
use_v2_cert_display_settings = serializers.BooleanField()
108 changes: 108 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/tests/test_course_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
Unit tests for course details views.
"""
import json
from unittest.mock import patch

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

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

from ..mixins import PermissionAccessMixin


@ddt.ddt
class CourseDetailsViewTest(CourseTestCase, PermissionAccessMixin):
"""
Tests for CourseDetailsView.
"""

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

def test_put_permissions_unauthenticated(self):
"""
Test that an error is returned in the absence of auth credentials.
"""
self.client.logout()
response = self.client.put(self.url)
error = self.get_and_check_developer_response(response)
self.assertEqual(error, "Authentication credentials were not provided.")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_put_permissions_unauthorized(self):
"""
Test that an error is returned if the user is unauthorised.
"""
client, _ = self.create_non_staff_authed_user_client()
response = client.put(self.url)
error = self.get_and_check_developer_response(response)
self.assertEqual(error, "You do not have permission to perform this action.")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

@patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True})
def test_put_invalid_pre_requisite_course(self):
pre_requisite_course_keys = [str(self.course.id), 'invalid_key']
request_data = {"pre_requisite_courses": pre_requisite_course_keys}
response = self.client.put(path=self.url, data=json.dumps(request_data), content_type="application/json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json()['error'], 'Invalid prerequisite course key')

def test_put_course_details(self):
request_data = {
"about_sidebar_html": "",
"banner_image_name": "images_course_image.jpg",
"banner_image_asset_path": "/asset-v1:edX+E2E-101+course+type@asset+block@images_course_image.jpg",
"certificate_available_date": "2029-01-02T00:00:00Z",
"certificates_display_behavior": "end",
"course_id": "E2E-101",
"course_image_asset_path": "/static/studio/images/pencils.jpg",
"course_image_name": "bar_course_image_name",
"description": "foo_description",
"duration": "",
"effort": None,
"end_date": "2023-08-01T01:30:00Z",
"enrollment_end": "2023-05-30T01:00:00Z",
"enrollment_start": "2023-05-29T01:00:00Z",
"entrance_exam_enabled": "",
"entrance_exam_id": "",
"entrance_exam_minimum_score_pct": "50",
"intro_video": None,
"language": "creative-commons: ver=4.0 BY NC ND",
"learning_info": [
"foo",
"bar"
],
"license": "creative-commons: ver=4.0 BY NC ND",
"org": "edX",
"overview": "<section class=\"about\"></section>",
"pre_requisite_courses": [],
"run": "course",
"self_paced": None,
"short_description": "",
"start_date": "2023-06-01T01:30:00Z",
"subtitle": "",
"syllabus": None,
"title": "",
"video_thumbnail_image_asset_path": "/asset-v1:edX+E2E-101+course+type@asset+block@images_course_image.jpg",
"video_thumbnail_image_name": "images_course_image.jpg",
"instructor_info": {
"instructors": [
{
"name": "foo bar",
"title": "title",
"organization": "org",
"image": "image",
"bio": ""
}
]
},
}
response = self.client.put(path=self.url, data=json.dumps(request_data), content_type="application/json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
80 changes: 80 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
Unit tests for course settings views.
"""
import ddt
from django.conf import settings
from django.urls import reverse
from mock import patch
from rest_framework import status

from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.utils import get_proctored_exam_settings_url
from common.djangoapps.util.course import get_link_for_about_page
from openedx.core.djangoapps.credit.tests.factories import CreditCourseFactory

from ..mixins import PermissionAccessMixin


@ddt.ddt
class CourseSettingsViewTest(CourseTestCase, PermissionAccessMixin):
"""
Tests for CourseSettingsView.
"""

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

def test_course_settings_response(self):
""" Check successful response content """
response = self.client.get(self.url)
expected_response = {
'about_page_editable': True,
'can_show_certificate_available_date_field': False,
'course_display_name': self.course.display_name,
'course_display_name_with_default': self.course.display_name_with_default,
'credit_eligibility_enabled': True,
'enrollment_end_editable': True,
'enable_extended_course_details': False,
'is_credit_course': False,
'is_entrance_exams_enabled': True,
'is_prerequisite_courses_enabled': False,
'language_options': settings.ALL_LANGUAGES,
'lms_link_for_about_page': get_link_for_about_page(self.course),
'marketing_enabled': False,
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(self.course.id),
'short_description_editable': True,
'sidebar_html_enabled': False,
'show_min_grade_warning': False,
'upgrade_deadline': None,
'use_v2_cert_display_settings': False,
}

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertDictEqual(expected_response, response.data)

@patch.dict('django.conf.settings.FEATURES', {'ENABLE_CREDIT_ELIGIBILITY': True})
def test_credit_eligibility_setting(self):
"""
Make sure if the feature flag is enabled we have updated the dict keys in response.
"""
_ = CreditCourseFactory(course_key=self.course.id, enabled=True)
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('credit_requirements', response.data)
self.assertTrue(response.data['is_credit_course'])

@patch.dict('django.conf.settings.FEATURES', {
'ENABLE_PREREQUISITE_COURSES': True,
'MILESTONES_APP': True,
})
def test_prerequisite_courses_enabled_setting(self):
"""
Make sure if the feature flags are enabled we have updated the dict keys in response.
"""
response = self.client.get(self.url)
self.assertIn('possible_pre_requisite_courses', response.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
18 changes: 16 additions & 2 deletions cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,28 @@

from openedx.core.constants import COURSE_ID_PATTERN

from . import views
from .views import (
CourseDetailsView,
CourseSettingsView,
ProctoredExamSettingsView,
)

app_name = 'v1'

urlpatterns = [
re_path(
fr'^proctored_exam_settings/{COURSE_ID_PATTERN}$',
views.ProctoredExamSettingsView.as_view(),
ProctoredExamSettingsView.as_view(),
name="proctored_exam_settings"
),
re_path(
fr'^course_settings/{COURSE_ID_PATTERN}$',
CourseSettingsView.as_view(),
name="course_settings"
),
re_path(
fr'^course_details/{COURSE_ID_PATTERN}$',
CourseDetailsView.as_view(),
name="course_details"
),
]
6 changes: 6 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Views for v1 contentstore API.
"""
from .course_details import CourseDetailsView
from .settings import CourseSettingsView
from .proctoring import ProctoredExamSettingsView
Loading

0 comments on commit 15fc4df

Please sign in to comment.