Wiele modeli w jednym django ModelForm?

99

Czy ModelFormw django można umieścić wiele modeli w jednym ? Próbuję utworzyć formularz edycji profilu. Muszę więc dołączyć niektóre pola z modelu User i modelu UserProfile. Obecnie używam 2 takich formularzy

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")

Czy istnieje sposób na skonsolidowanie ich w jeden formularz, czy po prostu muszę utworzyć formularz i samodzielnie obsłużyć ładowanie i zapisywanie bazy danych?

Jason Webb
źródło
Możliwy duplikat Django: wiele modeli w jednym szablonie przy użyciu formularzy
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Odpowiedzi:

93

Możesz po prostu pokazać oba formularze w szablonie wewnątrz jednego <form>elementu HTML. Następnie po prostu przetwarzaj formularze oddzielnie w widoku. Nadal będziesz mógł używać form.save()i nie musisz samodzielnie przetwarzać ładowania i zapisywania bazy danych.

W tym przypadku nie powinieneś tego potrzebować, ale jeśli zamierzasz używać formularzy z tymi samymi nazwami pól, zajrzyj do prefixkwarg dla formularzy django. (Odpowiedziałem na pytanie na ten temat tutaj ).

Zach
źródło
To dobra rada, ale są przypadki, że nie ma to zastosowania, np. niestandardowy formularz modelu dla zestawu formularzy.
Wtower
8
Jaki byłby prosty sposób, aby widok oparty na klasach mógł pokazywać więcej niż jeden formularz i szablon, który następnie łączy je w ten sam <form>element?
jozxyqk
1
Ale jak? Zwykle a FormViewma form_classprzypisany tylko jeden .
erikbwork,
@erikbwork W tym przypadku nie należy używać widoku FormView. Po prostu podklasuj TemplateViewi zaimplementuj tę samą logikę co FormView, ale z wieloma formularzami.
moppag
10

Możesz spróbować użyć tego fragmentu kodu:

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

Przykładowe zastosowanie:

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
źródło
Wygląda na to, że nie można tego użyć w panelu administracyjnym z powodu pewnych wyraźnych kontroli:admin.E016) The value of 'form' must inherit from 'BaseModelForm'.
WhyNotHugo
Jak mogę tego używać UpdateView?
Pavel Shlepnev
4

Erikbwork i ja mieliśmy problem polegający na tym, że do ogólnego widoku opartego na klasach można dołączyć tylko jeden model. Znalazłem podobny sposób podejścia do tego jak Miao, ale bardziej modułowy.

Napisałem Mixin, abyś mógł używać wszystkich ogólnych widoków opartych na klasach. Zdefiniuj model, pola, a teraz także child_model i child_field - a następnie możesz zawijać pola obu modeli w tagu, jak opisuje 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())

Przykładowe zastosowanie:

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

Lub z ModelFormClass:

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

Gotowe. Mam nadzieję, że to komuś pomoże.

LGG
źródło
Co w tym save_child_form.course_key = self.objectjest .course_key?
Adam Starrh
Myślę, że course_key jest powiązanym modelem, w moim przypadku jest to „user”, jak w UserProfile.user, który jest odnośnikiem wstecznym, może ta nazwa pola powinna być dostosowywalna, jeśli ma być mieszanką wielokrotnego użytku. Ale nadal mam inny problem, w którym formularz podrzędny nie jest faktycznie wypełniony danymi początkowymi, wszystkie pola z User są wstępnie wypełnione, ale nie dla UserProfile. Być może będę musiał to najpierw naprawić.
robvdl
Problemem, dla którego formularz potomny nie jest wypełniany, jest to, że w metodzie get_child_form wywołuje, return self.child_form_class(**self.get_form_kwargs())ale pobiera niewłaściwą instancję modelu kwargs['instance'], np. Instancja jest modelem głównym, a nie modelem podrzędnym. Aby to naprawić, musisz najpierw zapisać kwargs w zmiennej, kwargs = self.get_form_kwargs()a następnie zaktualizować kwargs['initial']odpowiednią instancję modelu przed wywołaniem return self.child_form_class(**kwargs). W moim przypadku tak było, kwargs['instance'] = kwargs['instance'].profilejeśli to ma sens.
robvdl
Niestety przy zapisywaniu nadal będzie się zawieszać w dwóch miejscach, jednym, w którym self.object nie istnieje jeszcze w form_valid, więc zgłasza błąd AttributeError, a innego miejsca nie ma. Nie jestem pewien, czy to rozwiązanie zostało w pełni przetestowane przed opublikowaniem, więc może lepiej będzie skorzystać z drugiej odpowiedzi za pomocą CombinedFormBase.
robvdl
1
Co to jest model_forms?
Babcia Bolesna
2

Prawdopodobnie powinieneś przyjrzeć się zestawom formularzy Inline . Zestawy formularzy wbudowanych są używane, gdy modele są powiązane kluczem obcym.

John Percival Hackworth
źródło
1
Zestawy formularzy wbudowanych są używane, gdy musisz pracować z relacją jeden do wielu. Na przykład firma, w której dodajesz pracowników. Próbuję połączyć 2 tabele w jeden formularz. To jest relacja jeden do jednego.
Jason Webb
Użycie zestawu formularzy Inline zadziałałoby, ale prawdopodobnie byłoby mniej niż idealne. Możesz również utworzyć Model, który obsługuje relację za Ciebie, a następnie użyć pojedynczego formularza. Wystarczy mieć jedną stronę z 2 formularzami, jak sugerowano na stackoverflow.com/questions/2770810/… .
John Percival Hackworth
2

Tutaj możesz sprawdzić moją odpowiedź na podobny problem.

Mówi o tym, jak połączyć rejestrację i profil użytkownika w jeden formularz, ale można go uogólnić na dowolną kombinację ModelForm.

Mitar
źródło
0

Użyłem Django betterforms „s wielopostaciowe i MultiModelForm w moim projekcie. Kod można jednak ulepszyć. Na przykład, jest zależny od django.six, który nie jest obsługiwany przez 3. +, ale wszystko to można łatwo naprawić

To pytanie pojawiło się kilka razy w StackOverflow, więc myślę, że nadszedł czas, aby znaleźć ustandaryzowany sposób radzenia sobie z tym.

J Eti
źródło