在Django中为多对多字段自定义相关对象

时间:2017-01-06 15:24:51

标签: python django django-admin many-to-many

有游戏实体,每个游戏都可以有1个或更多平台。此外,每个游戏可以有1个或多个相关游戏的链接(使用自己的平台)。这里看起来像 models.py

class Game(TimeStampedModel):
    gid = models.CharField(max_length=38, blank=True, null=True)
    name = models.CharField(max_length=512)
    platforms = models.ManyToManyField(
        Platform, blank=True, null=True)
    ...
    #here is the self-referencing m2m field
    related_games = models.ManyToManyField(
        "self", related_name="related", blank=True)

此模型在 admin.py

中提供此代码
@admin.register(Game)
class GameAdmin(AdminImageMixin, reversion.VersionAdmin):
    list_display = ("created", "name", "get_platforms"... )
    list_filter = ("platforms", "year",)
    #I'm interested in changing the field below
    filter_horizontal = ("related_games",)

    formfield_overrides = {
        models.ManyToManyField: {"widget": CheckboxSelectMultiple},
    }

    def get_platforms(self, obj):
        return ", ".join([p.name for p in obj.platforms.all()])

我需要扩展 filter_horizo​​ntal =(" related_games",)部分admin.py - 在相关游戏小部件中添加每个游戏的平台信息。它应该看起来像(游戏名称和平台列表):"虚拟战斗机(PS4,PSP,PS3)"。

该应用程序使用Django 1.7和Python 2.7

感谢您的关注。

1 个答案:

答案 0 :(得分:1)

默认情况下,filter_horizontal中每个项目显示的内容基于对象的__str____unicode__方法,因此您可以尝试以下内容:

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

这将使每个游戏在列表中(以及其他任何地方)显示为"名称(平台)",例如" Crash Bandicoot(PS1,PS2)"或者"战场(无)"如果它没有任何平台

或者,如果您不想更改模型的__unicode__方法,则需要将ModelAdmin设置为使用自定义ModelForm,指定related_games字段应将自定义ModelMultipleChoiceField与自定义FilteredSelectMultiple小部件一起使用,您需要在其中覆盖render_options方法。以下类应位于各自的单独文件中,但它们看起来像:

# 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)