Как создавать плагины¶
Простейший плагин¶
Мы начнем с примера очень простого плагина.
Вы можете использовать python manage.py startapp
для установки базового макета для вашего приложения плагина (не забудьте добавить ваш плагин в INSTALLED_APPS
). В качестве альтернативы, просто добавьте файл с именем cms_plugins.py
к существующему приложению Django.
Поместите ваши плагины в cms_plugins.py
. Для нашего примера включите следующий код:
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
Теперь мы почти закончили. Осталось только добавить шаблон. Добавьте следующее в корневой каталог шаблонов в файл с именем hello_plugin.html
:
<h1>Hello {% if request.user.is_authenticated %}{{ request.user.first_name }} {{ request.user.last_name}}{% else %}Guest{% endif %}</h1>
Этот плагин теперь будет приветствовать пользователей на вашем сайте либо по имени, если они вошли в систему, либо как Гость, если они не вошли.
Теперь давайте рассмотрим подробнее, что мы там сделали. Файлы cms_plugins.py
являются тем местом, где вы должны определить ваши подклассы cms.plugin_base.CMSPluginBase
, эти классы определяют различные плагины.
У этих классов есть два обязательных атрибута:
model
: Модель, которую вы хотите использовать для хранения информации об этом плагине. Если вам не требуется хранить какую-либо специальную информацию, например, конфигурацию, для ваших плагинов, вы можете просто использоватьcms.models.pluginmodel.CMSPlugin
(мы рассмотрим эту модель подробнее чуть позже). В обычном классе администратора вам не нужно предоставлять эту информацию, потому чтоadmin.site.register(Model, Admin)
позаботится об этом, но плагин не регистрируется таким образом.name
: Название вашего плагина, отображаемое в админке. Обычно хорошей практикой является пометить эту строку как переводимую с помощьюdjango.utils.translation.gettext_lazy()
, однако это необязательно. По умолчанию имя является более красивой версией имени класса.
И один из следующих должен быть определен, если render_plugin
атрибут True
(по умолчанию):
render_template
: Шаблон для рендеринга этого плагина.
ор
get_render_template
: Метод, который возвращает путь к шаблону для рендеринга плагина.
В дополнение к этим атрибутам, вы также можете переопределить метод render()
, который определяет переменные контекста шаблона, используемые для рендеринга вашего плагина. По умолчанию этот метод добавляет в ваш контекст только объекты instance
и placeholder
, но плагины могут переопределить его, чтобы включить любой контекст, который требуется.
Ряд других методов доступен для переопределения в подклассах CMSPluginBase. См: CMSPluginBase
для более подробной информации.
Устранение неполадок¶
Поскольку модули плагинов находятся и загружаются с помощью библиотеки импорта django, вы можете столкнуться с ошибками, поскольку во время выполнения окружение путей отличается. Если ваш cms_plugins не загружен или недоступен, попробуйте сделать следующее:
$ python manage.py shell
>>> from importlib import import_module
>>> m = import_module("myapp.cms_plugins")
>>> m.some_test_function()
Хранение конфигурации¶
Во многих случаях вы хотите хранить конфигурацию для экземпляров плагина. Например, если у вас есть плагин, который показывает последние записи в блоге, вы можете захотеть иметь возможность выбирать количество отображаемых записей. Другим примером может быть плагин для галереи, где вы хотите выбрать изображения, которые будут отображаться в плагине.
Для этого вы создаете модель Django путем подклассификации cms.models.pluginmodel.CMSPlugin
в models.py
установленного приложения.
Давайте усовершенствуем наш HelloPlugin
, сделав его имя отката для неаутентифицированных пользователей настраиваемым.
В нашем models.py
мы добавляем следующее:
from cms.models.pluginmodel import CMSPlugin
from django.db import models
class Hello(CMSPlugin):
guest_name = models.CharField(max_length=50, default='Guest')
Если вы следовали учебнику Django, это не должно показаться вам слишком новым. Единственное отличие от обычных моделей заключается в том, что вы создаете подкласс cms.models.pluginmodel.CMSPlugin
, а не django.db.models.Model
.
Теперь нам нужно изменить определение нашего плагина, чтобы использовать эту модель, поэтому наш новый cms_plugins.py
выглядит следующим образом:
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
Мы изменили атрибут model
, чтобы он указывал на нашу только что созданную модель Hello
и передали экземпляр модели в контекст.
В качестве последнего шага мы должны обновить наш шаблон, чтобы использовать эту новую конфигурацию:
<h1>Hello {% if request.user.is_authenticated %}
{{ request.user.first_name }} {{ request.user.last_name}}
{% else %}
{{ instance.guest_name }}
{% endif %}</h1>
Единственное, что мы там изменили, это то, что мы используем шаблонную переменную {{ instance.guest_name }}
вместо жестко закодированной строки Guest
в предложении else.
Предупреждение
Вы не можете называть поля вашей модели так же, как имена установленных плагинов, используя нижний регистр, из-за неявного отношения один-к-одному, которое Django использует для подклассифицированных моделей. Если вы используете все основные плагины, это включает: file
, googlemap
, link
, picture
, snippetptr
, teaser
, twittersearch
, twitterrecententries
и video
.
Кроме того, рекомендуется избегать использования page
в качестве поля модели, так как оно объявлено как свойство cms.models.pluginmodel.CMSPlugin
, и ваш плагин не будет работать как задумано в админке без дополнительной работы.
Предупреждение
Если вы используете Python 2.x и переопределяете метод __unicode__
из файла модели, убедитесь, что его результаты возвращаются в виде UTF8-строки. В противном случае сохранение экземпляра вашего плагина может закончиться неудачей, и во внешнем редакторе будет отображаться <пустой> экземпляр плагина. Для возврата в Unicode используйте оператор возврата типа return u'{0}'.format(self.guest_name)
.
Работа с отношениями¶
Каждый раз, когда страница с вашим пользовательским плагином публикуется, плагин копируется. Поэтому, если ваш пользовательский плагин имеет внешний ключ (к нему или от него) или отношения «многие-ко-многим», вы несете ответственность за копирование этих связанных объектов, если это необходимо, каждый раз, когда CMS копирует плагин - он не будет делать это за вас автоматически.
Каждая модель плагина наследует метод empty cms.models.pluginmodel.CMSPlugin.copy_relations()
от базового класса, и он вызывается при копировании вашего плагина. Таким образом, вы можете приспособить его для своих целей по мере необходимости.
Как правило, вы хотите, чтобы он копировал связанные объекты. Для этого вы должны создать метод copy_relations
в вашей модели плагина, который получает старый экземпляр плагина в качестве аргумента.
Однако вы можете решить, что связанные объекты не должны копироваться - например, вы можете оставить их в покое. Или, возможно, вы даже захотите выбрать для него совершенно другие отношения, или создать новые при копировании… это зависит от вашего плагина и от того, как вы хотите, чтобы он работал.
Если вы хотите скопировать связанные объекты, вам нужно сделать это двумя немного разными способами, в зависимости от того, есть ли у вашего плагина отношения к или от других объектов, которые тоже нужно скопировать:
Для отношений по внешнему ключу от других объектов¶
Ваш плагин может иметь элементы с внешними ключами к нему, что обычно происходит, если вы настроили его так, чтобы они были встроенными в его админку. Таким образом, у вас может быть две модели, одна для плагина, а другая для этих элементов:
class ArticlePluginModel(CMSPlugin):
title = models.CharField(max_length=50)
class AssociatedItem(models.Model):
plugin = models.ForeignKey(
ArticlePluginModel,
related_name="associated_item"
)
Затем вам понадобится метод copy_relations()
в вашей модели плагина, чтобы перебрать связанные элементы и скопировать их, передавая копии внешние ключи новому плагину:
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()
Для отношений «многие-ко-многим» или отношений по внешнему ключу к другим объектам¶
Предположим, что это соответствующие части вашего плагина:
class ArticlePluginModel(CMSPlugin):
title = models.CharField(max_length=50)
sections = models.ManyToManyField(Section)
Теперь, когда плагин копируется, вы хотите убедиться, что разделы остаются, поэтому он становится:
class ArticlePluginModel(CMSPlugin):
title = models.CharField(max_length=50)
sections = models.ManyToManyField(Section)
def copy_relations(self, oldinstance):
self.sections.set(oldinstance.sections.all())
Если ваши плагины имеют реляционные поля обоих типов, вам, конечно, может понадобиться использовать обои техники копирования, описанные выше.
Отношения между плагинами¶
Гораздо сложнее управлять копированием отношений, когда они переходят от одного плагина к другому.
Более подробную информацию смотрите в выпуске GitHub copy_relations() does not work for relations between cmsplugins #4143.
Расширенный¶
Встроенный администратор¶
Если вы хотите иметь отношение внешнего ключа как встроенную админку, вы можете создать класс admin.StackedInline
и поместить его в Plugin в «inlines». Тогда вы сможете использовать форму inline admin для ваших ссылок на внешний ключ:
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
Форма плагина¶
Поскольку cms.plugin_base.CMSPluginBase
расширяет django.contrib.admin.ModelAdmin
, вы можете настраивать форму для своих плагинов так же, как вы настраиваете интерфейсы администраторов.
Шаблон, который использует механизм редактирования плагина, имеет вид cms/templates/admin/cms/page/plugin/change_form.html
. Вам может понадобиться изменить его.
Если вы хотите настроить его под себя, то лучше всего сделать это следующим образом:
создайте собственный шаблон, расширяющий
cms/templates/admin/cms/page/plugin/change_form.html
для обеспечения необходимой вам функциональности;снабдите ваш подкласс
cms.plugin_base.CMSPluginBase
атрибутомchange_form_template
, указывающим на ваш новый шаблон.
Расширение admin/cms/page/plugin/change_form.html
гарантирует, что вы сохраните единый внешний вид и функциональность всех ваших плагинов.
Существуют различные причины, по которым вы можете захотеть сделать это. Например, у вас может быть фрагмент JavaScript, который должен ссылаться на переменную шаблона), которую вы, вероятно, поместите в {% block extrahead %}
, после {{ block.super }}
, чтобы наследовать существующие элементы, которые были в родительском шаблоне.
Работа со средствами массовой информации¶
Если ваш плагин зависит от определенных медиафайлов, JavaScript или таблиц стилей, вы можете включить их из шаблона плагина, используя django-sekizai. Ваши шаблоны CMS всегда должны иметь пространства имен css
и js
sekizai, поэтому их следует использовать для включения соответствующих файлов. Для получения дополнительной информации о django-sekizai, пожалуйста, обратитесь к django-sekizai documentation.
Обратите внимание, что sekizai не может помочь вам с шаблонами плагинов со стороны администратора - то, что написано ниже, относится к шаблонам вывода ваших плагинов.
Стиль секидзай¶
Чтобы полностью использовать возможности django-sekizai, полезно иметь последовательный стиль его использования. Вот набор соглашений, которые следует соблюдать (но не обязательно):
Один бит на
addtoblock
. Всегда включайте один внешний CSS или JS файл наaddtoblock
или один сниппет наaddtoblock
. Это необходимо для того, чтобы django-sekizai правильно определял дубликаты файлов.Внешние файлы должны располагаться на одной строке, без пробелов и новых строк между тегом
addtoblock
и тегами HTML.При использовании встроенного javascript или CSS теги HTML должны располагаться с новой строки.
Пример хороший:
{% load sekizai_tags %}
{% addtoblock "js" %}<script type="text/javascript" src="{{ MEDIA_URL }}myplugin/js/myjsfile.js"></script>{% endaddtoblock %}
{% addtoblock "js" %}<script type="text/javascript" src="{{ MEDIA_URL }}myplugin/js/myotherfile.js"></script>{% endaddtoblock %}
{% addtoblock "css" %}<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}myplugin/css/astylesheet.css">{% endaddtoblock %}
{% addtoblock "js" %}
<script type="text/javascript">
$(document).ready(function(){
doSomething();
});
</script>
{% endaddtoblock %}
Пример плохой:
{% load sekizai_tags %}
{% addtoblock "js" %}<script type="text/javascript" src="{{ MEDIA_URL }}myplugin/js/myjsfile.js"></script>
<script type="text/javascript" src="{{ MEDIA_URL }}myplugin/js/myotherfile.js"></script>{% endaddtoblock %}
{% addtoblock "css" %}
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}myplugin/css/astylesheet.css"></script>
{% endaddtoblock %}
{% addtoblock "js" %}<script type="text/javascript">
$(document).ready(function(){
doSomething();
});
</script>{% endaddtoblock %}
Примечание
Если для правильного отображения плагина требуется код javascript, в тег script можно добавить класс 'cms-execute-js-to-render'
. Это позволит загрузить и выполнить все скрипты с этим классом, которые не присутствовали ранее, при первом добавлении плагина на страницу. Если код javascript защищен от преждевременного выполнения слушателем событий 'load'
и/или 'DOMContentLoaded'
, в тег script можно добавить следующие классы:
Классное имя |
Соответствующий код javascript |
---|---|
cms-trigger-event-document-DOMContentLoaded |
|
cms-trigger-event-window-DOMContentLoaded |
|
cms-trigger-event-window-load |
|
События будут запущены один раз после того, как все скрипты будут успешно внедрены в DOM.
Контекст плагина¶
Плагин имеет доступ к контексту шаблона django. Вы можете переопределять переменные с помощью тега with
.
Пример:
{% with 320 as width %}{% placeholder "content" %}{% endwith %}
Контекстные процессоры плагинов¶
Контекстные процессоры плагинов - это вызываемые переменные, которые изменяют контекст всех плагинов перед рендерингом. Они включаются с помощью параметра CMS_PLUGIN_CONTEXT_PROCESSORS
.
Контекстный процессор плагина принимает 3 аргумента:
instance
: Экземпляр модели плагинаplaceholder
: экземпляр плейсхолдера, в котором появляется этот плагин.context
: Используемый контекст, включая запрос.
Возвращаемое значение должно представлять собой словарь, содержащий любые переменные, которые должны быть добавлены к контексту.
Пример:
def add_verbose_name(instance, placeholder, context):
'''
This plugin context processor adds the plugin model's verbose_name to context.
'''
return {'verbose_name': instance._meta.verbose_name}
Процессоры плагинов¶
Процессоры плагинов - это вызываемые переменные, которые изменяют вывод всех плагинов после рендеринга. Они включаются с помощью параметра CMS_PLUGIN_PROCESSORS
.
Процессор плагина принимает 4 аргумента:
instance
: Экземпляр модели плагинаplaceholder
: экземпляр плейсхолдера, в котором появляется этот плагин.rendered_content
: Строка, содержащая отрисованное содержимое плагина.original_context
: Исходный контекст для шаблона, используемого для рендеринга плагина.
Примечание
Процессоры плагинов также применяются к плагинам, встроенным в текстовые плагины (и к любому пользовательскому плагину, допускающему вложенные плагины). В зависимости от того, что делает ваш процессор, это может нарушить вывод. Например, если ваш процессор обертывает вывод в тег div
, вы можете получить теги div
внутри тегов p
, что недопустимо. Вы можете предотвратить такие случаи, возвращая rendered_content
без изменений, если instance._render_meta.text_enabled
является True
, что происходит при рендеринге встроенного плагина.
Пример¶
Предположим, вы хотите обернуть каждый плагин в главном placeholder в цветную рамку, но было бы слишком сложно редактировать шаблон каждого отдельного плагина:
В вашем settings.py
:
CMS_PLUGIN_PROCESSORS = (
'yourapp.cms_plugin_processors.wrap_in_colored_box',
)
В вашем yourapp.cms_plugin_processors.py
:
def wrap_in_colored_box(instance, placeholder, rendered_content, original_context):
'''
This plugin processor wraps each plugin's output in a colored box if it is in the "main" placeholder.
'''
# Plugins not in the main placeholder should remain unchanged
# Plugins embedded in Text should remain unchanged in order not to break output
if placeholder.slot != 'main' or (instance._render_meta.text_enabled and instance.parent):
return rendered_content
else:
from django.template import Context, Template
# For simplicity's sake, construct the template from a string:
t = Template('<div style="border: 10px {{ border_color }} solid; background: {{ background_color }};">{{ content|safe }}</div>')
# Prepare that template's context:
c = Context({
'content': rendered_content,
# Some plugin models might allow you to customise the colors,
# for others, use default colors:
'background_color': instance.background_color if hasattr(instance, 'background_color') else 'lightyellow',
'border_color': instance.border_color if hasattr(instance, 'border_color') else 'lightblue',
})
# Finally, render the content through that template, and return the output
return t.render(c)
Вложенные плагины¶
Вы можете вложить плагины CMS в самих себя. Для достижения этой функциональности требуется несколько вещей:
models.py
:
class ParentPlugin(CMSPlugin):
# add your fields here
class ChildPlugin(CMSPlugin):
# add your fields here
cms_plugins.py
:
from .models import ParentPlugin, ChildPlugin
@plugin_pool.register_plugin
class ParentCMSPlugin(CMSPluginBase):
render_template = 'parent.html'
name = 'Parent'
model = ParentPlugin
allow_children = True # This enables the parent plugin to accept child plugins
# You can also specify a list of plugins that are accepted as children,
# or leave it away completely to accept all
# child_classes = ['ChildCMSPlugin']
def render(self, context, instance, placeholder):
context = super().render(context, instance, placeholder)
return context
@plugin_pool.register_plugin
class ChildCMSPlugin(CMSPluginBase):
render_template = 'child.html'
name = 'Child'
model = ChildPlugin
require_parent = True # Is it required that this plugin is a child of another plugin?
# You can also specify a list of plugins that are accepted as parents,
# or leave it away completely to accept all
# parent_classes = ['ParentCMSPlugin']
def render(self, context, instance, placeholder):
context = super(ChildCMSPlugin, self).render(context, instance, placeholder)
return context
parent.html
:
{% load cms_tags %}
<div class="plugin parent">
{% for plugin in instance.child_plugin_instances %}
{% render_plugin plugin %}
{% endfor %}
</div>
child.html:
<div class="plugin child">
{{ instance }}
</div>
Если у вас есть атрибуты родительского плагина, к которым вам нужно получить доступ в дочернем, вы можете получить доступ к родительскому экземпляру, используя get_bound_plugin
:
class ChildPluginForm(forms.ModelForm):
class Meta:
model = ChildPlugin
exclude = ()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance:
parent, parent_cls = self.instance.parent.get_bound_plugin()
Миграция данных плагина¶
В связи с миграцией с Django MPTT на django-treebeard в версии 3.1, модель плагинов отличается между двумя версиями. Миграции схем не затрагиваются, так как системы миграции (как South, так и Django) обнаруживают различные базы.
Однако миграция данных - это совсем другая история.
Если ваша миграция данных делает что-то вроде:
MyPlugin = apps.get_model('my_app', 'MyPlugin')
for plugin in MyPlugin.objects.all():
... do something ...
В итоге вы можете получить ошибку типа django.db.utils.OperationalError: (1054, "Unknown column 'cms_cmsplugin.level' in 'field list'")
, поскольку в зависимости от порядка выполнения миграций исторические модели могут быть рассинхронизированы с применяемой схемой базы данных.
Для сохранения совместимости с 3.0 и 3.x вы можете заставить миграцию данных выполняться до миграции django CMS, которая создает поля treebeard, при этом миграция данных всегда будет выполняться на «старой» схеме базы данных, и конфликтов не будет.
Для южных миграций добавьте следующее:
from distutils.version import LooseVersion
import cms
USES_TREEBEARD = LooseVersion(cms.__version__) >= LooseVersion('3.1')
class Migration(DataMigration):
if USES_TREEBEARD:
needed_by = [
('cms', '0070_auto__add_field_cmsplugin_path__add_field_cmsplugin_depth__add_field_c')
]
Для миграций Django добавьте следующее:
from distutils.version import LooseVersion
import cms
USES_TREEBEARD = LooseVersion(cms.__version__) >= LooseVersion('3.1')
class Migration(migrations.Migration):
if USES_TREEBEARD:
run_before = [
('cms', '0004_auto_20140924_1038')
]