标签:, 发表于 Django开发 分类. 1条评论

前面的文章里面介绍了自定义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)

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

  • 没有正确释放出clear信号
  • 没有对added和removed这两个作一个判定就直接丢给了add和remove. 更合理的方法应是检查发现它们非空后再交给后面的函数处理.
  • 没有经过完善地测试, 没有被收入官方的patch. 我们的服务器上已经部署好了旧patch, 我不希望再打一个没有验证过的补丁.

我的解决方案

文章一开头我就说了, 这个问题我完全是在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)

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

  • 添加了对new为空时的判断来调用clear方法.
  • 添加了对add和remove的判断.
  • 改变了add和remove的顺序, 希望这样能够更快些.

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

2009-12-01 23:59