Merge pull request #1949 from cpennington/xblock-studio-js-and-css

Enable XBlock js and css in Studio
This commit is contained in:
Calen Pennington
2014-02-05 08:19:45 -05:00
28 changed files with 321 additions and 193 deletions

View File

@@ -1,7 +1,8 @@
#pylint: disable=E1101
import shutil
import json
import mock
import shutil
from textwrap import dedent
@@ -503,7 +504,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
This verifies that a video caption url is as we expect it to be
"""
resp = self._test_preview(Location('i4x', 'edX', 'toy', 'video', 'sample_video', None))
self.assertContains(resp, 'data-caption-asset-path="/c4x/edX/toy/asset/subs_"')
self.assertEquals(resp.status_code, 200)
content = json.loads(resp.content)
self.assertIn('data-caption-asset-path="/c4x/edX/toy/asset/subs_"', content['html'])
def _test_preview(self, location):
""" Preview test case. """
@@ -514,7 +517,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
locator = loc_mapper().translate_location(
course_items[0].location.course_id, location, True, True
)
resp = self.client.get_html(locator.url_reverse('xblock'))
resp = self.client.get_fragment(locator.url_reverse('xblock'))
self.assertEqual(resp.status_code, 200)
# TODO: uncomment when preview no longer has locations being returned.
# _test_no_locations(self, resp)

View File

@@ -57,6 +57,13 @@ class AjaxEnabledTestClient(Client):
"""
return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra)
def get_fragment(self, path, data=None, follow=False, **extra):
"""
Convenience method for client.get which sets the accept type to application/x-fragment+json
"""
return self.get(path, data or {}, follow, HTTP_ACCEPT="application/x-fragment+json", **extra)
@override_settings(MODULESTORE=TEST_MODULESTORE)
class CourseTestCase(ModuleStoreTestCase):

View File

@@ -1,26 +1,31 @@
"""Views for items (modules)."""
import hashlib
import logging
from uuid import uuid4
from collections import OrderedDict
from functools import partial
from static_replace import replace_static_urls
from xmodule_modifiers import wrap_xblock
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest
from django.http import HttpResponseBadRequest, HttpResponse
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_http_methods
from xblock.fields import Scope
from xblock.fragment import Fragment
from xblock.core import XBlock
import xmodule.x_module
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.modulestore import Location
from xmodule.x_module import prefer_xmodules
from util.json_request import expect_json, JsonResponse
from util.string_utils import str_to_bool
@@ -31,10 +36,10 @@ from ..utils import get_modulestore
from .access import has_course_access
from .helpers import _xmodule_recurse
from preview import handler_prefix, get_preview_html
from edxmako.shortcuts import render_to_response, render_to_string
from contentstore.views.preview import get_preview_fragment
from edxmako.shortcuts import render_to_string
from models.settings.course_grading import CourseGradingModel
from django.utils.translation import ugettext as _
from cms.lib.xblock.runtime import handler_url
__all__ = ['orphan_handler', 'xblock_handler']
@@ -43,6 +48,22 @@ log = logging.getLogger(__name__)
CREATE_IF_NOT_FOUND = ['course_info']
# In order to allow descriptors to use a handler url, we need to
# monkey-patch the x_module library.
# TODO: Remove this code when Runtimes are no longer created by modulestores
xmodule.x_module.descriptor_global_handler_url = handler_url
def hash_resource(resource):
"""
Hash a :class:`xblock.fragment.FragmentResource
"""
md5 = hashlib.md5()
for data in resource:
md5.update(data)
return md5.hexdigest()
# pylint: disable=unused-argument
@require_http_methods(("DELETE", "GET", "PUT", "POST"))
@login_required
@@ -88,7 +109,42 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
old_location = loc_mapper().translate_locator_to_location(locator)
if request.method == 'GET':
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
accept_header = request.META.get('HTTP_ACCEPT', 'application/json')
if 'application/x-fragment+json' in accept_header:
component = modulestore().get_item(old_location)
# Wrap the generated fragment in the xmodule_editor div so that the javascript
# can bind to it correctly
component.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime'))
try:
editor_fragment = component.render('studio_view')
# catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins.
except Exception as exc: # pylint: disable=W0703
log.debug("Unable to render studio_view for %r", component, exc_info=True)
editor_fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
modulestore().save_xmodule(component)
preview_fragment = get_preview_fragment(request, component)
hashed_resources = OrderedDict()
for resource in editor_fragment.resources + preview_fragment.resources:
hashed_resources[hash_resource(resource)] = resource
return JsonResponse({
'html': render_to_string('component.html', {
'preview': preview_fragment.content,
'editor': editor_fragment.content,
'label': component.display_name or component.scope_ids.block_type,
}),
'resources': hashed_resources.items()
})
elif 'application/json' in accept_header:
fields = request.REQUEST.get('fields', '').split(',')
if 'graderType' in fields:
# right now can't combine output of this w/ output of _get_module_info, but worthy goal
@@ -97,25 +153,8 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
rsp = _get_module_info(locator)
return JsonResponse(rsp)
else:
component = modulestore().get_item(old_location)
# Wrap the generated fragment in the xmodule_editor div so that the javascript
# can bind to it correctly
component.runtime.wrappers.append(partial(wrap_xblock, handler_prefix))
return HttpResponse(status=406)
try:
content = component.render('studio_view').content
# catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins.
except Exception as exc: # pylint: disable=W0703
log.debug("Unable to render studio_view for %r", component, exc_info=True)
content = render_to_string('html_error.html', {'message': str(exc)})
return render_to_response('component.html', {
'preview': get_preview_html(request, component),
'editor': content,
'label': component.display_name or component.category,
})
elif request.method == 'DELETE':
delete_children = str_to_bool(request.REQUEST.get('recurse', 'False'))
delete_all_versions = str_to_bool(request.REQUEST.get('all_versions', 'False'))
@@ -281,7 +320,7 @@ def _create_item(request):
data = None
template_id = request.json.get('boilerplate')
if template_id is not None:
clz = XBlock.load_class(category, select=prefer_xmodules)
clz = parent.runtime.load_block_type(category)
if clz is not None:
template = clz.get_template(template_id)
if template is not None:

View File

@@ -1,11 +1,12 @@
import logging
import hashlib
from functools import partial
from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from edxmako.shortcuts import render_to_response, render_to_string
from edxmako.shortcuts import render_to_string
from xmodule_modifiers import replace_static_urls, wrap_xblock
from xmodule.error_module import ErrorDescriptor
@@ -15,6 +16,7 @@ from xmodule.x_module import ModuleSystem
from xblock.runtime import KvsFieldData
from xblock.django.request import webob_to_django_response, django_to_webob_request
from xblock.exceptions import NoSuchHandlerError
from xblock.fragment import Fragment
from lms.lib.xblock.field_data import LmsFieldData
from lms.lib.xblock.runtime import quote_slashes, unquote_slashes
@@ -33,20 +35,6 @@ __all__ = ['preview_handler']
log = logging.getLogger(__name__)
def handler_prefix(block, handler='', suffix=''):
"""
Return a url prefix for XBlock handler_url. The full handler_url
should be '{prefix}/{handler}/{suffix}?{query}'.
Trailing `/`s are removed from the returned url.
"""
return reverse('preview_handler', kwargs={
'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
'handler': handler,
'suffix': suffix,
}).rstrip('/?')
@login_required
def preview_handler(request, usage_id, handler, suffix=''):
"""
@@ -91,7 +79,11 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
An XModule ModuleSystem for use in Studio previews
"""
def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False):
return handler_prefix(block, handler_name, suffix) + '?' + query
return reverse('preview_handler', kwargs={
'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
'handler': handler_name,
'suffix': suffix,
}) + '?' + query
def _preview_module_system(request, descriptor):
@@ -123,7 +115,7 @@ def _preview_module_system(request, descriptor):
# Set up functions to modify the fragment produced by student_view
wrappers=(
# This wrapper wraps the module in the template specified above
partial(wrap_xblock, handler_prefix, display_name_only=descriptor.location.category == 'static_tab'),
partial(wrap_xblock, 'PreviewRuntime', display_name_only=descriptor.location.category == 'static_tab'),
# This wrapper replaces urls in the output that start with /static
# with the correct course-specific url for the static content
@@ -153,15 +145,15 @@ def _load_preview_module(request, descriptor):
return descriptor
def get_preview_html(request, descriptor):
def get_preview_fragment(request, descriptor):
"""
Returns the HTML returned by the XModule's student_view,
specified by the descriptor and idx.
"""
module = _load_preview_module(request, descriptor)
try:
content = module.render("student_view").content
fragment = module.render("student_view")
except Exception as exc: # pylint: disable=W0703
log.debug("Unable to render student_view for %r", module, exc_info=True)
content = render_to_string('html_error.html', {'message': str(exc)})
return content
fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
return fragment