Django - CreateView non salva il modulo con il formset nidificato


14

Sto cercando di adattare un approccio per il salvataggio di formati nidificati con il modulo principale utilizzando la funzione di layout di Django-Crispy-Forms ma non riesco a salvarlo. Sto seguendo questo progetto di esempio di codice ma non sono riuscito a convalidare il formset per salvare i dati. Sarò davvero grato se qualcuno può sottolineare il mio errore. Devo anche aggiungere tre linee in linea nella stessa vista per EmployeeForm. Ho provato Django-Extra-Views ma non sono riuscito a farlo funzionare. Ti sarei grato se mi consiglia di aggiungere più di una riga per la stessa vista di circa 5. Tutto quello che voglio ottenere è una singola pagina per la creazione Employeee le sue linee simili Education, Experience, Others. Di seguito è riportato il codice:

Modelli:

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)

Visualizza:

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

Le forme:

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

Oggetto layout personalizzato come da esempio:

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>

Non ci sono errori nel terminale eo altrimenti. L'aiuto è molto apprezzato.


Una soluzione alternativa è che il form gestisca anche il formset: lo faccio usando un cached_property per il formset relativo in schinckel.net/2019/05/23/form-and-formset
Matthew Schinckel

Risposte:


0

Al momento non stai elaborando correttamente il formset nel tuo CreateView. form_validin quella vista gestirà solo il modulo padre, non i moduli. Quello che dovresti fare è sovrascrivere il postmetodo e lì devi convalidare sia il modulo che tutti i moduli ad esso collegati:

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)

Quindi modifichi in questo form_validmodo:

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

Il modo in cui stai attualmente utilizzando get_context_data()non è corretto: rimuovi completamente quel metodo. Dovrebbe essere usato solo per recuperare i dati di contesto per il rendering di un modello. Non dovresti chiamarlo dal tuo form_valid()metodo. Invece è necessario passare il formset a questo metodo dal post()metodo come indicato sopra.

Ho lasciato alcuni commenti aggiuntivi nel codice di esempio sopra che, si spera, ti aiuteranno a capirlo.


Ricreare un esempio localmente prima di rispondere. Ho provato il tuo pezzo ma non funziona.
Shazia Nusrat,

1
@ShaziaNusrat mi dispiace, non ho tempo di provare a capire cosa non funziona per te soprattutto se non dici ciò che hai provato e ciò che non ha funzionato ("Non funziona" non è un descrizione adeguata di ciò che non ha funzionato). Credo che ci sia abbastanza nella mia risposta per aiutarti a identificare ciò che devi cambiare con la tua attuale implementazione. In caso contrario, speriamo che qualcun altro sia in grado di darti una risposta più completa.
solarissmoke,

L'ho provato in codice per i test e ha funzionato con problemi. Ecco perché ti chiedo umilmente di provarlo dalla tua parte a livello locale in modo da potermi guidare meglio. Sono grato perché hai impiegato del tempo per aiutarmi. Ma non funziona.
Shazia Nusrat,

0

Forse ti piacerebbe vedere il pacchetto django-extra-views, il fornisce la vista CreateWithInlinesView, strega ti consente di creare un modulo con linee nidificate come linee Django-admin.

Nel tuo caso, sarebbe qualcosa del genere:

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>

La vista EmployeeCreateViewelaborerà i moduli per te come in Django-admin. Da questo punto è possibile applicare lo stile desiderato ai moduli.

Vi consiglio di visitare la documentazione per ulteriori informazioni

MODIFICATO: ho aggiunto management_form e i pulsanti js per aggiungere / rimuovere.


L'ho già provato, ma ciò non mi consente di aggiungere / eliminare pulsanti per più linee. Supporta solo uno in linea con i pulsanti JS. Ci ho già provato.
Shazia Nusrat,

1
Lo supporta, devi aggiungere il management_formper ciascunoformset
John

0

Hai detto che c'è un errore ma non lo stai mostrando nella tua domanda. L'errore (e l'intero traceback) è più importante di qualsiasi cosa tu abbia scritto (tranne che può essere da forms.py e views.py)

Il tuo caso è un po 'più complicato a causa dei formati e dell'utilizzo di più moduli sullo stesso CreateView. Non ci sono molti (o non molti buoni) esempi su Internet. Finché non digiti il ​​codice di django su come funzionano i form inline, avrai dei problemi.

Ok dritto al punto. Il problema è che i formati non sono inizializzati con la stessa istanza del modulo principale. E quando il modulo amin salva i dati nel database, l'istanza nel formset non viene modificata e alla fine non si dispone dell'ID dell'oggetto principale per essere inserito come chiave esterna. Cambiare l'attributo di istanza di un attributo form dopo init non è una buona idea.

In forme normali se lo chnage dopo is_valid si otterranno risultati imprevedibili. Per i form che cambiano l'attributo dell'istanza anche direttamente dopo che init non funzionerà, perché i form nel formset sono già inizializzati con qualche istanza e cambiarlo dopo non sarà di aiuto. La buona notizia è che è possibile modificare gli attributi dell'istanza dopo l'inizializzazione di Formset, poiché tutti gli attributi dell'istanza dei moduli punteranno allo stesso oggetto dopo l'inizializzazione del formset.

Hai due opzioni:

Invece di impostare l'attributo dell'istanza se il formset, imposta solo il file instance.pk. (Questa supposizione non l'ho mai fatto ma penso che dovrebbe funzionare. Il problema è che sembrerà un hack). Creare un modulo che inizializzerà contemporaneamente tutti i moduli / moduli. Quando viene chiamato il metodo is_valid (), tutti i moduli devono essere convalidati. Quando viene chiamato il metodo save (), tutti i moduli devono essere salvati. Quindi è necessario impostare l'attributo form_class di CreateView su quella classe di modulo. L'unica parte difficile è che dopo l'inizializzazione del modulo principale è necessario inizializzare gli altri (formests) con l'istanza del primo modulo. Inoltre, è necessario impostare i moduli / i moduli come attributi del modulo per poter accedere ad essi nel modello. Sto usando il secondo approccio quando devo creare un oggetto con tutti i suoi oggetti correlati.

inizializzato con alcuni dati (in questo caso dati POST) verificati per validità con is_valid () possono essere salvati con save () quando è valido. Conserva l'interfaccia del modulo e, se hai creato correttamente il modulo, puoi anche utilizzarlo non solo per la creazione ma per l'aggiornamento degli oggetti insieme ai loro oggetti correlati e le viste saranno molto semplici.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.