feat: Make content libraries an LTI 1.3 tool
Offers blockstore-based content through content libraries acting as an LTI 1.3 tool: - Content Library support LTI 1.3 launches offering blockstore-based content through resource links. - Content Library support LTI 1.3. AGS, allowing gradebook updates from graded assignments.
This commit is contained in:
committed by
Braden MacDonald
parent
bc5e6118a9
commit
14e2f29516
@@ -258,6 +258,16 @@ FEATURES = {
|
||||
# only supported in courses using split mongo.
|
||||
'ENABLE_CONTENT_LIBRARIES': True,
|
||||
|
||||
# .. toggle_name: FEATURES['ENABLE_CONTENT_LIBRARIES_LTI_TOOL']
|
||||
# .. toggle_implementation: DjangoSetting
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: When set to True, Content Libraries in
|
||||
# Studio can be used as an LTI 1.3 tool by external LTI platforms.
|
||||
# .. toggle_use_cases: open_edx
|
||||
# .. toggle_creation_date: 2021-08-17
|
||||
# .. toggle_tickets: https://github.com/edx/edx-platform/pull/27411
|
||||
'ENABLE_CONTENT_LIBRARIES_LTI_TOOL': False,
|
||||
|
||||
# Milestones application flag
|
||||
'MILESTONES_APP': False,
|
||||
|
||||
@@ -617,6 +627,7 @@ EDX_ROOT_URL = ''
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'auth_backends.backends.EdXOAuth2',
|
||||
'rules.permissions.ObjectPermissionBackend',
|
||||
'openedx.core.djangoapps.content_libraries.auth.LtiAuthenticationBackend',
|
||||
'openedx.core.djangoapps.oauth_dispatch.dot_overrides.backends.EdxRateLimitedAllowAllUsersModelBackend',
|
||||
'bridgekeeper.backends.RulePermissionBackend',
|
||||
]
|
||||
@@ -1629,6 +1640,9 @@ INSTALLED_APPS = [
|
||||
|
||||
# Allow Studio to use LMS for SSO
|
||||
'social_django',
|
||||
|
||||
# Content Library LTI 1.3 Support.
|
||||
'pylti1p3.contrib.django.lti1p3_tool_config',
|
||||
]
|
||||
|
||||
|
||||
|
||||
295
cms/templates/content_libraries/xblock_iframe.html
Normal file
295
cms/templates/content_libraries/xblock_iframe.html
Normal file
@@ -0,0 +1,295 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<!-- Open links in a new tab, not this iframe -->
|
||||
<base target="_blank">
|
||||
<meta charset="UTF-8">
|
||||
<!-- gettext & XBlock JS i18n code -->
|
||||
<script type="text/javascript" src="{{ lms_root_url }}/static/js/i18n/en/djangojs.js"></script>
|
||||
<!-- Most XBlocks require jQuery: -->
|
||||
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
|
||||
<!-- The Video XBlock requires "ajaxWithPrefix" -->
|
||||
<script type="text/javascript">
|
||||
$.postWithPrefix = $.post;
|
||||
$.getWithPrefix = $.get;
|
||||
$.ajaxWithPrefix = $.ajax;
|
||||
</script>
|
||||
<!-- The Video XBlock requires "Slider" from jQuery-UI: -->
|
||||
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
|
||||
<!-- The video XBlock depends on Underscore.JS -->
|
||||
<script type="text/javascript" src="{{ lms_root_url }}/static/common/js/vendor/underscore.js"></script>
|
||||
<!-- The video XBlock depends on jquery-cookie -->
|
||||
<script type="text/javascript" src="{{ lms_root_url }}/static/js/vendor/jquery.cookie.js"></script>
|
||||
<!--The Video XBlock has an undeclared dependency on 'Logger' -->
|
||||
<script>
|
||||
window.Logger = { log: function() { } };
|
||||
</script>
|
||||
<!-- Builtin XBlock types depend on RequireJS -->
|
||||
<script type="text/javascript" src="{{ lms_root_url }}/static/common/js/vendor/require.js"></script>
|
||||
<script type="text/javascript" src="{{ lms_root_url }}/static/js/RequireJS-namespace-undefine.js"></script>
|
||||
<script>
|
||||
// The minimal RequireJS configuration required for common LMS building XBlock types to work:
|
||||
(function (require, define) {
|
||||
require.config({
|
||||
baseUrl: "{{ lms_root_url }}/static/",
|
||||
paths: {
|
||||
accessibility: 'js/src/accessibility_tools',
|
||||
draggabilly: 'js/vendor/draggabilly',
|
||||
hls: 'common/js/vendor/hls',
|
||||
moment: 'common/js/vendor/moment-with-locales',
|
||||
HtmlUtils: 'edx-ui-toolkit/js/utils/html-utils',
|
||||
},
|
||||
});
|
||||
define('gettext', [], function() { return window.gettext; });
|
||||
define('jquery', [], function() { return window.jQuery; });
|
||||
define('jquery-migrate', [], function() { return window.jQuery; });
|
||||
define('underscore', [], function() { return window._; });
|
||||
}).call(this, require || RequireJS.require, define || RequireJS.define);
|
||||
</script>
|
||||
<!-- edX HTML Utils requires GlobalLoader -->
|
||||
<script type="text/javascript" src="{{ lms_root_url }}/static/edx-ui-toolkit/js/utils/global-loader.js"></script>
|
||||
<script>
|
||||
// The video XBlock has an undeclared dependency on edX HTML Utils
|
||||
RequireJS.require(['HtmlUtils'], function (HtmlUtils) {
|
||||
window.edx.HtmlUtils = HtmlUtils;
|
||||
// The problem XBlock depends on window.SR, though 'accessibility_tools' has an undeclared dependency on HtmlUtils:
|
||||
RequireJS.require(['accessibility']);
|
||||
});
|
||||
RequireJS.require(['edx-ui-toolkit/js/utils/string-utils'], function (StringUtils) {
|
||||
window.edx.StringUtils = StringUtils;
|
||||
});
|
||||
</script>
|
||||
<!--
|
||||
commons.js: this file produced by webpack contains many shared chunks of code.
|
||||
By including this, you have only to also import any of the smaller entrypoint
|
||||
files (defined in webpack.common.config.js) to get that entry point and all
|
||||
of its dependencies.
|
||||
-->
|
||||
<script type="text/javascript" src="{{ lms_root_url }}/static/xmodule_js/common_static/bundles/commons.js"></script>
|
||||
<!-- The video XBlock (and perhaps others?) expect this global: -->
|
||||
<script>
|
||||
window.onTouchBasedDevice = function() { return navigator.userAgent.match(/iPhone|iPod|iPad|Android/i); };
|
||||
</script>
|
||||
<!-- At least one XBlock (drag and drop v2) expects Font Awesome -->
|
||||
<link rel="stylesheet"
|
||||
href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<!-- Capa Problem Editing requires CodeMirror -->
|
||||
<link rel="stylesheet" href="{{ lms_root_url }}/static/js/vendor/CodeMirror/codemirror.css">
|
||||
<!-- Built-in XBlocks (and some plugins) depends on LMS CSS -->
|
||||
<link rel="stylesheet" href="{{ lms_root_url }}/static/css/lms-course.css">
|
||||
<!-- Configure and load MathJax -->
|
||||
<script type="text/x-mathjax-config">
|
||||
MathJax.Hub.Config({
|
||||
tex2jax: {
|
||||
inlineMath: [
|
||||
["\\(","\\)"],
|
||||
['[mathjaxinline]','[/mathjaxinline]']
|
||||
],
|
||||
displayMath: [
|
||||
["\\[","\\]"],
|
||||
['[mathjax]','[/mathjax]']
|
||||
]
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script type="text/x-mathjax-config">
|
||||
MathJax.Hub.signal.Interest(function(message) {
|
||||
if(message[0] === "End Math") {
|
||||
set_mathjax_display_div_settings();
|
||||
}
|
||||
});
|
||||
function set_mathjax_display_div_settings() {
|
||||
$('.MathJax_Display').each(function( index ) {
|
||||
this.setAttribute('tabindex', '0');
|
||||
this.setAttribute('aria-live', 'off');
|
||||
this.removeAttribute('role');
|
||||
this.removeAttribute('aria-readonly');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
// Activating Mathjax accessibility files
|
||||
window.MathJax = {
|
||||
menuSettings: {
|
||||
collapsible: true,
|
||||
autocollapse: false,
|
||||
explorer: true
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
|
||||
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
|
||||
MathJax extension libraries -->
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/mathjax@2.7.5/MathJax.js?config=TeX-MML-AM_SVG"></script>
|
||||
<!-- fragment head -->
|
||||
{{ fragment.head_html | safe }}
|
||||
</head>
|
||||
<body>
|
||||
<!-- fragment body -->
|
||||
{{ fragment.body_html | safe }}
|
||||
<!-- fragment foot -->
|
||||
{{ fragment.foot_html | safe }}
|
||||
<script>
|
||||
/**
|
||||
* Map of all URL handlers for this block and its children, keyed by usage
|
||||
* key.
|
||||
*/
|
||||
{% comment %}
|
||||
This variable is expected to be a valid JSON, which will be translated
|
||||
directly into a javascript object.
|
||||
{% endcomment %}
|
||||
|
||||
HANDLER_URL_MAP = {{ handler_urls_json | safe }};
|
||||
|
||||
/**
|
||||
* The JavaScript code which runs inside our IFrame and is responsible
|
||||
* for communicating with the parent window.
|
||||
*
|
||||
* This cannot use any imported functions because it runs in the IFrame,
|
||||
* not in our app webpack bundle.
|
||||
*/
|
||||
function blockFrameJS() {
|
||||
const CHILDREN_KEY = '_jsrt_xb_children'; // JavaScript RunTime XBlock children
|
||||
const USAGE_ID_KEY = '_jsrt_xb_usage_id';
|
||||
const HANDLER_URL = '_jsrt_xb_handler_url';
|
||||
|
||||
const uniqueKeyPrefix = `k${+Date.now()}-${Math.floor(Math.random() * 1e10)}-`;
|
||||
let messageCount = 0;
|
||||
|
||||
/**
|
||||
* The JavaScript runtime for any XBlock in the IFrame
|
||||
*/
|
||||
const runtime = {
|
||||
/**
|
||||
* An obscure and little-used API that retrieves a particular
|
||||
* XBlock child using its 'data-name' attribute
|
||||
* @param block The root DIV element of the XBlock calling this method
|
||||
* @param childName The value of the 'data-name' attribute of the root
|
||||
* DIV element of the XBlock child in question.
|
||||
*/
|
||||
childMap: (block, childName) => runtime.children(block).find((child) => child.element.getAttribute('data-name') === childName),
|
||||
children: (block) => block[CHILDREN_KEY],
|
||||
/**
|
||||
* Get the URL for the specified handler. This method must be synchronous, so
|
||||
* cannot make HTTP requests.
|
||||
*/
|
||||
handlerUrl: (block, handlerName, suffix, query) => {
|
||||
let url = block[HANDLER_URL].replace('handler_name', handlerName);
|
||||
if (suffix) {
|
||||
url += `${suffix}/`;
|
||||
}
|
||||
if (query) {
|
||||
url += `?${query}`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize an XBlock. This function should only be called by initializeXBlockAndChildren
|
||||
* because it assumes that function has already run.
|
||||
*/
|
||||
function initializeXBlock(element, callback) {
|
||||
const usageId = element[USAGE_ID_KEY];
|
||||
// Check if the XBlock has an initialization function:
|
||||
const initFunctionName = element.getAttribute('data-init');
|
||||
if (initFunctionName !== null) {
|
||||
// Since this block has an init function, it may need to call handlers:
|
||||
element[HANDLER_URL] = HANDLER_URL_MAP[usageId];
|
||||
// Now proceed with initializing the block's JavaScript:
|
||||
const InitFunction = (window)[initFunctionName];
|
||||
// Does the XBlock HTML contain arguments to pass to the InitFunction?
|
||||
let data = {};
|
||||
[].forEach.call(element.children, (childNode) => {
|
||||
// The newer/pure/Blockstore runtime uses 'xblock_json_init_args'
|
||||
// while the LMS runtime uses 'xblock-json-init-args'.
|
||||
if (
|
||||
childNode.matches('script.xblock_json_init_args')
|
||||
|| childNode.matches('script.xblock-json-init-args')
|
||||
) {
|
||||
data = JSON.parse(childNode.textContent);
|
||||
}
|
||||
});
|
||||
// An unfortunate inconsistency is that the old Studio runtime used
|
||||
// to pass 'element' as a jQuery-wrapped DOM element, whereas the LMS
|
||||
// runtime used to pass 'element' as the pure DOM node. In order not to
|
||||
// break backwards compatibility, we would need to maintain that.
|
||||
// However, this is currently disabled as it causes issues (need to
|
||||
// modify the runtime methods like handlerUrl too), and we decided not
|
||||
// to maintain support for legacy studio_view in this runtime.
|
||||
// const isStudioView = element.className.indexOf('studio_view') !== -1;
|
||||
// const passElement = isStudioView && (window as any).$ ? (window as any).$(element) : element;
|
||||
const blockJS = new InitFunction(runtime, element, data) || {};
|
||||
blockJS.element = element;
|
||||
callback(blockJS);
|
||||
} else {
|
||||
const blockJS = { element };
|
||||
callback(blockJS);
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively initialize the JavaScript code of each XBlock:
|
||||
function initializeXBlockAndChildren(element, callback) {
|
||||
// The newer/pure/Blockstore runtime uses the 'data-usage' attribute, while the LMS uses 'data-usage-id'
|
||||
const usageId = element.getAttribute('data-usage') || element.getAttribute('data-usage-id');
|
||||
if (usageId !== null) {
|
||||
element[USAGE_ID_KEY] = usageId;
|
||||
} else {
|
||||
throw new Error('XBlock is missing a usage ID attribute on its root HTML node.');
|
||||
}
|
||||
|
||||
const version = element.getAttribute('data-runtime-version');
|
||||
if (version != null && version !== '1') {
|
||||
throw new Error('Unsupported XBlock runtime version requirement.');
|
||||
}
|
||||
|
||||
// Recursively initialize any children first:
|
||||
// We need to find all div.xblock-v1 children, unless they're grandchilden
|
||||
// So we build a list of all div.xblock-v1 descendants that aren't descendants
|
||||
// of an already-found descendant:
|
||||
const childNodesFound = [];
|
||||
[].forEach.call(element.querySelectorAll('.xblock, .xblock-v1'), (childNode) => {
|
||||
if (!childNodesFound.find((el) => el.contains(childNode))) {
|
||||
childNodesFound.push(childNode);
|
||||
}
|
||||
});
|
||||
|
||||
// This code is awkward because we can't use promises (IE11 etc.)
|
||||
let childrenInitialized = -1;
|
||||
function initNextChild() {
|
||||
childrenInitialized += 1;
|
||||
if (childrenInitialized < childNodesFound.length) {
|
||||
const childNode = childNodesFound[childrenInitialized];
|
||||
initializeXBlockAndChildren(childNode, initNextChild);
|
||||
} else {
|
||||
// All children are initialized:
|
||||
initializeXBlock(element, callback);
|
||||
}
|
||||
}
|
||||
initNextChild();
|
||||
}
|
||||
|
||||
// Find the root XBlock node.
|
||||
// The newer/pure/Blockstore runtime uses '.xblock-v1' while the LMS runtime uses '.xblock'.
|
||||
const rootNode = document.querySelector('.xblock, .xblock-v1'); // will always return the first matching element
|
||||
initializeXBlockAndChildren(rootNode, () => {
|
||||
});
|
||||
|
||||
let lastHeight = -1;
|
||||
function checkFrameHeight() {
|
||||
const newHeight = document.documentElement.scrollHeight;
|
||||
if (newHeight !== lastHeight) {
|
||||
lastHeight = newHeight;
|
||||
}
|
||||
}
|
||||
// Check the size whenever the DOM changes:
|
||||
new MutationObserver(checkFrameHeight).observe(document.body, { attributes: true, childList: true, subtree: true });
|
||||
// And whenever the IFrame is resized
|
||||
window.addEventListener('resize', checkFrameHeight);
|
||||
}
|
||||
|
||||
window.addEventListener('load', blockFrameJS);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user