Appointment.models 源代码

from datetime import timedelta
from typing import cast

from django.db import models
from django.db.models.signals import pre_delete
from django.db.models import QuerySet, Q
from django.dispatch import receiver
from django.db import transaction

from utils.models.descriptor import admin_only
from utils.models.choice import choice, CustomizedDisplay, DefaultDisplay
from utils.models.manager import ManyRelatedManager
from utils.models.permission import PermissionModelBase, BasePermission
from generic.models import User
from Appointment.config import appointment_config as CONFIG

__all__ = [
    'User',
    'College_Announcement',
    'Participant',
    'Room',
    'Appoint',
    'LongTermAppoint',
    'CardCheckInfo',
]



[文档] class College_Announcement(models.Model): class Meta: verbose_name = "全院公告" verbose_name_plural = verbose_name
[文档] class Show_Status(models.IntegerChoices): Yes = 1 No = 0
show = models.SmallIntegerField('是否显示', choices=Show_Status.choices, default=0) announcement = models.CharField('通知内容', max_length=256, blank=True)
[文档] class Participant(models.Model): class Meta: verbose_name = '学生' verbose_name_plural = verbose_name ordering = ['Sid'] Sid = models.OneToOneField( User, related_name='+', on_delete=models.CASCADE, to_field='username', verbose_name='学号', primary_key=True, ) @property def name(self) -> str: return self.Sid.name @property def credit(self) -> int: '''通过此方法访问的信用分是只读的,修改应使用User.objects方法''' return self.Sid.credit hidden = models.BooleanField('不可搜索', default=False) longterm = models.BooleanField('可长期预约', default=False) # TODO: pht 2022-02-20 通过新的模型实现,允许每个房间有自己的规则 # 用户许可的字段,需要许可的房间刷卡时检查是否通过了许可 agree_time = models.DateField('上次许可时间', null=True, blank=True) appoint_list: 'AppointManager' @property def appoints_manager(self) -> 'ManyRelatedManager[Appoint]': '''获取所有预约,允许进行批量管理''' return cast('ManyRelatedManager[Appoint]', self.appoint_list)
[文档] def get_id(self) -> str: '''获取id(学号/组织账号)''' return self.Sid_id
@admin_only def __str__(self): '''仅用于后台呈现和搜索方便,任何时候不应使用''' acronym = self.Sid.acronym if acronym is None: return self.name return self.name + '_' + acronym
class RoomQuerySet(models.QuerySet['Room']): def permitted(self): '''只保留所有可预约的房间''' return self.filter(Rstatus=Room.Status.PERMITTED) def unlimited(self): '''只保留所有无需预约的房间''' return self.filter(Rstatus=Room.Status.UNLIMITED) def activated(self): '''只保留所有可用的房间''' return self.filter(Rstatus__in=[Room.Status.UNLIMITED, Room.Status.PERMITTED]) def basement_only(self): '''只保留所有地下室的房间''' return self.exclude(Rid__icontains="R") def russian_only(self): '''只保留所有俄文楼的房间''' return self.filter(Rid__icontains="R") class RoomManager(models.Manager['Room']): def get_queryset(self) -> RoomQuerySet: return RoomQuerySet(self.model, using=self._db, hints=self._hints) def all(self) -> RoomQuerySet: return super().all() # type: ignore def permitted(self): return self.get_queryset().permitted() def unlimited(self): return self.get_queryset().unlimited() def function_rooms(self): '''获取所有可预约功能房''' titles = ['航模', '绘画', '书法', '活动'] title_query = ~Q(Rtitle__icontains="研讨") title_query |= Q(Rtitle__icontains="/") for room_title in titles: title_query |= Q(Rtitle__icontains=room_title) return self.get_queryset().permitted().basement_only().filter(title_query) def talk_rooms(self): '''获取所有研讨室''' return self.get_queryset().permitted().filter(Rtitle__icontains="研讨") def russian_rooms(self): '''获取所有可预约俄文楼教室''' return self.get_queryset().permitted().russian_only() def interview_room_ids(self): '''获取所有可面试俄文楼教室''' return set()
[文档] class Room(models.Model): class Meta: verbose_name = '房间' verbose_name_plural = verbose_name ordering = ['Rid'] # 房间编号我不确定是否需要。如果地下室有门牌的话(例如B101)保留房间编号比较好 # 如果删除Rid记得把Rtitle设置成主键 Rid = models.CharField('房间编号', max_length=8, primary_key=True) Rtitle = models.CharField('房间名称', max_length=32) Rmin = models.IntegerField('房间预约人数下限', default=0) Rmax = models.IntegerField('房间使用人数上限', default=20) Rstart = models.TimeField('最早预约时间') Rfinish = models.TimeField('最迟预约时间') Rlatest_time = models.DateTimeField("摄像头心跳", auto_now_add=True) Rpresent = models.IntegerField('目前人数', default=0) # Rstatus 标记当前房间是否允许预约,可由管理员修改
[文档] class Status(models.IntegerChoices): PERMITTED = 0, '允许预约' # 允许预约 UNLIMITED = 1, '无需预约' # 允许使用 FORBIDDEN = 2, '禁止使用' # 禁止使用
Rstatus = models.SmallIntegerField('房间状态', choices=Status.choices, default=0) # 标记当前房间是否可以通宵使用,可由管理员修改(主要针对自习室) RIsAllNight = models.BooleanField('可通宵使用', default=False) # 是否需要许可,目前通过要求阅读固定须知实现,未来可拓展为许可模型(关联房间和个人) RneedAgree = models.BooleanField('需要许可', default=False) appoint_list: 'AppointManager' @property def appoints_manager(self) -> 'ManyRelatedManager[Appoint]': '''获取所有预约,允许进行批量管理''' return cast('ManyRelatedManager[Appoint]', self.appoint_list) objects: RoomManager = RoomManager() def __str__(self): return self.Rid + ' ' + self.Rtitle
class AppointQuerySet(models.QuerySet['Appoint']): def not_canceled(self): return self.exclude(Astatus=Appoint.Status.CANCELED) def terminated(self): return self.filter(Astatus__in=Appoint.Status.Terminals()) def unfinished(self): return self.exclude(Astatus__in=Appoint.Status.Terminals()) class AppointManager(models.Manager['Appoint']): def get_queryset(self) -> AppointQuerySet: return AppointQuerySet(self.model, using=self._db, hints=self._hints) def all(self) -> AppointQuerySet: return super().all() # type: ignore def not_canceled(self): return self.get_queryset().not_canceled() def unfinished(self): '''用于检查而非呈现,筛选还未结束的预约''' return self.exclude(Astatus__in=Appoint.Status.Terminals()) def displayable(self): '''个人主页页面,在"普通预约"和"查看下周"中会显示的预约''' return self.exclude(Atype=Appoint.Type.LONGTERM, Astatus=Appoint.Status.CANCELED)
[文档] class Appoint(models.Model, metaclass=PermissionModelBase):
[文档] class Permission(BasePermission): CREATE = choice('create_appointment', '创建预约') CANCEL = choice('cancel_appointment', '取消预约')
class Meta: verbose_name = '预约信息' verbose_name_plural = verbose_name ordering = ['Aid'] Aid = models.AutoField('预约编号', primary_key=True) # 申请时间为插入数据库的时间 Atime = models.DateTimeField('申请时间', auto_now_add=True) Astart = models.DateTimeField('开始时间') Afinish = models.DateTimeField('结束时间') Ausage = models.CharField('用途', max_length=256, null=True) Aannouncement = models.CharField( '预约通知', max_length=256, null=True, blank=True) Anon_yp_num = models.IntegerField("外院人数", default=0) Ayp_num = models.IntegerField('院内人数', default=0) # CheckStatus: 分钟内检测状态
[文档] class CheckStatus(models.IntegerChoices): FAILED = 0 # 预约在此分钟的检查尚未通过 PASSED = 1 # 预约在特定分钟内的检查是通过的 UNSAVED = 2 # 预约在此分钟内尚未记录检测状态
Acheck_status = models.SmallIntegerField('检测状态', choices=CheckStatus.choices, default=2) # 这里Room使用外键的话只能设置DO_NOTHING,否则删除房间就会丢失预约信息 # 所以房间信息不能删除,只能逻辑删除 # 调用时使用appoint_obj.Room和room_obj.appoint_list Room: 'models.ForeignKey[Room]' = models.ForeignKey( Room, verbose_name='房间号', related_name='appoint_list', null=True, on_delete=models.SET_NULL) # type: ignore # 通过类型提示限制操作类型,非只读操作应访问students_manager students: 'models.Manager[Participant]' = models.ManyToManyField( Participant, related_name='appoint_list', db_index=True) # type: ignore @property def students_manager(self) -> 'ManyRelatedManager[Participant]': '''获取所有参与者,允许进行批量管理''' return cast('ManyRelatedManager[Participant]', self.students) major_student: 'models.ForeignKey[Participant]' = models.ForeignKey( Participant, verbose_name='Appointer', null=True, on_delete=models.CASCADE) # type: ignore
[文档] class Status(models.IntegerChoices): CANCELED = choice(0, '已取消') APPOINTED = choice(1, '已预约') PROCESSING = choice(2, '进行中') WAITING = choice(3, '等待确认') CONFIRMED = choice(4, '已确认') VIOLATED = choice(5, '违约') JUDGED = choice(6, '申诉成功')
[文档] @classmethod def Terminals(cls) -> 'list[Appoint.Status]': return [cls.CANCELED, cls.CONFIRMED, cls.VIOLATED, cls.JUDGED]
Astatus = models.IntegerField('预约状态', choices=Status.choices, default=1) get_Astatus_display: CustomizedDisplay Aneed_num = models.IntegerField('检查人数要求') Acamera_check_num = models.IntegerField('检查次数', default=0) Acamera_ok_num = models.IntegerField('人数合格次数', default=0)
[文档] class Reason(models.IntegerChoices): R_NOVIOLATED = choice(0, '没有违约') R_LATE = choice(1, '迟到') R_TOOLITTLE = choice(2, '人数不足') R_ELSE = choice(3, '其它原因')
Areason = models.IntegerField('违约原因', choices=Reason.choices, default=0) get_Areason_display: DefaultDisplay
[文档] class Type(models.IntegerChoices): '''预约类型''' NORMAL = choice(0, '常规预约') TODAY = choice(1, '当天预约') TEMPORARY = choice(2, '临时预约') LONGTERM = choice(3, '长期预约') INTERVIEW = choice(4, '面试预约')
Atype = models.SmallIntegerField('预约类型', choices=Type.choices, default=Type.NORMAL) get_Atype_display: CustomizedDisplay objects: AppointManager = AppointManager()
[文档] def add_time(self, delta: timedelta): '''方便同时调整预约时间的函数,修改自身,不调用save保存''' self.Astart += delta self.Afinish += delta return self
[文档] def get_major_id(self) -> str: '''获取预约发起者id''' return self.major_student.Sid_id
[文档] def get_admin_url(self) -> str: '''获取后台搜索的url''' return f'/admin/Appointment/appoint/?q={self.pk}'
[文档] def get_status(self): if self.Astatus == Appoint.Status.VIOLATED: match cast(Appoint.Reason, self.Areason): case Appoint.Reason.R_NOVIOLATED: status = "未知错误,请联系管理员" case Appoint.Reason.R_LATE: status = "使用迟到" case Appoint.Reason.R_TOOLITTLE: status = "人数不足" case Appoint.Reason.R_ELSE: status = "管理员操作" else: status = self.get_Astatus_display() return status
[文档] def toJson(self): data = { 'Aid': self.Aid, # 预约编号 'Atime': self.Atime.strftime("%Y-%m-%dT%H:%M:%S"), # 申请提交时间 'Astart': self.Astart.strftime("%Y-%m-%dT%H:%M:%S"), # 开始使用时间 'Afinish': self.Afinish.strftime("%Y-%m-%dT%H:%M:%S"), # 结束使用时间 'Ausage': self.Ausage, # 房间用途 'Aannouncement': self.Aannouncement, # 预约通知 'Atype': self.get_Atype_display(), # 预约类型 'Astatus': self.get_Astatus_display(), # 预约状态 'Areason': self.Areason, 'Rid': self.Room.Rid, # 房间编号 'Rtitle': self.Room.Rtitle, # 房间名称 'yp_num': self.Ayp_num, # 院内人数 'non_yp_num': self.Anon_yp_num, # 外院人数 'major_student': { "Sname": self.major_student.name, # 发起预约人 "Sid": self.get_major_id(), }, 'students': [{ 'Sname': student.name, # 参与人姓名 'Sid': student.get_id(), } for student in self.students.all().select_related('Sid')], } return data
[文档] class CardCheckInfo(models.Model): # 这里Room使用外键的话只能设置DO_NOTHING,否则删除房间就会丢失预约信息 # 所以房间信息不能删除,只能逻辑删除 # 调用时使用appoint_obj.Room和room_obj.appoint_list Cardroom: Room = models.ForeignKey(Room, related_name='+', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='房间号') Cardstudent: Participant = models.ForeignKey( Participant, on_delete=models.CASCADE, verbose_name='刷卡者', null=True, blank=True, db_index=True) Cardtime = models.DateTimeField('刷卡时间', auto_now_add=True)
[文档] class Status(models.IntegerChoices): DOOR_CLOSE = 0, '不开门' # 开门:否 DOOR_OPEN = 1, '开门' # 开门:是
CardStatus = models.SmallIntegerField( '刷卡状态', choices=Status.choices, default=0) Message = models.CharField( '记录信息', max_length=256, null=True, blank=True) class Meta: verbose_name = "刷卡记录" verbose_name_plural = verbose_name
class LongTermAppointManager(models.Manager['LongTermAppoint']): def activated(self, this_semester=True) -> 'QuerySet[LongTermAppoint]': result = self.filter( status__in=[ LongTermAppoint.Status.APPROVED, LongTermAppoint.Status.REVIEWING, ]) if this_semester: result = result.filter( appoint__Astart__gt=CONFIG.semester_start, ) return result
[文档] class LongTermAppoint(models.Model): """ 记录长期预约所需要的全部信息 """ class Meta: verbose_name = '长期预约信息' verbose_name_plural = verbose_name appoint: Appoint = models.OneToOneField(Appoint, on_delete=models.CASCADE, verbose_name='单次预约信息') applicant: Participant = models.ForeignKey(Participant, on_delete=models.CASCADE, verbose_name='申请者') times = models.SmallIntegerField('预约次数', default=1) interval = models.SmallIntegerField('间隔周数', default=1) review_comment = models.TextField('评论意见', default='', blank=True)
[文档] class Status(models.IntegerChoices): REVIEWING = (0, '审核中') CANCELED = (1, '已取消') APPROVED = (2, '已通过') REJECTED = (3, '未通过')
status: 'int|Status' = models.SmallIntegerField('申请状态', choices=Status.choices, default=Status.REVIEWING) objects: LongTermAppointManager = LongTermAppointManager()
[文档] def create(self): '''原子化创建长期预约的全部后续子预约''' from Appointment.jobs import add_longterm_appoint conflict_week, appoints = add_longterm_appoint( appoint=self.appoint.pk, times=self.times - 1, interval=self.interval, ) return conflict_week, appoints
[文档] def cancel(self, all=False, delete=False): ''' 原子化取消长期预约以及它的子预约,不应出错 :param all: 取消全部,否则只取消未开始的预约, defaults to False :type all: bool, optional :param delete: 以数据库删除代替取消,长期预约也会级联删除, defaults to False :type delete: bool, optional :return: 取消的子预约数量 :rtype: int ''' from Appointment.appoint.manage import cancel_appoint with transaction.atomic(): # 取消子预约 appoints = self.sub_appoints(lock=True) if not all: appoints = appoints.filter(Astatus=Appoint.Status.APPOINTED) if delete: return appoints.delete()[0] count = len(appoints) for appoint in appoints: cancel_appoint(appoint, record=True, lock=False) self.status = LongTermAppoint.Status.CANCELED self.save() return count
[文档] def renew(self, times: int): '''原子化添加新的后续子预约,不应出错''' from Appointment.jobs import add_longterm_appoint times = max(0, times) with transaction.atomic(): conflict_week, appoints = add_longterm_appoint( appoint=self.appoint.pk, times=times, interval=self.interval, week_offset=self.times * self.interval, ) if conflict_week is not None: self.times += times self.save() return conflict_week, appoints
[文档] def sub_appoints(self, lock=False) -> QuerySet[Appoint]: ''' 获取时间升序的子预约,只有类型为长期预约的被视为子预约,不应出错 :param lock: 上锁,调用者需要自行开启事务, defaults to False :type lock: bool, optional :return: 时间升序的子预约 :rtype: QuerySet[Appoint] ''' from Appointment.utils.utils import get_conflict_appoints conflict_appoints = get_conflict_appoints( self.appoint, times=self.times, interval=self.interval, lock=lock) sub_appoints = conflict_appoints.filter( major_student=self.appoint.major_student, Atype=Appoint.Type.LONGTERM) return sub_appoints.order_by('Astart', 'Afinish')
[文档] def get_applicant_id(self) -> str: '''获取申请者id''' return self.applicant.get_id()
@receiver(pre_delete, sender=Appoint) def before_delete_Appoint(sender, instance, **kwargs): from Appointment.appoint.jobs import cancel_scheduler cancel_scheduler(instance.Aid)