Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create DRF endpoint to get course index context #33943

Merged
merged 5 commits into from
Jan 8, 2024
Merged
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
20 changes: 20 additions & 0 deletions cms/djangoapps/contentstore/rest_api/serializers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,23 @@ def to_internal_value(self, data):
)

return ret


class ProctoringErrorModelSerializer(serializers.Serializer):
"""
Serializer for proctoring error model item.
"""
deprecated = serializers.BooleanField()
display_name = serializers.CharField()
help = serializers.CharField()
hide_on_enabled_publisher = serializers.BooleanField()
value = serializers.CharField()


class ProctoringErrorListSerializer(serializers.Serializer):
"""
Serializer for proctoring error list.
"""
key = serializers.CharField()
message = serializers.CharField()
model = ProctoringErrorModelSerializer()
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .course_details import CourseDetailsSerializer
from .course_rerun import CourseRerunSerializer
from .course_team import CourseTeamSerializer
from .course_index import CourseIndexSerializer
from .grading import CourseGradingModelSerializer, CourseGradingSerializer
from .home import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer
from .proctoring import (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
API Serializers for course index
"""

from rest_framework import serializers

from cms.djangoapps.contentstore.rest_api.serializers.common import ProctoringErrorListSerializer


class InitialIndexStateSerializer(serializers.Serializer):
"""Serializer for initial course index state"""
expanded_locators = serializers.ListSerializer(child=serializers.CharField())
locator_to_show = serializers.CharField()


class CourseIndexSerializer(serializers.Serializer):
"""Serializer for course index"""
course_release_date = serializers.CharField()
course_structure = serializers.DictField()
deprecated_blocks_info = serializers.DictField()
discussions_incontext_feedback_url = serializers.CharField()
discussions_incontext_learnmore_url = serializers.CharField()
initial_state = InitialIndexStateSerializer()
initial_user_clipboard = serializers.DictField()
language_code = serializers.CharField()
lms_link = serializers.CharField()
mfe_proctored_exam_settings_url = serializers.CharField()
notification_dismiss_url = serializers.CharField()
proctoring_errors = ProctoringErrorListSerializer(many=True)
reindex_link = serializers.CharField()
rerun_notification_id = serializers.IntegerField()
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from rest_framework import serializers

from cms.djangoapps.contentstore.rest_api.serializers.common import ProctoringErrorListSerializer
from xmodule.course_block import get_available_providers


Expand Down Expand Up @@ -31,26 +32,6 @@ class ProctoredExamConfigurationSerializer(serializers.Serializer):
course_start_date = serializers.DateTimeField()


class ProctoringErrorModelSerializer(serializers.Serializer):
"""
Serializer for proctoring error model item.
"""
deprecated = serializers.BooleanField()
display_name = serializers.CharField()
help = serializers.CharField()
hide_on_enabled_publisher = serializers.BooleanField()
value = serializers.CharField()


class ProctoringErrorListSerializer(serializers.Serializer):
"""
Serializer for proctoring error list.
"""
key = serializers.CharField()
message = serializers.CharField()
model = ProctoringErrorModelSerializer()


class ProctoringErrorsSerializer(serializers.Serializer):
"""
Serializer for proctoring errors with url to proctored exam settings.
Expand Down
6 changes: 6 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .views import (
CourseDetailsView,
CourseTeamView,
CourseIndexView,
CourseGradingView,
CourseRerunView,
CourseSettingsView,
Expand Down Expand Up @@ -69,6 +70,11 @@
CourseSettingsView.as_view(),
name="course_settings"
),
re_path(
fr'^course_index/{COURSE_ID_PATTERN}$',
CourseIndexView.as_view(),
name="course_index"
),
re_path(
fr'^course_details/{COURSE_ID_PATTERN}$',
CourseDetailsView.as_view(),
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Views for v1 contentstore API.
"""
from .course_details import CourseDetailsView
from .course_index import CourseIndexView
from .course_team import CourseTeamView
from .course_rerun import CourseRerunView
from .grading import CourseGradingView
Expand Down
98 changes: 98 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/course_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""API Views for course index"""

import edx_api_doc_tools as apidocs
from django.conf import settings
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.rest_api.v1.serializers import CourseIndexSerializer
from cms.djangoapps.contentstore.utils import get_course_index_context
from common.djangoapps.student.auth import has_studio_read_access
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes


@view_auth_classes(is_authenticated=True)
class CourseIndexView(DeveloperErrorViewMixin, APIView):
"""View for Course Index"""

@apidocs.schema(
parameters=[
apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"),
apidocs.string_parameter(
"show",
apidocs.ParameterLocation.QUERY,
description="Query param to set initial state which fully expanded to see the item",
)],
responses={
200: CourseIndexSerializer,
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 index for outline.

**Example Request**

GET /api/contentstore/v1/course_index/{course_id}?show=block-v1:edx+101+y+type@course+block@course

**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 outline.

**Example Response**

```json
{
"course_release_date": "Set Date",
"course_structure": {},
"deprecated_blocks_info": {
"deprecated_enabled_block_types": [],
"blocks": [],
"advance_settings_url": "/settings/advanced/course-v1:edx+101+y76"
},
"discussions_incontext_feedback_url": "",
"discussions_incontext_learnmore_url": "",
"initial_state": {
"expanded_locators": [
"block-v1:edx+101+y76+type@chapter+block@03de0adc9d1c4cc097062d80eb04abf6",
"block-v1:edx+101+y76+type@sequential+block@8a85e287e30a47e98d8c1f37f74a6a9d"
],
"locator_to_show": "block-v1:edx+101+y76+type@chapter+block@03de0adc9d1c4cc097062d80eb04abf6"
},
"initial_user_clipboard": {
"content": null,
"source_usage_key": "",
"source_context_title": "",
"source_edit_url": ""
},
"language_code": "en",
"lms_link": "//localhost:18000/courses/course-v1:edx+101+y76/jump_to/block-v1:edx+101+y76",
"mfe_proctored_exam_settings_url": "",
"notification_dismiss_url": "/course_notifications/course-v1:edx+101+y76/2",
"proctoring_errors": [],
"reindex_link": "/course/course-v1:edx+101+y76/search_reindex",
"rerun_notification_id": 2
}
```
"""

course_key = CourseKey.from_string(course_id)
if not has_studio_read_access(request.user, course_key):
self.permission_denied(request)
course_index_context = get_course_index_context(request, course_key)
course_index_context.update({
"discussions_incontext_learnmore_url": settings.DISCUSSIONS_INCONTEXT_LEARNMORE_URL,
"discussions_incontext_feedback_url": settings.DISCUSSIONS_INCONTEXT_FEEDBACK_URL,
})

serializer = CourseIndexSerializer(course_index_context)
return Response(serializer.data)
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""
Unit tests for course index outline.
"""
from django.test import RequestFactory
from django.urls import reverse
from rest_framework import status

from cms.djangoapps.contentstore.rest_api.v1.mixins import PermissionAccessMixin
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.utils import get_lms_link_for_item
from cms.djangoapps.contentstore.views.course import _course_outline_json
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from xmodule.modulestore.tests.factories import BlockFactory, check_mongo_calls


class CourseIndexViewTest(CourseTestCase, PermissionAccessMixin):
"""
Tests for CourseIndexView.
"""

def setUp(self):
super().setUp()
with self.store.bulk_operations(self.course.id, emit_signals=False):
self.chapter = BlockFactory.create(
parent=self.course, display_name='Overview'
)
self.section = BlockFactory.create(
parent=self.chapter, display_name='Welcome'
)
self.unit = BlockFactory.create(
parent=self.section, display_name='New Unit'
)
self.xblock = BlockFactory.create(
parent=self.unit,
category='problem',
display_name='Some problem'
)
self.user = UserFactory()
self.factory = RequestFactory()
self.request = self.factory.get(f"/course/{self.course.id}")
self.request.user = self.user
self.reload_course()
self.url = reverse(
"cms.djangoapps.contentstore:v1:course_index",
kwargs={"course_id": self.course.id},
)

def test_course_index_response(self):
"""Check successful response content"""
response = self.client.get(self.url)
expected_response = {
"course_release_date": "Set Date",
"course_structure": _course_outline_json(self.request, self.course),
"deprecated_blocks_info": {
"deprecated_enabled_block_types": [],
"blocks": [],
"advance_settings_url": f"/settings/advanced/{self.course.id}"
},
"discussions_incontext_feedback_url": "",
"discussions_incontext_learnmore_url": "",
"initial_state": None,
"initial_user_clipboard": {
"content": None,
"source_usage_key": "",
"source_context_title": "",
"source_edit_url": ""
},
"language_code": "en",
"lms_link": get_lms_link_for_item(self.course.location),
"mfe_proctored_exam_settings_url": "",
"notification_dismiss_url": None,
"proctoring_errors": [],
"reindex_link": f"/course/{self.course.id}/search_reindex",
"rerun_notification_id": None
}

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

def test_course_index_response_with_show_locators(self):
"""Check successful response content with show query param"""
response = self.client.get(self.url, {"show": str(self.unit.location)})
expected_response = {
"course_release_date": "Set Date",
"course_structure": _course_outline_json(self.request, self.course),
"deprecated_blocks_info": {
"deprecated_enabled_block_types": [],
"blocks": [],
"advance_settings_url": f"/settings/advanced/{self.course.id}"
},
"discussions_incontext_feedback_url": "",
"discussions_incontext_learnmore_url": "",
"initial_state": {
"expanded_locators": [
str(self.unit.location),
str(self.xblock.location),
],
"locator_to_show": str(self.unit.location),
},
"initial_user_clipboard": {
"content": None,
"source_usage_key": "",
"source_context_title": "",
"source_edit_url": ""
},
"language_code": "en",
"lms_link": get_lms_link_for_item(self.course.location),
"mfe_proctored_exam_settings_url": "",
"notification_dismiss_url": None,
"proctoring_errors": [],
"reindex_link": f"/course/{self.course.id}/search_reindex",
"rerun_notification_id": None
}

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

def test_course_index_response_with_invalid_course(self):
"""Check error response for invalid course id"""
response = self.client.get(self.url + "1")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.data, {
"developer_message": f"Unknown course {self.course.id}1",
"error_code": "course_does_not_exist"
})

def test_number_of_calls_to_db(self):
"""
Test to check number of queries made to mysql and mongo
"""
with self.assertNumQueries(29, table_ignorelist=WAFFLE_TABLES):
with check_mongo_calls(3):
self.client.get(self.url)
Loading
Loading