#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);


トップ   差分 バックアップ リロード   一覧 検索 最終更新   ヘルプ   最終更新のRSS