-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Add new container page that can display nested xblocks #2539
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| import logging | ||
|
|
||
| from django.http import HttpResponse | ||
| from django.shortcuts import redirect | ||
| from edxmako.shortcuts import render_to_string, render_to_response | ||
| from xmodule.modulestore.django import loc_mapper, modulestore | ||
|
|
||
| __all__ = ['edge', 'event', 'landing'] | ||
|
|
||
|
|
@@ -35,3 +38,64 @@ def _xmodule_recurse(item, action): | |
| _xmodule_recurse(child, action) | ||
|
|
||
| action(item) | ||
|
|
||
|
|
||
| def get_parent_xblock(xblock): | ||
| """ | ||
| Returns the xblock that is the parent of the specified xblock, or None if it has no parent. | ||
| """ | ||
| locator = xblock.location | ||
| parent_locations = modulestore().get_parent_locations(locator, None) | ||
|
|
||
| if len(parent_locations) == 0: | ||
| return None | ||
| elif len(parent_locations) > 1: | ||
| logging.error('Multiple parents have been found for %s', unicode(locator)) | ||
| return modulestore().get_item(parent_locations[0]) | ||
|
|
||
|
|
||
| def _xblock_has_studio_page(xblock): | ||
| """ | ||
| Returns true if the specified xblock has an associated Studio page. Most xblocks do | ||
| not have their own page but are instead shown on the page of their parent. There | ||
| are a few exceptions: | ||
| 1. Courses | ||
| 2. Verticals | ||
| 3. XBlocks with children, except for: | ||
| - subsections (aka sequential blocks) | ||
| - chapters | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So much clearer than it used to be! Thank you! 😍 |
||
| """ | ||
| category = xblock.category | ||
| if category in ('course', 'vertical'): | ||
| return True | ||
| elif category in ('sequential', 'chapter'): | ||
| return False | ||
| elif xblock.has_children: | ||
| return True | ||
| else: | ||
| return False | ||
|
|
||
|
|
||
| def xblock_studio_url(xblock, course=None): | ||
| """ | ||
| Returns the Studio editing URL for the specified xblock. | ||
| """ | ||
| if not _xblock_has_studio_page(xblock): | ||
| return None | ||
| category = xblock.category | ||
| parent_xblock = get_parent_xblock(xblock) | ||
| if parent_xblock: | ||
| parent_category = parent_xblock.category | ||
| else: | ||
| parent_category = None | ||
| if category == 'course': | ||
| prefix = 'course' | ||
| elif category == 'vertical' and parent_category == 'sequential': | ||
| prefix = 'unit' # only show the unit page for verticals directly beneath a subsection | ||
| else: | ||
| prefix = 'container' | ||
| course_id = None | ||
| if course: | ||
| course_id = course.location.course_id | ||
| locator = loc_mapper().translate_location(course_id, xblock.location) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if you call
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. translate tries to guess the course id from the location in that case. If the location's category is 'course' , it's perfect. Otherwise, it will used the abbreviated course id (org/course w/o run).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some of the code paths I refactored to use this method were passing None so I left the behavior. As Don says, this does seem to be supported. I tried always passing None, but there were cases where the course was needed, so that's why I made it optional. |
||
| return locator.url_reverse(prefix) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ | |
|
|
||
| from django.core.exceptions import PermissionDenied | ||
| from django.contrib.auth.decorators import login_required | ||
| from django.http import HttpResponseBadRequest, HttpResponse | ||
| from django.http import HttpResponseBadRequest, HttpResponse, Http404 | ||
| from django.utils.translation import ugettext as _ | ||
| from django.views.decorators.http import require_http_methods | ||
|
|
||
|
|
@@ -164,7 +164,6 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid | |
| content_type="text/plain" | ||
| ) | ||
|
|
||
|
|
||
| # pylint: disable=unused-argument | ||
| @require_http_methods(("GET")) | ||
| @login_required | ||
|
|
@@ -185,7 +184,7 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v | |
|
|
||
| accept_header = request.META.get('HTTP_ACCEPT', 'application/json') | ||
|
|
||
| if 'application/x-fragment+json' in accept_header: | ||
| if 'application/json' in accept_header: | ||
| store = get_modulestore(old_location) | ||
| component = store.get_item(old_location) | ||
|
|
||
|
|
@@ -204,17 +203,46 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v | |
| fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)})) | ||
|
|
||
| store.save_xmodule(component) | ||
|
|
||
| elif view_name == 'student_view': | ||
| fragment = get_preview_fragment(request, component) | ||
| fragment.content = render_to_string('component.html', { | ||
| 'preview': fragment.content, | ||
| 'label': component.display_name or component.scope_ids.block_type, | ||
|
|
||
| # Native XBlocks are responsible for persisting their own data, | ||
| # so they are also responsible for providing save/cancel buttons. | ||
| 'show_save_cancel': isinstance(component, xmodule.x_module.XModuleDescriptor), | ||
| elif view_name == 'student_view' and component.has_children: | ||
| # For non-leaf xblocks on the unit page, show the special rendering | ||
| # which links to the new container page. | ||
| course_location = loc_mapper().translate_locator_to_location(locator, True) | ||
| course = store.get_item(course_location) | ||
| html = render_to_string('unit_container_xblock_component.html', { | ||
| 'course': course, | ||
| 'xblock': component, | ||
| 'locator': locator | ||
| }) | ||
| return JsonResponse({ | ||
| 'html': html, | ||
| 'resources': [], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's a bit silly to be using a JsonResponse to return HTML and nothing else, but I understand the need to be compatible with cases where you have other things you want to return in addition. Just pointing out that it's a bit awkward the way it is now.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good point. The problem is that the client doesn't know which of the two responses it will get for a particular child it is rendering. I could have the client handle either type of response dynamically, but that then pushes the complexity down to the JavaScript. I think I'm more comfortable leaving it like this. |
||
| }) | ||
| elif view_name in ('student_view', 'container_preview'): | ||
| is_container_view = (view_name == 'container_preview') | ||
|
|
||
| # Only show the new style HTML for the container view, i.e. for non-verticals | ||
| # Note: this special case logic can be removed once the unit page is replaced | ||
| # with the new container view. | ||
| is_read_only_view = is_container_view | ||
| context = { | ||
| 'container_view': is_container_view, | ||
| 'read_only': is_read_only_view, | ||
| 'root_xblock': component | ||
| } | ||
|
|
||
| fragment = get_preview_fragment(request, component, context) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a comment that fragment.content is added for container view in get_preview_fragment. |
||
| # For old-style pages (such as unit and static pages), wrap the preview with | ||
| # the component div. Note that the container view recursively adds headers | ||
| # into the preview fragment, so we don't want to add another header here. | ||
| if not is_container_view: | ||
| fragment.content = render_to_string('component.html', { | ||
| 'preview': fragment.content, | ||
| 'label': component.display_name or component.scope_ids.block_type, | ||
|
|
||
| # Native XBlocks are responsible for persisting their own data, | ||
| # so they are also responsible for providing save/cancel buttons. | ||
| 'show_save_cancel': isinstance(component, xmodule.x_module.XModuleDescriptor), | ||
| }) | ||
| else: | ||
| raise Http404 | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this raise an exception, instead of logging?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this code was just moved from another location (and I very much approve of sharing code instead of duplicating it). But in an ideal world that wasn't the day before the end of the iteration, I would request unit tests for this method in test_helpers.py.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried that, but the problem is that there are callers depending upon getting the first item in this situation. I don't fully understand why xblocks can have more than one parent, but it does seem to come up in the unit tests. The code I refactored into this method had the logging, so I just left it as is.
Let me know how you feel about this.