T.H/ 2023年 8月 31日/ 技術

はじめに

こんにちは。T.H.です。
今回は、Djangoの管理画面のカスタマイズのうち、比較的簡単に出来る範囲について説明します。
Model,ModelForm,ModelAdminを使用して管理画面を表示できている前提で進めます。

Djangoの管理画面のカスタマイズ段階

Djangoの管理画面のカスタマイズは大きくわけて2パターンです。

  1. Model,ModelForm,ModelAdminにカスタマイズ内容を記載
  2. template,css,javascriptの差し替え、機能拡張

カスタマイズを重ねていけば最終的には割となんでも出来る、ということにもなりますが、過度なカスタマイズはかえって困難な状況になりかねませんので、無理なカスタマイズはお勧めしません。

今回は初級編として、「Model,ModelForm,ModelAdminにカスタマイズ内容を記載」するケースについて記述します。私が遭遇し、それなりに使用頻度が高そうなものについてあげています。
また、ModelのFieldに設定するだけの内容(verbose_nameなど)はカットします。
初級編ではありますが、入門編ではない、ぐらいでしょうか。

一覧画面

  • 表示項目、表示内容をカスタマイズ
  • 絞り込み機能を追加

表示項目をカスタマイズ

list_displayに記載した項目が一覧に表示されます。そこからもう一歩踏み込んで、Modelに存在しない項目を追加することもできます。

下記のように記載することで、Modelに存在しないfile_nameという項目を一覧に表示することができます。なお、項目の内容はhtml形式になりますので、imgタグで画像を設定したりなども可能です。

class PageBaseFile(models.Model):
    file = models.FileField(
        upload_to=html_path,
        null=True,
        blank=True,
    )
    description = models.CharField(default="", editable=True, blank=True, max_length=128)

@admin.register(FileModel)
class FileModelAdmin(admin.ModelAdmin):
    form = FileModelForm
    list_display = ("file_name", "description")
    # FileModel.file.urlからファイル名を抽出
    def file_name(self, obj):
        return format_html(os.path.basename(obj.file.url))

    file_name.short_description = "ファイル名"

絞り込み機能を追加

検索窓

一覧に検索窓を追加するにはModelAdminにsearch_fieldsに検索対象のmodelのfieldを指定します。

上記で追加した表示項目は検索対象に出来ませんのでご注意ください。恐らく内部的に直接モデルを検索しているものと思われます。

@admin.register(FileModel)
class FileModelAdmin(admin.ModelAdmin):
    form = FileModelForm
    list_display = ("file_name", "description")
    search_fields = [
        "description",#OK
    ]
    # FileModel.file.urlからファイル名を抽出
    def file_name(self, obj):
        return format_html(os.path.basename(obj.file.url))

    file_name.short_description = "ファイル名"
@admin.register(FileModel)
class FileModelAdmin(admin.ModelAdmin):
    form = FileModelForm
    list_display = ("file_name", "description")
    search_fields = [
        "file_name",#NG
    ]
    # FileModel.file.urlからファイル名を抽出
    def file_name(self, obj):
        return format_html(os.path.basename(obj.file.url))

    file_name.short_description = "ファイル名"

検索窓の表示が分かりにくいこともあり、お手軽ですが少々使いにくいです。使用する際には検索対象が分かりやすくなるよう、一覧項目をシンプルにする必要があると思います。
使いやすくするためさらにカスタマイズすることも可能ですが、さらにひと手間かかりますのでここでは割愛します。

フィルタ機能追加

検索窓と同様にModelAdminに

list_filter = ['model_field']

を追加することで一覧にフィルタ機能を実装できます。こちらもデフォルトである程度気を効かせてくれてはいるのですが、少々使いにくいです。

model_fieldの部分にadmin.SimpleListFilter等を継承したクラスを入れることでカスタマイズできますので、そちらを試してみるとよいと思います。

編集画面

  • 入力項目のread only化
  • 表示をまとめる、折りたたむ
  • Many to Many
    • 検索widjetの利用
    • 複数のモデルを紐づける
    • throughを使用した際の注意点
  • リレーション先を同じ編集画面で編集
  • 保存時の処理をカスタマイズする

入力項目のread only化

下記のようにforms.ModelFormのinitで定義することができます。disabledとreadonlyで微妙に挙動が異なりますのでお好きな方を。

class SampleAdminForm(forms.ModelForm):
    class Meta:
        fields = ["model_field01", "model_field02",]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["model_field01"].widget.attrs["disabled"] = "disabled"
        # または
        self.fields["model_field02"].widget.attrs["readonly"] = "readonly"

表示をまとめる、折りたたむ

ModelAdminにfieldsetsを追加することで、入力項目のグループ化、表示順の制御、折り畳みが可能です。

  • "fields"に指定した項目を指定順にそのグループに表示します。
  • "classes": ("collapse",)が指定されたグループは初期状態で折りたたまれて表示されます。
  • "fields"に指定されなかった項目は入力項目として表示されません。(ModelFormのfieldsより優先されるようです)
@admin.register(SampleModel)
class SampleModelAdmin(admin.ModelAdmin):
    form = SampleModelForm
    fieldsets = (
        ("基本項目",{
            "fields": ("model_field01", "model_field02",)},),
        ("オプション項目", {
            "classes": ("collapse",), 
            "fields": ("model_field03", "model_field04")}),
    )

"classes"には"collapse"以外のオプションもありますので、必要に応じて設定するとよいかと思います。

Many to Many

検索widjetの利用

Many to Manyの項目は通常ModelMultipleChoiceFieldで描画されます。デフォルトでは非常に使いにくいため、
ModelAdminにfilter_horizontalを追加し、対象の項目を追加しましょう。

filter_horizontal = ("model_field01",)

これで検索窓が追加され、また、単純な操作で複数選択ができるようになります。

複数のモデルを紐づける

見た目のカスタマイズではありませんが、いくつかの種類をModelをMany to Manyで紐づけたい場合は、

  • 紐づけたいモデルのベースとなるモデルクラスを作成
  • 紐づけたいモデルはベースクラスから継承
  • Many to Manyはベースクラスに対して行う
    とすると、実現できます。

throughを使用した際の注意点

Many to Manyでthroughを指定すると自作のモデルクラスを中間クラスとして定義できます。
ただし、こうするとfilter_horizontalが効かなくなるため、そのままでは管理画面の使い勝手が悪くなってしまいます。回避策として、ModelFormから直接Widjetを指定することで、filter_horizontal相当の機能を実現できます。

class SampleAdminForm(forms.ModelForm):
    class Meta:
        model = SampleModel
        fields = "__all__"

    items = forms.ModelMultipleChoiceField(
        label="サンプル",
        queryset=SampleBaseModel.objects.all(),# リレーション先のモデルの取得クエリ
        required=False,
        widget=FilteredSelectMultiple(
            verbose_name="サンプル",
            is_stacked=False,
        ),
    )

リレーション先を同じ編集画面で編集

StackedInline,TabularInlineを使用すると、1対多でリレーションしているモデルを同じ編集画面で編集することができます。

class SampleModel(models.Model):
# 略

class SampleChildModel(models.Model):
    sample_model = models.ForeignKey(
        SampleModel,
        on_delete=models.SET_NULL,
        related_name="sample_child_sample_model",
        null=True,
        blank=True,
    )

class SampleModelAdminForm(forms.ModelForm):
# 略

class SampleChildInline(admin.StackedInline):
    model = SampleChildModel
    extra = 1 # 初期の表示数

@admin.register(SampleModel)
class SampleModelAdmin(admin.ModelAdmin):
    # カスタムフォーム
    form = SampleModelAdminForm
    inlines = [
        SampleChildInline,
    ]

とすることで、SampleModelの編集画面で同時にSampleChildModelを編集することが可能になります。
StackedInlineは縦、TabularInlineは横に項目が並びます。

保存時の処理をカスタマイズする

ModelAdminのsave_modelをオーバーライドすることで、保存時の動作をカスタマイズすることが出来ます。Modelのインスタンスの他、requestやformのインスタンスも受け取っているため柔軟にカスタマイズできます。

@admin.register(SampleModel)
class SampleModelAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        """
        request:HttpRequest, 
        obj:編集対象のModelインスタンス
        form:ModelFormインスタンス
        change: 変更/True、追加/False
        """
        # 固定でプレフィックスを付ける
        obj.huga = f'prefix_{obj.huga}'
        super().save_model(request, obj, form, change)

また、Many to Manyの中間テーブルに変更を加えたい場合などは、

def save_related(self, request, form, formsets, change):

を利用出来ます。

他にもオーバーライドできる有用なメソッドがいくつかありますので、公式リファレンスを参考にされると良いかと思います。

テンプレート差し替えなどが必要なこと(今回の範囲外)

  • 編集画面での項目追加、機能追加
  • css,jsのカスタマイズ
  • 外部のjsライブラリの導入

これらは見た目や使い勝手の改善において重要ですが、管理画面への導入は今まで上げたものより1段難しくなります。今回は初級編ということで範囲外とさせていただきました。そのうち記載できればと思います。

カスタマイズをあきらめるケース

管理画面は基本的に定義したModelと1対1になるような設計がされています。シンプルで分かりやすいのですが、一方、モデル設計と現実の見た目に乖離がある場合、管理者がきちんとデータモデルを把握する必要が出てきます。
管理者が開発者以外だとなかなか厳しい条件となりますし、管理画面側で隠蔽しラッピングすることは困難です。
そういったケースは管理用の画面を起こしたほうが有用なケースが多いかと思います。

最後に

Djangoを使用する際は必ずと言っていいほど使用する管理画面ですが、意外なほどカスタマイズの情報がまとまっておらず、断片的な情報を集めることが多かったです。網羅的、とは言えませんが使用頻度の高そうなものをある程度まとめて記載できたので、何かのお役に立てればうれしいです。

参考

https://docs.djangoproject.com/ja/4.2/intro/tutorial07/
https://docs.djangoproject.com/en/4.2/ref/contrib/admin/
https://logmi.jp/tech/articles/326156

About T.H

North Torch株式会社 プログラマ 技術的な経歴は.NETアプリケーションが一番長い。 その他はまだまだ勉強中。