pythondjangodjango-modelsdjango-formsdjango-templates

forms.ModelMultipleChoiceField with widget=FilteredSelectMultiple not working on custom new Django Admin


I am trying to show the forms.ModelMultipleChoiceField on custom new admin form page but it doesn't seem to show it the way it is showing on the Django page that was already made e.g. model product django admin page. Mine forms.ModelMultipleChoiceField is looking like this: Shows how my forms.ModelMultipleChoiceField looks like When it should look like this: Shows how forms.ModelMultipleChoiceField should look like

forms.py:

from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from django.contrib.admin.widgets import FilteredSelectMultiple
from home.models import Collection, Tag, Product

class ProductAssignForm(forms.Form):
    from_name = forms.CharField(required=True, max_length=255, label='From Name')
    to_name = forms.CharField(required=True, max_length=255, label='To Name')

    assign_collections_name = forms.ModelMultipleChoiceField(
        queryset=Collection.objects.all(),
        required=False,
        widget=FilteredSelectMultiple(
            verbose_name='Collections',
            is_stacked=False
        ),
        label='Assign Collection Name'
    )
    tags = forms.ModelMultipleChoiceField(
        queryset=Tag.objects.all(),
        required=False,
        widget=FilteredSelectMultiple(
            verbose_name='Tags',
            is_stacked=False
        ),
        label='Tags'
    )

    class Meta:
        model = Product
        fields = ['collections', 'tags']  # Include the tags field in the fields list

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper(self)
        self.helper.form_method = 'POST'
        self.helper.add_input(Submit('submit', 'Assign Products'))

partial code form admin.py:

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    form = ProductAdminForm
    list_display = ('name', 'quantity', 'price', 'first_collection')
    exclude = ('user', 'updated',)

    def save_model(self, request, obj, form, change):
        if not obj.user:
            obj.user = request.user
        obj.save()

    def first_collection(self, obj):
        first_collection = obj.collections.first()
        return first_collection.name if first_collection else 'No collection'

    def get_urls(self):
        # return [
        #     path('assign-products/', self.admin_site.admin_view(self.assign_products), name='assign-products'),
        # ] + super().get_urls()
        custom_urls = [
            path('assign-products/', self.admin_site.admin_view(self.assign_products), name='assign-products'),
            *super().get_urls(),
        ]
        return custom_urls
    
    def assign_products(self, request):
        opts = self.model._meta
        if request.method == 'POST':
            form = ProductAssignForm(request.POST)
            if form.is_valid():
                from_name = form.cleaned_data['from_name']
                to_name = form.cleaned_data['to_name']
                assign_collections_name = form.cleaned_data['assign_collections_name']
                tags = form.cleaned_data['tags']

                print(f"Searching products from '{from_name}' to '{to_name}'")

                # Normalizing the names by removing whitespace and non-alphanumeric characters
                from_name_normalized = ''.join(e for e in from_name if e.isalnum()).lower()
                to_name_normalized = ''.join(e for e in to_name if e.isalnum()).lower()

                # Search by search_name
                products = Product.objects.filter(
                    search_name__gte=from_name_normalized,
                    search_name__lte=to_name_normalized
                )

                print(f"search_handle__gte={from_name_normalized}, search_handle__lte={to_name_normalized}")
                print(f"Found {products.count()} products")

                for product in products:
                    if assign_collections_name:
                        print(f"Assigning collections to product '{product.name}'")
                        product.collections.set(assign_collections_name)
                    if tags:
                        print(f"Assigning tags to product '{product.name}'")
                        product.tags.set(tags)
                    product.save()

                return HttpResponseRedirect(request.path_info)
        else:
            form = ProductAssignForm()

        context = dict(
            self.admin_site.each_context(request),
            title="Assign Products",
            form=form,
            opts=opts,
            **self.admin_site.each_context(request),
        )
        return render(request, 'admin/assign_products.html', context)

assign_products.html:

{% block extrahead %}
    {{ block.super }}
    {{ form.media }}
    <link rel="stylesheet" type="text/css" href="{% static 'css/admin_custom.css' %}">
    <script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script>
    <script type="text/javascript" src="{% static 'admin/js/core.js' %}"></script>
    <script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script>
    <script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.init.js' %}"></script>
    <script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.form.js' %}"></script>
{% endblock %}

{% block javascript %}
    {{ block.super }}
    <script type="text/javascript">
        django.jQuery(document).ready(function() {
            django.jQuery('.selectfilter').filterchain();
        });
    </script>
{% endblock %}

{% block breadcrumbs %}
<div class="breadcrumbs">
    <a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
    &rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
    &rsaquo; <a href="{% url 'admin:assign-products' %}">{% translate 'Assign Products' %}</a>
</div>
{% endblock %}

{% block content %}
<div class="container">
    <h1>{% translate 'Assign Products' %}</h1>
    <form action="." method="post">
        {% csrf_token %}
        {% for field in form %}
            <div class="form-group">
                <label for="{{ field.id_for_label }}">{{ field.label }}</label>
                {{ field }}
                {% if field.help_text %}
                    <small class="form-text text-muted">{{ field.help_text }}</small>
                {% endif %}
                {% for error in field.errors %}
                    <div class="text-danger">{{ error }}</div>
                {% endfor %}
            </div>
        {% endfor %}
        <button type="submit" class="btn btn-primary">{% translate 'Assign Products' %}</button>
    </form>
</div>
{% endblock %}

I have tried crispy forms and normal forms, I have tried asking ChatGPT and ClaudeAI and even searching google.

Help would be really appreciated!

My models.py code:

class Collection(models.Model):
    """
    TODO: Modify product method to get all the products in the collection and the subcollections.
    TODO: THe functionality of default .all() is renamed to .all_self()
    """
    parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='subcollections')  # Store the parent collection, linked to itself
    cid = ShortUUIDField(unique=True, length=10, max_length=15, prefix="col", alphabet="abcdefghijklmnopqrstuvwxyz0123456789", editable=False)
    name = models.CharField(max_length=255)
    description = models.TextField(default='No description provided.', blank=True, null=True)
    images = models.ManyToManyField('Image', related_name='collections', blank=True)
    has_image = models.BooleanField(default=False)
    products = models.ManyToManyField('Product', related_name='collections', blank=True)
    tags = models.ManyToManyField('Tag', related_name='collections', blank=True)
    product_count = models.IntegerField(default=0)  # Store the number of products in this collection only
    total_product_count = models.IntegerField(default=0)  # Store the number of products in this collection and all subcollections
    created_at = models.DateTimeField(auto_now_add=True)  # Stores when the collection field was created
    handle = models.CharField(max_length=255, null=True, blank=True)  # This will be used to generate the URL
    active = models.BooleanField(default=True)  # This will be used to determine if the collection is active or not


class Product(models.Model):
    pid = ShortUUIDField(unique=True, length=10, max_length=20, prefix="prd", alphabet="abcdefghijklmnopqrstuvwxyz0123456789", editable=False)

    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)

    name = models.CharField(max_length=255, default="Fresh Pear")
    images = models.ManyToManyField('Image', related_name='products', blank=True)
    description = models.TextField(null=True, blank=True, default="This is the description")

    price = models.DecimalField(max_digits=9999999999, decimal_places=2, default="0.00")
    old_price = models.DecimalField(max_digits=9999999999, decimal_places=2, default="0.00", blank=True)

    specifications = models.JSONField(null=True, blank=True, default=dict)
    product_status = models.CharField(choices=PRODUCT_STATUS, max_length=10, default="in_review")
    status = models.BooleanField(default=True)
    # in_stock = models.BooleanField(default=True)
    quantity = models.DecimalField(max_digits=9999999999, decimal_places=0, default=0, blank=True)
    featured = models.BooleanField(default=False)
    digital = models.BooleanField(default=False)

    sku = ShortUUIDField(unique=True, length=8, max_length=11, prefix="sku", alphabet="0123456789")

    date = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(null=True, blank=True)

    handle = models.CharField(unique=True, max_length=255, null=True, blank=True)  # This will be used to generate the URL
    search_name = models.CharField(max_length=255, null=True, blank=True)  # This will be used to search the product

    tags = models.ManyToManyField('Tag', related_name='products', blank=True)
    # product.collections.[method] refers to Collection MTM Field declared in collection model
    # tags = models.ForeignKey(Tag, on_delete=models.SET_NULL, null=True)

    class Meta:
        verbose_name_plural = "Products"

    def __str__(self):
        return self.name
    
    def get_percentage(self):
        # (1000 - 900) / 1000 * 100 = 10% discount
        # Handle the cases of zero
        if self.old_price == 0:
            return 0
        return (self.old_price - self.price) / self.old_price * 100
    
    def save(self, *args, **kwargs):
        self.updated = timezone.now()
        super().save(*args, **kwargs)

Solution

  • Add the following code to your class, in this case, it is ProductAssignForm which is where I created my form and wrote the code that displayed Model.MultipleChoiceField with widget as FilteredSelectMultiple.

    The code is:

    class Media:
            # css = {
            #     'all':['admin/css/widgets.css',
            #            'css/uid-manage-form.css'],
            # }
            # Adding this javascript is crucial
            js = ['/admin/jsi18n/']
    

    Now, just refresh the page and it should work.

    Feel free to ask any questions you may like!