Django - CreateView speichert kein Formular mit verschachteltem Formularsatz

14

Ich versuche, einen Ansatz zum Speichern verschachtelter Formularsätze mit dem Hauptformular mithilfe der Layoutfunktion Django-Crispy-Forms anzupassen, kann ihn jedoch nicht speichern. Ich verfolge dieses Codebeispielprojekt, konnte jedoch das Formularset nicht validieren, um Daten zu speichern. Ich werde wirklich dankbar sein, wenn jemand auf meinen Fehler hinweisen kann. Ich muss auch drei Inlines in derselben Ansicht für EmployeeForm hinzufügen. Ich habe Django-Extra-Views ausprobiert, konnte das aber nicht zum Laufen bringen. Würde mich freuen, wenn Sie empfehlen, mehr als eine Inline für dieselbe Ansicht wie etwa 5 hinzuzufügen. Alles, was ich erreichen möchte, ist eine einzelne Seite zum Erstellen Employeeund deren Inlines wie Education, Experience, Others. Unten ist der Code:

Modelle:

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)

Aussicht:

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

Formen:

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')),
                )
            )

Benutzerdefiniertes Layoutobjekt wie im Beispiel:

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>

Es gibt keine Fehler im Terminal und / oder auf andere Weise. Hilfe wird sehr geschätzt.

Shazia Nusrat
quelle
Eine alternative Lösung besteht darin, dass das Formular auch das Formularset verarbeitet: Ich verwende eine cached_property für das zugehörige Formularset in schinckel.net/2019/05/23/form-and-formset
Matthew Schinckel

Antworten:

0

Sie verarbeiten das Formularset derzeit nicht ordnungsgemäß in Ihrem CreateView. form_validIn dieser Ansicht wird nur das übergeordnete Formular behandelt, nicht die Formularsätze. Was Sie tun sollten, ist die postMethode zu überschreiben , und dort müssen Sie sowohl das Formular als auch alle damit verbundenen Formularsätze überprüfen:

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)

Dann ändern Sie form_validwie folgt:

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

Die Art und Weise, wie Sie sie derzeit verwenden, get_context_data()ist nicht korrekt. Entfernen Sie diese Methode vollständig. Es sollte nur zum Abrufen von Kontextdaten zum Rendern einer Vorlage verwendet werden. Sie sollten es nicht von Ihrer form_valid()Methode aus aufrufen . Stattdessen müssen Sie das Formularset von der oben beschriebenen post()Methode an diese Methode übergeben .

Ich habe im obigen Beispielcode einige zusätzliche Kommentare hinterlassen, die Ihnen hoffentlich dabei helfen werden, dies herauszufinden.

Solarissmoke
quelle
Bitte erstellen Sie ein Beispiel lokal neu, bevor Sie antworten. Ich habe dein Stück ausprobiert, aber es funktioniert nicht.
Shazia Nusrat
1
@ShaziaNusrat Entschuldigung, ich habe keine Zeit herauszufinden, was für Sie nicht funktioniert, besonders wenn Sie nicht sagen, was Sie versucht haben und was nicht funktioniert hat ("Es funktioniert nicht" ist keine angemessene Beschreibung dessen, was nicht funktioniert hat). Ich glaube, meine Antwort enthält genug Informationen, um herauszufinden, was Sie an Ihrer aktuellen Implementierung ändern müssen. Wenn nicht, hoffen wir, dass jemand anderes Ihnen eine umfassendere Antwort geben kann.
Solarissmoke
Ich habe es im Code zum Testen versucht und es lief mit Problemen. Deshalb bitte ich Sie demütig, es vor Ort auf Ihrer Seite zu versuchen, damit Sie mich besser führen können. Ich bin dankbar, dass Sie sich etwas Zeit genommen haben, um mir zu helfen. Aber nicht funktionieren.
Shazia Nusrat
0

Vielleicht möchten Sie das Paket sehen django-extra-views, das die Ansicht bietet CreateWithInlinesView, mit der Sie Formulare mit verschachtelten Inlines wie Django-Admin-Inlines erstellen können.

In Ihrem Fall wäre es ungefähr so:

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>

Die Ansicht EmployeeCreateViewverarbeitet die Formulare für Sie wie in Django-admin. Ab diesem Punkt können Sie den gewünschten Stil auf die Formulare anwenden.

Ich empfehle Ihnen, die Dokumentation zu besuchen , um weitere Informationen zu erhalten

BEARBEITET: Ich habe hinzugefügt management_form und die js-Schaltflächen hinzugefügt / entfernt.

John
quelle
Ich habe das bereits versucht, aber dadurch kann ich keine Schaltflächen zum Hinzufügen / Löschen für mehrere Inlines verwenden. Es wird nur eine Inline mit JS-Schaltflächen unterstützt. Das habe ich schon versucht.
Shazia Nusrat
1
Es unterstützt es, Sie müssen das management_formfür jeden hinzufügenformset
John
0

Sie haben gesagt, dass ein Fehler vorliegt, aber Sie zeigen ihn in Ihrer Frage nicht an. Der Fehler (und der gesamte Traceback) ist wichtiger als alles, was Sie geschrieben haben (außer möglicherweise von forms.py und views.py).

Ihr Fall ist aufgrund der Formularsätze und der Verwendung mehrerer Formulare in derselben CreateView etwas schwieriger. Es gibt nicht viele (oder nicht viele gute) Beispiele im Internet. Bis Sie im Django-Code nachlesen, wie Inline-Formsets funktionieren, werden Sie Probleme haben.

Ok direkt auf den Punkt. Ihr Problem ist, dass Formelsätze nicht mit derselben Instanz wie Ihr Hauptformular initialisiert werden. Und wenn Ihr Amin-Formular die Daten in der Datenbank speichert, wird die Instanz im Formularsatz nicht geändert und am Ende haben Sie nicht die ID des Hauptobjekts, um als Fremdschlüssel abgelegt zu werden. Das Ändern des Instanzattributs eines Formularattributs nach init ist keine gute Idee.

In normalen Formen haben Sie unvorhersehbare Ergebnisse, wenn Sie es nach is_valid ändern. Bei Formularsätzen ist das Ändern des Instanzattributs auch direkt nach init nicht ausreichend, da die Formulare im Formularsatz bereits mit einer Instanz initialisiert wurden und das Ändern nach dem Formularsatz nicht hilft. Die gute Nachricht ist, dass Sie die Attribute der Instanz nach der Initialisierung von Formset ändern können, da alle Instanzattribute von Forms nach der Initialisierung von Formset auf dasselbe Objekt verweisen.

Sie haben zwei Möglichkeiten:

Anstatt das Instanzattribut für das Formset festzulegen, legen Sie nur die Instanz.pk fest. (Dies ist nur eine Vermutung, dass ich es nie getan habe, aber ich denke, es sollte funktionieren. Das Problem ist, dass es als Hack aussehen wird). Erstellen Sie ein Formular, das alle Formulare / Formularsätze gleichzeitig initialisiert. Wenn die Methode is_valid () aufgerufen wird, sollten alle fomrs validiert werden. Wenn die Methode save () aufgerufen wird, müssen alle Formulare gespeichert werden. Dann müssen Sie das Attribut form_class Ihrer CreateView auf diese Formularklasse setzen. Der einzige schwierige Teil ist, dass Sie nach der Initialisierung Ihres Hauptformulars die anderen (Formsests) mit der Instanz Ihres ersten Formulars initialisieren müssen. Außerdem müssen Sie die Formulare / Formularsätze als Attribute Ihres Formulars festlegen, um in der Vorlage darauf zugreifen zu können. Ich verwende den zweiten Ansatz, wenn ich ein Objekt mit allen zugehörigen Objekten erstellen muss.

initialisiert mit einigen Daten (in diesem Fall POST-Daten), die mit is_valid () auf Gültigkeit geprüft wurden, können mit save () gespeichert werden, wenn sie gültig sind. Sie behalten die Formularoberfläche bei und wenn Sie Ihr Formular korrekt erstellt haben, können Sie es sogar nicht nur zum Erstellen, sondern auch zum Aktualisieren von Objekten zusammen mit den zugehörigen Objekten verwenden. Die Ansichten sind sehr einfach.

Alexis Rouxel
quelle