diff --git a/cms/envs/common.py b/cms/envs/common.py index 58b82941150c..1f95f37d6bd9 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1018,6 +1018,11 @@ ##### EMBARGO ##### EMBARGO_SITE_REDIRECT_URL = None +##### custom vendor plugin variables ##### +# JavaScript code can access this data using `process.env.JS_ENV_EXTRA_CONFIG` +# One of the current use cases for this is enabling custom TinyMCE plugins +JS_ENV_EXTRA_CONFIG = {} + ############################### PIPELINE ####################################### PIPELINE = { diff --git a/common/lib/xmodule/xmodule/js/src/html/edit.js b/common/lib/xmodule/xmodule/js/src/html/edit.js index a86cf21cf811..33accebc07c3 100644 --- a/common/lib/xmodule/xmodule/js/src/html/edit.js +++ b/common/lib/xmodule/xmodule/js/src/html/edit.js @@ -95,7 +95,8 @@ tinyMCE incorrectly decides that the suffix should be "", which means it fails to load files. */ tinyMCE.suffix = ".min"; - this.tiny_mce_textarea = $(".tiny-mce", this.element).tinymce({ + + var tinyMceConfig = { script_url: baseUrl + "js/vendor/tinymce/js/tinymce/tinymce.full.min.js", font_formats: _getFonts(), theme: "modern", @@ -171,7 +172,41 @@ */ init_instance_callback: this.initInstanceCallback, browser_spellcheck: true - }); + }; + + if (typeof process != "undefined" && process.env.JS_ENV_EXTRA_CONFIG) { + var tinyMceAdditionalPlugins = process.env.JS_ENV_EXTRA_CONFIG.TINYMCE_ADDITIONAL_PLUGINS; + // check if we have any additional plugins passed + if (tinyMceAdditionalPlugins) { + // go over each plugin + tinyMceAdditionalPlugins.forEach(function (tinyMcePlugin) { + // check if plugins is not empty (ie there are existing plugins) + if (tinyMceConfig.plugins.trim()) { + tinyMceConfig.plugins += ', '; + } + + // add the plugin to the list of plugins + tinyMceConfig.plugins += tinyMcePlugin.name; + + // check if the plugin should be included in the toolbar + if (tinyMcePlugin.toolbar) { + // check if toolbar is not empty (ie there are already items in the toolbar) + if (tinyMceConfig.toolbar.trim()) { + tinyMceConfig.toolbar += ' | '; + } + + tinyMceConfig.toolbar += tinyMcePlugin.name; + } + + // add the additional settings for each plugin (if there is any) + if (tinyMcePlugin.extra_settings) { + tinyMceConfig[tinyMcePlugin.name] = tinyMcePlugin.extra_settings; + } + }); + } + } + + this.tiny_mce_textarea = $(".tiny-mce", this.element).tinymce(tinyMceConfig); tinymce.addI18n('en', { /* diff --git a/docs/guides/extension_points.rst b/docs/guides/extension_points.rst index d2fd62beff9c..9b4b5f4e18db 100644 --- a/docs/guides/extension_points.rst +++ b/docs/guides/extension_points.rst @@ -63,6 +63,9 @@ If you want to provide learners with new content experiences within courses, opt * - **External Graders** - Hold, Stable - An external grader is a service that receives learner responses to a problem, processes those responses, and returns feedback and a problem grade to the edX platform. You build and deploy an external grader separately from the edX platform. An external grader is particularly useful for software programming courses where learners are asked to submit complex code. See the `external grader documentation`_ for details. + * - **TinyMCE (Visual Text/HTML Editor) Plugins** + - Trial, Limited + - TinyMCE's functionality can be extended with so-called Plugins. Custom TinyMCE plugins can be particularly useful for serving certain content in courses that isn't available yet; they can also be used to facilitate the educator's work. `You can follow this guide to install and enable custom TinyMCE plugins`_. For a more detailed comparison of content integration options, see `Options for Extending the edX Platform`_ in the *Open edX Developer's Guide*. @@ -72,6 +75,7 @@ For a more detailed comparison of content integration options, see `Options for .. _Options for Extending the edX Platform: https://edx.readthedocs.io/projects/edx-developer-guide/en/latest/extending_platform/extending.html .. _custom JavaScript application: https://edx.readthedocs.io/projects/edx-developer-guide/en/latest/extending_platform/javascript.html .. _external grader documentation: https://edx.readthedocs.io/projects/open-edx-ca/en/latest/exercises_tools/external_graders.html +.. _You can follow this guide to install and enable custom TinyMCE plugins: extensions/tinymce_plugins.rst diff --git a/docs/guides/extensions/tinymce_plugins.rst b/docs/guides/extensions/tinymce_plugins.rst new file mode 100644 index 000000000000..f73ea5452f72 --- /dev/null +++ b/docs/guides/extensions/tinymce_plugins.rst @@ -0,0 +1,65 @@ +TinyMCE (Visual Text/HTML Editor) Plugins +----------------------------------------- + +The flexibility of the TinyMCE Visual Text and HTML editor makes it possible to configure and extend the editor using different plugins. In order to make use of that modularity in Studio, you'll need to follow two different steps. + +Installing Plugins +================== + +Initially, we'll need to specify which plugins need to install so that they can be bundled with the static assets. + +There's a decent `guide on installing the plugins through the edX configuration`_, specifically using the ``TINYMCE_ADDITIONAL_PLUGINS_LIST`` configuration variable. + +Enabling Plugins +================ + +Enabling the plugins requires adding a Studio environment setting which the JavaScript code can access, ``JS_ENV_EXTRA_CONFIG``. It is simply a dictionary which would contain different extra JavaScript configurations. + +The extra JavaScript configuration that's responsible for enabling TinyMCE plugins is ``TINYMCE_ADDITIONAL_PLUGINS``. This is a list of different TinyMCE plugins which you would want to enable. + +Each TinyMCE plugin has the following attributes. + +.. list-table:: + :header-rows: 1 + :widths: 15 10 75 + + * - attribute + - type + - description + * - ``name`` + - string + - The name of the TinyMCE plugin which would be included in the editor's list of plugins. + * - ``toolbar`` + - boolean + - Indicates whether this plugin should be displayed in the toolbar or not. + * - ``extra_settings`` + - object + - Specifies the extra plugin settings that need to be added to the TinyMCE editor's configuration. + +Here's an example: + +.. code:: yaml + + EDXAPP_CMS_ENV_EXTRA: + JS_ENV_EXTRA_CONFIG: + TINYMCE_ADDITIONAL_PLUGINS: + - name: adsklink + toolbar: true + extra_settings: + linktypes: + - Download + - Offer + filetypes: + - PDF + - ZIP + - Video + - Design + orientations: + - Vertical + - Horizontal + styles: + - Primary + - Normal + - Secondary + +.. _guide on installing the plugins through the edX configuration: https://github.com/edx/configuration/blob/master/playbooks/roles/tinymce_plugins/README.rst diff --git a/openedx/core/djangoapps/util/management/commands/print_setting.py b/openedx/core/djangoapps/util/management/commands/print_setting.py index 523a303874f5..7d33e0cf60c8 100644 --- a/openedx/core/djangoapps/util/management/commands/print_setting.py +++ b/openedx/core/djangoapps/util/management/commands/print_setting.py @@ -11,6 +11,8 @@ """ +import json + from django.conf import settings from django.core.management.base import BaseCommand, CommandError @@ -27,10 +29,23 @@ def add_arguments(self, parser): help='Specifies the list of settings to be printed.' ) + parser.add_argument( + '--json', + action='store_true', + help='Returns setting as JSON string instead.', + ) + def handle(self, *args, **options): settings_to_print = options.get('settings_to_print') + dump_as_json = options.get('json') for setting in settings_to_print: if not hasattr(settings, setting): raise CommandError('%s not found in settings.' % setting) - print(getattr(settings, setting)) + + setting_value = getattr(settings, setting) + + if dump_as_json: + setting_value = json.dumps(setting_value, sort_keys=True) + + print(setting_value) diff --git a/pavelib/assets.py b/pavelib/assets.py index 4014c4f2a288..9d2e3f12f1cc 100644 --- a/pavelib/assets.py +++ b/pavelib/assets.py @@ -5,6 +5,7 @@ import argparse import glob +import json import os import traceback from datetime import datetime @@ -778,10 +779,16 @@ def webpack(options): result = Env.get_django_settings(['STATIC_ROOT', 'WEBPACK_CONFIG_PATH'], "lms", settings=settings) static_root_lms, config_path = result static_root_cms, = Env.get_django_settings(["STATIC_ROOT"], "cms", settings=settings) - environment = 'NODE_ENV={node_env} STATIC_ROOT_LMS={static_root_lms} STATIC_ROOT_CMS={static_root_cms}'.format( + js_env_extra_config_setting, = Env.get_django_json_settings(["JS_ENV_EXTRA_CONFIG"], "cms", settings=settings) + js_env_extra_config = json.dumps(js_env_extra_config_setting or "{}") + environment = ( + "NODE_ENV={node_env} STATIC_ROOT_LMS={static_root_lms} STATIC_ROOT_CMS={static_root_cms} " + "JS_ENV_EXTRA_CONFIG={js_env_extra_config}" + ).format( node_env="development" if config_path == 'webpack.dev.config.js' else "production", static_root_lms=static_root_lms, - static_root_cms=static_root_cms + static_root_cms=static_root_cms, + js_env_extra_config=js_env_extra_config, ) sh( cmd( diff --git a/pavelib/paver_tests/test_servers.py b/pavelib/paver_tests/test_servers.py index 3bbff733e310..ae6e96154737 100644 --- a/pavelib/paver_tests/test_servers.py +++ b/pavelib/paver_tests/test_servers.py @@ -1,6 +1,8 @@ """Unit tests for the Paver server tasks.""" +import json + import ddt from paver.easy import call_task @@ -45,10 +47,12 @@ ) EXPECTED_PRINT_SETTINGS_COMMAND = [ "python manage.py lms --settings={settings} print_setting STATIC_ROOT WEBPACK_CONFIG_PATH 2>{log_file}", - "python manage.py cms --settings={settings} print_setting STATIC_ROOT 2>{log_file}" + "python manage.py cms --settings={settings} print_setting STATIC_ROOT 2>{log_file}", + "python manage.py cms --settings={settings} print_setting JS_ENV_EXTRA_CONFIG 2>{log_file} --json", ] EXPECTED_WEBPACK_COMMAND = ( "NODE_ENV={node_env} STATIC_ROOT_LMS={static_root_lms} STATIC_ROOT_CMS={static_root_cms} " + "JS_ENV_EXTRA_CONFIG={js_env_extra_config} " "$(npm bin)/webpack --config={webpack_config_path}" ) @@ -251,6 +255,7 @@ def verify_server_task(self, task_name, options, contracts_default=False): node_env="production", static_root_lms=None, static_root_cms=None, + js_env_extra_config=json.dumps("{}"), webpack_config_path=None )) expected_messages.extend(self.expected_sass_commands(system=system, asset_settings=expected_asset_settings)) @@ -297,6 +302,7 @@ def verify_run_all_servers_task(self, options): node_env="production", static_root_lms=None, static_root_cms=None, + js_env_extra_config=json.dumps("{}"), webpack_config_path=None )) expected_messages.extend(self.expected_sass_commands(asset_settings=expected_asset_settings)) diff --git a/pavelib/utils/envs.py b/pavelib/utils/envs.py index 5baae419d60e..c295fd43d672 100644 --- a/pavelib/utils/envs.py +++ b/pavelib/utils/envs.py @@ -236,12 +236,13 @@ class Env: SERVICE_VARIANT = 'lms' @classmethod - def get_django_settings(cls, django_settings, system, settings=None): + def get_django_settings(cls, django_settings, system, settings=None, print_setting_args=None): """ Interrogate Django environment for specific settings values :param django_settings: list of django settings values to get :param system: the django app to use when asking for the setting (lms | cms) :param settings: the settings file to use when asking for the value + :param print_setting_args: the additional arguments to send to print_settings :return: unicode value of the django setting """ if not settings: @@ -251,15 +252,17 @@ def get_django_settings(cls, django_settings, system, settings=None): os.makedirs(log_dir) settings_length = len(django_settings) django_settings = ' '.join(django_settings) # parse_known_args makes a list again + print_setting_args = ' '.join(print_setting_args or []) try: value = sh( django_cmd( system, settings, - "print_setting {django_settings} 2>{log_file}".format( + "print_setting {django_settings} 2>{log_file} {print_setting_args}".format( django_settings=django_settings, + print_setting_args=print_setting_args, log_file=cls.PRINT_SETTINGS_LOG_FILE - ) + ).strip() ), capture=True ) @@ -271,6 +274,22 @@ def get_django_settings(cls, django_settings, system, settings=None): print(f.read()) sys.exit(1) + @classmethod + def get_django_json_settings(cls, django_settings, system, settings=None): + """ + Interrogate Django environment for specific settings value + :param django_settings: list of django settings values to get + :param system: the django app to use when asking for the setting (lms | cms) + :param settings: the settings file to use when asking for the value + :return: json string value of the django setting + """ + return cls.get_django_settings( + django_settings, + system, + settings=settings, + print_setting_args=["--json"], + ) + @classmethod def covered_modules(cls): """ diff --git a/webpack.dev.config.js b/webpack.dev.config.js index 68906fcab1d3..3987e82fdf7d 100644 --- a/webpack.dev.config.js +++ b/webpack.dev.config.js @@ -20,7 +20,8 @@ module.exports = _.values(Merge.smart(commonConfig, { debug: true }), new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('development') + 'process.env.NODE_ENV': JSON.stringify('development'), + 'process.env.JS_ENV_EXTRA_CONFIG': process.env.JS_ENV_EXTRA_CONFIG }) ], module: { diff --git a/webpack.prod.config.js b/webpack.prod.config.js index 360ab56d4d01..dc85e4efae3b 100644 --- a/webpack.prod.config.js +++ b/webpack.prod.config.js @@ -17,7 +17,8 @@ var optimizedConfig = Merge.smart(commonConfig, { devtool: false, plugins: [ new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('production') + 'process.env.NODE_ENV': JSON.stringify('production'), + 'process.env.JS_ENV_EXTRA_CONFIG': process.env.JS_ENV_EXTRA_CONFIG }), new webpack.LoaderOptionsPlugin({ // This may not be needed; legacy option for loaders written for webpack 1 minimize: true