Search code examples
djangodjango-admindjango-admin-filtersdjango-autocomplete-light

Django Admin change_list filtering multiple ManyToMany


In the Django-Admin you have the possibility to define list_filter on fields of the model. This is working for ManyToMany-Fields as well.

class ModelA(models.Model):
    name = models.CharField(max_length=100, verbose_name="Name")

class ModelB(models.Model):
    model_a_relation = models.ManyToManyField(ModelA)

class ModelBAdmin(ModelAdmin):
    list_filter = [model_a_relation, ]

admin.site.register(ModelB, ModelBAdmin)

Now, I can filter my list of elements of ModelB by relation to ModelA in the Admin object_list of ModelB.

Now my question: Is it possible to filter by multiple objects of ModelA?

In the change_view of ModelB I use django-autocomplete-light to define relations. Can I use this widget to filter in change_list, too?

I imagine the query in the background of this filter like ModelB.objects.filter(model_a_relation__in=names), where names is a list of the chosen objects of ModelA.

Thanks, Horst


Solution

  • I made a really dirty try to solve my issue. It works and I want to share it with you.

    For better understanding I use new Models in this example:

    class Tag(models.Model):
        name = models.CharField(max_length=100, verbose_name="Name")
    
    class Book(models.Model):
        tags = models.ManyToManyField(Tag)
        read = models.BooleanField()
    
    class BookAdmin(ModelAdmin):
        list_filter = ['read', ]
    
    admin.site.register(Book, BookAdmin)
    

    At first, I overwrote the changelist_view of the BookAdmin.

    def changelist_view(self, request, extra_context=None):
    
        extra_context = extra_context if extra_context else {}
    
        q = request.GET.copy()
    
        tags = Tag.objects.all().values('id', 'name')
    
        current_tags = q.get('tags__id__in', [])
        tag_query = request.GET.copy()
        if current_tags:
            tag_query.pop('tags__id__in')
            current_tags = current_tags.split(',')
            all_tag = False
        else:
            all_tag = True
        for tag in tags:
            if str(tag['id']) in current_tags:
                tag['selected'] = True
                temp_list = list(current_tags)
                temp_list.remove(str(tag['id']))
                tag['tag_ids'] = ','.join(temp_list)
            else:
                tag['selected'] = False
                tag['tag_ids'] = ','.join(current_tags)
    
        extra_context['tag_query'] = '?' if len(tag_query.urlencode()) == 0 else '?' + tag_query.urlencode() + '&'
        extra_context['all_tag'] = all_tag
        extra_context['tags'] = tags
    
        return super(BookAdmin, self).changelist_view(request, extra_context=extra_context)
    

    As you can see, I look in GET, whether there some Tags are chosen or not. Then I build new GET-Parameter for each possible Tag.

    And then there is my overwriten change_list.html

    {% extends "admin/change_list.html" %}
    
    {% block content %}
        {{ block.super }}
    
        <h3 id="custom_tag_h3"> Fancy Tag filter</h3>
        <ul id="custom_tag_ul">
    
            <li{% if all_tag %} class="selected"{% endif %}>
                <a href="{{ tag_query }}">All</a>
            </li>
    
            {% for tag in tags %}
                <li{% if tag.selected %} class="selected"{% endif %}>
                    <a href="{{ tag_query }}tags__id__in={{ tag.tag_ids }}{% if not tag.selected %}{% if tag.tag_ids %},{% endif %}{{ tag.id }}{% endif %}">{{ tag.name }}</a>
                </li>
            {% endfor %}
    
        </ul>
    
        <script type="text/javascript">
            $('#changelist-filter').append($('#custom_tag_h3'));
            $('#custom_tag_h3').after($('#custom_tag_ul'));
        </script>
    
    {% endblock content %}
    

    This way I have a filter looking like the boolean read-filter where I can activate more than one option. By clicking on the filter, a new id is added to the query. Another click on already selected option removes id from query. Click on All removes the hole tags_in-parameter from URL.