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.
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?
UPDATE 2 TO KARANTSTHR
Here is a screenshot with your implementation.
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.