A Mezzanine package that allows you to add buttons and menus to the richtext editor by simply decorating a python function.
This package aims to fulfill the same needs as Wordpress's shortcodes but with the following advantages:
- Fallback to removing placeholder so if something goes wrong it is not shown to end-users. I've seen shortcode literals accidentally displayed on Wordpress sites too many times, though often due to syntax errors which we avoid by...
- Integration with the richtext editor so users have a seamless experience that doesn't involve learning syntax. CMS users shouldn't be writing any code -- long or short.
A mezzaine-shortcode is just a python function paired with a ModelForm
. The ModelForm
allows users to create content to insert into the page, while the function behaves similar to a Django view, with the following substitutions:
- Accepts a
ModelForm
instance as its sole argument rather than anHttpRequest
. - Returns a safe string of html rather than an
HttpResponse
.
- Python 3.x only. It wouldn't be too difficult to make this backwards compatible but it currently is not.
- I've only tested with Mezzanine 4.x, but I don't know of any reason it wouldn't work with older versions.
Pip install this package:
pip install mezzanine-shortcodes
Now make the following changes in your project:
settings.py
######################
# MEZZANINE SETTINGS #
######################
RICHTEXT_WIDGET_CLASS = 'shortcodes.forms.TinyMceWidget'
# This static file can be anywhere you please but you must define it.
TINYMCE_SETUP_JS = 'js/tinymce_setup.js'
################
# APPLICATIONS #
################
INSTALLED_APPS = (
...
'shortcodes',
)
urls.py
urlpatterns = [
...
# MEZZANINE-SHORTCODE'S URLS
# --------------------------
url("^shortcodes/", include('shortcodes.urls')),
...
# MEZZANINE'S URLS
# ----------------
# ADD YOUR OWN URLPATTERNS *ABOVE* THE LINE BELOW.
# ``mezzanine.urls`` INCLUDES A *CATCH ALL* PATTERN
# FOR PAGES, SO URLPATTERNS ADDED BELOW ``mezzanine.urls``
# WILL NEVER BE MATCHED!
]
tinymce_setup.js
- Add
shortcodes
toplugins
. - Add whatever menus or buttons you've created to
toolbar
. - Add
/static/shortcodes/tinymce/style.css
tocontent_css
array. - Set
valid_elements
to*[*]
to allow all. - Add
shortcode
tocontextmenu
.
tinyMCE.init({
...
plugins: [
"advlist autolink lists link image charmap print preview anchor",
"searchreplace visualblocks code fullscreen",
"insertdatetime media table contextmenu paste shortcodes"
],
toolbar: ("insertfile undo redo | styleselect | bold italic | " +
"alignleft aligncenter alignright alignjustify | " +
"bullist numlist outdent indent | link image table | " +
"example_menu example_button | code fullscreen"),
content_css: [window.__tinymce_css, '/static/shortcodes/tinymce/style.css'],
valid_elements: "*[*]",
contextmenu: "shortcode | link image inserttable | cell row column deletetable"
});
Add your shortcode definitions to a shortcodes.py
module in any installed app.
All buttons/menubuttons must have unique names (__name__
). All ModelForm
's must have unique verbose_name
's and a ModelForm
cannot be associated with multiple shortcodes.
Buttons are created with the button
decorator, which takes the following parameters:
modelform
(required): Reference to aModelForm
.icon
(optional): The string path to an image file starting from the static url. 'Free' buttons cannot display both a name and an icon, soverbose_name
is not shown if this is defined.tooltip
(optional): The string displayed on mouseover.
from django.utils.safestring import mark_safe
@shortcodes.button(
MyModelForm
icon='path/to/image.png',
tooltip='Click me.')
def my_button(instance):
return mark_safe('<div>Some html string.</div>')
In some cases, it may be simpler to instantiate a button with GenericButton
, which takes the following parameters:
name
(required): The identifying name that you'll pass into the tinymce toolbar.modelform
(required): Reference to aModelForm
.template
(required): A string template name which will be rendered with the associated model instance's fields in the context.- ... same kwargs as regular Buttons
shortcodes.GenericButton('my_button', MyModelForm, 'some_template.html')
Menus are just dropdown collections of buttons. They inherit from shortcodes.Menu
and have the following optional class attributes:
displayname
: The string to display in the toolbar.tooltip
: The string displayed on mouseover.
Menubuttons are registered with the shortcodes.menubutton
decorator, which takes the same arguments as regular buttons.
from django.utils.html import format_html
class SomeMenu(shortcodes.Menu):
displayname='Some Menu'
tooltip='Input your stuff'
@shortcodes.menubutton(MyModelForm)
def some_menubutton(instance):
return format_html('<p>Hello {name}!</p>', name=instance.name)
Or with shortcodes.GenericMenubutton
which behaves identically to regular generic buttons except it's in class scope:
class SomeMenu(shortcodes.Menu):
shortcodes.GenericMenubutton(
'some_menubutton', MyModelForm, 'some_template.html')
Mezzanine-shortcodes is designed for a one-to-one relationship between model instances and references to them in page content. This app even auto-cleans dereferenced model instances so you only have to interact with them through the shortcode forms. In this sense, even though you're defining the models, mezzanine-shortcodes "owns" them. A few concrete pieces of advice follow from this:
- Do not register your modelform in the admin. This adds the temptation to add or delete models directly which will lead to either dangling references or dangling model instances.
- Use dedicated models. Existing models are presumably being consumed elsewhere and you don't want to give them over to mezzanine-shortcodes, by which you could easily dereference and accidentally delete them. Rather, use relation fields to those existing models.
- If you think you need a one-to-many relationship you presumably have content you want to avoid duplicating and keep synchronized. Make a separate model to manage this content and a simple shortcode model with a relation field to it.
- As Django starts up and your apps are initialized, your decorated shortcodes are registered.
- When staff users edit a richtext page:
- Metadata about your shortcodes is injected into the page by a Django view and rendered into menus/buttons by javascript.
- When a button is clicked, its
ModelForm
is rendered into a dialog. When submitted, a placeholder html element is added to store a reference to theModelForm
and theModelForm
instance. - When the page is saved, the
ModelForm
instance is saved and it's reference is replaced with a reference to the primary key of the instance.
- When users view a richtext page, the placeholders are parsed, the
ModelForm
instances retrieved and passed into their associated function, and the placeholders are replaced with the return value.
This will give you an editable installation.
python setup.py develop
cd example_project
python manage.py createdb --noinput
python manage.py runserver
Then go to 127.0.0.1:8000/admin
, log in with admin
/ default
, and edit a Page to see the extra toolbar menus/buttons.
- To run browser tests, install python dependencies with
pip install -r dev-requirements.txt
. - To run browser tests headless, install phantomjs on your system. A
ghostdriver.log
file is created (and deleted after every TestCase) which may be useful for debugging these, though running them again with firefox is generally easier.
python test [--debug] [<webdriver>]
- --debug Write verbose output to ghostdriver.log.
- <webdriver> [phantomjs|firefox|chrome] If ommitted the browser tests will default to phantomjs and fall back to firefox if unavailable.