diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index ee3e520b5966..6fdd9c16737a 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -5,7 +5,7 @@ from .course_rerun import CourseRerunSerializer from .course_team import CourseTeamSerializer from .grading import CourseGradingModelSerializer, CourseGradingSerializer -from .home import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer +from .home import CourseHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer from .proctoring import ( LimitedProctoredExamSettingsSerializer, ProctoredExamConfigurationSerializer, @@ -20,3 +20,4 @@ VideoUsageSerializer, VideoDownloadSerializer ) +from .vertical_block import ContainerHandlerSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index 80296b9a766c..1afc51ed77af 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -31,7 +31,7 @@ class LibraryViewSerializer(serializers.Serializer): can_edit = serializers.BooleanField() -class CourseTabSerializer(serializers.Serializer): +class CourseHomeTabSerializer(serializers.Serializer): archived_courses = CourseCommonSerializer(required=False, many=True) courses = CourseCommonSerializer(required=False, many=True) in_process_course_actions = UnsucceededCourseSerializer(many=True, required=False, allow_null=True) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py new file mode 100644 index 000000000000..c5b54e200e75 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py @@ -0,0 +1,92 @@ +""" +API Serializers for unit page +""" + +from django.urls import reverse +from rest_framework import serializers + +from cms.djangoapps.contentstore.helpers import ( + xblock_studio_url, + xblock_type_display_name, +) + + +class ChildAncestorSerializer(serializers.Serializer): + """ + Serializer for representing child blocks in the ancestor XBlock. + """ + + url = serializers.SerializerMethodField() + display_name = serializers.CharField(source="display_name_with_default") + + def get_url(self, obj): + """ + Method to generate studio URL for the child block. + """ + return xblock_studio_url(obj) + + +class AncestorXBlockSerializer(serializers.Serializer): + """ + Serializer for representing the ancestor XBlock and its children. + """ + + children = ChildAncestorSerializer(many=True) + title = serializers.CharField() + is_last = serializers.BooleanField() + + +class ContainerXBlock(serializers.Serializer): + """ + Serializer for representing XBlock data. Doesn't include all data about XBlock. + """ + + display_name = serializers.CharField(source="display_name_with_default") + display_type = serializers.SerializerMethodField() + category = serializers.CharField() + + def get_display_type(self, obj): + """ + Method to get the display type name for the container XBlock. + """ + return xblock_type_display_name(obj) + + +class ContainerHandlerSerializer(serializers.Serializer): + """ + Serializer for container handler + """ + + language_code = serializers.CharField() + action = serializers.CharField() + xblock = ContainerXBlock() + is_unit_page = serializers.BooleanField() + is_collapsible = serializers.BooleanField() + position = serializers.IntegerField(min_value=1) + prev_url = serializers.CharField(allow_null=True) + next_url = serializers.CharField(allow_null=True) + new_unit_category = serializers.CharField() + outline_url = serializers.CharField() + ancestor_xblocks = AncestorXBlockSerializer(many=True) + component_templates = serializers.ListField(child=serializers.DictField()) + xblock_info = serializers.DictField() + draft_preview_link = serializers.CharField() + published_preview_link = serializers.CharField() + show_unit_tags = serializers.BooleanField() + user_clipboard = serializers.DictField() + is_fullwidth_content = serializers.BooleanField() + assets_url = serializers.SerializerMethodField() + unit_block_id = serializers.CharField(source="unit.location.block_id") + subsection_location = serializers.CharField(source="subsection.location") + + def get_assets_url(self, obj): + """ + Method to get the assets URL based on the course id. + """ + + context_course = obj.get("context_course", None) + if context_course: + return reverse( + "assets_handler", kwargs={"course_key_string": context_course.id} + ) + return None diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index 625c6ad16889..52674fcfc90f 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -1,10 +1,12 @@ """ Contenstore API v1 URLs. """ +from django.conf import settings from django.urls import re_path, path from openedx.core.constants import COURSE_ID_PATTERN from .views import ( + ContainerHandlerView, CourseDetailsView, CourseTeamView, CourseGradingView, @@ -94,6 +96,11 @@ CourseRerunView.as_view(), name="course_rerun" ), + re_path( + fr'^container_handler/{settings.USAGE_KEY_PATTERN}$', + ContainerHandlerView.as_view(), + name="container_handler" + ), # Authoring API # Do not use under v1 yet (Nov. 23). The Authoring API is still experimental and the v0 versions should be used diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index 8cacb92c474b..802c9ccbd717 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -14,3 +14,4 @@ VideoDownloadView ) from .help_urls import HelpUrlsView +from .vertical_block import ContainerHandlerView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index f8ee907d2e9f..b06cec44b7d3 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -8,7 +8,7 @@ from openedx.core.lib.api.view_utils import view_auth_classes from ....utils import get_home_context, get_course_context, get_library_context -from ..serializers import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer +from ..serializers import CourseHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer @view_auth_classes(is_authenticated=True) @@ -102,7 +102,7 @@ class HomePageCoursesView(APIView): description="Query param to filter by course org", )], responses={ - 200: CourseTabSerializer, + 200: CourseHomeTabSerializer, 401: "The requester is not authenticated.", }, ) @@ -160,7 +160,7 @@ def get(self, request: Request): "archived_courses": archived_courses, "in_process_course_actions": in_process_course_actions, } - serializer = CourseTabSerializer(courses_context) + serializer = CourseHomeTabSerializer(courses_context) return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py new file mode 100644 index 000000000000..ff117c5ecfe6 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py @@ -0,0 +1,67 @@ +""" +Unit tests for the vertical block. +""" +from django.urls import reverse +from rest_framework import status + +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.tests.factories import BlockFactory # lint-amnesty, pylint: disable=wrong-import-order + + +class ContainerHandlerViewTest(CourseTestCase): + """ + Unit tests for the ContainerHandlerView. + """ + + def setUp(self): + super().setUp() + self.chapter = BlockFactory.create( + parent=self.course, category="chapter", display_name="Week 1" + ) + self.sequential = BlockFactory.create( + parent=self.chapter, category="sequential", display_name="Lesson 1" + ) + self.vertical = self._create_block(self.sequential, "vertical", "Unit") + + self.store = modulestore() + self.store.publish(self.vertical.location, self.user.id) + + def _get_reverse_url(self, location): + """ + Creates url to current handler view api + """ + return reverse( + "cms.djangoapps.contentstore:v1:container_handler", + kwargs={"usage_key_string": location}, + ) + + def _create_block(self, parent, category, display_name, **kwargs): + """ + Creates a block without publishing it. + """ + return BlockFactory.create( + parent=parent, + category=category, + display_name=display_name, + publish_item=False, + user_id=self.user.id, + **kwargs + ) + + def test_success_response(self): + """ + Check that endpoint is valid and success response. + """ + url = self._get_reverse_url(self.vertical.location) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_valid_usage_key_string(self): + """ + Check that invalid 'usage_key_string' raises Http404. + """ + usage_key_string = "i4x://InvalidOrg/InvalidCourse/vertical/static/InvalidContent" + url = self._get_reverse_url(usage_key_string) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py new file mode 100644 index 000000000000..a5c860f3fc67 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py @@ -0,0 +1,132 @@ +""" API Views for unit page """ + +import edx_api_doc_tools as apidocs +from django.http import Http404 +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import UsageKey +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_container_handler_context +from cms.djangoapps.contentstore.rest_api.v1.serializers import ContainerHandlerSerializer +from openedx.core.lib.api.view_utils import view_auth_classes +from xmodule.modulestore.django import modulestore + + +@view_auth_classes(is_authenticated=True) +class ContainerHandlerView(APIView): + """ + View for container xblock requests to get vertical data. + """ + + def get_object(self, usage_key_string): + """ + Get an object by usage-id of the block + """ + try: + usage_key = UsageKey.from_string(usage_key_string) + except InvalidKeyError: + raise Http404 # lint-amnesty, pylint: disable=raise-missing-from + return usage_key + + @apidocs.schema( + parameters=[ + apidocs.string_parameter( + "usage_key_string", + apidocs.ParameterLocation.PATH, + description="Container usage key", + ), + ], + responses={ + 200: ContainerHandlerSerializer, + 401: "The requester is not authenticated.", + 404: "The requested locator does not exist.", + }, + ) + def get(self, request: Request, usage_key_string: str): + """ + Get an object containing vertical data. + + **Example Request** + + GET /api/contentstore/v1/container_handler/{usage_key_string} + + **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 vertical's container data. + + **Example Response** + + ```json + { + "language_code": "zh-cn", + "action": "view", + "xblock": { + "display_name": "Labs and Demos", + "display_type": "单元", + "category": "vertical" + }, + "is_unit_page": true, + "is_collapsible": false, + "position": 1, + "prev_url": "block-v1-edX%2BDemo_Course%2Btype%40vertical%2Bblock%404e592689563243c484", + "next_url": "block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40vertical%2Bblock%40vertical_aae927868e55", + "new_unit_category": "vertical", + "outline_url": "/course/course-v1:edX+DemoX+Demo_Course?format=concise", + "ancestor_xblocks": [ + { + "children": [ + { + "url": "/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%", + "display_name": "Introduction" + }, + ... + ], + "title": "Example Week 2: Get Interactive", + "is_last": false + }, + ... + ], + "component_templates": [ + { + "type": "advanced", + "templates": [ + { + "display_name": "批注", + "category": "annotatable", + "boilerplate_name": null, + "hinted": false, + "tab": "common", + "support_level": true + }, + ... + }, + ... + ], + "xblock_info": {}, + "draft_preview_link": "//preview.localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/...", + "published_preview_link": "///courses/course-v1:edX+DemoX+Demo_Course/jump_to/...", + "show_unit_tags": false, + "user_clipboard": { + "content": null, + "source_usage_key": "", + "source_context_title": "", + "source_edit_url": "" + }, + "is_fullwidth_content": false, + "assets_url": "/assets/course-v1:edX+DemoX+Demo_Course/", + "unit_block_id": "d6cee45205a449369d7ef8f159b22bdf", + "subsection_location": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations" + } + ``` + """ + usage_key = self.get_object(usage_key_string) + course_key = usage_key.course_key + with modulestore().bulk_operations(course_key): + context = get_container_handler_context(request, usage_key) + serializer = ContainerHandlerSerializer(context) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 76dae28a6e7d..5d36ac9e6dc4 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -7,10 +7,13 @@ from collections import defaultdict from contextlib import contextmanager from datetime import datetime, timezone +from urllib.parse import quote_plus from uuid import uuid4 from django.conf import settings from django.core.exceptions import ValidationError +from django.http import HttpResponseBadRequest +from django.shortcuts import redirect from django.urls import reverse from django.utils import translation from django.utils.translation import gettext as _ @@ -1688,6 +1691,136 @@ def get_course_videos_context(course_block, pagination_conf, course_key=None): return course_video_context +def get_container_handler_context(request, usage_key): + """ + Utils is used to get context for container xblock requests. + It is used for both DRF and django views. + """ + + from cms.djangoapps.contentstore.views.component import ( + _get_item_in_course, + get_component_templates, + get_unit_tags, + CONTAINER_TEMPLATES, + LIBRARY_BLOCK_TYPES, + ) + from cms.djangoapps.contentstore.helpers import get_parent_xblock, is_unit + from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import ( + add_container_page_publishing_info, + create_xblock_info, + ) + from openedx.core.djangoapps.content_staging import api as content_staging_api + + try: + course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key) + except ItemNotFoundError: + return HttpResponseBadRequest() + + component_templates = get_component_templates(course) + ancestor_xblocks = [] + parent = get_parent_xblock(xblock) + action = request.GET.get('action', 'view') + + is_unit_page = is_unit(xblock) + unit = xblock if is_unit_page else None + + if is_unit_page and use_new_unit_page(course.id): + return redirect(get_unit_url(course.id, unit.location)) + + is_first = True + block = xblock + + # Build the breadcrumbs and find the ``Unit`` ancestor + # if it is not the immediate parent. + while parent: + + if unit is None and is_unit(block): + unit = block + + # add all to nav except current xblock page + if xblock != block: + current_block = { + 'title': block.display_name_with_default, + 'children': parent.get_children(), + 'is_last': is_first + } + is_first = False + ancestor_xblocks.append(current_block) + + block = parent + parent = get_parent_xblock(parent) + + ancestor_xblocks.reverse() + + assert unit is not None, "Could not determine unit page" + subsection = get_parent_xblock(unit) + assert subsection is not None, "Could not determine parent subsection from unit " + str( + unit.location) + section = get_parent_xblock(subsection) + assert section is not None, "Could not determine ancestor section from unit " + str(unit.location) + + # for the sequence navigator + prev_url, next_url = get_sibling_urls(subsection, unit.location) + # these are quoted here because they'll end up in a query string on the page, + # and quoting with mako will trigger the xss linter... + prev_url = quote_plus(prev_url) if prev_url else None + next_url = quote_plus(next_url) if next_url else None + + show_unit_tags = use_tagging_taxonomy_list_page() + unit_tags = None + if show_unit_tags and is_unit_page: + unit_tags = get_unit_tags(usage_key) + + # Fetch the XBlock info for use by the container page. Note that it includes information + # about the block's ancestors and siblings for use by the Unit Outline. + xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page, tags=unit_tags) + + if is_unit_page: + add_container_page_publishing_info(xblock, xblock_info) + + # need to figure out where this item is in the list of children as the + # preview will need this + index = 1 + for child in subsection.get_children(): + if child.location == unit.location: + break + index += 1 + + # Get the status of the user's clipboard so they can paste components if they have something to paste + user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request) + library_block_types = [problem_type['component'] for problem_type in LIBRARY_BLOCK_TYPES] + is_library_xblock = xblock.location.block_type in library_block_types + + context = { + 'language_code': request.LANGUAGE_CODE, + 'context_course': course, # Needed only for display of menus at top of page. + 'action': action, + 'xblock': xblock, + 'xblock_locator': xblock.location, + 'unit': unit, + 'is_unit_page': is_unit_page, + 'is_collapsible': is_library_xblock, + 'subsection': subsection, + 'section': section, + 'position': index, + 'prev_url': prev_url, + 'next_url': next_url, + 'new_unit_category': 'vertical', + 'outline_url': '{url}?format=concise'.format(url=reverse_course_url('course_handler', course.id)), + 'ancestor_xblocks': ancestor_xblocks, + 'component_templates': component_templates, + 'xblock_info': xblock_info, + 'draft_preview_link': preview_lms_link, + 'published_preview_link': lms_link, + 'templates': CONTAINER_TEMPLATES, + 'show_unit_tags': show_unit_tags, + # Status of the user's clipboard, exactly as would be returned from the "GET clipboard" REST API. + 'user_clipboard': user_clipboard, + 'is_fullwidth_content': is_library_xblock, + } + return context + + class StudioPermissionsService: """ Service that can provide information about a user's permissions. diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index bafcdad35c71..193a62dcaa0e 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -4,13 +4,11 @@ import logging -from urllib.parse import quote_plus from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponseBadRequest -from django.shortcuts import redirect from django.utils.translation import gettext as _ from django.views.decorators.http import require_GET from opaque_keys import InvalidKeyError @@ -25,24 +23,12 @@ from common.djangoapps.student.auth import has_course_author_access from common.djangoapps.xblock_django.api import authorable_xblocks, disabled_xblocks from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag -from cms.djangoapps.contentstore.toggles import ( - use_new_problem_editor, - use_tagging_taxonomy_list_page, -) +from cms.djangoapps.contentstore.toggles import use_new_problem_editor from openedx.core.lib.xblock_utils import get_aside_from_xblock, is_xblock_aside from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration -from openedx.core.djangoapps.content_staging import api as content_staging_api from openedx.core.djangoapps.content_tagging.api import get_content_tags from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order -from ..toggles import use_new_unit_page -from ..utils import get_lms_link_for_item, get_sibling_urls, reverse_course_url, get_unit_url -from ..helpers import get_parent_xblock, is_unit, xblock_type_display_name -from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import ( - add_container_page_publishing_info, - create_xblock_info, - load_services_for_studio, -) +from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import load_services_for_studio __all__ = [ 'container_handler', @@ -121,6 +107,9 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st html: returns the HTML page for editing a container json: not currently supported """ + + from ..utils import get_container_handler_context + if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): try: @@ -128,112 +117,8 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st except InvalidKeyError: # Raise Http404 on invalid 'usage_key_string' raise Http404 # lint-amnesty, pylint: disable=raise-missing-from with modulestore().bulk_operations(usage_key.course_key): - try: - course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key) - except ItemNotFoundError: - return HttpResponseBadRequest() - component_templates = get_component_templates(course) - ancestor_xblocks = [] - parent = get_parent_xblock(xblock) - action = request.GET.get('action', 'view') - - is_unit_page = is_unit(xblock) - unit = xblock if is_unit_page else None - - if is_unit_page and use_new_unit_page(course.id): - return redirect(get_unit_url(course.id, unit.location)) - - is_first = True - block = xblock - - # Build the breadcrumbs and find the ``Unit`` ancestor - # if it is not the immediate parent. - while parent: - - if unit is None and is_unit(block): - unit = block - - # add all to nav except current xblock page - if xblock != block: - current_block = { - 'title': block.display_name_with_default, - 'children': parent.get_children(), - 'is_last': is_first - } - is_first = False - ancestor_xblocks.append(current_block) - - block = parent - parent = get_parent_xblock(parent) - - ancestor_xblocks.reverse() - - assert unit is not None, "Could not determine unit page" - subsection = get_parent_xblock(unit) - assert subsection is not None, "Could not determine parent subsection from unit " + str( - unit.location) - section = get_parent_xblock(subsection) - assert section is not None, "Could not determine ancestor section from unit " + str(unit.location) - - # for the sequence navigator - prev_url, next_url = get_sibling_urls(subsection, unit.location) - # these are quoted here because they'll end up in a query string on the page, - # and quoting with mako will trigger the xss linter... - prev_url = quote_plus(prev_url) if prev_url else None - next_url = quote_plus(next_url) if next_url else None - - show_unit_tags = use_tagging_taxonomy_list_page() - unit_tags = None - if show_unit_tags and is_unit_page: - unit_tags = get_unit_tags(usage_key) - - # Fetch the XBlock info for use by the container page. Note that it includes information - # about the block's ancestors and siblings for use by the Unit Outline. - xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page, tags=unit_tags) - - if is_unit_page: - add_container_page_publishing_info(xblock, xblock_info) - - # need to figure out where this item is in the list of children as the - # preview will need this - index = 1 - for child in subsection.get_children(): - if child.location == unit.location: - break - index += 1 - - # Get the status of the user's clipboard so they can paste components if they have something to paste - user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request) - library_block_types = [problem_type['component'] for problem_type in LIBRARY_BLOCK_TYPES] - is_library_xblock = xblock.location.block_type in library_block_types - - return render_to_response('container.html', { - 'language_code': request.LANGUAGE_CODE, - 'context_course': course, # Needed only for display of menus at top of page. - 'action': action, - 'xblock': xblock, - 'xblock_locator': xblock.location, - 'unit': unit, - 'is_unit_page': is_unit_page, - 'is_collapsible': is_library_xblock, - 'subsection': subsection, - 'section': section, - 'position': index, - 'prev_url': prev_url, - 'next_url': next_url, - 'new_unit_category': 'vertical', - 'outline_url': '{url}?format=concise'.format(url=reverse_course_url('course_handler', course.id)), - 'ancestor_xblocks': ancestor_xblocks, - 'component_templates': component_templates, - 'xblock_info': xblock_info, - 'draft_preview_link': preview_lms_link, - 'published_preview_link': lms_link, - 'templates': CONTAINER_TEMPLATES, - 'show_unit_tags': show_unit_tags, - # Status of the user's clipboard, exactly as would be returned from the "GET clipboard" REST API. - 'user_clipboard': user_clipboard, - 'is_fullwidth_content': is_library_xblock, - }) + container_handler_context = get_container_handler_context(request, usage_key) + return render_to_response('container.html', container_handler_context) else: return HttpResponseBadRequest("Only supports HTML requests") @@ -242,6 +127,9 @@ def get_component_templates(courselike, library=False): # lint-amnesty, pylint: """ Returns the applicable component templates that can be used by the specified course or library. """ + + from ..helpers import xblock_type_display_name + def create_template_dict(name, category, support_level, boilerplate_name=None, tab="common", hinted=False): """ Creates a component template dict. @@ -545,6 +433,9 @@ def _get_item_in_course(request, usage_key): Verifies that the caller has permission to access this item. """ + + from ..utils import get_lms_link_for_item + # usage_key's course_key may have an empty run property usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key)) diff --git a/openedx/core/djangoapps/content_staging/serializers.py b/openedx/core/djangoapps/content_staging/serializers.py index 06b687838f94..bf71fd9b1bb7 100644 --- a/openedx/core/djangoapps/content_staging/serializers.py +++ b/openedx/core/djangoapps/content_staging/serializers.py @@ -3,7 +3,6 @@ """ from rest_framework import serializers -from cms.djangoapps.contentstore.helpers import xblock_studio_url, xblock_type_display_name from common.djangoapps.student.auth import has_studio_read_access from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError @@ -34,6 +33,8 @@ class Meta: def get_block_type_display(self, obj): """ Get the friendly name for this XBlock/component type """ + from cms.djangoapps.contentstore.helpers import xblock_type_display_name + return xblock_type_display_name(obj.block_type) @@ -50,6 +51,8 @@ class UserClipboardSerializer(serializers.Serializer): def get_source_edit_url(self, obj) -> str: """ Get the URL where the user can edit the given XBlock, if it exists """ + from cms.djangoapps.contentstore.helpers import xblock_studio_url + request = self.context.get("request", None) user = request.user if request else None if not user: