2026-05-11 19:34:35 +08:00
|
|
|
|
from django.contrib import admin
|
|
|
|
|
|
from django.utils.html import format_html
|
|
|
|
|
|
from django.utils.safestring import mark_safe
|
|
|
|
|
|
|
|
|
|
|
|
from .models import Config, Guestbook, Memory, MemoryPhoto
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class JournalStaffAccessMixin:
|
|
|
|
|
|
"""
|
|
|
|
|
|
手账站点多为小项目:凡 is_staff 即可在后台增删改查(无需在「用户」里逐项勾选 journal.* 权限)。
|
|
|
|
|
|
生产环境若需细分权限,可移除此 Mixin 并改回 Django 默认的 has_perm 校验。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def has_module_permission(self, request):
|
|
|
|
|
|
u = request.user
|
|
|
|
|
|
return bool(getattr(u, "is_active", False) and getattr(u, "is_staff", False))
|
|
|
|
|
|
|
|
|
|
|
|
def has_view_permission(self, request, obj=None):
|
|
|
|
|
|
u = request.user
|
|
|
|
|
|
return bool(getattr(u, "is_active", False) and getattr(u, "is_staff", False))
|
|
|
|
|
|
|
|
|
|
|
|
def has_change_permission(self, request, obj=None):
|
|
|
|
|
|
u = request.user
|
|
|
|
|
|
return bool(getattr(u, "is_active", False) and getattr(u, "is_staff", False))
|
|
|
|
|
|
|
|
|
|
|
|
def has_add_permission(self, request):
|
|
|
|
|
|
u = request.user
|
|
|
|
|
|
return bool(getattr(u, "is_active", False) and getattr(u, "is_staff", False))
|
|
|
|
|
|
|
|
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
|
|
|
|
u = request.user
|
|
|
|
|
|
return bool(getattr(u, "is_active", False) and getattr(u, "is_staff", False))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MemoryPhotoInline(admin.TabularInline):
|
|
|
|
|
|
model = MemoryPhoto
|
|
|
|
|
|
extra = 1
|
|
|
|
|
|
fields = ("image", "sort_order")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(Memory)
|
|
|
|
|
|
class MemoryAdmin(JournalStaffAccessMixin, admin.ModelAdmin):
|
|
|
|
|
|
inlines = (MemoryPhotoInline,)
|
|
|
|
|
|
list_display = (
|
|
|
|
|
|
"event_title",
|
|
|
|
|
|
"story_order",
|
|
|
|
|
|
"date",
|
|
|
|
|
|
"unique_feature_type",
|
|
|
|
|
|
"nepal_art_style",
|
|
|
|
|
|
"is_finale",
|
|
|
|
|
|
"thumb_preview",
|
|
|
|
|
|
)
|
|
|
|
|
|
list_display_links = ("event_title",)
|
|
|
|
|
|
list_filter = ("unique_feature_type", "nepal_art_style", "is_finale", "date")
|
|
|
|
|
|
list_editable = ("story_order",)
|
|
|
|
|
|
search_fields = ("event_title", "main_text")
|
|
|
|
|
|
readonly_fields = ("image_preview",)
|
|
|
|
|
|
ordering = ("story_order", "id")
|
|
|
|
|
|
|
|
|
|
|
|
fieldsets = (
|
|
|
|
|
|
(
|
|
|
|
|
|
"叙事",
|
|
|
|
|
|
{
|
|
|
|
|
|
"fields": (
|
|
|
|
|
|
"story_order",
|
|
|
|
|
|
"date",
|
|
|
|
|
|
"event_title",
|
|
|
|
|
|
"is_finale",
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
"版式",
|
|
|
|
|
|
{
|
|
|
|
|
|
"fields": (
|
|
|
|
|
|
"unique_feature_type",
|
|
|
|
|
|
"nepal_art_style",
|
|
|
|
|
|
"image",
|
|
|
|
|
|
"image_preview",
|
|
|
|
|
|
"main_text",
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@admin.display(description="缩略图")
|
|
|
|
|
|
def thumb_preview(self, obj: Memory) -> str:
|
|
|
|
|
|
url = None
|
|
|
|
|
|
if obj.image:
|
|
|
|
|
|
url = obj.image.url
|
|
|
|
|
|
else:
|
|
|
|
|
|
first = obj.photos.order_by("sort_order", "id").first()
|
|
|
|
|
|
if first:
|
|
|
|
|
|
url = first.image.url
|
|
|
|
|
|
if not url:
|
|
|
|
|
|
return "—"
|
|
|
|
|
|
return format_html(
|
|
|
|
|
|
'<img src="{}" style="max-height:48px;border-radius:4px;" />',
|
|
|
|
|
|
url,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@admin.display(description="配图预览")
|
|
|
|
|
|
def image_preview(self, obj: Memory) -> str:
|
|
|
|
|
|
chunks: list[str] = []
|
|
|
|
|
|
if obj.image:
|
|
|
|
|
|
chunks.append(
|
|
|
|
|
|
format_html(
|
|
|
|
|
|
'<div class="mb-2"><span style="font-size:11px;color:#666">主图</span><br>'
|
|
|
|
|
|
'<img src="{}" style="max-width:280px;border-radius:8px;" /></div>',
|
|
|
|
|
|
obj.image.url,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
for p in obj.photos.order_by("sort_order", "id"):
|
|
|
|
|
|
chunks.append(
|
|
|
|
|
|
format_html(
|
|
|
|
|
|
'<img src="{}" style="max-width:200px;border-radius:6px;margin:4px;" />',
|
|
|
|
|
|
p.image.url,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
if not chunks:
|
|
|
|
|
|
return "—"
|
|
|
|
|
|
return mark_safe("".join(str(c) for c in chunks))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(Guestbook)
|
|
|
|
|
|
class GuestbookAdmin(JournalStaffAccessMixin, admin.ModelAdmin):
|
|
|
|
|
|
list_display = ("nickname", "short_message", "created_at")
|
|
|
|
|
|
search_fields = ("nickname", "message")
|
|
|
|
|
|
readonly_fields = ("created_at",)
|
|
|
|
|
|
|
|
|
|
|
|
@admin.display(description="留言")
|
|
|
|
|
|
def short_message(self, obj: Guestbook) -> str:
|
|
|
|
|
|
return obj.message[:80] + ("…" if len(obj.message) > 80 else "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(Config)
|
|
|
|
|
|
class ConfigAdmin(admin.ModelAdmin):
|
|
|
|
|
|
"""
|
|
|
|
|
|
站点配置单独写权限:不继承 JournalStaffAccessMixin,避免 MRO/多重继承下
|
|
|
|
|
|
has_change_permission 未按预期生效,导致 change 页全部为只读且 submit_row 不显示「保存」。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
save_on_top = True
|
|
|
|
|
|
|
|
|
|
|
|
list_display = (
|
|
|
|
|
|
"site_title",
|
|
|
|
|
|
"together_since",
|
|
|
|
|
|
"unlock_at",
|
|
|
|
|
|
)
|
|
|
|
|
|
list_display_links = ("site_title",)
|
|
|
|
|
|
fieldsets = (
|
|
|
|
|
|
(
|
|
|
|
|
|
"站点",
|
|
|
|
|
|
{"fields": ("site_title",)},
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
2026-05-20 16:31:07 +08:00
|
|
|
|
"时间",
|
2026-05-11 19:34:35 +08:00
|
|
|
|
{
|
|
|
|
|
|
"fields": (
|
|
|
|
|
|
"together_since",
|
|
|
|
|
|
"unlock_at",
|
|
|
|
|
|
),
|
2026-05-20 16:31:07 +08:00
|
|
|
|
"description": "「在一起」时间用于首页计数。unlock_at 已不使用,可留空。",
|
2026-05-11 19:34:35 +08:00
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _staff_ok(self, request) -> bool:
|
|
|
|
|
|
u = request.user
|
|
|
|
|
|
return bool(
|
|
|
|
|
|
getattr(u, "is_active", False)
|
|
|
|
|
|
and getattr(u, "is_authenticated", False)
|
|
|
|
|
|
and (getattr(u, "is_staff", False) or getattr(u, "is_superuser", False))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def has_module_permission(self, request):
|
|
|
|
|
|
return self._staff_ok(request)
|
|
|
|
|
|
|
|
|
|
|
|
def has_view_permission(self, request, obj=None):
|
|
|
|
|
|
return self._staff_ok(request)
|
|
|
|
|
|
|
|
|
|
|
|
def has_change_permission(self, request, obj=None):
|
|
|
|
|
|
return self._staff_ok(request)
|
|
|
|
|
|
|
|
|
|
|
|
def has_add_permission(self, request):
|
|
|
|
|
|
if not self._staff_ok(request):
|
|
|
|
|
|
return False
|
|
|
|
|
|
return not Config.objects.exists()
|
|
|
|
|
|
|
|
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
|
|
|
|
return False
|