DjangoのクラスベースVeiwで2つのフォームを使用する方法をご紹介します。
ここでは、1つのフォームで作品情報と画像情報の2つのテーブルへの登録を例にとります。
目次
条件
- Django 2.1.7
- Python 3.7.0
- django-superform 0.3.1
実装
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
ポイントは以下の通りです。
- CreateViewの「form_class = 」にforms.pyで定義したSuperModelForm渡す。
- CreateViewの「form_valid(self, form):」で、明示的にDBへの保存処理と、画面リダイレクト処理を記述する。
通常の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