python - fields - formularios personalizados django
Django ModelForm para campos de muchos a muchos (7)
Hice algo similar basado en el código de Clément con un formulario de administrador de usuario:
# models.py
class Clinica(models.Model):
...
users = models.ManyToManyField(User, null=True, blank=True, related_name=''clinicas'')
# admin.py
class CustomUserChangeForm(UserChangeForm):
clinicas = forms.ModelMultipleChoiceField(queryset=Clinica.objects.all())
def __init__(self,*args,**kwargs):
if ''instance'' in kwargs:
initial = kwargs.setdefault(''initial'',{})
initial[''clinicas''] = kwargs[''instance''].clinicas.values_list(''pk'',flat=True)
super(CustomUserChangeForm,self).__init__(*args,**kwargs)
def save(self,*args,**kwargs):
instance = super(CustomUserChangeForm,self).save(*args,**kwargs)
instance.clinicas = self.cleaned_data[''clinicas'']
return instance
class Meta:
model = User
admin.site.unregister(User)
UserAdmin.fieldsets += ( (u''Clinicas'', {''fields'': (''clinicas'',)}), )
UserAdmin.form = CustomUserChangeForm
admin.site.register(User,UserAdmin)
Considere los siguientes modelos y formas:
class Pizza(models.Model):
name = models.CharField(max_length=50)
class Topping(models.Model):
name = models.CharField(max_length=50)
ison = models.ManyToManyField(Pizza, blank=True)
class ToppingForm(forms.ModelForm):
class Meta:
model = Topping
Cuando ves el ToppingForm, te permite elegir qué pizzas van a seguir los ingredientes y todo es excelente.
Mi pregunta es: ¿cómo defino un ModelForm para Pizza que me permite aprovechar la relación de Muchos a Muchos entre Pizza y Topping y me permite elegir qué ingredientes van para Pizza?
No estoy seguro de obtener la pregunta al 100%, así que voy a correr con esta suposición:
Cada Pizza
puede tener muchos Topping
s. Cada Topping
puede tener muchas Pizza
s. Pero si se agrega un Topping
a una Pizza
, ese Topping
entonces automágicamente tendrá una Pizza
, y viceversa.
En este caso, tu mejor apuesta es una tabla de relaciones, que Django admite bastante bien. Podría verse así:
models.py
class PizzaTopping(models.Model):
topping = models.ForeignKey(''Topping'')
pizza = models.ForeignKey(''Pizza'')
class Pizza(models.Model):
name = models.CharField(max_length=50)
topped_by = models.ManyToManyField(''Topping'', through=PizzaTopping)
def __str__(self):
return self.name
def __unicode__(self):
return self.name
class Topping(models.Model):
name=models.CharField(max_length=50)
is_on = models.ManyToManyField(''Pizza'', through=PizzaTopping)
def __str__(self):
return self.name
def __unicode__(self):
return self.name
forms.py
class PizzaForm(forms.ModelForm):
class Meta:
model = Pizza
class ToppingForm(forms.ModelForm):
class Meta:
model = Topping
Ejemplo:
>>> p1 = Pizza(name="Monday")
>>> p1.save()
>>> p2 = Pizza(name="Tuesday")
>>> p2.save()
>>> t1 = Topping(name="Pepperoni")
>>> t1.save()
>>> t2 = Topping(name="Bacon")
>>> t2.save()
>>> PizzaTopping(pizza=p1, topping=t1).save() # Monday + Pepperoni
>>> PizzaTopping(pizza=p2, topping=t1).save() # Tuesday + Pepperoni
>>> PizzaTopping(pizza=p2, topping=t2).save() # Tuesday + Bacon
>>> tform = ToppingForm(instance=t2) # Bacon
>>> tform.as_table() # Should be on only Tuesday.
u''<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="Bacon" maxlength="50" /></td></tr>/n<tr><th><label for="id_is_on">Is on:</label></th><td><select multiple="multiple" name="is_on" id="id_is_on">/n<option value="1">Monday</option>/n<option value="2" selected="selected">Tuesday</option>/n</select><br /> Hold down "Control", or "Command" on a Mac, to select more than one.</td></tr>''
>>> pform = PizzaForm(instance=p1) # Monday
>>> pform.as_table() # Should have only Pepperoni
u''<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="Monday" maxlength="50" /></td></tr>/n<tr><th><label for="id_topped_by">Topped by:</label></th><td><select multiple="multiple" name="topped_by" id="id_topped_by">/n<option value="1" selected="selected">Pepperoni</option>/n<option value="2">Bacon</option>/n</select><br /> Hold down "Control", or "Command" on a Mac, to select more than one.</td></tr>''
>>> pform2 = PizzaForm(instance=p2) # Tuesday
>>> pform2.as_table() # Both Pepperoni and Bacon
u''<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="Tuesday" maxlength="50" /></td></tr>/n<tr><th><label for="id_topped_by">Topped by:</label></th><td><select multiple="multiple" name="topped_by" id="id_topped_by">/n<option value="1" selected="selected">Pepperoni</option>/n<option value="2" selected="selected">Bacon</option>/n</select><br /> Hold down "Control", or "Command" on a Mac, to select more than one.</td></tr>''
No estoy seguro de si esto es lo que estás buscando, pero ¿eres consciente de que Pizza tiene el atributo topping_set
? Usando ese atributo, puede agregar fácilmente un nuevo encabezado en su ModelForm.
new_pizza.topping_set.add(new_topping)
Otra forma simple de lograr esto es crear una tabla intermedia y usar campos en línea para hacerlo. Consulte este https://docs.djangoproject.com/en/1.2/ref/contrib/admin/#working-with-many-to-many-intermediary-models
Algunos ejemplos de código a continuación
models.py
class Pizza(models.Model):
name = models.CharField(max_length=50)
class Topping(models.Model):
name = models.CharField(max_length=50)
ison = models.ManyToManyField(Pizza, through=''PizzaTopping'')
class PizzaTopping(models.Model):
pizza = models.ForeignKey(Pizza)
topping = models.ForeignKey(Topping)
admin.py
class PizzaToppingInline(admin.TabularInline):
model = PizzaTopping
class PizzaAdmin(admin.ModelAdmin):
inlines = [PizzaToppingInline,]
class ToppingAdmin(admin.ModelAdmin):
inlines = [PizzaToppingInline,]
admin.site.register(Pizza, PizzaAdmin)
admin.site.register(Topping, ToppingAdmin)
Para ser sincero, pondría la relación de muchos a muchos en el modelo de Pizza
. Creo que esto más cerca de la realidad. Imagina a una persona que ordena varias pizzas. No dijo "Me gustaría queso en pizza uno y dos y tomates en pizza uno y tres" pero probablemente "Una pizza con queso, una pizza con queso y tomates, ...".
Por supuesto, es posible hacer que la forma funcione a su manera, pero yo iría con:
class Pizza(models.Model):
name = models.CharField(max_length=50)
toppings = models.ManyToManyField(Topping)
Supongo que tendrías que añadir aquí un nuevo ModelMultipleChoiceField
a tu PizzaForm
y vincular manualmente ese campo de formulario con el campo de modelo, ya que Django no lo hará automáticamente por ti.
El siguiente fragmento podría ser útil:
class PizzaForm(forms.ModelForm):
class Meta:
model = Pizza
# Representing the many to many related field in Pizza
toppings = forms.ModelMultipleChoiceField(queryset=Topping.objects.all())
# Overriding __init__ here allows us to provide initial
# data for ''toppings'' field
def __init__(self, *args, **kwargs):
# Only in case we build the form from an instance
# (otherwise, ''toppings'' list should be empty)
if kwargs.get(''instance''):
# We get the ''initial'' keyword argument or initialize it
# as a dict if it didn''t exist.
initial = kwargs.setdefault(''initial'', {})
# The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data.
initial[''toppings''] = [t.pk for t in kwargs[''instance''].topping_set.all()]
forms.ModelForm.__init__(self, *args, **kwargs)
# Overriding save allows us to process the value of ''toppings'' field
def save(self, commit=True):
# Get the unsave Pizza instance
instance = forms.ModelForm.save(self, False)
# Prepare a ''save_m2m'' method for the form,
old_save_m2m = self.save_m2m
def save_m2m():
old_save_m2m()
# This is where we actually link the pizza with toppings
instance.topping_set.clear()
for topping in self.cleaned_data[''toppings'']:
instance.topping_set.add(topping)
self.save_m2m = save_m2m
# Do we need to save all changes now?
if commit:
instance.save()
self.save_m2m()
return instance
Este PizzaForm
puede usarse en todas partes, incluso en el administrador:
# yourapp/admin.py
from django.contrib.admin import site, ModelAdmin
from yourapp.models import Pizza
from yourapp.forms import PizzaForm
class PizzaAdmin(ModelAdmin):
form = PizzaForm
site.register(Pizza, PizzaAdmin)
Nota
El método save()
puede ser un poco demasiado detallado, pero puedes simplificarlo si no necesitas soportar la situación commit=False
, entonces será así:
def save(self):
instance = forms.ModelForm.save(self)
instance.topping_set.clear()
for topping in self.cleaned_data[''toppings'']:
instance.topping_set.add(topping)
Tuvimos un problema similar en nuestra aplicación, que usó admin de django. Hay muchas relaciones entre usuarios y grupos y uno no puede agregar usuarios fácilmente a un grupo. He creado un patch para django, que hace esto, pero no le presto mucha atención ;-) Puedes leerlo y tratar de aplicar una solución similar a tu problema de pizza / cobertura. De esta manera, al estar dentro de un topping, puede agregar fácilmente pizzas relacionadas o viceversa.