Commit 49217ebe by Calen Pennington

Allow multiple client-side runtimes on a single page

Make XBlock client-side runtimes proper classes, so that handlerUrl can
be defined in a per-runtime way, and we can have multiple runtimes on a
single page.

[LMS-1630][LMS-1421][LMS-1517]
parent 0f8919a6
......@@ -10,6 +10,7 @@ from xmodule_modifiers import wrap_xblock
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_http_methods
from xblock.fields import Scope
......@@ -34,7 +35,7 @@ from .helpers import _xmodule_recurse
from preview import handler_prefix, get_preview_html
from edxmako.shortcuts import render_to_response, 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 +44,12 @@ 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
# pylint: disable=unused-argument
@require_http_methods(("DELETE", "GET", "PUT", "POST"))
@login_required
......
......@@ -33,20 +33,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 +77,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 +113,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
......
......@@ -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
......@@ -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",
......
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'
......
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:
......
......@@ -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,9 +68,10 @@ 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(str(block.scope_ids.usage_id))
template_context = {
'content': block.display_name if display_name_only else frag.content,
......
@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
......
@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:
......
......@@ -754,6 +754,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'))
......@@ -819,17 +820,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
......
@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/
......
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