.. _custom-plugins:
How to create Plugins
=====================
The simplest plugin
-------------------
We'll start with an example of a very simple plugin.
You may use ``python -m manage startapp`` to set up the basic layout for your plugin app
(remember to add your plugin to ``INSTALLED_APPS``). Alternatively, just add a file
called ``cms_plugins.py`` to an existing Django application.
Place your plugins in ``cms_plugins.py``. For our example, include the following code:
.. code-block::
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from cms.models.pluginmodel import CMSPlugin
from django.utils.translation import gettext_lazy as _
@plugin_pool.register_plugin
class HelloPlugin(CMSPluginBase):
model = CMSPlugin
render_template = "hello_plugin.html"
cache = False
Now we're almost done. All that's left is to add the template. Add the following into
the root template directory in a file called ``hello_plugin.html``:
.. code-block:: html+django
This plugin will now greet the users on your website either by their name if they're
logged in, or as Guest if they're not.
Now let's take a closer look at what we did there. The ``cms_plugins.py`` files are
where you should define your sub-classes of :class:`cms.plugin_base.CMSPluginBase`,
these classes define the different plugins.
There are two required attributes on those classes:
- ``model``: The model you wish to use for storing information about this plugin. If you
do not require any special information, for example configuration, to be stored for
your plugins, you can simply use :class:`cms.models.pluginmodel.CMSPlugin` (we'll look
at that model more closely in a bit). In a normal admin class, you don't need to
supply this information because ``admin.site.register(Model, Admin)`` takes care of
it, but a plugin is not registered in that way.
- ``name``: The name of your plugin as displayed in the admin. It is generally good
practice to mark this string as translatable using
:func:`django.utils.translation.gettext_lazy`, however this is optional. By default
the name is a nicer version of the class name.
And one of the following **must** be defined if ``render_plugin`` attribute is ``True``
(the default):
- ``render_template``: The template to render this plugin with.
**or**
- ``get_render_template``: A method that returns a template path to render the plugin
with.
In addition to those attributes, you can also override the
:meth:`~cms.plugin_base.CMSPluginBase.render()` method which determines the template
context variables that are used to render your plugin. By default, this method only adds
``instance`` and ``placeholder`` objects to your context, but plugins can override this
to include any context that is required.
A number of other methods are available for overriding on your CMSPluginBase
sub-classes. See: :class:`~cms.plugin_base.CMSPluginBase` for further details.
Troubleshooting
---------------
Since plugin modules are found and loaded by django's importlib, you might experience
errors because the path environment is different at runtime. If your `cms_plugins` isn't
loaded or accessible, try the following:
.. code-block::
$ python -m manage shell
>>> from importlib import import_module
>>> m = import_module("myapp.cms_plugins")
>>> m.some_test_function() # from the myapp.cms_plugins module
.. _storing configuration:
Storing configuration
---------------------
In many cases, you want to store configuration for your plugin instances. For example,
if you have a plugin that shows the latest blog posts, you might want to be able to
choose the amount of entries shown. Another example would be a gallery plugin where you
want to choose the pictures to show for the plugin.
To do so, you create a Django model by sub-classing
:class:`cms.models.pluginmodel.CMSPlugin` in the ``models.py`` of an installed
application.
Let's improve our ``HelloPlugin`` from above by making its fallback name for
non-authenticated users configurable.
In our ``models.py`` we add the following:
.. code-block::
from cms.models.pluginmodel import CMSPlugin
from django.db import models
class Hello(CMSPlugin):
guest_name = models.CharField(max_length=50, default='Guest')
If you followed the Django tutorial, this shouldn't look too new to you. The only
difference to normal models is that you sub-class
:class:`cms.models.pluginmodel.CMSPlugin` rather than :class:`django.db.models.Model`.
Now we need to change our plugin definition to use this model, so our new
``cms_plugins.py`` looks like this:
.. code-block::
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from django.utils.translation import gettext_lazy as _
from .models import Hello
@plugin_pool.register_plugin
class HelloPlugin(CMSPluginBase):
model = Hello
name = _("Hello Plugin")
render_template = "hello_plugin.html"
cache = False
def render(self, context, instance, placeholder):
context = super().render(context, instance, placeholder)
return context
We changed the ``model`` attribute to point to our newly created ``Hello`` model and
pass the model instance to the context.
As a last step, we have to update our template to make use of this new configuration:
.. code-block:: html+django
The only thing we changed there is that we use the template variable ``{{
instance.guest_name }}`` instead of the hard-coded ``Guest`` string in the else clause.
.. warning::
You cannot name your model fields the same as any installed plugins lower- cased
model name, due to the implicit one-to-one relation Django uses for sub-classed
models. If you use all core plugins, this includes: ``file``, ``googlemap``,
``link``, ``picture``, ``snippetptr``, ``teaser``, ``twittersearch``,
``twitterrecententries`` and ``video``.
Additionally, it is *recommended* that you avoid using ``page`` as a model field, as
it is declared as a property of :class:`cms.models.pluginmodel.CMSPlugin`. While the
use of ``CMSPlugin.page`` is deprecated the property still exists as a compatibility
shim.
.. _handling-relations:
Handling Relations
~~~~~~~~~~~~~~~~~~
Some user interactions make it necessary to create a copy of the plugin, most notably if
a user copies and pastes contents of a placeholder. So if your custom plugin has foreign
key (to it, or from it) or many-to-many relations you are responsible for copying those
related objects, if required, whenever the CMS copies the plugin - **it won't do it for
you automatically**.
Every plugin model inherits the empty
:meth:`cms.models.pluginmodel.CMSPlugin.copy_relations` method from the base class, and
it's called when your plugin is copied. So, it's there for you to adapt to your purposes
as required.
Typically, you will want it to copy related objects. To do this you should create a
method called ``copy_relations`` on your plugin model, that receives the **old
instance** of the plugin as an argument.
You may however decide that the related objects shouldn't be copied - you may want to
leave them alone, for example. Or, you might even want to choose some altogether
different relations for it, or to create new ones when it's copied... it depends on your
plugin and the way you want it to work.
If you do want to copy related objects, you'll need to do this in two slightly different
ways, depending on whether your plugin has relations *to* or *from* other objects that
need to be copied too:
For foreign key relations *from* other objects
++++++++++++++++++++++++++++++++++++++++++++++
Your plugin may have items with foreign keys to it, which will typically be the case if
you set it up so that they are inlines in its admin. So you might have two models, one
for the plugin and one for those items:
.. code-block::
class ArticlePluginModel(CMSPlugin):
title = models.CharField(max_length=50)
class AssociatedItem(models.Model):
plugin = models.ForeignKey(
ArticlePluginModel,
related_name="associated_item"
)
You'll then need the ``copy_relations()`` method on your plugin model to loop over the
associated items and copy them, giving the copies foreign keys to the new plugin:
.. code-block::
class ArticlePluginModel(CMSPlugin):
title = models.CharField(max_length=50)
def copy_relations(self, oldinstance):
# Before copying related objects from the old instance, the ones
# on the current one need to be deleted. Otherwise, duplicates may
# appear on the public version of the page
self.associated_item.all().delete()
for associated_item in oldinstance.associated_item.all():
# instance.pk = None; instance.pk.save() is the slightly odd but
# standard Django way of copying a saved model instance
associated_item.pk = None
associated_item.plugin = self
associated_item.save()
For many-to-many or foreign key relations *to* other objects
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Let's assume these are the relevant bits of your plugin:
.. code-block::
class ArticlePluginModel(CMSPlugin):
title = models.CharField(max_length=50)
sections = models.ManyToManyField(Section)
Now when the plugin gets copied, you want to make sure the sections stay, so it becomes:
.. code-block::
class ArticlePluginModel(CMSPlugin):
title = models.CharField(max_length=50)
sections = models.ManyToManyField(Section)
def copy_relations(self, oldinstance):
self.sections.set(oldinstance.sections.all())
If your plugins have relational fields of both kinds, you may of course need to use
*both* the copying techniques described above.
Relations *between* plugins
+++++++++++++++++++++++++++
It is much harder to manage the copying of relations when they are from one plugin to
another.
See the GitHub issue `copy_relations() does not work for relations between cmsplugins
#4143 `_ for more details.
Adding a model to an existing custom plugin
-------------------------------------------
When enhancing an existing django CMS plugin with additional functionality, you might need to
associate a database model with the plugin. This allows the plugin to store and manage data
persistently. However, introducing a model to an existing plugin requires careful handling to
prevent the disappearance of existing plugin instances, as discussed in `Issue #7476`_.
1. **Define the Model**:
In your application's `models.py`, define a model that inherits from `CMSPlugin`. This model
will store the plugin's data. To allow for automatic migration later, make sure that all model
fields have meaningful defaults.
.. code-block:: python
from django.db import models
from cms.models.pluginmodel import CMSPlugin
class MyPluginModel(CMSPlugin):
title = models.CharField(max_length=100, default='Default Title') # Add defaults
# Add other fields as needed
def __str__(self):
return self.title
2. **Update the Plugin Class**:
In your `cms_plugins.py`, associate the plugin with the newly created model by setting the `model` attribute.
.. code-block:: python
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from django.utils.translation import gettext_lazy as _
from .models import MyPluginModel
@plugin_pool.register_plugin
class MyPlugin(CMSPluginBase):
model = MyPluginModel
name = _("My Plugin")
render_template = "my_app/my_plugin_template.html"
# Configure other attributes as needed
3. **Create Migrations**:
Generate the necessary database migrations to reflect the new model. Do not apply them yet, since
the migrations will need an additional script to run.
.. code-block:: bash
python manage.py makemigrations
4. **Handle Existing Plugin Instances**:
After adding a model to an existing plugin, existing instances will not automatically
associate with the new model, leading to their disappearance in the CMS interface. To address
this, use a migration script to convert existing plugin instances to the new model.
This script can be added to the migration just created in step 3.
.. code-block:: python
def add_model_to_plugin(apps, schema_editor):
""" Adds instances for the new model.
ATTENTION: All fields of the model must have a valid default value!"""
# Adjust the following two lines
model = app.get_model("my_app", "MyGreatPluginModel") # Name of the plugin's new model class
plugin_type = "MyGreatPlugin" # Name of the plugin class
CMSPlugin = apps.get_model("cms", "CMSPlugin")
plugin_instances = CMSPlugin.objects.filter(plugin_type=plugin_type)
for plugin_instance in plugin_instances:
logger.info('Creating new model instance for plugin instance %s', plugin_instance.pk)
obj = model()
obj.pk = plugin_instance.pk
obj.cmsplugin_ptr = plugin_instance
obj.plugin_type = plugin_type
obj.placeholder = plugin_instance.placeholder
obj.parent = plugin_instance.parent
obj.language = plugin_instance.language
obj.position = plugin_instance.position
obj.creation_date = plugin_instance.creation_date
obj.save()
class Migration(migrations.Migration):
...
operations = [
...,
migrations.RunPython(add_model_to_plugin), # Add at the end of migration operations
]
This script migrates existing plugin instances to the new model structure, preserving data integrity.
5. **Run the migrations**:
Apply the modified migration.
.. code-block:: bash
python manage.py makemigrations
After applying migrations, thoroughly test the plugin to ensure that existing instances appear correctly
and that the new model functionalities work as intended.
.. _Issue #7476: https://github.com/django-cms/django-cms/issues/7476
Advanced
--------
Inline Admin
~~~~~~~~~~~~
If you want to have the foreign key relation as a inline admin, you can create an
``admin.StackedInline`` class and put it in the Plugin to "inlines". Then you can use
the inline admin form for your foreign key references:
.. code-block::
class ItemInlineAdmin(admin.StackedInline):
model = AssociatedItem
class ArticlePlugin(CMSPluginBase):
model = ArticlePluginModel
name = _("Article Plugin")
render_template = "article/index.html"
inlines = (ItemInlineAdmin,)
def render(self, context, instance, placeholder):
context = super().render(context, instance, placeholder)
items = instance.associated_item.all()
context.update({
'items': items,
})
return context
Plugin form
~~~~~~~~~~~
Since :class:`cms.plugin_base.CMSPluginBase` extends
:class:`django:django.contrib.admin.ModelAdmin`, you can customise the form for your
plugins just as you would customise your admin interfaces.
The template that the plugin editing mechanism uses is
``cms/templates/admin/cms/page/plugin/change_form.html``. You might need to change this.
If you want to customise this the best way to do it is:
- create a template of your own that extends
``cms/templates/admin/cms/page/plugin/change_form.html`` to provide the functionality
you require;
- provide your :class:`cms.plugin_base.CMSPluginBase` sub-class with a
``change_form_template`` attribute pointing at your new template.
Extending ``admin/cms/page/plugin/change_form.html`` ensures that you'll keep a unified
look and functionality across your plugins.
There are various reasons *why* you might want to do this. For example, you might have a
snippet of JavaScript that needs to refer to a template variable), which you'd likely
place in ``{% block extrahead %}``, after a ``{{ block.super }}`` to inherit the
existing items that were in the parent template.
.. _custom-plugins-handling-media:
Handling media
~~~~~~~~~~~~~~
If your plugin depends on certain media files, JavaScript or stylesheets, you can
include them from your plugin template using django-sekizai_. Your CMS templates are
always enforced to have the ``css`` and ``js`` sekizai namespaces, therefore those
should be used to include the respective files. For more information about
django-sekizai, please refer to the `django-sekizai documentation`_.
Note that sekizai **can't** help you with the **admin-side** plugin templates - what
follows is for your plugins' **output templates**.
Inline JavaScrip code
+++++++++++++++++++++
django CMS does not enforce a specific way to include JavaScript code in your plugins.
Inline JavaScript code is a potential security risk, so it is recommended to avoid it
where possible. Since version 4.2 django CMS itself has removed any inline JavaScript
from its code base to allow for meaningful Content Security Policy (CSP) headers to be
set. **It is good practice to avoid inline JavaScript in your plugins as well.**
If your project's CSP policy does not allow inline JavaScript, inline JavaScript
provided through Sekizai also will not be executed.
Sekizai style
+++++++++++++
To fully harness the power of django-sekizai, it is helpful to have a consistent style
on how to use it. Here is a set of conventions that should be followed (but don't
necessarily need to be):
- One bit per ``addtoblock``. Always include one external CSS or JS file per
``addtoblock`` or one snippet per ``addtoblock``. This is needed so django-sekizai
properly detects duplicate files.
- External files should be on one line, with no spaces or newlines between the
``addtoblock`` tag and the HTML tags.
- When using embedded javascript or CSS, the HTML tags should be on a newline.
A **good** example:
.. code-block:: html+django
{% load sekizai_tags %}
{% addtoblock "js" %}{% endaddtoblock %}
{% addtoblock "js" %}{% endaddtoblock %}
{% addtoblock "css" %}{% endaddtoblock %}
{% addtoblock "js" %}
{% endaddtoblock %}
A **bad** example:
.. code-block:: html+django
{% load sekizai_tags %}
{% addtoblock "js" %}
{% endaddtoblock %}
{% addtoblock "css" %}
{% endaddtoblock %}
{% addtoblock "js" %}{% endaddtoblock %}
.. note::
Due to the faster way of updating content in edit mode, ``