This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
from django.urls import path, reverse
|
||||||
|
from django.shortcuts import redirect
|
||||||
from unfold.admin import ModelAdmin, TabularInline
|
from unfold.admin import ModelAdmin, TabularInline
|
||||||
from unfold.decorators import display
|
from unfold.decorators import display
|
||||||
from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement
|
from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement
|
||||||
@@ -28,6 +30,83 @@ class TopicMediaInline(TabularInline):
|
|||||||
readonly_fields = ('created_at',)
|
readonly_fields = ('created_at',)
|
||||||
can_delete = True
|
can_delete = True
|
||||||
|
|
||||||
|
class OrderableAdminMixin:
|
||||||
|
"""
|
||||||
|
为 Admin 添加排序功能的 Mixin
|
||||||
|
提供上移、下移按钮,直接交换 order 值
|
||||||
|
"""
|
||||||
|
def get_urls(self):
|
||||||
|
urls = super().get_urls()
|
||||||
|
custom_urls = [
|
||||||
|
path('<path:object_id>/move-up/', self.admin_site.admin_view(self.move_up_view), name=f'{self.model._meta.app_label}_{self.model._meta.model_name}_move_up'),
|
||||||
|
path('<path:object_id>/move-down/', self.admin_site.admin_view(self.move_down_view), name=f'{self.model._meta.app_label}_{self.model._meta.model_name}_move_down'),
|
||||||
|
]
|
||||||
|
return custom_urls + urls
|
||||||
|
|
||||||
|
def move_up_view(self, request, object_id):
|
||||||
|
obj = self.get_object(request, object_id)
|
||||||
|
if obj:
|
||||||
|
# 找到排在它前面的一个 (order 小于它的最大值)
|
||||||
|
prev_obj = self.model.objects.filter(order__lt=obj.order).order_by('-order').first()
|
||||||
|
if prev_obj:
|
||||||
|
# 交换
|
||||||
|
obj.order, prev_obj.order = prev_obj.order, obj.order
|
||||||
|
obj.save()
|
||||||
|
prev_obj.save()
|
||||||
|
self.message_user(request, f"成功将 {obj} 上移")
|
||||||
|
else:
|
||||||
|
# 已经是第一个,或者前面没有更小的 order
|
||||||
|
pass
|
||||||
|
return redirect(request.META.get('HTTP_REFERER', '..'))
|
||||||
|
|
||||||
|
def move_down_view(self, request, object_id):
|
||||||
|
obj = self.get_object(request, object_id)
|
||||||
|
if obj:
|
||||||
|
# 找到排在它后面的一个 (order 大于它的最小值)
|
||||||
|
next_obj = self.model.objects.filter(order__gt=obj.order).order_by('order').first()
|
||||||
|
if next_obj:
|
||||||
|
# 交换
|
||||||
|
obj.order, next_obj.order = next_obj.order, obj.order
|
||||||
|
obj.save()
|
||||||
|
next_obj.save()
|
||||||
|
self.message_user(request, f"成功将 {obj} 下移")
|
||||||
|
return redirect(request.META.get('HTTP_REFERER', '..'))
|
||||||
|
|
||||||
|
def order_actions(self, obj):
|
||||||
|
# 只有专家用户才显示排序按钮
|
||||||
|
if not getattr(obj, 'is_star', True): # 默认为True是为了兼容其他模型,WeChatUser有is_star字段
|
||||||
|
return "默认排序"
|
||||||
|
|
||||||
|
# 使用 inline style 实现基本样式
|
||||||
|
btn_style = (
|
||||||
|
"display: inline-flex; align-items: center; justify-content: center; "
|
||||||
|
"width: 26px; height: 26px; border-radius: 6px; "
|
||||||
|
"background-color: #f3f4f6; color: #4b5563; text-decoration: none; "
|
||||||
|
"border: 1px solid #e5e7eb; transition: all 0.2s;"
|
||||||
|
)
|
||||||
|
# onmouseover js
|
||||||
|
hover_js = "this.style.backgroundColor='#dbeafe'; this.style.color='#2563eb'; this.style.borderColor='#bfdbfe';"
|
||||||
|
out_js = "this.style.backgroundColor='#f3f4f6'; this.style.color='#4b5563'; this.style.borderColor='#e5e7eb';"
|
||||||
|
|
||||||
|
return format_html(
|
||||||
|
'<div style="display: flex; align-items: center; gap: 6px;">'
|
||||||
|
'<a href="{}" title="上移" style="{}" onmouseover="{}" onmouseout="{}">'
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 15l-6-6-6 6"/></svg>'
|
||||||
|
'</a>'
|
||||||
|
'<span style="font-weight: 700; font-family: system-ui, -apple-system, sans-serif; min-width: 20px; text-align: center; color: #374151; font-size: 13px;">{}</span>'
|
||||||
|
'<a href="{}" title="下移" style="{}" onmouseover="{}" onmouseout="{}">'
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>'
|
||||||
|
'</a>'
|
||||||
|
'</div>',
|
||||||
|
reverse(f'admin:{self.model._meta.app_label}_{self.model._meta.model_name}_move_up', args=[obj.pk]),
|
||||||
|
btn_style, hover_js, out_js,
|
||||||
|
obj.order,
|
||||||
|
reverse(f'admin:{self.model._meta.app_label}_{self.model._meta.model_name}_move_down', args=[obj.pk]),
|
||||||
|
btn_style, hover_js, out_js,
|
||||||
|
)
|
||||||
|
order_actions.short_description = "排序调节"
|
||||||
|
order_actions.allow_tags = True
|
||||||
|
|
||||||
@admin.register(Activity)
|
@admin.register(Activity)
|
||||||
class ActivityAdmin(ModelAdmin):
|
class ActivityAdmin(ModelAdmin):
|
||||||
list_display = ('title', 'banner_display', 'start_time', 'location', 'signup_count', 'is_visible', 'is_active', 'auto_confirm', 'created_at')
|
list_display = ('title', 'banner_display', 'start_time', 'location', 'signup_count', 'is_visible', 'is_active', 'auto_confirm', 'created_at')
|
||||||
@@ -115,12 +194,24 @@ class ActivitySignupAdmin(ModelAdmin):
|
|||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
@admin.register(Topic)
|
@admin.register(Topic)
|
||||||
class TopicAdmin(ModelAdmin):
|
class TopicAdmin(OrderableAdminMixin, ModelAdmin):
|
||||||
list_display = ('title', 'category', 'author', 'get_related_item', 'reply_count', 'view_count', 'is_pinned', 'created_at')
|
list_display = ('title', 'category', 'author', 'get_related_item', 'reply_count', 'view_count', 'is_pinned', 'created_at', 'order_actions')
|
||||||
list_filter = ('category', 'is_pinned', 'created_at', 'related_product', 'related_service', 'related_course')
|
list_filter = ('category', 'is_pinned', 'created_at', 'related_product', 'related_service', 'related_course')
|
||||||
search_fields = ('title', 'content', 'author__nickname')
|
search_fields = ('title', 'content', 'author__nickname')
|
||||||
autocomplete_fields = ['author', 'related_product', 'related_service', 'related_course']
|
autocomplete_fields = ['author', 'related_product', 'related_service', 'related_course']
|
||||||
inlines = [TopicMediaInline, ReplyInline]
|
inlines = [TopicMediaInline, ReplyInline]
|
||||||
|
actions = ['reset_ordering']
|
||||||
|
|
||||||
|
@admin.action(description="重置排序 (按ID顺序)")
|
||||||
|
def reset_ordering(self, request, queryset):
|
||||||
|
"""
|
||||||
|
将所有帖子按ID顺序重新分配order值
|
||||||
|
"""
|
||||||
|
all_objects = Topic.objects.all().order_by('id')
|
||||||
|
for index, obj in enumerate(all_objects, start=1):
|
||||||
|
obj.order = index
|
||||||
|
obj.save(update_fields=['order'])
|
||||||
|
self.message_user(request, f"成功重置了 {all_objects.count()} 个帖子的排序权重。")
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('帖子内容', {
|
('帖子内容', {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 6.0.1 on 2026-02-24 16:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('community', '0013_alter_reply_options_reply_is_pinned'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='topic',
|
||||||
|
options={'ordering': ['order', '-is_pinned', '-created_at'], 'verbose_name': '论坛帖子', 'verbose_name_plural': '论坛帖子管理'},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='topic',
|
||||||
|
name='order',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='排序'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -139,6 +139,14 @@ class Topic(models.Model):
|
|||||||
is_pinned = models.BooleanField(default=False, verbose_name="置顶")
|
is_pinned = models.BooleanField(default=False, verbose_name="置顶")
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||||
|
order = models.IntegerField(default=0, verbose_name="排序")
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
is_new = self.pk is None
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
if is_new and getattr(self, 'order', 0) == 0:
|
||||||
|
Topic.objects.filter(pk=self.pk).update(order=self.pk)
|
||||||
|
self.order = self.pk
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
@@ -175,7 +183,7 @@ class Topic(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "论坛帖子"
|
verbose_name = "论坛帖子"
|
||||||
verbose_name_plural = "论坛帖子管理"
|
verbose_name_plural = "论坛帖子管理"
|
||||||
ordering = ['-is_pinned', '-created_at']
|
ordering = ['order', '-is_pinned', '-created_at']
|
||||||
|
|
||||||
|
|
||||||
class Reply(models.Model):
|
class Reply(models.Model):
|
||||||
|
|||||||
@@ -101,19 +101,7 @@ DATABASES = {
|
|||||||
|
|
||||||
#从环境变量获取数据库配置 (Docker 环境会自动注入这些变量。
|
#从环境变量获取数据库配置 (Docker 环境会自动注入这些变量。
|
||||||
|
|
||||||
# DB_HOST = os.environ.get('DB_HOST', '6.6.6.66')
|
DB_HOST = os.environ.get('DB_HOST', '6.6.6.66')
|
||||||
# if DB_HOST:
|
|
||||||
# DATABASES['default'] = {
|
|
||||||
# 'ENGINE': 'django.db.backends.postgresql',
|
|
||||||
# 'NAME': os.environ.get('DB_NAME', 'market'),
|
|
||||||
# 'USER': os.environ.get('DB_USER', 'market'),
|
|
||||||
# 'PASSWORD': os.environ.get('DB_PASSWORD', '123market'),
|
|
||||||
# 'HOST': DB_HOST,
|
|
||||||
# 'PORT': os.environ.get('DB_PORT', '5432'),
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
DB_HOST = os.environ.get('DB_HOST', '121.43.104.161')
|
|
||||||
if DB_HOST:
|
if DB_HOST:
|
||||||
DATABASES['default'] = {
|
DATABASES['default'] = {
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
@@ -121,10 +109,22 @@ if DB_HOST:
|
|||||||
'USER': os.environ.get('DB_USER', 'market'),
|
'USER': os.environ.get('DB_USER', 'market'),
|
||||||
'PASSWORD': os.environ.get('DB_PASSWORD', '123market'),
|
'PASSWORD': os.environ.get('DB_PASSWORD', '123market'),
|
||||||
'HOST': DB_HOST,
|
'HOST': DB_HOST,
|
||||||
'PORT': os.environ.get('DB_PORT', '6433'),
|
'PORT': os.environ.get('DB_PORT', '5432'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# DB_HOST = os.environ.get('DB_HOST', '121.43.104.161')
|
||||||
|
# if DB_HOST:
|
||||||
|
# DATABASES['default'] = {
|
||||||
|
# 'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
# 'NAME': os.environ.get('DB_NAME', 'market'),
|
||||||
|
# 'USER': os.environ.get('DB_USER', 'market'),
|
||||||
|
# 'PASSWORD': os.environ.get('DB_PASSWORD', '123market'),
|
||||||
|
# 'HOST': DB_HOST,
|
||||||
|
# 'PORT': os.environ.get('DB_PORT', '6433'),
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user