【python】Flask网页开发——论坛项目实战(完整代码)-CSDN博客

261 阅读6分钟

论坛项目实战(完整代码)

项目导航

【python】Flask网页开发——论坛项目实战(完整代码)
【python】Flask网页开发——论坛项目实战(1.导航条实现)
【python】Flask网页开发——论坛项目实战(2.登录与注册)
【python】Flask网页开发——论坛项目实战(3.问答模块)

项目展示

页面展示

首页
在这里插入图片描述
发布问答
在这里插入图片描述
登录
在这里插入图片描述
注册
在这里插入图片描述
问答详情
在这里插入图片描述

具体实现

1.导航条实现

2.登录与注册

3.问答模块

项目结构

1.创建项目

在这里插入图片描述

2.项目需要提前安装的包

  • pymysql:Python操作数据库的驱动程序
  • Flask-SQLAlchemy:在Flask中使用ORM模型操作数据库
  • cryptography:对密码进行加密和解密
  • Flask-Migrate:将ORM模型的变更同步到数据库中
  • WTForms:用于处理表单的Python库
  • Flask-Mail : Flask 框架的一个扩展,用于在 Web 应用程序中发送电子邮件。

3.项目结构

在这里插入图片描述

blueprints

forms.py

import wtforms
from wtforms.validators import length, email, EqualTo
from models.models import EmailCaptchaModel, UserModel


class LoginForm(wtforms.Form):
    email = wtforms.StringField(validators=[email()])
    password = wtforms.StringField(validators=[length(min=6, max=20)])


class RegisterForm(wtforms.Form):
    username = wtforms.StringField(validators=[length(min=3, max=20)])
    email = wtforms.StringField(validators=[email()])
    captcha = wtforms.StringField(validators=[length(min=4, max=4)])
    password = wtforms.StringField(validators=[length(min=6, max=20)])
    confirm_password = wtforms.StringField(validators=[EqualTo("password")])

    def validate_captcha(self, field):
        captcha = field.data
        email = self.email.data
        captcha_model = EmailCaptchaModel.query.filter_by(email=email).first()
        if not captcha_model or captcha_model.captcha.lower() != captcha.lower():
            raise wtforms.ValidationError("邮箱验证码错误!")

    def validate_email(self, field):
        email = field.data
        user_model = UserModel.query.filter_by(email=email).first()
        if user_model:
            raise wtforms.ValidationError("邮箱已存在!")


class QuestionForm(wtforms.Form):
    title = wtforms.StringField(validators=[length(min=3, max=200)])
    content = wtforms.StringField(validators=[length(min=5)])


class AnswerForm(wtforms.Form):
    content = wtforms.StringField(validators=[length(min=1)])

qa.py

from flask import Blueprint, render_template, request, g, redirect, url_for, flash
from decorators import login_required
from exts import db
from .forms import QuestionForm,AnswerForm
from models.models import QuestionModel, AnswerModel
from sqlalchemy import or_

bp = Blueprint("qa", __name__, url_prefix="/")


@bp.route("/")
def index():
    questions = QuestionModel.query.order_by(db.text("-create_time")).all()
    return render_template("index.html", questions=questions)


@bp.route("/question/public", methods=['GET', 'POST'])
@login_required  # 装饰器
def public_question():
    # 判断是否登录,没有登录,跳转到登录页面
    if request.method == 'GET':
        return render_template("public_question.html")
    else:
        form = QuestionForm(request.form)
        if form.validate():
            title = form.title.data
            content = form.content.data
            question = QuestionModel(title=title, content=content, author=g.user)
            db.session.add(question)
            db.session.commit()
            return redirect("/")
        else:
            flash("标题或内容格式错误!")
            return redirect(url_for("qa.public_question"))


@bp.route("/question/<int:question_id>")
def question_detail(question_id):
    question = QuestionModel.query.get(question_id)
    return render_template("detail.html", question=question)


@bp.route("/answer/<int:question_id>", methods=['POST'])
@login_required  # 装饰器
def answer(question_id):  # 注册
    form = AnswerForm(request.form)  # 存储前端表单的内容
    if form.validate():
        content = form.content.data
        answer_model = AnswerModel(content=content, author=g.user, question_id=question_id)
        db.session.add(answer_model)
        db.session.commit()
        return redirect(url_for("qa.question_detail", question_id=question_id))
    else:
        flash("请填写评论再提交!")
        return redirect(url_for("qa.question_detail", question_id=question_id))


@bp.route("/search")
def search():
    kword = request.args.get("kword")
    questions = QuestionModel.query.filter(or_(QuestionModel.title.contains(kword),QuestionModel.content.contains(kword))).order_by(db.text("-create_time"))

    return render_template("index.html",questions = questions)

user.py

from datetime import datetime
from flask import Blueprint, render_template, request, redirect, url_for, jsonify, session, flash
from exts import mail
from flask_mail import Message
from models.models import EmailCaptchaModel, UserModel
import string
import random
from exts import db
from .forms import RegisterForm, LoginForm
from werkzeug.security import generate_password_hash, check_password_hash

bp = Blueprint("user", __name__, url_prefix="/user")


@bp.route("/login", methods=['GET', 'POST'])
def login():    # 登录
    if request.method == 'GET':
        return render_template("login.html")
    else:
        form = LoginForm(request.form)  # 存储前端表单的内容
        if form.validate():
            email = form.email.data
            password = form.password.data
            user = UserModel.query.filter_by(email=email).first()
            if user and check_password_hash(user.password, password):
                session['user_id'] = user.id
                return redirect("/")
            else:
                flash("邮箱和密码不匹配!")
                return redirect(url_for("user.login"))
        else:
            flash("格式错误!")
            return redirect(url_for("user.login"))


@bp.route("/register", methods=['GET', 'POST'])
def register():    # 注册
    if request.method == 'GET':
        return render_template("register.html")
    else:
        form = RegisterForm(request.form)  # 存储前端表单的内容
        if form.validate():
            email = form.email.data
            username = form.username.data
            password = form.password.data

            hash_password = generate_password_hash(password)
            user = UserModel(email=email, username=username, password=hash_password)
            db.session.add(user)
            db.session.commit()
            return redirect(url_for("user.login"))
        else:
            return redirect(url_for("user.register"))


@bp.route("/captcha", methods=['POST'])
def get_captcha():  # 验证码
    email = request.form.get("email")
    letters = string.ascii_letters + string.digits
    captcha = "".join(random.sample(letters, 4))

    if email:
        message = Message(
            subject="邮箱主题",
            recipients=[email],
            body=f"【圈圈论坛】您的注册码是:{captcha},请不要告诉任何人哦!"
        )
        mail.send(message)
        captcha_model = EmailCaptchaModel.query.filter_by(email=email).first()
        if captcha_model:
            captcha_model.captcha = captcha
            captcha_model.create_time = datetime.now()
            db.session.commit()
        else:
            captcha_model = EmailCaptchaModel(email=email, captcha=captcha)
            db.session.add(captcha_model)
            db.session.commit()
            # print("captcha:", captcha)
        # 200 成功、正常的请求
        return jsonify({"code": 200})
    else:
        # 400 客户端错误
        return jsonify({"code": 400, "message": "请先传递邮箱!"})


@bp.route("/logout")
def logout():   # 退出登录
    session.clear()
    return redirect(url_for("user.login"))

models

models.py

from exts import db
from datetime import datetime


class UserModel(db.Model):
    __tablename__ = "user"
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    username = db.Column(db.String(100), nullable=False)
    password = db.Column(db.String(200), nullable=False)
    email = db.Column(db.String(100), nullable=False, unique=True)
    join_time = db.Column(db.DateTime, default=datetime.now)


class EmailCaptchaModel(db.Model):
    __tablename__ = "email_captcha"
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    email = db.Column(db.String(100), nullable=False, unique=True)
    captcha = db.Column(db.String(10), nullable=False)
    create_time = db.Column(db.DateTime(100), default=datetime.now)


class QuestionModel(db.Model):
    __tablename__ = "question"
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    create_time = db.Column(db.DateTime(100), default=datetime.now)
    author_id = db.Column(db.Integer, db.ForeignKey("user.id"))

    author = db.relationship("UserModel", backref="questions")


class AnswerModel(db.Model):
    __tablename__ = "answer"
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    content = db.Column(db.Text, nullable=False)
    create_time = db.Column(db.DateTime(100), default=datetime.now)
    question_id = db.Column(db.Integer, db.ForeignKey("question.id"))
    author_id = db.Column(db.Integer, db.ForeignKey("user.id"))

    question = db.relationship("QuestionModel", backref=db.backref("answers",order_by=create_time.desc()))
    author = db.relationship("UserModel", backref="answers")
  • flask db init:初始化迁移环境,只需要运行一次。
  • flask db migrate:生成一个迁移脚本,用于将数据库模式更新到最新版本。
  • flask db upgrade:将数据库迁移到最新版本。

static

bootstrap

bootstrap@4.6.min.css

Bootstrap中文文档: v4.bootcss.com/docs/gettin…
将此链接的内容全部复制,cdn.jsdelivr.net/npm/bootstr…,粘贴到bootstrap@4.6.min.css在这里插入图片描述

jquery

jquery.3.6.min.js

将此链接的内容全部复制,cdn.bootcdn.net/ajax/libs/j…
粘贴到jquery.3.6.min.js

js

register.js

function bindCaptchaBtnClick(){
    $("#captcha-btn").on("click",function(event){
        var $this = $(this);
        var email = $("input[name='email']").val();
//        alert(email);
        if(!email){
            alert("请输入邮箱!");
            return;
        }

        // 通过js发送网络请求,ajax:Async Javascript and XML(JSON)
        $.ajax({
            url:"/user/captcha",
            method:"POST",
            data:{
                "email":email
            },
            success:function(res){
                var code = res['code'];
                if(code == 200){
                    // 取消点击事件
                    $this.off("click");
                    // 开始倒计时
                    var countDown = 60;
                    var timer = setInterval(function(){
                        countDown -= 1;
                        if(countDown> 0){
                            $this.text(countDown+"秒后重新发送");
                        }else{
                            $this.text("获取验证码");
                            bindCaptchaBtnClick();
                            clearInterval(timer);
                        }
                    },1000);
                    alert("验证码发送成功!");
                }else{
                    alert(res['message']);
                }
            }
        })
    });
}

// 等网页文档所有元素都加载完成后再执行
$(function(){
    bindCaptchaBtnClick();
});

templates

base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}{% endblock %}</title>
    <link rel="stylesheet" href="{{url_for('static',filename='bootstrap/bootstrap@4.6.min.css')}}">
    {% block head %}{% endblock %}
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container">
        <a class="navbar-brand" href="#">论坛</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
                aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item active">
                    <a class="nav-link" href="{{ url_for('qa.index') }}">首页<span class="sr-only">(current)</span></a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{{ url_for('qa.public_question') }}">发布问答</a>
                </li>
                <li class="nav-item ml-2">
                    <form class="form-inline my-2 my-lg-0" method="get" action="{{ url_for('qa.search') }}">
                        <input class="form-control mr-sm-2" type="search" placeholder="关键词"
                               aria-label="Search" name="kword">
                        <button class="btn btn-outline-success my-2 my-sm-0" type="submit">搜索</button>
                    </form>
                </li>
            </ul>
        </div>

        <div class="collapse navbar-collapse" id="dengluzhuce">
            <ul class="navbar-nav">
                {% if user %}
                    <li class="nav-item">
                        <span class="nav-link">{{ user.username }}</span>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('user.logout') }}">退出登录</a>
                    </li>
                {% else %}
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('user.login') }}">登录</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('user.register') }}">注册</a>
                    </li>
                {% endif %}
            </ul>
        </div>
    </div>
    </nav>
<div class="container">
  {% block body %}{% endblock %}
</div>

</body>
</html>

index.html

{% extends "base.html" %}

{% block title %}论坛首页{% endblock %}

{% block head %}{% endblock %}

{% block body %}
<div class="row mt-4 justify-content-md-center">
    <div class="col"></div>
    <div class="col-6">
        <div class="card">
            {% for question in questions %}
            <div class="card-header">
                <a href="{{ url_for('qa.question_detail',question_id=question.id) }}">{{ question.title}}</a>
            </div>
            <div class="card-body">
                {{ question.content}}
                <div class="blockquote-footer" style="text-align:right">
                    <span>{{ question.author.username}}</span>
                    <span style="text-align:right">{{ question.create_time}}</span>
                </div>
            </div>
            {% endfor %}
        </div>
    </div>
    <div class="col"></div>
</div>
{% endblock %}

register.html

{% extends "base.html" %}

{% block title %}注册{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static',filename='bootstrap/bootstrap@4.6.min.css') }}">
<script src="{{ url_for('static',filename='jquery/jquery.3.6.min.js')}}"></script>
<script src="{{ url_for('static',filename='js/register.js')}}"></script>
{% endblock %}

{% block body %}
<div class="row mt-4 justify-content-md-center">
    <div class="col"></div>
    <div class="col-6">
        <h1 style="text-align:center">注册</h1>
        <form action="{{ url_for('user.register') }}" method="post" id="register-form">
            <!--        <input type="hidden" name="csrf_token" value="">-->
            <div class="form-group">
                <div class="input-group">
                    <input type="text" class="form-control" name="email" placeholder="邮箱">
                    <div class="input-group-append">
                        <button type="button" id="captcha-btn" class="btn btn-outline-secondary">发送验证码</button>
                    </div>
                </div>
            </div>
            <div class="form-group">
                <input type="text" class="form-control" name="captcha" placeholder="邮箱验证码">
            </div>
            <div class="form-group">
                <input type="text" class="form-control" name="username" placeholder="用户名">
            </div>
            <div class="form-group">
                <input type="password" class="form-control" name="password" placeholder="密码">
            </div>
            <div class="form-group">
                <input type="password" class="form-control" name="confirm_password" placeholder="确认密码">
            </div>
            <div class="form-group">
                <button type="submit" class="btn btn-warning btn-block" id="submit-btn">立即注册</button>
            </div>
            <div class="form-group">
                <a href="{{ url_for('user.login') }}" class="signup-link">返回登录</a>
                <a href="#" class="resetpwd-link" style="float:right;">找回密码</a>
            </div>
        </form>
    </div>
    <div class="col"></div>
</div>
{% endblock %}

login.html

{% extends "base.html" %}

{% block title %}
  登录
{% endblock %}
{% block head %}
{% endblock %}

{% block body %}
  <div class="row mt-4 justify-content-md-center">
      <div class="col"></div>
      <div class="col-6">
          <h1 style="text-align:center">登录</h1>
          <form action="{{ url_for('user.login') }}" method="post">
<!--            <input type="hidden" name="csrf_token" value="##">-->
            <div class="form-group">
              <input type="text" class="form-control" name="email" placeholder="邮箱">
            </div>
            <div class="form-group">
              <input type="password" class="form-control" name="password" placeholder="密码">
            </div>
            <div class="checkbox">
              <label>
                <input type="checkbox" name="remember" value="1">记住我
              </label>
            </div>
            {% for message in get_flashed_messages() %}
                <div class="form-group">
                      <div class="text-danger">{{ message }}</div>
                </div>
            {% endfor %}
            <div class="form-group">
              <button type="submit" class="btn btn-warning btn-block" id="submit-btn">立即登录</button>
            </div>
            <div class="form-group">
              <a href="{{ url_for('user.register') }}" class="signup-link">没有账号?立即注册</a>
              <a href="#" class="resetpwd-link" style="float:right;">找回密码</a>
            </div>
          </form>
      </div>
      <div class="col"></div>
  </div>
{% endblock %}

public_question.html

{% extends "base.html" %}

{% block title %}发布问答{% endblock %}

{% block body %}
<div class="row mt-4">
    <div class="col"></div>
    <div class="col-8">
        <h1 style="text-align:center">发布问答</h1>
        <form action="{{ url_for('qa.public_question') }}" method="post">
            <div class="form-group">
                <input type="text" name="title" class="form-control" placeholder="请输入标题">
            </div>
            <div class="form-group">
                <textarea name="content" class="form-control" id="" cols="30" rows="10"
                          placeholder="请输入内容"></textarea>
            </div>
            {% for message in get_flashed_messages() %}
            <div class="form-group">
                <div class="text-danger">{{ message }}</div>
            </div>
            {% endfor %}
            <div class="form-group" style="text-align:right">
                <button class="btn btn-primary">发布</button>
            </div>
        </form>
    </div>
    <div class="col"></div>
</div>
{% endblock %}

detail.html

{% extends "base.html" %}

{% block title %}{{ question.title }}{% endblock %}

{% block head %}{% endblock %}

{% block body %}
<div class="row mt-4 justify-content-md-center">
    <div class="col"></div>
    <div class="col-6">
        <div class="card">
            <div class="card-header" style="text-align:center">
                <h3>{{ question.title}}</h3>
                <span>作者:{{ question.author.username}}</span>
                <span style="text-align:right">时间:{{ question.create_time}}</span>
            </div>
            <div class="card-body">
                {{ question.content}}
            </div>

            <div class="card-footer">
                <h4>评论({{ question.answers|length }})</h4>
                <form action="{{ url_for('qa.answer',question_id=question.id) }}" method="post">
                    <div class="form-group">
                        <input type="text" placeholder="请填写评论" name="content" class="form-control">
                    </div>
                    {% for message in get_flashed_messages() %}
                    <div class="form-group">
                        <div class="text-danger">{{ message }}</div>
                    </div>
                    {% endfor %}
                    <div class="form-group" style="text-align:right;">
                        <button class="btn btn-primary">评论</button>
                    </div>
                </form>
                <div class="list-group ">
                    <div class="border border-bottom-0" style="padding:10px">
                        {% for answer in question.answers %}
                        <div class="d-flex w-100 justify-content-between">
                            <h5 class="mb-1">{{ answer.author.username }}</h5>
                            <small>{{ answer.create_time }}</small>
                        </div>
                        <p class="mb-1">{{ answer.content }}</p>
                        <hr color="black">
                        {% endfor %}
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="col"></div>
</div>
{% endblock %}

app.py

from flask import Flask, session, g
import config
from exts import db, mail
from models.models import UserModel
from flask_migrate import Migrate
from blueprints import qa_bp, user_bp

app = Flask(__name__)
app.config.from_object(config)

db.init_app(app)
mail.init_app(app)

app.register_blueprint(qa_bp)
app.register_blueprint(user_bp)

migrate = Migrate(app, db)


# 流程:请求——>before_request——>视图函数——>视图函数返回模板——>context_processor
@app.before_request
def before_request():
    user_id = session.get("user_id")
    if user_id:
        try:
            user = UserModel.query.get(user_id)
            # 给g绑定一个叫做user的变量
            # setattr(g,"user",user)
            # 全局变量
            g.user = user
        except:
            g.user = None


@app.context_processor
def context_processor():
    if hasattr(g, "user"):
        return {"user": g.user}
    else:
        return {}


if __name__ == '__main__':
    app.run(debug=True)

config.py

# MySQL所在的主机名
HOSTNAME = "127.0.0.1"
# MySQL监听的端口号,默认3306
PORT = 3306
# 连接MySQL的用户名
USERNAME = "root"
# 连接MySQL的密码
PASSWORD = "root"
# MySQL上创建的数据库名称
DATABASE = "python_bbs"
DB_URL = f"mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8mb4"
SQLALCHEMY_DATABASE_URI = DB_URL


# 邮箱配置
MAIL_SERVER = "smtp.qq.com"   #项目中用QQ邮箱
MAIL_PORT = 465
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_DEBUG = True   #发送邮件会提示日志信息
MAIL_USERNAME = "发送邮件的邮箱"
MAIL_PASSWORD = "授权码"  #登录qq邮箱——>设置——>账号——>POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务——>获取授权码
MAIL_DEFAULT_SENDER = "发送邮件的邮箱"

# 应用中使用 Flask-Session 等会话扩展时,需要设置一个随机的密钥,以防止会话数据被篡改。
# 可以在 Flask 应用的配置参数中添加 SECRET_KEY 变量,并设定一个复杂的随机字符串作为密钥
SECRET_KEY = 'a random string'

decorators.py

from flask import g, redirect, url_for
from functools import wraps


def login_required(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        if hasattr(g, "user"):
            return func(*args, **kwargs)
        else:
            return redirect(url_for("user.login"))

    return wrapper

exts.py

from flask_sqlalchemy import SQLAlchemy
from flask_mail import Mail


db = SQLAlchemy()
mail =Mail()