保存MPTTModel时的AttributeError

时间:2014-01-31 12:33:19

标签: django django-mptt

我正在尝试使用django-mptt在Django中构建一个菜单应用程序来创建嵌套的菜单项。构建树时,菜单项应按menu_order排序。

问题在于,无论何时添加嵌套菜单项,重新排序并保存菜单,都会引发此错误:

'NoneType' object has no attribute 'tree_id'

为了能够保存菜单,我必须从Django shell手动重建树,这并不总是有用,或者从子项删除父关系。

从MenuItem模型中删除order_insertion_by = ['menu_order']时,所有内容(排序除外)均按预期工作。

models.py:

class Menu(models.Model):
    POSITIONS = Choices(('header', _('Header')), ('footer', _('Footer')))

    title = models.CharField(max_length=255, default='')
    position = models.SlugField(choices=POSITIONS, max_length=64, default='')

    def save(self, *args, **kwargs):
        MenuItem.objects.rebuild()
        super(Menu, self).save(*args, **kwargs)

class MenuItem(MPTTModel):
    content_type = models.ForeignKey(ContentType, blank=True, null=True)
    object_id = models.PositiveIntegerField(blank=True, null=True)
    linked_object = generic.GenericForeignKey()

    menu = models.ForeignKey('Menu', related_name='menu_items')
    parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
    menu_order = models.PositiveSmallIntegerField(default=0)

    class MPTTMeta:
        order_insertion_by = ['menu_order']

admin.py:

class MenuItemInline(admin.StackedInline):
    model = MenuItem
    extra = 0
    sortable_field_name = 'menu_order'

    autocomplete_lookup_fields = {
        'generic': [['content_type', 'object_id']]
    }

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        field = super(MenuItemInline, self).formfield_for_foreignkey(db_field, request, **kwargs)

        if db_field.name == 'parent':
            if request._obj_ is not None:
                field.queryset = field.queryset.filter(menu=request._obj_)  
            else:
                field.queryset = field.queryset.none()

        return field

class MenuAdmin(admin.ModelAdmin):
    inlines = (MenuItemInline,)

    def get_form(self, request, obj=None, **kwargs):
        request._obj_ = obj
        return super(MenuAdmin, self).get_form(request, obj, **kwargs)

admin.site.register(Menu, MenuAdmin)

追溯:

Traceback:
File "/.../django/core/handlers/base.py" in get_response
  115.                         response = callback(request, *callback_args, **callback_kwargs)
File "/.../django/contrib/admin/options.py" in wrapper
  372.                 return self.admin_site.admin_view(view)(*args, **kwargs)
File "/.../django/utils/decorators.py" in _wrapped_view
  91.                     response = view_func(request, *args, **kwargs)
File "/.../django/views/decorators/cache.py" in _wrapped_view_func
  89.         response = view_func(request, *args, **kwargs)
File "/.../django/contrib/admin/sites.py" in inner
  202.             return view(request, *args, **kwargs)
File "/.../django/utils/decorators.py" in _wrapper
  25.             return bound_func(*args, **kwargs)
File "/.../django/utils/decorators.py" in _wrapped_view
  91.                     response = view_func(request, *args, **kwargs)
File "/.../django/utils/decorators.py" in bound_func
  21.                 return func(self, *args2, **kwargs2)
File "/.../django/db/transaction.py" in inner
  223.                 return func(*args, **kwargs)
File "/.../django/contrib/admin/options.py" in change_view
  1106.                 self.save_related(request, form, formsets, True)
File "/.../django/contrib/admin/options.py" in save_related
  764.             self.save_formset(request, form, formset, change=change)
File "/.../django/contrib/admin/options.py" in save_formset
  752.         formset.save()
File "/.../django/forms/models.py" in save
  514.         return self.save_existing_objects(commit) + self.save_new_objects(commit)
File "/.../django/forms/models.py" in save_existing_objects
  634.                 saved_instances.append(self.save_existing(form, obj, commit=commit))
File "/.../django/forms/models.py" in save_existing
  502.         return form.save(commit=commit)
File "/.../django/forms/models.py" in save
  370.                              fail_message, commit, construct=False)
File "/.../django/forms/models.py" in save_instance
  87.         instance.save()
File "/.../mptt/models.py" in save
  794.                                 self._tree_manager._move_node(self, rightmost_sibling, 'right', save=False)
File "/.../mptt/managers.py" in _move_node
  414.                 self._make_sibling_of_root_node(node, target, position)
File "/.../mptt/managers.py" in _make_sibling_of_root_node
  769.                     new_tree_id = getattr(right_sibling, self.tree_id_attr)

Exception Type: AttributeError at /admin/menus/menu/2/
Exception Value: 'NoneType' object has no attribute 'tree_id'

'NoneType'指的是right_sibling,即无。

原因追溯到上面的三行,其中设置了right_sibling

right_sibling = target.get_next_sibling()

get_next_sibling即使有下一个兄弟,也会返回无。

重新排序最后两个菜单项时,我最终会得到两个具有相同tree_idlftrght值的根节点。这导致get_next_sibling函数在最后两个节点的tree_id__gt=4均为4时查询tree_id的节点。

MenuItem对象在每个Menu对象上使用内联管理进行管理。它们可以使用Grappelli的可排序内联重新排序。新创建的子节点获得比以下根节点更高的menu_order值似乎存在问题。

我使用的是Python 2.7.4,Django 1.5.5和django-mptt 0.6.0。

这是django-mptt中的错误还是我做错了什么?

2 个答案:

答案 0 :(得分:0)

我通过添加另一个排序属性并在保存formset时重建菜单来解决这个问题。这可能不是最佳解决方案,现在每次保存都需要几秒钟。

models.py:

class MenuItem:
    parent_order = models.PositiveSmallIntegerField(default=0)

    def save(self, **kwargs):
        if self.parent:
            self.parent_order = self.parent.menu_order

        super(MenuItem, self).save(**kwargs)

    class MPTTMeta:
        order_insertion_by = ['parent_order', 'menu_order']

admin.py:

class MenuAdmin(MarketRelatedAdmin):
    def save_formset(self, request, form, formset, change):
        formset.save()
        MenuItem.objects.rebuild()

答案 1 :(得分:0)

我通过在TreeManager中添加两行来解决这个问题。

def _make_sibling_of_root_node(self, node, target, position):
    ...
    elif position == 'right':
                if target_tree_id > tree_id:
                    new_tree_id = target_tree_id
                    lower_bound, upper_bound = tree_id, target_tree_id
                    shift = -1
                else:
                    right_sibling = target.get_next_sibling()
                    if node == right_sibling:
                        return
                    # Addition
                    if not right_sibling:
                        return
                    # end of addition
                    new_tree_id = getattr(right_sibling, self.tree_id_attr)
                    lower_bound, upper_bound = new_tree_id, tree_id
                    shift = 1