Django: CreateView no guarda el formulario con un conjunto de formularios anidado

14

Estoy tratando de adaptar un enfoque para guardar conjuntos de formularios anidados con el formulario principal utilizando la función de diseño Django-Crispy-Forms, pero no puedo guardarlo. Estoy siguiendo este proyecto de ejemplo de código, pero no pude validar el conjunto de formularios para guardar datos. Estaré realmente agradecido si alguien puede señalar mi error. También necesito agregar tres líneas en la misma vista para EmployeeForm. Intenté Django-Extra-Views pero no pude hacer que eso funcionara. Le agradecería si me aconseja agregar más de una línea para la misma vista, como alrededor de 5. Todo lo que quiero es lograr una sola página para crear Employeey sus líneas como Education, Experience, Others. Debajo está el código:

modelos:

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)

Ver:

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

Formas:

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

Objeto de diseño personalizado como por ejemplo:

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>

No hay errores en la terminal y / o de otra manera. La ayuda es muy apreciada.

Shazia Nusrat
fuente
Una solución alternativa es hacer que el formulario maneje también el conjunto de formularios: lo hago usando una propiedad en caché para el conjunto de formularios relacionado en schinckel.net/2019/05/23/form-and-formset
Matthew Schinckel

Respuestas:

0

Actualmente no está procesando el formulario correctamente en su CreateView. form_validen esa vista solo manejará el formulario principal, no los conjuntos de formularios. Lo que debe hacer es anular el postmétodo, y allí debe validar tanto el formulario como los conjuntos de formularios adjuntos:

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)

Luego modificas form_validasí:

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 forma en que está utilizando actualmente get_context_data()no es correcta: elimine ese método por completo. Solo debe usarse para obtener datos de contexto para representar una plantilla. No deberías llamarlo desde tu form_valid()método. En su lugar, debe pasar el conjunto de formularios a este método desde el post()método descrito anteriormente.

Dejé algunos comentarios adicionales en el código de muestra anterior que esperamos lo ayuden a resolver esto.

solarissmoke
fuente
Vuelva a crear un ejemplo localmente antes de responder. He probado tu pieza pero no funciona.
Shazia Nusrat
1
@ShaziaNusrat lo siento, no tengo tiempo para tratar de resolver lo que no funciona para ti, especialmente si no dices lo que has intentado y lo que no funcionó ("No está funcionando" no es un descripción adecuada de lo que no funcionó). Creo que hay suficiente en mi respuesta para ayudarlo a identificar lo que necesita cambiar con su implementación actual. Si no es así, esperemos que alguien más pueda darle una respuesta más completa.
Solarissmoke
Lo probé en código para probar y funcionó con problemas. Es por eso que humildemente te pido que lo pruebes a tu lado localmente para que puedas guiarme mejor. Estoy agradecido porque te tomaste un tiempo para ayudarme. Pero no funciona.
Shazia Nusrat
0

Tal vez le gustaría ver el paquete django-extra-views, que proporciona la vista CreateWithInlinesView, que le permite crear formularios con líneas anidadas como las líneas de Django-admin.

En su caso, sería algo así:

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 EmployeeCreateViewprocesará los formularios por usted como en Django-admin. Desde este punto, puede aplicar el estilo que desee a los formularios.

Te recomiendo que visites la documentación para más información.

EDITADO: agregué management_form y los botones js para agregar / eliminar.

Juan
fuente
Ya lo intenté pero eso no me permite tener botones de agregar / eliminar para múltiples líneas. Solo admite uno en línea con botones JS. Ya lo he intentado.
Shazia Nusrat
1
Lo admite, debe agregar el management_formpara cada unoformset
John
0

Dijiste que hay un error pero no lo estás mostrando en tu pregunta. El error (y todo el rastreo) es más importante que cualquier cosa que haya escrito (excepto que puede ser de forms.py y views.py)

Su caso es un poco más complicado debido a los conjuntos de formularios y al uso de múltiples formularios en el mismo CreateView. No hay muchos (o no muchos buenos) ejemplos en Internet. Hasta que profundice en el código de django cómo funcionan los conjuntos de formularios en línea, tendrá problemas.

Ok directo al grano. Su problema es que los conjuntos de formularios no se inicializan con la misma instancia que su formulario principal. Y cuando su formulario amin guarda los datos en la base de datos, la instancia en el conjunto de formularios no cambia y al final no tiene el ID del objeto principal para poder colocarlo como clave foránea. Cambiar el atributo de instancia de un atributo de formulario después de init no es una buena idea.

En formas normales, si lo intercambia después de is_valid, obtendrá resultados impredecibles. Para los conjuntos de formularios, el cambio del atributo de instancia incluso directamente después de que init no funcione, ya que los formularios en el conjunto de formularios ya están inicializados con alguna instancia, y cambiarlo después no ayudará. La buena noticia es que puede cambiar los atributos de la instancia después de inicializar Formset, porque todos los atributos de instancia de los formularios apuntarán al mismo objeto después de inicializar formset.

Tienes dos opciones:

En lugar de establecer el atributo de instancia si el conjunto de formularios, establece solo el instance.pk. (Esto es solo una suposición, nunca lo he hecho, pero creo que debería funcionar. El problema es que se verá como un truco). Cree un formulario que inicializará todos los formularios / conjuntos de formularios a la vez. Cuando se llama al método is_valid (), todos los formularios deben validarse. Cuando se llama al método save (), todos los formularios deben guardarse. Luego, debe establecer el atributo form_class de su CreateView en esa clase de formulario. La única parte difícil es que después de que se inicializa su formulario principal, debe inicializar los otros (conjuntos de formularios) con la instancia de su primer formulario. También debe establecer los formularios / conjuntos de formularios como atributos de su formulario para tener acceso a ellos en la plantilla. Estoy usando el segundo enfoque cuando necesito crear un objeto con todos sus objetos relacionados.

inicializado con algunos datos (en este caso, datos POST) verificados para verificar su validez con is_valid () se puede guardar con save () cuando es válido. Conserva la interfaz del formulario y, si realizó el formulario correctamente, incluso puede usarlo no solo para crear sino para actualizar objetos junto con sus objetos relacionados y las vistas serán muy simples.

Alexis Rouxel
fuente