Question

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?

Was it helpful?

Solution

I solved this by adding another sorting attribute and rebuilding the menu when the formset is saved. This is probably not the most optimal solution, every save now takes a few extra seconds.

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()

OTHER TIPS

I solved this by adding two lines to the 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
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top