示例代码:
https://github.com/divio/djangocms-picture
以上一个图片的插件,安装后可在页面中添加图片,效果如下图:
以此为切入点,分析plugin的逻辑:
分析
1. plugin列表加载分析:
2. plugin实例添加到页面的分析:
通过DjDT分析得出,最终是交给了cms.admin.pageadmin.edit_plugin处理,如下:
而edit_plugin的代码非常简洁:
def edit_plugin(self, *args, **kwargs): with create_revision(): return super(PageAdmin, self).edit_plugin(*args, **kwargs)
而PageAdmin的继承关系如下:
class PageAdmin(PlaceholderAdminMixin, ModelAdmin):
基本可以猜出,edit_plugin应该是mixin里面的函数,如下:
具体代码:
@xframe_options_sameorigin def edit_plugin(self, request, plugin_id): plugin_id = int(plugin_id) cms_plugin = get_object_or_404(CMSPlugin.objects.select_related('placeholder'), pk=plugin_id) instance, plugin_admin = cms_plugin.get_plugin_instance(self.admin_site) if not self.has_change_plugin_permission(request, cms_plugin): return HttpResponseForbidden(force_unicode(_("You do not have permission to edit this plugin"))) plugin_admin.cms_plugin_instance = cms_plugin try: plugin_admin.placeholder = cms_plugin.placeholder except Placeholder.DoesNotExist: pass if request.method == "POST": # set the continue flag, otherwise will plugin_admin make redirect to list # view, which actually doesn't exists request.POST['_continue'] = True if request.POST.get("_cancel", False): # cancel button was clicked context = { 'CMS_MEDIA_URL': get_cms_setting('MEDIA_URL'), 'plugin': cms_plugin, 'is_popup': True, "type": cms_plugin.get_plugin_name(), 'plugin_id': plugin_id, 'icon': force_escape(escapejs(cms_plugin.get_instance_icon_src())), 'alt': force_escape(escapejs(cms_plugin.get_instance_icon_alt())), 'cancel': True, } instance = cms_plugin.get_plugin_instance()[0] if instance: context['name'] = force_unicode(instance) else: # cancelled before any content was added to plugin cms_plugin.delete() context.update({ "deleted": True, 'name': force_unicode(cms_plugin), }) return render_to_response('admin/cms/page/plugin/confirm_form.html', context, RequestContext(request)) if not instance: # instance doesn't exist, call add view response = plugin_admin.add_view(request) else: # already saved before, call change view # we actually have the instance here, but since i won't override # change_view method, is better if it will be loaded again, so # just pass id to plugin_admin response = plugin_admin.change_view(request, str(plugin_id)) if request.method == "POST" and plugin_admin.object_successfully_changed: self.post_edit_plugin(request, plugin_admin.saved_object) saved_object = plugin_admin.saved_object context = { 'CMS_MEDIA_URL': get_cms_setting('MEDIA_URL'), 'plugin': saved_object, 'is_popup': True, 'name': force_unicode(saved_object), "type": saved_object.get_plugin_name(), 'plugin_id': plugin_id, 'icon': force_escape(saved_object.get_instance_icon_src()), 'alt': force_escape(saved_object.get_instance_icon_alt()), } return render_to_response('admin/cms/page/plugin/confirm_form.html', context, RequestContext(request)) return response
经过调试发现,在
cms_plugin = get_object_or_404(CMSPlugin.objects.select_related('placeholder'), pk=plugin_id)
时plugin_id已经有值了,因此猜测,此id为plugin控件在控件表CMSPlugin中的id(CMSPlugin通过一个名为placeholder的字段m2m到Placeholder表,如下:)
@python_2_unicode_compatible class CMSPlugin(with_metaclass(PluginModelBase, MPTTModel)): ... placeholder = models.ForeignKey(Placeholder, editable=False, null=True)
placehoder似乎和plugin等价了,不过看不出玄机:
@python_2_unicode_compatible class Placeholder(models.Model): slot = models.CharField(_("slot"), max_length=50, db_index=True, editable=False) default_width = models.PositiveSmallIntegerField(_("width"), null=True, editable=False) cache_placeholder = True ... def get_plugins_list(self, language=None): return list(self.get_plugins(language)) def get_plugins(self, language=None): if language: return self.cmsplugin_set.filter(language=language).order_by('tree_id', 'lft') else: return self.cmsplugin_set.all().order_by('tree_id', 'lft')
回到正题:
instance, plugin_admin = cms_plugin.get_plugin_instance(self.admin_site)
新建时,instance为None,plugin_admin为PicturePlugin的实例。分析源码,得出,plugin是从控件池中取出来的,如下:
def get_plugin_class(self): from cms.plugin_pool import plugin_pool return plugin_pool.get_plugin(self.plugin_type) def get_plugin_class_instance(self, admin=None): plugin_class = self.get_plugin_class() # needed so we have the same signature as the original ModelAdmin return plugin_class(plugin_class.model, admin) def get_plugin_instance(self, admin=None): plugin = self.get_plugin_class_instance(admin) if hasattr(self, "_inst"): return self._inst, plugin if plugin.model != self.__class__: # and self.__class__ == CMSPlugin: # (if self is actually a subclass, getattr below would break) try: instance = plugin.model.objects.get(cmsplugin_ptr=self) instance._render_meta = self._render_meta except (AttributeError, ObjectDoesNotExist): instance = None else: instance = self self._inst = instance return self._inst, plugin
最终,由于instance为空,进入以下逻辑:
if not instance: # instance doesn't exist, call add view response = plugin_admin.add_view(request)
此处分析也有些力不从心(后续深入)
=============
经过以上分析,plugin列表的加载逻辑已经呼之欲出了,留给细心的读者自己发掘。
总结:
1. 控件加载用到的类:
CMSPlugin/PlaceholderAdminMixin/Placeholder/PageAdmin/Page等,继承关系如下:
2. 当在页面中添加plugin时,调用关系:
1). PageAdmin.edit_plugin
2). PlaceholderAdminMixin.edit_plugin , 其处理逻辑是:
a). 首先通过CMSPlugin得到一个cms_plugin对象。
b). 通过cms_plugin对象得到控件实例instance和plugin_admin,其中plugin_admin是重插件池 plugin_pool中根据类型得到的。
c). 如果实例为空(新建),通过plugin_admin的add_view方法返回一个response对象,否则(编辑),通过change_view方法返回response对象。
d). 如果请求方式是Post,并且plugin_admin的object_successfully_changed为真值,则给予相关提示信息。