数据库大作业2020013199吴蒙蔚

本文最后更新于:5 months ago

第三题:基于python的flask框架和sqlite数据库的网页设计

项目地址 https://github.com/VernalsWind/watchlist.git (git clone一下就可以啦)

需求分析:

本作业目的是建立一个电影发烧友的园地,凡是注册了该网站的电影发烧友共享一个电影片单,每个人的个人主页有一个私人影单,是共享影视片单的子集。用户可以订阅修改电影的名字和年份,多个用户可以订阅同一个电影,一个用户可以订阅多部电影,所有电影和用户之间是多对多的关系。

ER图

app.py文件

一、安装引入flask-sqlalchemy,flask-admin,flask-login等包

进入虚拟环境,输入pip freeze后可以查看
(venv) PS C:\Users\17503\myproject> pip freeze
cffi==1.15.1
click==8.1.3
colorama==0.4.5
cryptography==37.0.4
Flask==2.1.2
Flask-Admin==1.6.0
Flask-BasicAuth==0.2.0
Flask-Login==0.6.1
Flask-SQLAlchemy==2.5.1
greenlet==1.1.2
importlib-metadata==4.12.0
install==1.3.5
itsdangerous==2.1.2
Jinja2==3.1.0
MarkupSafe==2.1.1
pycparser==2.21
python-dotenv==0.20.0
SQLAlchemy==1.4.39
Werkzeug==2.1.2
WTForms==3.0.1
zipp==3.8.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import imp
from itertools import count
from plistlib import UID
from pydoc import cli
from turtle import title
from unicodedata import name
from flask import Flask, render_template,request
from flask import redirect,flash,url_for
from flask_sqlalchemy import SQLAlchemy
import os
from flask_login import login_required, logout_user,current_user
from flask_login import UserMixin,login_user
from flask_login import LoginManager
from werkzeug.security import generate_password_hash, check_password_hash
import click

二、基本配置flask 应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app=Flask(__name__)

app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev')

login_manager = LoginManager(app) # 实例化扩展类
login_manager.login_view = 'login'

@login_manager.user_loader
def load_user(user_id): # 创建用户加载回调函数,接受用户 ID 作为参数
user = User.query.get(int(user_id)) # 用 ID 作为 User 模型的主键查询对应的用户
return user # 返回用户对象

app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///'+os.path.join(app.root_path,'data.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 关闭对模型修改的监控

db=SQLAlchemy(app)

建立数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#数据模型:用户,电影,多对多,订阅
class User(db.Model,UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20))
password_hash = db.Column(db.String(128)) # 密码散列值

def set_password(self, password): # 用来设置密码的方法,接受密码作为参数
self.password_hash = generate_password_hash(password) # 将生成的密码保持到对应字段

def validate_password(self, password): # 用于验证密码的方法,接受密码作为参数
return check_password_hash(self.password_hash, password) # 返回布尔值

class Movie(db.Model):
id=db.Column(db.Integer,primary_key=True)
title=db.Column(db.String(60))
year = db.Column(db.String(4)) # 电影年份
class Subscribe(db.Model):
id=db.Column(db.Integer,primary_key=True)
uid=db.Column(db.Integer)
mid =db.Column(db.Integer)

#1.命令行交互flash initdb (--drop)初始化数据库
@app.cli.command()
@click.option('--drop',is_flag=True)
def initdb(drop):
if drop:
db.drop_all()
db.create_all()
click.echo('initiate successfully!')
#2.命令行交互flash forge,生成虚假数据
@app.cli.command()
def forge():
"""Generate fake data."""
db.create_all()


users=[{'username':'Grey Li','password_hash':generate_password_hash('dog')},
{'username':'Derly Qi','password_hash':generate_password_hash('cat')}
]#生成密码散列值,更加安全
movies = [
{'title': 'My Neighbor Totoro', 'year': '1988'},
{'title': 'Dead Poets Society', 'year': '1989'},
{'title': 'A Perfect World', 'year': '1993'},
{'title': 'Leon', 'year': '1994'},
{'title': 'Mahjong', 'year': '1996'},
{'title': 'Swallowtail Butterfly', 'year': '1996'},
{'title': 'King of Comedy', 'year': '1999'},
{'title': 'Devils on the Doorstep', 'year': '1999'},
{'title': 'WALL-E', 'year': '2008'},
{'title': 'The Pork of Music', 'year': '2012'},
]
subscribe=[
{'uid':1,'mid':1},
{'uid':1,'mid':2},
{'uid':1,'mid':3},
{'uid':2,'mid':3},
{'uid':2,'mid':4},
{'uid':2,'mid':5},
{'uid':3,'mid':5},
{'uid':3,'mid':6},
{'uid':3,'mid':7},
{'uid':4,'mid':7},
{'uid':4,'mid':8},
{'uid':4,'mid':9},
{'uid':4,'mid':10},
]
for u in users:
user=User(username=u['username'],password_hash=u['password_hash'])
db.session.add(user)
for m in movies:
movie = Movie(title=m['title'], year=m['year'])
db.session.add(movie)
for s in subscribe:
subscribe=Subscribe(uid=s['uid'],mid=s['mid'])
db.session.add(subscribe)
db.session.commit()
click.echo('Done.')

后台管理系统

1
2
3
4
5
6
7
8
9
10
11
12
13
admin=Admin(app, name='后台管理界面', template_mode='bootstrap3')

class myAdminView(ModelView):
def is_accessible(self):
return current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
return url_for('admin')


admin.add_view(myAdminView(User, db.session))
admin.add_view(myAdminView(Movie, db.session))
admin.add_view(myAdminView(Subscribe, db.session))

1.主页,查询全部电影、登录后添加电影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@app.route('/', methods=['GET', 'POST'])#设置路由和方法
def index():
if request.method == 'POST': # 判断是否是 POST 请求
# 获取表单数据
title = request.form.get('title') # 传入表单对应输入字段的 name 值
year = request.form.get('year')
# 验证数据
if not title or not year or len(year) > 4 or len(title) > 60:
flash('Invalid input.') # 显示错误提示
return redirect(url_for('index')) # 重定向回主页
# 保存表单数据到数据库
movie = Movie(title=title, year=year) # 创建记录
db.session.add(movie) # 添加到数据库会话
db.session.commit() # 提交数据库会话
flash('Item created.') # 显示成功创建的提示
return redirect(url_for('index')) # 重定向回主页
movies=Movie.query.all()#查询Movie表的所有的电影
return render_template('index.html',movies=movies)#渲染index.html文件,将获取的movies记录显示在{{}}包围的位置里

templates:

模板仅仅是文本文件。它可以生成任何基于文本的格式(HTML、XML、CSV、LaTex 等等)。 它并没有特定的扩展名,.html 或 .xml 都是可以的。

模板包含 变量表达式 ,这两者在模板求值的时候会被替换为值。模板中还有标签,控制模板的逻辑。模板语法的大量灵感来自于 Django 和 Python 。

Jinja 中最强大的部分就是模板继承。模板继承允许你构建一个包含你站点共同元素的基本模板“骨架”,并定义子模板可以覆盖的

index.html的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<ul class="movie-list">
{% for movie in movies %}<!--jinjia2嵌入python代码用{% %}-->
<li>{{ movie.title }} - {{ movie.year }}<!--嵌入变量用{{ }}-->



{% if current_user.is_authenticated %}<!--如果认证成功-->
<span class="float-right"><!--靠右-->
<a class="btn" href="{{ url_for('edit', movie_id=movie.id) }}">Edit</a><!--对应修改电影信息:update的edit函数:def edit(movie_id)-->
</span>

<span class="float-right">

<form class="inline-form" method="post" action="{{ url_for('delete', movie_id=movie.id) }}">
<!--对应删除电影信息:delete的edit函数:def delete(movie_id)-->
<input class="btn" type="submit" name="delete" value="Delete" onclick="return confirm('Are you sure?')">
</form>

</span>
{% endif %}<!--jinjia2的语法:需要endif结束判断-->

<span class="float-right">
<a class="imdb" href="https://www.imdb.com/find?q={{ movie.title }}" target="_blank"
title="Find this movie on IMDb">IMDb</a>
</span>
</li>
{% endfor %}<!--jinjia2的语法:需要endif结束循环-->
</ul>

2.所有用户查询

1
2
3
4
5
6
7
8
9
@app.route('/allUser')
def allUser():
name=[]
n=User.query.count()
for i in range(1,n+1):
a=User.query.get(i)
name.append(a.username)

return render_template('allUser.html',name=name)

3.修改电影信息(update)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
     
@app.route('/movie/edit/<int:movie_id>', methods=['GET', 'POST'])
@login_required
def edit(movie_id):
movie = Movie.query.get_or_404(movie_id)

if request.method == 'POST': # 处理编辑表单的提交请求
title = request.form['title']
year = request.form['year']

if not title or not year or len(year) > 4 or len(title) > 60:
flash('Invalid input.')
return redirect(url_for('edit', movie_id=movie_id)) # 重定向回对应的编辑页面

movie.title = title # 更新标题
movie.year = year # 更新年份
db.session.commit() # 提交数据库会话
flash('Item updated.')
return redirect(url_for('index')) # 重定向回主页

return render_template('edit.html', movie=movie) # 传入被编辑的电影记录

子模块:edit.html

1
2
3
4
5
6
7
8
9
10
{% extends 'index.html' %}

{% block content %}
<h3>Edit item</h3>
<form method="post">
Name <input type="text" name="title" autocomplete="off" required value="{{ movie.title }}">
Year <input type="text" name="year" autocomplete="off" required value="{{ movie.year }}">
<input class="btn" type="submit" name="submit" value="Update">
</form>
{% endblock %}

删除电影记录delete,同时删除订阅信息

1
2
3
4
5
6
7
8
9
10
11
12
@app.route('/movie/delete/<int:movie_id>', methods=['POST'])  # 限定只接受 POST 请求
@login_required
def delete(movie_id):
movie = Movie.query.get_or_404(movie_id) # 获取电影记录
db.session.delete(movie) # 删除对应的记录
subscribe=Subscribe.query.filter(Subscribe.mid==movie_id)
db.session.delete(subscribe)
db.session.commit() # 提交数据库会话
flash('Item deleted.')
return redirect(url_for('index')) # 重定向回主页


删除用户,同时删除订阅

1
2
3
4
5
6
7
8
9
10
11
12

@app.route('/user/delete/<user_name>',methods=['POST'])
@login_required
def deleteuser(user_name):
user=User.query.filter(User.username==user_name).first()
subscribe=Subscribe.query.filter(Subscribe.uid==user.id)
db.session.delete(user)
db.session.delete(subscribe)
db.session.commit()
flash('Item deleted.')
return redirect(url_for('allUser'))

用户认证

1
2
3
4
5
6
7
8
9
10
11
12
@app.route('/register',methods=['GET','POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user=User(username=username,password_hash=generate_password_hash (password))
db.session.add(user)
db.session.commit()
return redirect(url_for('index'))

return render_template('login.html')

登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']

if not username or not password:
flash('Invalid input.')
return redirect(url_for('login'))

user = User.query.filter(User.username==username).first()
# 验证用户名和密码是否一致
if user.validate_password(password):
login_user(user) # 登入用户
flash('Login success.')
return redirect(url_for('selflist',username=current_user.username)) # 重定向到主页

flash('Invalid username or password.') # 如果验证失败,显示错误消息
return redirect(url_for('login')) # 重定向回登录页面

return render_template('login.html')

Selflist个人主页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#个人主页
@app.route('/login/<username>',methods=['GET', 'POST'])
def selflist(username):
#根据传入的用户名进行搜索用户UID,进而搜索订阅SID,得到某用户的订阅电影列表
#查询功能
user=User.query.filter_by(username=username).first()
subscribe=Subscribe.query.filter(Subscribe.uid==user.id).all()
countsub=Subscribe.query.filter(Subscribe.uid==user.id).count()
movies=[]
for i in range(countsub):
movies.append(Movie.query.filter( Movie.id==subscribe[i].mid).first())

if request.method == 'POST': # 判断是否是 POST 请求
# 获取表单数据
title = request.form.get('title') # 传入表单对应输入字段的 name 值
year = request.form.get('year')
# 验证数据
if not title or not year or len(year) > 4 or len(title) > 60:
flash('Invalid input.') # 显示错误提示
return redirect(url_for('index')) # 重定向回主页
# 保存表单数据到数据库
movie = Movie(title=title, year=year) # 创建记录


db.session.add(movie) # 添加到数据库会话

db.session.commit() # 提交数据库会话
subscribe=Subscribe(uid=user.id,mid=movie.id)
db.session.add(subscribe)#insert 功能,用户订阅电影
db.session.commit()
flash('Item created.') # 显示成功创建的提示
#第二遍写,为了更新页面,是增加之后的再次查询,不是写错了
user=User.query.filter_by(username=username).first()
subscribe=Subscribe.query.filter(Subscribe.uid==user.id).all()
countsub=Subscribe.query.filter(Subscribe.uid==user.id).count()
movies=[]
for i in range(countsub):
movies.append(Movie.query.filter( Movie.id==subscribe[i].mid).first())
return render_template('selflist.html',name=user.username,movies=movies)
return render_template('selflist.html',name=user.username,movies=movies)

logout 登出

1
2
3
4
5
6
7
8
#登出
@app.route('/logout')
@login_required # 用于视图保护
def logout():
logout_user() # 登出用户
flash('Goodbye.')
return redirect(url_for('index')) # 重定向回首页

设置:更改用户名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#设置,改用户名
@app.route('/settings', methods=['GET', 'POST'])
@login_required
def settings():
if request.method == 'POST':
name = request.form['name']

if not name or len(name) > 20:
flash('Invalid input.')
return redirect(url_for('settings'))

current_user.username = name
# current_user 会返回当前登录用户的数据库记录对象
# 等同于下面的用法
# user = User.query.first()
# user.name = name
db.session.commit()
flash('Settings updated.')
return redirect(url_for('index'))

return render_template('settings.html')

flask run

1
2
3
if __name__=='__main__':
app.run()

第一步,cmd,cd进入myproject文件夹以后,激活虚拟环境venv\scripts\activate
第二步,命令行输入flask initdb, 出现initiated sucessfully!说明数据库初始化成功
第三步,生成虚假数据,命令行输入flask forge,出现Done!说明运行成功
第四步,命令行输入flash run
登录后通过http://localhost:5000/admin进入后台管理界面
有写的不清楚的地方欢迎交流