[Fixed]-Customize related objects for many-to-many field in Django

1đź‘Ť

âś…

By default, what is shown for each item in a filter_horizontal is based on the object’s __str__ or __unicode__ method, so you could try something like the following:

class Game(TimeStampedModel):
    # field definitions
    # ...
    def __unicode__(self):
        return '{0} ({1})'.format(
            self.name,
            (', '.join(self.platforms.all()) if self.platforms.exists()
                else 'none')
        )

This will make each game show in the list (and everywhere else) as “Name (Platforms)”, for example “Crash Bandicoot (PS1, PS2)” or “Battlefield (none)” if it doesn’t have any platforms

Alternatively, if you don’t want to change the __unicode__ method of your model, you’ll need to set your ModelAdmin to use a custom ModelForm, specifying that the related_games field should use a custom ModelMultipleChoiceField with a custom FilteredSelectMultiple widget, in which you will need to override the render_options method. The following classes should be in their respective separate files, but it would look something like:

# admin.py

class GameAdmin(AdminImageMixin, reversion.VersionAdmin):
    # ...
    form = GameForm
    # ...


# forms.py

from django import forms

class GameForm(forms.ModelForm):
    related_games = RelatedGamesField()

    class Meta:
        fields = (
            'gid',
            'name',
            'platforms',
            'related_games',
        )


# fields.py

from django.forms.models import ModelMultipleChoiceField

class RelatedGamesField(ModelMultipleChoiceField):
    widget = RelatedGamesWidget()


# widgets.py

from django.contrib.admin.widgets import FilteredSelectMultiple

class RelatedGamesWidget(FilteredSelectMultiple):
    def render_options(self, choices, selected_choices):
        # slightly modified from Django source code
        selected_choices = set(force_text(v) for v in selected_choices)
        output = []
        for option_value, option_label in chain(self.choices, choices):
            if isinstance(option_label, (list, tuple)):
                output.append(format_html(
                    '<optgroup label="{0}">',
                    # however you want to have the related games show up, eg.,
                    '{0} ({1})'.format(
                        option_value.name,
                        (', '.join(option_value.platforms.all())
                            if option_value.platforms.exists() else 'none')
                    )
                ))
                for option in option_label:
                    output.append(self.render_option(selected_choices, *option))
                output.append('</optgroup>')
            else:
                output.append(self.render_option(selected_choices, option_value, option_label))
        return '\n'.join(output)
👤Grace B

Leave a comment