Django-ネストされたフォームセットでフォームを保存しないCreateView


14

Django-Crispy-Formsレイアウト機能を使用して、ネストされたフォームセットをメインフォームに保存する方法を採用しようとしていますが、保存できません。このコード例のプロジェクトに従っていますが、フォームセットを検証してデータを保存できませんでした。誰かが私の間違いを指摘していただければ本当にありがたいです。また、EmployeeFormの同じビューに3つのインラインを追加する必要があります。Django-Extra-Viewsを試しましたが、うまくいきませんでした。約5のように、同じビューに複数のインラインを追加することをアドバイスしていただければ幸いです。作成する1つのページEmployeeとそのようなインラインを実現したいだけですEducation, Experience, Others。以下はコードです:

モデル:

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)

見る:

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

フォーム:

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

例によるカスタムレイアウトオブジェクト:

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>

端末やその他のエラーはありません。ヘルプは大歓迎です。


もう1つの解決策は、フォームにフォームセットを処理させることです。私は、schinckel.net / 2019/05/23 / form
Matthew Schinckel

回答:


0

現在、フォームセットをで適切に処理していませんCreateViewform_validそのビューでは、フォームセットではなく、親フォームのみを処理します。あなたがすべきことはpostメソッドをオーバーライドすることであり、フォームとそれにアタッチされているフォームセットの両方を検証する必要があります:

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)

次に、次のように変更しますform_valid

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

あなたが現在使用している方法get_context_data()は正しくありません-その方法を完全に削除してください。テンプレートをレンダリングするためのコンテキストデータをフェッチするためにのみ使用する必要があります。form_valid()メソッドからは呼び出さないでください。代わりにpost()、上記のメソッドからフォームセットをこのメソッドに渡す必要があります。

上記のサンプルコードにいくつかコメントを残しましたが、これを理解するのに役立ちます。


回答する前に、サンプルをローカルで再作成してください。私はあなたの作品を試しましたが、うまくいきませんでした。
Shazia Nusrat

1
@ShaziaNusrat申し訳ありませんが、特に何を試したのか、何が機能しなかったのかを言わなかった場合、特に何が機能していないのかを試す時間はありません(「機能していません」は、うまくいかなかったものの適切な説明)。私の答えには、現在の実装で何を変更する必要があるかを特定するのに十分であると思います。そうでない場合は、他の誰かがあなたにもっと包括的な答えを与えることができることを願いましょう。
solarissmoke

テスト用のコードで試したところ、問題が発生しました。ですから、私を率直に案内できるように、ローカルで試していただくようにお願いします。あなたが私を助けるのに少し時間をかけたので、私は感謝しています。しかし、機能していません。
Shazia Nusrat

0

たぶんあなたはパッケージを見たいと思いdjango-extra-viewsます、はビューを提供しCreateWithInlinesView、魔女はDjango-admin inlinesのようなネストされたインラインでフォームを作成することを可能にします。

あなたの場合、それはそのようなものでしょう:

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>

ビューEmployeeCreateViewはDjango-adminと同様にフォームを処理します。この時点から、必要なスタイルをフォームに適用できます。

詳細については、ドキュメントにアクセスすることをお勧めします

編集:私は追加management_form し、追加/削除するjsボタン。


私はすでにそれを試しましたが、それは複数のインラインのための追加/削除ボタンを持たせません。これは、JSボタン付きのインラインを1つだけサポートします。私はすでにそれを試しました。
Shazia Nusrat

1
それはそれをサポートします、あなたはmanagement_formそれぞれに追加する必要がありますformset
ジョン

0

エラーが発生したとのことですが、質問には表示されません。エラー(およびトレースバック全体)は、あなたが書いたものよりも重要です(forms.pyとviews.pyからのものである可能性があります)

フォームセットと同じCreateViewで複数のフォームを使用するため、ケースは少しトリッキーです。インターネットには多くの(または多くの良い)例はありません。インラインフォームセットがどのように機能するかをdjangoコードで掘り下げるまで、問題が発生します。

ポイントにまっすぐにわかりました。問題は、フォームセットがメインフォームと同じインスタンスで初期化されていないことです。そして、あなたのアミンフォームがデータベースにデータを保存するとき、フォームセットのインスタンスは変更されず、最終的には外部キーとして配置するためのメインオブジェクトのIDがありません。初期化後にフォーム属性のインスタンス属性を変更することはお勧めできません。

通常の形式では、is_validの後に変更すると、予期しない結果が生じます。initの直後でもインスタンス属性を変更するフォームセットの場合、何も起こらないため、フォームセット内のフォームはすでにいくつかのインスタンスで初期化されているため、後で変更しても効果がありません。すべてのフォームのインスタンス属性はformsetの初期化後に同じオブジェクトを指すため、Formsetの初期化後にインスタンスの属性を変更できるというのは良いニュースです。

次の2つのオプションがあります。

formsetの場合はインスタンス属性を設定する代わりに、instance.pkのみを設定します。(これは私がやったことはないという推測にすぎませんが、うまくいくと思います。問題はハックのように見えることです)。すべてのフォーム/フォームセットを一度に初期化するフォームを作成します。is_valid()メソッドが呼び出されると、すべてのフォームを検証する必要があります。save()メソッドが呼び出されると、すべてのフォームを保存する必要があります。次に、CreateViewのform_class属性をそのフォームクラスに設定する必要があります。唯一のトリッキーな部分は、メインフォームが初期化された後、最初のフォームのインスタンスで他のフォーム(フォームセット)を初期化する必要があることです。また、テンプレートでフォーム/フォームセットにアクセスするには、フォームの属性としてフォーム/フォームセットを設定する必要があります。関連するすべてのオブジェクトを含むオブジェクトを作成する必要がある場合は、2番目の方法を使用します。

is_valid()で有効性が確認されたいくつかのデータ(この場合はPOSTデータ)で初期化され、有効なときにsave()で保存できます。フォームインターフェイスを保持し、フォームを正しく作成した場合は、オブジェクトを作成するだけでなく、オブジェクトとその関連オブジェクトを更新するためにも使用でき、ビューは非常にシンプルになります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.