需求抽象与模型设计#
后台开发本质不就是 CRUD 吗 😂 /doge ,因此先从数据模型上进行设计
上文有提到过需求,主要功能包括发布学习内容,学生管理,制定学习计划,学生打卡
这里有三种角色,
- 管理员:可以有所有的功能
- 学习委员:每个班级一个学习委员,负责制定本班的学习推送计划
- 学生:可以接收学习内容,并打卡
因此我们可以思考下,抽象出表
- 学习内容表,记录学习内容
- 班级表,需要有个学委的角色
- 学生表,三种角色
- 打卡表,每个学生的打卡记录
- 推送策略,同时关联学习内容和班级
学习内容表,包含学习内容、标题、简介、学习链接,为了避免直接删除,加了一个是否上线的判断来标记删除
class StudyContent(models.Model):
"""
学习内容,包含标题,简介,学习链接
"""
title = models.CharField(max_length=100, verbose_name='标题')
brief = models.TextField(verbose_name='简介')
url = models.URLField(verbose_name='页面链接')
is_online = models.BooleanField(default=False, verbose_name='学习内容是否上线')
class Meta:
verbose_name = '学习内容'
verbose_name_plural = '学习内容'
def __str__(self):
return self.title
verbose_name 是为了能在后台展示的更清晰,不然就是一个 Object
学生表、班级表,学生表是需要引用班级表的外键,并且班级表也需要引用学生表作为外键
这里就遇到了第一个坑,根据上文描述这里其实存在循环依赖,导致后续新建用户失败
因此加上 black=True
来允许创建一个没有学委的班级,也允许创建一个学生,但是不指定班级
class SchoolClass(models.Model):
"""
班级表,每个学生属于班级,每个班级一个学委
"""
name = models.CharField(max_length=100, verbose_name='班级名')
study_committee = models.OneToOneField(
'Student',
on_delete=models.SET_NULL,
null=True,
blank=True, # 允许先创建一个没有学委的班级
related_name='managed_class',
verbose_name='学习委员'
)
class Meta:
verbose_name = '班级管理'
verbose_name_plural = '班级管理'
def __str__(self):
return self.name
class Student(AbstractUser):
"""
角色表
"""
STUDENT = 'ST'
ADMIN = 'AD'
STUDY_COMMITTEE = 'SC'
ROLE_CHOICES = [
(STUDENT, '学生'),
(ADMIN, '管理员'),
(STUDY_COMMITTEE, '学习委员')
]
role = models.CharField(
max_length=2, choices=ROLE_CHOICES, default=STUDENT, verbose_name='角色类型')
class_group = models.ForeignKey(SchoolClass, on_delete=models.SET_NULL, null=True, blank=True, related_name='students', verbose_name='所属班级')
is_staff = models.BooleanField(default=True) # 默认允许登录后台
def __str__(self):
return self.username
class Meta:
verbose_name = '用户管理'
verbose_name_plural = '用户管理'
为了复用 Django 后台的用户管理功能,我们让
Student
类继承自AbstractUser
类
后续在 admin 中还需要添加如下代码,这是后话
@admin.register(Student)
class StudentAdmin(UserAdmin):
pass
推送策略表,打卡记录表
打卡表关联了学习内容做外键
推送策略表同事关联了学习内容、班级做外键
class Checkin(models.Model):
"""
学生打卡表
"""
student = models.ForeignKey(Student, on_delete=models.CASCADE, verbose_name='学生')
study_content = models.ForeignKey(
StudyContent, on_delete=models.CASCADE, related_name='checkins', verbose_name='学习内容')
checkin_time = models.DateTimeField(auto_now_add=True, verbose_name='打卡时间')
is_valid = models.BooleanField(default=True, verbose_name='打卡记录是否有效') # 默认标记为有效
class Meta:
verbose_name = '学生打卡记录管理'
verbose_name_plural = '学生打卡记录管理'
def __str__(self):
return f"{self.student} 在 {self.checkin_time} 打卡了 {self.study_content}"
class PushStrategy(models.Model):
"""
推送策略,外键关联内容,班级、推送频率和推送时间
"""
study_content = models.ForeignKey(
StudyContent, on_delete=models.CASCADE, related_name='push_strategy', verbose_name='学习内容')
class_group = models.ForeignKey('SchoolClass', on_delete=models.CASCADE, related_name='push_strategies', verbose_name='班级')
frequency = models.CharField(max_length=20, verbose_name='推送频率')
push_time = models.DateTimeField(verbose_name='推送开始时间')
is_online = models.BooleanField(default=False, verbose_name='推送策略是否上线')
class Meta:
verbose_name = '推送策略管理'
verbose_name_plural = '推送策略管理'
unique_together = [['study_content', 'class_group']] # 保证这两个键唯一
def __str__(self):
return self.study_content.title
至此我们的模型就基本开发完了,每次修改完模型,都需要运行
python manage.py makemigrations
python manage.py migrate