From fe25b81de98d0e71dd37807f3a37f484f7541ae3 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Sat, 3 May 2025 14:29:57 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20cms=20Site,=20Page,=20Section=20adm?= =?UTF-8?q?in=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 4 ++++ app/cms/admin.py | 19 +++++++++++++++++++ app/cms/urls.py | 4 ++-- app/core/settings.py | 4 ++-- 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 app/cms/admin.py diff --git a/Makefile b/Makefile index f216cad..8e09600 100644 --- a/Makefile +++ b/Makefile @@ -64,6 +64,10 @@ local-makemigrations: local-migrate: @ENV_PATH=envfile/.env.local uv run python app/manage.py migrate +# Create admin superuser +local-createsuperuser: + @ENV_PATH=envfile/.env.local uv run python app/manage.py createsuperuser + # Devtools hooks-install: local-setup uv run pre-commit install diff --git a/app/cms/admin.py b/app/cms/admin.py new file mode 100644 index 0000000..3596f2c --- /dev/null +++ b/app/cms/admin.py @@ -0,0 +1,19 @@ +from cms.models import Page, Section, Sitemap +from django.contrib import admin + + +class SitemapAdmin(admin.ModelAdmin): + pass + + +class PageAdmin(admin.ModelAdmin): + pass + + +class SectionAdmin(admin.ModelAdmin): + pass + + +admin.site.register(Sitemap, SitemapAdmin) +admin.site.register(Page, PageAdmin) +admin.site.register(Section, SectionAdmin) diff --git a/app/cms/urls.py b/app/cms/urls.py index 0fba03c..2b53853 100644 --- a/app/cms/urls.py +++ b/app/cms/urls.py @@ -20,7 +20,7 @@ from rest_framework import routers cms_router = routers.SimpleRouter() -cms_router.register("sitemap/", views.SitemapListRetrieveViewSet, base_name="cms-sitemap") -cms_router.register("page/", views.PageListRetrieveViewSet, base_name="cms-page") +cms_router.register("sitemap/", views.SitemapListRetrieveViewSet, basename="cms-sitemap") +cms_router.register("page/", views.PageListRetrieveViewSet, basename="cms-page") urlpatterns = [path("", include(cms_router.urls))] diff --git a/app/core/settings.py b/app/core/settings.py index 979c5e5..32e3cb6 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -308,13 +308,13 @@ SESSION_COOKIE_SAMESITE = COOKIE_SAMESITE SESSION_COOKIE_SECURE = COOKIE_SECURE SESSION_COOKIE_HTTPONLY = COOKIE_HTTPONLY -SESSION_COOKIE_DOMAIN = COOKIE_DOMAIN +SESSION_COOKIE_DOMAIN = None if IS_LOCAL else COOKIE_DOMAIN CSRF_COOKIE_NAME = f"{COOKIE_PREFIX}csrftoken" CSRF_COOKIE_SAMESITE = COOKIE_SAMESITE CSRF_COOKIE_SECURE = COOKIE_SECURE CSRF_COOKIE_HTTPONLY = COOKIE_HTTPONLY -CSRF_COOKIE_DOMAIN = COOKIE_DOMAIN +CSRF_COOKIE_DOMAIN = None if IS_LOCAL else COOKIE_DOMAIN CSRF_TRUSTED_ORIGINS = set(env.list("CSRF_TRUSTED_ORIGINS", default=["https://pycon.kr"])) | { "https://local.dev.pycon.kr:3000", "https://localhost:3000", From ed3643390ec8e532bd38d5cb3eece0a53738e1ea Mon Sep 17 00:00:00 2001 From: earthyoung Date: Sat, 3 May 2025 22:58:41 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20cms=20app=20Site,=20Page,=20Section?= =?UTF-8?q?=20admin=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/cms/admin.py | 60 ++++++++++++++++++++++++++++++++++++++--- app/cms/admin_mixins.py | 26 ++++++++++++++++++ app/cms/models.py | 3 ++- 3 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 app/cms/admin_mixins.py diff --git a/app/cms/admin.py b/app/cms/admin.py index 3596f2c..d5e23ed 100644 --- a/app/cms/admin.py +++ b/app/cms/admin.py @@ -1,17 +1,69 @@ +from cms.admin_mixins import RelatedReadonlyFieldsMixin from cms.models import Page, Section, Sitemap from django.contrib import admin -class SitemapAdmin(admin.ModelAdmin): - pass +class SitemapAdmin(RelatedReadonlyFieldsMixin, admin.ModelAdmin): + fields = ["id", "parent_sitemap", "page", "name", "order", "display_start_at", "display_end_at"] + readonly_fields = ["id"] + related_readonly_config = { + "page": ["id", "is_active", "css", "title", "subtitle"], + "parent_sitemap": ["id", "name", "order", "display_start_at", "display_end_at"], + } + + def get_fieldsets(self, request, obj=...): + original_fieldsets = super().get_fieldsets(request, obj) + if obj and obj.parent_sitemap: + original_fieldsets.append( + ( + "Parent Sitemap 정보", + { + "fields": [f"get_parent_sitemap_{f}" for f in self.related_readonly_config["parent_sitemap"]], + "classes": ("collapse",), + }, + ) + ) + if obj and obj.page: + original_fieldsets.append( + ( + "Page 정보", + { + "fields": [f"get_page_{f}" for f in self.related_readonly_config["page"]], + "classes": ("collapse",), + }, + ) + ) + return original_fieldsets + + def get_queryset(self, request): + return super().get_queryset(request).select_related("page").select_related("parent_sitemap") class PageAdmin(admin.ModelAdmin): pass -class SectionAdmin(admin.ModelAdmin): - pass +class SectionAdmin(RelatedReadonlyFieldsMixin, admin.ModelAdmin): + fields = ["id", "page", "order", "css", "body"] + readonly_fields = ["id"] + related_readonly_config = {"page": ["id", "is_active", "css", "title", "subtitle"]} + + def get_fieldsets(self, request, obj=...): + original_fieldsets = super().get_fieldsets(request, obj) + if obj and obj.page: + original_fieldsets.append( + ( + "Page 정보", + { + "fields": [f"get_page_{f}" for f in self.related_readonly_config["page"]], + "classes": ("collapse",), + }, + ) + ) + return original_fieldsets + + def get_queryset(self, request): + return super().get_queryset(request).select_related("page") admin.site.register(Sitemap, SitemapAdmin) diff --git a/app/cms/admin_mixins.py b/app/cms/admin_mixins.py new file mode 100644 index 0000000..d60ef8a --- /dev/null +++ b/app/cms/admin_mixins.py @@ -0,0 +1,26 @@ +class RelatedReadonlyFieldsMixin: + related_readonly_config = {} + + def _generate_related_getter(self, rel, field, prefix=""): + def _func(admin_self, obj): + related = getattr(obj, rel) + return getattr(related, field) if related else None + + _func.short_description = f"{prefix} {field.replace('_', ' ')}" + return _func + + def _register_dynamic_readonly_fields(self): + for rel, fields in self.related_readonly_config.items(): + for field in fields: + method_name = f"get_{rel}_{field}" + getter = self._generate_related_getter(rel, field, prefix=rel.capitalize()) + setattr(self.__class__, method_name, getter) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._register_dynamic_readonly_fields() + + def get_readonly_fields(self, request, obj=None): + base = super().get_readonly_fields(request, obj) + generated = [f"get_{rel}_{field}" for rel, fields in self.related_readonly_config.items() for field in fields] + return list(base) + generated diff --git a/app/cms/models.py b/app/cms/models.py index c10914f..f1680d0 100644 --- a/app/cms/models.py +++ b/app/cms/models.py @@ -1,5 +1,6 @@ import uuid +from django.core.validators import MinValueValidator from django.db import models @@ -21,7 +22,7 @@ class Sitemap(models.Model): "self", null=True, blank=True, default=None, on_delete=models.SET_NULL, related_name="children" ) name = models.CharField(max_length=256) - order = models.IntegerField(default=0) + order = models.IntegerField(default=0, validators=MinValueValidator(0)) page = models.ForeignKey(Page, on_delete=models.PROTECT) display_start_at = models.DateTimeField(null=True, blank=True) display_end_at = models.DateTimeField(null=True, blank=True)