JavaScript Extensions¶
Extensions are not purely server-side. Often, they need to interact with some part of the client-side UI, whether that’s to augment a dialog, dynamically render new UI, or communicate with the API.
To hook into things on the client side, JavaScript-side extensions can be
created. These are defined server-side and then served up to the browser. They
work much like the typical Python extensions, in that you’ll create an
extension subclass (of RB.Extension()
) and make use of hooks.
Creating an Extension¶
You’ll first start by defining some information server-side about your
extension. You’ll need to create a subclass of
reviewboard.extensions.base.JSExtension
and reference it in your
main extension class, using the Extension.js_extensions
attribute. Your
JSExtension
subclass will also
specify the name of the JavaScript-side extension model class to instantiate.
Here’s an example:
from reviewboard.extensions.base import Extension, JSExtension
class SampleJSExtension(JSExtension):
model_class = 'SampleExtensionProject.Extension'
class SampleExtension(Extension):
js_extensions = [SampleJSExtension]
js_bundles = {
'default': {
'source_filenames': (
'js/extension.js',
),
}
}
You’ll then need to create your JavaScript-side extension in a file listed in
an appropriate bundle (such as the js/extension.js
shown above):
window.SampleExtensionProject = {};
SampleExtensionProject.Extension = RB.Extension.extend({
initialize() {
RB.Extension.prototype.initialize.call(this);
/* Your setup goes here. */
}
});
Now unlike Python-side extensions, you don’t need to worry about things like managing the shutdown logic for extensions. These extensions only run while the page is loaded, and are re-initialized on every page load. They do not persist.
Tip
You can create multiple JavaScript-side extensions, which is useful when you have different requirements for different pages. You don’t need to do everything in one extension.
See Page-Specific Extensions for more information.
Instantiating Hooks¶
We ship with a few JavaScript-side extension hooks that you can use. These are documented in the JavaScript Extension Hooks list, and are instantiated like so:
SampleExtensionProject.Extension = RB.Extension.extend({
initialize() {
RB.Extension.prototype.initialize.call(this);
new RB.SomeExampleHook({
extension: this,
...
});
}
});
See the documentation for each hook on its usage.
Note
There aren’t a lot of JavaScript-side hooks yet, and we’re still evaluating what makes sense to add here. If you have a particular need for a hook, you can suggest one on the reviewboard-dev list.
You can also manually listen to events, set up UI, register handlers, etc. without using hooks. Anything you set up will be undone when the user closes or leaves the page. However, please note that JavaScript-side classes/events are subject to change, so please code defensively!
Page-Specific Extensions¶
You can specify that an extension should only load on one or more specific pages, or define different extensions for different pages. This is really useful when you want to augment the behavior of the review request, a review UI, etc., but don’t want to carry all that logic around to every page.
To do this, you’ll make use of the JSExtension.apply_to
attribute. This is a list
of URL names that the extension will be loaded on. See the Static Media guide
on Applying To Specific Pages for a list.
You should also put your extension in a bundle that will be loaded only for
those same pages, using the apply_to
key for the bundle.
Here’s an example that loads the extension only for diff viewer page and one custom URL for your extension:
from reviewboard.extensions.base import Extension, JSExtension
from reviewboard.urls import diffviewer_url_names
class SampleJSExtension(JSExtension):
model_class = 'SampleExtensionProject.Extension'
apply_to = diffviewer_url_names + [
'sample-extension-project-my-diff-url',
]
class SampleExtension(Extension):
js_extensions = [SampleJSExtension]
js_bundles = {
'diffviewer-extension': {
'source_filenames': (
'js/diffviewer-extension.js',
),
'apply_to': SampleJSExtension.apply_to,
}
}
Accessing Extension Data¶
JavaScript-side extensions are automatically instantiated with some information about the extension. There are a few Backbone.js attributes available for your extension interface:
id
:The ID of your extension (same as
MyExtensionClass.id
).name
:The name of your extension (see Defining Extension Metadata).
settings
:Settings stored for your extension (see Extension Settings).
You can also define custom data to pass (see Custom Model Data).
Extension Settings¶
By default, your JavaScript-side extension will receive all of your
extension’s settings. These are read-only, and will be accessible through your
settings
attribute on your extension’s instance.
Here’s an example of how extension settings can work:
extension.py
:class SampleExtension(Extension): default_settings = { 'feature_enabled': True, } ...
extension.js
:SampleExtensionProject.Extension = RB.Extension.extend({ initialize() { RB.Extension.prototype.initialize.call(this); if (this.get('settings').feature_enabled) { ... }); } });
Warning
You may not want all your settings to be passed onto the page. There might be some secret information (license keys, for instance) that you’d like to keep from the page. Remember that anything loaded onto the page is available for the user to see.
To provide only certain settings to your extension, or to normalize the
content for the page, you can override JSExtension.get_settings
. For example:
class SampleJSExtension(JSExtension):
...
def get_settings(self):
settings = self.extension.settings
return {
'setting1': settings.get('setting1'),
'setting2': settings.get('setting2'),
...
}
Custom Model Data¶
You can also define custom data on the Python side that will be passed to your
extension instance, separately from settings. This is useful when you want to
precompute some form of data to pass down, based on the state of the server or
of your Python-side extension. This can be done by overriding
JSExtension.get_model_data
.
class SampleJSExtension(JSExtension):
...
def get_model_data(self):
return {
'some_state': SampleExtension.calculate_some_state(),
}
Your JavaScript-side extension can then get access to this data using standard Backbone.js attribute accessors:
SampleExtensionProject.Extension = RB.Extension.extend({
initialize() {
let someState;
RB.Extension.prototype.initialize.call(this);
someState = this.get('some_state');
...
}
});
Supporting Read-Only Mode¶
Reviewboard can be put into read-only mode by the site administrator, which disables API requests to the server and associated front-end features. When the site is in read-only mode, only changes made to models by superusers will be propagated to the server; changes made by all other users will be discarded.
Whether a user is in read-only mode can be checked by looking up the
readOnly
property in the RB.UserSession()
instance.
if (RB.UserSession.instance.get('readOnly')) {
/* Put code to run when in read-only mode here. */
}