サイトアイコン 知的好奇心

DjangoのクラスベースVeiwで2つのフォームを使用する方法

DjangoのクラスベースVeiwで2つのフォームを使用する方法をご紹介します。

ここでは、1つのフォームで作品情報と画像情報の2つのテーブルへの登録を例にとります。

条件

実装

models.py

以下のようなモデルを考えます。
作品:画像 = 1:N になるものとします。

from django.db import models
from django.urls import reverse
from django.contrib.auth.models import AbstractUser


class Work(models.Model):
    """作品モデル"""
    class Meta:
        db_table = 'work'

    title = models.CharField(verbose_name='タイトル', max_length=255)
    memo = models.CharField(verbose_name='メモ', max_length=255, default='', blank=True)
    created_at = models.DateTimeField(verbose_name='登録日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

    def __str__(self):
        return self.title + self.memo

    @staticmethod
    def get_absolute_url(self):
        return reverse('works:index')


class Image(models.Model):
    """イメージモデル"""
    class Meta:
        db_table = 'image'

    work = models.ForeignKey(Work, verbose_name='作品', on_delete=models.CASCADE)
    image = models.ImageField(upload_to="image/", verbose_name='イメージ', null=True, blank=True)
    created_at = models.DateTimeField(verbose_name='登録日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

    def __str__(self):
        return self.work.title

urls.py

一覧画面、登録画面、詳細画面を用意します。
本記事で主に使用するのは「登録画面」です。

from django.urls import path
from . import views

app_name = 'works'

urlpatterns = [
    # トップ画面(一覧画面)
    path('', views.ListView.as_view(), name='index'),

    # 詳細画面
    path('works/<int:pk>/', views.DetailView.as_view(), name='detail'),

    # 登録画面
    path('create/', views.CreateView.as_view(), name='create'),
]

forms.py

登録画面用のフォームを定義します。
django-superformを用いたSuperModelFormを定義しています。

from .models import Work, Image
from django import forms
import os
from django_superform import ModelFormField, SuperModelForm

VALID_EXTENSIONS = ['.jpg']


class UploadFileForm(forms.ModelForm):
    class Meta:
        model = Image
        fields = ('image',)

        image = forms.ImageField(
            label='画像',
            required=False,  # 必須 or 必須ではない
        )

    def clean_image(self):
        image = self.cleaned_data['image']
        if image:  # 画像ファイルが指定されている場合
            extension = os.path.splitext(image.name)[1]  # 拡張子を取得
            if not extension.lower() in VALID_EXTENSIONS:
                raise forms.ValidationError('jpgファイルを選択してください!')
        return image


class WorkSetForm(SuperModelForm):
    # 複数のフォームを使用する
    upload = ModelFormField(UploadFileForm)

    class Meta:
        model = Work
        fields = ('title', 'memo',)

        title = forms.CharField(
            initial='',
            label='タイトル',
            required=True,  # 必須
            max_length=255,
        )
        memo = forms.CharField(
            initial='',
            label='メモ',
            required=False,  # 必須ではない
            max_length=255,
        )

views.py

from django.shortcuts import reverse
from django.views import generic
from .models import Work, Image
import logging
from photo import settings
import os
from django.contrib import messages
from django.http import HttpResponseRedirect
from .forms import WorkSetForm

logger = logging.getLogger('development')

NO_IMAGE = '/image/noimage.jpg'  # NO IMAGEパス


class ListView(generic.ListView):

    paginate_by = 5
    template_name = 'works/index.html'
    model = Work


class CreateView(generic.CreateView):
    # 登録画面
    model = Work
    form_class = WorkSetForm  # SuperModelFormをセットする。

    # def get_success_url(self):  # 詳細画面にリダイレクトする。
    #     return reverse('works:detail', kwargs={'pk': self.object.pk})

    def form_valid(self, form):

        # result = super().form_valid(form)
        # return result

        # DBへの保存
        work = Work()
        work.title = form.instance.title
        work.memo = form.instance.memo
        work.save()

        if len(self.request.FILES) != 0 and\
                self.request.FILES['form-upload-image'].name:  # 画像ファイルが添付されている場合
            logger.debug("With Image.")

            # サーバーのアップロード先ディレクトリを作成、画像を保存
            save_dir = "/image/" + '{0}/'.format(work.pk)
            upload_dir = settings.MEDIA_ROOT + save_dir
            os.makedirs(upload_dir, exist_ok=True)  # ディレクトリが存在しない場合作成する
            path = os.path.join(upload_dir, self.request.FILES['form-upload-image'].name)
            with open(path, 'wb+') as destination:
                for chunk in self.request.FILES['form-upload-image'].chunks():
                    destination.write(chunk)

            # DBへの保存
            image = Image()
            image.work_id = work.pk  # 作品ID
            image.image = settings.MEDIA_URL + save_dir + self.request.FILES['form-upload-image'].name  # アップロードしたイメージパス(サーバー側)
            image.save()
        else:
            logger.debug("No Image.")

        messages.success(self.request, '作品情報を登録しました。')
        return HttpResponseRedirect(reverse('works:detail', kwargs={'pk': work.pk}))  # 詳細画面にリダイレクト

    def form_invalid(self, form):
        result = super().form_invalid(form)
        return result


class DetailView(generic.DetailView):
    # 詳細画面
    model = Work
    template_name = 'works/detail.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        if Image.objects.filter(work_id=self.object.pk).exists():  # 画像が紐づく場合
            # 作品に紐づく画像パスを取得
            image = Image.objects.values_list('image', flat=True).get(work_id=self.object.pk)
        else:
            # No Imageパス
            image = settings.MEDIA_URL + NO_IMAGE
        context['image'] = image

        return context

ポイントは以下の通りです。

通常のformであれば、CreateViewは以下のような記述になるかと思います。
しかし、SuperModelFormを用いた場合、Imageテーブルへのレコード追加の際にwork_idがわからないためエラーになります。
そこで前述の通り、明示的にWorkおよびImageテーブルへのDB保存処理を記述する必要があります。

# NGケース

class CreateView(generic.CreateView):
    # 登録画面
    model = Work
    form_class = WorkSetForm  # SuperModelFormをセットする。

    def get_success_url(self):  # 詳細画面にリダイレクトする。
        return reverse('works:detail', kwargs={'pk': self.object.pk})

    def form_valid(self, form):

        result = super().form_valid(form)
        return result

template

work_form.html

登録画面用のテンプレートです。

{% extends 'base.html' %}
{% load i18n static %}

{% block content %}
    <div class="col-lg-6 offset-lg-3">

    <h1>Registration</h1>

    <form method="post" enctype="multipart/form-data">

        {% csrf_token %}

        {% if form.errors %}
            <p class="error-msg">
            {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
            </p>
        {% endif %}

        {{ form.title.errors }}
        <div class="form-group form-inline">
            <label class="col-offset-1 col-3 control-label">
                {{ form.title.label_tag }}
            </label>
            <div class="">
                {{ form.title }}
            </div>
        </div>

        {{ form.memo.errors }}
        <div class="form-group form-inline">
            <label class="col-offset-1 col-3 control-label">
                {{ form.memo.label_tag }}
            </label>
            <div class="">
                {{ form.memo }}
            </div>
        </div>

        {{ form.upload.image.errors }}
        <div class="form-group form-inline">
            <label class="col-offset-1 col-3 control-label">
                {{ form.upload.image.label_tag }}
            </label>
            <div class="">
                {{ form.upload.image }}
            </div>
        </div>

        <div class="btn-toolbar justify-content-center" role="toolbar" aria-label="ボタングループのツールバー">
            <div class="btn-group mr-2">
                <a class="btn btn-primary w-150px" href="{% url 'works:index' %}" role="button">一覧画面へ</a>
            </div>
            <div class="btn-group mr-2">
                <input class="btn btn-success w-150px" type="submit" id="button" name="button" value="登録実行">
            </div>
        </div>

    </form>

    </div>
{% endblock %}

detail.html

詳細画面用のテンプレートです。

{% extends 'base.html' %}

{% block content %}
<div class="col-lg-6 offset-lg-3">

    <h1>Detail</h1>

    {% if messages %}
    <ul class="messages">
        {% for message in messages %}
            <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
                <h2 class="info-msg">{{ message }}</h2>
            </li>
        {% endfor %}
    </ul>
    {% endif %}

    <table class="table table-hover table-bordered">
        <tbody>
            <tr>
                <th width="40%">タイトル</th>
                <td width="60%">{{ work.title }}</td>
            </tr>
            <tr>
                <th width="40%">メモ</th>
                <td width="60%">{{ work.memo }}</td>
            </tr>
            <tr>
                <th width="40%">画像</th>
                <td width="60%">
                <img src="{{ image }}" alt={{ work.title }}  width="300" height="200" border="0" /></td>
            </tr>
        </tbody>
    </table>


    <div class="btn-toolbar justify-content-center" role="toolbar" aria-label="ボタングループのツールバー">
        <div class="btn-group mr-2">
            <a class="btn btn-primary w-150px" href="{% url 'works:index' %}" role="button">一覧画面へ</a>
        </div>
    </div>

</div>
{% endblock %}

実行結果

タイトル、メモと共に、画像も登録します。

DBのそれぞれのテーブルにレコードが追加されていることが分かります。

画像もアップロードされています。

サンプルソース

GitHubに当該記事のサンプルソースを公開しています。

https://github.com/kzmrt/photo

参考

django-superform

https://github.com/gregmuellegger/django-superform

モバイルバージョンを終了