从ForeignKey继承的自定义字段会在保存时引发异常

时间:2011-07-10 13:26:31

标签: django django-models

我写了以下自定义字段:

from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import signals
from sitetree.models import Tree, TreeItem
from south.modelsinspector import introspector

class AutoTreeItemField(models.ForeignKey):
    def __init__(self, *args, **kwargs):
        super(AutoTreeItemField, self).__init__(TreeItem, null=True)

        self.date_field = kwargs['date_field']
        self.__should_appear = kwargs['should_appear']
        self.year_menu_item_url = kwargs['year_menu_item_url']
        self.month_menu_item_url = kwargs['month_menu_item_url']
        self.item_menu_item_url = kwargs['item_menu_item_url']

        self.year_format = kwargs.get('year_format', '%Y')
        self.month_format = kwargs.get('month_format', '%B')
        self.inmenu = kwargs.get('inmenu', True)
        self.inbreadcrumbs = kwargs.get('inbreadcrumbs', True)
        self.insitetree = kwargs.get('insitetree', True)
        self.item_title = kwargs.get('title', 'title')

    def contribute_to_class(self, cls, name):
        super(AutoTreeItemField, self).contribute_to_class(cls, name)

        # Make this object the descriptor for field access.
        setattr(cls, self.name, self)

        self.tree = self.__get_or_create_tree(cls._meta.verbose_name_plural.lower())

        # Delete menu item after the instance is deleted
        signals.post_delete.connect(self.__delete, cls, True)

    def pre_save(self, model_instance, add):
        super(AutoTreeItemField, self).pre_save(model_instance, add)
        if self.should_appear(model_instance):
            year_menu_item = self.__get_or_create_year_tree_item(model_instance)
            month_menu_item = self.__get_or_create_month_tree_item(model_instance, year_menu_item)
            menu_item = self.__save_menu_item(model_instance, month_menu_item)
            setattr(model_instance, self.get_attname(), menu_item.id)

            return menu_item.id
        else:
            self.__delete_orphans(model_instance)

            return None

    def __delete(self, **kwargs):
        self.__delete_orphans(kwargs['instace'])

    def __get_or_create_tree(self, alias):
        try:
            return Tree.objects.get(alias=alias)
        except Tree.DoesNotExist:
            return Tree.objects.create(alias=alias)

    def should_appear(self, instance):
        if isinstance(self.__should_appear, str):
            return getattr(instance, self.__should_appear)
        elif callable(self.__should_appear):
            return self.__should_appear()

    def south_field_triple(self):
        """Returns a suitable description of this field for South."""
        args, kwargs = introspector(self)
        kwargs.update({'date_field': 'None'})
        return ('website.blog.fields.AutoTreeItemField', args, kwargs)

    def __get_or_create_year_tree_item(self, model_instance):
        year = self.__get_year(model_instance)

        try:
            return TreeItem.objects.get(title=year, tree=self.tree)
        except TreeItem.DoesNotExist:
            return TreeItem.objects.create(title=year,
                                           url=reverse(self.year_menu_item_url, args = [year]),
                                           tree=self.tree,
                                           inmenu=self.inmenu,
                                           inbreadcrumbs=self.inbreadcrumbs,
                                           insitetree=self.insitetree,
                                           parent=None)

    def __get_year(self, model_instance):
        return getattr(model_instance, self.date_field).strftime(self.year_format)

    def __get_or_create_month_tree_item(self, model_instance, year_menu_item):
        month = self.__get_month(model_instance)

        try:
            return TreeItem.objects.get(title=month, tree=self.tree, parent=year_menu_item)
        except TreeItem.DoesNotExist:
            return TreeItem.objects.create(title=month,
                                           url=reverse(self.month_menu_item_url, args = [getattr(model_instance, self.date_field).year, getattr(model_instance, self.date_field).month]),
                                           tree=self.tree,
                                           inmenu=self.inmenu,
                                           inbreadcrumbs=self.inbreadcrumbs,
                                           insitetree=self.insitetree,
                                           parent=year_menu_item)

    def __get_month(self, model_instance):
        return getattr(model_instance, self.date_field).strftime(self.month_format)

    def __save_menu_item(self, model_instance, month_tree_item):
        try:
            item = self.__get_menu_item(model_instance)

            item.title = getattr(model_instance, self.item_title)
            item.url = model_instance.get_absolute_url()
            item.parent = month_tree_item

            return item
        except TreeItem.DoesNotExist:
            return TreeItem.objects.create(title=getattr(model_instance, self.item_title),
                                           url=model_instance.get_absolute_url(),
                                           tree=self.tree,
                                           inmenu=self.inmenu,
                                           inbreadcrumbs=self.inbreadcrumbs,
                                           insitetree=self.insitetree,
                                           parent=month_tree_item)

    def __get_year_tree_item(self, model_instance):
        year = self.__get_year(model_instance)

        return TreeItem.objects.filter(title=year, tree=self.tree)

    def __delete_orphans(self, model_instance):
        menu_item = self.__get_menu_item(model_instance)

        try:
            if menu_item is not None:
                month_menu_item = menu_item.parent
                menu_item.delete()

                if TreeItem.objects.filter(parent=month_menu_item, parent__parent=self.__get_year_tree_item(model_instance), tree=self.tree).count():
                        year_menu_item = month_menu_item.parent

                        month_menu_item.delete()

                        if TreeItem.objects.filter(parent=year_menu_item, tree=self.tree).count() == 0:
                            year_menu_item.delete()
        except TreeItem.DoesNotExist:
            pass

    def __get_menu_item(self, model_instance):
        menu_item_id = getattr(model_instance, self.get_attname())
        return TreeItem.objects.get(id=menu_item_id)

但是当我试图保存它时,我得到了:

'AutoTreeItemField' object has no attribute '_meta'

这是完整的堆栈跟踪:

Environment:


Request Method: POST
Request URL: http://127.0.0.1:8000/admin/blog/draftpost/add/

Django Version: 1.3
Python Version: 2.7.1
Installed Applications:
['django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.sites',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.admin',
 'django.contrib.sites',
 'django.contrib.flatpages',
 'tagging',
 'reversion',
 'south',
 'sitetree',
 'dojango',
 'disqus',
 'website.blog',
 'website.cms']
Installed Middleware:
('django.middleware.common.CommonMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'dojango.middleware.DojoCollector',
 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware')


Traceback:
File "C:\Python27\lib\site-packages\django\core\handlers\base.py" in get_response
  111.                         response = callback(request, *callback_args, **callback_kwargs)
File "C:\Python27\lib\site-packages\django\contrib\admin\options.py" in wrapper
  307.                 return self.admin_site.admin_view(view)(*args, **kwargs)
File "C:\Python27\lib\site-packages\django\utils\decorators.py" in _wrapped_view
  93.                     response = view_func(request, *args, **kwargs)
File "C:\Python27\lib\site-packages\django\views\decorators\cache.py" in _wrapped_view_func
  79.         response = view_func(request, *args, **kwargs)
File "C:\Python27\lib\site-packages\django\contrib\admin\sites.py" in inner
  197.             return view(request, *args, **kwargs)
File "C:\Python27\lib\site-packages\django\db\transaction.py" in inner
  217.                 res = func(*args, **kwargs)
File "C:\Python27\lib\site-packages\reversion\revisions.py" in _create_on_success
  352.                 self.end()
File "C:\Python27\lib\site-packages\reversion\revisions.py" in end
  274.                     revision_set = self.follow_relationships(models)
File "C:\Python27\lib\site-packages\reversion\revisions.py" in follow_relationships
  244.         map(_follow_relationships, object_dict)
File "C:\Python27\lib\site-packages\reversion\revisions.py" in _follow_relationships
  243.                     _follow_relationships(parent_obj)
File "C:\Python27\lib\site-packages\reversion\revisions.py" in _follow_relationships
  213.             result_dict[obj] = self.get_version_data(obj, VERSION_CHANGE)
File "C:\Python27\lib\site-packages\reversion\revisions.py" in get_version_data
  254.         serialized_data = serializers.serialize(registration_info.format, [obj], fields=registration_info.fields)
File "C:\Python27\lib\site-packages\django\core\serializers\__init__.py" in serialize
  91.     s.serialize(queryset, **options)
File "C:\Python27\lib\site-packages\django\core\serializers\base.py" in serialize
  48.                             self.handle_fk_field(obj, field)
File "C:\Python27\lib\site-packages\django\core\serializers\python.py" in handle_fk_field
  53.                 if field.rel.field_name == related._meta.pk.name:

Exception Type: AttributeError at /admin/blog/draftpost/add/
Exception Value: 'AutoTreeItemField' object has no attribute '_meta'

任何人都可以向我展示这是怎么回事?可以采取哪些措施来解决这个问题呢? 奇怪的是,它发生在事务结束后,它将我需要的所有内容保存到数据库中。

编辑:
该错误是由尝试将我的实例序列化为json的reversion引起的。但是,这仍然是我的问题,因为_meta属性应该在那里,而不是。怎么解决这个问题?

2 个答案:

答案 0 :(得分:1)

我想到的第一件事是在以下评论中:

        # Make this object the descriptor for field access.
        setattr(cls, self.name, self)

您声明您的字段实例应该充当描述符,但它不实现描述符协议所需的__get____set__。请注意,ForeignKey本身也不会实现这些,而是​​使用ReverseSingleRelatedObjectDescriptor

现在,在您的情况下发生的情况是序列化程序看到该字段是ForeignKey,因此它期望模型实例位于模型实例上的名称中。它调用getattr(obj, field.name),它通常会调用ReverseSingleRelatedObjectDescriptor的{​​{1}}方法并返回相关模型的实例。在您的情况下,特定__get__调用会返回您的字段实例(因为它缺少getattr)而不是__get__实例,这就是出错的时候。

所以,为了帮助解决你的问题,我会选择一种完全不同的方法。从您的代码中可以看出,您需要的只是一些方便的方法,这些方法可以从包含TreeItem的实例访问,这些实例会自动使用AutoTreeItemField指向的相关TreeItem

我宁愿将字段保持为常规ForeignKey,而是将便捷方法附加到模型类。如果你需要在几个模型中使用它们,你总是可以创建一个mixin,如果你想要它非常整洁,你可以创建一个自定义的ForeignKey子类,它只会覆盖它的ForeignKey以自动添加mixin到contribute_to_class

如果您需要cls.__bases__位于不同的字段名称中,您甚至可以通过在字段名称前加上它们来动态创建方法的名称,并将它们调整为提供字段名称一个参数。可能性几乎是无穷无尽的。

编辑:这可以通过创建一个mixin类来完成,该类包含名称前缀为ForeignKey的所有方便的额外方法。您还必须让他们接受所有额外参数,例如_TREEITEMyear_formatmonth_format等作为关键字参数。将这些保存在inmenu子类'ForeignKey方法中,就像当前一样,然后在其__init__中执行以下操作:

contribute_to_class

此外,在from django.utils.functional import curry ... def contribute_to_class(self, cls, name): super(...).contribute_to_class(cls, name) cls.__bases__ += (TreeItemMixin,) # Now curry all your handy methods to pass your extra parameters to them. extra_kwargs = { 'year_format': self.year_format, # ... } setattr(cls, '%s_get_year' % (self.name,), curry(cls._TREEITEM_get_year, **extra_kwargs)) # Repeat the previous line for all your methods. 子类中,您可以保留ForeignKey和信号处理程序,只需记住在模型实例上调用相应的方法即可。当您想要在视图或业务逻辑中调用它们时,您可以在模型实例上使用方便的curried自动生成别名,例如pre_save

另一种可能性是在obj.myfield_get_year()中实现描述符协议以返回有效的AutoTreeItemField实例,但在这种情况下,您将无法访问其额外的方法,因为描述符将返回模型实例而不是自己。

答案 1 :(得分:0)

我相信错误在这里:

def __init__(self, *args, **kwargs):
    super(AutoTreeItemField, self).__init__(TreeItem, null=True)

你应该通过其他(未命名的)args:

def __init__(self, *args, **kwargs):
    if not args:
        args = [TreeItem]
    options = {'null': True}
    options.update(kwargs)
    super(AutoTreeItemField, self).__init__(*args, **options)

修改

我可能找到了一些东西。在正常的FK中,假设它被声明为

other = models.ForeignKey(OtherClass)

instance.other的类型是OtherClass,而不是ForeignKey。 OtherClass实例具有_meta属性,因为它们模型。

在你的情况下,似乎(来自django.core.serializers.python traceback)它正在尝试序列化AutoTreeItemField实例(当然不是可序列化的)而不是相关的TreeItem。

来自django.core.serializers.python模块,l。 48:

def handle_fk_field(self, obj, field):
    related = getattr(obj, field.name)

你有使用contrib_to_class方法:

setattr(cls, self.name, self)
在ForeignKey的contrib_to_class方法中

setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self))

鉴于ForeignKey实例不是RelatedObjectDescriptor实例,所有django序列化程序都希望找到后者,即实际模型的实例(或在被称为__get__方法时返回实例)。

最后,您可能会从方法中删除setattr(cls ...)行。