Django CMS 创建自定义插件

 

前言

 

CMS插件是一个可重复使用内容发布者,它能够嵌入到django CMS页面上,或者利用django CMS placeholder嵌入到任意内容。它们不需要进一步的干预就能够自动的发布信息。这就意味着,当你发布网页内容时,不管是什么内容,总能保持最新。

它就像一个魔术,但是更快。

如果你的需求在内嵌的或者第三方的插件里都实现了,那么你很幸运,否则的话,你需要去实现自己的CMS插件。但是不用太担心,写一个CMS插件非常简单。

 

为什么需要写一个插件

如果要把django应用集成到django CMS页面上,插件是最方便的方法。例如:如果你在部署一个唱片公司的django CMS站点,你可能想在主页上放一个"最新发布"的版块,这样可能需要经常编辑该页面去更新信息。然而,一个明智的唱片公司同样会在django里管理它的目录,这样的话django就已经知道这周它要发布什么。

这是一个很好的机会去利用这些信息来简化你的工作,你所需要做的事就是创建一个CMS插件把它插入到你的主页上,让它去完成发布信息的工作。

插件是可重复使用的,这样你可能只需要稍作修改就可以用于类似目的。

概述

一个django CMS插件基本上由三部分组成

  • editor插件:在每次部署时进行配置
  • publisher插件:自动完成决定发布哪些内容
  • template插件:渲染网页信息

 

这些与MVT (Model-View-Template)模式是一致的

  • model插件存储配置信息
  • view插件完成显示
  • template插件渲染信息

 

所以,要编写你自己的plugin,你需要从下面开始

 

cms.plugin_base.CMSPluginBase

注意事项

 

cms.plugin_base.CMSPluginBase 实际上是 django.contrib.admin.ModelAdmin 子类

 

因为 CMSPluginBase 从 ModelAdmin 子类化,所以 ModelAdmin 的几个重要的选项对CMS plugin开发者也适用。下面这些选项经常被用到::

  • exclude
  • fields
  • fieldsets
  • form
  • formfield_overrides
  • inlines
  • radio_fields
  • raw_id_fields
  • readonly_fields

 

然而,并不是ModelAdmin的所有的操作在CMS plugin都能用,特别是一些ModelAdminchangelist专用的那些选项是无效的。下面这些选项在CMS中需要被忽略:

  • actions
  • actions_on_top
  • actions_on_bottom
  • actions_selection_counter
  • date_hierarchy
  • list_display
  • list_display_links
  • list_editable
  • list_filter
  • list_max_show_all
  • list_per_page
  • ordering
  • paginator
  • preserve_fields
  • save_as
  • save_on_top
  • search_fields
  • show_full_result_count
  • view_on_site

 

关于model及配置的旁白

Model插件从cms.models.pluginmodel.CMSPlugin继承而来,实际上它是可选的。

如果它永远只做同一件事,你的插件可以不去配置,例如:如果你的插件永远只是发布过去几天里销量最好的唱票。很明显,这个不够灵活,他并不能发布过去几个月销量最好的。

通常,如果你发现你需要去配置你的插件,这就需要定义一个model。

 

最简单插件、

你可以用python manage.py startapp去设置基本的应用布局(记得把你的插件加入INSTALLED_APPS),或者,你也可以只需要在当前的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 ugettext_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>

 

该插件会在页面上显示欢迎信息,如果是登录用户,显示名字,否则显示Guest。

 

cms_plugins.py文件,你会子类化cms.plugin_base.CMSPluginBase,这些类会定义了不同的插件。

有两个属性是这些类必须的:

model:model用于存储你的插件信息。如果你不打算存储一些特别信息,直接用cms.models.pluginmodel.CMSPlugin就可以了。在一个正常的admin class,你不需要提供这个信息。

name:显示在admin上的你的插件名字。通过,实际工作中我们会通过django.utils.translation.ugettext_lazy()将改字符串设成可翻译的。

 

如果render_plugin设为True,下面内容必须定义

render_template:插件的渲染模板

get_render_template:返回渲染插件模板的路径

 

除了这些属性,你也可以重写render()方法,该方法决定渲染插件的模板上下文变量。默认情况下,这个方法只会把instanceplaceholder对象添加到你的context,插件可以通过重写这个方法添加更多的上下文内容。

你也可以重写其他的CMSPluginBase子类的方法,详细信息参考CMSPluginBase 

调试

 

因为插件的modules通过django的importlib加载,你可能会碰到路径环境导致的问题。如果你的cms_plugins不能加载或者访问,尝试下面的操作:

$ python manage.py shell

>>> from importlib import import_module

>>> m = import_module("myapp.cms_plugins")

>>> m.some_test_function()

 

存储配置

许多情况下,你需要给你的插件实例存储配置。例如:如果你有一个插件显示最新的发布博客,你可能也希望能够选择显示条目的数量。

去实现这些功能,你需要在已安装的models.py文件里,创建一个model子类化cms.models.pluginmodel.CMSPlugin

接下来,我们来改进一下上面的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')

 

这个跟正常的model定义没有太大差别,唯一不同是它是从cms.models.pluginmodel.CMSPlugin 继承而不是django.db.models.Model.

 

现在,我们需要修改我们的插件定义来使用这个model,新的cms_plugins.py如下

 

from cms.plugin_base import CMSPluginBase

from cms.plugin_pool import plugin_pool

from django.utils.translation import ugettext_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(HelloPlugin, self).render(context, instance, placeholder)

        return context

 

我们修改model属性,并且将model实例传递给context.

 

最后,更新模板,在模板里使用新的配置信息。

 

<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字符串

关系处理

每次你的页面发布时,如果自定义插件在页面里,那么它就会被拷贝。所以,你的自定义插件有ForeignKey (from或者to)或者m2m,你需要拷贝这些关联对象。它不会自动帮你完成

 

每个插件model会从基类继承空方法cms.models.pluginmodel.CMSPlugin.copy_relations(),在你的插件被拷贝时,它会被调用。所以,你可以在这儿适配你的目的。

 

典型情况下,你需要用它去拷贝关联对象。要实现该功能,你需要在你的插件model创建copy_relations()方法,老的instance会作为一个参数传入。

也有可能你决定不需要拷贝这些关联对象,你想让它们独立存在。这些取决于你想怎样让这些插件工作。

如果你想拷贝关联对象,你需要用两个近似的方法去实现,具体需要看你的插件和对象之间的关系 (from还是to)

For foreign key relations from other objects

你的插件可能有一些条目的外键指向它,这些是典型的admin内联场景。所以,你可能需要两个model,一个plugin,一个给那些条目。

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()        

 

For many-to-many or foreign key relations to other objects

 

假定你得插件有关联对象

class ArticlePluginModel(CMSPlugin):

    title = models.CharField(max_length=50)

    sections = models.ManyToManyField(Section)

 

当插件被拷贝是,我们需要section保持不变,所以改成如下:

class ArticlePluginModel(CMSPlugin):

    title = models.CharField(max_length=50)

    sections = models.ManyToManyField(Section)

   

    def copy_relations(self, oldinstance):

        self.sections = oldinstance.sections.all()

 

如果你的插件有这两种关联域,你就需要用到以上的两种技术。

Relations between plugins

如果插件直接有关联,关系拷贝就会变得非常困难。细节查看GitHub issue copy_relations() does not work for relations between cmsplugins #4143

 

高级

Inline Admin

如果你想外键关系作为inline admin,你需要创建admin.StackedInlineclass,并且把插件放到inlines。然后,你可以用这个inline admin form作为你的外键引用。

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(ArticlePlugin, self).render(context, instance, placeholder)

        items = instance.associated_item.all()

        context.update({

            'items': items,

        })

        return context

Plugin form

因为 cms.plugin_base.CMSPluginBase 从django.contrib.admin.ModelAdmin扩展而来, 你可以为你的插件定制化form,方法跟定制化admin form一样.

插件编辑机制使用的模板是cms/templates/admin/cms/page/plugin/change_form.html,你可能需要修改它。

如果你想定制化,最好的方法是:

  • cms/templates/admin/cms/page/plugin/change_form.html扩展一个你自己的template来实现你想要的功能
  • 在你的cms.plugin_base.CMSPluginBase子类里,将新模板赋值给变量change_form_template

 

cms/templates/admin/cms/page/plugin/change_form.html扩展能够保证你的插件和其他的外观和功能统一。

 

处理media

如果你的插件依赖于特定的media文件, JavaScript或者stylesheets, 你可以通过django-sekizai把它们加入到你的插件模板。你的CMS模板是强制要求加入css 和 js sekizai 域名空间。更多信息请参考 django-sekizai documentation.

Sekizai style

要想充分利用django-sekizai, 最好使用一致的风格,下面是一些遵守的惯例:

 

  • 一个 addtoblock一块. 每个 addtoblock包含一个外部css或者js文件,或者一个snippet. 这样的话django-sekizai非常容易检查重复文件.
  • 外部文件应该在同一行,在 addtoblock tag 和 the HTML tags之间没有空格.
  • 使用嵌入的javascript或CSS时, HTML tags 必须在新行.

 

一个好的例子:

{% 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 %}

Plugin Context

插件能够访问django模板,你可以通过with tag覆盖变量

例子:

{% with 320 as width %}{% placeholder "content" %}{% endwith %}

Plugin Context Processors

 

在渲染之前,插件context processor可以被调用去修改插件的context。可以通过CMS_PLUGIN_CONTEXT_PROCESSORS使能改功能。

一个插件context processor包含三个参数

  • instance: 插件model实例
  • placeholder: 当前插件所在的placeholder的实例.
  • context: 当前使用的context, 包含request.

 

它返回一个字典,包含了添加到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}

Plugin Processors

在渲染之前,插件processor可以被调用去修改插件的输出。可以通过CMS_PLUGIN_PROCESSORS使能改功能。

 

一个插件processor包含三个参数

  • instance: 插件model实例
  • placeholder: 当前插件所在的placeholder的实例.
  • rendered_content: 包含插件渲染内容的字符串.
  • original_context: 插件的原始context.

 

 

例子

加入你要在主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)

嵌套插件

你可以让插件相互嵌套。要实现这个功能,需要完成以下几个事情:

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(ParentCMSPlugin, self).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>

扩展placeholder或者插件的上下文菜单

有三种方法去扩展placeholder或者插件的上下文菜单

  • 扩展placeholder的上下文菜单
  • 或者所有插件的上下文菜单
  • 扩展当前插件的上下文菜单

 

你可以重写下面CMSPluginBase的三个方法来实现这个目的

例子

class AliasPlugin(CMSPluginBase):

    name = _("Alias")

    allow_children = False

    model = AliasPluginModel

    render_template = "cms/plugins/alias.html"

   

    def render(self, context, instance, placeholder):

        context = super(AliasPlugin, self).render(context, instance, placeholder)

        if instance.plugin_id:

            plugins = instance.plugin.get_descendants(include_self=True).order_by('placeholder', 'tree_id', 'level',

                                                                                  'position')

            plugins = downcast_plugins(plugins)

            plugins[0].parent_id = None

            plugins = build_plugin_tree(plugins)

            context['plugins'] = plugins

        if instance.alias_placeholder_id:

            content = render_placeholder(instance.alias_placeholder, context)

            print content

            context['content'] = mark_safe(content)

        return context

   

    def get_extra_global_plugin_menu_items(self, request, plugin):

        return [

            PluginMenuItem(

                _("Create Alias"),

                reverse("admin:cms_create_alias"),

                data={'plugin_id': plugin.pk, 'csrfmiddlewaretoken': get_token(request)},

            )

        ]

   

    def get_extra_placeholder_menu_items(self, request, placeholder):

        return [

            PluginMenuItem(

                _("Create Alias"),

                reverse("admin:cms_create_alias"),

                data={'placeholder_id': placeholder.pk, 'csrfmiddlewaretoken': get_token(request)},

            )

        ]

   

    def get_plugin_urls(self):

        urlpatterns = [

            url(r'^create_alias/$', self.create_alias, name='cms_create_alias'),

        ]

        return urlpatterns

   

    def create_alias(self, request):

        if not request.user.is_staff:

            return HttpResponseForbidden("not enough privileges")

        if not 'plugin_id' in request.POST and not 'placeholder_id' in request.POST:

            return HttpResponseBadRequest("plugin_id or placeholder_id POST parameter missing.")

        plugin = None

        placeholder = None

        if 'plugin_id' in request.POST:

            pk = request.POST['plugin_id']

            try:

                plugin = CMSPlugin.objects.get(pk=pk)

            except CMSPlugin.DoesNotExist:

                return HttpResponseBadRequest("plugin with id %s not found." % pk)

        if 'placeholder_id' in request.POST:

            pk = request.POST['placeholder_id']

            try:

                placeholder = Placeholder.objects.get(pk=pk)

            except Placeholder.DoesNotExist:

                return HttpResponseBadRequest("placeholder with id %s not found." % pk)

            if not placeholder.has_change_permission(request):

                return HttpResponseBadRequest("You do not have enough permission to alias this placeholder.")

        clipboard = request.toolbar.clipboard

        clipboard.cmsplugin_set.all().delete()

        language = request.LANGUAGE_CODE

        if plugin:

            language = plugin.language

        alias = AliasPluginModel(language=language, placeholder=clipboard, plugin_type="AliasPlugin")

        if plugin:

            alias.plugin = plugin

        if placeholder:

            alias.alias_placeholder = placeholder

        alias.save()

        return HttpResponse("ok")

插件数据迁移

在版本3.1,django MPTT迁移到了django-treebeard,插件模式在这两个版本是不同的。Schema迁移并没有受影响,因为迁移系统( 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'")。因为不同的迁移执行顺序,model历史数据可能会失步。

为保持3.0和3.x的兼容,你应该在执行django CMS迁移之前强制执行数据迁移,django CMS会创建treebeard域。通过执行这个,数据迁移会永远在老的数据库模式上执行,并不会对新的产生冲突。

 

对south迁移,添加下面代码

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')

        ]

 

下一篇开始会介绍如何自定义django CMS菜单

 

 

关注下方公众号获取更多文章

参考文档

http://docs.django-cms.org/en/release-3.4.x/how_to/custom_plugins.html

posted on 2018-03-07 18:46  汪汪家园  阅读(933)  评论(0编辑  收藏  举报