【Django】管理画面に自由記入欄（メモ）をインライン追加する

# はじめに
この記事は、自由記入欄をDjango管理画面に表示させるためのものです。
具体的には、子モデルのテキストフィールドを、親モデルのDjango管理画面に表示させ、編集・追加できるようにします。ここでは、顧客管理における対応履歴や社内メモなどを想定して、親：Customerモデル、子：Memoモデルとしています。

Memoモデルを作らず、Customerモデルにmemo**カラム**を追加するというやり方もありますが、情報集約系のモデルは肥大化しやすく、他方、メモは使うタイミングが限定的（ = ほとんどの場面で不要）なことから分離させる方法をとっています。
前者の方法であればModelAdminクラスだけで可能ですが、モデルを分ける後者ならさらにStackedInlineクラスでの実装が必要となります。

### この記事でできること
- 新しいモデルの作成
- 子モデルのテキストフィールドを、親モデルのDjango管理画面に表示させ、編集・追加できるようにする

### 目次
1. Memoモデルの実装
2. マイグレーションでDBに反映
3. Django管理画面に表示
4. 参考

---

今回の自由記入欄追加に関係する主なディレクトリ・ファイル構成は以下の通りです。

```
apps/
├── admin.py             # 管理画面で表示・編集するモデルや管理クラスをまとめて登録するファイル
├── admins/
│   ├── __init__.py      # 各管理クラスのimport集約（CustomerAdminのimport）
│   └── customer.py      # Customer管理画面（CustomerAdmin）・MemoInlineの定義
├── models/
│   ├── __init__.py      # 各モデルのimport集約（Customer, Memoもここでimport）
│   ├── customer.py      # Customerモデル本体
│   └── memo.py          # Memoモデル本体
```

---

# 1. Memoモデルの実装

### 1. `models/memo.py`の作成
Customerモデルと**1対1**で紐づくメモ用モデルを作成します。

```python
# models/memo.py
from django.db import models

class Memo(models.Model):
    customer = models.OneToOneField('Customer', null=False, blank=False, related_name='memo', verbose_name='顧客', on_delete=models.CASCADE)
    memo = models.TextField('メモ')
    created_at = models.DateTimeField('作成日時', auto_now_add=True)
    updated_at = models.DateTimeField('更新日時', auto_now=True)

    class Meta:
        verbose_name = verbose_name_plural = 'メモ'

    def __str__(self):
        return f"Memo for {self.customer.name}"
```

- **1顧客につき1メモ**のみ許容する場合は`OneToOneField`を使います。
- 複数メモを許容したい場合は`ForeignKey`に変更してください。

### 2.` models/__init__.py`でのimport
作成したMemoモデルをDjango全体で参照できるよう、`models/__init__.py`に以下を追記してください。

```python
from apps.models.memo import Memo
```

---

# 2. マイグレーションでDBに反映

モデルを作成したら、マイグレーションファイルを作成し、DBに反映します。

```sh
python manage.py makemigrations
python manage.py migrate
```

---

# 3. Django管理画面に表示

### 1. `admins/customer.py`でのインラインクラス実装
Customer管理画面でMemoをインライン追加・編集できるようにします。

```python
# admins/customer.py
from django.contrib import admin
from django.forms import Textarea
from apps.models import Customer, Memo

# Memoモデルの表示設定
class MemoInline(admin.StackedInline):
    model = Memo
    extra = 0  # 既存のメモのみ表示（空の追加フォームは出さない）
    can_delete = False  # 管理画面での削除不可
    fields = ('memo',)

    # テキストエリアのカスタマイズ
    def formfield_for_dbfield(self, db_field, request, **kwargs):
        if db_field.name == 'memo':
            kwargs['widget'] = Textarea(attrs={'rows': 10, 'cols': 90})
        return super().formfield_for_dbfield(db_field, request, **kwargs)


# Customerモデルの表示設定
class CustomerAdmin(admin.ModelAdmin):
    inlines = [MemoInline]  # Customer管理画面にメモを追加
    # ...他の設定...
```
- `MemoInline`を`CustomerAdmin`に、`inlines`として追加してください。
- テキストエリアのサイズ等をカスタムしたい場合は`formfield_for_dbfield`関数で設定してください。必須ではないです。
- `can_delete = False` で管理画面からの削除を禁止しています。
- `extra = 0` とすることで「空の追加フォームを表示しない」挙動になります（デフォルトは1で、常に空のフォームが1つ表示されます）。
	<details>
    <summary>extraの値による違いはこちら</summary>

    - `extra = 0`：空のフォームは表示されない
      - `extra = 0`で既存データがないとき → 何も表示されない
          <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894261/e8aa6c6f-4899-4f70-8fea-7f9a4d7aeaad.png" width=90%>

      - `extra = 0`で既存データがあるとき → 既存データのみ表示
          <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894261/52cb297c-5ae5-4329-baeb-a6baf8f3cce6.png" width=90%>

    - `extra = 1`：空のフォームが1つ表示される
      - `extra = 1`で既存データがないとき → 空のフォームが1つ表示される
          <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894261/ba8f5c09-8940-4c78-bb03-1b938bc9c0e8.png" width=90%>

      - `extra = 1`で既存データがあるとき → 既存データ＋空のフォームが1つ表示される（1対多の場合）
          <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894261/518148bc-b201-4990-888c-369ea9b6e9ad.png" width=90%>
            ↑上の画像はモデルの設定が`ForeignKey`の場合です。
            今回のように1対1（`OneToOneField`）の場合はメモが1つしか許容されないので、既存データがあれば空フォームは表示されません。

  </details>

### 2. `admin.py`と`admins/__init__.py`の確認

- `admins/__init__.py`でCustomerAdminが正しくimportされているか確認してください。

Django管理画面のカスタム管理クラス（ここでは`CustomerAdmin`）を他の場所からimportしやすくするため、`apps/admins/__init__.py`で以下のように`CustomerAdmin`を記載します。

```python
from apps.admins.customer import (
    CustomerAdmin
)
```
<br>

- Customerモデルの管理画面用クラス（CustomerAdmin）が`admin.py`で正しく登録されているか確認してください。
ここで登録されていないとDjango管理画面でCustomerAdminが表示されないので注意してください。

```python
# admin.py
from django.contrib import admin
from apps import models
from apps.admins import CustomerAdmin

admin.site.register(Customer, CustomerAdmin)
```

これでCustomer管理画面でMemoの追加・編集が可能になります。
お疲れさまでした！

---

# 参考
### Django管理画面の基本概念
**ModelAdmin**
- 各モデルの管理画面の表示・編集方法を定義するクラス。
- `admin.site.register(Model, ModelAdmin)` で管理画面に登録。

**InlineModelAdmin（インライン）**
- 親モデルの編集画面内で、関連する子モデル（ForeignKey/OneToOne/ManyToMany）の編集フォームを表示できる仕組み。
- 例: Customerの編集画面でMemoをインライン表示。
- `TabularInline`（表形式）と`StackedInline`（縦積み形式）がある。

**admin.site.register**
- モデルと管理クラスを管理画面に登録する関数。
- 登録しないと管理画面に表示されない。

### 参照URL

- [Django公式: OneToOneField（1対1リレーション）](https://docs.djangoproject.com/ja/4.2/topics/db/models/#one-to-one-relationships)
- [Django公式: InlineModelAdmin（インライン編集）](https://docs.djangoproject.com/ja/4.2/ref/contrib/admin/#inlinemodeladmin-objects)