38

I have an object with a ManyToMany relation with another object.
In the Django Admin this results in a very long list in a multiple select box.

I'd like to filter the ManyToMany relation so I only fetch Categories that are available in the City that the Customer has selected.

Is this possible? Will I have to create a widget for it? And if so—how do I copy the behavior from the standard ManyToMany field to it, since I would like the filter_horizontal function as well.

These are my simplified models:

class City(models.Model):
    name = models.CharField(max_length=200)


class Category(models.Model):
    name = models.CharField(max_length=200)
    available_in = models.ManyToManyField(City)
    

class Customer(models.Model):
    name = models.CharField(max_length=200)
    city = models.ForeignKey(City)
    categories = models.ManyToManyField(Category)
cxxl
  • 4,939
  • 3
  • 31
  • 52
schmilblick
  • 1,917
  • 1
  • 18
  • 25

7 Answers7

39

Ok, this is my solution using above classes. I added a bunch more filters to filter it correctly, but I wanted to make the code readable here.

This is exactly what I was looking for, and I found my solution here: http://www.slideshare.net/lincolnloop/customizing-the-django-admin#stats-bottom (slide 50)

Add the following to my admin.py:

class CustomerForm(forms.ModelForm): 
    def __init__(self, *args, **kwargs):
        super(CustomerForm, self).__init__(*args, **kwargs)
        wtf = Category.objects.filter(pk=self.instance.cat_id);
        w = self.fields['categories'].widget
        choices = []
        for choice in wtf:
            choices.append((choice.id, choice.name))
        w.choices = choices


class CustomerAdmin(admin.ModelAdmin):
    list_per_page = 100
    ordering = ['submit_date',] # didnt have this one in the example, sorry
    search_fields = ['name', 'city',]
    filter_horizontal = ('categories',)
    form = CustomerForm

This filters the "categories" list without removing any functionality! (ie: i can still have my beloved filter_horizontal :))

The ModelForms is very powerful, I'm a bit surprised it's not covered more in the documentation/book.

schmilblick
  • 1,917
  • 1
  • 18
  • 25
  • I noticed that after adding this code into a project I have that the selected options box (would be under chosen "Chosen categories" in your example) is empty even after selecting an option from the the 'Available categories' field. Did I miss something in implementing this? – Silfheed Aug 25 '09 at 03:44
  • Further reduction using list comprehension: self.fields['categories'].widget.choices = [(choice.id, choice.name) for choice in wtf] – Roberto Rosario Mar 25 '10 at 20:17
  • How to make categories filed read only. I am try read only_fields = ('users',) . But Its shown in single line separated by comma. I want to shown in line break ... – Varnan K Dec 31 '14 at 08:25
  • 1
    Note that it isn't necessary to build `widget.choices` yourself. It is sufficient to set the `field.queryset`: `self.fields['categories'].queryset = Category.objects.filter(pk=self.instance.cat_id)` – Fraser Harris Jun 18 '16 at 15:37
  • 1
    @FraserHarris you are not the hero we deserve, but you are the hero we need :D – Rodolfo Abarca Feb 17 '20 at 22:05
18

As far as i can understand you, is that you basically want to filter the shown choices according to some criteria (category according to city).

You can do exactly that by using limit_choices_to attribute of models.ManyToManyField. So changing your model definition as...

class Customer(models.Model):
    name = models.CharField(max_length=200)
    city = models.ForeignKey(City)
    categories = models.ManyToManyField(Category, limit_choices_to = {'available_in': cityId})

This should work, as limit_choices_to, is available for this very purpose.

But one things to note, limit_choices_to has no effect when used on a ManyToManyField with a custom intermediate table. Hope this helps.

simplyharsh
  • 35,488
  • 12
  • 65
  • 73
  • This looks like it could work! However... it made me realise i have to re-model my models :) Im reading in the docs that the admin dont care about the limit_choices_to as well, whats your take on that? – schmilblick Aug 05 '09 at 06:39
  • I'm trying to do the exact same thing the way you descripe @sim, but I get an error of `ValueError at /admin/foo/bar/: invalid literal for int() with base 10: 'city'`. Is there something I'm missing with how to implement this method of filtering? – nhinkle Oct 13 '13 at 06:44
  • @nhinkle That 'city' in the value is supposed to mean Id of the city object you want categories to limit to. My apology. I'll edit the answer to be more clear. – simplyharsh Oct 15 '13 at 10:12
11

Another way is with formfield_for_manytomany in Django Admin.

class MyModelAdmin(admin.ModelAdmin):
    def formfield_for_manytomany(self, db_field, request, **kwargs):
        if db_field.name == "cars":
            kwargs["queryset"] = Car.objects.filter(owner=request.user)
        return super(MyModelAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)

Considering that "cars" are the ManyToMany field.

Check this link for more info.

xleon
  • 6,201
  • 3
  • 36
  • 52
2

I think this is what you're looking for:

http://blog.philippmetzler.com/?p=52

we use django-smart-selects:

http://github.com/digi604/django-smart-selects

Philipp

Googol
  • 2,815
  • 2
  • 22
  • 13
  • 1
    Could you expand on your answer with examples? This is practically an URL only answer. Why is that blog what they're after? Why do you use Django-Smart-Selects? – AncientSwordRage Jan 21 '15 at 17:08
1

Since you're selecting the customer's city and categories in the same form, you would need some javascript to dynamically whittle down the categories selector to just the categories available in the city selected.

hopti
  • 396
  • 3
  • 4
  • I dont feel to keen on iterating on tens of thousands of DOM elements with javascript and comparing to another huge list. I'd say Javascript is definatly not the way to go, this has to be done back end when selecting out the Categories from the database. – schmilblick Aug 04 '09 at 11:52
0

Like Ryan says, there has to be some javascript to dynamically change the options based on what the user selects. The posted solution works if city is saved and the admin form is reloaded, thats when the filter works, but think of a situation where a user wants to edit an object and then changes the city drop down but the options in category wont refresh.

-2
Category.objects.filter(available_in=cityobject)

That should do it. The view should have the city that the user selected, either in the request or as a parameter to that view function.

AlbertoPL
  • 11,479
  • 5
  • 49
  • 73
  • But im talking about the django admin, are you saying i should duplicate the standard view and add the above? – schmilblick Aug 04 '09 at 11:24
  • Ah, I totally missed the whole "Django Admin" part of your question title. I still think this is the correct approach, although I'm not exactly sure where you would put it, or if this is even possible. – AlbertoPL Aug 04 '09 at 11:41