¿Múltiples modelos en un solo ModelForm de django?

98

¿Es posible tener varios modelos incluidos en un solo ModelFormen django? Estoy intentando crear un formulario de edición de perfil. Así que necesito incluir algunos campos del modelo de usuario y del modelo de perfil de usuario . Actualmente estoy usando 2 formularios como este

class UserEditForm(ModelForm):

    class Meta:
        model = User
        fields = ("first_name", "last_name")

class UserProfileForm(ModelForm):

    class Meta:
        model = UserProfile
        fields = ("middle_name", "home_phone", "work_phone", "cell_phone")

¿Hay alguna forma de consolidarlos en un formulario o solo necesito crear un formulario y manejar la carga de la base de datos y guardarme?

Jason Webb
fuente
Posible duplicado de Django: múltiples modelos en una plantilla usando formularios
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Respuestas:

93

Puede mostrar ambos formularios en la plantilla dentro de un <form>elemento html. Luego, procese los formularios por separado en la vista. Aún podrá usar form.save()y no tendrá que procesar la carga de la base de datos y guardarlo.

En este caso, no debería necesitarlo, pero si va a utilizar formularios con los mismos nombres de campo, busque en el prefixkwarg los formularios de django. (Respondí una pregunta al respecto aquí ).

Zach
fuente
Este es un buen consejo, pero hay casos en que no es aplicable, por ejemplo. formulario modelo personalizado para un conjunto de formularios.
Wtower
8
¿Cuál sería una forma sencilla de hacer una vista basada en clases capaz de mostrar más de un formulario y una plantilla que luego los combina en el mismo <form>elemento?
jozxyqk
1
¿Pero cómo? Por lo general, FormViewsolo tiene uno form_classasignado.
erikbwork
@erikbwork No debe utilizar FormView para este caso. Simplemente TemplateViewrealice una subclase e implemente la misma lógica que FormView, pero con múltiples formularios.
moppag
10

Puede intentar utilizar estos fragmentos de código:

class CombinedFormBase(forms.Form):
    form_classes = []

    def __init__(self, *args, **kwargs):
        super(CombinedFormBase, self).__init__(*args, **kwargs)
        for f in self.form_classes:
            name = f.__name__.lower()
            setattr(self, name, f(*args, **kwargs))
            form = getattr(self, name)
            self.fields.update(form.fields)
            self.initial.update(form.initial)

    def is_valid(self):
        isValid = True
        for f in self.form_classes:
            name = f.__name__.lower()
            form = getattr(self, name)
            if not form.is_valid():
                isValid = False
        # is_valid will trigger clean method
        # so it should be called after all other forms is_valid are called
        # otherwise clean_data will be empty
        if not super(CombinedFormBase, self).is_valid() :
            isValid = False
        for f in self.form_classes:
            name = f.__name__.lower()
            form = getattr(self, name)
            self.errors.update(form.errors)
        return isValid

    def clean(self):
        cleaned_data = super(CombinedFormBase, self).clean()
        for f in self.form_classes:
            name = f.__name__.lower()
            form = getattr(self, name)
            cleaned_data.update(form.cleaned_data)
        return cleaned_data

Ejemplo de uso:

class ConsumerRegistrationForm(CombinedFormBase):
    form_classes = [RegistrationForm, ConsumerProfileForm]

class RegisterView(FormView):
    template_name = "register.html"
    form_class = ConsumerRegistrationForm

    def form_valid(self, form):
        # some actions...
        return redirect(self.get_success_url())
Miao ZhiCheng
fuente
Parece que esto no se puede usar en el administrador debido a algunas verificaciones explícitas:admin.E016) The value of 'form' must inherit from 'BaseModelForm'.
WhyNotHugo
¿Cómo puedo usarlo con UpdateView?
Pavel Shlepnev
3

Erikbwork y yo tuvimos el problema de que solo se puede incluir un modelo en una vista basada en clases genérica. Encontré una forma similar de abordarlo como Miao, pero más modular.

Escribí un Mixin para que puedas usar todas las vistas genéricas basadas en clases. Defina modelo, campos y ahora también child_model y child_field, y luego puede ajustar los campos de ambos modelos en una etiqueta como describe Zach.

class ChildModelFormMixin: 
    ''' extends ModelFormMixin with the ability to include ChildModelForm '''
    child_model = ""
    child_fields = ()
    child_form_class = None

    def get_child_model(self):
        return self.child_model

    def get_child_fields(self):
        return self.child_fields

    def get_child_form(self):
        if not self.child_form_class:
            self.child_form_class = model_forms.modelform_factory(self.get_child_model(), fields=self.get_child_fields())
        return self.child_form_class(**self.get_form_kwargs())

    def get_context_data(self, **kwargs):
        if 'child_form' not in kwargs:
            kwargs['child_form'] = self.get_child_form()
        return super().get_context_data(**kwargs)

    def post(self, request, *args, **kwargs):
        form = self.get_form()
        child_form = self.get_child_form()

        # check if both forms are valid
        form_valid = form.is_valid()
        child_form_valid = child_form.is_valid()

        if form_valid and child_form_valid:
            return self.form_valid(form, child_form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form, child_form):
        self.object = form.save()
        save_child_form = child_form.save(commit=False)
        save_child_form.course_key = self.object
        save_child_form.save()

        return HttpResponseRedirect(self.get_success_url())

Ejemplo de uso:

class ConsumerRegistrationUpdateView(UpdateView):
    model = Registration
    fields = ('firstname', 'lastname',)
    child_model = ConsumerProfile
    child_fields = ('payment_token', 'cart',)

O con ModelFormClass:

class ConsumerRegistrationUpdateView(UpdateView):
    model = Registration
    fields = ('firstname', 'lastname',)
    child_model = ConsumerProfile
    child_form_class = ConsumerProfileForm

Hecho. Espero que ayude a alguien.

LGG
fuente
En esto save_child_form.course_key = self.object, ¿qué es .course_key?
Adam Starrh
Creo que course_key es el modelo relacionado, en mi caso es "usuario" como en UserProfile.user, que es una referencia inversa, tal vez ese nombre de campo debería ser personalizable si fuera un mixin reutilizable. Pero sigo teniendo otro problema en el que el formulario secundario no se completa con los datos iniciales, todos los campos de User se completan previamente pero no para UserProfile. Puede que tenga que arreglar eso primero.
robvdl
El problema por el que el formulario hijo no se completa es porque en el método get_child_form, llama return self.child_form_class(**self.get_form_kwargs())pero obtiene la instancia de modelo incorrecta kwargs['instance'], por ejemplo, la instancia es el modelo principal y no el modelo hijo. Para solucionarlo, primero debe guardar kwargs en una variable y kwargs = self.get_form_kwargs()luego actualizar kwargs['initial']con la instancia de modelo correcta antes de llamar return self.child_form_class(**kwargs). En mi caso eso fue kwargs['instance'] = kwargs['instance'].profilesi esto tiene sentido.
robvdl
Desafortunadamente, al guardar, todavía fallará en dos lugares, uno donde self.object aún no está en form_valid, por lo que arroja un AttributeError, y otra instancia de lugar no está allí. No estoy seguro de si esta solución se probó por completo antes de ser publicada, por lo que sería mejor ir con la otra respuesta usando CombinedFormBase.
robvdl
1
¿Qué es model_forms?
Granny Aching
2

Probablemente debería echar un vistazo a los conjuntos de formularios en línea . Los conjuntos de formularios en línea se utilizan cuando sus modelos están relacionados por una clave externa.

John Percival Hackworth
fuente
1
Los conjuntos de formularios en línea se utilizan cuando necesita trabajar con una relación de uno a varios. Como una empresa en la que agrega empleados. Estoy intentando combinar 2 tablas en una sola forma. Es una relación uno a uno.
Jason Webb
El uso de un conjunto de formularios en línea funcionaría, pero probablemente sería menos que ideal. También puede crear un modelo que maneje la relación por usted y luego usar un formulario único. Solo tener una sola página con 2 formularios como se sugiere en stackoverflow.com/questions/2770810/… funcionaría.
John Percival Hackworth
2

Puede consultar mi respuesta aquí para un problema similar.

Habla sobre cómo combinar el registro y el perfil de usuario en un formulario, pero se puede generalizar a cualquier combinación de ModelForm.

Mitar
fuente
0

Solía django betterforms 's multiforme y MultiModelForm en mi proyecto. Sin embargo, el código se puede mejorar. Por ejemplo, depende de django.six, que no es compatible con 3. +, pero todos estos se pueden solucionar fácilmente

Esta pregunta ha aparecido varias veces en StackOverflow, así que creo que es hora de encontrar una forma estandarizada de lidiar con esto.

J Eti
fuente