Опишу, как сделать собственный плагин для Django-CMS на примере слайдера фотогалереи.
Допустим, плагин должен уметь отображать саму галерею, а также выводить заголовок и опционально некоторый текст описания, который задаётся в настройках самого плагина. Также должна быть возможность удобного администрирования галерей, например, доступ к оным из админ-панели. И, наконец, должна быть возможность размещения на одной странице нескольких плагинов без конфликтов между ними.
Вот так это выглядит: [Исходный код примера здесь]2.
Допустим, уже установлена Django-CMS, создано приложение, и в нём есть такие модели:
class Gallery(models.Model):
name = models.CharField(_('name'), max_length=128)
class Photo(models.Model):
title = models.CharField(_('title'), max_length=128, null=True, blank=True)
file = ImageField(_('file'), upload_to='images')
gallery = models.ForeignKey(Gallery)
А также эти модели выведены в админку:
class PhotoInline(AdminInlineImageMixin, admin.TabularInline):
model = Photo
class GalleryAdmin(admin.ModelAdmin):
inlines = [PhotoInline]
admin.site.register(Gallery, GalleryAdmin)
Плагин описывается классом, унаследованным от класса CMSPluginBase
, в файле с именем cms_plugins.py
, расположенном в каталоге приложения. Полный пример такого класса:
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from django.utils.translation import ugettext as _
from .models import Slider
class SliderPlugin(CMSPluginBase):
module = _("Our Mega Plugins")
name = _("Slider Plugin")
render_template = "slider/slider_plugin.html"
model = Slider
def render(self, context, instance, placeholder):
context.update({'instance': instance})
return context
plugin_pool.register_plugin(SliderPlugin)
Свойства module
и name
описывают, как будет выглядеть плагин в интерфейсе добавления:
module
— раздел;name
— непосредственно название плагина.
render_template
, как можно догадаться из названия — имя шаблона плагина. Шаблон точно такой же, как и при разработке собственных тэгов.
model
— необязательное свойство. Оно нужно в том случае, если у плагина есть настройки. Значением является модель, в которой сохраняются настройки плагина. Эта модель описывается классом, унаследованным от CMSPlugin
. Опишем эту модель:
from cms.models import CMSPlugin
from djangocms_text_ckeditor.fields import HTMLField
class Slider(CMSPlugin):
title = models.CharField(_('title'), max_length=128, null=True, blank=True)
description = HTMLField(_('description'), null=True, blank=True)
gallery = models.ForeignKey(Gallery, verbose_name=_('gallery'), null=True, blank=True)
def get_title(self):
return self.title or self.gallery.name
def __str__(self):
return self.get_title()
Полями этого класса могут быть любые допустимые в django поля модели. В данном примере кроме стандартных используется поле типа HTMLField
, предоставляемое приложением djangocms_text_ckeditor
. Это TextField
с интегрированным визуальным редактором. Так выглядит получившееся окно редактирования свойств плагина:
Метод render
отвечает за подготовку контекста и отрисовку шаблона. Получаемый им аргумент instance
содержит объект плагина. Воспользуемся им для получения доступа к настройкам. Модель галереи в нашем случае выбирается в настройках, таким образом весь необходимый контент доступен через модель настройки, и её достаточно для вывода.
Осталось только сделать шаблон. Приведу его полностью:
{% load sekizai_tags thumbnail %}
<h1>{{ instance.get_title }}</h1>
{% if instance.description %}
<article>{{ instance.description|safe }}</article>
{% endif %}
<div id="carousel-example-{{ instance.id }}" class="carousel slide" data-ride="carousel">
<ol class="carousel-indicators">
{% for photo in instance.gallery.photo_set.all %}
<li data-target="#carousel-example-{{ instance.id }}" data-slide-to="{{ forloop.counter0 }}"{% if forloop.first %} class="active"{% endif %}></li>
{% endfor %}
</ol>
<div class="carousel-inner" role="listbox">
{% for photo in instance.gallery.photo_set.all %}
<div class="item{% if forloop.first %} active{% endif %}">
{% thumbnail photo.file "x400" as im %}
<img src="{{ im.url }}" class="img-responsive center-block">
{% endthumbnail %}
{% if photo.title %}
<div class="carousel-caption">{{ photo.title }}</div>
{% endif %}
</div>
{% endfor %}
</div>
<a class="left carousel-control" href="#carousel-example-{{ instance.id }}" role="button" data-slide="prev">
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="right carousel-control" href="#carousel-example-{{ instance.id }}" role="button" data-slide="next">
<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
{% addtoblock "js" %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
{% endaddtoblock %}
На что здесь стоит обратить внимание. Заголовок и описание берём из настроек плагина. ID карусели строим из id модели плагина во избежание конфликтов при размещении нескольких плагинов этого типа на одной странице. Сами фото из галереи доступны через связи: instance.gallery.photo_set.all
.
В этом примере используется bootstrap для оформления. Чтобы карусель работала, нужно подключить скрипты. Не стоит подключать в общем шаблоне те скрипты, которые нужны только в отдельных случаях, лучше подключать их по мере необходимости. В Django-CMS для этого предлагается использовать приложение sekizai. В коде шаблона подключаемые скрипты заключены в тэг {% addtoblock "js" %}
. В базовом шаблоне должен быть тэг {% render_block "js" %}
. Sekizai позаботится о том, чтобы код подключения скриптов был помещён на место тэга render_block
только один раз, независимо о того, сколько раз встречается addtoblock
.
Плагин готов, его можно использовать. Осталось только добавить управление данными в админ-панель.
Для этого в каталоге приложения следует создать файл cms_toolbar.py
, и описать класс, унаследованный от CMSToolbar
.
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from cms.toolbar_pool import toolbar_pool
from cms.toolbar_base import CMSToolbar
from cms.toolbar.items import Break
from cms.cms_toolbar import ADMIN_MENU_IDENTIFIER, ADMINISTRATION_BREAK
from .models import Gallery
@toolbar_pool.register
class SliderToolbar(CMSToolbar):
def populate(self):
admin_menu = self.toolbar.get_or_create_menu(
ADMIN_MENU_IDENTIFIER
)
position = admin_menu.find_first(
Break,
identifier=ADMINISTRATION_BREAK
) + 1
admin_menu.add_break('custom-break', position=position)
slider_menu = admin_menu.get_or_create_menu(
'slider-menu',
_('Slider'),
position=position
)
for gallery in Gallery.objects.all():
slider_menu.add_modal_item(
_('edit gallery %s') % gallery.name,
reverse(u'admin:slider_gallery_change', args=[gallery.id])
)
slider_menu.add_break('custom-break')
slider_menu.add_modal_item(
_('add gallery'),
reverse(u'admin:slider_gallery_add')
)
Все манипуляции с меню описываются в методе populate()
. Метод get_or_create_menu()
обеспечивает доступ к меню. Метод add_modal_item()
позволяет добавить пункт меню, по нажатию на который откроется модельное окно. Первый аргумент — текст пункта меню, второй — адрес страницы. В нашем случае это стандартные адреса джанговской админки. В данном случае приведён полностью работающий пример. но сама тема административного меню достаточно обширна и достойна отдельной статьи.