feat: Unit page API as DRF
This commit is contained in:
@@ -6,7 +6,7 @@ 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 .home import CourseHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer
|
||||
from .proctoring import (
|
||||
LimitedProctoredExamSettingsSerializer,
|
||||
ProctoredExamConfigurationSerializer,
|
||||
@@ -21,3 +21,4 @@ from .videos import (
|
||||
VideoUsageSerializer,
|
||||
VideoDownloadSerializer
|
||||
)
|
||||
from .vertical_block import ContainerHandlerSerializer
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
CourseIndexView,
|
||||
@@ -100,6 +102,11 @@ urlpatterns = [
|
||||
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
|
||||
|
||||
@@ -15,3 +15,4 @@ from .videos import (
|
||||
VideoDownloadView
|
||||
)
|
||||
from .help_urls import HelpUrlsView
|
||||
from .vertical_block import ContainerHandlerView
|
||||
|
||||
@@ -8,7 +8,7 @@ from rest_framework.views import APIView
|
||||
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 @@ class HomePageCoursesView(APIView):
|
||||
"archived_courses": archived_courses,
|
||||
"in_process_course_actions": in_process_course_actions,
|
||||
}
|
||||
serializer = CourseTabSerializer(courses_context)
|
||||
serializer = CourseHomeTabSerializer(courses_context)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
143
cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py
Normal file
143
cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py
Normal file
@@ -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)
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
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
|
||||
@@ -1784,6 +1785,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): # pylint: disable=too-many-statements
|
||||
"""
|
||||
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.
|
||||
|
||||
@@ -4,7 +4,6 @@ Studio component views
|
||||
|
||||
|
||||
import logging
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@@ -25,24 +24,14 @@ from common.djangoapps.edxmako.shortcuts import render_to_response
|
||||
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 cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import load_services_for_studio
|
||||
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 +110,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 +124,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 +131,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")
|
||||
|
||||
@@ -242,6 +145,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 +451,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))
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ Serializers for the content libraries REST API
|
||||
"""
|
||||
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 StagedContentSerializer(serializers.ModelSerializer):
|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user