diff --git a/cms/templates/index.html b/cms/templates/index.html index 3918a67f9c38..7a81afdfdb4a 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -552,6 +552,7 @@

${_('Need help?')}

%endif + <%static:webpack entry="StudioIndex"> var enableReruns = ${allow_course_reruns and rerun_creator_status and course_creator_status=='granted' | n, dump_js_escaped_json}; new StudioCourseIndex( diff --git a/common/djangoapps/pipeline_mako/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html index 2e0c5ca49e46..514cdf6cd521 100644 --- a/common/djangoapps/pipeline_mako/templates/static_content.html +++ b/common/djangoapps/pipeline_mako/templates/static_content.html @@ -1,13 +1,15 @@ <%page expression_filter="h"/> <%! import logging +import json from django.contrib.staticfiles.storage import staticfiles_storage from pipeline_mako import compressed_css, compressed_js from django.utils.translation import get_language_bidi from mako.exceptions import TemplateLookupException from edxmako.shortcuts import marketing_link -from openedx.core.djangolib.js_utils import js_escaped_string +from openedx.core.djangolib.js_utils import js_escaped_string, dump_js_escaped_json +from openedx.core.djangolib.markup import HTML from openedx.core.djangoapps.site_configuration.helpers import ( page_title_breadcrumbs, get_value, @@ -18,6 +20,7 @@ is_request_in_themed_site, ) from certificates.api import get_asset_url_by_slug +from webpack_loader.templatetags.webpack_loader import render_bundle logger = logging.getLogger(__name__) %> @@ -97,25 +100,15 @@ -include it as the first script in this block <% - from django.template import Template, Context - from webpack_loader.exceptions import WebpackLoaderBadStatsError - import json - body = capture(caller.body) body_dict = json.loads(body) body_dict['lang'] = lang - return Template(""" - -
- {% load render_bundle from webpack_loader %} - {% render_bundle page %} - """).render(Context({ - 'body': json.dumps(body_dict), - 'page': page - })) %> + +
+ ${HTML(render_bundle(page))} <%def name="webpack(entry)"> @@ -124,21 +117,41 @@ Uses the Django template engine because our webpack loader only provides template tags for Jinja and Django. <% - from django.template import Template, Context - from webpack_loader.exceptions import WebpackLoaderBadStatsError - return Template(""" - {% load render_bundle from webpack_loader %} - {% render_bundle entry %} - {% if body %} - - {% endif %} - """).render(Context({ - 'entry': entry, - 'body': capture(caller.body) - })) + body = capture(caller.body) %> + ${HTML(render_bundle(entry))} + % if body: + + % endif + + +<%def name="renderReact(component, id, props={})"> + <%doc> + Wrapper function to load a React component via webpack() and render + it onto the page, passing an optional context object via props. + component: (string) The component to render, as specified by the name + of its Webpack entry point. + id: (string) A unique id to apply to the component's container div. + props: (dict, optional) An object containing data to pass into the + component as props. + + + ${HTML(render_bundle(component))} + ${HTML(render_bundle('ReactRenderer'))} + +
+ <%def name="require_module(module_name, class_name)"> diff --git a/common/static/js/src/ReactRenderer.jsx b/common/static/js/src/ReactRenderer.jsx new file mode 100644 index 000000000000..db5a5f17644d --- /dev/null +++ b/common/static/js/src/ReactRenderer.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +class ReactRendererException extends Error { + constructor(message) { + super(`ReactRendererException: ${message}`); + Error.captureStackTrace(this, ReactRendererException); + } +} + +export class ReactRenderer { + constructor({ component, selector, componentName, props = {} }) { + Object.assign(this, { + component, + selector, + componentName, + props, + }); + this.handleArgumentErrors(); + this.targetElement = this.getTargetElement(); + this.renderComponent(); + } + + handleArgumentErrors() { + if (this.component === null) { + throw new ReactRendererException( + `Component ${this.componentName} is not defined. Make sure you're ` + + `using a non-default export statement for the ${this.componentName} ` + + `class, that ${this.componentName} has an entry point defined ` + + 'within the \'entry\' section of webpack.common.config.js, and that the ' + + 'entry point is pointing at the correct file path.', + ); + } + if (!(this.props instanceof Object && this.props.constructor === Object)) { + let propsType = typeof this.props; + if (Array.isArray(this.props)) { + propsType = 'array'; + } else if (this.props === null) { + propsType = 'null'; + } + throw new ReactRendererException( + `Invalid props passed to component ${this.componentName}. Expected ` + + `an object, but received a ${propsType}.`, + ); + } + } + + getTargetElement() { + const elementList = document.querySelectorAll(this.selector); + if (elementList.length !== 1) { + throw new ReactRendererException( + `Expected 1 element match for selector "${this.selector}" ` + + `but received ${elementList.length} matches.`, + ); + } else { + return elementList[0]; + } + } + + renderComponent() { + ReactDOM.render( + React.createElement(this.component, this.props, null), + this.targetElement, + ); + } +} diff --git a/conftest.py b/conftest.py index 02b08cb1419b..1f32a76982e3 100644 --- a/conftest.py +++ b/conftest.py @@ -1,10 +1,18 @@ """ Default unit test configuration and fixtures. """ - from __future__ import absolute_import, unicode_literals +import pytest # Import hooks and fixture overrides from the cms package to # avoid duplicating the implementation from cms.conftest import _django_clear_site_cache, pytest_configure # pylint: disable=unused-import + + +@pytest.fixture(autouse=True) +def no_webpack_loader(monkeypatch): + monkeypatch.setattr( + "webpack_loader.templatetags.webpack_loader.render_bundle", + lambda x: '' + ) diff --git a/webpack.common.config.js b/webpack.common.config.js index 18ab5d468818..42f92106f243 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -35,7 +35,10 @@ module.exports = { Currency: './openedx/features/course_experience/static/course_experience/js/currency.js', Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js', LatestUpdate: './openedx/features/course_experience/static/course_experience/js/LatestUpdate.js', - WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js' + WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js', + + // Common + ReactRenderer: './common/static/js/src/ReactRenderer.jsx' }, output: {