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( '', url, ) @admin.display(description="配图预览") def image_preview(self, obj: Memory) -> str: chunks: list[str] = [] if obj.image: chunks.append( format_html( '
主图
' '
', obj.image.url, ) ) for p in obj.photos.order_by("sort_order", "id"): chunks.append( format_html( '', 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",)}, ), ( "时间", { "fields": ( "together_since", "unlock_at", ), "description": "「在一起」时间用于首页计数。unlock_at 已不使用,可留空。", }, ), ) 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