From a972018bac7b837a56e1da3985c7f3814843fbe3 Mon Sep 17 00:00:00 2001 From: Takeru SHIINA Date: Sat, 10 Feb 2024 01:51:28 +0000 Subject: [PATCH 01/12] Change admin site header and title --- tsuke/admin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tsuke/admin.py b/tsuke/admin.py index 7c164a1..67849bc 100644 --- a/tsuke/admin.py +++ b/tsuke/admin.py @@ -11,5 +11,8 @@ class TsukeAdmin(admin.ModelAdmin): list_display = ["purchase_date", "amount", "user", "note", "payment_date"] +admin.site.site_header = "Tsukepp 管理画面" +admin.site.index_title = "メニュー" + admin.site.register(ItemCategory, ItemCategoryAdmin) admin.site.register(Tsuke, TsukeAdmin) From 57508fdbf8e4e091e9182525bb49324c0de4813e Mon Sep 17 00:00:00 2001 From: Takeru SHIINA Date: Sat, 10 Feb 2024 03:42:28 +0000 Subject: [PATCH 02/12] Add Tsuke aggregation page to admin --- tsuke/admin.py | 28 ++++++++++++++++++- tsuke/migrations/0008_tsuketotal.py | 24 ++++++++++++++++ tsuke/models.py | 10 +++++++ .../admin/tsuketotal_change_list.html | 27 ++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 tsuke/migrations/0008_tsuketotal.py create mode 100644 tsuke/templates/admin/tsuketotal_change_list.html diff --git a/tsuke/admin.py b/tsuke/admin.py index 67849bc..bf178bc 100644 --- a/tsuke/admin.py +++ b/tsuke/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin +from django.db.models import Sum -from .models import ItemCategory, Tsuke +from .models import ItemCategory, Tsuke, TsukeTotal class ItemCategoryAdmin(admin.ModelAdmin): @@ -10,9 +11,34 @@ class ItemCategoryAdmin(admin.ModelAdmin): class TsukeAdmin(admin.ModelAdmin): list_display = ["purchase_date", "amount", "user", "note", "payment_date"] +class TsukeTotalAdmin(admin.ModelAdmin): + list_display = ["user"] + change_list_template = "admin/tsuketotal_change_list.html" + + def changelist_view(self, request, extra_context=None): + response = super().changelist_view(request, extra_context) + try: + qs = response.context_data["cl"].queryset + except (AttributeError, KeyError): + return response + metrics = { + "total_amount": Sum("amount"), + } + + # ユーザ一覧を取得 + user_list = [tsuke.user for tsuke in qs] + # ユーザごとの合計金額を取得 + total_amount = { + user: qs.filter(user=user).aggregate(**metrics)["total_amount"] + for user in user_list + } + + response.context_data["summary"] = total_amount + return response admin.site.site_header = "Tsukepp 管理画面" admin.site.index_title = "メニュー" admin.site.register(ItemCategory, ItemCategoryAdmin) admin.site.register(Tsuke, TsukeAdmin) +admin.site.register(TsukeTotal, TsukeTotalAdmin) diff --git a/tsuke/migrations/0008_tsuketotal.py b/tsuke/migrations/0008_tsuketotal.py new file mode 100644 index 0000000..8c37d76 --- /dev/null +++ b/tsuke/migrations/0008_tsuketotal.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.6 on 2024-02-10 02:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("tsuke", "0007_alter_tsuke_payment_date"), + ] + + operations = [ + migrations.CreateModel( + name="TsukeTotal", + fields=[], + options={ + "verbose_name": "ツケ合計", + "verbose_name_plural": "ツケ合計", + "proxy": True, + "indexes": [], + "constraints": [], + }, + bases=("tsuke.tsuke",), + ), + ] diff --git a/tsuke/models.py b/tsuke/models.py index b135bf9..a467146 100644 --- a/tsuke/models.py +++ b/tsuke/models.py @@ -20,6 +20,7 @@ class Meta: def __str__(self): return str(self.category) + class Tsuke(models.Model): """1回のツケ""" @@ -40,3 +41,12 @@ class Meta: def __str__(self): return f"{self.amount}円({self.category})" + + +class TsukeTotal(Tsuke): + """各ユーザのツケの合計額を表示する""" + + class Meta: + proxy = True + verbose_name = "集計" + verbose_name_plural = "集計" diff --git a/tsuke/templates/admin/tsuketotal_change_list.html b/tsuke/templates/admin/tsuketotal_change_list.html new file mode 100644 index 0000000..af25e9b --- /dev/null +++ b/tsuke/templates/admin/tsuketotal_change_list.html @@ -0,0 +1,27 @@ +

集計

+ +{% extends "admin/change_list.html" %} + + +{% block content_title %} +

ユーザごとのツケ合計額

+{% endblock %} + +{% block result_list %} + + + + + + + + + {% for user, amount in summary.items %} + + + + + {% endfor %} + +
ユーザ合計
{{ user }}{{ amount }}
+{% endblock %} From aec2addc550bed65adddf6a1ccc6a6cf926ef03d Mon Sep 17 00:00:00 2001 From: Takeru SHIINA Date: Sat, 10 Feb 2024 06:59:14 +0000 Subject: [PATCH 03/12] Customize headers in admin pages --- tsuke/admin.py | 32 ++++++++++++++++++- .../admin/itemcategory_change_list.html | 7 ++++ tsuke/templates/admin/tsuke_change_form.html | 7 ++++ tsuke/templates/admin/tsuke_change_list.html | 28 ++++++++++++++++ .../admin/tsuketotal_change_list.html | 9 ++---- 5 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 tsuke/templates/admin/itemcategory_change_list.html create mode 100644 tsuke/templates/admin/tsuke_change_form.html create mode 100644 tsuke/templates/admin/tsuke_change_list.html diff --git a/tsuke/admin.py b/tsuke/admin.py index bf178bc..27e52f7 100644 --- a/tsuke/admin.py +++ b/tsuke/admin.py @@ -1,20 +1,50 @@ from django.contrib import admin from django.db.models import Sum +from django.http.request import HttpRequest +from django.template.response import TemplateResponse from .models import ItemCategory, Tsuke, TsukeTotal class ItemCategoryAdmin(admin.ModelAdmin): list_display = ["category"] + change_list_template = "admin/itemcategory_change_list.html" class TsukeAdmin(admin.ModelAdmin): - list_display = ["purchase_date", "amount", "user", "note", "payment_date"] + list_display = ["purchase_date", "amount", "user", "note", "is_paid", "payment_date"] + readonly_fields = [field.name for field in Tsuke._meta.fields] + list_filter = ["user", "category", "is_paid"] + change_list_template = "admin/tsuke_change_list.html" # 一覧画面 + change_form_template = "admin/tsuke_change_form.html" # 詳細画面 + + def has_add_permission(self, *args, **kwargs): + return False + + def changelist_view(self, request, extra_context=None): + response = super().changelist_view(request, extra_context) + try: + qs = response.context_data["cl"].get_queryset(request) + except (AttributeError, KeyError): + return response + + # フィルタ条件を取得 + filters = { + param: request.GET.getlist(param) for param in request.GET + if not param.startswith("_") + } + + response.context_data['filters'] = filters + return response + class TsukeTotalAdmin(admin.ModelAdmin): list_display = ["user"] change_list_template = "admin/tsuketotal_change_list.html" + def has_add_permission(self, *args, **kwargs): + return False + def changelist_view(self, request, extra_context=None): response = super().changelist_view(request, extra_context) try: diff --git a/tsuke/templates/admin/itemcategory_change_list.html b/tsuke/templates/admin/itemcategory_change_list.html new file mode 100644 index 0000000..4ff4eac --- /dev/null +++ b/tsuke/templates/admin/itemcategory_change_list.html @@ -0,0 +1,7 @@ +{% extends "admin/change_list.html" %} + +{% block content_title %} +

品目一覧

+{% endblock %} + + diff --git a/tsuke/templates/admin/tsuke_change_form.html b/tsuke/templates/admin/tsuke_change_form.html new file mode 100644 index 0000000..c7d8eea --- /dev/null +++ b/tsuke/templates/admin/tsuke_change_form.html @@ -0,0 +1,7 @@ +{% extends "admin/change_form.html" %} + +{% block content_title %} +

ツケ詳細

+{% endblock %} + + diff --git a/tsuke/templates/admin/tsuke_change_list.html b/tsuke/templates/admin/tsuke_change_list.html new file mode 100644 index 0000000..e91f5e6 --- /dev/null +++ b/tsuke/templates/admin/tsuke_change_list.html @@ -0,0 +1,28 @@ +{% extends "admin/change_list.html" %} + +{% block content_title %} +

+ ツケ一覧 + + {% for filter in cl.filter_specs %} + {% if filter.lookup_choices and filter.lookup_val %} + | {{ filter.lookup_title }} = + {% for choice in filter.lookup_choices %} + {% if choice.0|stringformat:"s" == filter.lookup_val %} + {{ choice.1 }} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + {% if "is_paid__exact" in filters %} + {% if filters.is_paid__exact.0 == "0" %} + | 未清算 + {% elif filters.is_paid__exact.0 == "1" %} + | 清算済 + {% endif %} + {% endif %} +

+{% endblock %} + + diff --git a/tsuke/templates/admin/tsuketotal_change_list.html b/tsuke/templates/admin/tsuketotal_change_list.html index af25e9b..7b75c28 100644 --- a/tsuke/templates/admin/tsuketotal_change_list.html +++ b/tsuke/templates/admin/tsuketotal_change_list.html @@ -1,10 +1,7 @@ -

集計

- {% extends "admin/change_list.html" %} - {% block content_title %} -

ユーザごとのツケ合計額

+

未清算ツケ金額の一覧

{% endblock %} {% block result_list %} @@ -12,13 +9,13 @@

ユーザごとのツケ合計額

ユーザ - 合計 + 未精算額合計 {% for user, amount in summary.items %} - {{ user }} + {{ user }} {{ amount }} {% endfor %} From e9b4487fa7450d5fcc18908fc77f645f56183cb4 Mon Sep 17 00:00:00 2001 From: Takeru SHIINA Date: Sat, 10 Feb 2024 07:14:49 +0000 Subject: [PATCH 04/12] Add comments --- tsuke/admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tsuke/admin.py b/tsuke/admin.py index 27e52f7..66c0553 100644 --- a/tsuke/admin.py +++ b/tsuke/admin.py @@ -19,6 +19,7 @@ class TsukeAdmin(admin.ModelAdmin): change_form_template = "admin/tsuke_change_form.html" # 詳細画面 def has_add_permission(self, *args, **kwargs): + """ツケは登録ページからしか追加できない""" return False def changelist_view(self, request, extra_context=None): @@ -43,6 +44,7 @@ class TsukeTotalAdmin(admin.ModelAdmin): change_list_template = "admin/tsuketotal_change_list.html" def has_add_permission(self, *args, **kwargs): + """ツケの合計は変更できない""" return False def changelist_view(self, request, extra_context=None): From d84637e48015109f3c6ba36c7230d2743ff21d58 Mon Sep 17 00:00:00 2001 From: Takeru SHIINA Date: Sat, 10 Feb 2024 07:14:57 +0000 Subject: [PATCH 05/12] Change header of Tsuke-total page --- tsuke/templates/admin/tsuketotal_change_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsuke/templates/admin/tsuketotal_change_list.html b/tsuke/templates/admin/tsuketotal_change_list.html index 7b75c28..a38119a 100644 --- a/tsuke/templates/admin/tsuketotal_change_list.html +++ b/tsuke/templates/admin/tsuketotal_change_list.html @@ -1,7 +1,7 @@ {% extends "admin/change_list.html" %} {% block content_title %} -

未清算ツケ金額の一覧

+

未清算のツケの合計額(ユーザごと)

{% endblock %} {% block result_list %} From 6cb27b3b8db77a76ede30a153e667795f1cef970 Mon Sep 17 00:00:00 2001 From: Takeru SHIINA Date: Sat, 10 Feb 2024 07:19:56 +0000 Subject: [PATCH 06/12] Remove sample test --- tsuke/tests/test_sample.py | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 tsuke/tests/test_sample.py diff --git a/tsuke/tests/test_sample.py b/tsuke/tests/test_sample.py deleted file mode 100644 index ceb67d9..0000000 --- a/tsuke/tests/test_sample.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.test import TestCase - - -class TestCi(TestCase): - - def test(self): - self.assertEqual(1, 1) From f645f10e0814a5918b511e6a6cdd059bc90a304e Mon Sep 17 00:00:00 2001 From: Takeru SHIINA Date: Sat, 10 Feb 2024 09:06:31 +0000 Subject: [PATCH 07/12] Fix aggregation err --- tsuke/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsuke/admin.py b/tsuke/admin.py index 66c0553..cc70e27 100644 --- a/tsuke/admin.py +++ b/tsuke/admin.py @@ -61,7 +61,7 @@ def changelist_view(self, request, extra_context=None): user_list = [tsuke.user for tsuke in qs] # ユーザごとの合計金額を取得 total_amount = { - user: qs.filter(user=user).aggregate(**metrics)["total_amount"] + user: qs.filter(user=user, is_paid=False).aggregate(**metrics)["total_amount"] or 0 for user in user_list } From 1e2484ca5c40f99219291f9234b3f1ce341bac57 Mon Sep 17 00:00:00 2001 From: Takeru SHIINA Date: Sat, 10 Feb 2024 09:06:55 +0000 Subject: [PATCH 08/12] Add test cases --- tsuke/tests/test_account.py | 69 +++++++++++++++++++++++++++++++++++++ tsuke/tests/test_admin.py | 46 +++++++++++++++++++++++++ tsuke/tests/test_views.py | 41 +--------------------- 3 files changed, 116 insertions(+), 40 deletions(-) create mode 100644 tsuke/tests/test_account.py create mode 100644 tsuke/tests/test_admin.py diff --git a/tsuke/tests/test_account.py b/tsuke/tests/test_account.py new file mode 100644 index 0000000..553a1b9 --- /dev/null +++ b/tsuke/tests/test_account.py @@ -0,0 +1,69 @@ +"""テスト用のアカウントやログイン状態を定義するモジュール""" + +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse_lazy + + +class LoggedInTestCase(TestCase): + """ + ログイン状態を定義するテストケース + (「動かして学ぶ!Python Django開発入門」p.286より) + """ + def setUp(self): + """テストメソッド実行前の事前設定""" + self.username = "testa" + self.password = 'xyab2023' + self.SUBMIT_TOKEN = "test_token" + + self.test_user = get_user_model().objects.create_user( + username=self.username, + password=self.password, + ) + + self.client.login( + username=self.username, + password=self.password + ) + + def set_pseudo_token(self): + """疑似submit tokenをセッションに格納する""" + sess = self.client.session + sess["submit_token"] = self.SUBMIT_TOKEN + sess.save() + + +class SuperuserLoggedInTestCase(LoggedInTestCase): + """管理サイトのログイン状態を定義するクラス""" + def setUp(self): + """テストメソッド実行前の事前設定""" + self.username = "admin" + self.password = 'admin2023' + self.SUBMIT_TOKEN = "test_token" + + self.test_user = get_user_model().objects.create_superuser( + username=self.username, + email="sample@example.com", + password=self.password + ) + + self.client.login( + username=self.username, + password=self.password + ) + + def set_pseudo_token(self): + """疑似submit tokenをセッションに格納する""" + sess = self.client.session + sess["submit_token"] = self.SUBMIT_TOKEN + sess.save() + + +class TestAccount(LoggedInTestCase): + """アカウント機能のテストクラス""" + def test_login_ok(self): + """すでにログイン状態であることの確認""" + response = self.client.get( + '/accounts/login/' + ) + self.assertRedirects(response, reverse_lazy('tsuke:index')) diff --git a/tsuke/tests/test_admin.py b/tsuke/tests/test_admin.py new file mode 100644 index 0000000..86a706d --- /dev/null +++ b/tsuke/tests/test_admin.py @@ -0,0 +1,46 @@ +from django.urls import reverse_lazy + +from ..models import ItemCategory, Tsuke +from .test_account import SuperuserLoggedInTestCase + + +class TestAdminTsukeTotal(SuperuserLoggedInTestCase): + """管理サイトのツケ合計表示用のテストクラス""" + + @classmethod + def setUpTestData(cls): + cls.category1 = ItemCategory.objects.create(category="飲み物") + cls.category2 = ItemCategory.objects.create(category="お菓子") + + def test_admin_tsuke_total(self): + """管理サイトのツケ合計が正しく表示されることを確認""" + url = reverse_lazy("admin:tsuke_tsuketotal_changelist") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + # 疑似データを登録 + Tsuke.objects.create( + amount=123, + category=self.category1, + note="お茶", + is_paid=False, + user=self.test_user, + ) + Tsuke.objects.create( + amount=456, + category=self.category2, + note="グミ", + is_paid=False, + user=self.test_user, + ) + Tsuke.objects.create( + amount=789, + category=self.category1, + note="コーヒー", + is_paid=True, # 清算済 + user=self.test_user, + ) + + # 未清算のツケのみの合計額が表示されることを確認 + response = self.client.get(url) + self.assertContains(response, "579") # 123 + 456 diff --git a/tsuke/tests/test_views.py b/tsuke/tests/test_views.py index c42e33d..7028ece 100644 --- a/tsuke/tests/test_views.py +++ b/tsuke/tests/test_views.py @@ -1,47 +1,8 @@ -from django.contrib.auth import get_user_model -from django.test import TestCase from django.urls import reverse_lazy from ..forms import TsukeCreateForm from ..models import ItemCategory, Tsuke - - -class LoggedInTestCase(TestCase): - """ - 各テストクラスで共通の事前準備処理をオーバーライドした - 独自TestCaseクラス - (「動かして学ぶ!Python Django開発入門」p.286より) - """ - def setUp(self): - """テストメソッド実行前の事前設定""" - self.username = "testa" - self.password = 'xyab2023' - self.SUBMIT_TOKEN = "test_token" - - self.test_user = get_user_model().objects.create_user( - username=self.username, - password=self.password, - ) - - self.client.login( - username=self.username, - password=self.password - ) - - def set_pseudo_token(self): - """疑似submit tokenをセッションに格納する""" - sess = self.client.session - sess["submit_token"] = self.SUBMIT_TOKEN - sess.save() - -class TestAccount(LoggedInTestCase): - """アカウント機能のテストクラス""" - def test_login_ok(self): - """すでにログイン状態であることの確認""" - response = self.client.get( - '/accounts/login/' - ) - self.assertRedirects(response, reverse_lazy('tsuke:index')) +from .test_account import LoggedInTestCase class TestTsukeCreateView(LoggedInTestCase): From ebaab6261533bd01cc36d0b81c94e12f0df0eaa2 Mon Sep 17 00:00:00 2001 From: Takeru SHIINA Date: Sat, 10 Feb 2024 09:07:23 +0000 Subject: [PATCH 09/12] Update admin pages --- tsuke/admin.py | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/tsuke/admin.py b/tsuke/admin.py index cc70e27..e0f780e 100644 --- a/tsuke/admin.py +++ b/tsuke/admin.py @@ -1,11 +1,27 @@ from django.contrib import admin from django.db.models import Sum -from django.http.request import HttpRequest -from django.template.response import TemplateResponse from .models import ItemCategory, Tsuke, TsukeTotal +class PaidFilter(admin.SimpleListFilter): + """清算済みフィルタ""" + title = "清算" + parameter_name = "is_paid" + + def lookups(self, request, model_admin): + return ( + ("paid", "済"), + ("unpaid", "未"), + ) + + def queryset(self, request, queryset): + if self.value() == "paid": + return queryset.filter(is_paid=True) + if self.value() == "unpaid": + return queryset.filter(is_paid=False) + + class ItemCategoryAdmin(admin.ModelAdmin): list_display = ["category"] change_list_template = "admin/itemcategory_change_list.html" @@ -14,20 +30,31 @@ class ItemCategoryAdmin(admin.ModelAdmin): class TsukeAdmin(admin.ModelAdmin): list_display = ["purchase_date", "amount", "user", "note", "is_paid", "payment_date"] readonly_fields = [field.name for field in Tsuke._meta.fields] - list_filter = ["user", "category", "is_paid"] + list_filter = ["user", "category", PaidFilter] change_list_template = "admin/tsuke_change_list.html" # 一覧画面 change_form_template = "admin/tsuke_change_form.html" # 詳細画面 - def has_add_permission(self, *args, **kwargs): + def has_add_permission(self, request, obj=None) -> bool: """ツケは登録ページからしか追加できない""" return False + def has_view_permission(self, request, obj=None) -> bool: + if request.user.is_staff: + return True # スタッフは閲覧可能 + return False + + def has_delete_permission(self, request, obj=None) -> bool: + if request.user.is_superuser: + return True # スーパーユーザのみ削除可能 + return False + def changelist_view(self, request, extra_context=None): response = super().changelist_view(request, extra_context) - try: - qs = response.context_data["cl"].get_queryset(request) - except (AttributeError, KeyError): - return response + + # try: + # qs = response.context_data["cl"].get_queryset(request) + # except (AttributeError, KeyError): + # return response # フィルタ条件を取得 filters = { From c09aaa00582b9379b8338992d9aeeee8378628e8 Mon Sep 17 00:00:00 2001 From: Takeru SHIINA Date: Sat, 10 Feb 2024 09:07:35 +0000 Subject: [PATCH 10/12] Add admin page button --- tsuke/templates/tsuke/base.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tsuke/templates/tsuke/base.html b/tsuke/templates/tsuke/base.html index 09f83a0..33f107c 100644 --- a/tsuke/templates/tsuke/base.html +++ b/tsuke/templates/tsuke/base.html @@ -58,6 +58,11 @@

Tsukepp

  • 履歴を見る
  • + {% if user.is_staff %} +
  • + 管理画面 +
  • + {% endif %} {% if user.id %}