#author("2025-02-13T00:48:02+09:00","","") #author("2025-02-13T00:49:45+09:00","","") [[Python/Django]] * InlineFormSet [#v23d3a1e] Django の 管理画面 (/admin/) では、通常 モデル毎にデータを設定可能であるが、 密接に関連するデータは、同じ画面で表示、編集できる方が好ましい。 次の図は、カスタムユーザーと、ユーザーのメールアドレスは、別モデルとして作成しているが、同じ画面で表示している。この画面の実現方法を記す。 &img("accounts.png", 50%); ** model [#a5dc2634] - カスタムユーザー (各フィールド適宜作成する) class CustomUser(AbstractBaseUser, PermissionMixin): _email = models.EmailField(verbose_name="Email") """ createsuperuser, createuser の設定用変数としてのみ使用 """ REQUIRED_FIEDS = ["username", "_email"] # 他、フィールド設定など &ref("custom_user.py"); - カスタムユーザーマネージャ # 通常通り記載 &ref("custom_user_manager.py"); - メールアドレス (カスタムユーザーへの ForeignKey を設定する) class EmailAddress(models.Model) user = models.ForeignKey( get_user_model(), on_delete=models.CASCADE, related_name="emails", verbose_name="User" ) """ ユーザー(カスタムユーザー) """ email = models.EmailField(unique=True, validators=[EmailValidator(), verbose_name="Email") # 他、フィールド設定など &ref("email_address.py"); ** admin.py [#v1b4a9db] from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.core.exceptions import ValidationError from django.forms.models import BaseInlineFormSet from django.utils.translation import gettext_lazy as _ from log_manager.trace_log import trace_log from .models import CustomUser, EmailAddress class EmailAddressInlineFormSet(BaseInlineFormSet): """メールアドレス用フォームセット。""" @trace_log def clean(self): """メールアドレスの primary 指定が複数ある場合、ValidationErro を投げます。""" super().clean() primary_count = sum(1 for form in self.forms if form.cleaned_data.get("is_primary", False)) if primary_count > 1: raise ValidationError(_("Only one primary email address can be set. ")) class EmailAddressInline(admin.TabularInline): """メールアドレスインライン用""" model = EmailAddress formset = EmailAddressInlineFormSet extra = 1 @admin.register(CustomUser) class CustomUserAdmin(UserAdmin): """カスタムユーザー管理。""" model = CustomUser """ 対象モデル。 """ inlines = [ EmailAddressInline, ] """ インライン表示。 """ # 他、適宜指定する。 # list_display = リスト表示のフィールドを指定 # search_fields = 検索用フィールドを指定 # ordering = ソート順 # fieldsets = 表示/編集のフィールドを指定 (ここに、emails 等は不要) # add_fieldsets = ユーザー新規追加時のフィールドセット # 例: ((None,{"classes": ("wide",),"fields": ("login_id", "username", "_email", "password1", "password2"),},),) # 以下のメソッドは、list_display 等に指定可能です。 # list_display = ("username", "get_primary_email", ) @trace_log def get_primary_email(self, obj): """プライマリのメールアドレスを返します。 Return: プライマリのメールアドレス。 """ primary_email = obj.get_primary_email() return primary_email if primary_email else _("None") get_primary_email.short_description = _("Email") """ プライマリメールアドレスの詳細。 """ # モデルを保存する場合のメールアドレス保存処理を追加している。 @trace_log def save_model(self, request, obj, form, change): """`email` が渡された場合、EmailAddress に保存します。""" super().save_model(request, obj, form, change) email = form.cleaned_data.get("_email") if email and not EmailAddress.objects.filter(user=obj, email=email).exists(): EmailAddress.objects.create(user=obj, email=email, is_primary=True) &ref(admin.py); ** InlineFormSet のテスト [#e4a46b8e] あまり情報がない気がする。(幾つかのAIで試したが、バグったテストコードしか出てこず。。。) 以下に例を記す。ポイントは、data の preifx 'emails-' 部分。公式ページ見ても明確には記載されてなさそう?(InlineFormSet ではない、form- の記載はあるが・・・) from django.form.models import inlineformset_factory def setup(self): self.user = CustomUser.objects.create_user(省略) self.EmailAddressIinlineFormSet = inlineformset_factory( CustomUser, # 親モデル EmailAddress, # モデル formset=EmailAddressInlineFormSet, fields=["user", "is_primary", "email"], extra=1, ) def test_clean(self): formset = self.EmailAddressInlineFormSet( instance=self.user, data={ "emails-TOTAL_FORMS": 2, "emails-INITIAL_FORMS": 0, "emails-MIN_NUM_FORMS": 0, "emails-MAX_NUM_FORMS": 1000, "emails-0-user": self.user.pk, "emails-0-is_primary": True, "emails-0-email": "testuser1@example.com", "emails-1-user": self.user.pk, "emails-1-is_primary": False, "emails-1-email": "testuser2@example.com", }, ) self.assertTrue(formset.is_valid()) # EmailAddressInlineFormSet の clean が call される。 &ref(tests_admin.py);