Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Django 管理サイトをカスタマイズする前に教えてほしかったこと / How to customize admin (DjangoCon JP 2021)

akiyoko
July 03, 2021

Django 管理サイトをカスタマイズする前に教えてほしかったこと / How to customize admin (DjangoCon JP 2021)

DjangoCon JP 2021 発表資料

本発表では、Django に標準搭載されている目玉機能のひとつである「管理サイト(Django Admin)」をカスタマイズする際の注意点について解説します。

コードをたった数行書き足すだけであらゆるモデルに対応したCRUD画面が追加できる管理サイトは、その手軽さで開発者からの評価も高く、「Django Developers Survey 2020」というアンケートでは有用なデフォルト機能ナンバーワンにも選ばれています。管理サイトは多くの開発者にとって最初に触れる Django アプリでもあり、開発中のデバッグから本番リリース後のデータメンテナンスまで幅広くお世話になることでしょう。

しかしながら、管理サイトは「万能」ではありません。良い面ばかりがクローズアップされがちな管理サイトですが、ここで敢えてマイナスの面を挙げてみます。

1. どんなカスタマイズが簡単にできるのか分からない
2. テンプレートを修正するのにコツが要る
3. 画面のスタイルを変えるのが大変
4. コードが断片化しやすくテストがしづらい
5. 日本語の情報が少ない

このように、管理サイトをカスタマイズをしようとしたときに特にトラブルの声が聞かれます。そこで本発表では、「Django 管理サイトをカスタマイズする前に教えてほしかったこと」と題して、管理サイトをカスタマイズするにあたって苦労するポイントとそれらの解決策について解説します。

特に、これから管理サイトをカスタマイズしようと考えている方は必聴です。

akiyoko

July 03, 2021
Tweet

More Decks by akiyoko

Other Decks in Programming

Transcript

  1. Django 管理サイトをカスタマイズする前に
    教えてほしかったこと
    2021.7.3 DjangoCongress JP 2021
    横瀬 明仁(akiyoko)

    View Slide

  2. このトークで話すこと
    1
    管理サイトをカスタマイズするにあたって
    苦労するポイントとその解決策
    (Django のバージョンは現時点で最新の 3.2 LTS)

    View Slide

  3. 目次
    1. 導入
    2. 管理サイトの基本仕様
    3. 全体構造と簡単なカスタマイズ
    4. テンプレートのカスタマイズ
    5. CSSのカスタマイズ
    6. 管理サイトの効率的なテスト
    7. 日本語情報
    8. まとめ
    2
    ( 3 min )
    ( 3 min )
    (10 min )
    ( 7 min )
    ( 7 min )
    ( 7 min )
    ( 1 min )
    ( 1 min )

    View Slide

  4. 自己紹介
    名 前 : 横瀬 明仁(akiyoko)
    所 属 : 株式会社 ワングリット
    Twitter : @aki_yok
    ブログ : akiyoko blog(https://akiyoko.hatenablog.jp/)
    Django 歴 : 8年くらい
    過去の発表 : 「現場で使える Django のセキュリティ対策」
    (DjangoCongress JP 2019)
    自費出版本 :
    3
    『現場で使える Django の教科書《基礎編》』
    『現場で使える Django の教科書《実践編》』
    『現場で使える Django REST Framework の教科書』
    『現場で使える Django 管理サイトのつくり方』

    View Slide

  5. 皆さん
    管理サイト、使ってますか?
    4

    View Slide

  6. (しまった … オンライン登壇だった)
    5

    View Slide

  7. 開発者の9割が管理サイトを使っている
    6
    Q.管理サイトを使っていますか?(N=92)
    約9割が「いつも使っている」
    あるいは「たまに使っている」
    と回答
    akiyoko 独自アンケート(2020年8月実施)より

    View Slide

  8. 開発者からの評価も高い
    7
    4000名を超えるアンケートで
    「管理サイト(admin)」が
    最も利用価値の高い組み込み
    アプリケーションに選出される
    Q.What contrib apps are most valuable to you?(N=4087)
    https://www.djangoproject.com/weblog/2020/jul/28/community-survey-2020/
    Django Developers Community Survey 2020

    View Slide

  9. 手軽で便利に使えるのが大きなメリット
    8
    Q.どんなところに管理サイトのメリットを感じますか?(N=89)
    akiyoko 独自アンケート(2020年8月実施)より
    手軽で便利!

    View Slide

  10. 管理サイトの代表的な困りごと
    9
    Q.管理サイトのデメリットを挙げるとすればどんなものがありますか?(N=84)
    カスタマイズ系の
    困りごとが多い…
    akiyoko 独自アンケート(2020年8月実施)より

    View Slide

  11. このトークですべて解決します 😉
    困りごと①:どんなカスタマイズが簡単にできるのか分からない
    困りごと②:テンプレートを修正するのにコツが要る
    困りごと③:画面のスタイルを変えるのが大変
    困りごと④:コードが断片化しやすくテストがしづらい
    困りごと⑤:日本語の情報が少ない
    10
    すべて
    解決します!

    View Slide

  12. その前に…
    管理サイトって何?
    11

    View Slide

  13. 管理サイトとは
    12
    INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    ]
    ひな型プロジェクトの設定ファイル(settings.py)
    モデルに対応したレコードの CRUD(追加・参照・変更・削除)画面
    を提供してくれる Django 組み込みのアプリケーション

    View Slide

  14. 基本仕様 画面遷移
    画面遷移
    13
    [<アプリ
    [<モデル名>]
    [追加]
    [追加]
    [<モデル名>
    [パスワードの変更]
    [保存してもう一つ追加]
    [保存]
    リンクを押下
    [保存]
    編集を続ける]
    [保存して編集を続ける]
    [削除]
    [選択された<モデル名>の削除]
    [削除]or[戻る]
    [戻る]
    [履歴]
    [最近行った操作]
    モデル変更画面へ
    [ログアウト]
    [もう一度ログイン]
    [<モデル名>]or[変更]
    [保存してもう一つ追加]
    [パスワードの変更]
    モデル
    追加画面
    アプリケーション
    ホーム画面
    ホーム画面
    モデル
    削除確認画面
    パスワード
    変更画面
    モデル
    一覧画面
    モデル
    変更画面
    [ログイン]
    ログアウト
    完了画面
    モデル
    変更履歴画面
    を追加]
    (一覧画面から遷移した場合)
    [保存して
    パスワード
    変更完了画面
    (編集画面から遷移た場合)
    ケーション名>]
    or[変更]
    ログイン画面

    View Slide

  15. 基本仕様 ログイン条件
    ログイン条件
    • is_staff と is_active 属性の値が
    いずれも True であること
    14
    Django デフォルトの User モデル

    View Slide

  16. 基本仕様 パーミッション
    15
    追加(add)
    Book
    変更(change)
    Book
    削除(delete)
    Book
    参照(view)
    Book
    ⇒ Book モデルの追加が可能
    ⇒ Book モデルの参照が可能
    ⇒ Book モデルの削除が可能
    ⇒ Book モデルの変更が可能
    モデル 操作 パーミッション
    × =
    • モデルの CRUD 操作を実行するためには、モデルごと・操作ごとの「パーミッション」と呼ばれ
    る権限が必要
    • ユーザーの追加画面・変更画面から「ユーザーパーミッション」として紐付けることが可能
    • is_superuser が True のユーザーは、すべてのパーミッションがあるとみなされる
    パーミッション(認可とアクセス制御)
    グループ
    ユーザー パーミッション
    ( User )
    ( Group )
    ( Permission )

    View Slide

  17. ポイント①:どんなカスタマイズが簡単にできるのか分からない
    ポイント②:テンプレートを修正するのにコツが要る
    ポイント③:画面のスタイルを変えるのが大変
    ポイント④:コードが断片化しやすくテストがしづらい
    ポイント⑤:日本語の情報が少ない
    16
    そもそもどんなカスタマイズ
    ができるの?
    ある程度以上のカスタマイズ
    になると急に難易度が上がる
    簡単にカスタマイズできるかどうか
    のジャッジにノウハウや調査が必要

    View Slide

  18. ポイント① まずは全体構造を理解しよう
    17
    全体構造(AdminSite クラスと ModelAdmin クラス)
    管理サイトをカスタマイズする前に知っておきたいこと

    View Slide

  19. ポイント① AdminSite と ModelAdmin
    18
    ルートURLconf(/urls.py)
    from django.contrib import admin
    from django.urls import path
    urlpatterns = [
    path('admin/', admin.site.urls),
    ]
    admin.site は AdminSite の
    グローバルオブジェクト
    shop/admin.py
    from django.contrib import admin
    from .models import Book
    class BookAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'price')
    admin.site.register(Book, BookAdmin)
    管理サイトにモデルを登録する際、
    ModelAdmin の派生クラスを第2引数で渡す
    (省略すると素の ModelAdmin が使われる)
    AdminSite クラス ModelAdmin クラス

    View Slide

  20. ポイント① 管理サイトの全体構造
    19
    View
    View View
    View
    モデル
    ModelAdmin
    View
    View
    管理サイト用
    テンプレート
    AdminSite
    View
    View
    モデル
    フォーム










    /admin///…
    の URLパターンには ModelAdmin が対応
    /admin/… の URLパターンには AdminSite が対応
    全体の画面項目や挙動 ⇒ AdminSite クラスに定義
    モデルごとの画面項目や挙動 ⇒ ModelAdmin クラスに定義
    全体構造

    View Slide

  21. ポイント① 管理サイトのカスタマイズ難易度
    20
    簡単!
    コツが必要!
    ⇒ ポイント② ③ で説明
    1) AdminSite を利用して、全体の設定をカスタマイズ
    2) ModelAdmin を利用して、モデルごとの画面項目をカスタマイズ
    3) テンプレートの仕組みを利用して、テンプレートをカスタマイズ
    4) 静的ファイルの仕組みを利用して、CSS をカスタマイズ

    View Slide

  22. ポイント①
    21
    1) AdminSite を利用して、全体の設定をカスタマイズ
    2) ModelAdmin を利用して、モデルごとの画面項目をカスタマイズ

    View Slide

  23. ポイント① 1) AdminSite の主要なクラス変数・メソッド (1/2)
    22
    # クラス変数 説明
    1 site_header 共通ヘッダの左側に表示されるサイト名。デフォルト値は「Django 管理サイト」
    2 site_title タグの一部として表示される値。デフォルト値は「Django サイト管理」
    3 site_url 共通ヘッダの右側に表示される「サイトを表示」のリンク先の URL。None をセット
    するとリンク自体が非表示になる。デフォルト値は「/」
    4 login_template ログイン画面のテンプレートのパス
    5 login_form ログインビューで使われるフォームクラス
    6 logout_template ログアウト完了画面のテンプレートのパス
    7 index_title ホーム画面の タグに表示されるタイトル。デフォルト値は「サイト管理」
    8 index_template ホーム画面のテンプレートのパス
    9 app_index_template アプリケーションホーム画面のテンプレートのパス
    10 password_change_template パスワード変更画面のテンプレートのパス
    11 password_change_done_template パスワード変更完了画面のテンプレートのパス
    12 empty_value_display モデル一覧画面で値が None の場合に表示される文字列。デフォルト値は「-」
    13 enable_nav_sidebar Django 3.1 で追加されたナビゲーションサイドバーを表示するかどうか
    (参考)https://docs.djangoproject.com/ja/3.2/ref/contrib/admin/#adminsite-attributes
    AdminSite の主要なクラス変数

    View Slide

  24. ポイント① 1) AdminSite の主要なクラス変数・メソッド (2/2)
    23
    # メソッド 説明
    1 has_permission 管理サイトにアクセスするために必要な条件を定義
    2 get_urls 管理サイトで使用する URLのパターンと対応するビューのリストを定義
    3 each_context 各テンプレートに渡すキーと値を定義
    4 login ログイン画面表示およびログイン実行時のビュー
    5 logout ログアウト実行時のビュー
    6 index ホーム画面を表示するためのビュー
    7 app_index アプリケーションホーム画面を表示するためのビュー
    8 password_change パスワード変更画面表示およびパスワード変更実行時のビュー
    9 password_change_done パスワード変更完了画面を表示するためのビュー
    AdminSite の主要なメソッド
    (参考)https://docs.djangoproject.com/ja/3.2/ref/contrib/admin/#adminsite-methods

    View Slide

  25. ポイント① 1) AdminSite を使ったカスタマイズ例
    ルートURLconf の中で admin.site の属性をダイレクトに書き換えてしまうのが簡単
    24
    ルートURLconf(/urls.py)
    from django.contrib import admin
    from django.urls import path
    def has_permission(request):
    return request.user.is_active
    admin.site.site_header = 'システム管理者用サイト'
    admin.site.site_title = 'マイプロジェクト'
    admin.site.index_title = 'ホーム'
    admin.site.site_url = None
    admin.site.has_permission = has_permission
    urlpatterns = [
    path('admin/', admin.site.urls),
    ]
    AdminSite 派生クラスを作成して、起動時に読み込ませることも可能
    https://docs.djangoproject.com/ja/3.2/ref/contrib/admin/#overriding-the-default-admin-site

    View Slide

  26. ポイント①
    25
    1)AdminSite を利用して、全体の設定をカスタマイズ
    2)ModelAdmin を利用して、モデルごとの画面項目をカスタマイズ

    View Slide

  27. ポイント① 2) ModelAdmin のクラス変数・メソッド (1/3)
    26
    # クラス変数 説明
    1 actions アクション一覧の設定
    2 actions_on_bottom アクション一覧をテーブルの下側に表示するかどうかの設定(デフォルト値は False)
    3 actions_on_top アクション一覧をテーブルの上側に表示するかどうかの設定(デフォルト値は True)
    4 date_hierarchy 日付ドリルダウンナビゲーションの設定
    5 empty_value_display フィールドの値が None の場合に表示する文字列
    6 list_display モデル一覧画面に表示するフィールドの設定
    7 list_display_links どの項目にモデル変更画面へのリンクを張るかを指定
    8 list_editable モデル一覧画面で変更を保存したいフィールドの設定
    9 list_filter 絞り込み(フィルタ)の設定
    10 list_max_show_all ページネーションUIに「全件表示」リンクを表示するための検索結果件数のしきい値
    11 list_per_page ページネーションUIを表示するための検索結果件数のしきい値
    12 list_select_related オブジェクト検索時に関連先モデルを JOIN して検索するようにするための設定
    13 ordering 初期表示時のソートの設定
    14 search_fields 簡易検索の設定
    15 sortable_by ソート可能な項目を限定するための設定
    ModelAdmin の主要なクラス変数(モデル一覧画面用)

    View Slide

  28. ポイント① 2) ModelAdmin のクラス変数・メソッド (2/3)
    27
    # クラス変数 説明
    1 autocomplete_fields ForeignKey や ManyToManyField のフィールドのウィジェットを、オートコンプリート可能な
    入力フィールドに変更するための設定
    2 exclude モデル追加・変更画面に表示しないフィールドの設定
    3 fields モデル追加・変更画面に表示するフィールドの設定
    4 fieldsets モデル追加・変更画面のフィールドをセクション区切りで表示するための設定
    5 form モデル追加画面・変更画面で利用する ModelForm クラスを差し替えるための設定
    6 formfield_overrides モデルのフィールドクラスに対応するフィールドオプションのマッピングを変更するための設定
    7 inlines インライン表示のための設定
    8 readonly_fields テキスト表示をするフィールドの設定
    9 save_as モデル変更画面に「保存してもう一つ追加」ボタンの代わりに、「新規保存」ボタンを表示する
    かどうかの設定(デフォルト値は False)
    10 save_as_continue モデル変更画面で「新規保存」ボタンを押下した後のリダイレクト先をモデル一覧画面にするか
    どうかの設定(デフォルト値は True)
    11 save_on_top フォーム上部に「保存」ボタンや「削除」ボタンを追加するかどうかの設定(デフォルト値は
    False)
    ModelAdmin の主要なクラス変数(モデル追加・変更画面用)
    (参考)https://docs.djangoproject.com/ja/3.2/ref/contrib/admin/#modeladmin-options

    View Slide

  29. ポイント① 2) ModelAdmin のクラス変数・メソッド (3/3)
    28
    # クラス変数 説明
    1 add_view モデル追加画面表示およびモデル追加実行時のビュー
    2 change_view モデル変更画面表示およびモデル変更実行時のビュー
    3 changelist_view モデル一覧画面を表示するためのビュー
    4 delete_view モデル削除確認画面表示およびモデル削除実行時のビュー
    5 get_actions アクション一覧を返す
    6 get_queryset モデル一覧画面でオブジェクトを検索するための QuerySet オブジェクトを返す
    7 get_urls モデルごとの URLのパターンと対応するビューのリストを返す
    8 has_add_permission モデルが追加できるかどうかを判定する
    9 has_change_permission モデルが変更できるかどうかを判定する
    10 has_delete_permission モデルが削除できるかどうかを判定する
    11 has_view_permission モデルが参照できるかどうかを判定する
    12 has_module_permission ホーム画面にモデルを表示できるかどうかを判定する
    13 save_model モデルを保存する
    14 save_formset インライン側のモデルを保存する。インライン表示を設定している場合にのみ呼び出される
    15 delete_model モデルを削除する
    ModelAdmin の主要なメソッド
    (参考)https://docs.djangoproject.com/ja/3.2/ref/contrib/admin/#modeladmin-methods

    View Slide

  30. ポイント① 2) ModelAdmin を使ったカスタマイズ例 (1/5)
    29
    shop/admin.py
    from django.contrib import admin
    from .models import Book
    class BookAdmin(admin.ModelAdmin):
    # 画面表示フィールド
    list_display = ('id', 'title', 'price', 'size', 'publish_date')
    admin.site.register(Book, BookAdmin)
    (モデル一覧画面)画面表示フィールド: list_display

    View Slide

  31. ポイント① 2) ModelAdmin を使ったカスタマイズ例 (2/5)
    30
    (モデル一覧画面)簡易検索: search_fields
    shop/admin.py
    from django.contrib import admin
    from .models import Book
    class BookAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'price', 'size', 'publish_date')
    # 簡易検索
    search_fields = ('title', 'publisher__name', 'authors__name')
    admin.site.register(Book, BookAdmin)

    View Slide

  32. ポイント① 2) ModelAdmin を使ったカスタマイズ例 (3/5)
    31
    (モデル一覧画面)フィルター: list_filter
    shop/admin.py
    from django.contrib import admin
    from .models import Book
    class BookAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'price', 'size', 'publish_date')
    # フィルター
    list_filter = ('size', 'price', 'publish_date')
    admin.site.register(Book, BookAdmin)

    View Slide

  33. ポイント① 2) ModelAdmin を使ったカスタマイズ例 (4/5)
    32
    (モデル一覧画面)アクション一覧: actions
    shop/admin.py
    from django.contrib import admin
    from django.utils import timezone
    from .models import Book
    class BookAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'price', 'size', 'publish_date')
    # アクション一覧
    actions = ['publish_today']
    @admin.action(
    description='出版日を今日に更新',
    permissions=('change',),
    )
    def publish_today(self, request, queryset):
    """選択されたレコードの出版日を今日に更新する"""
    queryset.update(publish_date=timezone.localdate())
    admin.site.register(Book, BookAdmin)

    View Slide

  34. ポイント① 2) ModelAdmin を使ったカスタマイズ例 (5/5)
    33
    (モデル追加・変更画面)フォーム: form
    shop/admin.py
    from django.contrib import admin
    from .forms import BookAdminForm
    from .models import Book
    class BookAdmin(admin.ModelAdmin):
    # フォーム
    form = BookAdminForm
    admin.site.register(Book, BookAdmin)
    shop/forms.py
    class BookAdminForm(forms.ModelForm):
    def clean_price(self):
    value = self.cleaned_data.get('price', 0)
    if value > 10000:
    raise forms.ValidationError(
    "価格は1万円を超えないでください。")
    return value

    View Slide

  35. ポイント① まとめ
    34
    AdminSite や ModelAdmin のクラス変数やメソッドを利用して
    特定の画面項目や挙動をカスタマイズするのは簡単
    どんなカスタマイズが簡単にできるのか分からない

    View Slide

  36. ポイント①:どんなカスタマイズが簡単にできるのか分からない
    ポイント②:テンプレートを修正するのにコツが要る
    ポイント③:画面のスタイルを変えるのが大変
    ポイント④:コードが断片化しやすくテストがしづらい
    ポイント⑤:日本語の情報が少ない
    35
    テンプレートってどうやって
    カスタマイズするの?
    どのテンプレートをカスタマイズ
    すればいいか分からない

    View Slide

  37. ポイント② まずは仕組みを理解しよう
    36
    テンプレートをカスタマイズする前に知っておきたいこと3つ
    1) テンプレートの優先順位
    2) テンプレートの継承
    3) 管理サイト本体のテンプレート構成

    View Slide

  38. ポイント②
    37
    テンプレートをカスタマイズする前に知っておきたいこと3つ
    1) テンプレートの優先順位
    2) テンプレートの継承
    3) 管理サイト本体のテンプレート構成

    View Slide

  39. ポイント② 1) テンプレートの優先順位
    38
    設定ファイルの「TEMPLATES」の「DIRS」で指定
    したディレクトリ
    「INSTALLED_APPS」に登録されたインストール済み
    アプリケーションのうち、「django.contrib.admin」より
    も上に追加しているアプリケーション配下の
    「templates」ディレクトリ
    管理サイト本体のテンプレート用ディレクトリ(インス
    トールディレクトリ配下「contrib/admin/templates/」)
    INSTALLED_APPS = [
    'myadmin.apps.MyadminConfig',
    'django.contrib.admin',
    ...
    ]
    TEMPLATES = [
    {
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'DIRS': [BASE_DIR / 'templates'],
    'APP_DIRS': True,
    ...
    },
    ]
    優先順位 ②
    優先順位 ①
    優先順位 ③
    mysite (← ベースディレクトリ)
    .
    |-- myadmin (←「INSTALLED_APPS」で上位に追加したアプリケーション)
    | .
    | |-- templates
    | | `-- admin
    | | `-- index.html ★ 優先順位 ②
    | .
    |
    |-- templates (←「TEMPLATES」の「DIRS」で指定したディレクトリ)
    | `-- admin
    | `-- index.html ★ 優先順位 ①
    .
    .
    | (↓ 管理サイト本体のテンプレートが配置されたディレクトリ)
    `-- venv/Lib/site-packages/django/contrib/admin/templates
    `-- admin
    `-- index.html ★ 優先順位 ③

    View Slide

  40. ポイント②
    39
    テンプレートをカスタマイズする前に知っておきたいこと3つ
    1) テンプレートの優先順位
    2) テンプレートの継承
    3) 管理サイト本体のテンプレート構成

    View Slide

  41. ポイント② 2) テンプレートの継承
    40
    django/contrib/admin/templates/admin/base.html(管理サイト本体側)
    /templates/admin/base.html
    ...(略)...
    {% block welcome-msg %}
    {% translate 'Welcome,' %}
    {% firstof user.get_short_name user.get_username %}.
    {% endblock %}
    ...(略)...
    テンプレートを extends タグで
    継承して、特定の block タグの
    内容を書き換えることが可能
    {% extends "admin/base.html" %}
    {% block welcome-msg %}
    {% firstof user.get_short_name user.get_username %} としてログイン中
    {% endblock %}

    View Slide

  42. ポイント②
    41
    テンプレートをカスタマイズする前に知っておきたいこと3つ
    1) テンプレートの優先順位
    2) テンプレートの継承
    3) 管理サイト本体のテンプレート構成

    View Slide

  43. ポイント② 3) 管理サイト本体のテンプレート構成 (1/3)
    42
    管理サイト本体のテンプレートは
    Django のインストールディレクトリ配下の
    contrib/admin/templates/ に配置されている
    Windows の場合
    venv/Lib/site-packages/django/contrib/admin/templates
    macOS の場合
    venv/lib/python3.9/site-packages/django/contrib/admin/templates

    View Slide

  44. ポイント② 3) 管理サイト本体のテンプレート構成 (2/3)
    どの画面でどのテンプレートが使われているか?
    43
    # 画面 メインとなる管理サイト本体のテンプレートファイル
    1 ログイン画面 admin/login.html
    2 ホーム画面 admin/index.html
    3 アプリケーションホーム画面 admin/app_index.html
    4 モデル一覧画面 admin/change_list.html
    5 モデル追加画面 admin/change_form.html
    6 モデル変更画面 admin/change_form.html
    7 モデル削除確認画面 admin/delete_confirmation.html(モデル変更画面から遷移した場合)
    admin/delete_selected_confirmation.html(モデル一覧画面から遷移した場合)
    8 モデル変更履歴画面 admin/object_history.html
    9 パスワード変更画面 registration/password_change_form.html
    10 パスワード変更完了画面 registration/password_change_done.html
    11 ログアウト完了画面 registration/logged_out.html

    View Slide

  45. ポイント② 3) 管理サイト本体のテンプレート構成 (3/3)
    モデル一覧画面
    44
    admin/base.html

    admin/base_site.html

    admin/change_list.html

    admin/nav_sidebar.html
    admin/app_list.html
    admin/change_list_object_tools.html
    admin/search_form.html
    admin/actions.html
    admin/change_list_results.html
    admin/pagination.html
    admin/filter.html
    必ず継承
    extendsタグだけでなく、includeタグや
    テンプレートタグによって、さまざまな
    テンプレートが使われている
    どのテンプレートが使われている
    かは「django-debug-toolbar」の
    「Templates」パネルを利用すれ
    ば簡単に確認可能
    メイン

    View Slide

  46. ポイント② テンプレートのカスタマイズ方針
    45
    View
    View
    ModelAdmin
    AdminSite
    テンプレートの優先順位 + 継承 の仕組みを使ってカスタマイズ
    プロジェクト内のテンプレート
    (「TEMPLATES」の「DIRS」ディレクトリ)
    管理サイト本体のテンプレート
    {% extends "admin/base.html" %}
    {% block xxx %}
    この内容で書き換え
    {% endblock %}
    admin/pagination.html
    admin/pagination.html
    継承がうまく使えなけれ
    ば、管理サイト本体のも
    のと同名のテンプレート
    ファイルでまるまる置き
    換えることも可能
    {% block xxx %}
    書き換え対象の文字列
    {% endblock %}
    admin/base.html
    admin/base.html

    View Slide

  47. ポイント② テンプレートのカスタマイズ例
    46
    /templates/admin/base.html
    {% extends "admin/base.html" %}
    {% block welcome-msg %}
    {% firstof user.get_short_name user.get_username %} としてログイン中
    {% endblock %}
    /settings.py
    TEMPLATES = [
    {
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'DIRS': [BASE_DIR / 'templates'],
    ...
    },
    ]

    View Slide

  48. ポイント② まとめ
    47
    テンプレートの優先順位 + 継承 の仕組みを使ってカスタマイズ
    「TEMPLATES」の「DIRS」で指定したディレクトリに
    管理サイト本体のテンプレートと同名のファイルを配置
    テンプレートを修正するのにコツが要る

    View Slide

  49. ポイント①:どんなカスタマイズが簡単にできるのか分からない
    ポイント②:テンプレートを修正するのにコツが要る
    ポイント③:画面のスタイルを変えるのが大変
    ポイント④:コードが断片化しやすくテストがしづらい
    ポイント⑤:日本語の情報が少ない
    48
    どうやって新しい CSS ファイル
    を読み込めばいいのか分からない
    CSS ファイルは
    どこに置けばいいの?

    View Slide

  50. ポイント③ まずは仕組みを理解しよう
    49
    CSS をカスタマイズする前に知っておきたいこと3つ
    1) 静的ファイルの優先順位
    2) テンプレートを継承して CSS ファイルを追加するための仕組み
    3) ModelAdmin で CSS ファイルを追加するための仕組み

    View Slide

  51. ポイント③
    50
    CSS をカスタマイズする前に知っておきたいこと3つ
    1) 静的ファイルの優先順位
    2) テンプレートを継承して CSS ファイルを追加するための仕組み
    3) ModelAdmin で CSS ファイルを追加するための仕組み

    View Slide

  52. ポイント③ 1) 静的ファイルの優先順位
    51
    設定ファイルの「STATICFILES_DIRS」で
    指定したディレクトリ
    「INSTALLED_APPS」に登録されたインストール済み
    アプリケーションのうち、「django.contrib.admin」より
    も上に追加しているアプリケーション配下の「static」
    ディレクトリ
    INSTALLED_APPS = [
    'myadmin.apps.MyadminConfig',
    'django.contrib.admin',
    ...
    ]
    STATICFILES_DIRS = [BASE_DIR / 'static']
    優先順位 ①
    優先順位 ②
    管理サイト本体の静的ファイル用ディレクトリ(インス
    トールディレクトリ配下の「contrib/admin/static/」)
    優先順位 ③
    mysite (← ベースディレクトリ)
    .
    |-- myadmin (←「INSTALLED_APPS」で上位に追加したアプリケーション)
    | .
    | |-- static
    | | `-- admin
    | | `-- css
    | | `-- base.css ★ 優先順位 ②
    | .
    |
    |-- static (←「STATICFILES_DIRS」で指定したディレクトリ)
    | `-- admin
    | `-- css
    | `-- base.css ★ 優先順位 ①
    .
    .
    | (↓ 管理サイト本体の静的ファイルが配置されたディレクトリ)
    `-- venv/Lib/site-packages/django/contrib/admin/static
    `-- admin
    `-- css
    `-- base.css ★ 優先順位 ③

    View Slide

  53. ポイント③
    52
    CSS をカスタマイズする前に知っておきたいこと3つ
    1) 静的ファイルの優先順位
    2) テンプレートを継承して CSS ファイルを追加するための仕組み
    3) ModelAdmin で CSS ファイルを追加するための仕組み

    View Slide

  54. ポイント③ 2) テンプレート継承で CSS ファイルを追加 (1/2)
    53
    django/contrib/admin/templates/admin/base.html
    管理サイト本体の base.html の extrastyle ブロックをオーバーライドする

    ...

    ...
    {% block extrastyle %}{% endblock %}
    ...
    {% block extrahead %}{% endblock %}
    {% block responsive %}
    ...

    ...
    {% endblock %}
    ...
    モバイルおよびタブレット用のスタイル定義
    管理サイト全体の基本スタイルが定義されている
    各画面のメインコンテンツのテンプレートからオーバーライドして追
    加の CSSファイルを読み込むために用意されたブロック。例えば、
    「admin/login.html」では「admin/css/login.css」が読み込まれる
    モデルごとの画面のテンプレートからオーバーライドして
    任意の静的ファイルを読み込むために用意されたブロック。
    後述する ModelAdmin の Media.css でも利用される

    View Slide

  55. ポイント③ 2) テンプレート継承で CSS ファイルを追加 (2/2)
    54
    View
    View
    ModelAdmin
    AdminSite
    プロジェクト内のテンプレート
    (「TEMPLATES」の「DIRS」ディレクトリ)
    管理サイト本体のテンプレート
    admin/base.html
    admin/base.html
    継承したテンプレートで extrastyle ブロックを書き換えて、CSSファイルを追加する
    {% extends "admin/base.html" %}
    {% block extrastyle %}
    href="{% static 'admin/css/base_extra.css' %}">
    {% endblock %}
    CSS
    admin/css/base_extra.css
    プロジェクト内の静的ファイル
    (「STATICFILES_DIRS」ディレクトリ)
    CSS
    admin/css/base.css など
    CSS
    CSS
    CSS
    管理サイト本体の静的ファイル

    View Slide

  56. ポイント③ 2) CSS のカスタマイズ例
    55
    /static/admin/css/base_extra.css
    #header {
    background: #f08e3e; /* ヘッダの背景色をオレンジ系に */
    }
    #branding h1 {
    font-size: 1.4em; /* ヘッダのサイト名のフォントを小さくする */
    }
    /templates/admin/base.html
    {% extends "admin/base.html" %}
    {% load static %}
    {% block extrastyle %}

    {% endblock %}
    /settings.py
    STATICFILES_DIRS = [BASE_DIR / 'static']
    base.html や base_site.html 以外のテンプレート
    を継承する場合は、{{ block.super }} を使って親テ
    ンプレートの内容を残しつつ、新たなCSSファイル
    を読み込むための記述をするように注意!

    View Slide

  57. ポイント③
    56
    CSS をカスタマイズする前に知っておきたいこと3つ
    1) 静的ファイルの優先順位
    2) テンプレートを継承して CSS ファイルを追加するための仕組み
    3) ModelAdmin で CSS ファイルを追加するための仕組み

    View Slide

  58. ポイント③ 3) ModelAdmin で CSS ファイルを追加
    57
    shop/admin.py
    from django.contrib import admin
    from .models import Book
    class BookAdmin(admin.ModelAdmin):
    class Media:
    css = {
    'all': ('admin/css/book.css',)
    }
    admin.site.register(Book, BookAdmin)
    ModelAdmin の「Media.css」で指定した CSS ファイルは、
    モデル一覧画面・追加画面・変更画面・削除確認画面のテンプレート
    (の extrahead ブロック)で読み込まれる

    View Slide

  59. ポイント③ 3) CSS のカスタマイズ例
    58
    /static/admin/css/book.css
    .change-list table .field-title {
    background: #fffacd; /* タイトル列の背景色を黄色系に */
    color: red; /* タイトル列の文字色を赤に */
    }
    shop/admin.py
    from django.contrib import admin
    from .models import Book
    class BookAdmin(admin.ModelAdmin):
    class Media:
    css = {
    'all': ('admin/css/book.css',)
    }
    admin.site.register(Book, BookAdmin)
    /settings.py
    STATICFILES_DIRS = [BASE_DIR / 'static']

    View Slide

  60. ポイント③ まとめ
    59
    テンプレートで extrastyle ブロックをオーバーライドして
    or
    ModelAdmin の「Media.css」を使って
    画面のスタイルを変えるのが大変
    新しい CSS ファイルを読み込む

    View Slide

  61. ポイント①:どんなカスタマイズが簡単にできるのか分からない
    ポイント②:テンプレートを修正するのにコツが要る
    ポイント③:画面のスタイルを変えるのが大変
    ポイント④:コードが断片化しやすくテストがしづらい
    ポイント⑤:日本語の情報が少ない
    60

    View Slide

  62. ポイント④ 困りごと
    61
    コードが断片化しやすくテストがしづらい
    カバレッジを100%にしても
    あまり意味がないのでは?
    shop/admin.py
    from django.contrib import admin
    from .models import Book
    class BookAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'price', 'size', 'publish_date')
    search_fields = ('title', 'publisher__name', 'authors__name')
    list_filter = ('size', 'price', 'publish_date')
    list_per_page = 10
    actions = ['publish_today']
    @admin.action(
    description='出版日を今日に更新',
    permissions=('change',),
    )
    def publish_today(self, request, queryset):
    queryset.update(publish_date=timezone.localdate())
    admin.site.register(Book, BookAdmin)
    品質を担保するためには
    どんなテストをすればいいの?

    View Slide

  63. ポイント④ 効率的なテストをしよう
    62
    管理サイトのテストを効率化できるパッケージを使う
    1) lxml で HTML をパースして画面部品を検証
    2) Selenium でブラウザテスト

    View Slide

  64. ポイント④
    63
    管理サイトのテストを効率化できるパッケージを使う
    1) lxml で HTML をパースして画面部品を検証
    2) Selenium でブラウザテスト

    View Slide

  65. ポイント④ 1) lxml を使って画面部品を検証
    lxml は、HTML や XML を解析するためのライブラリ
    64
    response
    HTMLElement
    HTMLElement
    HTMLElement
    rendered_content
    (HTML文字列)
    Django 標準の TestCase クラスのテストクライアントで取得したレスポンスの rendered_content 属性
    でレンダリング後の HTML が取れるので、それを lxml でパースすれば画面項目の検証が可能
    管理サイト側の HTML(要素の構造や class 属性・id 属性など)は固定されているため、
    画面側の都合でテストコードを書き直さなくて済む

    View Slide

  66. ポイント④ 1) lxml を使ったテストクラスの実装例 (1/2)
    65
    shop/tests/test_admin_book_change_list.py
    import datetime
    from django.contrib.auth import get_user_model
    from django.test import TestCase
    from .lxml_helpers import ChangeListPage
    from ..models import Book
    User = get_user_model()
    class TestAdminBookChangeList(TestCase):
    """管理サイトの Book モデル一覧画面のユニットテスト(システム管理者の場合)"""
    def setUp(self):
    # テストユーザー(システム管理者)を作成
    self.superuser = User.objects.create_superuser('admin', '[email protected]', 'pass12345')
    # Bookモデルのテストレコードを作成
    self.book = Book.objects.create(title='Book 1', price=1000, publish_date=datetime.date(2021, 7, 3))
    def test_page_items_for_result_list(self):
    # 管理サイトにログイン
    self.client.login(username=self.superuser.username, password='pass12345')
    # モデル一覧画面に遷移するためのリクエストを実行
    response = self.client.get('/admin/shop/book/')
    self.assertEqual(response.status_code, 200)
    # 画面項目を検証
    page = ChangeListPage(response.rendered_content)
    # 検索結果テーブル
    self.assertEqual(page.result_list_header_texts, ['ID', 'タイトル', '価格', 'サイズ', '出版日'])
    self.assertEqual(len(page.result_list_rows_texts), 1)
    self.assertEqual(page.result_list_rows_texts[0], ['Book 1', '1000', '-', '2021年7月3日'])

    View Slide

  67. ポイント④ 1) lxml を使ったテストクラスの実装例 (2/2)
    66
    shop/tests/lxml_helpers.py
    import lxml.html
    class ChangeListPage:
    """モデル一覧画面の画面項目検証用クラス"""
    def __init__(self, rendered_content):
    self.parsed_content = lxml.html.fromstring(rendered_content)
    @property
    def result_list(self):
    """検索結果テーブルのHTMLElementオブジェクト"""
    elements = self.parsed_content.xpath('//table[@id="result_list"]')
    return elements[0] if elements else None
    @property
    def result_list_header_texts(self):
    """検索結果テーブルのヘッダの表示内容"""
    if self.result_list is None:
    return None
    head = self.result_list.xpath('thead/tr')[0]
    return [e.text_content() for e in head.xpath('th[contains(@class, "column-")]/div[@class="text"]')]
    @property
    def result_list_rows_texts(self):
    """検索結果テーブルのデータ行の表示内容"""
    if self.result_list is None:
    return None
    rows = self.result_list.xpath('tbody/tr')
    return [[e.text_content() for e in row.xpath('td[contains(@class, "field-")]')] for row in rows]

    View Slide

  68. ポイント④
    67
    管理サイトのテストを効率化できるパッケージを使う
    1) lxml で HTML をパースして画面部品を検証
    2) Selenium でブラウザテスト

    View Slide

  69. ポイント④ 2) Seleinum でブラウザテスト
    Selenium はプログラムからブラウザを操作するためのツール
    68
    HTTP リクエスト
    HTTP レスポンス
    ブラウザ
    (Chrome)
    Selenium
    ChromeDriver
    ③’ ブラウザを操作
    WebDriver
    Webアプリケーション
    (Django プロジェクト)
    Webサーバ
    Selenium には Webブラウザの API を利用するための WebDriver
    インタフェースと主要なブラウザ向けの実装クラスが含まれている
    ( Chrome を利用する場合は ChromeDriver が別途必要)
    $ pip install selenium chromedriver-binary==91.*
    ・Python では selenium パッケージのインストールが必要
    ・ChromeDriver には chromedriver-binary パッケージが利用可能

    View Slide

  70. ポイント④ 2) AdminSeleniumTestCase クラス
    69
    Django
    unittest
    django.test.testcases
    SimpleTestCase
    django.test.testcases
    TransactionTestCase
    unittest.case
    TestCase
    django.test.testcases
    TestCase
    django.contrib.staticfiles.testing
    StaticLiveServerTestCase
    django.test.testcases
    LiveServerTestCase
    django.test.selenium
    SeleniumTestCase
    django.contrib.admin.tests
    AdminSeleniumTestCase
    Django 組み込みの TestCase クラス
    Django 標準の
    TestCase クラス
    • Selenium のテストには
    StaticLiveServerTestCaseを利用する
    • 特に、管理サイトの場合には
    AdminSeleniumTestCase を利用する
    setUpClass で WebDriver インスタンス(テスト
    メソッドからは self.selenium でアクセス可)を
    作成して、さらに Webサーバを起動してくれる

    View Slide

  71. ポイント④ 2) AdminSeleniumTestCase テストの実装例 (1/2)
    70
    shop/tests/test_admin_senario.py
    # chromedriver_binaryをimportすることでChromeDriverのパスを通してくれる
    import chromedriver_binary
    from django.contrib.admin.tests import AdminSeleniumTestCase
    from django.contrib.auth import get_user_model
    from selenium import webdriver
    User = get_user_model()
    class TestAdminSenario(AdminSeleniumTestCase):
    """管理サイトのシナリオテスト(システム管理者の場合)"""
    available_apps = None
    browser = 'chrome'
    @classmethod
    def create_webdriver(cls):
    """Chrome用のWebDriverインスタンスを作成する"""
    chrome_options = webdriver.ChromeOptions()
    # ヘッドレスモード
    chrome_options.add_argument('--headless')
    return webdriver.Chrome(chrome_options=chrome_options)
    def setUp(self):
    # テストユーザー(システム管理者)を作成
    self.superuser = User.objects.create_superuser('admin', '[email protected]', 'pass12345')
    def assert_title(self, text):
    """タイトルを検証する"""
    self.assertEqual(self.selenium.title.split(' | ')[0], text)
    (次ページに続く)

    View Slide

  72. ポイント④ 2) AdminSeleniumTestCase テストの実装例 (2/2)
    71
    def test_book_crud(self):
    # 1. システム管理者でログイン
    self.admin_login(self.superuser.username, 'pass12345')
    self.assert_title('サイト管理')
    # ホーム画面でスクリーンショットを撮る(1枚目)
    self.selenium.save_screenshot(f'{self.id()}-1.png')
    # 2. ホーム画面で「本」リンクを押下
    self.selenium.find_element_by_link_text('本').click()
    self.wait_page_loaded()
    self.assert_title('変更する 本 を選択')
    # モデル一覧画面でスクリーンショットを撮る(2枚目)
    self.selenium.save_screenshot(f'{self.id()}-2.png')
    # 3. モデル一覧画面で「本 を追加」ボタンを押下
    self.selenium.find_element_by_link_text('本 を追加').click()
    self.wait_page_loaded()
    self.assert_title('本 を追加')
    # 4. モデル追加画面で項目を入力して「保存」ボタンを押下
    self.selenium.find_element_by_name('title').send_keys('Book 1')
    self.selenium.find_element_by_name('price').send_keys('1000')
    self.selenium.find_element_by_name('publish_date').send_keys('2021-07-03')
    # モデル追加画面でスクリーンショットを撮る(3枚目)
    self.selenium.save_screenshot(f'{self.id()}-3.png')
    self.selenium.find_element_by_xpath('//input[@value="{}"]'.format('保存')).click()
    self.wait_page_loaded()
    self.assert_title('変更する 本 を選択')
    # モデル一覧画面でスクリーンショットを撮る(4枚目)
    self.selenium.save_screenshot(f'{self.id()}-4.png')
    # (以下略)
    (前ページから続く)

    View Slide

  73. ポイント④ まとめ
    72
    生成される HTMLを lxml でパースして画面部品を検証

    Selenium でブラウザテスト
    コードが断片化しやすくテストがしづらい

    View Slide

  74. ポイント①:どんなカスタマイズが簡単にできるのか分からない
    ポイント②:テンプレートを修正するのにコツが要る
    ポイント③:画面のスタイルを変えるのが大変
    ポイント④:コードが断片化しやすくテストがしづらい
    ポイント⑤:日本語の情報が少ない
    73
    まとまった日本語の
    情報が少ない
    いい本がないものか・・

    View Slide

  75. ポイント⑤ 解決策 😉
    『現場で使える Django 管理サイトのつくり方』
    74
    74
    【 Amazon 】 【 BOOTH 】
    (電子版) (ペーパー版)
    • 管理サイトだけにフォーカスした、ニッチでオンリーワンな一冊
    • 本文 152ページ
    • 2020年9月発行、Django 2.2 LTS 対応

    View Slide

  76. ポイント⑤ まとめ
    75
    『現場で使える Django 管理サイトのつくり方』を現場に一冊!
    日本語の情報が少ない

    View Slide

  77. まとめ
    76

    View Slide

  78. このトークで話したこと
    77
    管理サイトをカスタマイズするにあたって
    苦労するポイントとその解決策

    View Slide

  79. まとめ
    ポイント①:どんなカスタマイズが簡単にできるのか分からない
    ポイント②:テンプレートを修正するのにコツが要る
    ポイント③:画面のスタイルを変えるのが大変
    ポイント④:コードが断片化しやすくテストがしづらい
    ポイント⑤:日本語の情報が少ない
    78

    View Slide

  80. まとめ
    79
    ポイント①:どんなカスタマイズが簡単にできるのか分からない
    ポイント②:テンプレートを修正するのにコツが要る
    ポイント③:画面のスタイルを変えるのが大変
    ポイント④:コードが断片化しやすくテストがしづらい
    ポイント⑤:日本語の情報が少ない
    全体構造(AdminSiteとModelAdmin)を把握しよう
    テンプレートの優先順位と継承を使ってカスタマイズしよう
    CSSファイルを追加する仕組みを理解しよう
    lxml や Selenium を使おう
    『現場で使える Django 管理サイトのつくり方』を読もう

    View Slide

  81. サンプルコード
    80
    https://github.com/akiyoko/djangoconjp2021-customize-admin
    DjangoCongress JP 2021 発表用サンプルコード

    View Slide

  82. 宣伝
    (電子版・紙の本)
    『現場で使えるDjango の教科書
    《基礎編》』
    『現場で使えるDjango の教科書
    《実践編》』
    (電子版) (紙の本)
    『現場で使える
    Django REST Framework
    の教科書』
    (紙の本)
    (紙の本)
    81
    【 Amazon 】 【 BOOTH 】
    (電子版)
    技術書典11(7/10~)にて 3.2 対応の改訂版を頒布予定

    View Slide