From ef972dc9675b17a166714e2a58c1290c7407ff79 Mon Sep 17 00:00:00 2001 From: ruzniaievdm Date: Mon, 8 Jan 2024 16:43:56 +0200 Subject: [PATCH] fix: [AXIMST-10] Redirect unit page if flag MFE enabled (#2487) --- .../rest_api/v1/views/vertical_block.py | 143 ++++++++++++++++++ cms/djangoapps/contentstore/utils.py | 124 +++++++++++++++ .../contentstore/views/component.py | 114 +------------- 3 files changed, 275 insertions(+), 106 deletions(-) create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py 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..3f9a04851173 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py @@ -0,0 +1,143 @@ +""" API Views for unit page """ + +import edx_api_doc_tools as apidocs +from django.http import Http404, HttpResponseBadRequest +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.views.component import _get_item_in_course +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 # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order + + +@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): + try: + course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key) + except ItemNotFoundError: + return HttpResponseBadRequest() + + context = get_container_handler_context(request, usage_key, course, xblock) + context.update({ + 'draft_preview_link': preview_lms_link, + 'published_preview_link': lms_link, + }) + serializer = ContainerHandlerSerializer(context) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 61254cc6d29c..9cbb1ac426a9 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -7,6 +7,7 @@ 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 @@ -1786,6 +1787,129 @@ def _get_course_index_context(request, course_key, course_block): return course_index_context +def get_container_handler_context(request, usage_key, course, xblock): + """ + 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_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 + + 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 + + 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() + + if unit is None: + raise ValueError("Could not determine unit page") + + subsection = get_parent_xblock(unit) + if subsection is None: + raise ValueError(f"Could not determine parent subsection from unit {unit.location}") + + section = get_parent_xblock(subsection) + if section is None: + raise ValueError(f"Could not determine ancestor section from unit {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, + '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..9868a9ea393c 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -4,7 +4,6 @@ import logging -from urllib.parse import quote_plus from django.conf import settings from django.contrib.auth.decorators import login_required @@ -25,24 +24,13 @@ 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.helpers import is_unit +from cms.djangoapps.contentstore.toggles import use_new_problem_editor, use_new_unit_page 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, -) __all__ = [ 'container_handler', @@ -121,6 +109,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, get_unit_url + if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): try: @@ -132,10 +123,6 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st 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 @@ -143,97 +130,12 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st 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, + container_handler_context = get_container_handler_context(request, usage_key, course, xblock) + container_handler_context.update({ '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 render_to_response('container.html', container_handler_context) else: return HttpResponseBadRequest("Only supports HTML requests")