Django自定义admin界面 – Part3

2009-12-01 23:59

前面的文章里面介绍了自定义Django的admin的几种技巧. 今天在这儿补充一点最新的经验. 今天要说的内容实际上和admin没有特别直接的联系, 但是问题的产生和解决都是在admin里面完成的.

背景介绍

在我手头的项目里面需要利用Django的信号机制来完成一些额外的工作. 可惜的是, 直到现在, Django还没有一个官方的解决方案来为ManyToManyField提供信号机制. 不过, 这个问题早已有人注意到了, 在Django的Trac上有一个ticket就是专门解决这个问题的, 这个ticket的状态目前是assigned, 而且已经有一个能用的patch.

但是, 这个patch在admin界面下并不能正确给出相应的信号. 例如, 你减少了一个ManyToManyField里面的一项, 逻辑上你期待Django在处理时应该将这一项从数据表里面删除, 然后释放一个remove信号. 但事实上, Django会首先清空数据表里面的所有项, 然后将剩下的项一古脑地添加回来.

顺藤摸瓜

首先应该肯定的是, 这个patch应该是没有大问题的, 这个patch本身提供了一些doctest, 从doctest来看, 该释放的信号都正确释放了. 没有出现上面无厘头的清空后再添加的情况.

接下来, 我们要做一点顺藤摸瓜的工作. 我已经知道, 在admin界面中作出修改后, 逻辑部分的视图函数是django/contrib/admin/options.py文件里面的change_view. 这个函数中和保存ManyToMany关系相关的代码是:

if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, change=True)
form.save_m2m()
for formset in formsets:
self.save_formset(request, form, formset, change=True)

change_message = self.construct_change_message(request, form, formsets)
self.log_change(request, new_object, change_message)
return self.response_change(request, new_object)

显而易见, 保存m2m关系的实际上是form这个ModelForm实例的save_m2m方法. 这个方法相关的代码在django/forms/models.py文件中:

def save_m2m():
opts = instance._meta
cleaned_data = form.cleaned_data
for f in opts.many_to_many:
if fields and f.name not in fields:
continue
if f.name in cleaned_data:
f.save_form_data(instance, cleaned_data[f.name])

仔细观察这个函数可以发现, 直到执行最后一句f.save_form_data之前, 都还没有做实质性的操作. 因此, 前面所叙述的逻辑错误肯定是在save_form_data这个函数中出错了. 这个函数的代码在django/db/models/fields/related.py中, 一共就一行:

def save_form_data(self, instance, data):
setattr(instance, self.attname, data)

当时看到这个就傻眼了, 因为这个setattr是python内置的函数, 我去研究了一圈python的C源码, 没有看懂任何东西. 百无聊赖地翻着django/db/models/fields/related.py这个文件, 看到下面这段:

class ManyRelatedObjectsDescriptor(object):
def __set__(self, instance, value):
"""Some code here"""

manager = self.__get__(instance)
manager.clear()
manager.add(*value)

于是就知道问题出在哪儿了...

别人的解决方案

好吧, 就我这种水平也很难写出什么更深入的讨论了. 找到上面这个错误后, 我兴冲冲地去ticket页面发表意见. 发表完以后才看到另一个人已经给出了一个直接的解决方案:

# Old code
manager = self.__get__(instance)
manager.clear()
manager.add(*value)

# New code
manager = self.__get__(instance)
previous=set(manager.all())
new=set(value)
added=new-previous
removed=previous-new
manager.add(*added)
manager.remove(*removed)

这个解决方案很直观, 在用集合的减法那段写得很漂亮. 但是存在几个问题:

我的解决方案

文章一开头我就说了, 这个问题我完全是在admin的范围内解决的. 没有hack掉Django的源码(虽然很明显它是有问题的). 我的思路也很简单: 将整段代码全部放在admin.py里面, override掉默认的方法. 这一段就比较丑陋了, 将数个函数叠在一起可不好看. 于是这部分代码我就略过了. 给出核心部分的代码吧:

manager = self.__get__(instance)
previous=set(manager.all())
new=set(value)
if previous and not new:
return manager.clear()
added=new-previous
removed=previous-new
if removed:
manager.remove(*removed)
if added:
manager.add(*added)

相对于原有的解决方案, 我的代码主要有三个改变:

十二点了, 懒得进一步总结了, 反正该说的也差不多说完了, 就酱紫吧~