Django - CreateView não salva o formulário com o formset aninhado

14

Estou tentando adaptar uma abordagem para salvar conjuntos de formulários aninhados com o formulário principal usando o recurso de layout Django-Crispy-Forms, mas não consigo salvá-lo. Estou seguindo este projeto de exemplo de código, mas não foi possível validar o formset para salvar dados. Ficarei muito agradecido se alguém puder apontar meu erro. Também preciso adicionar três linhas na mesma exibição para EmployeeForm. Eu tentei o Django-Extra-Views, mas não consegui fazer esse trabalho. Apreciaria se você aconselhase a adição de mais de uma linha para a mesma exibição como em torno de 5. Tudo o que eu quero é que uma única página para criação Employeee suas linhas como Education, Experience, Others. Abaixo está o 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)

Visão:

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

Formulários:

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 layout personalizado, como por exemplo:

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>

Não há erros no terminal e / ou não. A ajuda é muito apreciada.

Shazia Nusrat
fonte
Uma solução alternativa é fazer com que o formulário lide também com o formset: eu o faço usando uma propriedade cached_property para o formset relacionado em schinckel.net/2019/05/23/form-and-formset
Matthew Schinckel

Respostas:

0

No momento, você não está processando o formset corretamente no seu CreateView. form_validnessa visão, manipulará apenas o formulário pai, não os conjuntos de formulários. O que você deve fazer é substituir o postmétodo e é necessário validar o formulário e quaisquer conjuntos de formulários anexados a ele:

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)

Então você modifica form_validassim:

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

A maneira como você está usando no momento get_context_data()não está correta - remova esse método completamente. Só deve ser usado para buscar dados de contexto para renderizar um modelo. Você não deve chamá-lo do seu form_valid()método. Em vez disso, você precisa passar o formset para esse método a partir do post()método descrito acima.

Deixei alguns comentários adicionais no código de exemplo acima, que esperamos ajudar você a descobrir isso.

solarissmoke
fonte
Recrie um exemplo localmente antes de responder. Eu tentei sua peça, mas não estou funcionando.
Shazia Nusrat 26/02
11
@ ShaziaNusrat desculpe, não tenho tempo para tentar descobrir o que não está funcionando para você, especialmente se você não diz o que tentou e o que não funcionou ("Não está funcionando" não é um descrição adequada do que não funcionou). Acredito que exista bastante em minha resposta para ajudá-lo a identificar o que você precisa alterar com sua implementação atual. Caso contrário, esperamos que outra pessoa seja capaz de fornecer uma resposta mais abrangente.
solarissmoke
Eu tentei no código para teste e correu com problemas. É por isso que humildemente solicito que você experimente do seu lado localmente, para que você possa me orientar melhor. Sou grato por você ter demorado algum tempo para me ajudar. Mas não está funcionando.
Shazia Nusrat 26/02
0

Talvez você queira ver o pacote django-extra-views, o fornece a view CreateWithInlinesView, pois permite criar formulários com linhas aninhadas, como as linhas do Django-admin.

No seu caso, seria algo assim:

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>

A view EmployeeCreateViewprocessará os formulários para você como no Django-admin. A partir deste ponto, você pode aplicar o estilo que deseja aos formulários.

Eu recomendo que você visite a documentação para obter mais informações

EDITADO: Eu adicionei management_form e os botões js para adicionar / remover.

John
fonte
Eu já tentei isso, mas isso não me permite adicionar / excluir botões para várias linhas internas. Ele suporta apenas uma linha com botões JS. Eu já tentei isso.
Shazia Nusrat 4/03
11
Ele suporta, você tem que adicionar o management_formpara cadaformset
John
0

Você disse que há um erro, mas não o está mostrando na sua pergunta. O erro (e todo o rastreamento) é mais importante do que qualquer coisa que você escreveu (exceto pode ser de forms.py e views.py)

Seu caso é um pouco mais complicado por causa dos conjuntos de formulários e do uso de vários formulários no mesmo CreateView. Não há muitos (ou não muitos bons) exemplos na internet. Até você digitar no código django como os formulários embutidos estão funcionando, você terá problemas.

Ok, direto ao ponto. Seu problema é que os conjuntos de formulários não são inicializados com a mesma instância do seu formulário principal. E quando seu formulário amin salva os dados no banco de dados, a instância no conjunto de formulários não é alterada e, no final, você não possui o ID do objeto principal para ser colocado como chave estrangeira. Alterar o atributo de instância de um atributo de formulário após o init não é uma boa ideia.

Em formas normais, se você alterá-lo após is_valid, terá resultados imprevisíveis. Para os conjuntos de formulários, a alteração do atributo da instância, mesmo diretamente após o init, não será alterada, pois os formulários no formset já foram inicializados com alguma instância e alterá-lo após não ajudará. A boa notícia é que você pode alterar os atributos da instância após a inicialização do Formset, porque todos os atributos da instância do formulário apontarão para o mesmo objeto após a inicialização do formset.

Você tem duas opções:

Em vez de definir o atributo da instância se o formset, defina apenas o instance.pk. (Isso é apenas um palpite: eu nunca faço isso, mas acho que deve funcionar. O problema é que ele parecerá hackeado). Crie um formulário que inicialize todos os formulários / conjuntos de formulários de uma só vez. Quando o método is_valid () é chamado, todos os fomrs devem ser validados. Quando o método save () é chamado, todos os formulários devem ser salvos. Em seguida, você precisa definir o atributo form_class do seu CreateView para essa classe de formulário. A única parte complicada é que, após a inicialização do formulário principal, você precisa inicializar os outros (testes de formulários) com a instância do seu primeiro formulário. Você também precisa definir os formulários / conjuntos de formulários como atributos do seu formulário para ter acesso a eles no modelo. Estou usando a segunda abordagem quando preciso criar um objeto com todos os objetos relacionados.

inicializado com alguns dados (neste caso, dados POST) verificados quanto à validade com is_valid () pode ser salvo com save () quando é válido. Você preserva a interface do formulário e, se você o criou corretamente, pode usá-lo não apenas para criar, mas também para atualizar objetos junto com seus objetos relacionados, e as visualizações serão muito simples.

Alexis Rouxel
fonte