Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

目次 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 )

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

開発者からの評価も高い 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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

管理サイトとは 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 組み込みのアプリケーション

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

ポイント① 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 クラス

Slide 20

Slide 20 text

ポイント① 管理サイトの全体構造 19 View View View View モデル ModelAdmin View View 管理サイト用 テンプレート AdminSite View View モデル フォーム U R L デ ィ ス パ ッ チ ャ /admin/<アプリケーション名>/<モデル名>/… の URLパターンには ModelAdmin が対応 /admin/… の URLパターンには AdminSite が対応 全体の画面項目や挙動 ⇒ AdminSite クラスに定義 モデルごとの画面項目や挙動 ⇒ ModelAdmin クラスに定義 全体構造

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

ポイント① 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 の主要なクラス変数

Slide 24

Slide 24 text

ポイント① 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

Slide 25

Slide 25 text

ポイント① 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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

ポイント① 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 の主要なクラス変数(モデル一覧画面用)

Slide 28

Slide 28 text

ポイント① 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

Slide 29

Slide 29 text

ポイント① 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

Slide 30

Slide 30 text

ポイント① 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

Slide 31

Slide 31 text

ポイント① 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)

Slide 32

Slide 32 text

ポイント① 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)

Slide 33

Slide 33 text

ポイント① 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)

Slide 34

Slide 34 text

ポイント① 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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

ポイント② 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 ★ 優先順位 ③

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

ポイント② 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 %}

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

ポイント② 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

Slide 44

Slide 44 text

ポイント② 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

Slide 45

Slide 45 text

ポイント② 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」パネルを利用すれ ば簡単に確認可能 メイン

Slide 46

Slide 46 text

ポイント② テンプレートのカスタマイズ方針 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

Slide 47

Slide 47 text

ポイント② テンプレートのカスタマイズ例 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'], ... }, ]

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

ポイント③ 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 ★ 優先順位 ③

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

ポイント③ 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 でも利用される

Slide 55

Slide 55 text

ポイント③ 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 %} {% endblock %} CSS admin/css/base_extra.css プロジェクト内の静的ファイル (「STATICFILES_DIRS」ディレクトリ) CSS admin/css/base.css など CSS CSS CSS 管理サイト本体の静的ファイル

Slide 56

Slide 56 text

ポイント③ 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ファイル を読み込むための記述をするように注意!

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

ポイント③ 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 ブロック)で読み込まれる

Slide 59

Slide 59 text

ポイント③ 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']

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

ポイント④ 困りごと 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) 品質を担保するためには どんなテストをすればいいの?

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

ポイント④ 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日'])

Slide 67

Slide 67 text

ポイント④ 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]

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

ポイント④ 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 パッケージが利用可能

Slide 70

Slide 70 text

ポイント④ 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サーバを起動してくれる

Slide 71

Slide 71 text

ポイント④ 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) (次ページに続く)

Slide 72

Slide 72 text

ポイント④ 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') # (以下略) (前ページから続く)

Slide 73

Slide 73 text

ポイント④ まとめ 72 生成される HTMLを lxml でパースして画面部品を検証 + Selenium でブラウザテスト コードが断片化しやすくテストがしづらい

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

まとめ 76

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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