モデルの設定方法に関しては、「順序」列のあるスルーテーブルがモデルを表す理想的な方法であるという点で正しいです。また、Djangoではフィールドセットでその関係を参照することはできません。この問題を解決する秘訣は、の「fieldsets」または「fields」で指定するフィールド名がModelAdmin
実際にはのフィールドではなく、のフィールドを参照してModel
いることを覚えておくことModelForm
です。私たちの心の喜び。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
を書きましたが、ここにあなたを助けるための要約があります:
あなたが書いた順序付けられたm2mウィジェットのコードがないので、から単純に継承するプレースホルダーダミーウィジェットを使用しましたTextInput
。ContestResults
入力には、競合他社IDのコンマ区切りリスト(スペースなし)が保持され、文字列内でのそれらの出現順序によって、モデル内の「ランク」列の値が決まります。
何が起こるかというとModelForm
、コンテストのデフォルトを独自のものでオーバーライドし、その中に「結果」フィールドを定義します(モデルのm2mフィールドと名前が競合するため、フィールドを「コンテスト参加者」と呼ぶことはできません)。 )。次に、フォームが管理者に表示されたときに呼び出されるをオーバーライドします__init__()
。これにより、コンテストに対してすでに定義されている可能性のあるContestResultsをフェッチし、それらを使用してウィジェットにデータを入力できます。また、をオーバーライド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)
ご参考までに、私は自分が取り組んでいるプロジェクトでこの問題に遭遇したので、このコードのほとんどはそのプロジェクトからのものです。お役に立てば幸いです。