Merge pull request #1949 from cpennington/xblock-studio-js-and-css
Enable XBlock js and css in Studio
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,6 @@ XBlock runtime implementations for edX Studio
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
import xmodule.x_module
|
||||
from lms.lib.xblock.runtime import quote_slashes
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
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,
|
||||
'suffix': suffix,
|
||||
}).rstrip('/')
|
||||
@@ -27,4 +26,3 @@ def handler_url(block, handler_name, suffix='', query='', thirdparty=False):
|
||||
|
||||
return url
|
||||
|
||||
xmodule.x_module.descriptor_global_handler_url = handler_url
|
||||
|
||||
@@ -28,6 +28,7 @@ requirejs.config({
|
||||
"tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
|
||||
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
|
||||
"xmodule": "xmodule_js/src/xmodule",
|
||||
"xblock/cms.runtime.v1": "coffee/src/xblock/cms.runtime.v1",
|
||||
"xblock": "xmodule_js/common_static/coffee/src/xblock",
|
||||
"utility": "xmodule_js/common_static/js/src/utility",
|
||||
"accessibility": "xmodule_js/common_static/js/src/accessibility_tools",
|
||||
|
||||
@@ -27,6 +27,7 @@ requirejs.config({
|
||||
"tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/tiny_mce",
|
||||
"jquery.tinymce": "xmodule_js/common_static/js/vendor/tiny_mce/jquery.tinymce",
|
||||
"xmodule": "xmodule_js/src/xmodule",
|
||||
"xblock/cms.runtime.v1": "coffee/src/xblock/cms.runtime.v1",
|
||||
"xblock": "xmodule_js/common_static/coffee/src/xblock",
|
||||
"utility": "xmodule_js/common_static/js/src/utility",
|
||||
"sinon": "xmodule_js/common_static/js/vendor/sinon-1.7.1",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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", ->
|
||||
beforeEach ->
|
||||
@@ -24,7 +24,7 @@ define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (Mo
|
||||
</section>
|
||||
</li>
|
||||
"""
|
||||
spyOn($.fn, 'load').andReturn(@moduleData)
|
||||
spyOn($, 'ajax').andReturn(@moduleData)
|
||||
|
||||
@moduleEdit = new ModuleEdit(
|
||||
el: $(".component")
|
||||
@@ -56,14 +56,63 @@ define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (Mo
|
||||
beforeEach ->
|
||||
spyOn(@moduleEdit, 'loadDisplay')
|
||||
spyOn(@moduleEdit, 'delegateEvents')
|
||||
spyOn($.fn, 'append')
|
||||
spyOn($, 'getScript')
|
||||
|
||||
window.loadedXBlockResources = undefined
|
||||
|
||||
@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", ->
|
||||
expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/xblock/#{@moduleEdit.model.id}", jasmine.any(Function))
|
||||
@moduleEdit.$el.load.mostRecentCall.args[1]()
|
||||
expect($.ajax).toHaveBeenCalledWith(
|
||||
url: "/xblock/#{@moduleEdit.model.id}"
|
||||
type: "GET"
|
||||
headers:
|
||||
Accept: 'application/x-fragment+json'
|
||||
success: jasmine.any(Function)
|
||||
)
|
||||
expect(@moduleEdit.loadDisplay).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", ->
|
||||
beforeEach ->
|
||||
spyOn(XBlock, 'initializeBlock')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
|
||||
"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) ->
|
||||
class ModuleEdit extends Backbone.View
|
||||
tagName: 'li'
|
||||
@@ -75,9 +75,37 @@ define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
|
||||
|
||||
render: ->
|
||||
if @model.id
|
||||
@$el.load(@model.url(), =>
|
||||
@loadDisplay()
|
||||
@delegateEvents()
|
||||
$.ajax(
|
||||
url: @model.url()
|
||||
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) =>
|
||||
|
||||
22
cms/static/coffee/src/xblock/cms.runtime.v1.coffee
Normal file
22
cms/static/coffee/src/xblock/cms.runtime.v1.coffee
Normal file
@@ -0,0 +1,22 @@
|
||||
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:
|
||||
- xmodule_js/common_static/js/vendor/URI.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/xblock
|
||||
- xmodule_js/common_static/coffee/src/xblock/
|
||||
- xmodule_js/common_static/js/vendor/URI.min.js
|
||||
|
||||
# Paths to source JavaScript files
|
||||
src_paths:
|
||||
|
||||
@@ -54,6 +54,8 @@ lib_paths:
|
||||
- xmodule_js/src/xmodule.js
|
||||
- xmodule_js/common_static/coffee/src/jquery.immediateDescendents.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
|
||||
src_paths:
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
.xmodule_VideoModule {
|
||||
|
||||
// display mode
|
||||
&.xmodule_display {
|
||||
&.xblock-student_view {
|
||||
|
||||
// full screen
|
||||
.video-controls .add-fullscreen {
|
||||
|
||||
@@ -183,16 +183,16 @@
|
||||
border-left: 1px solid $mediumGrey;
|
||||
border-right: 1px solid $mediumGrey;
|
||||
|
||||
.xmodule_display {
|
||||
.xblock-student_view {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.new .xmodule_display {
|
||||
.new .xblock-student_view {
|
||||
background: $yellow;
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
.xblock-student_view {
|
||||
@include transition(background-color $tmg-s3 linear 0s);
|
||||
padding: 20px 20px 22px;
|
||||
font-size: 24px;
|
||||
|
||||
@@ -420,7 +420,7 @@ body.course.unit,.view-unit {
|
||||
}
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
.xblock-student_view {
|
||||
padding: 2*$baseline $baseline $baseline;
|
||||
overflow-x: auto;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from xblock.fragment import Fragment
|
||||
from xmodule.seq_module import SequenceModule
|
||||
from xmodule.vertical_module import VerticalModule
|
||||
from xmodule.x_module import shim_xmodule_js, XModuleDescriptor, XModule
|
||||
from lms.lib.xblock.runtime import quote_slashes
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,26 +30,28 @@ def wrap_fragment(fragment, new_content):
|
||||
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
|
||||
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
|
||||
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 runtime_class: The name of the javascript runtime class to use to load this block
|
||||
: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 frag: The :class:`Fragment` to be wrapped
|
||||
:param context: The context passed to the view being rendered
|
||||
:param display_name_only: If true, don't render the fragment content at all.
|
||||
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
|
||||
class_name = getattr(block, 'unmixed_class', block.__class__).__name__
|
||||
|
||||
data = {}
|
||||
data.update(extra_data)
|
||||
css_classes = ['xblock', 'xblock-' + view]
|
||||
|
||||
if isinstance(block, (XModule, XModuleDescriptor)):
|
||||
@@ -65,14 +68,15 @@ def wrap_xblock(handler_prefix, block, view, frag, context, display_name_only=Fa
|
||||
|
||||
if frag.js_init_fn:
|
||||
data['init'] = frag.js_init_fn
|
||||
data['runtime-class'] = runtime_class
|
||||
data['runtime-version'] = frag.js_init_version
|
||||
data['handler-prefix'] = handler_prefix(block)
|
||||
data['block-type'] = block.scope_ids.block_type
|
||||
data['usage-id'] = quote_slashes(unicode(block.scope_ids.usage_id).encode('utf-8'))
|
||||
|
||||
template_context = {
|
||||
'content': block.display_name if display_name_only else frag.content,
|
||||
'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))
|
||||
|
||||
@@ -2,9 +2,9 @@ describe "XBlock", ->
|
||||
beforeEach ->
|
||||
setFixtures """
|
||||
<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 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 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'/>
|
||||
@@ -13,8 +13,11 @@ describe "XBlock", ->
|
||||
|
||||
describe "initializeBlock", ->
|
||||
beforeEach ->
|
||||
XBlock.runtime.vA = jasmine.createSpy().andReturn('runtimeA')
|
||||
XBlock.runtime.vZ = jasmine.createSpy().andReturn('runtimeZ')
|
||||
window.TestRuntime = {}
|
||||
@runtimeA = {name: 'runtimeA'}
|
||||
@runtimeZ = {name: 'runtimeZ'}
|
||||
TestRuntime.vA = jasmine.createSpy().andReturn(@runtimeA)
|
||||
TestRuntime.vZ = jasmine.createSpy().andReturn(@runtimeZ)
|
||||
|
||||
window.initFnA = jasmine.createSpy()
|
||||
window.initFnZ = jasmine.createSpy()
|
||||
@@ -28,12 +31,12 @@ describe "XBlock", ->
|
||||
@missingInitBlock = XBlock.initializeBlock($('#missing-init')[0])
|
||||
|
||||
it "loads the right runtime version", ->
|
||||
expect(XBlock.runtime.vA).toHaveBeenCalledWith($('#vA')[0], @fakeChildren)
|
||||
expect(XBlock.runtime.vZ).toHaveBeenCalledWith($('#vZ')[0], @fakeChildren)
|
||||
expect(TestRuntime.vA).toHaveBeenCalledWith($('#vA')[0], @fakeChildren)
|
||||
expect(TestRuntime.vZ).toHaveBeenCalledWith($('#vZ')[0], @fakeChildren)
|
||||
|
||||
it "loads the right init function", ->
|
||||
expect(window.initFnA).toHaveBeenCalledWith('runtimeA', $('#vA')[0])
|
||||
expect(window.initFnZ).toHaveBeenCalledWith('runtimeZ', $('#vZ')[0])
|
||||
expect(window.initFnA).toHaveBeenCalledWith(@runtimeA, $('#vA')[0])
|
||||
expect(window.initFnZ).toHaveBeenCalledWith(@runtimeZ, $('#vZ')[0])
|
||||
|
||||
it "loads when missing versions", ->
|
||||
expect(@missingVersionBlock.element).toBe($('#missing-version'))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
describe "XBlock.runtime.v1", ->
|
||||
describe "XBlock.Runtime.v1", ->
|
||||
beforeEach ->
|
||||
setFixtures """
|
||||
<div class='xblock' data-handler-prefix='/xblock/fake-usage-id/handler'/>
|
||||
@@ -10,9 +10,7 @@ describe "XBlock.runtime.v1", ->
|
||||
|
||||
@element = $('.xblock')[0]
|
||||
|
||||
@runtime = XBlock.runtime.v1(@element, @children)
|
||||
it "provides a handler url", ->
|
||||
expect(@runtime.handlerUrl(@element, 'foo')).toBe('/xblock/fake-usage-id/handler/foo')
|
||||
@runtime = new XBlock.Runtime.v1(@element, @children)
|
||||
|
||||
it "provides a list of children", ->
|
||||
expect(@runtime.children).toBe(@children)
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
@XBlock =
|
||||
runtime: {}
|
||||
Runtime: {}
|
||||
|
||||
initializeBlock: (element) ->
|
||||
$element = $(element)
|
||||
children = @initializeBlocks($element)
|
||||
runtime = $element.data("runtime-class")
|
||||
version = $element.data("runtime-version")
|
||||
initFnName = $element.data("init")
|
||||
if version? and initFnName?
|
||||
runtime = @runtime["v#{version}"](element, children)
|
||||
if runtime? and version? and initFnName?
|
||||
runtime = new window[runtime]["v#{version}"](element, children)
|
||||
initFn = window[initFnName]
|
||||
block = initFn(runtime, element) ? {}
|
||||
else
|
||||
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.element = element
|
||||
|
||||
@@ -1,24 +1,5 @@
|
||||
@XBlock.runtime.v1 = (element, children) ->
|
||||
childMap = {}
|
||||
$.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
|
||||
}
|
||||
class XBlock.Runtime.v1
|
||||
constructor: (@element, @children) ->
|
||||
@childMap = {}
|
||||
$.each @children, (idx, child) =>
|
||||
@childMap[child.name] = child
|
||||
|
||||
@@ -21,7 +21,7 @@ from courseware.access import has_access, get_user_role
|
||||
from courseware.masquerade import setup_masquerade
|
||||
from courseware.model_data import FieldDataCache, DjangoKeyValueStore
|
||||
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 psychometrics.psychoanalyze import make_psychometrics_data_update_handler
|
||||
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
|
||||
# Wrap the output display in a single div to allow for the XModule
|
||||
# javascript to be bound correctly
|
||||
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
|
||||
# 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
|
||||
# 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.
|
||||
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:
|
||||
anonymous_student_id = anonymous_id_for_user(user, course_id)
|
||||
else:
|
||||
|
||||
@@ -24,7 +24,6 @@ from django_comment_client.utils import has_forum_access
|
||||
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
|
||||
from student.models import CourseEnrollment
|
||||
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
|
||||
@@ -206,7 +205,7 @@ def _section_send_email(course_id, access, course):
|
||||
ScopeIds(None, None, None, 'i4x://dummy_org/dummy_course/html/dummy_name')
|
||||
)
|
||||
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
|
||||
section_data = {
|
||||
'section_key': 'send_email',
|
||||
|
||||
@@ -61,7 +61,6 @@ import track.views
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
from django.utils.translation import ugettext as _u
|
||||
from lms.lib.xblock.runtime import handler_prefix
|
||||
|
||||
from microsite_configuration.middleware import MicrositeConfiguration
|
||||
|
||||
@@ -848,7 +847,7 @@ def instructor_dashboard(request, course_id):
|
||||
ScopeIds(None, None, None, 'i4x://dummy_org/dummy_course/html/dummy_name')
|
||||
)
|
||||
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
|
||||
|
||||
# Enable instructor email only if the following conditions are met:
|
||||
|
||||
@@ -750,6 +750,7 @@ main_vendor_js = [
|
||||
'js/vendor/ova/ova.js',
|
||||
'js/vendor/ova/catch/js/catch.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'))
|
||||
@@ -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
|
||||
# the jasmine test page
|
||||
PIPELINE_JS = {
|
||||
'application': {
|
||||
|
||||
# Application will contain all paths not in courseware_only_js
|
||||
'source_filenames': sorted(
|
||||
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)
|
||||
) + [
|
||||
'source_filenames': sorted(common_js) + sorted(project_js) + [
|
||||
'js/form.ext.js',
|
||||
'js/my_courses_dropdown.js',
|
||||
'js/toggle_login_modal.js',
|
||||
|
||||
@@ -58,63 +58,6 @@ def 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):
|
||||
"""
|
||||
A runtime mixin that provides a handler_url function that routes
|
||||
@@ -127,7 +70,34 @@ class LmsHandlerUrls(object):
|
||||
# pylint: disable=no-member
|
||||
def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False):
|
||||
"""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
|
||||
|
||||
@@ -6,7 +6,7 @@ from ddt import ddt, data
|
||||
from mock import Mock
|
||||
from unittest import TestCase
|
||||
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 = [
|
||||
'',
|
||||
@@ -41,23 +41,31 @@ class TestHandlerUrl(TestCase):
|
||||
def setUp(self):
|
||||
self.block = Mock()
|
||||
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):
|
||||
self.assertFalse(handler_url(self.course_id, 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(self.runtime.handler_url(self.block, 'handler').endswith('/'))
|
||||
|
||||
self.assertFalse(handler_url(self.course_id, 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(self.runtime.handler_url(self.block, 'handler', 'suffix').endswith('/'))
|
||||
|
||||
self.assertFalse(handler_url(self.course_id, 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(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(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(self.runtime.handler_url(self.block, 'handler', query='query').endswith('/'))
|
||||
|
||||
def _parsed_query(self, 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):
|
||||
self.assertIn('foo=bar', self._parsed_query('foo=bar'))
|
||||
@@ -66,7 +74,7 @@ class TestHandlerUrl(TestCase):
|
||||
|
||||
def _parsed_path(self, handler_name='handler', 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):
|
||||
self.assertTrue(self._parsed_path(suffix="foo").endswith('foo'))
|
||||
|
||||
18
lms/static/coffee/src/xblock/lms.runtime.v1.coffee
Normal file
18
lms/static/coffee/src/xblock/lms.runtime.v1.coffee
Normal file
@@ -0,0 +1,18 @@
|
||||
@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:
|
||||
- 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/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/xblock
|
||||
- xmodule_js/src/capa/
|
||||
|
||||
Reference in New Issue
Block a user