はじめに
DjangoでOneToOneで関連付けた複数のモデルを1つのフォームに表示して、同時に保存、更新する必要がありました。
django-extra-viewsを使ってもできるようですが、今回はモデルは2個だけなので何とかできないかと調べました。
クラスを使って作成しています。
ユーザ認証を使わない場合は、各クラスに記載しているLoginRequiredMixinを削除してください。
環境
Debian bullseye 64bit Python 3.10.2 Djnago 4.0.3 Bootstrap 5.0.2
project ┣ -- fee ┣ -- admin.py ┣ -- forms.py ┣ -- models.py ┣ -- urls.py ┣ -- views.py ┣ -- template ┣ -- base.html ┣ -- fee ┣ -- admfee_create.html
CreateView
CreateViewの部分は下記をそのまま使わせていただきました。
UpdateView
UpdateViewはCreateViewを元にして作成しました。
CreateViewの部分と同様に基本となるモデルは通常通り取得できますが、OneToOneなどのモデルは工夫する必要があります。
models.py
feeというアプリケーションを作成して、AdmFeeとUserFeeの2個のモデルを定義します。
UserFeeからAdmFeeをOneToOneで参照します。
from django.db import models import uuid class AdmFee(models.Model): id = models.UUIDField('UUID', default=uuid.uuid4, primary_key=True, editable=False) created_time = models.DateTimeField('登録日時', auto_now_add=True) updated_time = models.DateTimeField(verbose_name='更新日時', auto_now=True) year = models.IntegerField(verbose_name='年', choices=list(zip(range(2022, 2040), range(2022, 2040)))) # 管理画面の表示名を変更する。 class Meta: verbose_name = "負担金額" verbose_name_plural = "負担金額" class UserFee(models.Model): id = models.UUIDField('UUID', default=uuid.uuid4, primary_key=True, editable=False) created_time = models.DateTimeField('登録日時', auto_now_add=True) updated_time = models.DateTimeField(verbose_name='更新日時', auto_now=True) # CASCADE:元データも削除 admfee = models.OneToOneField(AdmFee, verbose_name='負担金額', on_delete=models.CASCADE, related_name='admfee') exp_total = models.IntegerField(verbose_name='合計', default=0, blank=True) # 管理画面の表示名を変更する。 class Meta: verbose_name = "ユーザー支出先" verbose_name_plural = "ユーザー支出先"
views.py
2個のモデルを同時に作成、アップデートするクラスを記述します。
from django.shortcuts import render from django.views.generic import CreateView, UpdateView from django.views.generic.edit import ModelFormMixin from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction from django.urls import reverse class AdmFeeCreateView(LoginRequiredMixin, CreateView, ModelFormMixin): model = AdmFee fields = () template_name = 'fee/admfee_create.html' # 更新ページ表示用の変数をtemplateに渡す。 def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) # contextは辞書型 ctx.update({ 'admfee_form': AdmFeeForm(**self.get_form_kwargs()), 'userfee_form': UserFeeForm(**self.get_form_kwargs()), }) return ctx def post(self, request, *args, **kwargs): # POSTデータをコピー admfee_form_set = request.POST.copy() userfee_form_set = request.POST.copy() # 'form-TOTAL_FORMS'を上書き admfee_form_set['form-TOTAL_FORMS'] = 3 userfee_form_set['form-TOTAL_FORMS'] = 5 # 新規オブジェクト生成 admfee_form = AdmFeeForm(admfee_form_set) userfee_form = UserFeeForm(userfee_form_set) # validのチェック if admfee_form.is_valid() and userfee_form.is_valid(): admfee_query = admfee_form .save(commit=False) userfee_query = userfee_form.save(commit=False) # トランザクションの設定 with transaction.atomic(): admfee_query.save() # OneToOneのインスタンスを代入 userfee_query.admfee = admfee_query userfee_query.save() # リダイレクト先を記載 # urls.pyのname変数で定義した名前を指定する。 return HttpResponseRedirect(reverse('admfee_create')) class AdmFeeUpdateView(LoginRequiredMixin, UpdateView): model = AdmFee form_class = AdmFeeForm success_url = reverse_lazy('admfee_update') template_name = 'fee/admfee_create.html' # 更新ページ表示用の変数をtemplateに渡す。 def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) admfee_form = AdmFeeForm(**self.get_form_kwargs()) # AdmFeeのpkから対応するUserFeeを探す。 userfee = UserFee.objects.get(admfee=self.kwargs['pk']) # UserFeeのデータをinstanceで渡す。 # https://jpdebug.com/p/2145748 userfee_form = UserFeeForm(instance=userfee) ctx.update({ 'admfee_form': admfee_form, 'userfee_form': userfee_form, }) return ctx # 2個のform(AdmFee、UserFee)を同時に更新する。 def form_valid(self, form): # form変数はmodelで指定したフォーム。 admfee_form = form # 2個目のフォームの内容を取得してinstanceを指定する。 userfee = UserFee.objects.get(admfee=self.kwargs['pk']) userfee_form = UserFeeForm(self.request.POST or None, instance=userfee) # validのチェック # form_valid関数に記述しているのでadmfee_form.is_valid()は不要かもしれない。 if admfee_form.is_valid() and userfee_form.is_valid(): admfee_query = admfee_form.save(commit=False) userfee_query = userfee_form.save(commit=False) # トランザクションの設定 with transaction.atomic(): admfee_query.save() userfee_query.save() # urls.pyのname変数で定義した名前を指定する。 return HttpResponseRedirect(reverse('admfee_create')) # バリデーションに失敗 def form_invalid(self, form): messages.warning(self.request, 'Error:Could not save to database.') return super().form_invalid(form)
forms.py
2個のモデルについて、それぞれフォームを作成します。
from django import forms from .models import UserFee, AdmFee class AdmFeeForm(forms.ModelForm): class Meta: model = AdmFee fields = '__all__' # フォームで非表示にする項目は「exclude」で指定する。 # admin画面では表示される。 exclude = ['',] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Bootstrap # class名の先頭に「form-control」を付加する。 for field in self.fields.values(): field.widget.attrs['class'] = 'form-control' class UserFeeForm(forms.ModelForm): class Meta: model = UserFee fields = '__all__' # フォームで非表示にする項目は「exclude」で指定する。 # admin画面では表示される。 exclude = ['admfee',] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Bootstrap # class名の先頭に「form-control」を付加する。 for field in self.fields.values(): field.widget.attrs['class'] = 'form-control'
urls.py
AdmFeeCreateViewとAdmFeeUpdateViewを設定します。
from django.urls import path from . import views urlpatterns = [ path('admfee_create', views.AdmFeeCreateView.as_view(), name='admfee_create'), path('update_admfee/<uuid:pk>/', views.AdmFeeUpdateView.as_view(), name='admfee_update'), ]
admin.py
管理画面で関連付けた2個のモデルを同時に表示するために設定します。
こうすることで、OneToOneで関連付けた2つのモデルを管理画面で同時に編集することが可能です。
Djangoの管理画面では今回と同様のことができるので、時間があればソースを追っかけてみようと思います。
from django.contrib import admin # UserFeeを同じ画面に表示する設定 class UserFeeInline(admin.StackedInline): model = UserFee max_num = 1 can_delete = False verbose_name_plural = 'ユーザー負担金' class AdmFeeAdmin(admin.ModelAdmin): # UserFeeを同じ画面に表示する設定 inlines = [UserFeeInline,] # 保存ボタンの配置を上部に変更 save_on_top = True # デフォルトの「保存して1つ追加」を「新規保存」に変更。 # 追加されているデータを元に新しくデータを保存する。 save_as = True list_display = 'year', 'created_time', 'updated_time') search_fields = ('id', 'year') admin.site.register(AdmFee, AdmFeeAdmin)
admfee_create.html
データの作成とアップデートに使います。
views.pyで設定したadmfee_formとuserfee_formを使って変数を参照します。
また、リダイレクト先はurls.pyで設定したadmfee_createとadmfee_updateを使います。
admfee_formとuserfee_formはfor文で回しても良いです。
{% extends 'base.html' %} {% block title %} データ作成・更新 {% endblock title %} {% block h1_title %} データ作成・更新 {% endblock h1_title %} <!-- 本体 --> {% block body %} <form method="post"> {% csrf_token %} <p class="text-center"> <input type="submit" class="btn btn-primary" value="登録する"> <input type="button" class="btn btn-danger" value="戻る " onClick="javascript:history.go(-1);"> </p> <div class="bg-light bg-gradient"> <p class="fs-3 fw-bold"> 基本データ </p> <table class="table table-border align-middle text-center"> <tr> <th class="table-warning">{{ admfee_form.year.label }}</th> <td>{{ admfee_form.year }}</td> </tr> </table> </div> <div class="bg-light bg-gradient"> <p class="fs-3 fw-bold"> 支出先データ </p> <table class="table table-bordered table-striped align-middle text-center"> <tr> <th class="table-danger"></th> <td>{{ userfee_form.exp_total }}</td> </tr> </table> </form> </div> {% endblock body %}
base.html
他のテンプレートの元となるファイルです。
admfee_create.htmlの先頭で読み込みます。
{% load i18n static %} <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <link rel="shortcut icon" type="image/png" href="{% static 'favicon/favicon.ico' %}"> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> <!-- Google Web font start --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap" rel="stylesheet"> <!-- Google Web font end --> <script src="{% static 'js/jquery-3.6.0.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script> <title> {% block title %} {% endblock %} </title> </head> <body> <!-- container: 表示幅を可変にする。--> <div class="container-fluid"> <!-- formのエラー表示 --> {% if form.errors %} {% for field, errors in form.errors.items %} {% for error in errors %} <div class="alert alert-danger" role="alert"> <p class="mb-0">{{ field }} {{ error }}</p> </div> {% endfor %} {% endfor %} {% endif %} <!-- 「form.errors」以外のエラー表示(ex:カスタムバリデーションなど) --> {% if non_field_errors %} <div class="alert alert-danger" role="alert"> {% for error in form.non_field_errors %} <p{% if forloop.last %} class="mb-0"{% endif %}>{{ error }}</p> {% endfor %} </div> {% endif %} <!-- SuccessMessageMixinのメッセージ表示 --> {% if messages %} <div class="text-center messages alert alert-primary" role="alert"> {% for message in messages %} {{ message }} {% endfor %} </div> {% endif %} <h1>hoge</h1> {% block allauth %} {% endblock %} {% block charge_404 %} {% endblock %} {% block body %} {% endblock %} <!-- フッタ開始 --> <footer class="container-fluid"> <small> ©hoge </small> </footer> <!-- フッタ終了 --> <!-- end container --> </div> </body> </html>
Comments