En cuanto a cómo configurar los modelos, tiene razón en que una tabla directa con una columna de "orden" es la forma ideal de representarla. También tienes razón en que Django no te permitirá referirte a esa relación en un fieldset. El truco para descifrar este problema es recordar que los nombres de campo que especifique en los "fieldsets" o "campos" de un ModelAdmin
en realidad no se refieren a los campos del Model
, sino a los campos del ModelForm
, que son gratuitos. anular para nuestro deleite de corazón Con muchos campos de 2, esto se pone complicado, pero tengan paciencia conmigo:
Digamos que está tratando de representar los concursos y competidores que compiten en ellos, con un pedido many2many entre concursos y competidores donde el orden representa la clasificación de los competidores en ese concurso Su models.py
sería parecido a esto:
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()
Con suerte, esto es similar a lo que estás tratando. Ahora, para el administrador. He escrito un ejemplo admin.py
con un montón de comentarios para explicar lo que está sucediendo, pero aquí hay un resumen para ayudarlo:
Como no tengo el código para el widget m2m ordenado que ha escrito, he utilizó un widget ficticio marcador de posición que simplemente hereda de TextInput
. La entrada contiene una lista separada por comas (sin espacios) de ID de concursante, y el orden de su aparición en la cadena determina el valor de su columna de "rango" en el modelo ContestResults
.
Lo que sucede es que anulamos el ModelForm
predeterminado para el concurso con el nuestro, y luego definimos un campo de "resultados" dentro de él (no podemos llamar al campo "concursantes", ya que habría un conflicto de nombre con el m2m campo en el modelo). A continuación, reemplazamos el __init__()
, que se invoca cuando el formulario se muestra en el administrador, por lo que podemos buscar cualquier ContestResults que ya se haya definido para el Concurso y usarlos para rellenar el widget. También reemplazamos save()
, de modo que podamos obtener los datos del widget y crear los Resultados de concurso necesarios.
Tenga en cuenta que, en aras de la simplicidad, este ejemplo omite cosas como la validación de los datos del widget, por lo que las cosas se romperán si intenta escribir algo inesperado en la entrada de texto. Además, el código para crear ContestResults es bastante simplista y podría mejorarse enormemente.
También debería agregar que en realidad he ejecutado este código y verificado que funciona.
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)
En caso de que se esté preguntando, yo había encontramos con este problema por mí mismo en un proyecto en el que he estado trabajando, por lo que la mayor parte de este código proviene de ese proyecto. Espero que le sea útil.
+1 por usar la palabra 'idiosincrasias' en su pregunta. Buena idea. Lo estoy pensando. –
¿Puede decirnos por qué usa un modelo "directo" en lugar de una relación de muchos a muchos? ¿Qué metadatos adicionales está almacenando en esta relación? El problema que veo al tener esta característica implementada en el administrador predeterminado es que actualmente no hay forma de agregar los metadatos adicionales en la relación directa simplemente eligiendo elementos de una lista como lo implica su widget. – Thomas
Thomas: los datos de pedido serían el campo adicional, dando al modelo "directo" tres campos: una ForeignKey a cada lado del M2M y un orden de representación de PositiveIntegerField, con los tres campos siendo unique_together. –