Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions app/cms/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from cms.admin_mixins import RelatedReadonlyFieldsMixin
from cms.models import Page, Section, Sitemap
from core.admin import BaseAbstractModelAdminMixin
from django import forms
from django.contrib import admin
from django.utils.html import format_html
Expand Down Expand Up @@ -151,7 +152,7 @@ class Meta:


@admin.register(Sitemap)
class SitemapAdmin(RelatedReadonlyFieldsMixin, admin.ModelAdmin):
class SitemapAdmin(BaseAbstractModelAdminMixin, RelatedReadonlyFieldsMixin, admin.ModelAdmin):
fields = [
"id",
"parent_sitemap",
Expand Down Expand Up @@ -194,16 +195,20 @@ def get_fieldsets(self, request, obj=...):
return original_fieldsets

def get_queryset(self, request):
return super().get_queryset(request).select_related("page").select_related("parent_sitemap")
return super().get_queryset(request).select_related("page", "parent_sitemap")


class PageAdmin(admin.ModelAdmin):
pass
@admin.register(Page)
class PageAdmin(BaseAbstractModelAdminMixin, admin.ModelAdmin):
fields = ["id", "css", "title", "subtitle"]
readonly_fields = ["id"]
queryset = Page.objects.prefetch_related("sections")


@admin.register(Section)
class SectionAdmin(RelatedReadonlyFieldsMixin, admin.ModelAdmin):
class SectionAdmin(BaseAbstractModelAdminMixin, RelatedReadonlyFieldsMixin, admin.ModelAdmin):
form = SectionAdminForm
queryset = Section.objects.select_related("page")
fields = ["id", "page", "order", "css", "body"]
readonly_fields = ["id"]
related_readonly_config = {"page": ["id", "is_active", "css", "title", "subtitle"]}
Expand All @@ -221,9 +226,3 @@ def get_fieldsets(self, request, obj=...):
)
)
return original_fieldsets

def get_queryset(self, request):
return super().get_queryset(request).select_related("page")


admin.site.register(Page)
46 changes: 46 additions & 0 deletions app/core/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from core.models import BaseAbstractModel
from django.contrib import admin
from django.db import models
from django.forms import ModelForm
from django.http import HttpRequest

INITIAL_FIELDS = INITIAL_READONLY_FIELDS = [
"id",
"created_at",
"created_by",
"updated_at",
"updated_by",
"deleted_at",
"deleted_by",
]


class AdminProtocol(admin.ModelAdmin):
model: type[BaseAbstractModel]


class BaseAbstractModelAdminMixin(AdminProtocol):
def get_queryset(self, request: HttpRequest) -> models.QuerySet[BaseAbstractModel]:
"""Override the default queryset to filter out soft-deleted objects."""
return super().get_queryset(request).filter_active().select_related("created_by", "updated_by", "deleted_by")

def save_model(self, request: HttpRequest, obj: BaseAbstractModel, form: ModelForm, change: bool) -> None:
"""Override save_model to set created_by and updated_by fields."""
if not change:
obj.created_by = request.user
obj.updated_by = request.user
super().save_model(request, obj, form, change)

def get_fields(self, request: HttpRequest, obj: models.Model | None = None) -> list[str]:
fields = list(super().get_fields(request, obj))
for field in INITIAL_FIELDS:
if field not in fields:
fields.append(field)
return fields

def get_readonly_fields(self, request, obj: models.Model | None = None) -> list[str]:
readonly_fields = list(super().get_readonly_fields(request, obj))
for field in INITIAL_READONLY_FIELDS:
if field not in readonly_fields:
readonly_fields.append(field)
return readonly_fields
3 changes: 3 additions & 0 deletions app/core/const/system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SYSTEM_ID = 0
SYSTEM_USERNAME = "system"
SYSTEM_EMAIL = "system@python.or.kr"
11 changes: 2 additions & 9 deletions app/core/middleware/request_response_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
get_request_log_data,
get_response_log_data,
)
from core.middleware.type import GetResponseCallable
from django.http.request import HttpRequest
from django.http.response import HttpResponseBase
from django.utils.deprecation import MiddlewareMixin
Expand All @@ -16,11 +17,6 @@
slack_logger = logging.getLogger("slack_logger")


# From django-stubs
class _GetResponseCallable(typing.Protocol):
def __call__(self, request: HttpRequest, /) -> HttpResponseBase: ...


class LoggerExtraDataType(typing.TypedDict):
request: dict[str, typing.Any]
response: dict[str, typing.Any]
Expand All @@ -41,10 +37,7 @@ class LoggerExtraType(typing.TypedDict):
class RequestResponseLogger(MiddlewareMixin):
sync_capable = True
async_capable = False
get_response: _GetResponseCallable

def __init__(self, get_response: _GetResponseCallable) -> None:
self.get_response = get_response
get_response: GetResponseCallable

def __call__(self, request: HttpRequest) -> HttpResponseBase:
before_session_data = dict(request.session.items()) if config.DEBUG_COLLECT_SESSION_DATA else {}
Expand Down
15 changes: 15 additions & 0 deletions app/core/middleware/thread_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from core.middleware.type import GetResponseCallable
from core.util.thread_local import thread_local
from django.http.request import HttpRequest
from django.http.response import HttpResponseBase
from django.utils.deprecation import MiddlewareMixin


class ThreadLocalMiddleware(MiddlewareMixin):
sync_capable = True
async_capable = False
get_response: GetResponseCallable

def __call__(self, request: HttpRequest) -> HttpResponseBase:
thread_local.current_request = request
return self.get_response(request)
9 changes: 9 additions & 0 deletions app/core/middleware/type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import typing

from django.http.request import HttpRequest
from django.http.response import HttpResponseBase


# From django-stubs
class GetResponseCallable(typing.Protocol):
def __call__(self, request: HttpRequest, /) -> HttpResponseBase: ...
19 changes: 16 additions & 3 deletions app/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import typing
import uuid

from core.util.thread_local import get_current_user
from django.contrib.auth import get_user_model
from django.db import models
from django.db.models.functions import Now
Expand All @@ -13,8 +14,15 @@


class BaseAbstractModelQuerySet(models.QuerySet):
def create(self, **kwargs: dict) -> typing.Self:
current_user = get_current_user()
return super().create(**(kwargs | {"created_by": current_user, "updated_by": current_user}))

def update(self, **kwargs: dict) -> typing.Self:
return super().update(**(kwargs | {"updated_by": get_current_user()}))

def delete(self) -> int: # type: ignore[override]
return super().update(deleted_at=Now(), updated_at=Now())
return super().update(deleted_by=get_current_user(), deleted_at=Now())

def hard_delete(self) -> tuple[int, dict[str, int]]:
return super().delete()
Expand Down Expand Up @@ -54,6 +62,11 @@ def save( # type: ignore[override]
update_fields: collections.abc.Iterable[str] | None = None,
) -> None:
if update_fields:
update_fields = set(update_fields) | {"updated_at"}

update_fields = set(update_fields) | {"updated_at", "updated_by"}
self.updated_by = get_current_user()
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)

def delete(self, using: str | None = None) -> None:
self.deleted_at = Now()
self.deleted_by = get_current_user()
super().save(using=using, update_fields={"deleted_by", "deleted_at"})
2 changes: 2 additions & 0 deletions app/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@
"corsheaders.middleware.CorsMiddleware",
# simple-history
"simple_history.middleware.HistoryRequestMiddleware",
# Thread Local Middleware
"core.middleware.thread_middleware.ThreadLocalMiddleware",
# Request Response Logger
"core.middleware.request_response_logger.RequestResponseLogger",
]
Expand Down
30 changes: 30 additions & 0 deletions app/core/util/thread_local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from __future__ import annotations

import contextlib
import importlib
import threading
import typing

from core.const.system import SYSTEM_ID
from django.http.request import HttpRequest

if typing.TYPE_CHECKING:
from user.models import UserExt

thread_local = threading.local()


def get_request() -> HttpRequest | None:
with contextlib.suppress(AttributeError):
return thread_local.current_request
return None


def get_current_user() -> "UserExt" | None:
if request := get_request():
return request.user

if UserExt := getattr(importlib.import_module("user.models"), "UserExt", None):
return UserExt.objects.filter(id=SYSTEM_ID).first()

return None
11 changes: 6 additions & 5 deletions app/file/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from core.admin import BaseAbstractModelAdminMixin
from django.contrib import admin
from django.http.request import HttpRequest
from django.http.response import HttpResponseNotAllowed, JsonResponse
Expand All @@ -7,12 +8,12 @@


@admin.register(PublicFile)
class PublicFileAdmin(admin.ModelAdmin):
fields = ["id", "file", "mimetype", "hash", "size", "created_at", "updated_at", "deleted_at"]
readonly_fields = ["id", "mimetype", "hash", "size", "created_at", "updated_at", "deleted_at"]
class PublicFileAdmin(BaseAbstractModelAdminMixin, admin.ModelAdmin):
fields = ["file", "mimetype", "hash", "size"]
readonly_fields = ["mimetype", "hash", "size"]

def get_readonly_fields(self, request: HttpRequest, obj: PublicFile | None = None) -> list[str]:
return self.readonly_fields + (["file"] if obj else [])
def get_readonly_fields(self, request: HttpRequest, obj: PublicFile | None = None) -> set[str]:
return super().get_readonly_fields(request, obj) + (["file"] if obj else [])

def get_urls(self) -> list[URLPattern]:
return [
Expand Down
28 changes: 28 additions & 0 deletions app/user/migrations/0002_create_superuser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.2 on 2025-05-24 14:30
import typing
import uuid

from core.const.system import SYSTEM_EMAIL, SYSTEM_ID, SYSTEM_USERNAME
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps

if typing.TYPE_CHECKING:
from user.models import UserExt as UserExtType


def create_superuser(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
UserExt: type["UserExtType"] = apps.get_model("user", "UserExt")

if not UserExt.objects.filter(id=SYSTEM_ID).exists():
UserExt.objects.create_superuser(
id=SYSTEM_ID,
username=SYSTEM_USERNAME,
email=SYSTEM_EMAIL,
password=uuid.uuid4().hex,
)


class Migration(migrations.Migration):
dependencies = [("user", "0001_initial")]
operations = [migrations.RunPython(create_superuser, migrations.RunPython.noop)]