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 index 9f8f039..64687ef 100644 --- a/app/cms/admin.py +++ b/app/cms/admin.py @@ -1,3 +1,4 @@ +from cms.admin_mixins import RelatedReadonlyFieldsMixin from cms.models import Page, Section, Sitemap from django import forms from django.contrib import admin @@ -149,10 +150,70 @@ class Meta: widgets = {"body": CodeEditorWidget()} +@admin.register(Sitemap) +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 + + @admin.register(Section) -class SectionAdmin(admin.ModelAdmin): +class SectionAdmin(RelatedReadonlyFieldsMixin, admin.ModelAdmin): form = SectionAdminForm + 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(Page) -admin.site.register(Sitemap) 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/migrations/0003_alter_historicalsitemap_order_alter_sitemap_order.py b/app/cms/migrations/0003_alter_historicalsitemap_order_alter_sitemap_order.py new file mode 100644 index 0000000..5ff8b2e --- /dev/null +++ b/app/cms/migrations/0003_alter_historicalsitemap_order_alter_sitemap_order.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2 on 2025-05-07 11:52 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("cms", "0002_historicalpage_historicalsection_historicalsitemap"), + ] + + operations = [ + migrations.AlterField( + model_name="historicalsitemap", + name="order", + field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)]), + ), + migrations.AlterField( + model_name="sitemap", + name="order", + field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/app/cms/models.py b/app/cms/models.py index c10914f..431368c 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) 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",