こんにちは。sinyです。
この記事はDjango Advent Calendar 2022の17日目の記事です。
Django admin画面上でCRUDの詳細ページを実装する方法を紹介します。
ここでは、下図のような注文者毎に購入した注文明細情報一覧を表示するような詳細ページを実装する手順をご紹介します。
事前準備
まず、仮想環境を作成&アクティベートして必要なモジュールのインストールとDjangoプロジェクト、アプリケーション作成まで完了させましょう。
python -m venv env env\scripts\activate
Django本体と画像ファイルを扱うためPillowをインストールします。
pip install django Pillow
続いてDjangoプロジェクトを作成しましょう。
django-admin startproject config .
次にproductsという名前のアプリケーションを作成します。
python manage.py startapp products
config/settings.py
で以下の設定を行います。
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'products.apps.ProductsConfig', #追加 ] LANGUAGE_CODE = 'ja' #変更 TIME_ZONE = 'Asia/Tokyo' #変更 #以下を追加 MEDIA_URL = 'media/' MEDIA_ROOT = BASE_DIR / 'media/'
モデルの定義
製品と注文、注文明細データを管理するモデルを以下の通り定義します。
from django.contrib.auth.models import User from django.db import models class Product(models.Model): class Meta: verbose_name = "製品" verbose_name_plural = "製品" title = models.CharField(max_length=255) price = models.IntegerField() image = models.ImageField() def __str__(self): return self.title class Order(models.Model): class Meta: verbose_name = "注文" verbose_name_plural = "注文" user = models.ForeignKey(User, on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.user} {self.created.date()}" class OrderItem(models.Model): class Meta: verbose_name = "注文明細" verbose_name_plural = "注文明細" order = models.ForeignKey(Order, on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE) count = models.PositiveIntegerField() def __str__(self): return f"{self.product} x {self.count}"
Productクラスには製品のタイトル(title)、値段(price)、製品の画像(imageフィールドを定義します。
Orderクラスはユーザの注文データを格納するモデルクラスで、ユーザ名(user)と注文日時(cerated)フィールドを定義します。
最後に注文明細を管理するためのOrderItemクラスを定義します。
フィールドは注文データ(Orderクラスを外部参照)、製品(Productクラスを外部参照)、注文数(count)です。
OrderItemはあるユーザが注文した製品の個数(注文明細)を製品毎に1レコード登録するような構成を想定しています。
モデルの定義が終わったので一旦マイグレーションを行っておきましょう。
python manage.py makemigrations Migrations for 'products': products\migrations\0001_initial.py - Create model Order - Create model Product - Create model OrderItem
python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, products, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying auth.0012_alter_user_first_name_max_length... OK Applying products.0001_initial... OK Applying sessions.0001_initial... OK
また、管理者ユーザも作成しておきましょう。
python manage.py createsuperuser ユーザー名 (leave blank to use 'sinfo'): admin メールアドレス: Password: Password (again): Superuser created successfully.
Adminサイトの初期設定
続いてadminサイトにモデルを表示させるため`products/admin.py`を以下の通り設定しましょう。
from django.contrib import admin from products.models import Product, OrderItem, Order @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ['title', 'price'] class OrderItemInline(admin.TabularInline): model = OrderItem @admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display = ['user', 'created'] inlines = [OrderItemInline] @admin.register(OrderItem) class OrderItemAdmin(admin.ModelAdmin): list_display = ['order', 'product', 'count']
OrderItemInlinをOrderAdminのinlinesに指定することで注文データ(Order)を登録する際に複数の注文明細(製品と個数)を一括で登録できるようにしています。
開発サーバを起動してadminサイトにログオンします。
python manage.py runserver Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). December 12, 2022 - 06:28:03 Django version 3.2.4, using settings 'config.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CTRL-BREAK.
以下の通り「注文、注文明細、製品」が表示されるので、まずはサンプルの注文データを登録しましょう。
以下の様に複数のユーザ、製品を登録し、いくつか注文データも登録しておきましょう。
Productには製品の画像も適宜指定しておきましょう。
adminサイト上で画像表示を行えるようにconfig/urls.py
に管理サイトの URL 構成を追加します。
from django.contrib import admin from django.urls import path from django.conf import settings #追加 from django.conf.urls.static import static #追加 urlpatterns = [ path('admin/', admin.site.urls), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) #追加
注文概要のページの作成
adminサイトの「注文」をクリックすると、現状は以下の通りユーザ名と注文作成日の一覧情報だけが表示されている状態になっています。
ここに詳細ページへのリンクを追加してユーザ毎に注文した注文明細データを一覧表示する注文概要ページを追加します。
まず、products/admin.py
に注文の詳細ページを表示させるためのOrderDetailViewクラスを以下の通り追加します。
from django.views.generic.detail import DetailView class OrderDetailView(DetailView): template_name = "admin/products/order/detail.html" model = Order
次に、注文概要ページへリンクするURLパターンを追加します。
adminサイト内で独自のURLパターンを追加するにはModelAdminクラス内に定義されているget_urlsメソッドを使います。
以下の様にOrderAdminクラス内にget_urls メソッドを追加して、注文(Order)の詳細データを表示するURLパターンを追加します。
from django.urls import path, reverse #追加 from django.utils.html import format_html #追加 @admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display = ['user', 'created','order_detail'] # order_detailを追加 inlines = [OrderItemInline] #ここから下を追加 def get_urls(self): urls = super().get_urls() # defaultのadmin urlをロード detail_urls = [ path("<pk>/order_detail",self.admin_site.admin_view(OrderDetailView.as_view()),name=f"products_order_detail",) ] return detail_urls + urls def order_detail(self, obj): url = reverse("admin:products_order_detail", args=[obj.pk]) return format_html(f'<a href="{url}">📝</a>') #ここまでを追加
具体的には、detail_urlsというURLパターン変数を新たに定義し、その中にpathメソッドでOrderDetailViewクラスを呼び出すURLパターンを定義します。
path("<pk>/detail",self.admin_site.admin_view(OrderDetailView.as_view()),name=f"products_order_detail",)
上記が注文概要ページへリンクするURLパターンで「http://127.0.0.1:8000/admin/products/order/<pk>/order_detail」といったURL設計になります。
※ get_urls で定義したURLは 「admin/アプリ名/モデル名/URLパターン」という設計になります。
また、が注文概要ページへリンク列をorder_detailとして定義しているのが以下の部分です。
from django.urls import path, reverse #reverseを追加 from django.utils.html import format_html #追加 def order_detail(self, obj): url = reverse("admin:products_order_detail", args=[obj.pk]) return format_html(f'<a href="{url}">📝</a>')
order_detailという名称でメソッドを定義し、引数にobjを指定します。
reverseメソッドにURLパターンとargsにリンク先のOrderインスタンスのpk番号を指定しリンク先となる情報をurl変数に格納します。
最後にreturnでformat_htmlを使ってhtml形式で注文概要ページへのリンクを戻り値とします。
そして、OrderAdminクラスのlist_display にorder_detailを追加することでadminサイトの「注文」のページにorder_detail列が追加されます。
adminサイト画面を更新して「注文」画面を開くと以下の通りORDER_DETAIL列が追加されます。
試しにORDER_DETAIL列をクリックすると以下のようなエラーが表示されると思います。
TemplateDoesNotExist at /admin/products/order/3/order_detail
admin/products/order/detail.html
このエラーはリンク先となる注文概要ページのテンプレート(detail.html)が存在しないためです。
そこでproducts/templates/admin/products/order/detail.html
を以下の通り作成しましょう。
{% extends 'admin/base_site.html' %} {% block content %} <h1>Order by {{ object.user }} {{ object.created.date }}</h1> <dl> <dt>氏名:</dt><dd>{{ object.user.username }}</dd> <dt>メールアドレス:</dt><dd>{{ object.user.email }}</dd> </dl> {% endblock %}
再度、adminサイト画面を更新して「注文」画面のORDER_DETAIL列のリンクボタンを押してみましょう。
「http://127.0.0.1:8000/admin/products/order/<pk>/order_detail」に遷移して以下のような注文概要ページが表示されます。
現時点では、注文者の氏名とメールアドレスだけが表示されるようになっています。
このページに注文した製品と注文数とその合計金額、さらに複数商品を注文した場合のTotalの合計金額を表示させるようにします。
products/models.py
を開いてOrderクラスに注文合計金額を算出するプロパティメソッド(total_amount)を、OrderItemクラス内に1明細あたりの合計金額を算出するプロパティメソッド(total_amount)を追加します。
class Order(models.Model): class Meta: verbose_name = "注文" verbose_name_plural = "注文" user = models.ForeignKey(User, on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.user} {self.created.date()}" #ここから下を追加 @property def total_amount(self): return sum([item.total_amount for item in self.orderitem_set.all()]) class OrderItem(models.Model): class Meta: verbose_name = "注文明細" verbose_name_plural = "注文明細" order = models.ForeignKey(Order, on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE) count = models.PositiveIntegerField() def __str__(self): return f"{self.product} x {self.count}" #ここから下を追加 @property def total_amount(self): return self.product.price * self.count
最後にdetail.htmlを以下の通り修正します。
{% extends 'admin/base_site.html' %} {% block content %} <h1>Order by {{ object.user }} {{ object.created.date }}</h1> <dl> <dt>氏名:</dt> <dd>{{ object.user.username }}</dd> <dt>メールアドレス:</dt> <dd>{{ object.user.email }}</dd> </dl> <!-- ここから下を追加 --> <table style="width: 100%; margin-bottom: 10px;"> <thead> <tr> <td></td> <td>製品名</td> <td>価格</td> <td>注文数量</td> <td>小計金額</td> </tr> </thead> <tbody> {% for item in object.orderitem_set.all %} <tr> <td><img src="{{ item.product.image.url }}" height="50"></td> <td>{{ item.product.title }}</td> <td>{{ item.product.price }}円</td> <td>x {{ item.count }}</td> <td>{{ item.total_amount }}円</td> </tr> {% endfor %} </tbody> </table> <p style="font-size: 18px;text-align: end;padding-right: 60px;"> 合計金額: {{ object.total_amount }}円 </p> <!-- ここまでを追加 --> {% endblock %}
以下の通り、注文画面のORDER DETAIL列のリンクアイコンをクリックすると該当ユーザが注文した注文明細の一覧と合計金額が表示されるようになります。
以上、「Django admin画面で詳細ページを実装する方法」のご紹介でした。
参考になれば幸いです。