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

Django 管理サイトをカスタマイズする前に教えてほしかったこと / How to customize admin (DjangoCon JP 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. 目次 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 )
  2. 自己紹介 名 前 : 横瀬 明仁(akiyoko) 所 属 : 株式会社

    ワングリット Twitter : @aki_yok ブログ : akiyoko blog(https://akiyoko.hatenablog.jp/) Django 歴 : 8年くらい 過去の発表 : 「現場で使える Django のセキュリティ対策」 (DjangoCongress JP 2019) 自費出版本 : 3 『現場で使える Django の教科書《基礎編》』 『現場で使える Django の教科書《実践編》』 『現場で使える Django REST Framework の教科書』 『現場で使える Django 管理サイトのつくり方』
  3. 開発者からの評価も高い 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
  4. 管理サイトとは 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 組み込みのアプリケーション
  5. 基本仕様 画面遷移 画面遷移 13 [<アプリ [<モデル名>] [追加] [追加] [<モデル名> [パスワードの変更]

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

    Book ⇒ Book モデルの追加が可能 ⇒ Book モデルの参照が可能 ⇒ Book モデルの削除が可能 ⇒ Book モデルの変更が可能 モデル 操作 パーミッション × = • モデルの CRUD 操作を実行するためには、モデルごと・操作ごとの「パーミッション」と呼ばれ る権限が必要 • ユーザーの追加画面・変更画面から「ユーザーパーミッション」として紐付けることが可能 • is_superuser が True のユーザーは、すべてのパーミッションがあるとみなされる パーミッション(認可とアクセス制御) グループ ユーザー パーミッション ( User ) ( Group ) ( Permission )
  7. ポイント① 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 クラス
  8. ポイント① 管理サイトの全体構造 19 View View View View モデル ModelAdmin View

    View 管理サイト用 テンプレート AdminSite View View モデル フォーム U R L デ ィ ス パ ッ チ ャ /admin/<アプリケーション名>/<モデル名>/… の URLパターンには ModelAdmin が対応 /admin/… の URLパターンには AdminSite が対応 全体の画面項目や挙動 ⇒ AdminSite クラスに定義 モデルごとの画面項目や挙動 ⇒ ModelAdmin クラスに定義 全体構造
  9. ポイント① 管理サイトのカスタマイズ難易度 20 簡単! コツが必要! ⇒ ポイント② ③ で説明 1)

    AdminSite を利用して、全体の設定をカスタマイズ 2) ModelAdmin を利用して、モデルごとの画面項目をカスタマイズ 3) テンプレートの仕組みを利用して、テンプレートをカスタマイズ 4) 静的ファイルの仕組みを利用して、CSS をカスタマイズ
  10. ポイント① 1) AdminSite の主要なクラス変数・メソッド (1/2) 22 # クラス変数 説明 1

    site_header 共通ヘッダの左側に表示されるサイト名。デフォルト値は「Django 管理サイト」 2 site_title <title> タグの一部として表示される値。デフォルト値は「Django サイト管理」 3 site_url 共通ヘッダの右側に表示される「サイトを表示」のリンク先の URL。None をセット するとリンク自体が非表示になる。デフォルト値は「/」 4 login_template ログイン画面のテンプレートのパス 5 login_form ログインビューで使われるフォームクラス 6 logout_template ログアウト完了画面のテンプレートのパス 7 index_title ホーム画面の <h1> タグに表示されるタイトル。デフォルト値は「サイト管理」 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 の主要なクラス変数
  11. ポイント① 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
  12. ポイント① 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
  13. ポイント① 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 の主要なクラス変数(モデル一覧画面用)
  14. ポイント① 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
  15. ポイント① 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
  16. ポイント① 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
  17. ポイント① 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)
  18. ポイント① 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)
  19. ポイント① 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)
  20. ポイント① 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
  21. ポイント② 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 ★ 優先順位 ③
  22. ポイント② 2) テンプレートの継承 40 django/contrib/admin/templates/admin/base.html(管理サイト本体側) <ベースディレクトリ>/templates/admin/base.html ...(略)... {% block welcome-msg

    %} {% translate 'Welcome,' %} <strong>{% firstof user.get_short_name user.get_username %}</strong>. {% endblock %} ...(略)... テンプレートを extends タグで 継承して、特定の block タグの 内容を書き換えることが可能 {% extends "admin/base.html" %} {% block welcome-msg %} {% firstof user.get_short_name user.get_username %} としてログイン中 {% endblock %}
  23. ポイント② 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
  24. ポイント② 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
  25. ポイント② 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」パネルを利用すれ ば簡単に確認可能 メイン
  26. ポイント② テンプレートのカスタマイズ方針 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
  27. ポイント② テンプレートのカスタマイズ例 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'], ... }, ]
  28. ポイント③ 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 ★ 優先順位 ③
  29. ポイント③ 2) テンプレート継承で CSS ファイルを追加 (1/2) 53 django/contrib/admin/templates/admin/base.html 管理サイト本体の base.html

    の extrastyle ブロックをオーバーライドする <head> ... <link rel="stylesheet" type="text/css" href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}"> ... {% block extrastyle %}{% endblock %} ... {% block extrahead %}{% endblock %} {% block responsive %} ... <link rel="stylesheet" type="text/css" href="{% static "admin/css/responsive.css" %}"> ... {% endblock %} ... </head> モバイルおよびタブレット用のスタイル定義 管理サイト全体の基本スタイルが定義されている 各画面のメインコンテンツのテンプレートからオーバーライドして追 加の CSSファイルを読み込むために用意されたブロック。例えば、 「admin/login.html」では「admin/css/login.css」が読み込まれる モデルごとの画面のテンプレートからオーバーライドして 任意の静的ファイルを読み込むために用意されたブロック。 後述する ModelAdmin の Media.css でも利用される
  30. ポイント③ 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 %} <link rel="stylesheet" type="text/css" href="{% static 'admin/css/base_extra.css' %}"> {% endblock %} CSS admin/css/base_extra.css プロジェクト内の静的ファイル (「STATICFILES_DIRS」ディレクトリ) CSS admin/css/base.css など CSS CSS CSS 管理サイト本体の静的ファイル
  31. ポイント③ 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 %} <link rel="stylesheet" type="text/css" href="{% static 'admin/css/base_extra.css' %}"> {% endblock %} <設定ディレクトリ>/settings.py STATICFILES_DIRS = [BASE_DIR / 'static'] base.html や base_site.html 以外のテンプレート を継承する場合は、{{ block.super }} を使って親テ ンプレートの内容を残しつつ、新たなCSSファイル を読み込むための記述をするように注意!
  32. ポイント③ 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 ブロック)で読み込まれる
  33. ポイント③ 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']
  34. ポイント④ 困りごと 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) 品質を担保するためには どんなテストをすればいいの?
  35. ポイント④ 1) lxml を使って画面部品を検証 lxml は、HTML や XML を解析するためのライブラリ 64

    response HTMLElement HTMLElement HTMLElement rendered_content (HTML文字列) Django 標準の TestCase クラスのテストクライアントで取得したレスポンスの rendered_content 属性 でレンダリング後の HTML が取れるので、それを lxml でパースすれば画面項目の検証が可能 管理サイト側の HTML(要素の構造や class 属性・id 属性など)は固定されているため、 画面側の都合でテストコードを書き直さなくて済む
  36. ポイント④ 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日'])
  37. ポイント④ 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]
  38. ポイント④ 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 パッケージが利用可能
  39. ポイント④ 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サーバを起動してくれる
  40. ポイント④ 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) (次ページに続く)
  41. ポイント④ 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') # (以下略) (前ページから続く)
  42. ポイント⑤ 解決策 😉 『現場で使える Django 管理サイトのつくり方』 74 74 【 Amazon

    】 【 BOOTH 】 (電子版) (ペーパー版) • 管理サイトだけにフォーカスした、ニッチでオンリーワンな一冊 • 本文 152ページ • 2020年9月発行、Django 2.2 LTS 対応
  43. 宣伝 (電子版・紙の本) 『現場で使えるDjango の教科書 《基礎編》』 『現場で使えるDjango の教科書 《実践編》』 (電子版) (紙の本)

    『現場で使える Django REST Framework の教科書』 (紙の本) (紙の本) 81 【 Amazon 】 【 BOOTH 】 (電子版) 技術書典11(7/10~)にて 3.2 対応の改訂版を頒布予定