Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,11 @@
##### EMBARGO #####
EMBARGO_SITE_REDIRECT_URL = None

##### custom vendor plugin variables #####
Comment thread
bradenmacdonald marked this conversation as resolved.
# 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 = {
Expand Down
39 changes: 37 additions & 2 deletions common/lib/xmodule/xmodule/js/src/html/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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', {

/*
Expand Down
4 changes: 4 additions & 0 deletions docs/guides/extension_points.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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*.

Expand All @@ -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



Expand Down
65 changes: 65 additions & 0 deletions docs/guides/extensions/tinymce_plugins.rst
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"""


import json

from django.conf import settings
from django.core.management.base import BaseCommand, CommandError

Expand All @@ -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)
11 changes: 9 additions & 2 deletions pavelib/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import argparse
import glob
import json
import os
import traceback
from datetime import datetime
Expand Down Expand Up @@ -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(
Expand Down
8 changes: 7 additions & 1 deletion pavelib/paver_tests/test_servers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Unit tests for the Paver server tasks."""


import json

import ddt
from paver.easy import call_task

Expand Down Expand Up @@ -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}"
)

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
25 changes: 22 additions & 3 deletions pavelib/utils/envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
)
Expand All @@ -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):
"""
Expand Down
3 changes: 2 additions & 1 deletion webpack.dev.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
3 changes: 2 additions & 1 deletion webpack.prod.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down