django CMS 插件体系深度解析:模型、视图与模板三要素及自定义插件实战

发布时间:2026/9/24 15:57:25
django CMS 插件体系深度解析:模型、视图与模板三要素及自定义插件实战 django CMS 插件体系深度解析模型、视图与模板三要素及自定义插件实战【免费下载链接】django-cmsThe easy-to-use and developer-friendly enterprise CMS powered by Django项目地址: https://gitcode.com/gh_mirrors/dj/django-cmsCMS PluginCMS 插件是 django CMS 中可复用的内容发布组件可以被插入到 CMS 页面或任何使用了 django CMS 占位符的内容中实现信息的自动发布、无需人工干预。本文围绕 docs/explanation/plugins.rst 的核心脉络系统讲解插件的概念、三大组成要素模型 / 视图 / 模板、CMSPluginBase继承自ModelAdmin的可生效与不可生效选项并结合仓库源码与 docs/how_to/09-custom_plugins.rst 实操指南给出从最简单的插件到带模型配置、嵌套子插件、关系复制、插件处理器的完整实战方案。读完本文你将掌握如何判断何时该写插件、何时该用 apphook并能独立编写、注册、配置和发布一个生产可用的自定义 CMS 插件。什么是 CMS Plugin可复用的内容发布器CMS Plugin 是 django CMS 三大核心构建块之一。根据 docs/explanation/composition.rst 的定义一个 django CMS 站点由三类组件拼装而成内容对象Content Object持有可编辑内容并暴露占位符的对象页面Page是最典型的一种插件Plugin编辑器可以拖入占位符的可复用内容组件它拥有编辑器通过模型填写的数据以及渲染该数据的模板Apphook把 Django 应用挂载到页面树上的标准方式。三者的关系可以概括为内容对象拥有占位符占位符容纳插件插件负责组合composition。插件永远活在占位符里从不独立存在——即使同一个插件出现在多个内容对象上每一次出现都是一个独立的插件实例拥有各自的配置数据。插件最核心的价值在于自动发布信息一旦配置好并插入页面它就会持续发布最新内容无需人工维护。这意味着你发布在网页上的任何内容都能随时保持最新——像魔法一样只不过更快。为什么要写自己的插件插件是将另一个 Django 应用的内容集成到 django CMS 页面中最便捷的方式。文档中给出了一个唱片公司网站的经典例子假设你要在首页放一个最新发行Latest releases栏目你可以定期手动编辑页面更新信息但唱片公司通常本来就用 Django 管理自己的曲库——Django 已经知道本周的新发行是什么。此时只需创建一个 CMS 插件插入首页剩下的事情全部交给插件自动完成。插件还是可复用的同一家公司如果正在发行一系列瑞士朋克经典再版唱片你可以在该系列页面上插入同一个插件仅做略微不同的配置即可发布该系列近期新发行的信息。插件还是 Apphook一个决策辅助在动手写插件前先回答一个问题这些内容到底住在哪里如果它适合放进别人页面上的某个占位符里它就是插件如果它是一类独立的东西——拥有自己的列表视图、详情视图和 URL——那它就是一个应用需要通过 apphook 挂载。你想要…选择…为什么一个编辑器可拖入任意占位符的可复用内容组件插件插件是占位符内部的组合单元一个完整的子应用博客、曲库、投票、搜索ApphookApphook 拥有 URL 前缀并自带内容对象一个完全由编辑器组合内容的页面页面 插件页面内容对象的默认流程一个主体由 Django 视图驱动的页面页面 Apphook页面提供 URL应用提供视图让编辑器选择哪些记录显示在页面内嵌列表中插件模型引用你的记录组件住在页面上只有数据住在应用模型里让编辑器通过移动页面来移动子站点的 URLApphook视图和内容对象是你的URL 属于页面首页放活动预告并且在/events/下有完整活动子站两者都要——预告用插件子站用 apphook常见组合插件渲染摘要apphook 拥有/events/及其以下值得注意的边界情况一个需要自己详情 URL 的产品卡片插件仍然是插件它住在占位符里但详情 URL 应该来自挂载在 CMS 页面上的 apphook这样 URL 才是编辑器可控的同一页面上多个相同插件是相互独立的实例共享模型和模板但不共享数据跨页面复用同一份内容而不复制它则应使用 Alias 内容对象配合 Alias 插件嵌入。插件的三个组成部分一个 django CMS 插件本质上由三个组件构成与 Django 熟悉的 Model-View-Template 模式一一对应组件功能继承自model可选插件实例配置CMSPluginview显示逻辑CMSPluginBasetemplate渲染——modelCMSPlugin 子类可选插件模型——即 cms.models.pluginmodel.CMSPlugin 的子类——是可选的。如果某个插件只需要做一件事、不需要任何配置你可以完全不要模型。例如一个只发布过去七天最畅销唱片的插件就不需要配置当然这样也失去了灵活性——你无法用同一个插件去发布上个月最畅销的信息。因此实践中你会发现通常还是需要模型来保存配置。从源码看CMSPlugin基类自身带有若干由 CMS 内部管理的字段见 cms/models/pluginmodel.pyplaceholder所属占位符外键、parent父插件外键根级插件为None、position占位符与语言内的唯一位置、language、plugin_type插件类名、creation_date、changed_date等。子类化时有两条硬性限制CMSPlugin的子类不能再被子类化子类不能定义名为text的字段。在 django CMS 4 中插件实例的创建与删除统一由占位符管理详见后文通过占位符 API 创建与删除插件这也是保证插件树完整性的关键设计。viewCMSPluginBase显示逻辑cms.plugin_base.CMSPluginBase 是插件的视图层负责显示逻辑。一个值得注意的实现事实是CMSPluginBase实际上是django.contrib.admin.ModelAdmin的子类——这在 cms/plugin_base.py 的类定义中明确可见。这意味着插件开发者在编写插件时可以沿用大量熟悉的ModelAdmin选项。CMSPluginBase还使用了一个自定义元类CMSPluginBaseMetaclasscms/plugin_base.py它在类创建时自动完成多项校验与默认值设置校验model属性必须是CMSPlugin或其子类否则抛出SubClassNeededError校验必须定义render_template属性或get_render_template方法否则抛出ImproperlyConfigured未指定form时自动基于模型生成一个ModelForm排除position、placeholder、language、plugin_type、path、depth等 CMS 内部字段未指定fieldsets时根据模型字段自动生成基础字段区与高级选项折叠区未指定name时自动把类名HelloPlugin转换为更友好的Hello Plugin。可用的 ModelAdmin 选项由于CMSPluginBase继承自ModelAdmin以下ModelAdmin选项对 CMS 插件开发者是有效的并且非常常用excludefieldsfieldsetsformformfield_overridesinlinesradio_fieldsraw_id_fieldsreadonly_fields这些选项让插件编辑表单完全等同于一个可定制的 Django 管理后台表单。例如通过form挂载自定义ModelForm以实现字段清洗见下文安全部分通过inlines把外键关联对象以内联表单形式展示。被 CMS 忽略的 ModelAdmin 选项需要注意并非所有ModelAdmin选项在 CMS 插件中都有效。特别是任何仅被ModelAdmin的changelist列表页使用的选项都不会产生效果因为插件根本没有列表页。原文档明确列出的无效选项包括actions、actions_on_top、actions_on_bottom、actions_selection_counterdate_hierarchylist_display、list_display_links、list_editable、list_filterlist_max_show_all、list_per_pageordering、paginatorprepopulated_fields、preserve_fieldssave_as、save_on_topsearch_fields、show_full_result_countview_on_sitetemplate渲染层插件模板负责最终输出。模板通过render_template属性静态指定或get_render_template方法动态返回模板路径提供二者至少必须定义其一当render_plugin为默认值True时。插件的render()方法cms/plugin_base.py决定模板上下文默认实现只把instance和placeholder加入 context覆盖时建议先调用super().render(...)以保留这两个默认变量再补充自定义上下文。实战编写第一个最简单的插件完整的实操教程见 docs/how_to/09-custom_plugins.rst这里继承其核心步骤并补充源码细节。插件代码放在应用的cms_plugins.py文件中可通过python -m manage startapp创建插件应用记得加入INSTALLED_APPS也可以直接往已有应用添加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.htmlh1Hello {% if request.user.is_authenticated %}{{ request.user.first_name }} {{ request.user.last_name}}{% else %}Guest{% endif %}/h1这个插件会向登录用户显示其姓名向未登录访客显示 Guest。两个必需的类属性在CMSPluginBase子类上有两个必需属性model用于存储插件信息的模型。如果插件不需要保存任何特殊信息如配置可以直接使用 CMSPlugin。注意与普通 admin 类的区别普通 admin 通过admin.site.register(Model, Admin)注册模型信息由注册机制提供而插件不是这样注册的所以必须显式给出model。name插件在 admin 中显示的名称。通常建议用gettext_lazy标记为可翻译字符串可选未指定时默认取类名的友好化形式元类会自动把HelloPlugin转为Hello Plugin。渲染模板二选一当render_plugin为True默认值时以下二者必须定义其一render_template渲染该插件的模板路径get_render_template返回模板路径的方法用于根据上下文动态选择模板。在render_plugin False的情况下插件完全不渲染此时两者都可以不定义但allow_children不能与render_plugin False同时为True这是 cms/plugin_pool.py 中的显式校验。插件的发现与注册机制plugin_pool.register_plugin装饰器背后是 cms/plugin_pool.py 中的PluginPool.register_plugin()它校验插件必须是CMSPluginBase的子类以类名作为注册键重复注册会抛出PluginAlreadyRegistered。插件的发现则由discover_plugins()cms/plugin_pool.py完成它调用 Django 的autodiscover_modules(cms_plugins)遍历所有已安装应用中名为cms_plugins.py的模块并自动导入。这就是插件文件必须命名为cms_plugins.py的原因。导入后插件会按module和name排序。若你的cms_plugins模块加载失败或不可访问可在 shell 中直接验证$ python -m manage shell from importlib import import_module m import_module(myapp.cms_plugins) m.some_test_function() # 来自 myapp.cms_plugins 模块的函数存储配置为插件添加模型很多插件需要保存实例级配置。例如一个显示最新博客文章的插件可能想配置显示条数一个画廊插件需要选择要展示的图片。做法是在某个已安装应用的models.py中创建CMSPlugin的子类。把上面的HelloPlugin升级为可配置的版本。先在models.py中添加模型from cms.models.pluginmodel import CMSPlugin from django.db import models class Hello(CMSPlugin): guest_name models.CharField(max_length50, defaultGuest)与普通 Django 模型的唯一区别就是继承CMSPlugin而非models.Model。然后修改插件定义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最后更新模板用可配置的{{ instance.guest_name }}替换硬编码的 Guesth1Hello {% if request.user.is_authenticated %} {{ request.user.first_name }} {{ request.user.last_name}} {% else %} {{ instance.guest_name }} {% endif %}/h1命名字段时的两个注意事项不能把模型字段命名为与任何已安装插件的模型同名小写形式因为 Django 对子类模型使用隐式一对一关系。使用全部核心插件时需要避开的名称包括file、googlemap、link、picture、snippetptr、teaser、twittersearch、twitterrecententries、video。建议避免使用page作为模型字段名CMSPlugin上已声明了一个page属性cms/models/pluginmodel.py虽然其使用已废弃但仍作为兼容性垫片存在。处理关联对象copy_relations一些用户操作会触发插件的复制最典型的是复制粘贴占位符内容。如果自定义插件带有外键指向它或从它出发或多对多关系你有责任在 CMS 复制插件时复制这些关联对象——CMS 不会自动帮你做。每个插件模型都从基类继承了空的copy_relations方法cms/models/pluginmodel.py插件被复制时会调用它。典型做法是在插件模型上实现接收旧实例参数的copy_relations方法当然你也可以决定不复制关联对象或为新副本选择完全不同的关联取决于插件的行为设计。场景一外键从其他对象指向插件这通常出现在把关联条目做成插件 admin 内联inline的情况下class ArticlePluginModel(CMSPlugin): title models.CharField(max_length50) class AssociatedItem(models.Model): plugin models.ForeignKey( ArticlePluginModel, related_nameassociated_item )此时copy_relations()需要遍历关联条目并为新插件创建副本class ArticlePluginModel(CMSPlugin): title models.CharField(max_length50) def copy_relations(self, oldinstance): # 复制前先删除当前实例上已有的关联对象 # 否则公开版页面上可能出现重复 self.associated_item.all().delete() for associated_item in oldinstance.associated_item.all(): # instance.pk None; instance.save() 是 Django 复制已保存 # 模型实例的标准略显奇特但正确做法 associated_item.pk None associated_item.plugin self associated_item.save()场景二多对多或外键从插件指向其他对象class ArticlePluginModel(CMSPlugin): title models.CharField(max_length50) sections models.ManyToManyField(Section) def copy_relations(self, oldinstance): self.sections.set(oldinstance.sections.all())如果插件两类关系都有通常需要同时使用上面两种复制技巧。插件与插件之间的关联复制要困难得多仓库文档提到可参考copy_relations() does not work for relations between cmsplugins这一已知议题见 docs/how_to/09-custom_plugins.rst。给已有插件添加模型数据迁移当需要为已有插件新增模型时必须小心处理否则现有插件实例会从 CMS 界面消失见仓库文档引用的 Issue #7476。正确流程为定义模型在models.py中定义继承CMSPlugin的模型所有字段必须带有有意义的默认值以便后续自动迁移更新插件类在cms_plugins.py中把model属性指向新模型生成迁移但先不应用执行python manage.py makemigrations编写数据迁移在刚生成的迁移文件中追加RunPython操作遍历CMSPlugin.objects.filter(plugin_typeplugin_type)为每个既有实例创建对应模型记录把pk、cmsplugin_ptr、placeholder、parent、language、position、creation_date等字段从旧实例拷贝过去然后save()应用迁移并测试执行迁移后全面测试确认既有实例正常显示、新模型功能按预期工作。嵌套插件父子结构CMS 插件支持嵌套。实现嵌套需要父插件声明allow_children True并在父插件模板中渲染子插件。以 docs/how_to/09-custom_plugins.rst 的示例为骨架# models.py class ParentPlugin(CMSPlugin): # 在此添加字段 class ChildPlugin(CMSPlugin): # 在此添加字段# 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 # 允许父插件接受子插件 # 也可以指定允许作为子插件的列表或完全不指定以接受全部 # child_classes [ChildCMSPlugin] # 条目可以是 glob 模式例如 child_classes [Bootstrap*] # 特殊值 child_classes auto 表示只接受那些在 parent_classes # 中显式声明本插件的插件由子插件主动加入 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 更可取 # 显式命名 parent_classes 本身已强制插件必须有父级 # 两者同时设置是冗余的。 # 这里 * 展开为所有已注册插件表示任何插件都可作为父级—— # 但该插件必须有一个父级不能直接添加到占位符。 parent_classes [*] def render(self, context, instance, placeholder): context super(ChildCMSPlugin, self).render(context, instance, placeholder) return context父插件模板通过instance.child_plugin_instances遍历子插件并用{% render_plugin %}渲染见 cms/plugin_base.py 中allow_children的文档说明!-- parent.html -- {% load cms_tags %} div classplugin parent {% for plugin in instance.child_plugin_instances %} {% render_plugin plugin %} {% endfor %} /div!-- child.html -- div classplugin child {{ instance }} /div如果子插件需要访问父插件的属性可在表单初始化时通过self.instance.parent.get_bound_plugin()获取父实例。关于父子约束源码提供了更精细的控制cms/plugin_base.pychild_classes父插件侧限制只允许列表中的插件作为子级条目支持 glob 模式如Bootstrap*、*Link*会针对所有已注册插件名展开[]或匹配不到任何插件的模式表示不允许任何子插件而None默认表示无限制特殊值auto表示恰好接受那些在自己的parent_classes中显式列出本插件的插件。parent_classes子插件侧限制列出允许的父类[]表示不允许任何父级只能添加到占位符None默认表示无限制。require_parent该插件是否必须作为另一个插件的子级。disable_child_plugins在结构模式下禁用子插件的拖拽。相关限制还可以通过CMS_PLACEHOLDER_CONF中的child_classes/parent_classes/require_parent键按占位符覆盖。限制插件可用的模型allowed_models 与 allowed_plugins本特性在文档中标明为 5.1 版本新增源码实现在 cms/plugin_pool.py 的get_all_plugins_for_model。django CMS 提供两套互补的过滤机制控制插件与模型的搭配插件级过滤allowed_models——限制某个插件可用于哪些模型plugin_pool.register_plugin class PageSpecificPlugin(CMSPluginBase): name Page Only Plugin model CMSPlugin render_template page_specific.html allowed_models [cms.pagecontent] # 仅限 CMS 页面allowed_models的取值None默认可用于所有带占位符的模型格式为app_label.modelname的模型标识列表如[cms.pagecontent, myapp.mymodel]空列表[]不能用于任何模型。模型标识会被自动规范化为小写见 cms/plugin_base.py 的元类处理因此[cms.PageContent]与[cms.pagecontent]等价。模型级过滤allowed_plugins——限制某个模型上可添加哪些插件from django.db import models from cms.models import PlaceholderField class BlogPost(models.Model): title models.CharField(max_length200) placeholders PlaceholderRelationField() # 只允许在博客文章中放这些插件 allowed_plugins [TextPlugin, LinkPlugin, PicturePlugin]allowed_plugins的取值None默认所有插件都允许但仍受插件自身allowed_models过滤、插件类名列表、空列表[]不允许任何插件。两者同时定义时两个过滤都必须通过插件才可用对应源码 cms/plugin_pool.py 的双重过滤逻辑。此外插件还支持allowed_slots属性cms/plugin_base.py按占位符 slot 名支持footer_*这类 glob限制可用位置与CMS_PLACEHOLDER_CONF的plugins/excluded_plugins互为补充两个过滤同样必须同时通过。需要注意allowed_plugins中出现的未注册插件名、allowed_models中不存在的模型标识都会被静默忽略不抛错误。通过占位符 API 创建与删除插件文档标明为 4.0 版本新增见 docs/how_to/09-custom_plugins.rst。插件存在于占位符内部从 django CMS 4 起占位符统一管理插件的创建与删除并负责对整棵插件树做必要调整。不通过占位符创建或删除插件会导致插件树损坏。创建插件有两种方式# 方式一占位符的 add_plugin 方法 new_instance MyPluginModel( plugin_datasecret, placeholderplaceholder_to_add_to, position1, # 占位符中的第一个插件 ) placeholder_to_add_to.add_plugin(new_instance) assert new_instance.pk is not None # 已保存到数据库# 方式二cms.api.add_plugin 函数 new_plugin cms.api.add_plugin( placeholder_to_add_to, MyPlugin, positionfirst-child, # 占位符中的第一个位置无父级 datadict(plugin_datasecret), )删除插件会连同其所有子插件一起删除old_instance.placeholder.delete_plugin(old_instance)警告不要用PluginModel.objects.create(...)或PluginModel.objects.delete()来创建或删除插件实例——这很可能抛出数据库完整性异常或产生不一致的插件树导致意外行为也不要使用queryset.delete()批量删除插件这会破坏插件树。高级特性与扩展点将插件标记为 slot5.1 版本新增源码属性见 cms/plugin_base.py。把插件的is_slot属性设为True可将其标记为槽位——一个结构性容器用户不能直接编辑它。插件仍会正常渲染但在结构面板上双击不会打开编辑对话框移动插件或添加子插件不受影响。适用于没有可配置字段、或完全由父插件管理的插件plugin_pool.register_plugin class SeparatorPlugin(CMSPluginBase): name Separator render_template separator.html is_slot True对于第三方插件无需修改其源码即可在AppConfig.ready()中标记class MyAppConfig(AppConfig): name myapp def ready(self): from cms.plugin_pool import plugin_pool plugin_pool.get_plugin(SomeThirdPartyPlugin).is_slot True自定义结构面板外观5.1 版本新增。在插件的模型CMSPlugin子类而非CMSPluginBase子类上定义add_structureboard_classes方法其返回值会被追加到结构面板中包裹该插件的cms-draggable容器的 CSS 类上可配合自定义 CSS 实现按状态/配置区分插件外观——典型场景是标记停用或草稿插件。方法返回的类还会应用到该插件的子插件上从而可以为整个子树设置样式。注意该方法在结构面板渲染期间被调用应保持轻量、避免数据库查询。扩展占位符或插件的上下文菜单通过覆盖CMSPluginBase上的两个方法cms/plugin_base.py可以扩展上下文菜单返回PluginMenuItem实例列表cms/plugin_base.pyPluginMenuItem支持名称、URL、POST 数据、确认文本与自定义 actionget_extra_placeholder_menu_items(request, placeholder)为所有占位符的上下文菜单添加条目get_extra_plugin_menu_items(request, plugin)为所有插件的上下文菜单添加条目。典型用法如内置的AliasPlugin模式是在菜单项中 POSTplugin_id/placeholder_id与 CSRF token 到插件自定义 URL实现创建别名之类的操作插件还可以通过get_plugin_urls()注册自己的 URL 模式它们会被挂载到 django CMS 页面 admin 的插件路径下默认形如/admin/cms/page/plugin/plugin-name/。插件上下文处理器Plugin Context Processors通过CMS_PLUGIN_CONTEXT_PROCESSORS设置启用它们是可调用对象在所有插件渲染前修改其上下文。接收三个参数instance插件模型实例、placeholder插件所在的占位符实例、context当前上下文含 request。返回值是一个字典包含要加入上下文的变量def add_verbose_name(instance, placeholder, context): return {verbose_name: instance._meta.verbose_name}插件处理器Plugin Processors通过CMS_PLUGIN_PROCESSORS设置启用在所有插件渲染后修改其输出。接收四个参数instance、placeholder、rendered_content渲染后的字符串、original_context渲染插件所用的原始上下文。例如在 settings 中配置CMS_PLUGIN_PROCESSORS (yourapp.cms_plugin_processors.wrap_in_colored_box,)然后在处理器中为main占位符的每个插件输出套上彩色边框盒子。注意插件处理器也会作用于嵌入 Text 插件的子插件包裹式输出可能产生非法 HTML如p内嵌套div可通过instance._render_meta.text_enabled判断是否为内嵌渲染并原样返回。内联 admin 与插件表单由于CMSPluginBase继承自ModelAdmin可以像定制 admin 一样定制插件表单。外键关联可以做成admin.StackedInline并放入插件的inlines元组。插件编辑界面使用的模板是cms/templates/admin/cms/page/plugin/change_form.html自定义的最佳方式是新建模板extends该模板以保持统一外观然后在插件类上设置change_form_template指向它默认值定义在 cms/plugin_base.py。处理媒体资源与内联脚本如果插件依赖 JS 或 CSS应在插件输出模板中用 django-sekizai 的{% addtoblock js %}/{% addtoblock css %}引入CMS 模板始终强制存在css和js两个 sekizai 命名空间sekizai 对 admin 侧的插件模板无效。使用规范每个addtoblock只放一个外部文件或一段内联代码便于 sekizai 去重外部文件应写成单行且addtoblock标签与 HTML 标签间无空格换行。内联 JavaScript 是潜在安全风险应尽量避免——django CMS 4.2 起已从自身代码库移除全部内联 JS 以便设置有意义的 CSP 头如果项目 CSP 不允许内联 JS通过 Sekizai 提供的内联 JS 也不会被执行。编辑模式下内容刷新后会触发DOMContentLoaded、window.load推荐监听它执行插件 JS以及为兼容保留的cms-content-refreshjQuery 事件。安全注意事项插件输出是受信任的 HTML插件渲染的任何内容都会作为**标记markup**插入页面。django CMS 渲染每个插件的模板、拼接结果字符串并在交给外层模板前标记为安全——这不是缺失的检查而是占位符能工作的根本原因模板输出本身就是 HTML二次转义会把每个插件的div变成可见的lt;divgt;。Django 自身的render_to_string()、Template.render()和{% include %}返回安全字符串也是同理。由此得出明确的结论占位符管线无法保护你免受插件渲染内容的伤害转义是插件自身的责任且发生在插件模板内部。不要破坏自动转义Django 模板默认转义变量{{ instance.headline }}是安全的而|safe、mark_safe()、带未转义参数的format_html()、{% autoescape off %}都会告诉 Django这个值已是可信标记只应作用于你自己代码产出的标记绝不用于来自表单、请求、导入或外部 API 的值。在保存时净化而非渲染时需要存 HTML 的插件富文本正文、嵌入片段应在存储时清洗例如用nh3.clean()配合允许标签/属性白名单让数据库里已是安全值使模板、订阅源、API、搜索索引等所有消费方共同受益。渲染时净化更弱它每次请求都执行且此时值已与周围模板标记无法区分白名单要么宽到失去意义要么窄到破坏布局。若字段本不打算包含标记则无需净化——保持自动转义开启直接{{ instance.body }}渲染即可。谁算攻击者内容编辑器在你的信任边界内。被授权添加插件的员工用户按设计可以改变访客所见内容页面级权限控制的是他们能碰哪些页面而非能放什么内容参见 docs/explanation/permissions.rst——权限不能替代信任。但这不意味着上面的建议可有可无编辑器账号可能被盗用或共享、内容可能从其他系统导入、插件字段可能被无人审核的程序化来源填充——把你没有亲手渲染的一切都当作不可信数据。CSP 是第二道防线可限制注入脚本造成的损害。超越 Python 插件djangocms-frontend到目前为止描述的所有插件都需要一个 Python 类——CMSPluginBase子类通常还有一个模型。djangocms-frontend提供了两条更轻量的插件路径底层都会转换为完整的 django CMS 插件模板组件Template components编写一个 Django 模板放到某个应用内的cms_components目录djangocms-frontend 会在启动时自动检测它。字段直接在模板中声明——无需任何 Python 文件。自定义组件Custom components在cms_components.py文件中编写一个 Python 类继承CMSFrontendComponent把字段声明为 Django 表单字段属性并用components.register装饰器注册。这让你对新增/编辑表单拥有完全控制权——fieldsets、自定义校验、mixins——同时省去完整CMSPluginBase子类的样板代码。两条路径都与框架无关djangocms-frontend 自带的组件和 mixin 面向 Bootstrap 5但你不受其约束。它们的取舍在于范围模板组件完全不能包含 Python 代码自定义组件不能给插件或模型类添加方法。当需要这种完全控制时CMSPluginBase子类才是正确的工具。分步教程和示例请参阅 djangocms-frontend 自己的文档。结语从概念到源码的完整链路回顾整条链路插件是 django CMS 中占位符内部的组合单元由可选的CMSPlugin模型、继承自ModelAdmin的CMSPluginBase视图类和渲染模板三部分组成通过 cms/plugin_pool.py 的autodiscover_modules(cms_plugins)自动发现、plugin_pool.register_plugin注册实例的创建与删除必须经由占位符 APIPlaceholder.add_plugin/delete_plugin或cms.api.add_plugin以保证插件树一致关系的复制依赖copy_relations()钩子嵌套、模型/槽位限制、菜单扩展、插件处理器等高级能力均有对应的类属性与源码方法可直接查阅。对于插件还是 apphook的抉择记住一句口诀能塞进占位符的是插件自带列表/详情/URL 的是应用apphook 挂载。围绕 docs/how_to/09-custom_plugins.rst 的完整教程与 cms/plugin_base.py 的类文档含大量示例与注意事项再结合本文梳理的源码依据你已具备编写、注册、配置、安全加固并发布自定义 django CMS 插件的完整能力。【免费下载链接】django-cmsThe easy-to-use and developer-friendly enterprise CMS powered by Django项目地址: https://gitcode.com/gh_mirrors/dj/django-cms创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询