Django CMS 建立自定義插件

 

前言

 

CMS插件是一個可重複使用內容發佈者,它可以嵌入到django CMS頁面上,或者利用django CMS placeholder嵌入到任意內容。它們不須要進一步的干預就可以自動的發佈信息。這就意味着,當你發佈網頁內容時,不論是什麼內容,總能保持最新。 javascript

它就像一個魔術,可是更快。 css

若是你的需求在內嵌的或者第三方的插件裏都實現了,那麼你很幸運,不然的話,你須要去實現本身的CMS插件。可是不用太擔憂,寫一個CMS插件很是簡單。 html

 

爲何須要寫一個插件

若是要把django應用集成到django CMS頁面上,插件是最方便的方法。例如:若是你在部署一個唱片公司的django CMS站點,你可能想在主頁上放一個"最新發布"的版塊,這樣可能須要常常編輯該頁面去更新信息。然而,一個明智的唱片公司一樣會在django裏管理它的目錄,這樣的話django就已經知道這周它要發佈什麼。 java

這是一個很好的機會去利用這些信息來簡化你的工做,你所須要作的事就是建立一個CMS插件把它插入到你的主頁上,讓它去完成發佈信息的工做。 python

插件是可重複使用的,這樣你可能只須要稍做修改就能夠用於相似目的。 git

概述

一個django CMS插件基本上由三部分組成 github

  • editor插件:在每次部署時進行配置
  • publisher插件:自動完成決定發佈哪些內容
  • template插件:渲染網頁信息

 

這些與MVT (Model-View-Template)模式是一致的 shell

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

 

因此,要編寫你本身的plugin,你須要從下面開始 數據庫

 

cms.plugin_base.CMSPluginBase

注意事項

 

cms.plugin_base.CMSPluginBase 其實是 django.contrib.admin.ModelAdmin 子類 django

 

由於 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

相關文章
相關標籤/搜索