スキップしてメイン コンテンツに移動

Djangoでサブフォームがある場合について

サブフォーム側に複数レコードがあり、なおかつ複数サブフォームを使う必要があり、はじめはインラインフォームをつかってできないかとおもいやっていたのですが、登録はともかく、更新などがうまくいきませんでした。そこでしらべたところ、ほぼそのままなんですが、こちらの記事を参考にしたところ、(ありがとうございます m(__)m) django-extra-viewsを使うことで凡そのことができたので、備忘録として挙げてみました。
まず、django-extra-viewsをインストールします。debianではpython3-django-extra-viewsパッケージがあるので、これをインストールしておきます。
つづいてviews.pyとforms.pyですが以下の様にしました。
#views.py
class artCreateView(CreateWithInlinesView):
    model = art
    fields = ('art_name', 'cat1', 'cat2', 'cat3', 'cat4', 'url1', 'url2',)
    context_object_name = 'art'
    inlines = [art_propForm]
    template_name = 'myapp/edit.html'
    success_url = reverse_lazy('pc:top')
    factory_kwargs = { 'can_order': True, 'can_delete': True}

class  artUpdateView(UpdateWithInlinesView):
    model = art
    form_class = artForm
    inlines = [art_propForm]
    template_name = 'myapp/edit.html'
    success_url = reverse_lazy('pc:top')
    factory_kwargs = { 'can_order': True, 'can_delete': True}
#forms.py
from django import forms
from extra_views.advanced import InlineFormSetFactory
from .models import art, art_prop

class artForm(forms.ModelForm):
    class Meta:
        model = art
        fields = ('art_name', 'cat1', 'cat2', 'cat3', 'cat4', 'url1', 'url2',)
    
    def save(self, commit=True):
        instance = super(artForm, self).save(commit=commit)

        if commit:
            instance.save()

        return instance

class art_propForm(InlineFormSetFactory):
    model = art_prop
    fields = '__all__'
viewsとformsで基本的に必要な記述は一サブフォームの場合これだけです。非常にシンプルですね。models.pyは割愛しますが、つづいてurls.pyとテンプレートは以下のようにしました。
from django.urls import path, include
from . import views

app_name = 'myapp'

urlpatterns = [
    path('create/', views.artCreateView.as_view(), name='create'),
    path('update/<int:pk>/', views.artUpdateView.as_view(), name='update'),
]
{% extends 'base/base.html' %}
{% load static%}

{% block content %}

<form action="." method="post">
{% csrf_token %}

<p>article</p>
<table>
    {{ form.as_table }}
</table>

{% for formset in inlines %}
    {{ formset.prefix }}
    <p>子テーブル(child{{forloop.counter}})</p>
    <table id="formset{{forloop.counter}}" class="table table-sm ">
        {{ formset.management_form }}
        {% for form in formset %}
            <!-- ヘッダ出力 -->
            {% ifequal forloop.counter0 0 %}
            {% for field in form %}
                <td class="bg-light">
                    {% if field.field.widget.is_hidden %}
                    {% else %}
                    {{ field.label_tag }}
                    {% endif %}
                </td>
            {% endfor %}
            {% endifequal %}
            <tr>
                {% for field in form %}
                <td>
                    {{ field }}
                </td>
                {% endfor %}
            </tr>
        {% endfor %}
    </table>
    <button id="add{{forloop.counter}}" type="button" class="ui standard button">add1</button>
    <button id="del{{forloop.counter}}" type="button" class="ui standard button">del1</button>
    
{% endfor %}
<br/>
<br/>
<input class="ui standard button" type="submit" value="登録" />

{% endblock content %}

{% block extrajs %}
<script>
    $('#add1').click(function() {
        var prop = $("#formset1 tr:last-child")
        var formCount = $('#id_art_prop_set-TOTAL_FORMS').val();
        clone = prop.clone().insertAfter(prop);
//        clone.children().not(':last').children().each(function(idx,elm){
        clone.children().children().each(function(idx,elm){
            console.log("add1: before updating...");
            updateElementIndex(this,'art_prop_set', formCount);
            $(this).val('');
            $(this).prop('checked', false)
        });
        $('#id_art_prop_set-TOTAL_FORMS').val(++formCount);
    });

    $('#del1').on('click', function(){
        var prop = $('#formset1 tr');
        var line = $('#id_art_prop_set-TOTAL_FORMS').val();
        var i = 0;
        var j = line;
        prop.each(function(IDX, ELM){
            if ($(ELM).find('input[type="checkbox"]').prop('checked')){
                i++;
                //console.log("i is : ", i);
                //console.log("DELETE is checkd: ", IDX);
                if (line == 1 || line == i ){
                    $(ELM).find("input").val('');
                    $(ELM).find("input:checkbox").prop('checked', false);
                    j=1;
                }
                else {
                    $(ELM).hide();
                    j = j -1;
                }                
            }
            else {
                //console.log("DELET is not checkd. ", IDX);
            }
//            $('#id_art_prop_set-TOTAL_FORMS').val(j);          
        });

        prop = $("#formset1 tr")
        console.log('prop.length', prop.length);
        for (var i=0, formCount = prop.length; i < formCount; ++i) {
//            $(prop.get(i)).children().not(':last').children().each(function() {
            $(prop.get(i)).find('input').not('label').each(function() {
                console.log('i - 1: ', i - 1);
                if (i == 0){
                    console.log('do nothing ...')
                }
                else
                {
//                    updateElementIndex(this, 'art_prop_set', i - 1);
                }              
            });
        }
    });

    function updateElementIndex(el, prefix, ndx) {
        var id_regex = new RegExp('(' + prefix + '-\\d+)');
        var replacement = prefix + '-' + ndx;
        if ($(el).attr("for")) $(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
        if (el.id) el.id = el.id.replace(id_regex, replacement);
        if (el.name) el.name = el.name.replace(id_regex, replacement);
    };

    $('#add2').click(function() {
    });
    $('#del2').click(function() {
    })
</script>
{% endblock %}
テンプレートは、javascriptの部分がごちゃごちゃしていて汚いのですが、とりあえず以上の様にしました。サブフォームを展開するときに、id=formset{{forloop.counter}}としてあるので、追加・削除(実際には非表示化で更新時に削除)ボタンをクリックしたときに、目的のサブフォーム列を操作できるようにjavascript側で識別して動作できるようにできました。このあと画像の登録も追加する予定なのですが、コードの記述量が非常にすくなくて済みそうです。
今回は以上です。それでは。

コメント