Writing Review Board Extensions¶
New in version 1.7: Many of the features described here are new in Review Board 1.7.
Overview¶
Review Board’s functionality can be enhanced by installing a Review Board extension. Writing and installing an extension is an excellent way to tailor Review Board to your exact needs. Here are a few examples of the many things you can accomplish by writing an extension:
- Modify the user interface, providing new links or buttons.
- Generate statistics for report gathering.
- Interface Review Board with other systems (e.g. an IRC bot).
Extension Structure¶
Extensions must follow a certain structure to be recognized and loaded by Review Board. They are distributed inside Python Eggs following a few conventions. See Python Egg for more information.
Note
The Review Board extension system has been designed to follow many of Django’s conventions. The structure of an extension package tries to mirror that of Django apps.
The main constituent of an extension is a class which inherits from the Extension base class reviewboard.extensions.base.Extension.
The Review Board repository contains a script for generating the initial code an extension requires. See Extension Boilerplate Generator for more information.
Minimum Extension Structure¶
At minimum, an extension requires the following files:
- setup.py
- extensiondir/__init__.py
- extensiondir/extension.py
The following are a description and example of each file. In each example extensiondir has been replaced with the extension’s package, ‘sample_extension’:
setup.py
This is the file used to create the Python Egg. It defines the Entry Point along with other meta-data. See Python Egg for a description of features relevant to Review Board extensions. Example:
from setuptools import setup PACKAGE = "sample_extension" VERSION = "0.1" setup( name=PACKAGE, version=VERSION, description="description of extension", author="Your Name", packages=["sample_extension"], entry_points={ 'reviewboard.extensions': '%s = sample_extension.extension:SampleExtension' % PACKAGE, }, )See Python Egg and the setuptools documentation for more information.
sample_extension/__init__.py
This file indicates the sample_extension is a Python package.
sample_extension/extension.py
This is the main module of the extension. The Extension subclass should be defined here. Example:
from reviewboard.extensions.base import Extension class SampleExtension(Extension): def __init__(self, *args, **kwargs): super(SampleExtension, self).__init__(*args, **kwargs)
Other Structure¶
Review Board also expects extensions to follow a few other conventions when naming files. The following files serve a special purpose:
- models.py
- An extension may define Django models in this file. The corresponding tables will be created in the database when the extension is loaded. See Models for more information.
- models/
- As an alternative to using models.py, a Python package may be created in models/, which may define models in its modules.
- admin_urls.py
- An extension may define urls for configuration in the admin panel. It is only used when is_configurable is set True. For more information, see Admin URLs.
- admin.py
- This file allows an extension to register models in its own Django admin site. It is only used when has_admin_site is set True. For more information, see extension-admin-sites.
Extension Class¶
The main component of an extension is a class inheriting from reviewboard.extensions.base.Extension. It can optionally set the following attributes:
- is_configurable
- has_admin_site
- default_settings
- requirements
The base class also provides the following attributes:
- settings
- class reviewboard.extensions.base.Extension¶
- is_configurable¶
A Boolean indicating whether the extension supports configuration in the Review Board admin panel. The default is False. See Configuration for more information.
- has_admin_site¶
A Boolean that indicates whether a Django admin site should be generated for the extension. The default is False. See Admin Site for more information.
- default_settings¶
A Dictionary which acts as a default for settings. The default is {}, an empty dictionary. See Default Settings for more information.
- requirements¶
A list of strings providing the names of other extensions the extension requires. An extension may only be enabled if all other extensions in its requirements list are also enabled. See Dependencies for more information.
- settings¶
An instance of djblets.extensions.base.settings. This attribute gives each extension an easy-to-use and persistent data store for settings. See Settings for more information.
Models¶
Extensions are able to define Django Models to expand the database schema. When an extension is loaded, it is added to INSTALLED_APPS automatically. New Models are then written to the database by Review Board, which runs syncdb programmatically.
Note
Review Board is also able to evolve the database programmatically. This allows a developer to make changes to an extension’s models after release.
Extensions use the same convention as Django applications when defining Models. In order to define new Models, a models.py file, or a models/ directory constituting a Python package should be created.
Here is an example models.py file:
from django.db import models
class MyExtensionsSampleModel(models.Model):
name = models.CharField(max_length=128)
enabled = models.BooleanField()
Note
When an extension is disabled, tables for its models are not dropped. For a development installation, an evolution to drop these tables may be generated using:
./reviewboard/manage.py evolve --purge
Alternativley, when developing against a Review Board install, rb-site may be used:
rb-site manage /path/to/site evolve -- --purge
Settings¶
Each extension is given a settings dictionary which it can load from the database using load() and save to the database using save(). This is found in the settings attribute and is an instance of the djblets.extensions.base.settings class.
A set of defaults may be provided in default_settings to make initialization of the dictionary simple. See Default Settings for more information.
- class djblets.extensions.base.settings¶
- load()¶
Retrieves the dictionary entries from the database.
- save()¶
Stores the dictionary entries to the database.
Here is an example of how to save settings:
settings['mysetting'] = "New Setting Value"
settings.save() # Store the settings in the database.
And an example of how to load settings:
settings.load() # Retrieve the settings from the database.
mysetting = settings['mysetting'] # Read the setting value.
Default Settings¶
To provide defaults for the settings dictionary, an extension may use the default_settings attribute. If a key is not found in settings, default_settings will be checked. If neither dictionary contains the key, a KeyError exception will be thrown.
Here is an example extension setting default_settings:
class SampleExtension(Extension):
default_settings = {
'mysetting': 1,
'anothersetting': 4,
'stringsetting': "I'm a string setting",
}
def __init__(self, *args, **kwargs):
super(SampleExtension, self).__init__(*args, **kwargs)
Configuration¶
For administrative configuration, extensions are able to hook into the Review Board admin panel.
By setting is_configurable to True, an extension is assigned a URL namespace under the admin path. New URLs are added to this namespace using an admin_urls.py file. See Admin URLs for more information.
Review Board also supplies views, templates, and forms, making management of Settings painless. See Settings Form for more information.
Admin URLs¶
If an extension has is_configurable set to True, it will be assigned a URL namespace under the admin path. A button labeled Configure will appear in the list of installed extensions, linking to the base path of this namespace.
To specify URLs in the namespace, an admin_urls.py file should be created, taking the form of a Django URLconf module. This module should follow Django’s conventions, defining a urlpatterns variable.
- urlpatterns¶
Used to specify URLs. This should be a Python list, in the format returned by the function django.conf.urls.patterns().
The following is an example admin_url.py file:
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('sample_extension.views',
url(r'^$', 'configure')
)
This would direct the base URL of the namespace to the configure view.
For a more in depth explanation of URLconfs please see the Django URLs documentation.
Settings Form¶
Review Board supplies the views, templates, and a base Django form to make creating a configuration UI for Settings painless. To take advantage of this feature, do the following:
- Define a new form class inheriting from djblets.extensions.forms.SettingsForm
- Create a new URL pattern to reviewboard.extensions.views.configure_extension, providing the extension class and form class. See Admin URLs for more information on creating URL patterns.
Here is an example form class:
from django import forms
from djblets.extensions.forms import SettingsForm
class SampleExtensionSettingsForm(SettingsForm):
field1 = forms.IntegerField(min_value=0, initial=1, help_text="Field 1")
Here is an example URL pattern for the form:
from django.conf.urls.defaults import patterns
from sample_extension.extension import SampleExtension
from sample_extension.forms import SampleExtensionSettingsForm
urlpatterns = patterns('',
(r'^$', 'reviewboard.extensions.views.configure_extension',
{'ext_class': SampleExtension,
'form_class': SampleExtensionSettingsForm,
}),
)
Admin Site¶
By setting has_admin_site to True, an extension will be given its own Django admin site. A button labeled Database will appear in the list of installed extensions, linking to the base path of the admin site.
The extension’s instance of django.contrib.admin.sites.AdminSite will exist in the admin_site attribute of the Extension.
Models should be registered to the Admin site in an admin.py file. Here is an example admin.py file:
from reviewboard.extensions.base import get_extension_manager
from sample_extension.extension import SampleExtension
from sample_extension.models import SampleModel
# You must get the loaded instance of the extension to register to the
# admin site.
extension_manager = get_extension_manager()
extension = extension_manager.get_enabled_extension(SampleExtension.id)
# Register the Model to the sample_extensions admin site.
extension.admin_site.register(SampleModel)
For more information on Django admin sites, please see the Django Admin Site documentation.
Useful Hooks¶
Extensions hooks are the primary mechanism for customizing Review Board’s appearance and behavior.
Hooks need only be instantiated for Review Board to “notice” them, and are automatically removed when the extension shuts down.
The following hooks are available for use by extensions.
URLHook¶
reviewboard.extensions.hooks.URLHook are used to extend the URL patterns that Review Board wil recognize and respond to.
URLHook requires two arguments for initialization: the extension instance and the URL patterns.
Example usage in an Extension:
class SampleExtension(Extension):
def __init__(self, *args, **kwargs):
super(SampleExtension, self).__init__(*args, **kwargs)
pattern = patterns('', (r'^sample_extension/',
include('sample_extension.urls')))
self.url_hook = URLHook(self, pattern)
Notice how sample_extension.urls was included in the patterns. In this case, sample_extension is the package name for the extension, and urls is the module that contains the patterns:
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('sample_extension.views',
url(r'^$', 'dashboard'),
)
DashboardHook¶
reviewboard.extensions.hooks.DashboardHook can be used to define a custom dashboard page for your Extension. DashboardHook requires two arguments for initialization: the extension instance and a list of entries. Each entry in this list must be a dictionary with the following keys:
- label: Label to appear on the dashboard’s navigation pane.
- url: URL for the dashboard page.
If the extension needs only one dashboard, then it needs only one entry in this list. (See NavigationBarHook)
Example usage in an Extension:
class SampleExtension(Extension):
def __init__(self, *args, **kwargs):
super(SampleExtension, self).__init__(*args, **kwargs)
self.dashboard_hook = DashboardHook(
self,
entries = [
{
'label': 'A SampleExtension Label',
'url': settings.SITE_ROOT + 'sample_extension/',
}
]
)
Corresponding code in views.py:
def dashboard(request, template_name='sample_extension/dashboard.html'):
return render_to_response(template_name, RequestContext(request))
Corresponding template dashboard.html:
{% extends "base.html" %}
{% load djblets_deco %}
{% load i18n %}
{% block title %}{% trans "sample_extension Dashboard" %}{% endblock %}
{% block content %}
{% box "reports" %}
<h1 class="title">{% trans "sample_extension Dashboard" %}</h1>
<div class="main">
<p>{% trans "This is my new Dashboard page for Review Board" %}</p>
</div>
{% endbox %}
{% endblock %}
Review UI Integration¶
Review UIs are used in reviewing file attachments of particular mimetypes. For example, an Image Review UI is used to render image files and allow comments to be attached to specific areas of an image. Similarly, a Markdown Review UI renders the raw text from a .md file into a corresponding HTML.
Extensions can integrate custom Review UIs into Review Board by defining a hook that subclasses ReviewUIHook. Each extension may define and register zero or more Review UIs. When the extension is enabled through the admin page, the hook registers its list of Review UIs. Likewise, the hook unregisters these Review UIs when the extension is disabled.
We use a simple XMLReviewUI that performs syntax highlighting as an example to demonstrate the key anatomical points for integrating ReviewUIs through extensions.
Subclassing ReviewUIHook¶
extension.py must use a Review UI Hook to register its list of Review UIs. This can be using reviewboard.extensions.hooks.ReviewUIHook directly, using a subclass of it. ReviewUIHook expects a list of Review UIs as argument in addition to the extension instance.
Example: XMLReviewUIExtension:
class XMLReviewUIExtension(Extension):
def __init__(self, *args, **kwargs):
super(XMLReviewUIExtension, self).__init__(*args, **kwargs)
self.reviewui_hook = ReviewUIHook(self, [XMLReviewUI])
ReviewUI Class¶
Each Review UI must be defined by its own ReviewUI class that subclasses reviewboard.reviews.ui.base.FileAttachmentReviewUI. It must also define the following class variables:
- supported_mimetypes: a list of mimetypes of the files that this Review UI will be responsible for rendering.
- template_name: where to find the html template used when rendering this Review UI
- object_key: a unique name to identify this Review UI
Example: XMLReviewUI:
import logging
from django.utils.encoding import force_unicode
import pygments
from reviewboard.reviews.ui.base import FileAttachmentReviewUI
class XMLReviewUI(FileAttachmentReviewUI):
"""ReviewUI for XML mimetypes"""
supported_mimetypes = ['application/xml', 'text/xml']
template_name = 'xml_review_ui_extension/xml.html'
object_key = 'xml'
The class should also have some function to render the particular mimetype(s) that it is responsible for. There are no restrictions on the name of the function or what it returns, but it should be in agreement with logic specified in its corresponding template.
Example: render() in XMLReviewUI. This simply uses the pygments API to convert raw XML into syntax-highlighted HTML:
def render(self):
data_string = ""
f = self.obj.file
try:
f.open()
data_string = f.read()
except (ValueError, IOError), e:
logging.error('Failed to read from file %s: %s' % (self.obj.pk, e))
f.close()
return pygments.highlight(
force_unicode(data_string),
pygments.lexers.XmlLexer(),
pygments.formatters.HtmlFormatter())
ReviewUI Template¶
Here is the template that corresponds to the above Review UI:
xml_review_ui_extension/templates/xml_review_ui_extension/xml.html:
{% extends base_template %}
{% load i18n %}
{% load reviewtags %}
{% block title %}{{xml.filename}}{% if caption %}: {{caption}}
{% endif %}{% endblock %}
{% block scripts-post %}
{{block.super}}
<script language="javascript"
src="{{MEDIA_URL}}ext/xml-review-ui-extension/js/XMLReviewableModel.js">
</script>
<script language="javascript"
src="{{MEDIA_URL}}ext/xml-review-ui-extension/js/XMLReviewableView.js">
</script>
<script language="javascript">
$(document).ready(function() {
var view = new RB.XMLReviewableView({
model: new RB.XMLReviewable({
attachmentID: '{{xml.id}}',
caption: '{{caption|escapejs}}',
rendered: '{{review_ui.render|escapejs}}'
})
});
view.render();
$('#xml-review-ui-container').append(view.$el);
});
</script>
{% endblock %}
{% block review_ui_content %}
<div id="xml-review-ui-container"></div>
{% endblock %}
ReviewUI JavaScript¶
Here are the corresponding JavaScript used in the above template.
xml_review_ui_extension/htdocs/js/XMLReviewableModel.js:
/*
* Provides review capabilities for XML files.
*/
RB.XMLReviewable = RB.FileAttachmentReviewable.extend({
defaults: _.defaults({
rendered: ''
}, RB.FileAttachmentReviewable.prototype.defaults)
});
xml_review_ui_extension/htdocs/js/XMLReviewableView.js:
/*
* Displays a review UI for XML files.
*/
RB.XMLReviewableView = RB.FileAttachmentReviewableView.extend({
className: 'xml-review-ui',
/*
* Renders the view.
*/
renderContent: function() {
this.$el.html(this.model.get('rendered'));
return this;
}
});
Python Egg¶
Extensions are packaged and distributed as Python Eggs. This allows for automatic detection of installed extensions, packaging of static files, and dependency checking.
The setuptools module is used to create a Python Egg. A setup.py file is created for this purpose. See the setuptools documentation for a full description of features.
Entry Point¶
To facilitate the auto-detection of installed extensions, a reviewboard.extensions entry point must be defined for each Extension Class. Here is an example entry point definition:
entry_points={
'reviewboard.extensions':
'sample_extension = sample_extension.extension:SampleExtension',
},
This defines an entry point for the SampleExtension class from the sample_extension.extension module. Here is an example of a full setup.py file defining this entry point:
from setuptools import setup
PACKAGE = "sample_extension"
VERSION = "0.1"
setup(
name=PACKAGE,
version=VERSION,
description="Description of extension",
author="Your Name",
packages=["sample_extension"],
entry_points={
'reviewboard.extensions':
'sample_extension = sample_extension.extension:SampleExtension',
},
)
Static Files¶
Any static files (such as css, html, and javascript) the extension requires must be added to the package data of the Python Egg. Here is an example of how this is done in the setup.py file:
package_data={
'sample_extension': [
'htdocs/css/*.css',
'htdocs/js/*.js',
'templates/rbreports/*.html',
'templates/rbreports/*.txt',
],
}
Here is an example of a full setup.py file including the static files:
from setuptools import setup
PACKAGE = "sample_extension"
VERSION = "0.1"
setup(
name=PACKAGE,
version=VERSION,
description="Description of extension",
author="Your Name",
packages=["sample_extension"],
entry_points={
'reviewboard.extensions':
'sample_extension = sample_extension.extension:SampleExtension',
},
package_data={
'sample_extension': [
'htdocs/css/*.css',
'htdocs/js/*.js',
'templates/rbreports/*.html',
'templates/rbreports/*.txt',
],
}
)
Dependencies¶
Any dependencies of the extension are defined in the setup.py file using install_requires. Here is an example of a full :file`setup.py` file including a dependency:
from setuptools import setup
PACKAGE = "sample_extension"
VERSION = "0.1"
setup(
name=PACKAGE,
version=VERSION,
description="Description of extension",
author="Your Name",
packages=["sample_extension"],
entry_points={
'reviewboard.extensions':
'sample_extension = sample_extension.extension:SampleExtension',
},
install_requires=['PythonPackageIDependOn>=0.1']
)
This will ensure any packages the extension requires will be installed. See the Setuptools documentation for more information on install_requires.
In addition to requiring python packages when installing, an extension can declare a list of additional extensions it requires. This requirements list gives the name of each extension that must be enabled before allowing the extension itself to be enabled. This list is declared by setting the requirements attribute. Here is an example of an extension defining a requirements list:
class SampleExtension(Extension):
requirements = ['other_extension.extension.OtherExtension']
def __init__(self, *args, **kwargs):
super(RBWebHooksExtension, self).__init__(*args, **kwargs)
Developing With a Python Egg¶
In order for Review Board to detect an extension, the Python Egg must be generated using the setup.py file, and installed. During development this can be done by installing a link in the Python installation to the source directory of your extension. This is accomplished by running:
python setup.py develop
If changes are made to the setup.py file this should be executed again.
See the Setuptools documentation for more information.
Extension Boilerplate Generator¶
The Review Board repository contains a script for generating the boilerplate code for a new extension. This script is part of the Review Board tree and is located here:
./contrib/tools/generate_extension.py