FastAPI
六、数据库操作
FastAPI支持操作各种数据库,但本身并没有内置关于任何数据库相关的模块。因此我们可以根据需求使用任何数据库,包括关系型(SQL)数据库,例如:PostgreSQL、MySQL、SQLite、Oracle、Microsoft SQL Server,也包括非关系数据库(NoSQL),例如:Redis、MongoDB、Elasticsearch、向量数据库、图数据库等。
6.1 原生操作
6.1.1 pymysql
main.py
,代码:
import uvicorn
from fastapi import FastAPI, status
from pydantic import BaseModel, Field
import pymysql
# 初始化数据库
conn = pymysql.connect(user="root", password="123", host="127.0.0.1", port=3306, database="school")
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)
app = FastAPI()
def dict_to_sql(table, model):
"""
把字典数据转化SQL添加语句
:param table: 表名
:param model: 数据模型
:return:
"""
data = dict(model)
query = f"insert into {table} "
# 组装字段列表
fields = "`,`".join(data.keys())
query += f"(`{fields}`) values "
values = str(list(data.values()))
query += f"({values[1:-1]})"
return query
class StudentIn(BaseModel):
name: str = Field(title="姓名")
age: int = Field(title="年龄")
class_id: str = Field(title="班级")
sex: int = Field(title="性别")
description: str = Field(title="个人介绍")
@app.post("/student", status_code=status.HTTP_201_CREATED)
async def index(student: StudentIn):
"""
添加学生
:param student:
:return:
"""
query = dict_to_sql("student", student)
cursor.execute(query)
conn.commit()
return {
"id": cursor.lastrowid,
**dict(student)
}
if __name__ == '__main__':
uvicorn.run("main:app", reload=True)
上面的pymysql是同步操作数据库,所以实际上在开发中如果使用pymysql的话,没有进行异步,会导致两个问题:
- 无法发挥FastAPI的高性能,也就是无法在高并发场景下发挥FastAPI的异步特性。
- FastAPI的底层设计是基于异步的,所以pymysql在某些场景下会出现bug。
fastAPI提供给pymysql的同步转异步的方案:
pip install mysql-connector-python pymysql
连接数据库,db_conn.py
,代码:
from fastapi import Depends
import mysql.connector
def get_db_connection():
connection = mysql.connector.connect(
host='localhost',
port=3306,
user="root",
password="123456",
database="example_db"
)
return connection
def get_db():
connection = get_db_connection()
db = connection.cursor()
try:
yield db
finally:
db.close()
connection.close()
操作数据库,main.py
,代码:
import uvicorn
from fastapi import FastAPI, Depends
import mysql.connector
from mysql.connector import cursor
app = FastAPI()
def get_db_connection():
connection = mysql.connector.connect(
host='localhost',
port=3306,
user="root",
password="123",
database="school"
)
return connection
def get_db():
connection = get_db_connection()
db = connection.cursor()
try:
yield db
finally:
db.close()
connection.close()
@app.get("/users")
async def get_users(db: cursor.MySQLCursor = Depends(get_db)):
query = "SELECT * FROM tb_student"
db.execute(query)
result = db.fetchall()
if result:
return {"users": result}
else:
return {"error": "User not found"}
if __name__ == '__main__':
uvicorn.run("main:app", reload=True)
6.2 ORM
在大型的web开发中,我们肯定会用到数据库操作,那么FastAPI也支持数据库的开发,你可以用 PostgreSQL、MySQL、 SQLite Oracle 等。本文用MySQL为例。我们看下在fastapi是如何操作设计数据库的。
fastapi是一个很优秀的异步web框架,但是缺少一个合适的orm,fastAPI早期官方文档中数据操作代码里面的例子使用的是sqlalchemy,后面官方废弃了这块代码,选择让用户自己根据业务自主选择合适的ORM。
6.2.1 Tortoise
Tortoise ORM 是受 Django框架的ORM启发的易于使用的异步 ORM (对象关系映射器),基于asyncio模块开发的。
官方文档:https://tortoise.github.io/
依赖:
Tortoise ORM 必须运行CPython >= 3.8
目前支持以下数据库:
- PostgreSQL >= 9.4(使用
asyncpg
)- SQLite(使用
aiosqlite
)- MySQL/MariaDB(使用
aiomysql
或使用asyncmy)- Microsoft SQL Server/Oracle(使用asyncodbc)
在终端下使用以下命令进行安装:
pip install tortoise-orm[aiomysql]
快速使用
配置数据库信息,settings.py
,代码:
""""项目配置"""
# tortoise-orm数据库配置
TORTOISE_ORM = {
"connections": {
"default": {
'engine': 'tortoise.backends.mysql', # MySQL or Mariadb
'credentials': { # 连接参数
'host': '127.0.0.1', # 数据库IP/域名地址
'port': 3306, # 端口
'user': 'root', # 连接账户
'password': '123', # 连接密码
'database': 'school', # 数据库
'charset': 'utf8mb4', # 编码
'minsize': 1, # 连接池中的最小连接数
'maxsize': 5, # 连接池中的最大连接数
"echo": True # 执行数据库操作时,是否打印SQL语句
}
}
},
'apps': { # 默认所在的应用目录
'models': { # 数据模型的分组名
'models': ['models'], # 模型所在目录文件的导包路径[字符串格式]
'default_connection': 'default', # 上一行配置中的模型列表的默认连接配置
}
},
# 当use_tz=False,当前tortoise-orm会默认使用当前程序所在操作系统的时区,
'use_tz': True,
# 当use_tz=True时,当前tortoise-orm会默认使用timezone配置项中的时区
'timezone': 'Asia/Shanghai' # 自定义时区,
}
完成上面的数据配置操作以后,我们需要在上面TORTOISE_ORM
配置字典的apps.models的models
中填写的路径里面补充上数据表模型,models.py
,代码:
from tortoise.models import Model # 类似SQLALchemy的BaseModel
from tortoise import fields # 类似SQLAlchemy的mapped_column
"""
tortoise中所有的数据模型,必须直接或间接继承于tortoise.models.Model
"""
class Class(Model):
"""班级"""
# tortoise ORM默认生成一个主键,默认叫pk,
# 如果不想要pk作为默认的主键,可以自己重新声明一个新的字段,把这个字段设置为pk=True,则会自动覆盖原来的pk主键
id = fields.IntField(pk=True)
name = fields.CharField(max_length=255, unique=True, description='班级名称')
def __repr__(self):
"""类似SQLAlchemy的__repr__,打印模型方法,这里也必须返回字符串格式"""
# tortoise中的self.pk用于代表pk=True的那个字段值
return f"Class(id={self.pk}, name={self.name})"
class Teacher(Model):
"""老师"""
id = fields.IntField(pk=True, description='主键')
name = fields.CharField(max_length=255, description='姓名')
tno = fields.IntField(unique=True, description='工号')
password = fields.CharField(max_length=255, description='密码')
def __repr__(self):
return f"Teacher(id={self.id}, tno={self.tno}, name={self.name})"
class Student(Model):
"""学生"""
id = fields.IntField(pk=True, description='主键')
sno = fields.IntField(description='学号')
name = fields.CharField(max_length=255, description='姓名')
password = fields.CharField(max_length=255, description='密码')
# 一对多
class_ = fields.ForeignKeyField('models.Class', related_name='students')
# 多对多
courses = fields.ManyToManyField('models.Course', related_name='students', description='学生选课表')
def __repr__(self):
return f"Student(id={self.id}, sno={self.sno}, name={self.name})"
class Course(Model):
"""课程"""
id = fields.IntField(pk=True, description='主键')
name = fields.CharField(max_length=255, description='课程名')
teacher = fields.ForeignKeyField('models.Teacher', related_name='courses', description='课程讲师')
def __repr__(self):
return f"Course(id={self.id}, name={self.name}, teacher={self.teacher.name})"
常用模型字段声明
模型字段 | python格式 | mysql格式 | 描述 |
---|---|---|---|
fields.BigIntField() | int | big int | 长整型 |
fieds.IntField() | int | int | 整型 |
fieds.SmallIntField() | int | smallint | 整型 |
fields.DecimalField(max_digits=整体长度, decimal_places=小数点后的小数位) | decimal.Decimal | numeric | 浮点型的定点数 |
fields.FloatField() | float | float | 浮点型 |
fields.BooleanField() | bool | bool/tiny | 布尔类型 |
fields.CharField() | str | varchar() | 可变长度字符串 |
fields.TextField() | str | text | 文本类型 |
fields.DateField() | datetime.date | date | 日期格式,“%Y-%m-%d” |
fields.TimeField() | datetime.time | time | 时间格式,“%H:%M:%S” |
fields.DatetimeField() | datetime.datetime | datetime | 日期时间格式,“%Y-%m-%d %H:%M:%S” |
fields.IntEnumField() | enum.Enum | enum | 单选类型,成员必须是整型 |
fields.CharEnumField() | enum.Enum | enum | 单选类型,成员必须是字符串 |
fields.UUIDField() | uuid.uuid4() | str | uuid格式的字符串 |
fields.OneToOneField(“主模型导包路径”, related_name=“反向字段”, on_delete=fields.CASCADE) | 1对1,外键字段 | ||
fields.ForeignKeyField(“主模型导包路径”, related_name=“反向字段”) | 1对多,外键字段 | ||
fields.ManyToManyField(“主模型导包路径”, related_name=“反向字段”) | 多对多,外键字段 |
注册Tortoise到项目中,main.py
,代码:
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from tortoise.contrib.fastapi import register_tortoise
import settings
app = FastAPI()
# 把Tortoise-orm注册到App应用对象中
register_tortoise(
app,
config=settings.TORTOISE_ORM,
generate_schemas=True,
add_exception_handlers=True,
)
if __name__ == '__main__':
uvicorn.run('main:app', host='0.0.0.0', port=8000, reload=True)
数据迁移
如果希望让tortoiseORM也像SQLAlchemy那样具备数据迁移功能【根据python代码中的数据模型自动建表】,可以考虑使用aerich
迁移工具来完成数据迁移。aerich
是一种开源的数据库ORM迁移工具,需要结合tortoise
异步orm框架使用。安装aerich
命令如下:
pip install aerich
首次运行需要初始化数据迁移工具
要使用aerich,必须在tortoiseORM的连接配置信息中新增关于aerich的模型。settings.py
,代码:
# TORTOISE ORM的数据库连接配置
TORTOISE_ORM = {
....
'apps': {
'models': {
'models': ['models', "aerich.models"], # "aerich.models" 就是新增内容
'default_connection': 'default',
}
},
...
}
如果是项目开发的首次进行数据库迁移,则需要先进行aerich插件的初始化配置(这个只需要操作一次),项目根目录下执行[main.py入口文件所在目录下]:
# 切换到main.py所在目录下
cd xxx
aerich init -t settings.TORTOISE_ORM
# settings.TORTOISE_ORM 表示tortoiseORM配置信息所在导包路径
说明:
初始化完会在当前目录生成一个文件:pyproject.toml和一个文件夹:migrations
pyproject.toml
:保存配置文件路径,低版本可能是aerich.ini
migrations
:存放数据库的迁移历史文件
初始化数据库(一般情况下只用一次)
在项目根目录下使用终端执行初始化数据库的命令,如下:
aerich init-db
执行这段命令,aerich会自动根据TORTOISE_ORM
配置字典的apps.models的models
中填写的路径搜索所有直接或间接继承了tortoise.models.Model
模型基类的所有数据表模型,根据这些数据表模型自动创建数据表。执行效果:
数据迁移的用法
命令 | 描述 |
---|---|
aerich migrate [–name 本次操作的备注信息] | 根据当前模型的实际结构生成一个迁移文件记录,备注信息建议是英文的 |
aerich upgrade | 对数据迁移进行升级操作(0->1->2…->n),表示把迁移记录中的SQL语句执行并同步到数据库中 |
aerich downgrade | 对数据迁移进行降级操作(n->…->2->1->0),表示回滚操作 |
aerich history | 查看数据库中的迁移版本历史 |
常用操作
添加数据
模型类.create()
使用模型类方法添加一条数据
student = await Student.create(name="张无忌", password='123456', sno=2009, class__id=1)
模型对象.save()
使用模型对象方法添加一条数据
student = Student(name="张无忌", password='123456', sno=2009, class__id=1)
await student.save()
模型类.bulk_create
批量添加多条数据
student_list = [
Student(name="张晓明", password='123456', sno=2007, class__id=1),
Student(name="张晓亮", password='123456', sno=2010, class__id=1),
]
await Student.bulk_create(student_list)
查询数据
get
获取一条数据,获取不到数据则报错
from tortoise import exceptions
try:
student = await Student.get(id=1)
except exceptions.DoesNotExist as e:
# 查不到数据
print(e)
get_or_none
获取一条数据,获取不到则返回None,不会报错
student = await Student.get_or_none(id=1)
all
查询所有数据,返回所有数据集(QuerySet对象),如果不加任何查询过滤条件,则默认返回表中的所有记录。
students = await Student.all()
更新数据
更新一条数据
# 先查询数据
student = await Student.get_or_none(id=1)
# 如果有数据,则修改模型对象
if student:
student.name = "张晓龙"
student.sex = True
await student.save() # 保存数据,达到修改目的
更新一条或多条数据,取决于过滤条件
await Student.filter(id=1).update(name='张大明')
删除数据
删除一条数据
student = await Student.get_or_none(id=1)
if student:
await student.delete()
删除一条或多条数据,取决于过滤条件
await Student.filter(id=1).delete()
过滤查询
filter
根据条件查询数据,返回满足条件的数据集(QuerySet对象),后续可以使用 all()
方法获取所有的查询结果,或者使用 first()
方法获取第一个结果。
# 查询所有的男生
students = await Student.filter(sex=True).all()
for student in students:
print(student.id, student.name)
# 获取名字为'xiaoming'的第一个数据
student = await Student.filter(name="xiaoming").first()
if student:
print(student.id, student.name)
else:
print("No Found")
使用比较运算符
进行条件过滤
# = 获取男生的数据
students = await Student.filter(sex=True).all()
# __not 获取学号(sno)不等于1的学生数据
students = await Student.filter(sno__not=1).all()
# __gt 获取年龄(age)大于16的数据
students = await Student.filter(age__gt=16).all()
# __gte 获取id大于等于1的数据
students = await Student.filter(id__gte=1).all()
# __lt 获取age小于16的数据
students = await Student.filter(age__lt=16).all()
# __lte 获取age小于等于18的数据
students = await Student.filter(age__lte=18).all()
使用成员运算符
作为过滤条件
# __in 获取姓名 在 指定列表中的数据
names = ['小明', '小红']
students = await Student.filter(name__in=names).all()
for student in students:
print(student.id, student.name)
# __not_in 获取姓名 不在 指定列表中的数据
names = ['小明', '小红']
students = await Student.filter(name__not_in=names).all()
for student in students:
print(student.id, student.name)
使用模糊查询
作为过滤条件,类似SQL语句中的Like关键字。
"""
Tortoise ORM 提供了icontains、istartswith、iendswith等操作符进行模糊查询。
"""
# 字段istartswith 以指定条件开头
# 姓张的同学
student = await Student.filter(sno__istartswith='张')
# 名字里面包含'白'字的同学
student = await Student.filter(name__icontains='白')
# 手机尾号是5989的学生
student = await Student.filter(mobile__iendswith='5989')
__range
查询数值大小在指定区间的数据
# 查询学号在2001-2024之间的
students = await Student.filter(sno__range=[2001, 2024]).all()
for student in students:
print(student.id, student.name)
__isnull:
是否为空(IS NULL)
# 查询学生姓名为空的数据
students = await Student.filter(name__isnull=True).all()
__regex:
正则表达式匹配(REGEXP 或 LIKE,取决于数据库)
__iregex:
不区分大小写的正则表达式匹配
# 查询名字匹配正则表达式的数据
pattern = r'^张.*' # 以'张'开头的名字
students = await Student.filter(name__regex=pattern).all()
exclude()
方法用于排除满足条件的数据,返回不满足条件的数据集。与filter结果相反。
# 获取名字不是'赵小明'的所有数据
students = await Student.exclude(name='赵小明').all()
for student in students:
print(student.id, student.name)
结果处理
count()
方法用于统计满足条件的数据数量。
# 统计女生的数量
count = await Student.filter(sex=False).count()
print(count)
order_by()
方法用于按照指定字段排序查询结果。
# 按id升序(从小到大)获取所有数据
students = await Student.all().order_by("id")
for student in students:
print(student.id, student.name)
# 按id降序(从大到小,字段左边使用减号)获取所有数据
students = await Student.all().order_by("-id")
for student in students:
print(student.id, student.name)
limit
和 offset
用于限制返回的结果数量和跳过指定数量的结果。
limit
对结果进行数量限制
offset
查询结果提取的开始下标
# limit 和 offset() 方法可以用于限制返回的结果数量和跳过指定数量的结果。
# 获取前5个用户
sutdents_0_5 = await Student.all().limit(5)
for student in sutdents_0_5:
print(student.id, student.name)
# 跳过前5个用户,再获取5个用户
sutdents_6_10 = await Student.all().offset(5).limit(5)
for student in sutdents_6_10:
print(student.id, student.name)
values()
指定列查询,表示只查询指定的单个或多个字段。
print(await students.courses.values("id","name"))
连表查询
连表查询指定外键所在模型的部分字段
students = await Student.all().values("name", "class___name", "courses__name")
连表查询指定外键所在模型的全部字段
students = await Student.all().prefetch_related("class_", "courses")
案例:学生选课系统
基于tortoise-orm实现学生管理,项目目录结构如下:
school/ # 项目根目录
├── static/ # 项目静态文件[css/js/images/视频/音频]文件存储目录
│ └── images/ # 图片存储目录
├── apps/ # 项目应用目录
│ ├── __init__.py
│ ├── news/ # 新闻相关的应用代码
│ │ └── models.py # 新闻相关模型文件保存目录
│ ├── students/ # 学生相关的应用代码
│ │ ├── templates/ # 学生相关模板文件保存目录
│ │ ├── models.py # 学生相关模型文件保存目录
│ │ ├── api.py # 学生相关接口文件,前后端分离【目前不存要创建】
│ │ └── views.py # 学生相关视图文件,前后端不分离
│ └── user/ # 用户相关的应用代码
│ └── models.py # 用户相关模型的保存文件
├── main.py # 项目程序入口,从这里启动项目
├── migrations/ # 数据迁移文件存储目录
│ └── models/ # 数据迁移的历史版本文件
└── settings.py # 项目的配置文件
school/settings.py
,代码:
""""项目配置"""
# tortoise-orm数据库配置
TORTOISE_ORM = {
"connections": {
"default": {
'engine': 'tortoise.backends.mysql', # MySQL or Mariadb
'credentials': { # 连接参数
'host': '127.0.0.1', # 数据库IP/域名地址
'port': 3306, # 端口
'user': 'root', # 连接账户
'password': '123', # 连接密码
'database': 'school', # 数据库
'charset': 'utf8mb4', # 编码
'minsize': 1, # 连接池中的最小连接数
'maxsize': 5, # 连接池中的最大连接数
"echo": True # 执行数据库操作时,是否打印SQL语句
}
}
},
'apps': { # 默认所在的应用目录
'models': { # 数据模型的分组名
'models': ['apps.news.models', 'apps.user.models', 'apps.students.models', 'aerich.models'], # 模型所在目录文件的导包路径[字符串格式]
'default_connection': 'default', # 上一行配置中的模型列表的默认连接配置
}
},
# 当use_tz=False,当前tortoise-orm会默认使用当前程序所在操作系统的时区,
'use_tz': True,
# 当use_tz=True时,当前tortoise-orm会默认使用timezone配置项中的时区
'timezone': 'Asia/Shanghai' # 自定义时区,
}
school/apps/students/models.py
,代码:
"""模型文件 Model"""
from tortoise import models, fields
class Class(models.Model):
# 字段列表
id = fields.IntField(pk=True, description='主键')
name = fields.CharField(max_length=50, unique=True, description='班级名称')
# 元数据
class Meta:
table = "school_class"
description="班级信息"
def __repr__(self):
return f"Class (id={self.pk}, name={self.name})"
class Student(models.Model):
id = fields.IntField(pk=True, description='主键')
sno = fields.IntField(description='学号')
name = fields.CharField(max_length=50, unique=True, description='班级名称')
password = fields.CharField(max_length=255, description='密码')
class_ = fields.ForeignKeyField('models.Class', related_name='students', description='所属班级')
courses = fields.ManyToManyField('models.Course', related_name='students', description='课表')
sex = fields.BooleanField(default=True, null=True, description='性别')
# auto_now_add=True 当新增数据时,默认使用当前时间对象作为默认值填写到当前字段
created_time = fields.DatetimeField(auto_now_add=True, description='创建时间')
# auto_now=True 当数据被修改时,默认使用当前时间对象作为默认值写入到当前字段
updated_time = fields.DatetimeField(auto_now=True, description="更新时间")
# 元数据
class Meta:
table = "school_student"
description="学生信息"
def __repr__(self):
return f"Student(id={self.id}, sno={self.sno}, name={self.name})"
class Teacher(models.Model):
id = fields.IntField(pk=True, description='主键')
name = fields.CharField(max_length=255, description='姓名')
sex = fields.BooleanField(default=True, description='性别')
tno = fields.IntField(unique=True, description='工号')
password = fields.CharField(max_length=255, description='密码')
# 元数据
class Meta:
table = "school_teacher"
description="老师信息"
def __repr__(self):
return f"Teacher(id={self.id}, tno={self.tno}, name={self.name})"
class Course(models.Model):
id = fields.IntField(pk=True, description='主键')
name = fields.CharField(max_length=255, description='课程名')
teacher = fields.ForeignKeyField('models.Teacher', related_name='courses', description='课程讲师')
# 元数据
class Meta:
table = "school_course"
description="课程信息"
def __repr__(self):
return f"Teacher(id={self.id}, name={self.name}, teacher={this.teacher.name})"
school/main.py
,代码:
"""程序入口,将来在这个文件中启动项目"""
import uvicorn
from fastapi import FastAPI
from apps.students.views import router as student_router
from fastapi.staticfiles import StaticFiles
from tortoise.contrib.fastapi import register_tortoise
import settings
app = FastAPI()
# 把tortoise-orm注册到app中
register_tortoise(
app,
config=settings.TORTOISE_ORM,
generate_schemas=True,
add_exception_handlers=True,
)
# 允许外界通过指定路径访问到服务端对应目录下的文件信息
app.mount("/static", StaticFiles(directory="static"))
app.include_router(student_router, prefix='/students')
if __name__ == '__main__':
uvicorn.run("main:app", host='127.0.0.1', port=8000, reload=True)
接下来,就可以编写操作数据的视图代码了,school/students/views.py
,代码:
"""视图文件 View"""
# 导入路由类
from fastapi import APIRouter
from fastapi.requests import Request
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="apps/students/templates")
# 创建一个分组路由对象
router = APIRouter()
"""班级管理"""
@router.get("/class/add")
async def add_class(request: Request):
"""添加班级[表单功能]"""
return templates.TemplateResponse("add_class.html", locals())
班级管理-添加班级功能
展示添加班级的表单数据,school/apps/students/templates/add_class.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加班级信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 800px;
height: 440px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
form{
width: 700px;
margin: 20px auto 20px;
}
form .item{
height: 50px;
line-height: 50px;
border-bottom: 1px solid rgba(0,0,0,.1);
margin-bottom: 10px;
}
form .item input{
outline: none;
height: 24px;
line-height: 24px;
font-size: 16px;
text-indent: 4px;
background: transparent;
border: 1px solid rgba(0,0,0,.3);
}
.submit{
position: absolute;
bottom: 20px;
left: 0;
right: 0;
margin: auto;
width: 120px;
height: 46px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,.3);
cursor: pointer;
outline: none;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>添加班级信息</h1>
<form action="/students/class/add" method="post">
<div class="item">
<label>班级名称:</label>
<input type="text" name="name">
</div>
<button class="submit">提交</button>
</form>
</div>
</div>
</body>
</html>
``school/students/views.py`,代码:
"""视图文件 View"""
# 导入路由类
from fastapi import APIRouter, Form
from fastapi.requests import Request
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="apps/students/templates")
# 创建一个分组路由对象
router = APIRouter()
"""班级管理"""
@router.get("/class/add")
async def add_class(request: Request):
"""添加班级[表单展示]"""
return templates.TemplateResponse("add_class.html", locals())
@router.post('/class/add')
async def add_class(name: str = Form()):
"""添加班级[数据处理]"""
print(name)
return {"提交成功!"}
运行项目之前,先保证school数据库中的数据表没有与项目同名的(当然,也可以清空school数据库中所有的数据表):
cd xxx/school
aerich init -t settings.TORTOISE_ORM
aerich init-db
访问路径:http://127.0.0.1:8000/students/class/add,效果如下:
有了基本效果以后,接下来,我们通过学习Tortoise-orm的数据库操作来完成业务功能。
基于添加操作完成班级数据的添加
school/apps/students/views.py
,代码:
"""视图文件 View"""
from fastapi import APIRouter, Form
from fastapi.requests import Request
from fastapi.templating import Jinja2Templates
from . import models
from tortoise.exceptions import IntegrityError
templates = Jinja2Templates(directory="apps/students/templates")
# 创建一个分组路由对象
router = APIRouter()
"""班级管理"""
@router.get("/class/add")
async def add_class(request: Request):
"""添加班级[表单展示]"""
return templates.TemplateResponse("add_class.html", locals())
@router.post('/class/add')
async def add_class(request: Request, name: str = Form()):
"""添加班级[数据处理]"""
try:
# 添加数据,方式1,通过模型类提供的create来添加数据
# class_ = await models.Class.create(name=name)
# print(class_)
# 添加数据,方式2,通过模型类实例对象的save方法来添加数据
class_ = models.Class(name=name)
await class_.save()
errors = "创建成功!"
url = "/students/class"
return templates.TemplateResponse("success.html",locals())
except IntegrityError as e:
errors = "当前班级已存在,不能重复添加!"
url = "/students/class/add"
return templates.TemplateResponse("errors.html", locals())
except Exception as e:
errors = "位置的错误,导致添加数据失败!"
url = "/students/class/add"
return templates.TemplateResponse("errors.html", locals())
新增错误过渡页面和成功过渡页面
school/apps/students/templates/errors.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
body{
margin: 0;
padding: 0;
}
.wrapper{
background: url("/static/images/errors.png") no-repeat;
background-position: center 20px;
background-size: 20%;
position: relative;
padding: 300px;
}
.title{
font-size: 36px;
}
p{
text-align: center;
}
</style>
</head>
<body>
<div class="wrapper">
<p class="title">{{ errors }}</p>
<p>页面跳转中,<span class="has_time">5</span>秒后跳转</p>
</div>
<script>
var time = 3;
var has_time = document.querySelector('.has_time')
setInterval(()=>{
time-=1;
if(time<1){
location.href="{{ url }}";
}else{
has_time.innerHTML = time;
}
}, 1000)
</script>
</body>
</html>
school/apps/students/templates/success.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
body{
margin: 0;
padding: 0;
}
.wrapper{
background: url("/static/images/success.jpg") no-repeat;
background-position: center 20px;
background-size: 40%;
position: relative;
padding: 300px;
}
.title{
font-size: 36px;
}
p{
text-align: center;
}
</style>
</head>
<body>
<div class="wrapper">
<p class="title">{{ errors }}</p>
<p>页面跳转中,<span class="has_time">5</span>秒后跳转</p>
</div>
<script>
var time = 3;
var has_time = document.querySelector('.has_time')
setInterval(()=>{
time-=1;
if(time<1){
location.href="{{ url }}";
}else{
has_time.innerHTML = time;
}
}, 1000)
</script>
</body>
</html>
提示图标如下,保存即可。
班级管理-班级列表功能
班级列表页的默认模板效果,school/apps/students/templates/list_class.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加班级信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 1200px;
height: 640px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
.header-item, .list-item{
height: 48px;
line-height: 48px;
cursor: pointer;
display: flex;
margin: 15px;
}
.list-item .keys, .pk, .fields{
flex: 2;
border-bottom: 1px solid rgba(0,0,0,.3);
text-align: center;
}
.pk, .list-item .keys:first-child{
flex: 1;
}
.btn{
border: 1px solid rgba(0,0,0,.3);
padding: 8px 16px;
}
.btn:hover{
border-radius: 6px;
background: rgba(0,0,0,0.6);
color: #fff;
}
.pagination{
text-align: center;
}
.pagination span,.pagination a{
text-decoration: none;
font-size: 16px;
color: #666;
border: 1px solid rgba(0,0,0,0.3);
padding: 5px 12px;
}
.pagination span:hover,.pagination a:hover{
border: 1px solid rgba(0,0,0,0.8);
color: #333;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>班级信息列表 <a class="btn add-btn" href="/students/class/add">新建班级</a></h1>
<!-- 表头 -->
<div class="header-item">
<div class="pk">ID编号</div>
<div class="fields">班级</div>
<div class="fields">操作</div>
</div>
<div class="list-item">
<div class="keys">1</div>
<div class="keys">人工2023班</div>
<div class="keys">
<span class="btn">编辑</span>
<span class="btn">删除</span>
</div>
</div>
<div class="list-item">
<div class="keys">2</div>
<div class="keys">人工2024班</div>
<div class="keys">
<span class="btn">编辑</span>
<span class="btn">删除</span>
</div>
</div>
<div class="pagination">
<a href="?pn=1">1</a>
<a href="?pn=2">2</a>
<a href="?pn=3">3</a>
</div>
</div>
</div>
</body>
</html>
school/apps/students/views.py
,代码:
"""视图文件 View"""
from fastapi import APIRouter, Form
from fastapi.requests import Request
from fastapi.templating import Jinja2Templates
from . import models
from tortoise.exceptions import IntegrityError
from tortoise.functions import Count
import math
templates = Jinja2Templates(directory="apps/students/templates")
# 创建一个分组路由对象
router = APIRouter()
"""班级管理"""
@router.get("/class/add")
async def add_class(request: Request):
"""添加班级[表单展示]"""
return templates.TemplateResponse("add_class.html", locals())
@router.post('/class/add')
async def add_class(request: Request, name: str = Form()):
"""添加班级[数据处理]"""
try:
# 添加数据,方式1,通过模型类提供的create来添加数据
# class_ = await models.Class.create(name=name)
# print(class_)
# 添加数据,方式2,通过模型类实例对象的save方法来添加数据
class_ = models.Class(name=name)
await class_.save()
errors = "创建成功!"
url = "/students/class" # 班级列表的url地址
return templates.TemplateResponse("success.html",locals())
except IntegrityError as e:
errors = "当前班级已存在,不能重复添加!"
url = "/students/class/add" # 返回添加页面的表单url地址
return templates.TemplateResponse("errors.html", locals())
except Exception as e:
errors = "未知的错误,导致添加数据失败!"
url = "/students/class/add" # 返回添加页面的表单url地址
return templates.TemplateResponse("errors.html", locals())
@router.get("/class")
async def list_class(request: Request, pn:int = 1):
"""
班级列表
# 数据分页,依靠SQL语句来完成的
# 第1页 offset 0, limit 6 下标:0,1,2,3,4,5
# 第2页 offset 6, limit 6 下标: 6,7,8,9,10,11
# 第3页 offset 12, limit 6 下标: 12,...
"""
# 每一页展示的数据量
limit = 6
# 获取总数据量
class_list = await models.Class.all()
total = len(class_list)
# 向上取整获取总页数
max_pn = math.ceil(total/limit)
# 判断当前页码不能超出总页码的范围
if pn > max_pn:
pn = max_pn
if pn < 1:
pn = 1
start = (pn-1) * limit # 每一页数据的查询开始位置
class_list = await models.Class.all().limit(limit).offset(start)
return templates.TemplateResponse('list_class.html', locals())
school/apps/students/templates/list_class.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加班级信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 1200px;
height: 640px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
.header-item, .list-item{
height: 48px;
line-height: 48px;
cursor: pointer;
display: flex;
margin: 15px;
}
.list-item .keys, .pk, .fields{
flex: 2;
border-bottom: 1px solid rgba(0,0,0,.3);
text-align: center;
}
.pk, .list-item .keys:first-child{
flex: 1;
}
.btn{
border: 1px solid rgba(0,0,0,.3);
padding: 8px 16px;
}
.btn:hover{
border-radius: 6px;
background: rgba(0,0,0,0.6);
color: #fff;
}
.pagination{
text-align: center;
}
.pagination span,.pagination a{
text-decoration: none;
font-size: 16px;
color: #666;
border: 1px solid rgba(0,0,0,0.3);
padding: 5px 12px;
}
.pagination span:hover,.pagination a:hover{
border: 1px solid rgba(0,0,0,0.8);
color: #333;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>班级信息列表 <a class="btn add-btn" href="/students/class/add">新建班级</a></h1>
<!-- 表头 -->
<div class="header-item">
<div class="pk">ID编号</div>
<div class="fields">班级</div>
<div class="fields">操作</div>
</div>
{% for class in class_list %}
<div class="list-item">
<div class="keys">{{ class.id }}</div>
<div class="keys">{{ class.name }}</div>
<div class="keys">
<span class="btn">编辑</span>
<span class="btn">删除</span>
</div>
</div>
{% endfor %}
<div class="pagination">
<a href="?pn=1">首页</a>
{% if pn > 1 %}
<a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
{% endif %}
<span href="?pn={{ pn }}">{{ pn }}</span>
{% if pn < max_pn %}
<a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
{% endif %}
<a href="?pn={{ max_pn }}">尾页</a>
</div>
</div>
</div>
</body>
</html>
访问http://127.0.0.1:8000/students/class,效果如下:
班级管理-删除班级功能
school/apps/students/views.py
,代码:
from tortoise.exceptions import IntegrityError, DoesNotExist
.....
前面代码省略
.....
@router.get('/class/del/{id}')
async def del_class(request: Request, id: int):
"""删除班级"""
# # 删除方式1:
# try:
# # 使用get方法获取一个数据,查不到或者查询到的数据有多条都会报错
# class_ = await models.Class.get(id=id)
# # 使用first方法获取一个数据,查不到数据会返回None,查询到多条结果,则返回下标为0的数据作为结果
# except DoesNotExist:
# errors = "不存在的班级,无法删除!"
# url = "/students/class" # 返回班级列表页面的url地址
# return templates.TemplateResponse("errors.html", locals())
# 删除方式2:
class_ = await models.Class.filter(id=id).first()
if class_:
await class_.delete()
errors = "删除成功!"
url = "/students/class" # 班级列表的url地址
return templates.TemplateResponse("success.html", locals())
school/apps/students/templates/list_class.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加班级信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 1200px;
height: 640px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
.header-item, .list-item{
height: 48px;
line-height: 48px;
cursor: pointer;
display: flex;
margin: 15px;
}
.list-item .keys, .pk, .fields{
flex: 2;
border-bottom: 1px solid rgba(0,0,0,.3);
text-align: center;
}
.pk, .list-item .keys:first-child{
flex: 1;
}
.btn{
border: 1px solid rgba(0,0,0,.3);
padding: 8px 16px;
}
.btn:hover{
border-radius: 6px;
background: rgba(0,0,0,0.6);
color: #fff;
}
.pagination{
text-align: center;
}
.pagination span,.pagination a{
text-decoration: none;
font-size: 16px;
color: #666;
border: 1px solid rgba(0,0,0,0.3);
padding: 5px 12px;
}
.pagination span:hover,.pagination a:hover{
border: 1px solid rgba(0,0,0,0.8);
color: #333;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>班级信息列表 <a class="btn add-btn" href="/students/class/add">新建班级</a></h1>
<!-- 表头 -->
<div class="header-item">
<div class="pk">ID编号</div>
<div class="fields">班级</div>
<div class="fields">操作</div>
</div>
{% for class in class_list %}
<div class="list-item">
<div class="keys">{{ class.id }}</div>
<div class="keys">{{ class.name }}</div>
<div class="keys">
<span class="btn">编辑</span>
<a href="/students/class/del/{{ class.id }}"><span class="btn">删除</span></a>
</div>
</div>
{% endfor %}
<div class="pagination">
<a href="?pn=1">首页</a>
{% if pn > 1 %}
<a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
{% endif %}
<span href="?pn={{ pn }}">{{ pn }}</span>
{% if pn < max_pn %}
<a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
{% endif %}
<a href="?pn={{ max_pn }}">尾页</a>
</div>
</div>
</div>
</body>
</html>
班级管理-更新班级功能
服务端提供显示更新数据表单视图函数,school/apps/students/views.py
,代码:
@router.get("/class/edit/{id}")
async def edit_class(request: Request, id: int):
"""显示编辑班级数据的表单页面"""
class_ = await models.Class.filter(id=id).first()
"""避免写面条式代码不利于维护,不利于后续人员的查看"""
# if class_:
# return templates.TemplateResponse("edit_class.html", locals())
# else:
# errors = "当前班级不存在,请重新确认!"
# url = "/students/class" # 班级列表的url地址
# return templates.TemplateResponse("errors.html", locals())
if not class_:
errors = "当前班级不存在,请重新确认!"
url = "/students/class" # 班级列表的url地址
return templates.TemplateResponse("errors.html", locals())
return templates.TemplateResponse("edit_class.html", locals())
在列表页中,提供点击跳转到修改指定班级的表单页面的a标签,school/apps/students/templates/list_class.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加班级信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 1200px;
height: 640px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
.header-item, .list-item{
height: 48px;
line-height: 48px;
cursor: pointer;
display: flex;
margin: 15px;
}
.list-item .keys, .pk, .fields{
flex: 2;
border-bottom: 1px solid rgba(0,0,0,.3);
text-align: center;
}
.pk, .list-item .keys:first-child{
flex: 1;
}
.btn{
border: 1px solid rgba(0,0,0,.3);
padding: 8px 16px;
}
.btn:hover{
border-radius: 6px;
background: rgba(0,0,0,0.6);
color: #fff;
}
.pagination{
text-align: center;
}
.pagination span,.pagination a{
text-decoration: none;
font-size: 16px;
color: #666;
border: 1px solid rgba(0,0,0,0.3);
padding: 5px 12px;
}
.pagination span:hover,.pagination a:hover{
border: 1px solid rgba(0,0,0,0.8);
color: #333;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>班级信息列表 <a class="btn add-btn" href="/students/class/add">新建班级</a></h1>
<!-- 表头 -->
<div class="header-item">
<div class="pk">ID编号</div>
<div class="fields">班级</div>
<div class="fields">操作</div>
</div>
{% for class in class_list %}
<div class="list-item">
<div class="keys">{{ class.id }}</div>
<div class="keys">{{ class.name }}</div>
<div class="keys">
<a href="/students/class/edit/{{ class.id }}"><span class="btn">编辑</span></a>
<a href="/students/class/del/{{ class.id }}"><span class="btn">删除</span></a>
</div>
</div>
{% endfor %}
<div class="pagination">
<a href="?pn=1">首页</a>
{% if pn > 1 %}
<a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
{% endif %}
<span href="?pn={{ pn }}">{{ pn }}</span>
{% if pn < max_pn %}
<a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
{% endif %}
<a href="?pn={{ max_pn }}">尾页</a>
</div>
</div>
</div>
</body>
</html>
编辑班级数据的表单页面,school/apps/students/templates/edit_class.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>编辑班级信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 800px;
height: 440px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
form{
width: 700px;
margin: 20px auto 20px;
}
form .item{
height: 50px;
line-height: 50px;
border-bottom: 1px solid rgba(0,0,0,.1);
margin-bottom: 10px;
}
form .item input{
outline: none;
height: 24px;
line-height: 24px;
font-size: 16px;
text-indent: 4px;
background: transparent;
border: 1px solid rgba(0,0,0,.3);
}
.submit{
position: absolute;
bottom: 20px;
left: 0;
right: 0;
margin: auto;
width: 120px;
height: 46px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,.3);
cursor: pointer;
outline: none;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>编辑班级信息 <a class="btn add-btn" href="/students/class">班级列表</a></h1>
<form action="/students/class/edit/{{ class_.id }}" method="post">
<div class="item">
<label>班级名称:</label>
<input type="text" name="name" value="{{ class_.name }}">
</div>
<button class="submit">提交</button>
</form>
</div>
</div>
</body>
</html>
在表单提交数据的时候,服务端提供一个post的接口接受本次编辑的数据,school/apps/students/views.py
,代码:
@router.post("/class/edit/{id}")
async def edit_class(request: Request, id: int, name: str=Form()):
"""编辑班级数据的表单处理程序"""
try:
# # 更新数据,方式1:
# class_ = await models.Class.filter(id=id).first()
# if not class_:
# errors = "当前班级不存在,请重新确认!"
# url = "/students/class" # 班级列表的url地址
# return templates.TemplateResponse("errors.html", locals())
#
# # 把查询出来的模型对象的属性进行修改
# class_.name = name
# await class_.save()
# 更新数据,方式2:
await models.Class.filter(id=id).update(**{"name": name})
except IntegrityError as e:
errors = "已存在同名班级,无法修改!"
url = "/students/class" # 班级列表的url地址
return templates.TemplateResponse("errors.html", locals())
except Exception as e:
errors = "未知的错误导致修改失败!"
url = "/students/class" # 班级列表的url地址
return templates.TemplateResponse("errors.html", locals())
errors = "编辑成功!"
url = "/students/class" # 班级列表的url地址
return templates.TemplateResponse("success.html", locals())
学生管理-添加学生功能
school/apps/students/templates/add_student.html
,填写学生数据的表单模板,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加学生信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 800px;
height: 440px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
form{
width: 700px;
margin: 20px auto 20px;
}
form .item{
height: 50px;
line-height: 50px;
border-bottom: 1px solid rgba(0,0,0,.1);
margin-bottom: 10px;
}
form .item input, select{
outline: none;
height: 24px;
line-height: 24px;
font-size: 16px;
text-indent: 4px;
background: transparent;
border: 1px solid rgba(0,0,0,.3);
}
.submit{
position: absolute;
bottom: 20px;
left: 0;
right: 0;
margin: auto;
width: 120px;
height: 46px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,.3);
cursor: pointer;
outline: none;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>添加学生信息 <a class="btn add-btn" href="/students/stu">学生列表</a></h1>
<form action="/students/stu/add" method="post">
<div class="item">
<label>学号:</label><input type="text" name="sno">
</div>
<div class="item">
<label>班级:</label>
<select name="class_id">
<option value="52">中文11班</option>
<option value="51">商务英语10班</option>
</select>
</div>
<div class="item">
<label>账号:</label><input type="text" name="name">
</div>
<div class="item">
<label>密码:</label><input type="password" name="password">
</div>
<div class="item">
<label>性别:</label>
<label><input type="radio" name="sex" checked value="1">男</label>
<label><input type="radio" name="sex" value="0">女</label>
</div>
<button class="submit">提交</button>
</form>
</div>
</div>
</body>
</html>
效果:
在视图中加载当前表单,显示到浏览器,school/apps/students/views.py
,视图,代码:
"""学生管理"""
@router.get("/stu/add")
async def add_student(request: Request):
"""添加学生[表单显示]"""
class_list = await models.Class.all()
return templates.TemplateResponse("add_student.html", locals())
模板中显示班级列表,让用户在浏览器可以选择对应的班级,school/apps/students/templates/add_student.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加学生信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 800px;
height: 440px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
form{
width: 700px;
margin: 20px auto 20px;
}
form .item{
height: 50px;
line-height: 50px;
border-bottom: 1px solid rgba(0,0,0,.1);
margin-bottom: 10px;
}
form .item input, select{
outline: none;
height: 24px;
line-height: 24px;
font-size: 16px;
text-indent: 4px;
background: transparent;
border: 1px solid rgba(0,0,0,.3);
}
.submit{
position: absolute;
bottom: 20px;
left: 0;
right: 0;
margin: auto;
width: 120px;
height: 46px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,.3);
cursor: pointer;
outline: none;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>添加学生信息 <a class="btn add-btn" href="/students/stu">学生列表</a></h1>
<form action="/students/stu/add" method="post">
<div class="item">
<label>学号:</label><input type="text" name="sno">
</div>
<div class="item">
<label>班级:</label>
<select name="class_id">
{% for class_ in class_list %}
<option value="{{ class_.id }}">{{ class_.name }}</option>
{% endfor %}
</select>
</div>
<div class="item">
<label>账号:</label><input type="text" name="name">
</div>
<div class="item">
<label>密码:</label><input type="password" name="password">
</div>
<div class="item">
<label>性别:</label>
<label><input type="radio" name="sex" checked value="1">男</label>
<label><input type="radio" name="sex" value="0">女</label>
</div>
<button class="submit">提交</button>
</form>
</div>
</div>
</body>
</html>
效果:
在视图中,接受并验证客户端提交过来的表单数据,并保存数据库中,school/apps/students/views.py
,视图,代码:
@router.post("/stu/add")
async def add_student(request: Request, sno: int = Form(), name: str=Form(), password: str = Form(), class_id: int=Form(), sex: bool=Form()):
"""添加学生[表单处理]"""
"""接收数据,验证数据"""
# 学号不能重复
student = await models.Student.filter(sno=sno).first()
if student:
errors = "当前学号已经重复,请重新填写!"
url = "/students/stu/add" # 添加学生[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
# 班级必须存在
class_ = await models.Class.filter(id=class_id).first()
if not class_:
errors = "当前班级不存在,请重新选择!"
url = "/students/stu/add" # 添加学生[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
# 用户名不能重复,必须唯一
student = await models.Student.filter(name=name).first()
if student:
errors = "当前账号已经重复,请重新填写!"
url = "/students/stu/add" # 添加学生[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
# todo 密码需要加密,后面加上去
password =
"""添加数据"""
try:
student = await models.Student.create(sno=sno, name=name, password=password, sex=sex, class__id=class_id)
errors = "添加成功!"
url = "/students/stu" # 学生列表的url地址
return templates.TemplateResponse("success.html", locals())
except Exception as e:
errors = "未知的错误导致添加失败!"
url = "/students/stu/add" # 添加学生[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
上面的代码中,虽然已经实现了学生的数据添加功能,但是密码直接明文保存到数据库中,并不安全。所以我们需要对密码进行加盐加密,school/apps/students/views.py
,视图,代码:
import hmac, secrets, string
import settings
... 中间代码省略...
... 中间代码省略...
... 中间代码省略...
@router.post("/stu/add")
async def add_student(request: Request, sno: int = Form(), name: str=Form(), password: str = Form(), class_id: int=Form(), sex: bool=Form()):
"""添加学生[表单处理]"""
"""接收数据,验证数据"""
# 学号不能重复
student = await models.Student.filter(sno=sno).first()
if student:
errors = "当前学号已经重复,请重新填写!"
url = "/students/stu/add" # 添加学生[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
# 班级必须存在
class_ = await models.Class.filter(id=class_id).first()
if not class_:
errors = "当前班级不存在,请重新选择!"
url = "/students/stu/add" # 添加学生[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
# 用户名不能重复,必须唯一
student = await models.Student.filter(name=name).first()
if student:
errors = "当前账号已经重复,请重新填写!"
url = "/students/stu/add" # 添加学生[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
# 密码加盐加密
salt = "".join(secrets.choice(string.ascii_letters + string.digits) for i in range(6))
password = hmac.new(settings.SECRET_KEY.encode(encoding="utf-8"), (password+salt).encode(encoding="utf-8"), digestmod='SHA256').hexdigest()
password = f"{password}|{salt}"
"""添加数据"""
try:
student = await models.Student.create(sno=sno, name=name, password=password, sex=sex, class__id=class_id)
errors = "添加成功!"
url = "/students/stu" # 学生列表的url地址
return templates.TemplateResponse("success.html", locals())
except Exception as e:
errors = "未知的错误导致添加失败!"
url = "/students/stu/add" # 添加学生[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
school/settings.py
,代码:
# 用于加密的秘钥[整个项目的统一盐值,随机生成或者自己乱写就可以了],数据库中有数据以后,就不能随便更改了!
SECRET_KEY = "a@#@)%sdkgml12,^(@M,.d,acxm"
学生管理-学生列表功能
school/apps/students/views.py
,视图,代码:
@router.get('/stu')
async def list_student(request: Request, pn: int=1):
"""学生列表"""
import math
# 每一页展示的数据量
limit = 6
# 获取总数据量
student_list = await models.Student.all()
total = len(student_list)
# 向上取整获取总页数
max_pn = math.ceil(total/limit)
# 判断当前页码不能超出总页码的范围
if pn > max_pn:
pn = max_pn
if pn < 1:
pn = 1
start = (pn-1) * limit # 每一页数据的查询开始位置
student_list = await models.Student.all().limit(limit).offset(start)
for student in student_list:
student.my_class = await student.class_
return templates.TemplateResponse('list_student.html', locals())
school/apps/students/templates/add_student.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>学生信息列表</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 1200px;
height: 640px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
.header-item, .list-item{
height: 48px;
line-height: 48px;
cursor: pointer;
display: flex;
margin: 15px;
}
.list-item .keys, .pk, .fields{
flex: 2;
border-bottom: 1px solid rgba(0,0,0,.3);
text-align: center;
}
.pk, .list-item .keys:first-child{
flex: 1;
}
.btn{
border: 1px solid rgba(0,0,0,.3);
padding: 8px 16px;
}
.btn:hover{
border-radius: 6px;
background: rgba(0,0,0,0.6);
color: #fff;
}
.pagination{
text-align: center;
}
.pagination span,.pagination a{
text-decoration: none;
font-size: 16px;
color: #666;
border: 1px solid rgba(0,0,0,0.3);
padding: 5px 12px;
}
.pagination span:hover,.pagination a:hover{
border: 1px solid rgba(0,0,0,0.8);
color: #333;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>学生信息列表 <a class="btn add-btn" href="/students/stu/add">新建学生</a></h1>
<!-- 表头 -->
<div class="header-item">
<div class="pk">ID编号</div>
<div class="fields">账号</div>
<div class="fields">性别</div>
<div class="fields">班级</div>
<div class="fields">操作</div>
</div>
{% for student in student_list %}
<div class="list-item">
<div class="keys">{{ student.id }}</div>
<div class="keys">{{ student.name }}</div>
<div class="keys">{% if student.sex %}男{% else %}女{% endif %}</div>
<div class="keys">{{ student.my_class.name }}</div>
<div class="keys">
<a href="/students/stu/edit/{{ student.id }}"><span class="btn">编辑</span></a>
<a href="/students/stu/del/{{ student.id }}"><span class="btn">删除</span></a>
</div>
</div>
{% endfor %}
<div class="pagination">
<a href="?pn=1">首页</a>
{% if pn > 1 %}
<a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
{% endif %}
<span href="?pn={{ pn }}">{{ pn }}</span>
{% if pn < max_pn %}
<a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
{% endif %}
<a href="?pn={{ max_pn }}">尾页</a>
</div>
</div>
</div>
</body>
</html>
访问学生列表http://127.0.0.1:8000/students/stu,浏览器显示效果,如下:
学生管理-删除学生功能
school/apps/students/views.py
,代码:
@router.get("/stu/del/{id}")
async def del_student(request: Request, id: int):
"""删除学生数据"""
await models.Student.filter(id=id).delete()
errors = "删除成功!"
url = "/students/stu" # 学生列表的url地址
return templates.TemplateResponse("success.html", locals())
学生管理-更新学生数据
显示更新学生数据的表单页面,schooh/apps/students/views.py
,代码:
@router.get("/stu/edit/{id}")
async def edit_student(request: Request, id: int):
"""更新学生数据[显示表单]"""
student = await models.Student.filter(id=id).first()
if not student:
errors = "当前学生不存在!请重新确认!"
url = "/students/stu" # 学生列表的url地址
return templates.TemplateResponse("errors.html", locals())
# 和添加一样,需要查询班级列表,让学生可以修改班级ID
class_list = await models.Class.all()
return templates.TemplateResponse("edit_student.html", locals())
表单页面模板是直接复制add_student.html
的模板,因为这两个页面的表单数据一样,school/apps/students/templates/edit_student.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>更新学生信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 800px;
height: 440px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
form{
width: 700px;
margin: 20px auto 20px;
}
form .item{
height: 50px;
line-height: 50px;
border-bottom: 1px solid rgba(0,0,0,.1);
margin-bottom: 10px;
}
form .item input, select{
outline: none;
height: 24px;
line-height: 24px;
font-size: 16px;
text-indent: 4px;
background: transparent;
border: 1px solid rgba(0,0,0,.3);
}
.submit{
position: absolute;
bottom: 20px;
left: 0;
right: 0;
margin: auto;
width: 120px;
height: 46px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,.3);
cursor: pointer;
outline: none;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>更新学生信息 <a class="btn add-btn" href="/students/stu">学生列表</a></h1>
<form action="/students/stu/edit/{{ student.id }}" method="post">
<div class="item">
<label>学号:</label><input type="text" name="sno" value="{{ student.sno }}">
</div>
<div class="item">
<label>班级:</label>
<select name="class_id">
{% for class_ in class_list %}
<option value="{{ class_.id }}" {% if student.class__id == class_.id %}selected{% endif %}>{{ class_.name }}</option>
{% endfor %}
</select>
</div>
<div class="item">
<label>账号:</label><input type="text" name="name" value="{{ student.name }}">
</div>
<div class="item">
<label>密码:</label><input type="password" name="password" placeholder="如需重设密码,请填写新密码。" title="如果需要重新设置密码,请填写新密码。">
</div>
<div class="item">
<label>性别:</label>
<label><input type="radio" name="sex" {% if student.sex %}checked{% endif %} value="1">男</label>
<label><input type="radio" name="sex" {% if not student.sex %}checked{% endif %} value="0">女</label>
</div>
<button class="submit">提交</button>
</form>
</div>
</div>
</body>
</html>
在服务端提供表单处理的接口,schooh/apps/students/views.py
,代码:
@router.post("/stu/edit/{id}")
async def edit_student(request: Request, id: int, sno: int = Form(), name: str=Form(), password: str = Form(""), class_id: int=Form(), sex: bool=Form()):
"""更新学生数据[表单处理]"""
student = await models.Student.filter(id=id).first()
if not student:
errors = "当前学生不存在!请重新确认!"
url = "/students/stu" # 学生列表的url地址
return templates.TemplateResponse("errors.html", locals())
try:
student.sno = sno
student.name = name
student.class__id = class_id
student.sex = sex
if len(password) > 1:
# 密码加盐加密
salt = "".join(secrets.choice(string.ascii_letters + string.digits) for i in range(6)) # 随机盐值
password = hmac.new(settings.SECRET_KEY.encode(encoding="utf-8"), (password + salt).encode(encoding="utf-8"),
digestmod='SHA256').hexdigest()
password = f"{password}|{salt}"
student.password = password
await student.save()
errors = "更新成功!"
url = "/students/stu" # 学生列表的url地址
return templates.TemplateResponse("success.html", locals())
except Exception as e:
errors = f"更新出错!错误提示:{e}"
url = f"/students/stu/edit/{id}" # 对应ID的学生的编辑表单的url地址
return templates.TemplateResponse("errors.html", locals())
班级管理-查询当前班级学生
主要是在原有所有学生列表的基础上新增一个Query查询参数,表示班级ID。
在班级列表页模板中,school/apps/students/templates/list_class.html
,代码:
{% for class in class_list %}
<div class="list-item">
<div class="keys">{{ class.id }}</div>
<div class="keys">{{ class.name }}</div>
<div class="keys">
<a href="/students/stu?class_id={{ class.id }}"><span class="btn">学生</span></a>
<a href="/students/class/edit/{{ class.id }}"><span class="btn">编辑</span></a>
<a href="/students/class/del/{{ class.id }}"><span class="btn">删除</span></a>
</div>
</div>
{% endfor %}
在服务端接口代码中原来的学生列表方法中新增接收查询参数class_id,并在代码中判断是否有该参数传递过来,如果有则按班级查询学生信息,school/apps/students/views.py
,代码:
@router.get('/stu')
async def list_student(request: Request, pn: int=1, class_id: int = None):
"""学生列表"""
import math
# 每一页展示的数据量
limit = 6
query = models.Student.all()
if class_id:
query = models.Student.filter(class__id=class_id).all()
# 获取总数据量
total = await query.count()
# 向上取整获取总页数
max_pn = math.ceil(total/limit)
# 判断当前页码不能超出总页码的范围
if pn > max_pn:
pn = max_pn
if pn < 1:
pn = 1
start = (pn-1) * limit # 每一页数据的查询开始位置
student_list = await query.limit(limit).offset(start)
for student in student_list:
student.my_class = await student.class_
return templates.TemplateResponse('list_student.html', locals())
老师管理-老师列表
school/apps/students/views.py
,视图代码:
@router.get('/teacher')
async def list_teacher(request: Request, pn: int=1):
"""老师列表"""
import math
# 每一页展示的数据量
limit = 6
query = models.Teacher.all()
# 获取总数据量
total = await query.count()
# 向上取整获取总页数
max_pn = math.ceil(total/limit)
# 判断当前页码不能超出总页码的范围
if pn > max_pn:
pn = max_pn
if pn < 1:
pn = 1
start = (pn-1) * limit # 每一页数据的查询开始位置
teacher_list = await query.limit(limit).offset(start)
return templates.TemplateResponse('list_teacher.html', locals())
复制原来学生的列表页模板,改名为:list_teacher.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>老师信息列表</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 1200px;
height: 640px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
.header-item, .list-item{
height: 48px;
line-height: 48px;
cursor: pointer;
display: flex;
margin: 15px;
}
.list-item .keys, .pk, .fields{
flex: 2;
border-bottom: 1px solid rgba(0,0,0,.3);
text-align: center;
}
.pk, .list-item .keys:first-child{
flex: 1;
}
.btn{
border: 1px solid rgba(0,0,0,.3);
padding: 8px 16px;
}
.btn:hover{
border-radius: 6px;
background: rgba(0,0,0,0.6);
color: #fff;
}
.pagination{
text-align: center;
}
.pagination span,.pagination a{
text-decoration: none;
font-size: 16px;
color: #666;
border: 1px solid rgba(0,0,0,0.3);
padding: 5px 12px;
}
.pagination span:hover,.pagination a:hover{
border: 1px solid rgba(0,0,0,0.8);
color: #333;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>老师信息列表
<a class="btn add-btn" href="/students/teacher/add">新建老师</a>
</h1>
<!-- 表头 -->
<div class="header-item">
<div class="pk">ID编号</div>
<div class="fields">工号</div>
<div class="fields">账号</div>
<div class="fields">操作</div>
</div>
{% for teacher in teacher_list %}
<div class="list-item">
<div class="keys">{{ teacher.id }}</div>
<div class="keys">{{ teacher.tno }}</div>
<div class="keys">{{ teacher.name }}</div>
<div class="keys">
<a href="/students/teacher/edit/{{ teacher.id }}"><span class="btn">编辑</span></a>
<a href="/students/teacher/del/{{ teacher.id }}"><span class="btn">删除</span></a>
</div>
</div>
{% endfor %}
<div class="pagination">
<a href="?pn=1">首页</a>
{% if pn > 1 %}
<a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
{% endif %}
<span href="?pn={{ pn }}">{{ pn }}</span>
{% if pn < max_pn %}
<a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
{% endif %}
<a href="?pn={{ max_pn }}">尾页</a>
</div>
</div>
</div>
</body>
</html>
老师管理-添加老师功能
视图代码:
@router.get("/teacher/add")
async def add_teacher(request: Request):
"""添加老师[表单显示]"""
return templates.TemplateResponse("add_teacher.html", locals())
@router.post("/teacher/add")
async def add_teacher(request: Request, tno: int = Form(), name: str=Form(), password: str = Form()):
"""添加老师[表单处理]"""
"""接收数据,验证数据"""
# 学号不能重复
teacher = await models.Teacher.filter(tno=tno).first()
if teacher:
errors = "当前工号已经重复,请重新填写!"
url = "/students/teacher/add" # 添加老师[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
# 用户名不能重复,必须唯一
teacher = await models.Teacher.filter(name=name).first()
if teacher:
errors = "当前账号已经重复,请重新填写!"
url = "/students/teacher/add" # 添加老师[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
# 密码加盐加密
salt = "".join(secrets.choice(string.ascii_letters + string.digits) for i in range(6)) # 随机盐值
password = hmac.new(settings.SECRET_KEY.encode(encoding="utf-8"), (password+salt).encode(encoding="utf-8"), digestmod='SHA256').hexdigest()
password = f"{password}|{salt}"
"""添加数据"""
try:
student = await models.Teacher.create(tno=tno, name=name, password=password)
errors = "添加成功!"
url = "/students/teacher" # 老师列表的url地址
return templates.TemplateResponse("success.html", locals())
except Exception as e:
errors = "未知的错误导致添加失败!"
url = "/students/teacher/add" # 添加老师[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
模板从add_student.html
中复制改名为add_teacher.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加老师信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 800px;
height: 440px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
form{
width: 700px;
margin: 20px auto 20px;
}
form .item{
height: 50px;
line-height: 50px;
border-bottom: 1px solid rgba(0,0,0,.1);
margin-bottom: 10px;
}
form .item input, select{
outline: none;
height: 24px;
line-height: 24px;
font-size: 16px;
text-indent: 4px;
background: transparent;
border: 1px solid rgba(0,0,0,.3);
}
.submit{
position: absolute;
bottom: 20px;
left: 0;
right: 0;
margin: auto;
width: 120px;
height: 46px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,.3);
cursor: pointer;
outline: none;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>添加老师信息 <a class="btn add-btn" href="/students/teacher">老师列表</a></h1>
<form action="/students/teacher/add" method="post">
<div class="item">
<label>工号:</label><input type="text" name="tno">
</div>
<div class="item">
<label>账号:</label><input type="text" name="name">
</div>
<div class="item">
<label>密码:</label><input type="password" name="password">
</div>
<button class="submit">提交</button>
</form>
</div>
</div>
</body>
</html>
老师管理-删除老师功能
视图代码:
@router.get("/teacher/del/{id}")
async def del_teacher(request: Request, id: int):
"""删除老师数据"""
await models.Teacher.filter(id=id).delete()
errors = "删除成功!"
url = "/students/teacher" # 老师列表的url地址
return templates.TemplateResponse("success.html", locals())
老师管理-编辑老师功能
视图代码:
@router.get("/teacher/edit/{id}")
async def edit_teacher(request: Request, id: int):
"""更新老师数据[显示表单]"""
teacher = await models.Teacher.filter(id=id).first()
if not teacher:
errors = "当前老师不存在!请重新确认!"
url = "/students/teacher" # 老师列表的url地址
return templates.TemplateResponse("errors.html", locals())
return templates.TemplateResponse("edit_teacher.html", locals())
@router.post("/teacher/edit/{id}")
async def edit_teacher(request: Request, id: int, tno: int = Form(), name: str=Form(), password: str = Form("")):
"""更新老师数据[表单处理]"""
teacher = await models.Teacher.filter(id=id).first()
if not teacher:
errors = "当前老师不存在!请重新确认!"
url = "/students/teacher" # 老师列表的url地址
return templates.TemplateResponse("errors.html", locals())
try:
teacher.tno = tno
teacher.name = name
if len(password) > 1:
# 密码加盐加密
salt = "".join(secrets.choice(string.ascii_letters + string.digits) for i in range(6)) # 随机盐值
password = hmac.new(settings.SECRET_KEY.encode(encoding="utf-8"), (password + salt).encode(encoding="utf-8"),
digestmod='SHA256').hexdigest()
password = f"{password}|{salt}"
teacher.password = password
await teacher.save()
errors = "更新成功!"
url = "/students/teacher" # 老师列表的url地址
return templates.TemplateResponse("success.html", locals())
except Exception as e:
errors = f"更新出错!错误提示:{e}"
url = f"/students/teacher/edit/{id}" # 对应ID的老师的编辑表单的url地址
return templates.TemplateResponse("errors.html", locals())
模板复制edit_student.html
,改名为edit_teacher.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>更新老师信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 800px;
height: 440px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
form{
width: 700px;
margin: 20px auto 20px;
}
form .item{
height: 50px;
line-height: 50px;
border-bottom: 1px solid rgba(0,0,0,.1);
margin-bottom: 10px;
}
form .item input, select{
outline: none;
height: 24px;
line-height: 24px;
font-size: 16px;
text-indent: 4px;
background: transparent;
border: 1px solid rgba(0,0,0,.3);
}
.submit{
position: absolute;
bottom: 20px;
left: 0;
right: 0;
margin: auto;
width: 120px;
height: 46px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,.3);
cursor: pointer;
outline: none;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>更新老师信息 <a class="btn add-btn" href="/students/teacher">老师列表</a></h1>
<form action="/students/teacher/edit/{{ teacher.id }}" method="post">
<div class="item">
<label>工号:</label><input type="text" name="tno" value="{{ teacher.tno }}">
</div>
<div class="item">
<label>账号:</label><input type="text" name="name" value="{{ teacher.name }}">
</div>
<div class="item">
<label>密码:</label><input type="password" name="password" placeholder="如需重设密码,请填写新密码。" title="如果需要重新设置密码,请填写新密码。">
</div>
<button class="submit">提交</button>
</form>
</div>
</div>
</body>
</html>
课程管理-添加课程功能
视图代码:
"""课程管理"""
@router.get("/course/add")
async def add_course(request: Request):
"""添加课程[表单显示]"""
teacher_list = await models.Teacher.all()
return templates.TemplateResponse("add_course.html", locals())
@router.post("/course/add")
async def add_course(request: Request, name: str=Form(), teacher_id: int=Form()):
"""添加课程[表单处理]"""
"""接收数据,验证数据"""
# 课程名称不能重复
course = await models.Course.filter(name=name).first()
if course:
errors = "当前课程已经重复,请重新填写!"
url = "/students/course/add" # 添加课程[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
try:
course = await models.Course.create(name=name, teacher_id=teacher_id)
errors = "添加成功!"
url = "/students/course" # 课程列表的url地址
return templates.TemplateResponse("success.html", locals())
except Exception as e:
errors = "未知的错误导致添加失败!"
url = "/students/course/add" # 添加课程[表单显示]的url地址
return templates.TemplateResponse("errors.html", locals())
因为添加学生需要查询班级,同理添加课程需要查询老师,所以从模板add_student.html
复制并改名为add_course.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加课程信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 800px;
height: 440px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
form{
width: 700px;
margin: 20px auto 20px;
}
form .item{
height: 50px;
line-height: 50px;
border-bottom: 1px solid rgba(0,0,0,.1);
margin-bottom: 10px;
}
form .item input, select{
outline: none;
height: 24px;
line-height: 24px;
font-size: 16px;
text-indent: 4px;
background: transparent;
border: 1px solid rgba(0,0,0,.3);
}
.submit{
position: absolute;
bottom: 20px;
left: 0;
right: 0;
margin: auto;
width: 120px;
height: 46px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,.3);
cursor: pointer;
outline: none;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>添加课程信息 <a class="btn add-btn" href="/students/course">课程列表</a></h1>
<form action="/students/course/add" method="post">
<div class="item">
<label>课程名称:</label><input type="text" name="name">
</div>
<div class="item">
<label>授课老师:</label>
<select name="teacher_id">
{% for teacher in teacher_list %}
<option value="{{ teacher.id }}">{{ teacher.name }}</option>
{% endfor %}
</select>
</div>
<button class="submit">提交</button>
</form>
</div>
</div>
</body>
</html>
课程管理-课程列表功能
视图代码:
@router.get('/course')
async def list_course(request: Request, pn: int=1, teacher_id: int = None):
"""课程列表"""
import math
# 每一页展示的数据量
limit = 6
query = models.Course.all()
if teacher_id:
query = models.Course.filter(teacher_id=teacher_id).all()
# 获取总数据量
total = await query.count()
# 向上取整获取总页数
max_pn = math.ceil(total/limit)
# 判断当前页码不能超出总页码的范围
if pn > max_pn:
pn = max_pn
if pn < 1:
pn = 1
start = (pn-1) * limit # 每一页数据的查询开始位置
course_list = await query.limit(limit).offset(start)
for course in course_list:
course.teacher = await course.teacher
return templates.TemplateResponse('list_course.html', locals())
从学生列表页模板list_student.html
复制改名为list_course.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>课程信息列表</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 1200px;
height: 640px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
.header-item, .list-item{
height: 48px;
line-height: 48px;
cursor: pointer;
display: flex;
margin: 15px;
}
.list-item .keys, .pk, .fields{
flex: 2;
border-bottom: 1px solid rgba(0,0,0,.3);
text-align: center;
}
.pk, .list-item .keys:first-child{
flex: 1;
}
.btn{
border: 1px solid rgba(0,0,0,.3);
padding: 8px 16px;
}
.btn:hover{
border-radius: 6px;
background: rgba(0,0,0,0.6);
color: #fff;
}
.pagination{
text-align: center;
}
.pagination span,.pagination a{
text-decoration: none;
font-size: 16px;
color: #666;
border: 1px solid rgba(0,0,0,0.3);
padding: 5px 12px;
}
.pagination span:hover,.pagination a:hover{
border: 1px solid rgba(0,0,0,0.8);
color: #333;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>课程信息列表
<a class="btn add-btn" href="/students/course/add">新建课程</a>
</h1>
<!-- 表头 -->
<div class="header-item">
<div class="pk">ID编号</div>
<div class="fields">课程名称</div>
<div class="fields">授课老师</div>
<div class="fields">操作</div>
</div>
{% for course in course_list %}
<div class="list-item">
<div class="keys">{{ course.id }}</div>
<div class="keys">{{ course.name }}</div>
<div class="keys">{{ course.teacher.name }}</div>
<div class="keys">
<a href="/students/stu/edit/{{ course.id }}"><span class="btn">编辑</span></a>
<a href="/students/stu/del/{{ course.id }}"><span class="btn">删除</span></a>
</div>
</div>
{% endfor %}
<div class="pagination">
<a href="?pn=1">首页</a>
{% if pn > 1 %}
<a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
{% endif %}
<span href="?pn={{ pn }}">{{ pn }}</span>
{% if pn < max_pn %}
<a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
{% endif %}
<a href="?pn={{ max_pn }}">尾页</a>
</div>
</div>
</div>
</body>
</html>
学生管理-我的课程功能
1、在当前学生列表中新增一个选课
按钮,list_student.html
,代码:
<div class="keys">
<a href="/students/stu/course/{{ student.id }}"><span class="btn">选课</span></a>
<a href="/students/stu/edit/{{ student.id }}"><span class="btn">编辑</span></a>
<a href="/students/stu/del/{{ student.id }}"><span class="btn">删除</span></a>
</div>
新建模板文件,school/apps/students/templates/my_course.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>xiaobai的课程信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 800px;
height: 440px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
form{
width: 700px;
margin: 20px auto 20px;
}
form .item{
height: 50px;
line-height: 50px;
border-bottom: 1px solid rgba(0,0,0,.1);
margin-bottom: 10px;
}
form .item input{
outline: none;
height: 24px;
line-height: 24px;
font-size: 16px;
text-indent: 4px;
background: transparent;
border: 1px solid rgba(0,0,0,.3);
}
.submit{
position: absolute;
bottom: 20px;
left: 0;
right: 0;
margin: auto;
width: 120px;
height: 46px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,.3);
cursor: pointer;
outline: none;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
.transfer{
width: 100%;
border: 1px solid rgba(0,0,0,.3);
min-height: 240px;
text-align: center;
position: relative;
}
.transfer .all-box{
float: left;
width: 45%;
min-height: 240px;
border: 1px solid rgba(0,0,0,.3);
}
.transfer .selected-box{
float: right;
width: 45%;
min-height: 240px;
border: 1px solid rgba(0,0,0,.3);
}
.transfer .all-box ul,
.transfer .selected-box ul{
overflow: scroll;
height: 180px;
}
.transfer ul{
list-style: none;
padding: 0;
margin: 0;
font-size: 14px;
color: #555;
}
.transfer ul li{
height: 24px;
line-height: 28px;
text-align: left;
text-indent: 100px;
}
.transfer .btn-list{
position: absolute;
margin: auto;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: 84px;
width: 60px;
}
.transfer .btn-list div{
background: cornflowerblue;
height: 40px;
color: #ffffff;
line-height: 40px;
font-size: 24px;
cursor: pointer;
width: 60px;
border-radius: 8px;
border-bottom: 4px solid #ffffff;
border-top: 4px solid #ffffff;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>xiaobai的课程信息 <a class="btn add-btn" href="/students/stu">学生列表</a></h1>
<form action="/students/stu/course" method="post">
<div class="transfer">
<div class="all-box">
<p>未选课程</p>
<ul>
<li><label><input type="checkbox" name="" id=""> Python基础</label></li>
<li><label><input type="checkbox" name="" id=""> Python入门</label></li>
<li><label><input type="checkbox" name="" id=""> Python进阶</label></li>
<li><label><input type="checkbox" name="" id=""> Python深入</label></li>
<li><label><input type="checkbox" name="" id=""> Python实战</label></li>
<li><label><input type="checkbox" name="" id=""> Python实战</label></li>
<li><label><input type="checkbox" name="" id=""> Python实战</label></li>
<li><label><input type="checkbox" name="" id=""> Python实战</label></li>
</ul>
</div>
<div class="btn-list">
<div>></div>
<div><</div>
</div>
<div class="selected-box">
<p>已选课程</p>
<ul>
<li><label><input type="checkbox" name="" id=""> fastAPI基础</label></li>
<li><label><input type="checkbox" name="" id=""> fastAPI入门</label></li>
</ul>
</div>
</div>
<button class="submit">提交</button>
</form>
</div>
</div>
</body>
</html>
FastAPI服务端提供视图接口,加载上面的模板文件,展示我的课程界面,students/views.py
,代码:
@router.get('/stu/course/{id}')
async def my_course(request: Request, id: int):
# 只查询当前模型的数据,并不会帮我们把外键数据查询出来
# student = await models.Student.filter(id=id).first()
# 查询当前模型数据的同时,顺便把外键字段courses的数据也查询出来
student = await models.Student.filter(id=id).first().prefetch_related('class_', 'courses')
if not student:
errors = "当前学生不存在,请重新确认!"
url = "/students/stu" # 课程列表的url地址
return templates.TemplateResponse("errors.html", locals())
# 当前学生已选课程
my_coruses_id = [course.id for course in student.courses]
# 查询所有未选课程
course_list = await models.Course.filter(id__not_in=my_coruses_id).all()
return templates.TemplateResponse('my_course.html', locals())
展示课程数据到模板中,my_course.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ student.name }}的课程信息</title>
<style>
body{
margin: 0;
padding: 0;
}
.bg{
width: 100vw;
height: 100vh;
background: url('/static/images/bg.jpg') no-repeat;
background-size: 100%;
position: relative;
}
.form-box{
background: rgba(255,255,255,0.8);
width: 800px;
height: 440px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
box-shadow: 6px 6px 6px rgba(0,0,0,.1);
}
h1{
font-size: 32px;
font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
font-weight: normal;
text-align: center;
line-height: 32px;
height: 54px;
border-bottom: 1px solid rgba(0,0,0,.7);
margin: 15px 15px 0 ;
color: #666;
}
form{
width: 700px;
margin: 20px auto 20px;
}
form .item{
height: 50px;
line-height: 50px;
border-bottom: 1px solid rgba(0,0,0,.1);
margin-bottom: 10px;
}
form .item input{
outline: none;
height: 24px;
line-height: 24px;
font-size: 16px;
text-indent: 4px;
background: transparent;
border: 1px solid rgba(0,0,0,.3);
}
.submit{
position: absolute;
bottom: 20px;
left: 0;
right: 0;
margin: auto;
width: 120px;
height: 46px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,.3);
cursor: pointer;
outline: none;
}
.add-btn{
position: absolute;
right: 15px;
text-decoration: none;
color: #333333;
font-size: 16px;
}
.transfer{
width: 100%;
border: 1px solid rgba(0,0,0,.3);
min-height: 240px;
text-align: center;
position: relative;
}
.transfer .all-box{
float: left;
width: 45%;
min-height: 240px;
border: 1px solid rgba(0,0,0,.3);
}
.transfer .selected-box{
float: right;
width: 45%;
min-height: 240px;
border: 1px solid rgba(0,0,0,.3);
}
.transfer .all-box ul,
.transfer .selected-box ul{
overflow: scroll;
height: 180px;
}
.transfer ul{
list-style: none;
padding: 0;
margin: 0;
font-size: 14px;
color: #555;
}
.transfer ul li{
height: 24px;
line-height: 28px;
text-align: left;
text-indent: 100px;
}
.transfer .btn-list{
position: absolute;
margin: auto;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: 84px;
width: 60px;
}
.transfer .btn-list div{
background: cornflowerblue;
height: 40px;
color: #ffffff;
line-height: 40px;
font-size: 24px;
cursor: pointer;
width: 60px;
border-radius: 8px;
border-bottom: 4px solid #ffffff;
border-top: 4px solid #ffffff;
}
</style>
</head>
<body>
<div class="bg">
<div class="form-box">
<h1>{{ student.name }}的课程信息 <a class="btn add-btn" href="/students/stu">学生列表</a></h1>
<form action="/students/stu/course/{{ student.id }}" method="post">
{# 设置一个用户看不见的隐藏框,里面记录所有已选课程 #}
<input type="hidden" name="courses" value="{{ my_coruses_id }}">
<div class="transfer">
<div class="all-box">
<p>未选课程</p>
<ul>
{% for course in course_list %}
<li><label><input type="checkbox" name="" value="{{ course.id }}" > {{ course.name }}</label></li>
{% endfor %}
</ul>
</div>
<div class="btn-list">
<div class="right">></div>
<div class="left"><</div>
</div>
<div class="selected-box">
<p>已选课程</p>
<ul>
{% for course in student.courses %}
<li><label><input type="checkbox" name="" value="{{ course.id }}" > {{ course.name }}</label></li>
{% endfor %}
</ul>
</div>
</div>
<button class="submit">提交</button>
</form>
</div>
</div>
<script>
var all_box = document.querySelector('.all-box ul');
var selected_box = document.querySelector('.selected-box ul');
let right = document.querySelector('.btn-list .right');
let left = document.querySelector('.btn-list .left');
let course_input = document.querySelector('input[name=courses]');
let submit = document.querySelector('.submit');
// 选课
right.onclick = ()=>{
let selected_input = all_box.querySelectorAll('li input:checked');
for(let item of selected_input){
// 循环每一个input,获取该input框的父级元素label的父级元素li,并把li追加到已选课程的ul下面作为子元素
selected_box.append(item.parentElement.parentElement);
// 取消勾选
item.checked =false;
}
}
// 消课
left.onclick = ()=>{
let selected_input = selected_box.querySelectorAll('li input:checked');
for(let item of selected_input){
// 循环每一个input,获取该input框的父级元素label的父级元素li,并把li追加到未选课程的ul下面作为子元素
all_box.append(item.parentElement.parentElement);
// 取消勾选
item.checked = false;
}
}
// 点击提交表单
submit.onclick = ()=>{
let all_input = selected_box.querySelectorAll('li input');
let course_id_list = []
for(let item of all_input){
course_id_list.push(parseInt(item.value));
}
console.log(course_id_list);
course_input.value = `[${course_id_list}]`;
// 在提交表单事件中,可以使用return false阻止提交按钮触发表单提交事件,
// return false;
}
</script>
</body>
</html>
FastAPI服务端接受表单隐藏数据,把学生选的课程添加到数据库中。
@router.post("/stu/course/{id}")
async def my_course(request: Request, id: int, courses: str = Form()):
"""我的课程[数据处理]"""
student = await models.Student.filter(id=id).first().prefetch_related('class_', 'courses')
if not student:
errors = "当前学生不存在,请重新确认!"
url = "/students/stu" # 课程列表的url地址
return templates.TemplateResponse("errors.html", locals())
# 分割表单提交上来的已选课程的ID列表
# course_id_list = eval(courses)
course_id_list = []
if courses[1:-1]:
course_id_list = [int(id) for id in courses[1:-1].split(",")]
try:
# 把表单提交的已选课程的ID保存到课程与学生的关系表中
await student.courses.clear() # 清空当前学生的所有选课
# 把本次学生的选课全部查询出来,并与当前学生进行关系绑定
course_list = await models.Course.filter(id__in=course_id_list).all()
for course in course_list:
await student.courses.add(course)
errors = "选课成功!"
url = f"/students/stu/course/{student.id}" # 我的课程的url地址
return templates.TemplateResponse("success.html", locals())
except Exception as e:
errors = f"未知的错误导致选课失败!{e}"
url = f"/students/course/{student.id}" # 我的课程的url地址
return templates.TemplateResponse("errors.html", locals())
6.2.2 SQLAlchemy
async_sqlalchemy.py
,使用ORM连接mysql,并初始化ORM,代码:
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Integer
# 创建异步引擎
database_url = "mysql+aiomysql://root:123@127.0.0.1:3306/school?charset=utf8mb4"
engine = create_async_engine(database_url, future=True, echo=True)
# 创建异步 SessionLocal
async_session = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False, autocommit=False
)
class BaseModel(AsyncAttrs, DeclarativeBase):
"""异步模型基类"""
pass
main.py
,代码:
import uvicorn
from fastapi import FastAPI, Body, status
from async_sqlalchemy import async_engine, BaseModel, mapped_column, Mapped, async_session, String, select, update, delete, insert
from pydantic import BaseModel as PyModel
class Class(BaseModel):
"""班级信息"""
__tablename__ = "my_class"
__table_args__ = {
"extend_existing": True,
}
id: Mapped[int] = mapped_column(primary_key=True, comment='主键')
name: Mapped[str] = mapped_column(String(50), unique=True, comment='分类名称')
def __repr__(self) -> str:
"""打印模型对象时输入文本提示"""
return f"Class(id={self.id}, name={self.name})"
async def init_db():
"""项目启动时提前建表"""
async with async_engine.begin() as conn:
# 删除当前程序中所有的模型对应的数据表
# await conn.run_sync(BaseModel.metadata.drop_all)
# 创建当前程序中所有的模型的数据表,如果数据表不存在的情况下
await conn.run_sync(BaseModel.metadata.create_all)
app = FastAPI(
title="学生选课系统",
summary="学生选课系统API文档",
description="学生选课系统API文档的详细说明内容",
version="v1.0.0",
on_startup=[init_db], # 全局事件,当项目启动时,会自动执行的函数列表
on_shutdown=[], # 全局事件,当项目停止时,会自动执行的函数列表
)
@app.get("/")
async def main():
return {}
class ClassIn(PyModel):
name: str
@app.post("/class")
async def add_class(class_in: ClassIn):
"""添加班级"""
name = class_in.name
async with async_session() as session:
# # 方式1:使用add添加数据
# class_object = Class(name=name)
# session.add(class_object)
# await session.commit()
# return class_object
# 方式2:使用insert添加数据
query = insert(Class).values(name=name)
await session.execute(query)
await session.commit()
return {}
@app.get("/class/{id}")
async def get_class(id: int):
"""获取一条数据"""
async with async_session() as session:
# # 获取数据
# query = select(Class).filter(Class.id == id)
# queryset = await session.scalars(query)
# return queryset.first()
# 获取数据
class_obj = await session.get(Class, id)
return class_obj
@app.get("/class")
async def list_class():
"""获取多条数据"""
async with async_session() as session:
query = select(Class)
queryset = await session.scalars(query)
return queryset.all()
@app.put("/class/{id}")
async def edit_class(id: int, class_in: ClassIn):
"""编辑班级"""
name = class_in.name
async with async_session() as session:
# # 方式1:先查询当前要编辑的模型,修改模型的字段值,提交事务
# query = select(Class).where(Class.id == id)
# queryset = await session.scalars(query)
# class_object = queryset.first()
# # 进行字段值替换
# class_object.name = name
# # 提交事务
# await session.commit()
# # 把修改后的模型返回给客户端
# return class_object
# 方式2:使用update方法来实现
data = {
"name": name
}
query = update(Class).where(Class.id == id).values(**data)
await session.execute(query)
await session.commit()
return {
'id': id,
'name': name,
}
@app.delete("/class/{id}", status_code=status.HTTP_204_NO_CONTENT)
async def del_class(id: int):
"""删除班级"""
async with async_session() as session:
# # 方式1:线查询模型,使用模型来删除
# class_object = await session.get(Class, id)
# if class_object:
# await session.delete(class_object)
# await session.commit()
# 方式2: 直接使用delete对模型按条件删除
query = delete(Class).where(Class.id == id)
await session.execute(query)
await session.commit()
if __name__ == '__main__':
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)