前面的文章里面介绍了自定义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)
相对于原有的解决方案, 我的代码主要有三个改变:
十二点了, 懒得进一步总结了, 反正该说的也差不多说完了, 就酱紫吧~