• 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

  • 相关阅读:
    avalov+require实现tab栏
    动态加载js,css
    Zepto.js
    Linux 的文件和目录管理类命令
    shell 的基本理解
    Linux 日期时间命令
    Linux 关机命令
    type 命令
    命令类型即使用帮助
    cd 命令
  • 原文地址:https://www.cnblogs.com/2dogslife/p/8524645.html
Copyright © 2020-2023  润新知