Django - CreateView ne sauvegarde pas le formulaire avec le jeu de formulaires imbriqué

14

J'essaie d'adapter une approche pour enregistrer des ensembles de formulaires imbriqués avec le formulaire principal à l'aide de la fonctionnalité de mise en page Django-Crispy-Forms, mais je ne peux pas l'enregistrer. Je suis en train de suivre cet exemple de projet, mais je n'ai pas pu valider formset pour enregistrer les données. Je serai vraiment reconnaissant si quelqu'un peut signaler mon erreur. J'ai également besoin d'ajouter trois inlines dans la même vue pour EmployeeForm. J'ai essayé Django-Extra-Views mais je n'ai pas pu faire fonctionner ça. J'apprécierais si vous conseillez d'ajouter plus d'une ligne pour la même vue, comme environ 5. Tout ce que je veux, c'est une seule page pour la création Employeeet ses lignes comme Education, Experience, Others. Voici le code:

des modèles:

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)

Vue:

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

Formes:

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

Objet de mise en page personnalisé selon l'exemple:

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>

Il n'y a aucune erreur dans le terminal et / ou autrement. L'aide est très appréciée.

Shazia Nusrat
la source
Une autre solution consiste à faire gérer le formulaire par le formulaire également: je le fais en utilisant un cached_property pour le formulaire associé dans schinckel.net/2019/05/23/form-and-formset
Matthew Schinckel

Réponses:

0

Vous ne traitez pas actuellement correctement l'ensemble de formulaires dans votre CreateView. form_validdans cette vue, ne traitera que le formulaire parent, pas les jeux de formulaires. Ce que vous devez faire est de remplacer la postméthode, et là, vous devez valider le formulaire et tous les jeux de formulaires qui lui sont attachés:

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)

Ensuite, vous modifiez form_validcomme suit:

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

La façon dont vous utilisez actuellement get_context_data()n'est pas correcte - supprimez complètement cette méthode. Il ne doit être utilisé que pour récupérer des données de contexte pour le rendu d'un modèle. Vous ne devez pas l'appeler depuis votre form_valid()méthode. Au lieu de cela, vous devez passer le formset à cette méthode à partir de la post()méthode décrite ci-dessus.

J'ai laissé quelques commentaires supplémentaires dans l'exemple de code ci-dessus qui, je l'espère, vous aideront à comprendre cela.

solarissmoke
la source
Veuillez recréer un exemple localement avant de répondre. J'ai essayé ta pièce mais je ne travaille pas.
Shazia Nusrat
1
@ShaziaNusrat désolé, je n'ai pas le temps d'essayer de comprendre ce qui ne fonctionne pas pour vous, surtout si vous ne dites pas ce que vous avez essayé et ce qui n'a pas fonctionné ("ça ne marche pas" n'est pas un description adéquate de ce qui n'a pas fonctionné). Je crois qu'il y a suffisamment dans ma réponse pour vous aider à identifier ce que vous devez changer avec votre implémentation actuelle. Sinon, espérons que quelqu'un d'autre pourra vous donner une réponse plus complète.
solarissmoke
Je l'ai essayé dans le code pour les tests et il a rencontré des problèmes. C'est pourquoi je vous demande humblement de l'essayer de votre côté localement afin que vous puissiez mieux me guider. Je vous suis reconnaissant d'avoir pris le temps de m'aider. Mais ne fonctionne pas.
Shazia Nusrat
0

Peut-être que vous aimeriez voir le package django-extra-views, le fournit la vue CreateWithInlinesView, qui vous permet de créer un formulaire avec des inlines imbriquées comme Django-admin inlines.

Dans votre cas, ce serait quelque chose comme ça:

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 vue EmployeeCreateViewtraitera les formulaires pour vous comme dans Django-admin. À partir de là, vous pouvez appliquer le style souhaité aux formulaires.

Je vous recommande de visiter la documentation pour plus d'informations

EDITÉ: j'ai ajouté management_form et les boutons js pour ajouter / supprimer.

John
la source
J'ai déjà essayé cela, mais cela ne me permettra pas d'avoir des boutons d'ajout / suppression pour plusieurs inserts. Il ne prend en charge qu'une seule ligne avec des boutons JS. J'ai déjà essayé ça.
Shazia Nusrat
1
Il le supporte, vous devez ajouter le management_formpour chacunformset
John
0

Vous avez dit qu'il y avait une erreur mais vous ne la montrez pas dans votre question. L'erreur (et toute la trace) est plus importante que tout ce que vous avez écrit (sauf qu'elle peut provenir de forms.py et views.py)

Votre cas est un peu plus délicat en raison des jeux de formulaires et de l'utilisation de plusieurs formulaires sur le même CreateView. Il n'y a pas beaucoup (ou pas beaucoup de bons) exemples sur Internet. Jusqu'à ce que vous creusiez dans le code django comment fonctionnent les jeux de formulaires en ligne, vous aurez des problèmes.

Ok droit au but. Votre problème est que les jeux de formulaires ne sont pas initialisés avec la même instance que votre formulaire principal. Et lorsque votre formulaire amin enregistre les données dans la base de données, l'instance du jeu de formulaires n'est pas modifiée et, à la fin, vous n'avez pas l'ID de l'objet principal pour être mis en tant que clé étrangère. Changer l'attribut d'instance d'un attribut de formulaire après init n'est pas une bonne idée.

Dans des formes normales, si vous le modifiez après is_valid, vous obtiendrez des résultats imprévisibles. Pour les jeux de formulaires, changer l'attribut d'instance même directement après init ne fera rien, car les formulaires dans le jeu de formulaires sont déjà initialisés avec une instance, et le changer après n'aidera pas. La bonne nouvelle est que vous pouvez modifier les attributs de l'instance après l'initialisation de Formset, car tous les attributs d'instance de formulaires pointeront vers le même objet après l'initialisation de formset.

Vous avez deux options:

Au lieu de définir l'attribut d'instance si le formset, définissez uniquement l'instance.pk. (C'est juste une supposition que je n'ai jamais fait, mais je pense que cela devrait fonctionner. Le problème est qu'il ressemblera à du hack). Créez un formulaire qui initialisera tous les formulaires / jeux de formulaires à la fois. Quand c'est la méthode is_valid () qui est appelée, tous les fomrs doivent être validés. Quand sa méthode save () est appelée, tous les formulaires doivent être enregistrés. Ensuite, vous devez définir l'attribut form_class de votre CreateView sur cette classe de formulaire. La seule partie délicate est qu'après l'initialisation de votre formulaire principal, vous devez initialiser les autres (formulaires) avec l'instance de votre premier formulaire. Vous devez également définir les formulaires / jeux de formulaires en tant qu'attributs de votre formulaire afin d'y avoir accès dans le modèle. J'utilise la deuxième approche lorsque j'ai besoin de créer un objet avec tous ses objets associés.

initialisé avec certaines données (dans ce cas données POST) dont la validité est vérifiée avec is_valid () peut être enregistré avec save () lorsqu'il est valide. Vous conservez l'interface du formulaire et si vous avez créé votre formulaire correctement, vous pouvez même l'utiliser non seulement pour créer mais aussi pour mettre à jour des objets avec leurs objets associés et les vues seront très simples.

Alexis Rouxel
la source