I'm trying to build a menu app in Django using django-mptt to create nested menu items. The menu items should be ordered by menu_order
when the tree is built.
The problem is that whenever I add nested menu items, reorder them and save the menu, this error is raised:
'NoneType' object has no attribute 'tree_id'
To be able to save the menu I have to either manually rebuild the tree from Django shell, which does not always help, or remove the parent relation from the child items.
When removing order_insertion_by = ['menu_order']
from the MenuItem model, everything (except ordering) works as intended.
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)
The traceback:
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' refers to right_sibling
which is None.
The cause traces back to three lines above, where right_sibling
is set:
right_sibling = target.get_next_sibling()
get_next_sibling
returns None even though there is a next sibling.
When reordering the two last menu items, I sometimes end up with two root nodes with the same tree_id
, lft
, and rght
values. This is causing the get_next_sibling
function to query for a node where tree_id__gt=4
when the last two nodes both have a tree_id
of 4.
The MenuItem objects are managed with an inline admin on every Menu object. They can be reordered using Grappelli's sortable inlines. There seems to be an issue with newly created child nodes getting a higher menu_order value than the following root nodes.
I'm using Python 2.7.4, Django 1.5.5, and django-mptt 0.6.0.
Is this a bug in django-mptt or am I doing something wrong?