5

The default widget for ManyToManyFields in django-admin is difficult to use. I can set filter_horizontal on individual fields and get a much nicer widget.

How can I set filter_horizontal as the default on all ManyToManyFields?

(I'd also be happy with filter_vertical of course.)

I've searched around for a solution and didn't find anything on Google or SO. I can think of how to do this with some meta-programming, but if someone already did this or if it's in Django somewhere, I'd love to hear about it.

Jerome Baum
  • 736
  • 6
  • 15

2 Answers2

4

The best way to modify classes defined in pre-existing code is to use a mixin. You need to modify the formfield_for_manytomany method of ModelAdmin class; the method is defined in BaseModelAdmin.

Add the following code in a module that's guaranteed to run when your Django server starts up [a models.py of one of your own apps]:

from django.contrib.admin.options import ModelAdmin
from django.contrib.admin import widgets
class CustomModelAdmin:
    def formfield_for_manytomany(self, db_field, request=None, **kwargs):
        """
        Get a form Field for a ManyToManyField.
        """
        # If it uses an intermediary model that isn't auto created, don't show
        # a field in admin.
        if not db_field.rel.through._meta.auto_created:
            return None
        db = kwargs.get('using')

        if db_field.name in self.raw_id_fields:
            kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel, using=db)
            kwargs['help_text'] = ''
        else:
            kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, False) # change second argument to True for filter_vertical

        return db_field.formfield(**kwargs)

ModelAdmin.__bases__ = (CustomModelAdmin,) + ModelAdmin.__bases__

Note (27 Aug 2019):

I'm fully aware of how subclassing/inheritance works, and that's best practice for solving problems like this. However, as I've reiterated in the comments below, subclassing will not solve the OP's problem as stated ie. making filter_horizontal or filter_vertical the default. With subclassing, not only will you need to register your subclass for all your models, you'll have to unregister each ModelAdmin subclass that's registered in builtin Django apps and third-party apps you've installed, then register your new ModelAdmin subclasses. So for example for Django's builtin User model ...

admin.site.unregister(User)
class CustomModelAdmin(admin.ModelAdmin):
    """ Add your changes here """
admin.site.register(User, CustomModelAdmin)

... then repeat similar code for all Django apps and third-party apps you've installed. I don't think this is what the OP wanted, hence my answer.

Simon Kagwi
  • 1,636
  • 2
  • 19
  • 25
  • Why not just do this with single inheritance? Or indeed, multiple inheritance, rather than monkeying around with magic properties like `__bases__`? – Marcin Jan 02 '12 at 12:46
  • Please explain what you mean, or propose your own solution by posting an answer. My own understanding of the question is that the OP wants `filter_horizontal` used on all `ManyToManyField`s without having to create a `ModelAdmin` subclass for each model that has such a field. – Simon Kagwi Jan 02 '12 at 12:58
  • What part of my comment is unclear? The only thing that is unclear is why you are suggesting monkey-patching a library class rather than using inheritance. – Marcin Jan 02 '12 at 13:00
  • Sure you can use inheritance, but that would mean either subclassing `(SomeOtherClass, ModelAdmin)` or `(CustomModelAdminClass)` for each model with a `ManyTOManyField`, and registering using `admin.site.register` for each model. Unless I did not understand the question, the OP is trying to avoid that, and just have a different widget used as the default. – Simon Kagwi Jan 02 '12 at 13:12
  • Hey. For the question as stated, @SimonKagwi is right -- if I can set this in one place, that is preferred. However I don't know if I like the idea of messing around with `__bases__` -- a "clean" solution (e.g. something in `settings`) would be nicer. If that kind of solution doesn't exist, I prefer many `register` calls to the `__bases__` magic above. Like I said in the question: I can already do this with meta-programming, so I'm hoping to hear about a simpler solution. @Marcin, how would you approach this problem? I'd love to see your solution. – Jerome Baum Jan 02 '12 at 15:16
  • @JeromeBaum: Have CustomModelAdmin inherit from ModelAdmin. Use CustomModelAdmin for all the models you want to treat in this way. Job done. – Marcin Jan 02 '12 at 15:50
  • @Marcin: this is the normal way of doing it, and I'm fully aware of that. But that **does not make filter_vertical or filter_horizontal the default widget on ManyToManyFields**, which I believe is what Jerome wanted to achieve. – Simon Kagwi Jan 02 '12 at 18:44
  • @SimonKagwi: It would be the default on every model which used CustomModelAdmin. – Marcin Jan 02 '12 at 18:55
  • I'm going to wait a couple of days or so and then mark this answer as correct if there are no other responses. I am going to use multiple `register` calls (@Marcin what you said), but @SimonKagwi, you posted `CustomModelAdmin` and answered the question as stated. I also up-voted this to counter the down-vote -- I think that's a bit harsh given that most of the answer is very useful and it's only the `__bases__` stuff that lead to the down-vote. – Jerome Baum Jan 03 '12 at 16:05
  • If you use this on any version of Django other than whichever version this was copied from, you'll break stuff. Your colleagues will also curse you for injecting behaviour that there's no sign of in the inheritance hierarchy. My [answer](https://stackoverflow.com/a/57676539/1308967) is slightly better but not much. – Chris Aug 27 '19 at 14:14
  • @Chris I've added a note about why I posted my answer – Simon Kagwi Aug 27 '19 at 20:03
  • That's cool @SimonKagwi, wasn't a personal comment, tangents are often educational and you must know inheritance if you know `__bases__` - that's not basic Python! Although you don't need to unregister - you must have something scanning and registering things automatically if you need this - my code below works, uses inheritance, doesn't use unregister, and is much less likely to break after a Django upgrade. All the best though, no offence or criticism intended! I learned from your post and couldn't have devised mine without it! – Chris Aug 27 '19 at 22:56
0

Still slightly hacky, but better than the other answer, which would have caused all sorts of pain during Django upgrades. Inherit from this (don't monkey-patch):

class BaseAdmin(models.Admin):
    def formfield_for_manytomany(self, db_field, request=None, **kwargs):
        """
        Get a form Field for a ManyToManyField. Tweak so filter_horizontal 
        control used by default. If raw_id or autocomplete are specified
        will take precedence over this.
        """
        filter_horizontal_original = self.filter_horizontal
        self.filter_horizontal = (db_field.name,)
        form_field = super().formfield_for_manytomany(db_field, request=None, **kwargs)
        self.filter_horizontal = filter_horizontal_original
        return form_field


@admin.register(AcmeModel)
class AcmeModelAdmin(BaseAdmin):
    # Sub-classes can still specify raw_id, autocomplete, etc.
    # That will override our filter_horizontal defaulting.
    pass

For some reason setting filter_horizontal in __init__ doesn't work, and there's no get_filter_horizontal to override.

As discussed above, avoid monkey-patching and just inherit from BaseClass. You and especially your colleagues will be grateful when you come back to this in 6 months and can't find this in the inheritance hierarchy.

Chris
  • 5,664
  • 6
  • 44
  • 55