diff --git a/cms/envs/common.py b/cms/envs/common.py index 58b8294115..1f95f37d6b 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1018,6 +1018,11 @@ COURSE_IMPORT_EXPORT_STORAGE = 'django.core.files.storage.FileSystemStorage' ##### 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 a86cf21cf8..33accebc07 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 d2fd62beff..9b4b5f4e18 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 0000000000..f73ea5452f --- /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 523a303874..7d33e0cf60 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 @@ django-extensions that we were actually using. """ +import json + from django.conf import settings from django.core.management.base import BaseCommand, CommandError @@ -27,10 +29,23 @@ class Command(BaseCommand): 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 4014c4f2a2..9d2e3f12f1 100644 --- a/pavelib/assets.py +++ b/pavelib/assets.py @@ -5,6 +5,7 @@ Asset compilation and collection. 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 3bbff733e3..ae6e961547 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_INDEX_COURSE_COMMAND = ( ) 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 @@ class TestPaverServerTasks(PaverTestCase): 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 @@ class TestPaverServerTasks(PaverTestCase): 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 5baae419d6..c295fd43d6 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 @@ class Env: 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 @@ class Env: 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 68906fcab1..3987e82fdf 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 360ab56d4d..dc85e4efae 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