【问题标题】:Ordered ManyToManyField that can be used in fieldsets可在字段集中使用的有序 ManyToManyField
【发布时间】:2010-07-06 22:37:58
【问题描述】:

我一直在处理一个有序的 ManyToManyField 小部件,并且它的前端方面运行良好:

很遗憾,我在让后端正常工作时遇到了很多麻烦。连接后端的最明显方法是使用through 表,该表将模型与ForeignKeys 键控到关系的两侧并覆盖保存方法。这会很好用,除了由于内容的特殊性,绝对要求将此小部件放置在字段集中(使用 ModelAdmin fieldsets 属性),即apparently not possible。

我没有想法。有什么建议吗?

谢谢!

【问题讨论】:

  • +1 用于在您的问题中使用“特质”一词。很酷的想法。我正在考虑。
  • 您能告诉我们为什么您使用“通过”模型而不是股票多对多关系吗?您在此关系中存储了哪些额外的元数据?我看到在默认管理员中实现此功能的问题是,目前无法通过从小部件暗示的列表中选择元素来在直通关系中添加额外的元数据。
  • Thomas:排序数据将是附加字段,为“通过”模型提供三个字段:M2M 每一侧的 ForeignKey 和表示顺序的 PositiveIntegerField,所有三个字段都是 unique_together。
  • 我知道有点晚了,但你愿意分享前端代码吗?

标签: python django django-admin manytomanyfield


【解决方案1】:

关于如何设置模型,您说得对,带有“订单”列的直通表是表示它的理想方式。你也是对的,Django 不会让你在字段集中引用该关系。解决这个问题的诀窍是记住你在ModelAdmin的“fieldsets”或“fields”中指定的字段名称实际上并不是指Model的字段,而是指@987654324的字段@,我们可以随心所欲地覆盖它。对于 many2many 字段,这会变得很棘手,但请耐心等待:

假设您试图代表比赛和参加比赛的竞争对手,比赛和竞争对手之间的顺序为 many2many,其中顺序代表竞争对手在该比赛中的排名。您的 models.py 将如下所示:

from django.db import models

class Contest(models.Model):
    name = models.CharField(max_length=50)
    # More fields here, if you like.
    contestants = models.ManyToManyField('Contestant', through='ContestResults')

class Contestant(models.Model):
    name = models.CharField(max_length=50)

class ContestResults(models.Model):
    contest = models.ForeignKey(Contest)
    contestant = models.ForeignKey(Contestant)
    rank = models.IntegerField()

希望这与您正在处理的问题相似。现在,对于管理员。我写了一个示例admin.py,其中包含大量 cmets 来解释正在发生的事情,但这里有一个摘要可以帮助您:

由于我没有您编写的有序 m2m 小部件的代码,因此我使用了一个简单地从 TextInput 继承的占位符虚拟小部件。输入包含一个以逗号分隔的参赛者 ID 列表(不含空格),它们在字符串中出现的顺序决定了它们在 ContestResults 模型中的“排名”列的值。

发生的情况是,我们用我们自己的覆盖默认的 ModelForm 为 Contest,然后在其中定义一个“结果”字段(我们不能称该字段为“参赛者”,因为与模型中的 m2m 字段)。然后我们覆盖__init__(),当表单显示在管理员中时调用它,因此我们可以获取可能已经为竞赛定义的任何竞赛结果,并使用它们来填充小部件。我们还覆盖了save(),这样我们就可以依次从小部件中获取数据并创建所需的 ContestResults。

请注意,为简单起见,此示例省略了诸如验证小部件数据之类的事情,因此如果您尝试在文本输入中输入任何意外内容,事情就会中断。此外,创建 ContestResults 的代码非常简单,可以大大改进。

我还应该补充一点,我实际上已经运行了这段代码并验证了它是否有效。

from django import forms
from django.contrib import admin
from models import Contest, Contestant, ContestResults

# Generates a function that sequentially calls the two functions that were
# passed to it
def func_concat(old_func, new_func):
    def function():
        old_func()
        new_func()
    return function

# A dummy widget to be replaced with your own.
class OrderedManyToManyWidget(forms.widgets.TextInput):
    pass

# A simple CharField that shows a comma-separated list of contestant IDs.
class ResultsField(forms.CharField):
    widget = OrderedManyToManyWidget()

class ContestAdminForm(forms.models.ModelForm):
    # Any fields declared here can be referred to in the "fieldsets" or
    # "fields" of the ModelAdmin. It is crucial that our custom field does not
    # use the same name as the m2m field field in the model ("contestants" in
    # our example).
    results = ResultsField()

    # Be sure to specify your model here.
    class Meta:
        model = Contest

    # Override init so we can populate the form field with the existing data.
    def __init__(self, *args, **kwargs):
        instance = kwargs.get('instance', None)
        # See if we are editing an existing Contest. If not, there is nothing
        # to be done.
        if instance and instance.pk:
            # Get a list of all the IDs of the contestants already specified
            # for this contest.
            contestants = ContestResults.objects.filter(contest=instance).order_by('rank').values_list('contestant_id', flat=True)
            # Make them into a comma-separated string, and put them in our
            # custom field.
            self.base_fields['results'].initial = ','.join(map(str, contestants))
            # Depending on how you've written your widget, you can pass things
            # like a list of available contestants to it here, if necessary.
        super(ContestAdminForm, self).__init__(*args, **kwargs)

    def save(self, *args, **kwargs):
        # This "commit" business complicates things somewhat. When true, it 
        # means that the model instance will actually be saved and all is
        # good. When false, save() returns an unsaved instance of the model.
        # When save() calls are made by the Django admin, commit is pretty
        # much invariably false, though I'm not sure why. This is a problem
        # because when creating a new Contest instance, it needs to have been
        # saved in the DB and have a PK, before we can create ContestResults.
        # Fortunately, all models have a built-in method called save_m2m()
        # which will always be executed after save(), and we can append our
        # ContestResults-creating code to the existing same_m2m() method.
        commit = kwargs.get('commit', True)
        # Save the Contest and get an instance of the saved model
        instance = super(ContestAdminForm, self).save(*args, **kwargs)
        # This is known as a lexical closure, which means that if we store
        # this function and execute it later on, it will execute in the same
        # context (i.e. it will have access to the current instance and self).
        def save_m2m():
            # This is really naive code and should be improved upon,
            # especially in terms of validation, but the basic gist is to make
            # the needed ContestResults. For now, we'll just delete any
            # existing ContestResults for this Contest and create them anew.
            ContestResults.objects.filter(contest=instance).delete()
            # Make a list of (rank, contestant ID) tuples from the comma-
            # -separated list of contestant IDs we get from the results field.
            formdata = enumerate(map(int, self.cleaned_data['results'].split(',')), 1)
            for rank, contestant in formdata:
                ContestResults.objects.create(contest=instance, contestant_id=contestant, rank=rank)
        if commit:
            # If we're committing (fat chance), simply run the closure.
            save_m2m()
        else:
            # Using a function concatenator, ensure our save_m2m closure is
            # called after the existing save_m2m function (which will be
            # called later on if commit is False).
            self.save_m2m = func_concat(self.save_m2m, save_m2m)
        # Return the instance like a good save() method.
        return instance

class ContestAdmin(admin.ModelAdmin):
    # The precious fieldsets.
    fieldsets = (
        ('Basic Info', {
            'fields': ('name', 'results',)
        }),)
    # Here's where we override our form
    form = ContestAdminForm

admin.site.register(Contest, ContestAdmin)

如果您想知道,我自己在一个我一直从事的项目中遇到了这个问题,所以大部分代码都来自那个项目。我希望你觉得它有用。

【讨论】:

  • 感谢您的周到和周到的回复,谢谢!今天我将深入研究这一点,让你知道结果如何。
  • 我非常接近让它工作,谢谢!关键是字段集实际上是指表单上的字段,而不是模型上的指针。我遇到的一个问题是设置 ModelForm.base_fields[field].initial 似乎不需要;该字段中的数据仍然由数据库中的值填充(与您的情况不同,我实际上将覆盖一个真正的 m2m 字段的初始值;这似乎是一个重要的区别。关于我如何实现这一点的任何建议?相关django-users 帖子,bit.ly/a7R3ao
  • 啊,我想我以前遇到过这种情况。在某些形式中,您需要设置ModelForm.initial[field],而不是ModelForm.base_fields[field].initial。你能试试,让我知道结果如何?
  • 在尝试添加与模型中的任何字段都不匹配但对应于自定义保存选项的 BooleanField 时遇到了同样的问题。谢谢。 我不明白的一件事是为什么 django 需要如此复杂才能完成这些事情。我真的想知道有时 python 是否会让事情变得非常混乱
  • 在把我的头撞到这样的东西上一段时间后,这让我很开心。谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-03
  • 2013-03-14
  • 1970-01-01
  • 2011-11-11
  • 2023-03-15
相关资源
最近更新 更多