修正django admin raw_id_fields bug

Note: 上午还说 最近更新可能会比较缓慢的 ,结果下午就又上来发了一篇,我自己也很囧啊……

Django Admin中使用ForeignKey是一个比较痛苦的事, 默认使用的是 Select 在数据量比较大的情况下要选中需要选择的对象实在是太难了…… 最近刚刚好又碰到了这个问题,为了图方便,干脆就祭起了 raw_id_fields 这个属性来把外键的选择简化为直接输入id。

可是使用下来出现了问题,如果输入的id无效,Django并不会返回期望的包含错误信息的form,而是直接报了个 django.template.TemplateSyntaxError 的错误。情况很奇怪,FK使用的是 ModelChoiceField ,理论上对无效id的错误已经做了捕捉,那问题到底出在什么地方呢?

这儿再次bs以下Django默认的Template Debug。本来图省事就直接用Django内建的server跑测试,当使用werkzeug作为开发服务器后,使用相同的操作重现错误,就看到了最初报错的地方了。

Original Traceback (most recent call last):
  File "/usr/lib/python2.6/site-packages/django/template/debug.py", line 71, in render_node
    result = node.render(context)
  File "/usr/lib/python2.6/site-packages/django/template/debug.py", line 87, in render
    output = force_unicode(self.filter_expression.resolve(context))
  File "/usr/lib/python2.6/site-packages/django/utils/encoding.py", line 71, in force_unicode
    s = unicode(s)
  File "/usr/lib/python2.6/site-packages/django/forms/forms.py", line 356, in __unicode__
    return self.as_widget()
  File "/usr/lib/python2.6/site-packages/django/forms/forms.py", line 391, in as_widget
    return widget.render(name, data, attrs=attrs)
  File "/usr/lib/python2.6/site-packages/django/contrib/admin/widgets.py", line 126, in render
    output.append(self.label_for_value(value))
  File "/usr/lib/python2.6/site-packages/django/contrib/admin/widgets.py", line 150, in label_for_value
    obj = self.rel.to._default_manager.get(**{key: value})
  File "/usr/lib/python2.6/site-packages/django/db/models/manager.py", line 120, in get
    return self.get_query_set().get(*args, **kwargs)
  File "/usr/lib/python2.6/site-packages/django/db/models/query.py", line 305, in get
    % self.model._meta.object_name)
DoesNotExist: TargetUser matching query does not exist.

顺着这个trackback的信息往上找,立刻就找到了问题所在 label_for_value 。问题出在了 ForeignKeyRawIdWidget 的处理上。Raw id field会在值有效的情况下在输入框后面显示链接对象的相关信息,但是这个widget使用了渲染时传入的value是否为空来判断该值是否有效,在有效的情况下,会调用 label_for_value(value) 来获得该值对应的对象信息。但是在这个函数里并没有处理 ObjectDoesNotExist 的异常,所以出现了上述的错误。

那既然发现了错误,在不改变Django源码的前提下,定义一个Widget的子类来覆盖掉原来的 label_for_value 的行为。

from django.contrib.admin.widgets import ForeignKeyRawIdWidget
class AdminEnhancedFKRawIdWidget(ForeignKeyRawIdWidget):
    def label_for_value(self, value):
        try:
            return super(AdminEnhancedFKRawIdWidget, self).label_for_value(value)
        except self.rel.to.DoesNotExist:
            return ''

然后在相关的ModelAdmin中加上如下代码:

class SomeModelAdmin(admin.ModelAdmin):
    raw_id_fields = ('some_field', )

    def get_form(self, request, obj=None, **kwargs):
        form = super(SomeModelAdmin,self).get_form(request, obj,**kwargs)
        w = form.base_fields['some_field']
        w.widget = AdminEnhancedFKRawIdWidget(w.widget.rel, w.widget.attrs)
        return form

当然也可以使用嫁接的方法来达到目的,我这儿就不贴了。

P.S. 跑到Django Code上建了一个 Ticket ,然后很快就被告知duplicated了……

发表评论

评论备注:

  1. 留言时的头像是Gravatar提供的服务。
  2. By submitting a comment here you grant this site a perpetual license to reproduce your words and name/web site in attribution. So, you don't fully own your words, so to speak.