Skip to content
Open
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
78 changes: 53 additions & 25 deletions Appointment/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,22 @@ class College_AnnouncementAdmin(admin.ModelAdmin):
class ParticipantAdmin(admin.ModelAdmin):
actions_on_top = True
actions_on_bottom = True
search_fields = ('Sid__username', 'Sid__name', 'Sid__pinyin', 'Sid__acronym')
search_fields = ('Sid__username', 'Sid__name',
'Sid__pinyin', 'Sid__acronym')
list_display = ('Sid_id', 'name', 'credit', 'longterm', 'hidden')
list_display_links = ('Sid_id', 'name')

class AgreeFilter(admin.SimpleListFilter):
title = '签署状态'
parameter_name = 'Agree'

def lookups(self, request, model_admin):
'''针对字段值设置过滤器的显示效果'''
return (
('true', "已签署"),
('false', "未签署"),
)

def queryset(self, request, queryset):
'''定义过滤器的过滤动作'''
if self.value() == 'true':
Expand All @@ -71,11 +72,11 @@ class AppointInline(admin.TabularInline):
]
show_change_link = True
# 可申诉的范围只有一周,筛选两周内范围的即可

def get_queryset(self, request):
return super().get_queryset(request).filter(
Astart__gte=datetime.now().date() - timedelta(days=14))


inlines = [AppointInline]

actions = []
Expand Down Expand Up @@ -116,7 +117,8 @@ class RoomAdmin(admin.ModelAdmin):
'RneedAgree',
)
list_display_links = ('Rid', )
list_editable = ('Rtitle', 'Rmin', 'Rmax', 'Rstart', 'Rfinish', 'RneedAgree')
list_editable = ('Rtitle', 'Rmin', 'Rmax',
'Rstart', 'Rfinish', 'RneedAgree')
search_fields = ('Rid', 'Rtitle')
list_filter = ('Rstatus', 'RIsAllNight', 'RneedAgree')

Expand All @@ -140,7 +142,7 @@ class AppointAdmin(admin.ModelAdmin):
LETTERS = set(string.digits + string.ascii_letters + string.punctuation)
search_fields = ('Room__Rtitle', 'Room__Rid',
'major_student__name', "students__name",
'major_student__pinyin', # 仅发起者缩写,方便搜索者区分发起者和参与者
'major_student__pinyin', # 仅发起者缩写,方便搜索者区分发起者和参与者
)
list_display = (
'Aid',
Expand Down Expand Up @@ -169,16 +171,16 @@ class AppointAdmin(admin.ModelAdmin):
readonly_fields = ('Atime', )

class ActivateFilter(admin.SimpleListFilter):
title = '有效状态' # 过滤标题显示为"以 有效状态"
parameter_name = 'Activate' # 过滤器使用的过滤字段
title = '有效状态' # 过滤标题显示为"以 有效状态"
parameter_name = 'Activate' # 过滤器使用的过滤字段

def lookups(self, request, model_admin):
'''针对字段值设置过滤器的显示效果'''
return (
('true', "有效"),
('false', "无效"),
)

def queryset(self, request, queryset):
'''定义过滤器的过滤动作'''
if self.value() == 'true':
Expand All @@ -200,15 +202,17 @@ def get_search_results(self, request, queryset, search_term: str):
if ' ' not in search_term:
# 判断时需要增加exists,否则会报错,似乎是QuerySet的缓存问题?
if str.isascii(search_term) and str.isalpha(search_term):
pinyin_result = queryset.filter(major_student__pinyin__icontains=search_term)
pinyin_result = queryset.filter(
major_student__pinyin__icontains=search_term)
if pinyin_result.exists():
return pinyin_result, False
elif str.isascii(search_term) and str.isalnum(search_term):
room_result = queryset.filter(Room__Rid__iexact=search_term)
if room_result.exists():
return room_result, False
else:
room_result = queryset.filter(Room__Rtitle__icontains=search_term)
room_result = queryset.filter(
Room__Rtitle__icontains=search_term)
if room_result.exists():
return room_result, False
return super().get_search_results(request, queryset, search_term)
Expand All @@ -232,7 +236,8 @@ def usage_display(self, obj):
usage = obj.Ausage
else:
usage = obj.Ausage[:half_len] + '...' + obj.Ausage[3-half_len:]
usage = '<br/>'.join([usage[i:i+batch] for i in range(0, len(usage), batch)])
usage = '<br/>'.join([usage[i:i+batch]
for i in range(0, len(usage), batch)])
return mark_safe(usage)

@as_display('通过率')
Expand Down Expand Up @@ -275,7 +280,6 @@ def _waiting2confirm(self, appoint: Appoint):
students_id=[appoint.get_major_id()], admin=True)
logger.info(f"{appoint.Aid}号预约被管理员通过,发起人:{_appointor(appoint)}")


def _violated2judged(self, appoint: Appoint):
appoint.Astatus = Appoint.Status.JUDGED
appoint.save()
Expand All @@ -284,7 +288,6 @@ def _violated2judged(self, appoint: Appoint):
students_id=[appoint.get_major_id()], admin=True)
logger.info(f"{appoint.Aid}号预约被管理员通过,发起人:{_appointor(appoint)}")


@as_action('所选条目 通过', actions, 'change', update=True)
def confirm(self, request, queryset: QuerySet[Appoint]): # 确认通过
invalid = []
Expand All @@ -303,18 +306,18 @@ def confirm(self, request, queryset: QuerySet[Appoint]): # 确认通过
message = f'部分成功!但{invalid}状态不为等待、违约,不允许更改!'
return self.message_user(request, message, messages.WARNING)


@as_action('所选条目 违约', actions, 'change', update=True)
def violate(self, request, queryset: QuerySet[Appoint]): # 确认违约
for appoint in queryset:
if (appoint.Astatus == Appoint.Status.VIOLATED
and appoint.Areason == Appoint.Reason.R_ELSE):
and appoint.Areason == Appoint.Reason.R_ELSE):
return self.message_user(
request, '操作失败!只允许对未审核的条目操作!', messages.WARNING)
ori_status = appoint.get_status()
if appoint.Astatus != Appoint.Status.VIOLATED:
appoint.Astatus = Appoint.Status.VIOLATED
User.objects.modify_credit(appoint.get_major_id(), -1, '地下室:后台')
User.objects.modify_credit(
appoint.get_major_id(), -1, '地下室:后台')
appoint.Areason = Appoint.Reason.R_ELSE
appoint.save()

Expand All @@ -326,7 +329,6 @@ def violate(self, request, queryset: QuerySet[Appoint]): # 确认违约

return self.message_user(request, "设为违约成功!")


@as_action('更新定时任务', actions, ['add', 'change'])
def refresh_scheduler(self, request, queryset):
'''
Expand All @@ -339,8 +341,8 @@ def refresh_scheduler(self, request, queryset):
start = appoint.Astart
finish = appoint.Afinish
if start > finish:
return self.message_user(request,
f'操作失败,预约{aid}开始和结束时间冲突!请勿篡改数据!', messages.WARNING)
return self.message_user(request,
f'操作失败,预约{aid}开始和结束时间冲突!请勿篡改数据!', messages.WARNING)
cancel_scheduler(aid) # 注销原有定时任务 无异常
set_scheduler(appoint) # 开始时进入进行中 结束后判定
set_appoint_reminder(appoint)
Expand All @@ -349,7 +351,6 @@ def refresh_scheduler(self, request, queryset):
return self.message_user(request, str(e), messages.WARNING)
return self.message_user(request, '定时任务更新成功!')


def longterm_wk(self, request, queryset, times, interval_week=1):
new_appoints = {}
for appoint in queryset:
Expand All @@ -365,7 +366,8 @@ def longterm_wk(self, request, queryset, times, interval_week=1):
longterm_info = jobs.get_longterm_display(times, interval_week)
notify_appoint(appoint, MessageType.LONGTERM_CREATED,
f'新增了{longterm_info}同时段预约', admin=True)
new_appoints[appoint.pk] = list(appoints.values_list('pk', flat=True))
new_appoints[appoint.pk] = list(
appoints.values_list('pk', flat=True))
except Exception as e:
return self.message_user(request, f'长线化失败!', messages.WARNING)
new_infos = []
Expand All @@ -377,8 +379,8 @@ def longterm_wk(self, request, queryset, times, interval_week=1):
new_infos.append(f'{appoint}->{new_appoint_ids}')
return self.message_user(request, f'长线化成功!生成预约{";".join(new_appoints)}')


# @as_action('增加一周本预约', actions, 'add', single=True)

def longterm1(self, request, queryset):
return self.longterm_wk(request, queryset, 1)

Expand Down Expand Up @@ -418,7 +420,7 @@ class CardCheckInfoAdmin(admin.ModelAdmin):
'Cardtime', 'CardStatus',
('Cardroom', admin.EmptyFieldListFilter),
]

@as_display('刷卡者', except_value='-')
def student_display(self, obj):
return obj.Cardstudent.name
Expand All @@ -433,3 +435,29 @@ class LongTermAppointAdmin(admin.ModelAdmin):

def view_on_site(self, obj: LongTermAppoint):
return f'/underground/review?Lid={obj.pk}'


@admin.register(AI_Inspection_Info)
class AI_Inspection_InfoAdmin(admin.ModelAdmin):
list_display = ['id', 'Iperson', 'Iroom', 'Ireason_truncated',
'Iresult', 'Itimestamp', 'Idetail_truncated']
list_select_related = ['Iperson', 'Iroom']
search_fields = ['Iperson__name', 'Iperson__Sid__username',
'Iroom__Rtitle', 'Ireason', 'Idetail']
list_filter = ['Iresult', 'Itimestamp']
readonly_fields = ['Iperson', 'Iroom',
'Ireason', 'Iresult', 'Itimestamp', 'Idetail']

@as_display('事由')
def Ireason_truncated(self, obj):
if len(obj.Ireason) > 30:
return obj.Ireason[:27] + '...'
return obj.Ireason

@as_display('详情')
def Idetail_truncated(self, obj):
if not obj.Idetail:
return '-'
if len(obj.Idetail) > 30:
return obj.Idetail[:27] + '...'
return obj.Idetail
14 changes: 14 additions & 0 deletions Appointment/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,18 @@ class AppointmentConfig(Config):
# 是否允许单人同一时段预约两个房间
allow_overlap = False

# AI 审核功能开启与否,使用的 API 地址和密钥
AI_Inspection_Enabled = LazySetting('AI_Inspection/enabled', type=bool)
AI_Inspection_Method = LazySetting(
'AI_Inspection/method', type=str, default="Ollama") # "Ollama" or "GLM"

# Ollama 参数
Ollama_ADDR = LazySetting('AI_Inspection/Ollama_ADDR', type=str)

# GLM 参数
GLM_API_KEY = LazySetting('AI_Inspection/GLM_API_Key', type=str)
GLM_Timeout = LazySetting(
'AI_Inspection/GLM_Timeout', type=int, default=30)


appointment_config = AppointmentConfig(ROOT_CONFIG, 'underground')
69 changes: 69 additions & 0 deletions Appointment/migrations/0004_ai_inspection_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Generated by Django 5.0.14 on 2025-09-16 19:51

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("Appointment", "0003_alter_appoint_options_alter_appoint_areason"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="AI_Inspection_Info",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("Ireason", models.CharField(max_length=256, verbose_name="事由")),
(
"Iresult",
models.SmallIntegerField(
choices=[(1, "合规"), (0, "不合规"), (-1, "错误")],
default=-1,
verbose_name="审核结果",
),
),
(
"Idetail",
models.CharField(
blank=True, max_length=512, verbose_name="审核详情"
),
),
(
"Itimestamp",
models.DateTimeField(auto_now_add=True, verbose_name="审核时间"),
),
(
"Iperson",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="预约人",
),
),
(
"Iroom",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="Appointment.room",
verbose_name="房间号",
),
),
],
options={
"verbose_name": "AI审核信息",
"verbose_name_plural": "AI审核信息",
},
),
]
34 changes: 30 additions & 4 deletions Appointment/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
'Appoint',
'LongTermAppoint',
'CardCheckInfo',
'AI_Inspection_Info',
]



class College_Announcement(models.Model):
class Meta:
verbose_name = "全院公告"
Expand Down Expand Up @@ -172,7 +172,8 @@ class Status(models.IntegerChoices):
UNLIMITED = 1, '无需预约' # 允许使用
FORBIDDEN = 2, '禁止使用' # 禁止使用

Rstatus = models.SmallIntegerField('房间状态', choices=Status.choices, default=0)
Rstatus = models.SmallIntegerField(
'房间状态', choices=Status.choices, default=0)

# 标记当前房间是否可以通宵使用,可由管理员修改(主要针对自习室)
RIsAllNight = models.BooleanField('可通宵使用', default=False)
Expand Down Expand Up @@ -249,7 +250,7 @@ class CheckStatus(models.IntegerChoices):
PASSED = 1 # 预约在特定分钟内的检查是通过的
UNSAVED = 2 # 预约在此分钟内尚未记录检测状态
Acheck_status = models.SmallIntegerField('检测状态',
choices=CheckStatus.choices, default=2)
choices=CheckStatus.choices, default=2)

# 这里Room使用外键的话只能设置DO_NOTHING,否则删除房间就会丢失预约信息
# 所以房间信息不能删除,只能逻辑删除
Expand Down Expand Up @@ -309,7 +310,7 @@ class Type(models.IntegerChoices):
INTERVIEW = choice(4, '面试预约')

Atype = models.SmallIntegerField('预约类型',
choices=Type.choices, default=Type.NORMAL)
choices=Type.choices, default=Type.NORMAL)
get_Atype_display: CustomizedDisplay

objects: AppointManager = AppointManager()
Expand Down Expand Up @@ -519,6 +520,31 @@ def get_applicant_id(self) -> str:
return self.applicant.get_id()


class AI_Inspection_Info(models.Model):
"""
记录AI审核的结果和理由
"""
class Meta:
verbose_name = 'AI审核信息'
verbose_name_plural = verbose_name

Iperson = models.ForeignKey(
User, on_delete=models.CASCADE, verbose_name='预约人')
Iroom = models.ForeignKey(
Room, on_delete=models.CASCADE, verbose_name='房间号')
Ireason = models.CharField('事由', max_length=256)

class Result(models.IntegerChoices):
APPROVED = (1, '合规')
NOT_APPROVED = (0, '不合规')
ERROR = (-1, '错误')

Iresult = models.SmallIntegerField(
'审核结果', choices=Result.choices, default=Result.ERROR)
Idetail = models.CharField('审核详情', max_length=512, blank=True)
Itimestamp = models.DateTimeField('审核时间', auto_now_add=True)


@receiver(pre_delete, sender=Appoint)
def before_delete_Appoint(sender, instance, **kwargs):
from Appointment.appoint.jobs import cancel_scheduler
Expand Down
Loading