Commit 6d67d0c6 by Calen Pennington

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

Enable XBlock js and css in Studio
parents 7339f568 569c5def
#pylint: disable=E1101 #pylint: disable=E1101
import shutil import json
import mock import mock
import shutil
from textwrap import dedent from textwrap import dedent
...@@ -503,7 +504,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -503,7 +504,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
This verifies that a video caption url is as we expect it to be 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)) 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): def _test_preview(self, location):
""" Preview test case. """ """ Preview test case. """
...@@ -514,7 +517,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -514,7 +517,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
locator = loc_mapper().translate_location( locator = loc_mapper().translate_location(
course_items[0].location.course_id, location, True, True 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) self.assertEqual(resp.status_code, 200)
# TODO: uncomment when preview no longer has locations being returned. # TODO: uncomment when preview no longer has locations being returned.
# _test_no_locations(self, resp) # _test_no_locations(self, resp)
......
...@@ -57,6 +57,13 @@ class AjaxEnabledTestClient(Client): ...@@ -57,6 +57,13 @@ class AjaxEnabledTestClient(Client):
""" """
return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra) 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) @override_settings(MODULESTORE=TEST_MODULESTORE)
class CourseTestCase(ModuleStoreTestCase): class CourseTestCase(ModuleStoreTestCase):
......
"""Views for items (modules).""" """Views for items (modules)."""
import hashlib
import logging import logging
from uuid import uuid4 from uuid import uuid4
from collections import OrderedDict
from functools import partial from functools import partial
from static_replace import replace_static_urls from static_replace import replace_static_urls
from xmodule_modifiers import wrap_xblock from xmodule_modifiers import wrap_xblock
from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required 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 django.views.decorators.http import require_http_methods
from xblock.fields import Scope from xblock.fields import Scope
from xblock.fragment import Fragment
from xblock.core import XBlock from xblock.core import XBlock
import xmodule.x_module
from xmodule.modulestore.django import modulestore, loc_mapper from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.locator import BlockUsageLocator from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.x_module import prefer_xmodules
from util.json_request import expect_json, JsonResponse from util.json_request import expect_json, JsonResponse
from util.string_utils import str_to_bool from util.string_utils import str_to_bool
...@@ -31,10 +36,10 @@ from ..utils import get_modulestore ...@@ -31,10 +36,10 @@ from ..utils import get_modulestore
from .access import has_course_access from .access import has_course_access
from .helpers import _xmodule_recurse from .helpers import _xmodule_recurse
from preview import handler_prefix, get_preview_html from contentstore.views.preview import get_preview_fragment
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_string
from models.settings.course_grading import CourseGradingModel 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'] __all__ = ['orphan_handler', 'xblock_handler']
...@@ -43,6 +48,22 @@ log = logging.getLogger(__name__) ...@@ -43,6 +48,22 @@ log = logging.getLogger(__name__)
CREATE_IF_NOT_FOUND = ['course_info'] 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 # pylint: disable=unused-argument
@require_http_methods(("DELETE", "GET", "PUT", "POST")) @require_http_methods(("DELETE", "GET", "PUT", "POST"))
@login_required @login_required
...@@ -88,34 +109,52 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid ...@@ -88,34 +109,52 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
old_location = loc_mapper().translate_locator_to_location(locator) old_location = loc_mapper().translate_locator_to_location(locator)
if request.method == 'GET': if request.method == 'GET':
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): accept_header = request.META.get('HTTP_ACCEPT', 'application/json')
fields = request.REQUEST.get('fields', '').split(',')
if 'graderType' in fields: if 'application/x-fragment+json' in accept_header:
# right now can't combine output of this w/ output of _get_module_info, but worthy goal
return JsonResponse(CourseGradingModel.get_section_grader_type(locator))
# TODO: pass fields to _get_module_info and only return those
rsp = _get_module_info(locator)
return JsonResponse(rsp)
else:
component = modulestore().get_item(old_location) component = modulestore().get_item(old_location)
# Wrap the generated fragment in the xmodule_editor div so that the javascript # Wrap the generated fragment in the xmodule_editor div so that the javascript
# can bind to it correctly # can bind to it correctly
component.runtime.wrappers.append(partial(wrap_xblock, handler_prefix)) component.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime'))
try: try:
content = component.render('studio_view').content editor_fragment = component.render('studio_view')
# catch exceptions indiscriminately, since after this point they escape the # catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable # dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins. # component-goblins.
except Exception as exc: # pylint: disable=W0703 except Exception as exc: # pylint: disable=W0703
log.debug("Unable to render studio_view for %r", component, exc_info=True) log.debug("Unable to render studio_view for %r", component, exc_info=True)
content = render_to_string('html_error.html', {'message': str(exc)}) editor_fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
modulestore().save_xmodule(component)
preview_fragment = get_preview_fragment(request, component)
return render_to_response('component.html', { hashed_resources = OrderedDict()
'preview': get_preview_html(request, component), for resource in editor_fragment.resources + preview_fragment.resources:
'editor': content, hashed_resources[hash_resource(resource)] = resource
'label': component.display_name or component.category,
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
return JsonResponse(CourseGradingModel.get_section_grader_type(locator))
# TODO: pass fields to _get_module_info and only return those
rsp = _get_module_info(locator)
return JsonResponse(rsp)
else:
return HttpResponse(status=406)
elif request.method == 'DELETE': elif request.method == 'DELETE':
delete_children = str_to_bool(request.REQUEST.get('recurse', 'False')) delete_children = str_to_bool(request.REQUEST.get('recurse', 'False'))
delete_all_versions = str_to_bool(request.REQUEST.get('all_versions', 'False')) delete_all_versions = str_to_bool(request.REQUEST.get('all_versions', 'False'))
...@@ -281,7 +320,7 @@ def _create_item(request): ...@@ -281,7 +320,7 @@ def _create_item(request):
data = None data = None
template_id = request.json.get('boilerplate') template_id = request.json.get('boilerplate')
if template_id is not None: 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: if clz is not None:
template = clz.get_template(template_id) template = clz.get_template(template_id)
if template is not None: if template is not None:
......
import logging import logging
import hashlib
from functools import partial from functools import partial
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponseBadRequest from django.http import Http404, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required 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_modifiers import replace_static_urls, wrap_xblock
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
...@@ -15,6 +16,7 @@ from xmodule.x_module import ModuleSystem ...@@ -15,6 +16,7 @@ from xmodule.x_module import ModuleSystem
from xblock.runtime import KvsFieldData from xblock.runtime import KvsFieldData
from xblock.django.request import webob_to_django_response, django_to_webob_request from xblock.django.request import webob_to_django_response, django_to_webob_request
from xblock.exceptions import NoSuchHandlerError from xblock.exceptions import NoSuchHandlerError
from xblock.fragment import Fragment
from lms.lib.xblock.field_data import LmsFieldData from lms.lib.xblock.field_data import LmsFieldData
from lms.lib.xblock.runtime import quote_slashes, unquote_slashes from lms.lib.xblock.runtime import quote_slashes, unquote_slashes
...@@ -33,20 +35,6 @@ __all__ = ['preview_handler'] ...@@ -33,20 +35,6 @@ __all__ = ['preview_handler']
log = logging.getLogger(__name__) 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 @login_required
def preview_handler(request, usage_id, handler, suffix=''): def preview_handler(request, usage_id, handler, suffix=''):
""" """
...@@ -91,7 +79,11 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method ...@@ -91,7 +79,11 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
An XModule ModuleSystem for use in Studio previews An XModule ModuleSystem for use in Studio previews
""" """
def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False): 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): def _preview_module_system(request, descriptor):
...@@ -123,7 +115,7 @@ 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 # Set up functions to modify the fragment produced by student_view
wrappers=( wrappers=(
# This wrapper wraps the module in the template specified above # 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 # This wrapper replaces urls in the output that start with /static
# with the correct course-specific url for the static content # with the correct course-specific url for the static content
...@@ -153,15 +145,15 @@ def _load_preview_module(request, descriptor): ...@@ -153,15 +145,15 @@ def _load_preview_module(request, descriptor):
return 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, Returns the HTML returned by the XModule's student_view,
specified by the descriptor and idx. specified by the descriptor and idx.
""" """
module = _load_preview_module(request, descriptor) module = _load_preview_module(request, descriptor)
try: try:
content = module.render("student_view").content fragment = module.render("student_view")
except Exception as exc: # pylint: disable=W0703 except Exception as exc: # pylint: disable=W0703
log.debug("Unable to render student_view for %r", module, exc_info=True) log.debug("Unable to render student_view for %r", module, exc_info=True)
content = render_to_string('html_error.html', {'message': str(exc)}) fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
return content return fragment
...@@ -4,7 +4,6 @@ XBlock runtime implementations for edX Studio ...@@ -4,7 +4,6 @@ XBlock runtime implementations for edX Studio
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import xmodule.x_module
from lms.lib.xblock.runtime import quote_slashes from lms.lib.xblock.runtime import quote_slashes
...@@ -17,7 +16,7 @@ def handler_url(block, handler_name, suffix='', query='', thirdparty=False): ...@@ -17,7 +16,7 @@ def handler_url(block, handler_name, suffix='', query='', thirdparty=False):
raise NotImplementedError("edX Studio doesn't support third-party xblock handler urls") raise NotImplementedError("edX Studio doesn't support third-party xblock handler urls")
url = reverse('component_handler', kwargs={ url = reverse('component_handler', kwargs={
'usage_id': quote_slashes(str(block.scope_ids.usage_id)), 'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
'handler': handler_name, 'handler': handler_name,
'suffix': suffix, 'suffix': suffix,
}).rstrip('/') }).rstrip('/')
...@@ -27,4 +26,3 @@ def handler_url(block, handler_name, suffix='', query='', thirdparty=False): ...@@ -27,4 +26,3 @@ def handler_url(block, handler_name, suffix='', query='', thirdparty=False):
return url return url
xmodule.x_module.descriptor_global_handler_url = handler_url
...@@ -28,6 +28,7 @@ requirejs.config({ ...@@ -28,6 +28,7 @@ requirejs.config({
"tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce", "tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce", "jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
"xmodule": "xmodule_js/src/xmodule", "xmodule": "xmodule_js/src/xmodule",
"xblock/cms.runtime.v1": "coffee/src/xblock/cms.runtime.v1",
"xblock": "xmodule_js/common_static/coffee/src/xblock", "xblock": "xmodule_js/common_static/coffee/src/xblock",
"utility": "xmodule_js/common_static/js/src/utility", "utility": "xmodule_js/common_static/js/src/utility",
"accessibility": "xmodule_js/common_static/js/src/accessibility_tools", "accessibility": "xmodule_js/common_static/js/src/accessibility_tools",
......
...@@ -27,6 +27,7 @@ requirejs.config({ ...@@ -27,6 +27,7 @@ requirejs.config({
"tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce", "tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce", "jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
"xmodule": "xmodule_js/src/xmodule", "xmodule": "xmodule_js/src/xmodule",
"xblock/cms.runtime.v1": "coffee/src/xblock/cms.runtime.v1",
"xblock": "xmodule_js/common_static/coffee/src/xblock", "xblock": "xmodule_js/common_static/coffee/src/xblock",
"utility": "xmodule_js/common_static/js/src/utility", "utility": "xmodule_js/common_static/js/src/utility",
"sinon": "xmodule_js/common_static/js/vendor/sinon-1.7.1", "sinon": "xmodule_js/common_static/js/vendor/sinon-1.7.1",
......
define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (ModuleEdit, ModuleModel) -> define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmodule"], ($, ModuleEdit, ModuleModel) ->
describe "ModuleEdit", -> describe "ModuleEdit", ->
beforeEach -> beforeEach ->
...@@ -24,7 +24,7 @@ define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (Mo ...@@ -24,7 +24,7 @@ define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (Mo
</section> </section>
</li> </li>
""" """
spyOn($.fn, 'load').andReturn(@moduleData) spyOn($, 'ajax').andReturn(@moduleData)
@moduleEdit = new ModuleEdit( @moduleEdit = new ModuleEdit(
el: $(".component") el: $(".component")
...@@ -56,14 +56,63 @@ define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (Mo ...@@ -56,14 +56,63 @@ define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (Mo
beforeEach -> beforeEach ->
spyOn(@moduleEdit, 'loadDisplay') spyOn(@moduleEdit, 'loadDisplay')
spyOn(@moduleEdit, 'delegateEvents') spyOn(@moduleEdit, 'delegateEvents')
spyOn($.fn, 'append')
spyOn($, 'getScript')
window.loadedXBlockResources = undefined
@moduleEdit.render() @moduleEdit.render()
$.ajax.mostRecentCall.args[0].success(
html: '<div>Response html</div>'
resources: [
['hash1', {kind: 'text', mimetype: 'text/css', data: 'inline-css'}],
['hash2', {kind: 'url', mimetype: 'text/css', data: 'css-url'}],
['hash3', {kind: 'text', mimetype: 'application/javascript', data: 'inline-js'}],
['hash4', {kind: 'url', mimetype: 'application/javascript', data: 'js-url'}],
['hash5', {placement: 'head', mimetype: 'text/html', data: 'head-html'}],
['hash6', {placement: 'not-head', mimetype: 'text/html', data: 'not-head-html'}],
]
)
it "loads the module preview and editor via ajax on the view element", -> it "loads the module preview and editor via ajax on the view element", ->
expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/xblock/#{@moduleEdit.model.id}", jasmine.any(Function)) expect($.ajax).toHaveBeenCalledWith(
@moduleEdit.$el.load.mostRecentCall.args[1]() url: "/xblock/#{@moduleEdit.model.id}"
type: "GET"
headers:
Accept: 'application/x-fragment+json'
success: jasmine.any(Function)
)
expect(@moduleEdit.loadDisplay).toHaveBeenCalled() expect(@moduleEdit.loadDisplay).toHaveBeenCalled()
expect(@moduleEdit.delegateEvents).toHaveBeenCalled() expect(@moduleEdit.delegateEvents).toHaveBeenCalled()
it "loads inline css from fragments", ->
expect($('head').append).toHaveBeenCalledWith("<style type='text/css'>inline-css</style>")
it "loads css urls from fragments", ->
expect($('head').append).toHaveBeenCalledWith("<link rel='stylesheet' href='css-url' type='text/css'>")
it "loads inline js from fragments", ->
expect($('head').append).toHaveBeenCalledWith("<script>inline-js</script>")
it "loads js urls from fragments", ->
expect($.getScript).toHaveBeenCalledWith("js-url")
it "loads head html", ->
expect($('head').append).toHaveBeenCalledWith("head-html")
it "doesn't load body html", ->
expect($.fn.append).not.toHaveBeenCalledWith('not-head-html')
it "doesn't reload resources", ->
count = $('head').append.callCount
$.ajax.mostRecentCall.args[0].success(
html: '<div>Response html 2</div>'
resources: [
['hash1', {kind: 'text', mimetype: 'text/css', data: 'inline-css'}],
]
)
expect($('head').append.callCount).toBe(count)
describe "loadDisplay", -> describe "loadDisplay", ->
beforeEach -> beforeEach ->
spyOn(XBlock, 'initializeBlock') spyOn(XBlock, 'initializeBlock')
......
define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1", define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
"js/views/feedback_notification", "js/views/metadata", "js/collections/metadata" "js/views/feedback_notification", "js/views/metadata", "js/collections/metadata"
"js/utils/modal", "jquery.inputnumber", "xmodule", "coffee/src/main"], "js/utils/modal", "jquery.inputnumber", "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
(Backbone, $, _, gettext, XBlock, NotificationView, MetadataView, MetadataCollection, ModalUtils) -> (Backbone, $, _, gettext, XBlock, NotificationView, MetadataView, MetadataCollection, ModalUtils) ->
class ModuleEdit extends Backbone.View class ModuleEdit extends Backbone.View
tagName: 'li' tagName: 'li'
...@@ -75,9 +75,37 @@ define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1", ...@@ -75,9 +75,37 @@ define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
render: -> render: ->
if @model.id if @model.id
@$el.load(@model.url(), => $.ajax(
@loadDisplay() url: @model.url()
@delegateEvents() type: 'GET'
headers:
Accept: 'application/x-fragment+json'
success: (data) =>
@$el.html(data.html)
for value in data.resources
do (value) =>
hash = value[0]
if not window.loadedXBlockResources?
window.loadedXBlockResources = []
if hash not in window.loadedXBlockResources
resource = value[1]
switch resource.mimetype
when "text/css"
switch resource.kind
when "text" then $('head').append("<style type='text/css'>#{resource.data}</style>")
when "url" then $('head').append("<link rel='stylesheet' href='#{resource.data}' type='text/css'>")
when "application/javascript"
switch resource.kind
when "text" then $('head').append("<script>#{resource.data}</script>")
when "url" then $.getScript(resource.data)
when "text/html"
switch resource.placement
when "head" then $('head').append(resource.data)
window.loadedXBlockResources.push(hash)
@loadDisplay()
@delegateEvents()
) )
clickSaveButton: (event) => clickSaveButton: (event) =>
......
define ["jquery", "xblock/runtime.v1", "URI"], ($, XBlock, URI) ->
@PreviewRuntime = {}
class PreviewRuntime.v1 extends XBlock.Runtime.v1
handlerUrl: (element, handlerName, suffix, query, thirdparty) ->
uri = URI("/preview/xblock").segment($(@element).data('usage-id'))
.segment('handler')
.segment(handlerName)
if suffix? then uri.segment(suffix)
if query? then uri.search(query)
uri.toString()
@StudioRuntime = {}
class StudioRuntime.v1 extends XBlock.Runtime.v1
handlerUrl: (element, handlerName, suffix, query, thirdparty) ->
uri = URI("/xblock").segment($(@element).data('usage-id'))
.segment('handler')
.segment(handlerName)
if suffix? then uri.segment(suffix)
if query? then uri.search(query)
uri.toString()
...@@ -59,7 +59,8 @@ lib_paths: ...@@ -59,7 +59,8 @@ lib_paths:
- xmodule_js/common_static/js/vendor/URI.min.js - xmodule_js/common_static/js/vendor/URI.min.js
- xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min.js - xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js - xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/coffee/src/xblock - xmodule_js/common_static/coffee/src/xblock/
- xmodule_js/common_static/js/vendor/URI.min.js
# Paths to source JavaScript files # Paths to source JavaScript files
src_paths: src_paths:
......
...@@ -54,6 +54,8 @@ lib_paths: ...@@ -54,6 +54,8 @@ lib_paths:
- xmodule_js/src/xmodule.js - xmodule_js/src/xmodule.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js - xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/js/test/i18n.js - xmodule_js/common_static/js/test/i18n.js
- xmodule_js/common_static/coffee/src/xblock/
- xmodule_js/common_static/js/vendor/URI.min.js
# Paths to source JavaScript files # Paths to source JavaScript files
src_paths: src_paths:
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
.xmodule_VideoModule { .xmodule_VideoModule {
// display mode // display mode
&.xmodule_display { &.xblock-student_view {
// full screen // full screen
.video-controls .add-fullscreen { .video-controls .add-fullscreen {
......
...@@ -183,16 +183,16 @@ ...@@ -183,16 +183,16 @@
border-left: 1px solid $mediumGrey; border-left: 1px solid $mediumGrey;
border-right: 1px solid $mediumGrey; border-right: 1px solid $mediumGrey;
.xmodule_display { .xblock-student_view {
display: none; display: none;
} }
} }
.new .xmodule_display { .new .xblock-student_view {
background: $yellow; background: $yellow;
} }
.xmodule_display { .xblock-student_view {
@include transition(background-color $tmg-s3 linear 0s); @include transition(background-color $tmg-s3 linear 0s);
padding: 20px 20px 22px; padding: 20px 20px 22px;
font-size: 24px; font-size: 24px;
......
...@@ -420,7 +420,7 @@ body.course.unit,.view-unit { ...@@ -420,7 +420,7 @@ body.course.unit,.view-unit {
} }
} }
.xmodule_display { .xblock-student_view {
padding: 2*$baseline $baseline $baseline; padding: 2*$baseline $baseline $baseline;
overflow-x: auto; overflow-x: auto;
......
...@@ -15,6 +15,7 @@ from xblock.fragment import Fragment ...@@ -15,6 +15,7 @@ from xblock.fragment import Fragment
from xmodule.seq_module import SequenceModule from xmodule.seq_module import SequenceModule
from xmodule.vertical_module import VerticalModule from xmodule.vertical_module import VerticalModule
from xmodule.x_module import shim_xmodule_js, XModuleDescriptor, XModule from xmodule.x_module import shim_xmodule_js, XModuleDescriptor, XModule
from lms.lib.xblock.runtime import quote_slashes
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -29,26 +30,28 @@ def wrap_fragment(fragment, new_content): ...@@ -29,26 +30,28 @@ def wrap_fragment(fragment, new_content):
return wrapper_frag return wrapper_frag
def wrap_xblock(handler_prefix, block, view, frag, context, display_name_only=False): # pylint: disable=unused-argument def wrap_xblock(runtime_class, block, view, frag, context, display_name_only=False, extra_data=None): # pylint: disable=unused-argument
""" """
Wraps the results of rendering an XBlock view in a standard <section> with identifying Wraps the results of rendering an XBlock view in a standard <section> with identifying
data so that the appropriate javascript module can be loaded onto it. data so that the appropriate javascript module can be loaded onto it.
:param handler_prefix: A function that takes a block and returns the url prefix for :param runtime_class: The name of the javascript runtime class to use to load this block
the javascript handler_url. This prefix should be able to have {handler_name}/{suffix}?{query}
appended to it to return a valid handler_url
:param block: An XBlock (that may be an XModule or XModuleDescriptor) :param block: An XBlock (that may be an XModule or XModuleDescriptor)
:param view: The name of the view that rendered the fragment being wrapped :param view: The name of the view that rendered the fragment being wrapped
:param frag: The :class:`Fragment` to be wrapped :param frag: The :class:`Fragment` to be wrapped
:param context: The context passed to the view being rendered :param context: The context passed to the view being rendered
:param display_name_only: If true, don't render the fragment content at all. :param display_name_only: If true, don't render the fragment content at all.
Instead, just render the `display_name` of `block` Instead, just render the `display_name` of `block`
:param extra_data: A dictionary with extra data values to be set on the wrapper
""" """
if extra_data is None:
extra_data = {}
# If any mixins have been applied, then use the unmixed class # If any mixins have been applied, then use the unmixed class
class_name = getattr(block, 'unmixed_class', block.__class__).__name__ class_name = getattr(block, 'unmixed_class', block.__class__).__name__
data = {} data = {}
data.update(extra_data)
css_classes = ['xblock', 'xblock-' + view] css_classes = ['xblock', 'xblock-' + view]
if isinstance(block, (XModule, XModuleDescriptor)): if isinstance(block, (XModule, XModuleDescriptor)):
...@@ -65,14 +68,15 @@ def wrap_xblock(handler_prefix, block, view, frag, context, display_name_only=Fa ...@@ -65,14 +68,15 @@ def wrap_xblock(handler_prefix, block, view, frag, context, display_name_only=Fa
if frag.js_init_fn: if frag.js_init_fn:
data['init'] = frag.js_init_fn data['init'] = frag.js_init_fn
data['runtime-class'] = runtime_class
data['runtime-version'] = frag.js_init_version data['runtime-version'] = frag.js_init_version
data['handler-prefix'] = handler_prefix(block)
data['block-type'] = block.scope_ids.block_type data['block-type'] = block.scope_ids.block_type
data['usage-id'] = quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8'))
template_context = { template_context = {
'content': block.display_name if display_name_only else frag.content, 'content': block.display_name if display_name_only else frag.content,
'classes': css_classes, 'classes': css_classes,
'data_attributes': ' '.join('data-{}="{}"'.format(key, value) for key, value in data.items()), 'data_attributes': ' '.join(u'data-{}="{}"'.format(key, value) for key, value in data.items()),
} }
return wrap_fragment(frag, render_to_string('xblock_wrapper.html', template_context)) return wrap_fragment(frag, render_to_string('xblock_wrapper.html', template_context))
......
...@@ -2,9 +2,9 @@ describe "XBlock", -> ...@@ -2,9 +2,9 @@ describe "XBlock", ->
beforeEach -> beforeEach ->
setFixtures """ setFixtures """
<div> <div>
<div class='xblock' id='vA' data-runtime-version="A" data-init="initFnA" data-name="a-name"/> <div class='xblock' id='vA' data-runtime-version="A" data-runtime-class="TestRuntime" data-init="initFnA" data-name="a-name"/>
<div> <div>
<div class='xblock' id='vZ' data-runtime-version="Z" data-init="initFnZ"/> <div class='xblock' id='vZ' data-runtime-version="Z" data-runtime-class="TestRuntime" data-init="initFnZ"/>
</div> </div>
<div class='xblock' id='missing-version' data-init='initFnA' data-name='no-version'/> <div class='xblock' id='missing-version' data-init='initFnA' data-name='no-version'/>
<div class='xblock' id='missing-init' data-runtime-version="A" data-name='no-init'/> <div class='xblock' id='missing-init' data-runtime-version="A" data-name='no-init'/>
...@@ -13,8 +13,11 @@ describe "XBlock", -> ...@@ -13,8 +13,11 @@ describe "XBlock", ->
describe "initializeBlock", -> describe "initializeBlock", ->
beforeEach -> beforeEach ->
XBlock.runtime.vA = jasmine.createSpy().andReturn('runtimeA') window.TestRuntime = {}
XBlock.runtime.vZ = jasmine.createSpy().andReturn('runtimeZ') @runtimeA = {name: 'runtimeA'}
@runtimeZ = {name: 'runtimeZ'}
TestRuntime.vA = jasmine.createSpy().andReturn(@runtimeA)
TestRuntime.vZ = jasmine.createSpy().andReturn(@runtimeZ)
window.initFnA = jasmine.createSpy() window.initFnA = jasmine.createSpy()
window.initFnZ = jasmine.createSpy() window.initFnZ = jasmine.createSpy()
...@@ -28,12 +31,12 @@ describe "XBlock", -> ...@@ -28,12 +31,12 @@ describe "XBlock", ->
@missingInitBlock = XBlock.initializeBlock($('#missing-init')[0]) @missingInitBlock = XBlock.initializeBlock($('#missing-init')[0])
it "loads the right runtime version", -> it "loads the right runtime version", ->
expect(XBlock.runtime.vA).toHaveBeenCalledWith($('#vA')[0], @fakeChildren) expect(TestRuntime.vA).toHaveBeenCalledWith($('#vA')[0], @fakeChildren)
expect(XBlock.runtime.vZ).toHaveBeenCalledWith($('#vZ')[0], @fakeChildren) expect(TestRuntime.vZ).toHaveBeenCalledWith($('#vZ')[0], @fakeChildren)
it "loads the right init function", -> it "loads the right init function", ->
expect(window.initFnA).toHaveBeenCalledWith('runtimeA', $('#vA')[0]) expect(window.initFnA).toHaveBeenCalledWith(@runtimeA, $('#vA')[0])
expect(window.initFnZ).toHaveBeenCalledWith('runtimeZ', $('#vZ')[0]) expect(window.initFnZ).toHaveBeenCalledWith(@runtimeZ, $('#vZ')[0])
it "loads when missing versions", -> it "loads when missing versions", ->
expect(@missingVersionBlock.element).toBe($('#missing-version')) expect(@missingVersionBlock.element).toBe($('#missing-version'))
......
describe "XBlock.runtime.v1", -> describe "XBlock.Runtime.v1", ->
beforeEach -> beforeEach ->
setFixtures """ setFixtures """
<div class='xblock' data-handler-prefix='/xblock/fake-usage-id/handler'/> <div class='xblock' data-handler-prefix='/xblock/fake-usage-id/handler'/>
...@@ -10,9 +10,7 @@ describe "XBlock.runtime.v1", -> ...@@ -10,9 +10,7 @@ describe "XBlock.runtime.v1", ->
@element = $('.xblock')[0] @element = $('.xblock')[0]
@runtime = XBlock.runtime.v1(@element, @children) @runtime = new XBlock.Runtime.v1(@element, @children)
it "provides a handler url", ->
expect(@runtime.handlerUrl(@element, 'foo')).toBe('/xblock/fake-usage-id/handler/foo')
it "provides a list of children", -> it "provides a list of children", ->
expect(@runtime.children).toBe(@children) expect(@runtime.children).toBe(@children)
......
@XBlock = @XBlock =
runtime: {} Runtime: {}
initializeBlock: (element) -> initializeBlock: (element) ->
$element = $(element) $element = $(element)
children = @initializeBlocks($element) children = @initializeBlocks($element)
runtime = $element.data("runtime-class")
version = $element.data("runtime-version") version = $element.data("runtime-version")
initFnName = $element.data("init") initFnName = $element.data("init")
if version? and initFnName? if runtime? and version? and initFnName?
runtime = @runtime["v#{version}"](element, children) runtime = new window[runtime]["v#{version}"](element, children)
initFn = window[initFnName] initFn = window[initFnName]
block = initFn(runtime, element) ? {} block = initFn(runtime, element) ? {}
else else
elementTag = $('<div>').append($element.clone()).html(); elementTag = $('<div>').append($element.clone()).html();
console.log("Block #{elementTag} is missing data-runtime-version or data-init, and can't be initialized") console.log("Block #{elementTag} is missing data-runtime, data-runtime-version or data-init, and can't be initialized")
block = {} block = {}
block.element = element block.element = element
......
@XBlock.runtime.v1 = (element, children) -> class XBlock.Runtime.v1
childMap = {} constructor: (@element, @children) ->
$.each children, (idx, child) -> @childMap = {}
childMap[child.name] = child $.each @children, (idx, child) =>
@childMap[child.name] = child
return {
# Generate the handler url for the specified handler.
#
# element is the html element containing the xblock requesting the url
# handlerName is the name of the handler
# suffix is the optional url suffix to include in the handler url
# query is an optional query-string (note, this should not include a preceding ? or &)
handlerUrl: (element, handlerName, suffix, query) ->
handlerPrefix = $(element).data("handler-prefix")
suffix = if suffix? then "/#{suffix}" else ''
query = if query? then "?#{query}" else ''
"#{handlerPrefix}/#{handlerName}#{suffix}#{query}"
# A list of xblock children of this element
children: children
# A map of name -> child for the xblock children of this element
childMap: childMap
}
...@@ -21,7 +21,7 @@ from courseware.access import has_access, get_user_role ...@@ -21,7 +21,7 @@ from courseware.access import has_access, get_user_role
from courseware.masquerade import setup_masquerade from courseware.masquerade import setup_masquerade
from courseware.model_data import FieldDataCache, DjangoKeyValueStore from courseware.model_data import FieldDataCache, DjangoKeyValueStore
from lms.lib.xblock.field_data import LmsFieldData from lms.lib.xblock.field_data import LmsFieldData
from lms.lib.xblock.runtime import LmsModuleSystem, handler_prefix, unquote_slashes from lms.lib.xblock.runtime import LmsModuleSystem, unquote_slashes
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
from student.models import anonymous_id_for_user, user_by_anonymous_id from student.models import anonymous_id_for_user, user_by_anonymous_id
...@@ -339,7 +339,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours ...@@ -339,7 +339,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
# Wrap the output display in a single div to allow for the XModule # Wrap the output display in a single div to allow for the XModule
# javascript to be bound correctly # javascript to be bound correctly
if wrap_xmodule_display is True: if wrap_xmodule_display is True:
block_wrappers.append(partial(wrap_xblock, partial(handler_prefix, course_id))) block_wrappers.append(partial(wrap_xblock, 'LmsRuntime', extra_data={'course-id': course_id}))
# TODO (cpennington): When modules are shared between courses, the static # TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory # prefix is going to have to be specific to the module, not the directory
...@@ -379,7 +379,8 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours ...@@ -379,7 +379,8 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours
# As we have the time to manually test more modules, we can add to the list # As we have the time to manually test more modules, we can add to the list
# of modules that get the per-course anonymized id. # of modules that get the per-course anonymized id.
is_pure_xblock = isinstance(descriptor, XBlock) and not isinstance(descriptor, XModuleDescriptor) is_pure_xblock = isinstance(descriptor, XBlock) and not isinstance(descriptor, XModuleDescriptor)
is_lti_module = not is_pure_xblock and issubclass(descriptor.module_class, LTIModule) module_class = getattr(descriptor, 'module_class', None)
is_lti_module = not is_pure_xblock and issubclass(module_class, LTIModule)
if is_pure_xblock or is_lti_module: if is_pure_xblock or is_lti_module:
anonymous_student_id = anonymous_id_for_user(user, course_id) anonymous_student_id = anonymous_id_for_user(user, course_id)
else: else:
......
...@@ -24,7 +24,6 @@ from django_comment_client.utils import has_forum_access ...@@ -24,7 +24,6 @@ from django_comment_client.utils import has_forum_access
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from student.models import CourseEnrollment from student.models import CourseEnrollment
from bulk_email.models import CourseAuthorization from bulk_email.models import CourseAuthorization
from lms.lib.xblock.runtime import handler_prefix
from .tools import get_units_with_due_date, title_or_url from .tools import get_units_with_due_date, title_or_url
...@@ -206,7 +205,7 @@ def _section_send_email(course_id, access, course): ...@@ -206,7 +205,7 @@ def _section_send_email(course_id, access, course):
ScopeIds(None, None, None, 'i4x://dummy_org/dummy_course/html/dummy_name') ScopeIds(None, None, None, 'i4x://dummy_org/dummy_course/html/dummy_name')
) )
fragment = course.system.render(html_module, 'studio_view') fragment = course.system.render(html_module, 'studio_view')
fragment = wrap_xblock(partial(handler_prefix, course_id), html_module, 'studio_view', fragment, None) fragment = wrap_xblock('LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": course_id})
email_editor = fragment.content email_editor = fragment.content
section_data = { section_data = {
'section_key': 'send_email', 'section_key': 'send_email',
......
...@@ -61,7 +61,6 @@ import track.views ...@@ -61,7 +61,6 @@ import track.views
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from django.utils.translation import ugettext as _u from django.utils.translation import ugettext as _u
from lms.lib.xblock.runtime import handler_prefix
from microsite_configuration.middleware import MicrositeConfiguration from microsite_configuration.middleware import MicrositeConfiguration
...@@ -848,7 +847,7 @@ def instructor_dashboard(request, course_id): ...@@ -848,7 +847,7 @@ def instructor_dashboard(request, course_id):
ScopeIds(None, None, None, 'i4x://dummy_org/dummy_course/html/dummy_name') ScopeIds(None, None, None, 'i4x://dummy_org/dummy_course/html/dummy_name')
) )
fragment = html_module.render('studio_view') fragment = html_module.render('studio_view')
fragment = wrap_xblock(partial(handler_prefix, course_id), html_module, 'studio_view', fragment, None) fragment = wrap_xblock('LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": course_id})
email_editor = fragment.content email_editor = fragment.content
# Enable instructor email only if the following conditions are met: # Enable instructor email only if the following conditions are met:
......
...@@ -750,6 +750,7 @@ main_vendor_js = [ ...@@ -750,6 +750,7 @@ main_vendor_js = [
'js/vendor/ova/ova.js', 'js/vendor/ova/ova.js',
'js/vendor/ova/catch/js/catch.js', 'js/vendor/ova/catch/js/catch.js',
'js/vendor/ova/catch/js/handlebars-1.1.2.js' 'js/vendor/ova/catch/js/handlebars-1.1.2.js'
'js/vendor/URI.min.js'
] ]
discussion_js = sorted(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.js')) discussion_js = sorted(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.js'))
...@@ -815,17 +816,18 @@ PIPELINE_CSS = { ...@@ -815,17 +816,18 @@ PIPELINE_CSS = {
} }
common_js = set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.js')) - set(courseware_js + discussion_js + staff_grading_js + open_ended_js + notes_js + instructor_dash_js)
project_js = set(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.js')) - set(courseware_js + discussion_js + staff_grading_js + open_ended_js + notes_js + instructor_dash_js)
# test_order: Determines the position of this chunk of javascript on # test_order: Determines the position of this chunk of javascript on
# the jasmine test page # the jasmine test page
PIPELINE_JS = { PIPELINE_JS = {
'application': { 'application': {
# Application will contain all paths not in courseware_only_js # Application will contain all paths not in courseware_only_js
'source_filenames': sorted( 'source_filenames': sorted(common_js) + sorted(project_js) + [
set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.js') +
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.js')) -
set(courseware_js + discussion_js + staff_grading_js + open_ended_js + notes_js + instructor_dash_js)
) + [
'js/form.ext.js', 'js/form.ext.js',
'js/my_courses_dropdown.js', 'js/my_courses_dropdown.js',
'js/toggle_login_modal.js', 'js/toggle_login_modal.js',
......
...@@ -58,63 +58,6 @@ def unquote_slashes(text): ...@@ -58,63 +58,6 @@ def unquote_slashes(text):
return re.sub(r'(;;|;_)', _unquote_slashes, text) return re.sub(r'(;;|;_)', _unquote_slashes, text)
def handler_url(course_id, block, handler, suffix='', query='', thirdparty=False):
"""
Return an XBlock handler url for the specified course, block and handler.
If handler is an empty string, this function is being used to create a
prefix of the general URL, which is assumed to be followed by handler name
and suffix.
If handler is specified, then it is checked for being a valid handler
function, and ValueError is raised if not.
"""
view_name = 'xblock_handler'
if handler:
# Be sure this is really a handler.
func = getattr(block, handler, None)
if not func:
raise ValueError("{!r} is not a function name".format(handler))
if not getattr(func, "_is_xblock_handler", False):
raise ValueError("{!r} is not a handler name".format(handler))
if thirdparty:
view_name = 'xblock_handler_noauth'
url = reverse(view_name, kwargs={
'course_id': course_id,
'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
'handler': handler,
'suffix': suffix,
})
# If suffix is an empty string, remove the trailing '/'
if not suffix:
url = url.rstrip('/')
# If there is a query string, append it
if query:
url += '?' + query
return url
def handler_prefix(course_id, block):
"""
Returns a prefix for use by the Javascript handler_url function.
The prefix is a valid handler url after the handler name is slash-appended
to it.
"""
# This depends on handler url having the handler_name as the final piece of the url
# so that leaving an empty handler_name really does leave the opportunity to append
# the handler_name on the frontend
# This is relied on by the xblock/runtime.v1.coffee frontend handlerUrl function
return handler_url(course_id, block, '').rstrip('/?')
class LmsHandlerUrls(object): class LmsHandlerUrls(object):
""" """
A runtime mixin that provides a handler_url function that routes A runtime mixin that provides a handler_url function that routes
...@@ -127,7 +70,34 @@ class LmsHandlerUrls(object): ...@@ -127,7 +70,34 @@ class LmsHandlerUrls(object):
# pylint: disable=no-member # pylint: disable=no-member
def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False): def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False):
"""See :method:`xblock.runtime:Runtime.handler_url`""" """See :method:`xblock.runtime:Runtime.handler_url`"""
return handler_url(self.course_id, block, handler_name, suffix='', query='', thirdparty=thirdparty) view_name = 'xblock_handler'
if handler_name:
# Be sure this is really a handler.
func = getattr(block, handler_name, None)
if not func:
raise ValueError("{!r} is not a function name".format(handler_name))
if not getattr(func, "_is_xblock_handler", False):
raise ValueError("{!r} is not a handler name".format(handler_name))
if thirdparty:
view_name = 'xblock_handler_noauth'
url = reverse(view_name, kwargs={
'course_id': self.course_id,
'usage_id': quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8')),
'handler': handler_name,
'suffix': suffix,
})
# If suffix is an empty string, remove the trailing '/'
if not suffix:
url = url.rstrip('/')
# If there is a query string, append it
if query:
url += '?' + query
return url
class LmsModuleSystem(LmsHandlerUrls, ModuleSystem): # pylint: disable=abstract-method class LmsModuleSystem(LmsHandlerUrls, ModuleSystem): # pylint: disable=abstract-method
......
...@@ -6,7 +6,7 @@ from ddt import ddt, data ...@@ -6,7 +6,7 @@ from ddt import ddt, data
from mock import Mock from mock import Mock
from unittest import TestCase from unittest import TestCase
from urlparse import urlparse from urlparse import urlparse
from lms.lib.xblock.runtime import quote_slashes, unquote_slashes, handler_url from lms.lib.xblock.runtime import quote_slashes, unquote_slashes, LmsModuleSystem
TEST_STRINGS = [ TEST_STRINGS = [
'', '',
...@@ -41,23 +41,31 @@ class TestHandlerUrl(TestCase): ...@@ -41,23 +41,31 @@ class TestHandlerUrl(TestCase):
def setUp(self): def setUp(self):
self.block = Mock() self.block = Mock()
self.course_id = "org/course/run" self.course_id = "org/course/run"
self.runtime = LmsModuleSystem(
static_url='/static',
track_function=Mock(),
get_module=Mock(),
render_template=Mock(),
replace_urls=str,
course_id=self.course_id,
)
def test_trailing_characters(self): def test_trailing_characters(self):
self.assertFalse(handler_url(self.course_id, self.block, 'handler').endswith('?')) self.assertFalse(self.runtime.handler_url(self.block, 'handler').endswith('?'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler').endswith('/')) self.assertFalse(self.runtime.handler_url(self.block, 'handler').endswith('/'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', 'suffix').endswith('?')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', 'suffix').endswith('?'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', 'suffix').endswith('/')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', 'suffix').endswith('/'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', 'suffix', 'query').endswith('?')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', 'suffix', 'query').endswith('?'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', 'suffix', 'query').endswith('/')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', 'suffix', 'query').endswith('/'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', query='query').endswith('?')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', query='query').endswith('?'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', query='query').endswith('/')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', query='query').endswith('/'))
def _parsed_query(self, query_string): def _parsed_query(self, query_string):
"""Return the parsed query string from a handler_url generated with the supplied query_string""" """Return the parsed query string from a handler_url generated with the supplied query_string"""
return urlparse(handler_url(self.course_id, self.block, 'handler', query=query_string)).query return urlparse(self.runtime.handler_url(self.block, 'handler', query=query_string)).query
def test_query_string(self): def test_query_string(self):
self.assertIn('foo=bar', self._parsed_query('foo=bar')) self.assertIn('foo=bar', self._parsed_query('foo=bar'))
...@@ -66,7 +74,7 @@ class TestHandlerUrl(TestCase): ...@@ -66,7 +74,7 @@ class TestHandlerUrl(TestCase):
def _parsed_path(self, handler_name='handler', suffix=''): def _parsed_path(self, handler_name='handler', suffix=''):
"""Return the parsed path from a handler_url with the supplied handler_name and suffix""" """Return the parsed path from a handler_url with the supplied handler_name and suffix"""
return urlparse(handler_url(self.course_id, self.block, handler_name, suffix=suffix)).path return urlparse(self.runtime.handler_url(self.block, handler_name, suffix=suffix)).path
def test_suffix(self): def test_suffix(self):
self.assertTrue(self._parsed_path(suffix="foo").endswith('foo')) self.assertTrue(self._parsed_path(suffix="foo").endswith('foo'))
......
@LmsRuntime = {}
class LmsRuntime.v1 extends XBlock.Runtime.v1
handlerUrl: (element, handlerName, suffix, query, thirdparty) ->
courseId = $(@element).data("course-id")
usageId = $(@element).data("usage-id")
handlerAuth = if thirdparty then "handler_noauth" else "handler"
uri = URI('/courses').segment(courseId)
.segment('xblock')
.segment(usageId)
.segment(handlerAuth)
.segment(handlerName)
if suffix? then uri.segment(suffix)
if query? then uri.search(query)
uri.toString()
...@@ -40,6 +40,7 @@ lib_paths: ...@@ -40,6 +40,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/jquery.cookie.js - xmodule_js/common_static/js/vendor/jquery.cookie.js
- xmodule_js/common_static/js/vendor/flot/jquery.flot.js - xmodule_js/common_static/js/vendor/flot/jquery.flot.js
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js - xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/common_static/js/vendor/URI.min.js
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js - xmodule_js/common_static/coffee/src/jquery.immediateDescendents.js
- xmodule_js/common_static/coffee/src/xblock - xmodule_js/common_static/coffee/src/xblock
- xmodule_js/src/capa/ - xmodule_js/src/capa/
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment