Search code examples
djangodjango-mptt

Django: using mptt-django to allocate products to categories in a many-to-many relationship


I have a many-to-many relationship between products and categories. I am using mptt to manage the display of categories on the admin (which works fine) and so I can allocate products to categories.

While the categories are working fine, it's the product side I'm having problems with.

In my models.py I have (truncated for brevity):

from mptt.models import MPTTModel,TreeForeignKey,TreeManyToManyField

class ProductCategory(MPTTModel):
    parent = TreeForeignKey('self', null=True, blank=True)

class Product(models.Model)
    categories = TreeManyToManyField(ProductCategory)

In my admin.py I have

from django.forms import CheckboxSelectMultiple
from .models import Product,ProductCategory
from django.db import models
from django.contrib import admin
from mptt.admin import MPTTModelAdmin
from mptt.models import TreeManyToManyField

class ProductAdmin(admin.ModelAdmin):

   formfield_overrides = {
      TreeManyToManyField:{'widget':CheckboxSelectMultiple},
   }

This almost gets the job done but all of the categories are displayed with checkboxes which is not what I want since you don't want to add products to categories which have sub categories.

enter image description here

What I want is the display of categories such that you can only add products to categories which don't have children, so in this example

Category 1
Category 2 sub sub 
Category 3 sub

And the labels would be

Category 1
Category 2 > Catgory 2 sub > Category 2 sub sub
Category 3 > Category 3 sub

Many thanks for any help!

UPDATE

Hi, I realise me question may be too broad!

As a Django newbie, what I think I need to do is create a custom widget possibly overriding the CheckboxSelectMultiple widget, that I can apply to my 'categories' model attribute.

At the moment, I am using formfield_overrides for the TreeManyToManyField as discussed above

formfield_overrides = {
    TreeManyToManyField:{'widget':CheckboxSelectMultiple},
}

Not only does this not give the desired result but also rather than using the standard CheckboxSelectMultiple for all TreeManyToMany fields, can I specify this widget just for my 'categories' attribute?

Notwithstanding this aside, would it be possible to show me how I can create a custom widget, passing the information about the category tree and selected states (for a product which has already been allocated to categories)? I've been trying to sort this myself but have been struggling - any help much appreciated!

UPDATE FOLLOWING KARANTSTHR COMMENT

Here is a screenshot of the categories - all good in showing the categories with no children, but can we show the parents to these categories in the label as described above?

enter image description here

UPDATE 2 TO KARANTSTHR

Here is a screenshot with your implementation.

enter image description here


Solution

  • ModelAdmin is very flexible, you can specify custom ModelForm for form option in ProductAdmin (form is ModelAdmin option ) to filter out categories not having any child category.
    So, I think you don't need custom widget to solve your problem, just change admin.py as

    from django import forms
    from django.forms import CheckboxSelectMultiple
    from .models import Product,ProductCategory
    from django.contrib import admin
    from mptt.admin import MPTTModelAdmin
    from mptt.models import TreeManyToManyField
    
    
    class filterCategories(forms.ModelForm):
        class Meta:
            model = Product
            fields = '__all__'
    
        def __init__(self,*args,**kwargs):
            super(filterCategories, self).__init__(*args,**kwargs)
            self.fields['categories'].queryset = ProductCategory.objects.filter(children=None)
    
    
    class ProductAdmin(admin.ModelAdmin):
        form = filterCategories
        formfield_overrides = { TreeManyToManyField:{'widget':CheckboxSelectMultiple},}
    
    
    admin.site.register(Product,ProductAdmin)
    admin.site.register(ProductCategory,MPTTModelAdmin)
    


    UPDATE 1
    you need to add related_name in parent field of ProductCategory class in models.py
    So ProductCategory class in models.py would be as below

    class ProductCategory(MPTTModel):
        name = models.CharField(max_length=100)
        parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
    
        def __str__(self):
            return self.name
    

    UPDATE 2
    you can change str method for ProductCategory as follow for labels

    def __str__(self):
        try:
            ancestors = self.get_ancestors(include_self=True)
            ancestors = [i.name for i in ancestors]
        except:
            ancestors = [self.name]
    
        return ' > '.join(ancestors[:len(ancestors) + 1])
    

    note that this is not a radical approach for gaining label ( object's name ) of your choice, I will update my answer if I find a better solution.