Skip to content

Commit 92eeb82

Browse files
authored
[25.05.25 / TASK-192] Feature - 0.6v 신규 피쳐를 위한 base model & admin (#29)
* feature: 신규 feature 를 위한 모델링 1차 가안 * delete: 사용하지 않는 view 정리 * feature: trend 와 mail log 모델 마이그레이션 파일 추가 * feature: insight admin 세팅 * feature: 기본 트랜드 관련 admin 구성 완료 * feature: insight model 을 위한 conftest 파일과 모델 전용 테스트 * feature: admin json preview mixin test 추가 * delete: insight admin 에서 UserFilter 그냥 삭제 * feature: insight admin 을 위한 test 추가 * modify: QR 코드 admin 과 test 부분 업데이트, 잘 안쓰는 것들 제거, user 링킹 최적화 * modify: format_html 잘 못 사용하는 부분 리펙토링 * modify: type hinting 강화 * feature: admin 을 base mixin 과 디렉토리로 분리 * feature: JsonPreviewMixin 에 django.template.loader 활용 * modify: mail log 모델에 user 가 null이 될 수 있음에 대응 개발
1 parent 26ad70d commit 92eeb82

26 files changed

+1185
-44
lines changed

backoffice/settings/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"users",
6868
"posts",
6969
"noti", # 공지와 알림 관련 도메인
70+
"insight", # 게시글, 트랜드 인사이트 관련 도메인
7071
]
7172

7273
MIDDLEWARE = [
@@ -86,7 +87,7 @@
8687
TEMPLATES = [
8788
{
8889
"BACKEND": "django.template.backends.django.DjangoTemplates",
89-
"DIRS": [],
90+
"DIRS": [os.path.join(BASE_DIR, "templates")],
9091
"APP_DIRS": True,
9192
"OPTIONS": {
9293
"context_processors": [

common/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
import json
2+
from dataclasses import dataclass
3+
from typing import Type, TypeVar
4+
15
from django.db import models
26

7+
from utils.utils import from_dict, to_dict
8+
39

410
class TimeStampedModel(models.Model): # type: ignore
511
"""
@@ -17,3 +23,21 @@ class TimeStampedModel(models.Model): # type: ignore
1723

1824
class Meta:
1925
abstract = True
26+
27+
28+
T = TypeVar("T")
29+
30+
31+
# dataclass 베이스 mixin
32+
@dataclass
33+
class SerializableMixin:
34+
def to_dict(self) -> dict:
35+
return to_dict(self)
36+
37+
def to_json_dict(self) -> dict:
38+
"""Django Model의 JSON 필드 저장용"""
39+
return json.loads(json.dumps(self.to_dict()))
40+
41+
@classmethod
42+
def from_dict(cls: Type[T], data: dict) -> T:
43+
return from_dict(cls, data)

insight/__init__.py

Whitespace-only changes.

insight/admin/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .base_admin import * # noqa: F403
2+
from .user_weekly_trend_admin import * # noqa: F403
3+
from .weekly_trend_admin import * # noqa: F403

insight/admin/base_admin.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import json
2+
3+
from django.contrib import admin
4+
from django.template.defaultfilters import truncatechars
5+
from django.template.loader import render_to_string
6+
from django.utils.html import format_html
7+
from django.utils.safestring import mark_safe
8+
9+
from insight.models import UserWeeklyTrend, WeeklyTrend
10+
11+
12+
class BaseTrendAdminMixin:
13+
"""공통된 트렌드 관련 필드를 표시하기 위한 Mixin"""
14+
15+
@admin.display(description="주 기간")
16+
def week_range(self, obj: WeeklyTrend | UserWeeklyTrend):
17+
"""주 기간을 표시"""
18+
return format_html(
19+
"{} ~ {}",
20+
obj.week_start_date.strftime("%Y-%m-%d"),
21+
obj.week_end_date.strftime("%Y-%m-%d"),
22+
)
23+
24+
@admin.display(description="인사이트 미리보기")
25+
def insight_preview(self, obj: WeeklyTrend | UserWeeklyTrend):
26+
"""인사이트 미리보기"""
27+
return self.get_json_preview(obj, "insight")
28+
29+
@admin.display(description="처리 완료")
30+
def is_processed_colored(self, obj: WeeklyTrend | UserWeeklyTrend):
31+
"""처리 상태를 색상으로 표시"""
32+
if obj.is_processed:
33+
return format_html(
34+
'<span style="color: green; font-weight: bold;">{}</span>', "✓"
35+
)
36+
return format_html(
37+
'<span style="color: red; font-weight: bold;">{}</span>', "✗"
38+
)
39+
40+
@admin.display(description="처리 완료 시간")
41+
def processed_at_formatted(self, obj: WeeklyTrend | UserWeeklyTrend):
42+
"""처리 완료 시간 포맷팅"""
43+
if obj.processed_at:
44+
return obj.processed_at.strftime("%Y-%m-%d %H:%M")
45+
return "-"
46+
47+
48+
class JsonPreviewMixin:
49+
"""JSONField를 보기 좋게 표시하기 위한 Mixin"""
50+
51+
def get_json_preview(
52+
self, obj: WeeklyTrend | UserWeeklyTrend, field_name, max_length=150
53+
):
54+
"""JSONField 내용의 미리보기를 반환"""
55+
json_data = getattr(obj, field_name, {})
56+
if not json_data:
57+
return "-"
58+
59+
# JSON 문자열로 변환하여 일부만 표시
60+
json_str = json.dumps(json_data, ensure_ascii=False)
61+
return truncatechars(json_str, max_length)
62+
63+
@admin.display(description="인사이트 데이터")
64+
def formatted_insight(self, obj: WeeklyTrend | UserWeeklyTrend):
65+
"""인사이트 JSON을 보기 좋게 포맷팅하여 표시"""
66+
if not hasattr(obj, "insight") or not obj.insight:
67+
return "-"
68+
69+
context = {"insight": obj.insight, "user": getattr(obj, "user", None)}
70+
# render_to_string을 사용하여 템플릿 렌더링
71+
html = render_to_string("insights/insight_preview.html", context)
72+
return mark_safe(html)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from django.contrib import admin
2+
from django.db.models import QuerySet
3+
from django.http import HttpRequest
4+
from django.urls import reverse
5+
from django.utils.html import format_html
6+
7+
from insight.admin import BaseTrendAdminMixin, JsonPreviewMixin
8+
from insight.models import UserWeeklyTrend
9+
from utils.utils import get_local_now
10+
11+
12+
@admin.register(UserWeeklyTrend)
13+
class UserWeeklyTrendAdmin(
14+
admin.ModelAdmin, JsonPreviewMixin, BaseTrendAdminMixin
15+
):
16+
list_display = (
17+
"id",
18+
"user_info",
19+
"week_range",
20+
"insight_preview",
21+
"is_processed_colored",
22+
"processed_at_formatted",
23+
"created_at",
24+
)
25+
list_filter = ("is_processed", "week_start_date")
26+
search_fields = ("user__email", "insight")
27+
readonly_fields = (
28+
"processed_at",
29+
"formatted_insight",
30+
)
31+
raw_id_fields = ("user",)
32+
33+
fieldsets = (
34+
(
35+
"사용자 정보",
36+
{
37+
"fields": ("user", "week_start_date", "week_end_date"),
38+
},
39+
),
40+
(
41+
"인사이트 데이터",
42+
{
43+
"fields": ("formatted_insight",),
44+
"classes": ("wide", "extrapretty"),
45+
},
46+
),
47+
(
48+
"처리 상태",
49+
{
50+
"fields": ("is_processed", "processed_at"),
51+
},
52+
),
53+
)
54+
55+
actions = ["mark_as_processed"]
56+
57+
def get_queryset(self, request: HttpRequest):
58+
queryset = super().get_queryset(request)
59+
return queryset.select_related("user")
60+
61+
@admin.display(description="사용자")
62+
def user_info(self, obj: UserWeeklyTrend):
63+
"""사용자 정보를 표시"""
64+
if not obj.user:
65+
return "-"
66+
67+
user_url = reverse("admin:users_user_change", args=[obj.user.id])
68+
return format_html(
69+
'<a href="{}" target="_blank">{}</a>',
70+
user_url,
71+
obj.user.email or f"사용자 {obj.user.id}",
72+
)
73+
74+
@admin.action(description="선택된 항목을 처리 완료로 표시하기")
75+
def mark_as_processed(
76+
self, request: HttpRequest, queryset: QuerySet[UserWeeklyTrend]
77+
):
78+
"""선택된 항목을 처리 완료로 표시"""
79+
queryset.update(is_processed=True, processed_at=get_local_now())
80+
self.message_user(
81+
request,
82+
f"{queryset.count()}개의 사용자 인사이트가 처리 완료로 표시되었습니다.",
83+
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from django.contrib import admin
2+
from django.db.models import QuerySet
3+
from django.http import HttpRequest
4+
5+
from insight.admin import BaseTrendAdminMixin, JsonPreviewMixin
6+
from insight.models import WeeklyTrend
7+
from utils.utils import get_local_now
8+
9+
10+
@admin.register(WeeklyTrend)
11+
class WeeklyTrendAdmin(
12+
admin.ModelAdmin, JsonPreviewMixin, BaseTrendAdminMixin
13+
):
14+
list_display = (
15+
"id",
16+
"week_range",
17+
"insight_preview",
18+
"is_processed_colored",
19+
"processed_at_formatted",
20+
"created_at",
21+
)
22+
list_filter = ("is_processed", "week_start_date")
23+
search_fields = ("insight",)
24+
readonly_fields = ("processed_at", "formatted_insight")
25+
fieldsets = (
26+
(
27+
"기간 정보",
28+
{
29+
"fields": ("week_start_date", "week_end_date"),
30+
},
31+
),
32+
(
33+
"인사이트 데이터",
34+
{
35+
"fields": ("formatted_insight",),
36+
"classes": ("wide", "extrapretty"),
37+
},
38+
),
39+
(
40+
"처리 상태",
41+
{
42+
"fields": ("is_processed", "processed_at"),
43+
},
44+
),
45+
)
46+
47+
actions = ["mark_as_processed"]
48+
49+
@admin.action(description="선택된 항목을 처리 완료로 표시하기")
50+
def mark_as_processed(
51+
self, request: HttpRequest, queryset: QuerySet[WeeklyTrend]
52+
):
53+
"""선택된 항목을 처리 완료로 표시"""
54+
queryset.update(is_processed=True, processed_at=get_local_now())
55+
self.message_user(
56+
request,
57+
f"{queryset.count()}개의 트렌드가 처리 완료로 표시되었습니다.",
58+
)

insight/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class InsightConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "insight"

0 commit comments

Comments
 (0)