Django - CreateView nie zapisuje formularza z zagnieżdżonym zestawem formularzy

14

Próbuję dostosować podejście do zapisywania zagnieżdżonych zestawów formularzy w formularzu głównym za pomocą funkcji układu Django-Crispy-Forms, ale nie mogę go zapisać. Śledzę ten przykładowy projekt kodu, ale nie mogłem uzyskać poprawności zestawu formatów w celu zapisania danych. Będę naprawdę wdzięczny, jeśli ktoś wskaże mój błąd. Muszę również dodać trzy wstawki w tym samym widoku dla EmployeeForm. Próbowałem Django-Extra-Views, ale nie mogłem tego zrobić. Byłbym wdzięczny, gdybyś zalecił dodanie więcej niż jednego wstawki dla tego samego widoku, jak około 5. Wszystko, co chcę, aby osiągnąć tę jedną stronę do tworzenia Employeei jej wstawki jak Education, Experience, Others. Poniżej znajduje się kod:

modele:

class Employee(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='employees',
                                null=True, blank=True)
    about = models.TextField()
    street = models.CharField(max_length=200)
    city = models.CharField(max_length=200)
    country = models.CharField(max_length=200)
    cell_phone = models.PositiveIntegerField()
    landline = models.PositiveIntegerField()

    def __str__(self):
        return '{} {}'.format(self.id, self.user)

    def get_absolute_url(self):
        return reverse('bars:create', kwargs={'pk':self.pk})

class Education(models.Model):
    employee = models.ForeignKey('Employee', on_delete=models.CASCADE, related_name='education')
    course_title = models.CharField(max_length=100, null=True, blank=True)
    institute_name = models.CharField(max_length=200, null=True, blank=True)
    start_year = models.DateTimeField(null=True, blank=True)
    end_year = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return '{} {}'.format(self.employee, self.course_title)

Widok:

class EmployeeCreateView(CreateView):
    model = Employee
    template_name = 'bars/crt.html'
    form_class = EmployeeForm
    success_url = None

    def get_context_data(self, **kwargs):
        data = super(EmployeeCreateView, self).get_context_data(**kwargs)
        if self.request.POST:
            data['education'] = EducationFormset(self.request.POST)
        else:
            data['education'] = EducationFormset()
        print('This is context data {}'.format(data))
        return data


    def form_valid(self, form):
        context = self.get_context_data()
        education = context['education']
        print('This is Education {}'.format(education))
        with transaction.atomic():
            form.instance.employee.user = self.request.user
            self.object = form.save()
            if education.is_valid():
                education.save(commit=False)
                education.instance = self.object
                education.save()

        return super(EmployeeCreateView, self).form_valid(form)

    def get_success_url(self):
        return reverse_lazy('bars:detail', kwargs={'pk':self.object.pk})

Formularze:

class EducationForm(forms.ModelForm):
    class Meta:
        model = Education
        exclude = ()
EducationFormset =inlineformset_factory(
    Employee, Education, form=EducationForm,
    fields=['course_title', 'institute_name'], extra=1,can_delete=True
    )

class EmployeeForm(forms.ModelForm):

    class Meta:
        model = Employee
        exclude = ('user', 'role')

    def __init__(self, *args, **kwargs):
        super(EmployeeForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_tag = True
        self.helper.form_class = 'form-horizontal'
        self.helper.label_class = 'col-md-3 create-label'
        self.helper.field_class = 'col-md-9'
        self.helper.layout = Layout(
            Div(
                Field('about'),
                Field('street'),
                Field('city'),
                Field('cell_phone'),
                Field('landline'),
                Fieldset('Add Education',
                    Formset('education')),
                HTML("<br>"),
                ButtonHolder(Submit('submit', 'save')),
                )
            )

Niestandardowy obiekt układu jak na przykład:

from crispy_forms.layout import LayoutObject, TEMPLATE_PACK
from django.shortcuts import render
from django.template.loader import render_to_string

class Formset(LayoutObject):
    template = "bars/formset.html"

    def __init__(self, formset_name_in_context, template=None):
        self.formset_name_in_context = formset_name_in_context
        self.fields = []
        if template:
            self.template = template

    def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
        formset = context[self.formset_name_in_context]
        return render_to_string(self.template, {'formset': formset})

Formset.html:

{% load static %}
{% load crispy_forms_tags %}
{% load staticfiles %}

<table>
{{ formset.management_form|crispy }}

    {% for form in formset.forms %}
            <tr class="{% cycle 'row1' 'row2' %} formset_row-{{ formset.prefix }}">
                {% for field in form.visible_fields %}
                <td>
                    {# Include the hidden fields in the form #}
                    {% if forloop.first %}
                        {% for hidden in form.hidden_fields %}
                            {{ hidden }}
                        {% endfor %}
                    {% endif %}
                    {{ field.errors.as_ul }}
                    {{ field|as_crispy_field }}
                </td>
                {% endfor %}
            </tr>
    {% endfor %}

</table>
<br>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js">
</script>
<script src="{% static 'js/jquery.formset.js' %}">
</script>
<script type="text/javascript">
    $('.formset_row-{{ formset.prefix }}').formset({
        addText: 'add another',
        deleteText: 'remove',
        prefix: '{{ formset.prefix }}',
    });
</script>

Nie ma błędów w terminalu ani w żaden inny sposób. Pomoc jest bardzo ceniona.

Shazia Nusrat
źródło
Alternatywnym rozwiązaniem jest również obsługa formularza przez zestaw formularzy: robię to za pomocą właściwości cached_property dla powiązanego zestawu formularzy
Matthew Schinckel

Odpowiedzi:

0

Obecnie nie przetwarzasz poprawnie zestawu formularzy w swoim CreateView. form_validw tym widoku obsłuży tylko formularz nadrzędny, a nie zestawy formularzy. To, co powinieneś zrobić, to przesłonić postmetodę i tam musisz sprawdzić poprawność zarówno formularza, jak i wszelkich dołączonych do niego zestawów formularzy:

def post(self, request, *args, **kwargs):
    form = self.get_form()
    # Add as many formsets here as you want
    education_formset = EducationFormset(request.POST)
    # Now validate both the form and any formsets
    if form.is_valid() and education_formset.is_valid():
        # Note - we are passing the education_formset to form_valid. If you had more formsets
        # you would pass these as well.
        return self.form_valid(form, education_formset)
    else:
        return self.form_invalid(form)

Następnie modyfikujesz form_validtak:

def form_valid(self, form, education_formset):
    with transaction.atomic():
        form.instance.employee.user = self.request.user
        self.object = form.save()
        # Now we process the education formset
        educations = education_formset.save(commit=False)
        for education in educations:
            education.instance = self.object
            education.save()
        # If you had more formsets, you would accept additional arguments and
        # process them as with the one above.
    # Don't call the super() method here - you will end up saving the form twice. Instead handle the redirect yourself.
    return HttpResponseRedirect(self.get_success_url())

Sposób, w jaki obecnie korzystasz, get_context_data()jest nieprawidłowy - usuń tę metodę całkowicie. Należy go używać tylko do pobierania danych kontekstowych do renderowania szablonu. Nie powinieneś wywoływać tego ze swojej form_valid()metody. Zamiast tego musisz przekazać zestaw formularzy do tej metody z post()metody opisanej powyżej.

W powyższym przykładowym kodzie zostawiłem kilka dodatkowych komentarzy, które, mam nadzieję, pomogą ci to rozgryźć.

solarissmoke
źródło
Przed odpowiedzią utwórz przykład lokalnie. Próbowałem twojego utworu, ale nie działa.
Shazia Nusrat
1
@ShaziaNusrat przepraszam, nie mam czasu, aby spróbować dowiedzieć się, co nie działa, szczególnie jeśli nie powiesz, co próbowałeś, a co nie działało („To nie działa” nie jest odpowiedni opis tego, co nie działało). Uważam, że w mojej odpowiedzi jest wystarczająco dużo, aby pomóc Ci określić, co musisz zmienić w obecnej implementacji. Jeśli nie, to miejmy nadzieję, że ktoś inny udzieli bardziej wyczerpującej odpowiedzi.
solarissmoke
Wypróbowałem to w kodzie do testowania i działało z problemami. Dlatego pokornie proszę cię, abyś spróbował go po twojej stronie, abyś mógł lepiej mnie poprowadzić. Jestem wdzięczny, ponieważ poświęciłeś mi trochę czasu. Ale nie działa.
Shazia Nusrat
0

Być może chciałbyś zobaczyć pakiet django-extra-views, zapewnia widok CreateWithInlinesView, który pozwala tworzyć formularz z zagnieżdżonymi wstawkami, takimi jak wstawki Django-admin.

W twoim przypadku byłoby to coś takiego:

views.py

class EducationInline(InlineFormSetFactory):
    model = Education
    fields = ['course_title', 'institute_name']


class EmployeeCreateView(CreateWithInlinesView):
    model = Employee
    inlines = [EducationInline,]
    fields = ['about', 'street', 'city', 'cell_phone', 'landline']
    template_name = 'bars/crt.html'

crt.html

<form method="post">
  ...
  {{ form }}
  <table>
  {% for formset in inlines %}
    {{ formset.management_form }}
      {% for inline_form in formset %}
        <tr class="{% cycle 'row1' 'row2' %} formset_row-{{ formset.prefix }}">
          {{ inline_form }}
        </tr>
      {% endfor %}
  {% endfor %}
  </table>
  ...
  <input type="submit" value="Submit" />
</form>

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js">
</script>
<script src="{% static 'js/jquery.formset.js' %}">
</script>
<script type="text/javascript">
    {% for formset in inlines %}
      $('.formset_row-{{ formset.prefix }}').formset({
          addText: 'add another',
          deleteText: 'remove',
          prefix: '{{ formset.prefix }}',
      });
    {% endfor %}
</script>

Widok EmployeeCreateViewprzetworzy dla ciebie formularze jak w Django-admin. Od tego momentu możesz zastosować żądany styl do formularzy.

Polecam odwiedzić dokumentację, aby uzyskać więcej informacji

ZMIENIONO: Dodałem management_form i przyciski js, aby dodać / usunąć.

Jan
źródło
Próbowałem już tego, ale nie pozwala mi to dodawać / usuwać przycisków dla wielu wstawek. Obsługuje tylko jeden wbudowany przycisk JS. Próbowałem już tego.
Shazia Nusrat
1
Obsługuje to, musisz dodać management_formdla każdegoformset
John
0

Powiedziałeś, że wystąpił błąd, ale nie wyświetlasz go w swoim pytaniu. Błąd (i całe śledzenie) jest ważniejszy niż cokolwiek, co napisałeś (może być z forms.py i views.py)

Twoja sprawa jest nieco trudniejsza ze względu na zestawy formularzy i używanie wielu formularzy w tym samym CreateView. W Internecie nie ma wielu (lub wielu dobrych) przykładów. Dopóki nie zagłębisz się w kod django, jak działają wbudowane zestawy formularzy, będziesz mieć problemy.

Ok od razu do rzeczy. Problem polega na tym, że zestawy formularzy nie są inicjowane z tą samą instancją, co formularz główny. A kiedy formularz aminowy zapisuje dane w bazie danych, instancja w zestawie formularzy nie jest zmieniana, a na końcu nie masz identyfikatora głównego obiektu, aby można go było wprowadzić jako klucz obcy. Zmiana atrybutu instancji atrybutu formularza po init nie jest dobrym pomysłem.

W normalnych formach, jeśli zmienisz go po is_valid, będziesz mieć nieprzewidywalne wyniki. W przypadku zestawów formularzy zmiana atrybutu instancji nawet bezpośrednio po init nie zmieni się, ponieważ formularze w zestawie formularzy są już zainicjowane z jakąś instancją, a zmiana po niej nie pomoże. Dobrą wiadomością jest to, że można zmienić atrybuty instancji po zainicjowaniu zestawu formularzy, ponieważ atrybuty instancji wszystkich formularzy będą wskazywały na ten sam obiekt po zainicjowaniu zestawu formularzy.

Masz dwie opcje:

Zamiast ustawiać atrybut instancji, jeśli zestaw formularzy, ustaw tylko instance.pk. (To tylko przypuszczenie, że nigdy tego nie zrobiłem, ale myślę, że powinno działać. Problem polega na tym, że będzie wyglądać jak hack). Utwórz formularz, który zainicjuje wszystkie formularze / zestawy formularzy jednocześnie. Gdy wywoływana jest metoda is_valid (), wszystkie fomrs powinny zostać sprawdzone. Po wywołaniu metody save () wszystkie formularze muszą zostać zapisane. Następnie należy ustawić atrybut form_class obiektu CreateView na tę klasę formularza. Jedyną trudną częścią jest to, że po zainicjowaniu głównego formularza należy zainicjować pozostałe (formsests) za pomocą wystąpienia pierwszego formularza. Musisz także ustawić formularze / zestawy formularzy jako atrybuty formularza, aby mieć do nich dostęp w szablonie. Korzystam z drugiego podejścia, gdy muszę utworzyć obiekt ze wszystkimi powiązanymi obiektami.

zainicjowany z niektórymi danymi (w tym przypadku danymi POST) sprawdzonymi pod kątem ważności za pomocą is_valid () można zapisać za pomocą save (), gdy jest ważny. Zachowujesz interfejs formularza i jeśli poprawnie wykonałeś formularz, możesz go nawet użyć nie tylko do tworzenia, ale i do aktualizacji obiektów wraz z obiektami pokrewnymi, a widoki będą bardzo proste.

Alexis Rouxel
źródło