Skip to content

Commit

Permalink
feat: create course home api DRF (openedx#33173) (openedx#33204)
Browse files Browse the repository at this point in the history
  • Loading branch information
ruzniaievdm authored and andrey-canon committed May 9, 2024
1 parent ffa25dc commit e094571
Show file tree
Hide file tree
Showing 13 changed files with 552 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""
Serializers for v1 contentstore API.
"""
from .settings import CourseSettingsSerializer
from .home import CourseHomeSerializer
from .course_details import CourseDetailsSerializer
from .course_rerun import CourseRerunSerializer
from .proctoring import (
LimitedProctoredExamSettingsSerializer,
ProctoredExamConfigurationSerializer,
ProctoredExamSettingsSerializer,
)
from .settings import CourseSettingsSerializer
19 changes: 19 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/serializers/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
Common API Serializers
"""

from rest_framework import serializers

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


class CourseCommonSerializer(serializers.Serializer):
"""Serializer for course renders"""
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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
API Serializers for course rerun
"""

from rest_framework import serializers


class CourseRerunSerializer(serializers.Serializer):
""" Serializer for course rerun """
allow_unicode_course_id = serializers.BooleanField()
course_creator_status = serializers.CharField()
display_name = serializers.CharField()
number = serializers.CharField()
org = serializers.CharField()
run = serializers.CharField()
62 changes: 62 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/serializers/home.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
API Serializers for course home
"""

from rest_framework import serializers

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

from .common import CourseCommonSerializer


class UnsucceededCourseSerializer(serializers.Serializer):
"""Serializer for unsucceeded course"""
display_name = serializers.CharField()
course_key = CourseKeyField()
org = serializers.CharField()
number = serializers.CharField()
run = serializers.CharField()
is_failed = serializers.BooleanField()
is_in_progress = serializers.BooleanField()
dismiss_link = serializers.CharField()


class LibraryViewSerializer(serializers.Serializer):
"""Serializer for library view"""
display_name = serializers.CharField()
library_key = serializers.CharField()
url = serializers.CharField()
org = serializers.CharField()
number = serializers.CharField()
can_edit = serializers.BooleanField()


class CourseHomeSerializer(serializers.Serializer):
"""Serializer for course home"""
allow_course_reruns = serializers.BooleanField()
allow_to_create_new_org = serializers.BooleanField()
allow_unicode_course_id = serializers.BooleanField()
allowed_organizations = serializers.ListSerializer(
child=serializers.CharField(),
allow_empty=True
)
archived_courses = CourseCommonSerializer(required=False, many=True)
can_create_organizations = serializers.BooleanField()
course_creator_status = serializers.CharField()
courses = CourseCommonSerializer(required=False, many=True)
in_process_course_actions = UnsucceededCourseSerializer(many=True, required=False, allow_null=True)
libraries = LibraryViewSerializer(many=True, required=False, allow_null=True)
libraries_enabled = serializers.BooleanField()
library_authoring_mfe_url = serializers.CharField()
optimization_enabled = serializers.BooleanField()
redirect_to_library_authoring_mfe = serializers.BooleanField()
request_course_creator_url = serializers.CharField()
rerun_creator_status = serializers.BooleanField()
show_new_library_button = serializers.BooleanField()
split_studio_home = serializers.BooleanField()
studio_name = serializers.CharField()
studio_short_name = serializers.CharField()
studio_request_email = serializers.CharField()
tech_support_email = serializers.CharField()
platform_name = serializers.CharField()
user_is_active = serializers.BooleanField()
18 changes: 3 additions & 15 deletions cms/djangoapps/contentstore/rest_api/v1/serializers/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,11 @@

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()
from .common import CourseCommonSerializer


class CourseSettingsSerializer(serializers.Serializer):
""" Serializer for course settings """
"""Serializer for course settings"""
about_page_editable = serializers.BooleanField()
can_show_certificate_available_date_field = serializers.BooleanField()
course_display_name = serializers.CharField()
Expand All @@ -36,7 +24,7 @@ class CourseSettingsSerializer(serializers.Serializer):
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)
possible_pre_requisite_courses = CourseCommonSerializer(required=False, many=True)
short_description_editable = serializers.BooleanField()
show_min_grade_warning = serializers.BooleanField()
sidebar_html_enabled = serializers.BooleanField()
Expand Down
36 changes: 36 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/tests/test_course_rerun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Unit tests for course rerun.
"""
from django.urls import reverse
from rest_framework import status

from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.rest_api.v1.mixins import PermissionAccessMixin


class CourseRerunViewTest(CourseTestCase, PermissionAccessMixin):
"""
Tests for CourseRerunView.
"""

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

def test_course_rerun_response(self):
"""Check successful response content"""
response = self.client.get(self.url)
expected_response = {
"allow_unicode_course_id": False,
"course_creator_status": "granted",
"display_name": self.course.display_name,
"number": self.course.id.course,
"org": self.course.id.org,
"run": self.course.id.run,
}

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertDictEqual(expected_response, response.data)
89 changes: 89 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/tests/test_home.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""
Unit tests for home page view.
"""
import ddt
from django.conf import settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_switch
from rest_framework import status

from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.views.course import ENABLE_GLOBAL_STAFF_OPTIMIZATION
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from xmodule.modulestore.tests.factories import CourseFactory


@ddt.ddt
class HomePageViewTest(CourseTestCase):
"""
Tests for HomePageView.
"""

def setUp(self):
super().setUp()
self.url = reverse("cms.djangoapps.contentstore:v1:home")

def test_home_page_response(self):
"""Check successful response content"""
response = self.client.get(self.url)
course_id = str(self.course.id)

expected_response = {
"allow_course_reruns": True,
"allow_to_create_new_org": False,
"allow_unicode_course_id": False,
"allowed_organizations": [],
"archived_courses": [],
"can_create_organizations": True,
"course_creator_status": "granted",
"courses": [{
"course_key": course_id,
"display_name": self.course.display_name,
"lms_link": f'//{settings.LMS_BASE}/courses/{course_id}/jump_to/{self.course.location}',
"number": self.course.number,
"org": self.course.org,
"rerun_link": f'/course_rerun/{course_id}',
"run": self.course.id.run,
"url": f'/course/{course_id}',
}],
"in_process_course_actions": [],
"libraries": [],
"libraries_enabled": True,
"library_authoring_mfe_url": settings.LIBRARY_AUTHORING_MICROFRONTEND_URL,
"optimization_enabled": False,
"redirect_to_library_authoring_mfe": False,
"request_course_creator_url": "/request_course_creator",
"rerun_creator_status": True,
"show_new_library_button": True,
"split_studio_home": False,
"studio_name": settings.STUDIO_NAME,
"studio_short_name": settings.STUDIO_SHORT_NAME,
"studio_request_email": "",
"tech_support_email": "[email protected]",
"platform_name": settings.PLATFORM_NAME,
"user_is_active": True,
}

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

@override_waffle_switch(ENABLE_GLOBAL_STAFF_OPTIMIZATION, True)
def test_org_query_if_passed(self):
"""Test home page when org filter passed as a query param"""
foo_course = self.store.make_course_key('foo-org', 'bar-number', 'baz-run')
test_course = CourseFactory.create(
org=foo_course.org,
number=foo_course.course,
run=foo_course.run
)
CourseOverviewFactory.create(id=test_course.id, org='foo-org')
response = self.client.get(self.url, {"org": "foo-org"})
self.assertEqual(len(response.data['courses']), 1)
self.assertEqual(response.status_code, status.HTTP_200_OK)

@override_waffle_switch(ENABLE_GLOBAL_STAFF_OPTIMIZATION, True)
def test_org_query_if_empty(self):
"""Test home page with an empty org query param"""
response = self.client.get(self.url)
self.assertEqual(len(response.data['courses']), 0)
self.assertEqual(response.status_code, status.HTTP_200_OK)
14 changes: 13 additions & 1 deletion cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
""" Contenstore API v1 URLs. """

from django.urls import re_path
from django.urls import re_path, path

from openedx.core.constants import COURSE_ID_PATTERN

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

app_name = 'v1'

urlpatterns = [
path(
'home',
HomePageView.as_view(),
name="home"
),
re_path(
fr'^proctored_exam_settings/{COURSE_ID_PATTERN}$',
ProctoredExamSettingsView.as_view(),
Expand All @@ -28,4 +35,9 @@
CourseDetailsView.as_view(),
name="course_details"
),
re_path(
fr'^course_rerun/{COURSE_ID_PATTERN}$',
CourseRerunView.as_view(),
name="course_rerun"
),
]
2 changes: 2 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
Views for v1 contentstore API.
"""
from .course_details import CourseDetailsView
from .course_rerun import CourseRerunView
from .home import HomePageView
from .settings import CourseSettingsView
from .proctoring import ProctoredExamSettingsView
76 changes: 76 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/course_rerun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
""" API Views for course rerun """

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.contentstore.utils import get_course_rerun_context
from cms.djangoapps.contentstore.rest_api.v1.serializers import CourseRerunSerializer
from common.djangoapps.student.roles import GlobalStaff
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
from xmodule.modulestore.django import modulestore


@view_auth_classes(is_authenticated=True)
class CourseRerunView(DeveloperErrorViewMixin, APIView):
"""
View for course rerun.
"""

@apidocs.schema(
parameters=[
apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"),
],
responses={
200: CourseRerunSerializer,
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 get(self, request: Request, course_id: str):
"""
Get an object containing course rerun.
**Example Request**
GET /api/contentstore/v1/course_rerun/{course_id}
**Response Values**
If the request is successful, an HTTP 200 "OK" response is returned.
The HTTP 200 response contains a single dict that contains keys that
are the course's rerun.
**Example Response**
```json
{
"allow_unicode_course_id": False,
"course_creator_status": "granted",
"number": "101",
"display_name": "new edx course",
"org": "edx",
"run": "2023",
}
```
"""

if not GlobalStaff().has_user(request.user):
self.permission_denied(request)

course_key = CourseKey.from_string(course_id)
with modulestore().bulk_operations(course_key):
course_block = modulestore().get_course(course_key)
course_rerun_context = get_course_rerun_context(course_key, course_block, request.user)
course_rerun_context.update({
'org': course_key.org,
'number': course_key.course,
'run': course_key.run,
})
serializer = CourseRerunSerializer(course_rerun_context)
return Response(serializer.data)
Loading

0 comments on commit e094571

Please sign in to comment.