【Django】adminサイトの追加・編集ページのレイアウトをカスタマイズする


投稿日 2019年11月10日 >> 更新日 2023年3月2日

今回はadminサイトの追加・編集ページのレイアウトをカスタマイズしていこうと思います。

下図のようなチェンジリストである中間ページのカスタマイズは別記事の方で説明しているので、良ければそちらもご覧ください。

adminサイトのカラーチェンジについてはこちらの記事で説明しております。

ここでは簡単な説明にとどめているため、詳しく知りたいという方はDjango公式ドキュメントをご参照ください。

なお各ページの名称ですが、「追加ページをアッドページ」と呼ぶ事や、「編集ページをチェンジフォーム」と呼ばれることがあるので、ここでは各ページを「追加・編集ページ」と呼ぶことにします。

adminサイトに表示されるモデルフィールドの挙動を分かりやすくしたいので、簡単な発注アプリを作成してからadminサイトの追加・編集ページのレイアウトをカスタマイズしていきたいと思います。

今回は「views.py」や「urls.py」には手を付けないので、ご了承ください。

実行環境&使用ライブラリ

実行環境
Windows Subsystem for Linux
Python 3.6.8
pip 9.0.1
使用ライブラリ ライセンス
Django==2.2.6 BSD

簡単な発注アプリを作成

それでは発注アプリを作成していきますが、今回主に編集するファイルはアプリディレクトリ内の「models.py」と「admin.py」です。

なのでまずプロジェクトを立ち上げて「order」アプリとしてスタートします。


$ django-admin startproject project

$ cd project

/project$ python3 manage.py startapp order

プロジェクトディレクトリ内の「settings.py」を編集します。

# project/project/settings.py

INSTALLED_APPS = [
    'order.apps.OrderConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

次に、アプリディレクトリ内の「models.py」を編集します。

7つのモデルがあり、上から「会社情報」「発注者」「仕入先」「項目類」「申込書」「項目詳細」「合計金額」となっていて、その内上から5つがカテゴリー化されています。

# project/order/models.py

from django.db import models


""" 会社情報 """
class Company(models.Model):
    company_name = models.CharField('会社名', max_length=50)

    def __str__(self):
        return self.company_name

    class Meta:
        verbose_name = '会社情報'
        verbose_name_plural = '1, 会社情報'


""" 発注者 """
class Orderer(models.Model):
    first_name = models.CharField('姓', max_length=10)
    last_name = models.CharField('名', max_length=10)

    def __str__(self):
        return '{0} {1}'.format(self.first_name, self.last_name)

    class Meta:
        verbose_name = '発注者'
        verbose_name_plural = '2, 発注者'


""" 仕入先 """
class Supplier(models.Model):
    supplier = models.CharField('仕入先', max_length=50)

    def __str__(self):
        return self.supplier

    class Meta:
        verbose_name = '仕入先'
        verbose_name_plural = '3, 仕入先'


""" 項目類 """
class Item(models.Model):
    item = models.CharField('項目', max_length=100)

    def __str__(self):
        return self.item

    class Meta:
        verbose_name = '項目類'
        verbose_name_plural = '4, 項目類'


""" 申込書 """
class Application(models.Model):
    supplier = models.ForeignKey(Supplier, verbose_name='仕入先', on_delete=models.PROTECT)
    company_name = models.ForeignKey(Company, verbose_name='会社名', on_delete=models.PROTECT)
    orderer_name = models.ForeignKey(Orderer, verbose_name='発注者', on_delete=models.PROTECT)
    created_at = models.DateField('作成日', null=True, blank=True)
    delivery_date = models.DateField('納期', null=True, blank=True)

    class Meta:
        verbose_name = '申込書'
        verbose_name_plural = '5, 申込書'


""" 項目詳細 """
class ItemDetail(models.Model):
    application = models.ForeignKey(Application, verbose_name='申込書', on_delete=models.PROTECT)
    item = models.ForeignKey(Item, verbose_name='項目', on_delete=models.PROTECT)
    quantity = models.IntegerField('数量', default=1)
    unit_price = models.CharField('単価', max_length=20)
    amount_money = models.CharField('金額', max_length=20)

    class Meta:
        verbose_name = '項目詳細'
        verbose_name_plural = '項目詳細'


""" 合計金額 """
class TotalPrice(models.Model):
    application = models.ForeignKey(Application, verbose_name='申込書', on_delete=models.PROTECT)
    total = models.CharField('合計金額', max_length=20)

    class Meta:
        verbose_name = '合計金額'
        verbose_name_plural = '合計金額'

次に「admin.py」ファイルを編集してadminサイトにデフォルト表示させます。

# project/order/admin.py

from django.contrib import admin
from .models import (Company, Orderer, Supplier,
                    Item, Application, ItemDetail, TotalPrice)


admin.site.register(Company)
admin.site.register(Orderer)
admin.site.register(Supplier)
admin.site.register(Item)
admin.site.register(Application)
admin.site.register(ItemDetail)
admin.site.register(TotalPrice)

編集が終わったらターミナルにてモデルをデータベースに反映させ、スーパーユーザーを作成したらadminサイトにアクセスします。


$ python3 manage.py makemigrations

$ python3 manage.py migrate

$ python3 manage.py createsuperuser

$ python3 manage.py runserver
127.0.0.1:8000

ブラウザにて「127.0.0.1:8000/admin」にアクセスすると

InlineModelAdminを使ってモデル同士を結合

現在の設定では7つのモデルが表示されていて、上から順番にデータを入れていけば発注書が出来上がるという流れになっています。

ただし「申込書」「項目詳細」「合計金額」は他のモデルと「ForeignKey」によって紐づけられているため、わざわざ各モデルページにアクセスしなくても追加・編集を行うことができます。

しかしそれでもページ移動を繰り返し行わないといけないので少し不便です。

なので、それぞれカテゴリーと紐づいている「申込書」「項目詳細」「合計金額」のモデルをadminの「InlineModelAdminクラス」を使って1つにまとめたいと思います。

このカスタマイズは、はじめての Django アプリ作成、その 7でも詳しく説明されています。

InlineModelAdminクラスには2つのサブクラス「StackedInline」と「TabularInline」があるのでそれぞれ見ていきましょう。

# project/order/admin.py

from django.contrib import admin
from .models import (Company, Orderer, Supplier,
                    Item, Application, ItemDetail, TotalPrice)


""" StackedInlineサブクラスを使って項目詳細を設定 """
class ItemDetailInline(admin.StackedInline):
    model = ItemDetail


""" 申込書 """
class ApplicationAdmin(admin.ModelAdmin):
    inlines = [ItemDetailInline]        # 設定したInlineクラスを指定


admin.site.register(Company)
admin.site.register(Orderer)
admin.site.register(Supplier)
admin.site.register(Item)
admin.site.register(Application, ApplicationAdmin)
# admin.site.register(ItemDetail)
admin.site.register(TotalPrice)

リロードし、申込書の「追加ページ」にアクセスします。

ヘッダーで区切られたStackedInlineサブクラスのフィールドは、縦に改行されて表示されています。

InlineModelAdminクラスのデフォルトではフォームが3セットとなっています。

もう1つのTabularInlineサブクラスを使うことで、並列化されたテーブル形式で表示されるようになります。

# project/order/admin.py

from django.contrib import admin
from .models import (Company, Orderer, Supplier,
                    Item, Application, ItemDetail, TotalPrice)


""" TabularInlineサブクラスを使って項目詳細を設定 """
class ItemDetailInline(admin.TabularInline):
    model = ItemDetail


class ApplicationAdmin(admin.ModelAdmin):
    inlines = [ItemDetailInline]


admin.site.register(Company)
admin.site.register(Orderer)
admin.site.register(Supplier)
admin.site.register(Item)
admin.site.register(Application, ApplicationAdmin) # 追記
# admin.site.register(ItemDetail)
admin.site.register(TotalPrice)

現在フォームが3セット表示されていますが、表示数をコントロールする場合は「extra」機能に表示させたい数を指定します。

# project/order/admin.py

from django.contrib import admin
from .models import (Company, Orderer, Supplier,
                    Item, Application, ItemDetail, TotalPrice)


class ItemDetailInline(admin.TabularInline):
    model = ItemDetail
    extra = 1     # 1セット指定


class ApplicationAdmin(admin.ModelAdmin):
    inlines = [ItemDetailInline]


.........
.........
admin.site.register(Application, ApplicationAdmin) 
# admin.site.register(ItemDetail)
admin.site.register(TotalPrice)

「フォームセットの追加」の数もコントロールするには、「max_num」もしくわ「min_num」機能を使います。

# project/order/admin.py

from django.contrib import admin
from .models import (Company, Orderer, Supplier,
                    Item, Application, ItemDetail, TotalPrice)


class ItemDetailInline(admin.TabularInline):
    model = ItemDetail
    extra = 1
    max_num = 5     # 最大5セットまで追加できる


class ApplicationAdmin(admin.ModelAdmin):
    inlines = [ItemDetailInline]


.........
.........
admin.site.register(Application, ApplicationAdmin) 
# admin.site.register(ItemDetail)
admin.site.register(TotalPrice)

納まりもよくなったところで、「合計金額」のモデルも同じように「申込書」モデルに結合させます。

# project/order/admin.py

from django.contrib import admin
from .models import (Company, Orderer, Supplier,
                    Item, Application, ItemDetail, TotalPrice)


class ItemDetailInline(admin.TabularInline):
    model = ItemDetail
    extra = 1
    max_num = 5


""" InlineModelAdminクラス合計金額 """
class TotalPriceInline(admin.TabularInline):
    model = TotalPrice
    max_num = 1


class ApplicationAdmin(admin.ModelAdmin):
    inlines = [ItemDetailInline, TotalPriceInline]     # 追記


.........
.........
admin.site.register(Application, ApplicationAdmin) 
# admin.site.register(ItemDetail)
# admin.site.register(TotalPrice)

これで1つのモデルに集約され簡易的に追加・編集を行えるようになりました。

続いては、「申込書」モデル自体のレイアウトを変更していきます。

fieldsetsを使ってフィールドのレイアウトを変更

デフォルトの追加・編集ページでは下図のようにただ改行されたフィールドが表示されています。

ModelAdminの「fieldsets」を使うことによってフィールドのレイアウトをコントロールすることができます。

fieldsetsに渡す要素は、「[('お好きな名前', {'fields': ('フィールド名',)})]」のようなリスト形式内にタプルで囲われたディクショナリ形式(辞書型)です。

今回は分かりやすくリスト形式にしましたが、タプルでも機能します。

実際のコードを例に見ていきましょう。

# project/order/admin.py

from django.contrib import admin
from .models import (Company, Orderer, Supplier,
                    Item, Application, ItemDetail, TotalPrice)


........
........


class ApplicationAdmin(admin.ModelAdmin):
    """ 追加 """
    fieldsets = [
        ('仕入先',   {'fields': ('supplier',)}),
        ('会社情報', {'fields': ('company_name', 'orderer_name')}),
        ('発注日',   {'fields': ('created_at', 'delivery_date')}),
    ]
    inlines = [ItemDetailInline, TotalPriceInline]


.........
.........

各フィールドにヘッダーが設けられます。

ディクショナリ(辞書({}))内の「fields」キーごとにしっかりまとめられていることに注意しましょう。

それぞれ種類ごとに分けられましたが、幅を少し取り過ぎているため改行されているフィールドを並列化させます。

それを行うには、fieldsキーに渡されているタプル要素を「タプル」でまとめることによって、横並びになります。

※タプルで囲った後に、カンマを忘れないようにしましょう。

# project/order/admin.py

    fieldsets = [
        ('仕入先',   {'fields': ('supplier',)}),
        ('会社情報', {'fields': (('company_name', 'orderer_name'),)}),   # タプルで囲む
        ('発注日',   {'fields': (('created_at', 'delivery_date'),)}),     # タプルで囲む
    ]

ヘッダー部分が不要な場合は、「[(None, {'fields': ('フィールド名',)})]」とすることで表示されなくなります。

ついでに、状態はそのままにしてフィールドをまとめてしまいます。

# project/order/admin.py

class ApplicationAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,   {'fields': ('supplier',
                            ('company_name', 'orderer_name'),
                            ('created_at', 'delivery_date'),
                             )}),
    ]
    inlines = [ItemDetailInline, TotalPriceInline]

さらに、ディクショナリで「classes」キーを与えると表示/非表示のアクションを加えることができます。

classesを呼ぶことによってadminのスタイルシート(CSS)で定義されているクラス名「collapse」と「wide」のどちらかを使うことができます。

今回はcollapseを例に行います。

「納期(delivery_date)」の部分を非表示として折りたたんでみます。

# project/order/admin.py

class ApplicationAdmin(admin.ModelAdmin):
    fieldsets = [
        (None, {'fields': ('supplier',
                          ('company_name', 'orderer_name', 'created_at')
                           )}
        ),
        ('納期オープン', {'classes': ('collapse',),
                         'fields': ('delivery_date',)}),
    ]
    inlines = [ItemDetailInline, TotalPriceInline]

追加・編集ページでの保存ボタン配置変更

保存ボタンの配置は、デフォルトでは下部に設定されていますが、「save_on_top」機能にTureを与えることで上部に追加することができます。

# project/order/admin.py

class ApplicationAdmin(admin.ModelAdmin):
    save_on_top = True      # 追加
    fieldsets = [
        (None, {'fields': ('supplier',
                          ('company_name', 'orderer_name',
                           'created_at')
                           )}
        ),
        ('納期オープン', {'classes': ('collapse',),
                         'fields': ('delivery_date',)}),
    ]
    inlines = [ItemDetailInline, TotalPriceInline]

編集ページで新規保存ボタンを追加する

追加されているデータを元に新しくデータを保存したい場合があります。

なので、「save_as」機能にTrueを与え、デフォルトの「保存して 1 つ追加」を「新規保存」に置き換えます。

適当にデータを保存し、挙動を見てみます。

※「list_display」はチェンジリストページにフィールド名を表示させます。

# project/order/admin.py

class ApplicationAdmin(admin.ModelAdmin):
    list_display = ('supplier', 'orderer_name') # チェンジリストにフィールド名を表示
    save_on_top = True
    save_as = True # 追加
    fieldsets = [
        (None, {'fields': ('supplier',
                          ('company_name', 'orderer_name',
                           'created_at')
                           )}
        ),
        ('納期オープン', {'classes': ('collapse',),
                         'fields': ('delivery_date',)}),
    ]
    inlines = [ItemDetailInline, TotalPriceInline]

○○工場の編集ページに移動すると新規保存に置き換わっています。

実際に新規保存を行ってみると、アラートで保存されたことを伝えてくれます。

保存された後にチェンジリストページですぐに確認したい場合があると思います。

その時は、「save_as_continue」機能をFalseにすることで、チェンジリストページにリダイレクトされるようになります。

※デフォルトではTrueです

# project/order/admin.py

class ApplicationAdmin(admin.ModelAdmin):
    list_display = ('supplier', 'orderer_name')
    save_on_top = True
    save_as = True
    save_as_continue = False # 追加
    fieldsets = [
        (None, {'fields': ('supplier',
                          ('company_name', 'orderer_name',
                           'created_at')
                           )}
        ),
        ('納期オープン', {'classes': ('collapse',),
                         'fields': ('delivery_date',)}),
    ]
    inlines = [ItemDetailInline, TotalPriceInline]

adminサイトは非常に柔軟なのでさまざまなカスタマイズを楽しむことができます。

むしろデフォルトでも全く問題無く機能するのでDjangoは本当に優れていると実感しました。

他にもモデルごとにCSSやjava Scriptを適用させ、自分で作った機能を使用することができるので、ますます開発心がくすぐられます。

それでは以上となります。

最後までご覧いただきありがとうございました。