Flask-2进阶,结合前端篇


前端

CSS

https://www.cnblogs.com/dotnet261010/p/7198892.html

CSS的引入方式共有三种:

行内样式:使用style属性引入CSS样式

内部样式表:在style标签中书写CSS代码

外部样式表

1、样式优先级

行内样式>内部样式>外部样式(后两者是就近原则)

2、选择器优先级

优先级:ID选择器>类选择器>标签选择器

padding

设置 p 元素的 4 个内边距:

p
  {
  padding:2cm 4cm 3cm 4cm;
  }
  四个属性顺序:上 右 下 左
  三个属性顺序:上 左右 下
  两个属性:上下 左右
  一个属性:四周

margin 四个外边距,属性顺序同 padding

透明度设置

1、css rgba()设置颜色透明度,只背景透明

rgba(R,G,B,A);

R:红色值。正整数 (0~255)
G:绿色值。正整数 (0~255)
B:蓝色值。正整数(0~255)
A:透明度。取值0~1之间

.demo2{
	background:rgba(255,0,0,0.5);
}

2、css opacity属性设置背景透明度,所有元素透明

opacity: value ;

value :指定不透明度,从0.0(完全透明)到1.0(完全不透明)。
opacity 属性具有继承性,会使容器中的所有元素都具有透明度;

.demo2{
	opacity:0.5;
}

标签

标签定义超链接,用于从一张页面链接到另一张页面

猜想:其余标签 href 无效

设置背景图片

<style>
    body{background-image:url("{{ url_for('static',filename='/img/bj.png') }}")}
</style>

css圆角

border-radius: 5px;

label标签

label 元素不会向用户呈现任何特殊效果。不过,它为鼠标用户改进了可用性

如果您在 label 元素内点击文本,就会触发此控件,就是说,当用户选择该标签时,浏览器就会自动将焦点转到和标签相关的表单控件上

1

jQuery

基础内容查看jQuery笔记

image-20210116100918022

  • ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效
  • val() - 设置或返回表单字段的值

CDN 引入

<head>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>
例:search搜索
{% block middle %}
    <p>
    <input type="text" placeholder="search" name="search">
    <input type="button" value="search" id="search">
    </p>

    <table border="1" cellspacing="0" id="table1">
        <tr>
            <td>index</td>
            <td>username</td>
            <td>phone</td>
            <td>datatime</td>
            <td>action</td>

        </tr>
        {% for user in users %}
        <tr>
            <td>{{ loop.index }}</td>
            <td>{{ user.username }}</td>
            <td>{{ user.phone }}</td>
            <td>{{ user.rdatetime }}</td>
            <td>
                <a href="javascript:;">update</a>
                <a href="javascript:;">delete</a>
            </td>
        </tr>
        {% endfor %}
    </table>

    {% block js %}
        <script>
            $('#search').click(
                //因为#search是上边search按钮的id标签选择器名
                function () {
                    let content = $("input[name='search']").val()  
                    //所声明的变量,只在let命令所在的代码块内有效
                    //#input[name='search]使用这个进行选择的原因:找到对应元素
                    //<-元素选择器   标签选择器->let content = $("#input[name='search']").val()要提前指定id   
                    // 类选择器$("#input[name='search']").val()

                    alert(content);
                }
                )
        </script>

    {% endblock %}

{% endblock %}

image-20210118092538197

例:Tab 标签选择栏

1

image-20210122085414331

{% block mycontent %}
    <div id="middle">
        <div id="left">
            <div>
                <button type="button" class="btn btn-default">个人中心</button>
                <button type="button" class="btn btn-primary">信息修改</button>
            </div>
        </div>

        <div id="right">
            <div class="info_tab">content</div>
            <div class="info_tab">content</div>
        </div>
    </div>
    </div>
{% endblock %}

{% block scripts %}
    {{ super() }}
    <script>
        $(function () {
            $('.info_tab').hide();
            $('.info_tab').first().show();
            {#排他思想#}
            $('#left button').each(function (i) {
                $(this).click(function () {
                    $('.info_tab').hide();
                    $('.info_tab').eq(i).show();
                })
            })
        })
    </script>
{% endblock %}

1

vscode 和 pychram 等都支持快捷语句

table>tr>td{图片$}*4
按下 tab 键
<table>
    <tr>
        <td>图片1</td>
        <td>图片2</td>
        <td>图片3</td>
        <td>图片4</td>
    </tr>
</table>	
submit和button的区别

一、submit

1、提交按钮,点击之后直接将数据提交的服务器端。

2、使用submit后,页面支持键盘enter键操作。

3、submit需要有表单时,提交时才会带数据。

4、当有表单的时候,如果提交的数据很多,那么使用submit比button要好,可以减少很多数据的获取动作,但是要记得表单提交的数据要验证。

二、button

简单的按钮,有按钮的一些事务处理,有脚本就通过脚本将参数传过去。

button默认是不提交任何数据。

如果没有表单的话,又想通过提交某些数据给后台进行回应,则需要通过button,当然使用submit也可以,但是前提要拦截onclick事件。

table标签

https://www.w3school.com.cn/tags/tag_th.asp

  • table 表格
  • tr 行
  • th 定义表格内的表头单元格
  • td 单元格
label标签

<form>
  <label for="male">Male</label>
  <input type="radio" name="sex" id="male" />
  <br />
  <label for="female">Female</label>
  <input type="radio" name="sex" id="female" />
</form>
Bootstrap

Bootstrap 是一个用于快速开发 Web 应用程序和网站的前端框架,Bootstrap 是基于 HTML、CSS、JAVASCRIPT 的

文档:https://v3.bootcss.com/getting-started/

与前端相关优质项目

  • React 用于构建用户界面的 JavaScript 框架

  • svelte Svelte 是构建 Web 应用程序的一种新方法

  • nodejs Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境

  • npm NPM(node package manager)是 Node.js 世界的包管理器

  • vue.js Vue.js - 是一套构建用户界面的渐进式框架

  • Markdown Markdown 是一种轻量级标记语言

Bootstrap CDN

<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

<!-- 可选的 Bootstrap 主题文件(一般不用引入) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">

<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

Flask插件

Flask-Bootstrap 不支持 Bootstrap4

Bootstrap-Flask 轻量级代替品

这两个不能共存

使用需要安装,注意是虚拟环境,安装后就不需要下载bootstrap或者使用cdn了

pip install  -i https://pypi.tuna.tsinghua.edu.cn/simpletensorflow

使用 Flask-Bootstrap

配置初始化Flask

#ext/__init__.py
from flask_bootstrap import Bootstrap
bootstrap=Bootstrap()

#apps/__init__.py
from ext import db, bootstrap
create_app()
	# 初始化bootstrap
    # db.init_app(app=app) 和数据库db的格式相同
    bootstrap.init_app(app=app)

继承

{#base.html#}
{% extends 'bootstrap/base.html' %}
{% block title %}
index
{% endblock %}

内置 block 块

  • title

  • navbar

  • content

  • styles

  • scripts

  • head 注意:是head,不是heads

  • body

简单实例

image-20210120212522879

{% extends "bootstrap/base.html" %}

{% block title %}Flasky{% endblock %}

{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/user/register">register</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/">login</a></li>
            </ul>
        </div>
    </div>
</div>
{% endblock %}

{% block content %}
    {% block mycontent %}
    
    {% endblock %}
<div class="container">
    <div class="page-header">
    {% block page_content %}{% endblock %}
    </div>
</div>
{% endblock %}
Ajax

异步 JavaScript 和 XML

jQuery load() 方法

$(selector).load(URL,data,callback);
# 入口函数
必需的 URL 参数规定您希望加载的 URL。
可选的 data 参数规定与请求一同发送的查询字符串键/值对集合。
可选的 callback 参数是 load() 方法完成后所执行的函数名称。

实例:实现用户注册手机号信息判断

1

templates/register.html

......
{% block scripts %}
    {{ super() }}
    <script>
        {#当输入域失去焦点 (blur) #}
        $('#inputPhone').blur(function () {
            let phone = $(this).val();
            let span_ele = $(this).next('span');
            if (phone.length == 11){
                span_ele.css({"color":"#ff0011", "font-size": "12px"})
                span_ele.text('')
                $.get('{{ url_for('user.check_phone') }}',{phone:phone},function (data){
                {#  data为返回数据  #}
                    if(data.code != 200){
                        span_ele.text('用户已注册')
                    }
                })
            }
            else{
                span_ele.css({"color":"#ff0011", "font-size": "12px"})
                {#注意是字典格式#}
                span_ele.text('手机号长度有误')
            }
        })
    </script>
{% endblock %}

apps/user/view.py

......
@user_bp.route('check_phone', methods=['GET', 'POST'], endpoint='check_phone')
def check_phone():
    phone = request.args.get('phone')
    user = User.query.filter(User.phone == phone).all()
    print(user)
    # code:200可用  400不可用
    if len(user) > 0:
        return jsonify(code=400)
    else:
        return jsonify(code=200)
    # ajax 返回的数据必须是 json 格式   jsonify()内部封装了 json.dumps()
    
#或者:
    if len(user) > 0:
        code=400
    else:
        code=200
    return jsonify(code=code)
只要是每一种逻辑都有对应 return 不至于找不到返回内容即可

用户操作

<!--show.html-->
<tr>
    <td>{{ loop.index }}</td>
    <td>{{ user.username }}</td>
    <td>{{ user.phone }}</td>
    <td>{{ user.rdatetime }}</td>
	<td>
    <a href="{{ url_for('user.update') }}?id={{ user.id }}">update</a>
	<a href="{{ url_for('user.delete') }}?id={{ user.id }}">delete</a>
     {#错误写法 <a href="/{{ url_for('user.delete') }}?id={{ user.id }}">delete</a> #}
     {# 这样写他就找 / #}
</tr>

image-20210119103919569

查询

image-20210119142433502

<div>
    <p>
    <input type="text" placeholder="search" name="search">
    <input type="button" value="search" id="search">
    </p>
</div>
# view.py
@user_bp.route('/search', endpoint='search')
def user_search():
    if request.args.get('search'):
        keyword = request.args.get('search')

        user_list = User.query.filter(or_(User.username.contains(keyword), User.username.contains(keyword))).all()
        # 查询手机号或者用户名
        return render_template('user/show.html', users=user_list)
    else:
        return redirect(url_for('user.show'))
逻辑删除

就相当于更新,在数据库里添加isdelete字段进行判断

#model.py
from datetime import datetime
from ext import db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    username = db.Column(db.String(15), nullable=False)
    password = db.Column(db.String(64), nullable=False)
    phone = db.Column(db.String(11), unique=True)
    rdatetime = db.Column(db.DateTime, default=datetime.now)
    isdelete = db.Column(db.Boolean, default=False)
    # 逻辑删除字段
#view.py
@user_bp.route('/delete', endpoint='delete')
def user_delete():
    id = request.args.get('id')
    user = User.query.get(id)
    user.isdelete = True
    db.session.commit()
    return redirect(url_for('user.show'))

@user_bp.route('/show', endpoint='show')
def user_show():
    users = User.query.filter(User.isdelete == False).all()
    return render_template('user/show.html', users=users)

删除后数据库里依然有数据

bug:注册用户时手机号因为是unqiue的,如果手机号重复会报错

解决:注册时加一层数据库查询手机号,如果手机号存在则更新用户其他数据

物理删除
@user_bp.route('/delete', endpoint='delete')
def user_delete():
    id = request.args.get('id')
    user = User.query.get(id)
    db.session.delete(user)
    db.session.commit()
    return redirect(url_for('user.show'))

不需要单独设置 isdelete 列

更新用户
#view.py
@user_bp.route('/update', endpoint='update', methods=['GET', 'POST'])
def user_update():
    if request.method == 'POST':
        id = request.form.get('id')
        new_user = User.query.get(id)
        new_user.username = request.form.get('new_username')
        new_user.password = request.form.get('new_password')
        new_user.phone = request.form.get('new_phone')
        # 只有添加数据是用 db.sesson.add()
        db.session.commit()
        return redirect(url_for('user.show'))
    else:
        id = request.args.get('id')
        user = User.query.get(id)
        return render_template('user/update.html', user=user)
<!--update.html-->
{% extends 'base.html' %}

{% block middle %}
    <form action="{{ url_for('user.update') }}" method="post">
        <p><input type="hidden" name="id" value="{{ user.id }}"></p>
                {# 隐藏表单提交用户id #}
        <p><input type="text" name="new_username" value="{{ user.username }}"></p>
        <p><input type="text" name="new_password" placeholder="new password"></p>
        <p><input type="text" name="new_phone" value="{{ user.phone }}"></p>
        <p><input type="submit" value="submit"></p>
    </form>
{% endblock %}

多表关系

文档:http://www.pythondoc.com/flask-sqlalchemy/models.html#one-to-many

表与表间的关系可能十分复杂

#### 外键

外键表示了两个关系之间的相关联系,以另一个关系的外键作主关键字的表被称为主表,具有此外键的表被称为主表的从表,外键又称作外关键字image-20210119145824052

user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

image-20210119184316860

外键是从表中的一个字段

一对多

one to manyimage-20210119210440785项目完成总览image-20210119201054575

user/model.py

from datetime import datetime
from ext import db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    username = db.Column(db.String(15), nullable=False)
    password = db.Column(db.String(64), nullable=False)
    phone = db.Column(db.String(11), unique=True)
    email = db.Column(db.String(25))
    icon = db.Column(db.String(40))
    isdelete = db.Column(db.Boolean, default=False)
    rdatetime = db.Column(db.DateTime, default=datetime.now)

    articles = db.relationship('Article', backref='user')
    # 用于反向查找,实际不会添加到数据库中,相当于把两张表连起来
    # 和外键匹配
    # Article 为模型名大写
    # user小写因为表名在数据库中为小写

article/model.py

from datetime import datetime

from ext import db


class Article(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text, nullable=False)
    pdatetim = db.Column(db.DateTime, default=datetime.now)
    click_num = db.Column(db.Integer, default=0)
    save_num = db.Column(db.Integer, default=0)
    love_num = db.Column(db.Integer, default=0)

    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    # 外键,这个字段会添加数据库中
@article_bp.route('/all')
def all_article():
    articles = Article.query.all()
    return render_template('article/all.html', articles=articles)


@article_bp.route('/all1')
def all_by_id():
    id = request.args.get('id')
    users = User.query.get(id)
    return render_template('article/all1.html', users=users)

article/add_article.html

<form action="{{ url_for("article.publish") }}" method="post">
    <p><input type="text" name="title" placeholder="文章标题"></p>
    <p><textarea cols="50" rows="10" name="content" placeholder="输入文章内容"></textarea></p>
    <p>
        user:
        <select name="uid">
            <option value="0">please select user</option>
            {% for user in users %}
                <option value="{{ user.id }}">{{ user.username }}</option>
            {% endfor %}
        </select>
    </p>
    <p><input type="submit" value="add article"></p>
</form>

article/all.html

<!--文章->查user.username-->
{% for article in articles %}
<div id="article">
<p>
    <h3>{{ article.title }}</h3>
    <div>作者:{{ article.user.username }}</div>
            {#  backref 反向引用  #}
    <p>
        {{ article.content }}
    </p>
    <div>{{ article.pdatetime }}</div>
</p>
</div>
{% endfor %}

article/all1.html

{#根据用户id找文章#}
{% for article in users.articles %}
<div id="article">
<h1>
    {{ article.title }}
</h1>
<div>
    {{ users.username }}
</div>
<p>
    {{ article.content }}
</p>
<p>
    {{ article.pdatetim }}
</p>
</div>
{% endfor %}
多对多

many to many

image-20210119210342498

image-20210120090256933

可以通过中间表建立关系

pycharm查看表关系

首先选择多个表->diagrams->show

多对多实例:用户与商品

功能:用户和商品展示,用户购卖商品,根据商品找用户,根据用户找商品1

image-20210120144631854

上图为实际表关系

apps/goods/model.py

from apps.user.model import User
from ext import db

class Goods(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    gname = db.Column(db.String(100), nullable=False)
    price = db.Column(db.Float, nullable=False)
    # back reference
    users = db.relationship('User', backref='goodslist', secondary='user_goods')
    # relationship 定义的字段不会填入数据库,是给view和template定义的
    # users和User表建立联系但是没有外键,所以需要 secondary适用于多对多情况
    # Goods可以通过 Goods.users.xxx 查users表  反向:users.googlist.xxx查Goods表
    # relationship 定义在Goods表或User表都可以,只是需要改值

# 中间表 添加表不需要执行 python app.py db init 此命令尽在没有migrations文件夹时需执行,添加字段也是migrate即可
class User_goods(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    goods_id = db.Column(db.Integer, db.ForeignKey(Goods.id), nullable=False)
    num = db.Column(db.Integer, default=0)

apps/user/model.py

from datetime import datetime
from ext import db


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    username = db.Column(db.String(15), nullable=False)
    password = db.Column(db.String(64), nullable=False)
    phone = db.Column(db.String(11), unique=True)
    email = db.Column(db.String(25))
    icon = db.Column(db.String(40))
    isdelete = db.Column(db.Boolean, default=False)
    rdatetime = db.Column(db.DateTime, default=datetime.now)

#或者在此添加    #goods=db.relationship('Goods',backref='userlist',secondary='user_goods')

apps/goods/view.py

from flask import Blueprint, render_template, request
from apps.goods.model import Goods, User_goods
from apps.user.model import User
from ext import db

goods_bp = Blueprint('goods', __name__)


@goods_bp.route('/')
def goods_index():
    users_list = User.query.all()
    goods_list = Goods.query.all()
    return render_template('goods/show.html', users_list=users_list, goods_list=goods_list)


@goods_bp.route('/buy')
def goods_buy():
    ug = User_goods()
    # 先创建 User_goods 的实例化对象
    ug.user_id = request.args.get('uid')
    ug.goods_id = request.args.get('gid')
    db.session.add(ug)
    # 添加 ug 对象到缓存中
    db.session.commit()
    return 'add ok'


# 根据商品找用户
@goods_bp.route('/finduser', endpoint='finduser')
def find_user():
    goods_id = request.args.get('gid')
    goods = Goods.query.get(goods_id)
    return render_template('goods/finduser.html', goods=goods)


# 根据用户找商品
@goods_bp.route('/findgoods', endpoint='findgoods')
def find_goods():
    user_id = request.args.get('uid')
    users = User.query.get(user_id)
    return render_template('goods/findgoods.html', users=users)

image-20210120105426370

templates/goods/show.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>show</title>
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
    <style>
        #goods_table tr td{
            width: 100px;
            background-color: cornflowerblue;
        }
    </style>
</head>

<body>
<div>
<form>
    <p>
        {#展示用户#}
        <select name="uid" >
            <option value="0">please select user</option>
            {% for users in users_list %}
                <option value="{{ users.id }}">{{ users.username }}</option>
            {% endfor %}
        </select>
    </p>

    {#展示商品#}
    <table border="1px solid" cellspacing="0" id="goods_table" style="text-align: center">
        <tr>
            <th>index</th>
            <th>goods</th>
            <th>price</th>
            <th>action</th>
        </tr>
        {% for goods in goods_list %}
            <tr>
                <td>{{ loop.index }}</td>
                <td><a href="{{ url_for('goods.finduser') }}?gid={{ goods.id }}">{{ goods.gname }}</a></td>
                    {# 建立超链接 #}
                <td>{{ goods.price }}</td>
                <td><input type="button" class="btngoods" value="buy" tag="{{ goods.id }}"></td>
                {#  通过tag标签把goods.id值传给view  #}
            </tr>
        {% endfor %}
    </table>
</form>
</div>

<script>
    $('.btngoods').click(function () {
        goods_id = $(this).attr('tag');
        user_id = $("select[name='uid']").val();
        {#  css选择器 选择具有值为uid的name属性的select标签 注意引号  #}
        location.href='/buy?uid='+user_id+'&'+'gid='+goods_id;
    })
</script>
</body>
</html>

templates/goods/finduser.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>finduser</title>
</head>
<body>
<div>
    <h3>{{ goods.gname }}</h3>
    <br>
        {% for user in goods.users %}
            <p>
                <a href="{{ url_for('goods.findgoods') }}?uid={{ user.id }}">{{ user.username }}</a>
            </p>
        {% endfor %}
</div>
</body>
</html>

templates/goods/findgoods.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>findgoods</title>
</head>
<body>
<div>
    <h3>{{ users.username }}</h3>
</div>
    {% for goods in users.goodslist %}
    <p>{{ goods.gname }}------{{ goods.price }}</p>
    {% endfor %}
</body>
</html>

app.py

from apps.user.model import User
from apps.article.model import Article
from apps.goods.model import *
# 一定要将表对象在此引用否则无法在数据库生成表
多对多实例2:用户,文章,评论

image-20210119140831577

from datetime import datetime
from apps.user.model import User
from ext import db


class Atype(db.Model):
    __tablename__ = 'A_type'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    type_name = db.Column(db.String(25), nullable=False)
    articles = db.relationship('Article', backref='typename')


class Article(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text, nullable=False)
    pdatetime = db.Column(db.DateTime, default=datetime.now)
    click_num = db.Column(db.Integer, default=0)
    save_num = db.Column(db.Integer, default=0)
    love_num = db.Column(db.Integer, default=0)
    type_id = db.Column(db.Integer, db.ForeignKey('A_type.id'))
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))

    comments = db.relationship('Comment', backref='articles')


class Comment(db.Model):
    # 自定义表名
    __tablename__ = 'comment'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    comment = db.Column(db.String(255), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    article_id = db.Column(db.Integer, db.ForeignKey('article.id'))
    # 注意 db.ForeignKey() 格式
    cdatetime = db.Column(db.DateTime, default=datetime.now)

    def __str__(self):
        return self.comment

注意:用pycharm查看时 atype 没有连起来(可能没成功建立起外键关系),试了很多次都是这样

image-20210120181746326

ELSE

补充蓝图
#apps/user/__init__.py
user_bp = Blueprint('user', __name__, url_prefix='/user')

@user_bp.route('/', endpoint='index')
def index():
    return 'index'

#添加url_prefix后
Map([<Rule '/user/' (HEAD, OPTIONS, GET) -> user.index>,
 <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>])

# url_prefix='/user' 必须是加 / 
ValueError: urls must start with a leading slash

image-20210120193027022

数据加密

除了 hashlib 库,Flask 自带了hash加盐加密方法

加密:generate_password_hash

from werkzeug.security import generate_password_hash

#generate_password_hash(password,method='pbkdf2:sha256',salt_length=*)

user.password = generate_password_hash(password)

# 数据格式:method$salt$hash

注意:加密后长度是变化的,所以数据库 password 字段要注意长度

Flask 由于封装问题修改数据字段长度在进行 migrate 会显示 not update,Flask 只能是添加删除数据库字段可会显示有更新,可以通过 pycharm 更改

Django 都可以

密码匹配:check_password_hash

@user_bp.route('/login', methods=['GET', 'POST'], endpoint='login')
def user_login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        users = User.query.filter(User.username == username).all()
        for user in users:
            flag = check_password_hash(user.password, password)
            # flask 封装检查password函数,返回值bool类型,匹配为Ture,否则为False
            #user.password 为加密过的密码,password 为未加密密码
            if flag:
                return jsonify(msg='登录成功', code=200)
        else:
            return jsonify(msg='登录失败', code=400)
    else:
        return render_template('user/login.html')

会话机制

记录用户登陆状态

HTTP是无状态协议

Cookie,Session

Cookie

保存

# 通过 response 对象保存
response = redirect(xxx)
response = render_template(xxx)
response = Response()
response = make_response(xxx)
response = jsonify(xxx)
# 通过对象调用方法
response.set_cookie(key,value,max_age)

set_cookie(属性)
#属性
name		cookie的名称
value		cookie的值
expire		过期时间
path		有效路径
domain		域名
secure		https专用 true为安全传输
httponly	仅通过http协议访问,不能通过js

#例:
@user_bp.route('/login', methods=['GET', 'POST'], endpoint='login')
def user_login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        users = User.query.filter(User.username == username).all()
        for user in users:
            flag = check_password_hash(user.password, password)
            # flask 封装检查password函数,匹配为Ture,否则为False
            if flag:
                response = redirect(url_for('user.index'))
                # 需要先创建一个 response 对象
                response.set_cookie(key='username', value=username, max_age=1800)
                return response
        else:
            return render_template('user/login.html', msg='登陆失败')
    else:
        return render_template('user/login.html')

获取

# 通过 request 对象获取
request.form.get()
request.args.get
cookie也在 request 对象中
request.cookies.get(key) --->  value

#例:
@user_bp.route('/', endpoint='index')
def index():
    username = request.cookies.get('username')
    return render_template('user/index.html', username=username)

删除

# 通过 response 对象删除
response = redirect(xxx)
response = render_template(xxx)
response = Response()
response = make_response(xxx)
response = jsonify(xxx)
# 通过对象调用方法
response.delete_cookie(key)

#例:
@user_bp.route('/logout',endpoint='logout')
def user_logout():
    response=redirect(url_for('user.login'))
    response.delete_cookie('username')
    return response
Session

在服务器保存,字典类型

cookie,session结合使用,cookie存储session_id,具体数据存储在session

需要设置 setting.py

SECRET_KEY = ‘xxxxx’

xx这个值自己定义即可,目的是用于 sessionid 的加密

#例:settings.py
class Config:
    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:root@127.0.0.1:3306/flask'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_ECHO = True
    SECRET_KEY='fdsajfidposajgiopds'

设置:

若果要使用session,需要直接导入:
from flask import session

session[key]=value
会将key=value保存到session内存空间中

#例
@user_bp.route('/login', methods=['GET', 'POST'], endpoint='login')
def user_login():
	......
    session['username'] = username
    return redirect(url_for('user.index'))
	......

获取

value = session[key] 或 value = session.get(key)

#例:
@user_bp.route('/', endpoint='index')
def index():
    username = session['username']
    return render_template('user/index.html', username=username)

清除

session.clear()		删除session内存空间和删除cookie,(常用)

del session[key]		只会删除session中的这个键值对,不会删除session空间和cookie

# 例
@user_bp.route('/logout', endpoint='logout')
def user_logout():
    session.clear()
    return redirect(url_for('user.index'))

手机验证码

云短信服务 SMS(Short Message Service,SMS)

可以找各家提供的免费赠送的部分,有些是产生验证码返回给后台,有的是后台产生验证码发送给 SMS,再发送给前端

image-20210122102133931

网易开发文档:https://support.dun.163.com/documents/2018101001?docId=210168759616458752

控制台:https://dun.163.com/dashboard#/m/sms/index/?pid=YD00030772740753

查看凭证

image-20210122091759603

签名需要实名认证审核

image-20210122091602427

image-20210122091449899

例:手机验证码登录

具体页面效果

1

taskId的调用-使用session实现

image-20210122133611694

apps/user/smssend.py —sms开发文档中有

from hashlib import md5
import json
import random
import time
import urllib
import urllib.request


class SmsSendAPIDemo(object):
    """易盾短信发送接口示例代码"""
    API_URL = "https://sms.dun.163.com/v2/sendsms"
    VERSION = "v2"

    def __init__(self, secret_id, secret_key, business_id):
        """
        Args:
            secret_id (str) 产品密钥ID,产品标识
            secret_key (str) 产品私有密钥,服务端生成签名信息使用
            business_id (str) 业务ID,易盾根据产品业务特点分配
        """
        self.secret_id = secret_id
        self.secret_key = secret_key
        self.business_id = business_id

    def gen_signature(self, params=None):
        """生成签名信息
        Args:
            params (object) 请求参数
        Returns:
            参数签名md5值
        """
        buff = ""
        for k in sorted(params.keys()):
            buff += str(k) + str(params[k])
        buff += self.secret_key
        return md5(buff.encode("utf-8")).hexdigest()

    def send(self, params):
        """请求易盾接口
        Args:
            params (object) 请求参数
        Returns:
            请求结果,json格式
        """
        params["secretId"] = self.secret_id
        params["businessId"] = self.business_id
        params["version"] = self.VERSION
        params["timestamp"] = int(time.time() * 1000)
        params["nonce"] = int(random.random() * 100000000)
        params["signature"] = self.gen_signature(params)

        try:

            params = urllib.parse.urlencode(params)
            params = params.encode('utf-8')
            request = urllib.request.Request(self.API_URL, params)
            content = urllib.request.urlopen(request, timeout=5).read()
            return json.loads(content)
        except Exception as ex:
            print("调用API接口失败:", str(ex))

apps/user/view.py 主要逻辑功能实现,可以把发送验证码功能单独封装

import hashlib
from flask import Blueprint, render_template, request, url_for, redirect, jsonify, session
from werkzeug.security import generate_password_hash, check_password_hash

from apps import db
from apps.user.model import User
from apps.user.smssend import SmsSendAPIDemo

user_bp = Blueprint('user', __name__, url_prefix='/user')


@user_bp.route('/', endpoint='index')
def index():
    uid = session.get('uid')
    # 如果没有 uid 对应的值,也就是取不到的话值为 None
    if uid:
        user = User.query.get(uid)
        return render_template('user/index.html', user=user)
    return render_template('user/index.html')


@user_bp.route('/center', endpoint='center')
def center():
    uid = session.get('uid')
    if uid:
        user = User.query.get(uid)
        return render_template('user/center.html', user=user)
    return render_template('user/center.html')


@user_bp.route('/register', methods=['GET', 'POST'], endpoint='register')
def register():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        repassword = request.form.get('repassword')
        phone = request.form.get('phone')
        email = request.form.get('email')
        print(email)
        if password == repassword:
            user = User()
            user.username = username
            user.password = generate_password_hash(password)
            user.phone = phone
            user.email = email
            db.session.add(user)
            db.session.commit()
            return 'ok'
        else:
            return 'password is diff'
    else:
        return render_template('user/register.html')


@user_bp.route('/check_phone', methods=['GET', 'POST'], endpoint='check_phone')
def check_phone():
    phone = request.args.get('phone')
    user = User.query.filter(User.phone == phone).all()
    print(user)
    # code:200可用  400不可用
    if len(user) > 0:
        code = 400
    else:
        code = 200
    return jsonify(code=code)
    # ajax 返回的数据必须是 json 格式   jsonify()内部封装了 json.dumps()


@user_bp.route('/login_check_phone', methods=['GET', 'POST'], endpoint='login_check_phone')
def login_check_phone():
    phone = request.args.get('phone')
    user = User.query.filter(User.phone == phone).all()
    print(user)
    # code:200已注册  400未注册
    if len(user) > 0:
        code = 200
    else:
        code = 400
    return jsonify(code=code)
    # ajax 返回的数据必须是 json 格式   jsonify()内部封装了 json.dumps()


@user_bp.route('/login', methods=['GET', 'POST'], endpoint='login')
def user_login():
    if request.method == 'POST':
        # f=1 账户密码登录   f=2 手机号验证码登录
        if request.args.get('f') == '1':
            username = request.form.get('username')
            password = request.form.get('password')
            users = User.query.filter(User.username == username).all()
            for user in users:
                flag = check_password_hash(user.password, password)
                # flask 封装检查password函数,匹配为Ture,否则为False
                if flag:
                    session['uid'] = user.id
                    return redirect(url_for('user.center'))
            else:
                return render_template('user/login.html', msg='登陆失败')

        elif request.args.get('f') == '2':
            # '2' 一定要加引号,因为request.args.get('f')接受的是字符串类型
            phone = request.form.get('phone')
            code = request.form.get('code')
            valid_code = session.get('code')
            if code == valid_code:
                # 先验证 验证码是否正确
                user = User.query.filter(User.phone == phone).first()
                if user:
                    session['uid'] = user.id
                    return redirect(url_for('user.center'))
            else:
                return render_template('user/login.html', msg='验证码有误')
        else:
            return 'request params is worry'

    else:
        return render_template('user/login.html')


@user_bp.route('/logout', endpoint='logout')
def user_logout():
    session.clear()
    return redirect(url_for('user.login'))


@user_bp.route('/send_code', endpoint='send_code')
def send_code():
    # phone = request.args.get('phone')
    # SECRET_ID = "05c6ee1b7319dfd6aad7e2c51b00cc73"  # 产品密钥ID,产品标识
    # SECRET_KEY = "7042637886876a15101994b371f169d3"  # 产品私有密钥,服务端生成签名信息使用,请严格保管,避免泄露
    # BUSINESS_ID = "225e296526b24078887d47a06dd234b7"  # 业务ID,易盾根据产品业务特点分配
    # api = SmsSendAPIDemo(SECRET_ID, SECRET_KEY, BUSINESS_ID)
    # params = {
    #     "mobile": "your mobile",
    #     "templateId": "10084",
    #     "paramType": "json",
    #     "params": "json格式字符串"
    #     # 国际短信对应的国际编码(非国际短信接入请注释掉该行代码)
    #     # "internationalCode": "对应的国家编码"
    # }
    # ret = api.send(params)
    session['code'] = '12345'
    return jsonify(code=200, msg='短信发送成功')
    # if ret is not None:
    #     if ret["code"] == 200:
    #         taskId = ret["data"]["taskId"]
    #         # 收到验证码 taskId 是局部变量 login() 没法调用
    #         # 如果将 taskId 设为全局变量多用户登录时将存在逻辑问题
    #         session[phone] = taskId
    #         return jsonify(code=200, msg='短信发送成功')
    #     else:
    #         return jsonify(code=400, msg='短信发送失败')

templates/user/login.html

{% extends 'base.html' %}

{% block styles %}
    {{ super() }}
    {# 加了super就是继承原有的样式 #}
    <style>
        #container {
            width: 400px;
            margin: 20px auto;
        }
    </style>
{% endblock %}

{% block mycontent %}
    <form class="form-horizontal" id="container" method="post" action="{{ url_for('user.login') }}?f=1">

        <div>
            <ul class="nav nav-tabs" id="loginselect">
                <li role="presentation" id="passwordlogin"><a>账号密码登录</a></li>
                <li role="presentation" id="codelogin"><a>手机验证码登录</a></li>
            </ul>
        </div>
        <br>
        <div id="div1">
            <div class="form-group">
                <label for="inputUsername" class="col-sm-2 control-label">username</label>
                <div class="col-sm-10">
                    <input type="text" class="form-control" id="inputUsername" placeholder="Username" name="username">
                </div>
            </div>

            <div class="form-group">
                <label for="inputPassword" class="col-sm-2 control-label">Password</label>
                <div class="col-sm-10">
                    <input type="password" class="form-control" id="inputPassword" placeholder="Password"
                           name="password">
                </div>
            </div>


            <div class="form-group">
                <div class="col-sm-offset-4 col-md-10">
                    <button type="submit" class="btn btn-default" id="sign">Sign in</button>
                </div>
                <p>{{ msg }}</p>
            </div>
        </div>

    </form>
    <form class="form-horizontal" id="container" method="post" action="{{ url_for('user.login') }}?f=2">
        <div id="div2">
            <div class="form-group">
                <label for="inputphone" class="col-sm-0 control-label"></label>
                <div class="col-sm-10">
                    <input type="text" class="form-control" id="inputphone" placeholder="phone" name="phone">
                    <span></span>
                </div>
            </div>
            <div class="form-group" style=" float: left ">
                <label for="inputcode" class="col-sm-0  control-label"></label>
                <div class="col-sm-10">
                    <input type="text" class="form-control" col-sm-offset-10 id="inputcode" placeholder="code"
                           name="code">
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-offset-0 col-md-2" style="float: left">
                    <button type="button" class="btn btn-default" id="sendcode" name="code">send code</button>
                        {#  此处type类型未button,点击不提交整个表单,只是触发jquery                  #}
                </div>
            </div>
            <div class="form-group" style="float: left">
                <div class="col-sm-offset-10 col-md-2">
                    <button type="submit" class="btn btn-default" id="codesign">Sign in</button>
                </div>
                <p>{{ msg }}</p>
            </div>
        </div>
    </form>
{% endblock %}

{% block scripts %}
    {{ super() }}
    <script>
        $(document).ready(function () {
            $('#div2').hide()
            $('#div1').show()
            $('#passwordlogin').addClass('active')
        })
        $('#passwordlogin').click(function () {
            $('#div2').hide()
            $('#div1').show()
            $('#passwordlogin').addClass('active')
            $('#codelogin').removeClass('active')
        })
        $('#codelogin').click(function () {
            $('#div1').hide()
            $('#div2').show()
            $('#codelogin').addClass('active')
            $('#passwordlogin').removeClass('active')
        })

        $('#inputphone').blur(function () {
            let phone = $(this).val();
            let span_ele = $(this).next('span');
            if (phone.length == 11) {
                span_ele.css({"color": "#ff0011", "font-size": "12px"})
                span_ele.text('')
                $.get('{{ url_for('user.login_check_phone') }}', {phone: phone}, function (data) {
                    {#  data为返回数据  #}
                    if (data.code != 200) {
                        span_ele.text('用户未注册')
                    }
                })
            } else {
                span_ele.css({"color": "#ff0011", "font-size": "12px"})
                {#注意是字典格式#}
                span_ele.text('手机号长度有误')
            }
        })

        $('#sendcode').click(function () {
            let span_ele2 = $(this).next('span');
            let phone = $(this).val();
            $.get('{{ url_for('user.send_code') }}', {phone: phone}, function (data) {
                if (data.code == 200) {
                    alert(data.msg);
                }
                else{
                    alert(data.msg);
                }
            })
        })

    </script>
{% endblock %}

templates/user/register.html

{% extends 'base.html' %}

{% block styles %}
    {{ super() }}
    {# 加了super就是继承原有的样式 #}
    <style>
        #container {
            width: 400px;
            margin: 20px auto;
        }
    </style>
{% endblock %}

{% block mycontent %}
    <form class="form-horizontal" id="container" method="post" action="{{ url_for('user.register') }}">
        <div class="form-group">
            <label for="inputUsername" class="col-sm-2 control-label">username</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="inputUsername" placeholder="Username" name="username">
            </div>
        </div>

        <div class="form-group">
            <label for="inputPassword" class="col-sm-2 control-label">Password</label>
            <div class="col-sm-10">
                <input type="password" class="form-control" id="inputPassword" placeholder="Password" name="password">
            </div>
        </div>

        <div class="form-group">
            <label for="inputRePassword" class="col-sm-2 control-label">Confirm Password</label>
            <div class="col-sm-10">
                <input type="password" class="form-control" id="inputRePassword" placeholder="Confirm Password"
                       name="repassword">
            </div>
        </div>

        <div class="form-group">
            <label for="inputPhone" class="col-sm-2 control-label">Phone</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="inputPhone" placeholder="Phone" name="phone">
                <span></span>
            </div>
        </div>

        <div class="form-group">
            <label for="inputEmail" class="col-sm-2 control-label">Email</label>
            <div class="col-sm-10">
                <input type="email" class="form-control" id="inputEmail" placeholder="email" name="email">
            </div>
        </div>

        <div class="form-group">
            <div class="col-sm-offset-4 col-md-10">
                <button type="submit" class="btn btn-default">Sign in</button>
            </div>
        </div>
    </form>
{% endblock %}

{% block scripts %}
    {{ super() }}
    <script>
        {#当输入域失去焦点 (blur) #}
        $('#inputPhone').blur(function () {
            let phone = $(this).val();
            let span_ele = $(this).next('span');
            if (phone.length == 11){
                span_ele.css({"color":"#ff0011", "font-size": "12px"})
                span_ele.text('')
                $.get('{{ url_for('user.check_phone') }}',{phone:phone},function (data){
                {#  data为返回数据  #}
                    if(data.code != 200){
                        span_ele.text('用户已注册')
                    }
                })
            }
            else{
                span_ele.css({"color":"#ff0011", "font-size": "12px"})
                {#注意是字典格式#}
                span_ele.text('手机号长度有误')
            }
        })
    </script>
{% endblock %}

/center

image-20210122142257723

权限验证

目的:只要走center路由,就判断用户是否登录,若登录则正常显示,没有登陆的话跳转到登录页面

Flask 钩子函数这里写图片描述
  • 直接应用在 app 上:

    before_first_request
    #在处理第一个请求前运行装饰的要运行的视图函数, 只会执行一次
    
    *before_request
    #在每次请求前运行
    
    after_request
    #如果没有未处理的异常抛出,在每次请求后运行,需要接收一个 Response 类的对象作为参数 并返回一个新的Response 对象 或者 直接返回接受到的Response 对象
    
    teardown_request
    #在每次请求后运行,即使有未处理的异常
    
  • 应用到蓝图:

    before_app_first_request
    
    *before_app_request
    
    after_app_request
    
    teardown_app_request
    

装饰器的形式存在

钩子函数全局有效,设置一次,其他路由直接添加到对应列表即可

flask.g

g 对象,本次请求的对象

尽在当次请求中有效

实例
require_login_list = ['/user/center']

#@user_bp.before_app_request
#def checkveir():
#    if request.path in require_login_list:
#        uid = session['uid']
#        if not uid:
#            return redirect(url_for('user.index'))
#        else:
#            user = User.query.get(uid)
#            g.user = user
# 这样写如果没有登陆,就取不到uid,无法继续执行

def checkveir():
    if request.path in require_login_list:
        try:
            id = session['uid']
            user = User.query.get(id)
            g.user = user
        except:
            return redirect(url_for('user.login'))

......
@user_bp.route('/center', endpoint='center')
def center():
    uid = session.get('uid')
    if uid:
        user = User.query.get(uid)
        return render_template('user/center.html', user=g.user)
    return render_template('user/center.html')

其他功能

文件上传1

上传界面html

<form action="{{ url_for('user.change') }}" method="post" enctype="multipart/form-data">
            {#   图片上传必须设置enctype          #}
    <input type="file" id="exampleInputFile" name="icon">
	<img src="{% if user.icon %} {{ url_for('static',filename=user.icon ) }}{% else %}{{ url_for('static',filename='img/1.jpg') }}{% endif %}" alt="" width="90" height="92">
                {# 图片使用固定的引入方式,也最好是加一层判断          #}
</form>    

print( icon ) icon是一个文件对象,使用 icon.filename 获取文件名

image-20210122214654827

view.py change

@user_bp.route('/change', methods=['GET', 'POST'], endpoint='change')
def change():
    if request.method == 'POST':
        # id = request.form.get('id')  无需这样写,用不着传id,因为session里边就有uid
        username = request.form.get('username')
        email = request.form.get('email')
        phone = request.form.get('phone')
        icon = request.files.get('icon')
        # 有文件就要使用 request.files.get()方式
        icon_filename = icon.filename
        # icon 是一个对象,icon.filename 取出他的名字
        suffix = icon_filename.rsplit('.')[-1]
        # 取出文件后缀名
        if suffix in ALLOWED_EXTENSIONS:
            icon_filename = secure_filename(icon_filename)
            # 封装的文件名转换函数,下划线替换空格等,使文件名符合 python 文件名规则
            file_path = os.path.join(Config.UPLOAD_ICON_DIR, icon_filename)
            # 文件路径拼接上文件名称
            icon.save(file_path)
            # 保存文件

        users = User.query.all()
        for user in users:
            print(user.phone)
            if phone == g.user.phone or phone != user.phone:
                user = g.user
                user.username = username
                user.phone = phone
                user.email = email
                #icon_path_and_name = '/upload/icon/'+icon_filename
                # 此处写相对路径即可,因为{{ url_for('static',filename=user.icon ) }}前段时static目录下找
                #user.icon = icon_path_and_name  可以加一个判断如果文件名没传过来就让path为空,或者是数据库只保存文件名前端这样去找 {{ url_for('static',filename='upload/icon/'+user.icon ) }}
                db.session.commit()
                return render_template('user/center.html', user=g.user)
            else:
                return render_template('user/center.html', msg='手机号重复', user=g.user)
                # 遍历判断手机号是否重复

        return render_template('user/center.html', user=g.user)
    return render_template('user/center.html', user=g.user)

settings.py

class Config:
......
    BASE_DIR = os.path.dirname(os.path.abspath(__name__))
    # 项目路径
    STATIC_DIR = os.path.join(BASE_DIR, 'static')
    # 静态文件的路径
    TEMPLATE_DIR = os.path.join(BASE_DIR, 'templates')
    UPLOAD_ICON_DIR = os.path.join(STATIC_DIR, 'upload/icon')
    # 头像上传目录
    UPLOAD_PHOTO_DIR = os.path.join(STATIC_DIR, 'upload/photo')
    # 相册的上传目录

image-20210123100457546

富文本编辑器

TingMCE web 富文本编辑器,可以嵌入到任意Web应用中使用

同类程序有:UEditor、Kindeditor、Simditor、CKEditor、wangEditor、Suneditor、froala等等

开发文档:http://tinymce.ax-z.cn/quick-start.php

  • 经典模式
  • 内联模式(inline) 页面的编辑视图与阅读视图合二为一
  • 沉浸无干扰模式(distraction-free)

使用步骤

第1步:引入 TingMCE 脚本

在中插入如下这行代码

<script src="{{ url_for('static', filename='tinymce/tinymce.min.js') }}"></script>

第2步:将TinyMCE初始化为页面的一部分

需要使用**tinymce.init()**来进行初始化。

tinymce.init()内的初始化对象包含众多参数,但都是可省略的,唯一必须的参数就是selector(允许通过css选择器指定TinyMCE要绑定的内容容器,传统模式需指定textarea,内联或沉浸模式可指定div或其它块元素,推荐div)

$(function () {
    tinymce.init({
        selector: '.mytextarea',
        //  使用CSS选择器语法来确定页面上哪个元素被TinyMCE替换成编辑器
        //  与页面关联,可以用类选择器,也可以用标签选择器,前后对应即可
        height:400,
        inline:false,
        //  selector的目标设为textarea是无法启用内联模式的,必须是普通的块元素,div最佳
        plugins:'advlist autolink link image lists preview', //字符串方式
        //  plugins : ['advlist','autolink','link'], //数组方式
        //  启用插件非常简单,只需将插件名作为参数,多个插件用空格分隔的字符串,也支持使用数组的方式
        toolbar:true,
        //  工具栏
        menubar:true,
        //  菜单栏
        //  ........
    });
})

base.html 引入tingmce

{% block head %}
    {#  注意是head,不是heads  #}
    <head>
        {{ super() }}
        <title>ocean</title>
        <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
        {#   注意先导入jqeury,在引入tingmce     #}
        <script type="text/javascript" src="{{ url_for('static', filename= 'js/center.js') }}"></script>
        <script type="text/javascript" src="{{ url_for('static', filename= 'tinymce/tinymce.min.js') }}"></script>
    </head>
{% endblock %}

center.html

<textarea  class="mytextarea" cols="30" rows="10">hello</textarea>

image-20210124101914495

tingmce 文章内容格式会转换成 html 标签格式存储到数据库,因为富文本有很多符号,表情不能以字符串方式存储,将content行改为二进制存储格式

image-20210125092610594

取出的过程也要转换,下图箭头表示为转换image-20210125095250019

需要自定义一个过滤器

#user/view.py
@user_bp.app_template_filter('cdecode')
def content_decode(content):
    content = content.decode('utf-8')
    return content[:200]
    # 只取前200个字,没有效果
    
    
#index.html
{% block mycontent %}
    {% for article in articles %}
        <div id="art_div">
            <h3>{{ article.title }}</h3>
            <P>{{ article.content | cdecode | safe |  truncate(200) }}</P>
        </div>
        <br>
    {% endfor %}
{% endblock %}

image-20210125095146738

Tingmce 会自动对内容里出现的 html 标签进行转义image-20210125095736164

image-20210125095544648

出现 样式 标签可能会有冲突

文章分页

数据分页查询

# 4 3 2 | 1  因为只有四篇
pagination = Article.query.order_by(-Article.pdatetime).paginate(page=2, per_page=3)
# 返回一个 pagination 对象,page是查询第几页,per_page每页几条
pagination.items
# 返回对象列表 [<Article 4>, <Article 3>, <Article 2>]
pagination.page
# 当前的页码数 1
pagination.prev_num
# 当前的前一个页码数 none
pagination.next_num
# 当前的后一个页码数 2
pagination.has_next
# 有没有下一页 True
pagination.has_prev
# 有没有上一页 False
pagination.pages
# 总页数 2

实例:

1

apps/user/view.py

@user_bp.route('/', endpoint='index')
def index():
    uid = session.get('uid',None)
    # None 为默认值
    # uid = session[uid]这样写如果uid取不到会报错Key error
    page = request.args.get('page', 1)
    # 默认值为 1
    pagination = Article.query.order_by(-Article.pdatetime).paginate(page=page, per_page=3)
    art_type = Art_type.query.all()
    # 按照文章发表时间倒序排序
    if uid:
        user = User.query.get(uid)
        return render_template('user/index.html', user=user, art_type=art_type, pagination=pagination)
    return render_template('user/index.html', pagination=pagination)

templates/user/index.html

{% block mycontent %}
    {#  文章部分  #}
    {% for article in pagination.items %}
        <div id="art_div">
            <h4>{{ article.title }}</h4>
            <p class="anchor_text">{{ article.users.username }}</p>
            {#  通过User relationship backref逆向查找  #}
            <P>{{ article.content | cdecode | safe }}</P>
            <div class="art_div_foot">
                <p style="width: 200px">发布时间:{{ article.pdatetime }}</p>
                <p>点击数:{{ article.click_num }}</p>
                <p>收藏数:{{ article.save_num }}</p>
                <p>点赞数:{{ article.lovw_num }}</p>
            </div>
        </div>
        <br>
    {% endfor %}

    {#  分页栏  #}
    <nav aria-label="Page navigation" class="col-md-4 col-md-offset-4">
        <ul class="pagination">
            {#  判断是否存在上一页,决定是否可以点击,设置disabled属性          #}
            <li {% if not pagination.has_prev %}
                class="disabled"
            {% endif %} >
                <a href="{{ url_for('user.index') }}?page={{ pagination.prev_num }}" aria-label="Previous">
                    <span aria-hidden="true">&laquo;</span>
                </a>
            </li>

            {#  判断页码于查询页码是否一致,一致重定向          #}
            {% for page_num in range(1,pagination.pages + 1) %}
                <li {% if pagination.page == page_num %} class="active" {% endif %}><a
                        href="{{ url_for('user.index') }}?page={{ page_num }}">{{ page_num }}</a></li>
            {% endfor %}


            <li {% if not pagination.has_next %}
                class="disabled"
                {% endif %} >
                <a href="{{ url_for('user.index') }}?page={{ pagination.next_num }}" aria-label="Next">
                    <span aria-hidden="true">&raquo;</span>
                </a>
            </li>
        </ul>
    </nav>
{% endblock %}

pycharm 折叠代码快捷键

  • 所有代码折叠:ctrl+alt±
  • 所有代码展开:ctrl+alt++
  • 折叠某一点:ctrl±
  • 展开某一层:ctrl++
点赞收藏

以点赞为例

article.py

@article_bp1.route('/love', endpoint='article_love')
def art_love():
    art_id = request.args.get('art_id')
    tag = request.args.get('tag')
    article = Article.query.get(art_id)
    if tag == '1':
        article.love_num -= 1
    else:
        article.love_num += 1
    db.session.commit()
    return jsonify(num=article.love_num)
{% block newcontent %}
    <div class="container">
        <h2>{{ article.title }}</h2>
        <div class="art_div_foot">
            <span id="right_part">发布时间:{{ article.pdatetime }}</span>
            <span id="left_part">
                <span class="glyphicon glyphicon-log-in" aria-hidden="true"></span>
                    &nbsp;<span tag="0">{{ article.click_num }}</span>&nbsp;&nbsp;&nbsp;&nbsp;
                <span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span>
                    &nbsp;<span tag="0">{{ article.love_num }}</span>&nbsp;&nbsp;&nbsp;&nbsp;
                <span class="glyphicon glyphicon-star-empty" aria-hidden="true"></span>
                    &nbsp;<span tag="0">{{ article.save_num }}</span>
            </span>
        </div>
        <br>
        <p>{{ article.content | cdecode| safe }}</p>
    </div>
{% endblock %}

# ajax
{% block scripts %}
    {{ super() }}
    <script>
        $(function () {
            $('.glyphicon-thumbs-up').click(function () {
                let $this = $(this);
                let tag = $this.next('span').attr('tag');
                {#tag=0 表示为点赞,tag=1表示已点赞#}
                $.get("{{ url_for('article.article_love') }}", {art_id:{{ article.id }}, tag: tag}, function (data) {
                    $this.next('span').text(data.num);
                })
                {#点赞#}
                if (tag == 1) {
                    $this.css({'color':'black'});
                    $this.next('span').attr('tag', "0");
                } else if (tag == 0) {
                    $this.css({"color":"red"});
                    $this.next('span').attr('tag', "1");
                }
            })
        })
    </script>
{% endblock %}

有一个漏洞就是没有用户记录状态,刷新后就可以再次点赞

应该再建一张表格保存,用户点赞过的文章id

收藏原理相同

云存储

七牛云控制台:https://portal.qiniu.com/kodo/overview

开发文档:https://developer.qiniu.com/kodo

py软件开发工具包:https://developer.qiniu.com/kodo/1242/python

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple qiniu

图片上传

utils/utils.py

# -*- coding: utf-8 -*-
# flake8: noqa
import random
from qiniu import Auth, put_file, etag, put_data
import qiniu.config

def upload_qiniu(filestorage):
    # 需要填写你的 Access Key 和 Secret Key
    access_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
    secret_key = 'xxxxxxxxxxxxxxxxxxx'
    # 构建鉴权对象
    q = Auth(access_key, secret_key)
    # 要上传的空间
    bucket_name = 'oceanpic'
    # 上传后保存的文件名
    filename = filestorage.filename
    ran = random.randint(1, 1000)
    suffix = filename.rsplit('.')[-1]
    key = filename.rsplit('.')[0] + '_' + str(ran) + '.' + suffix
    # 生成上传 Token,可以指定过期时间等
    token = q.upload_token(bucket_name, key, 3600)
    # 要上传文件的本地路径,put_file指定路径上传
    # localfile = './sync/bbb.jpg'
    # ret, info = put_file(token, key, localfile)

    # 上传二进制流 put_data 是七牛库里封装的函数
    ret, info = put_data(token, key, filestorage.read())
    # ret 和 info 是对象,属性是 json 字符串类型数据
    # filestorge.read() 二进制方式读取
    return ret, info

ret,info

ret
{'hash': 'Fq5QJYBt6mGL54sEJxHa26j5BqeQ', 'key': 'u=433095846,3800291930&fm=111&gp=0_521.jpg'}

info
_ResponseInfo__response:<Response [200]>, exception:None, status_code:200, text_body:{"hash":"Fq5QJYBt6mGL54sEJxHa26j5BqeQ","key":"u=433095846,3800291930\u0026fm=111\u0026gp=0_521.jpg"
}, req_id:wYkAAACFs_aNpl0W, x_log:X-Log

user/view.py

@user_bp.route('/upload_photo', methods=['GET', 'POST'])
def upload_photo():
    photo = request.files.get('photo')
    # FileStorage 对象,photo.filename获取文件名,photo.save(path)可以保存文件
    if photo:
        ret, info = upload_qiniu(photo)
        if info.status_code == 200:
            # 将文件名保存到数据库
            photo = Photo()
            photo.photo_name = ret['key']
            # 取出文件名
            photo.user_id = g.user.id
            db.session.add(photo)
            db.session.commit()
            # 添加数据
            return redirect(url_for('user.center'))
        return render_template('user/center.html', user=g.user, msg='文件上传失败')
    return redirect(url_for('user.center'))

别忘了把 /user/upload_photo 加到钩子函数列表中

图片显示

七牛私有空间没法分享外链

@user_bp.route('/photograph', endpoint='photograph')
def show_photo():
    page = int(request.args.get('page', 1))
    photos = Photo.query.paginate(page=page, per_page=8)
    return render_template('user/photo.html', photos=photos, user=g.user)

html

{% for photo in photos.items %}
    <div id="show_photo_large_container">
        <img class="show_photo_large" src="http://qn3yof5yl.hb-bkt.clouddn.com/{{ photo.photo_name }}"
             alt="img is lost">
    </div>
{% endfor %}

图片删除

utils/utils.py

def del_qiniu(photo_name):
    access_key = 'l3JQ8x0z4Ly5L_65QEL2VKQCz2gH0bi0GD-A6Qoa'
    secret_key = 'RB1Wouc_TO6ais15icDd7YHs-PXnhtEQWovMJBGB'
    # 初始化Auth状态
    q = Auth(access_key, secret_key)
    # 初始化BucketManager
    bucket = BucketManager(q)
    # 你要测试的空间, 并且这个key在你空间中存在
    bucket_name = 'oceanpic'
    key = photo_name
    # 删除bucket_name 中的文件 key
    ret, info = bucket.delete(bucket_name, key)
    return ret, info

view.py

@user_bp.route('/del_photo', endpoint='del_photo')
def del_photo():
    photo_id = request.args.get('pid')
    photo = Photo.query.get(photo_id)
    photo_name = photo.photo_name
    user_id = g.user.id
    del_user_id = photo.user_id
    if user_id == del_user_id:
        ret, info = del_qiniu(photo_name)
        if info.status_code == 200:
            db.session.delete(photo)
            db.session.commit()
            return redirect(url_for('user.center'))
    else:
        return render_template('404.html', msg='危险越权操作!已记录您的IP')
    return render_template('404.html')
防止越权
@article_bp1.route('/delete', endpoint='delete')
def del_article():
    aid = request.args.get('aid')
    user_id = g.user.id
    # 判断删除文章是不是当前用户的,防止越权操作,这种只能删除当前图片
    referer = request.headers['referer']
    article_user = Article.query.get(aid)
    del_user_id = article_user.user_id
    if user_id == del_user_id:
        db.session.delete(article_user)
        db.session.commit()
        return redirect(url_for('user.center'))
    else:
        return render_template('404.html', msg='危险越权操作!已记录您的IP', referer=referer)

#     aid = request.args.get('aid')
#     user_id = g.user.id
#     # 判断删除文章是不是当前用户的,防止越权操作,能删除当前用户的图片
#     user_art_list = Article.query.filter(Article.user_id == user_id).all()
#     referer = request.headers['referer']
#     for article in user_art_list:
#         if int(aid) == int(article.id):
#             flag = True
#             break
#         else:
#             flag = False
#     if flag:
#         article = Article.query.get(aid)
#         db.session.delete(article)
#         db.session.commit()
#         return redirect(url_for('user.center'))
#     else:
#         return render_template('404.html', msg='危险越权操作!已记录您的IP', referer=referer)
一对一关系

Flask 没有预设一对一关系,一对多默认返回列表image-20210127154716674

只能是从表的外键使用 unique 进行约束

image-20210127154912320

主表查询时使用 [0] 进行截取第一个对象

image-20210127154937471

但是这样操作可能会报错,需要在查询时加异常处理

try:

except:

et_key)
# 初始化BucketManager
bucket = BucketManager(q)
# 你要测试的空间, 并且这个key在你空间中存在
bucket_name = ‘oceanpic’
key = photo_name
# 删除bucket_name 中的文件 key
ret, info = bucket.delete(bucket_name, key)
return ret, info


view.py

```python
@user_bp.route('/del_photo', endpoint='del_photo')
def del_photo():
    photo_id = request.args.get('pid')
    photo = Photo.query.get(photo_id)
    photo_name = photo.photo_name
    user_id = g.user.id
    del_user_id = photo.user_id
    if user_id == del_user_id:
        ret, info = del_qiniu(photo_name)
        if info.status_code == 200:
            db.session.delete(photo)
            db.session.commit()
            return redirect(url_for('user.center'))
    else:
        return render_template('404.html', msg='危险越权操作!已记录您的IP')
    return render_template('404.html')
防止越权
@article_bp1.route('/delete', endpoint='delete')
def del_article():
    aid = request.args.get('aid')
    user_id = g.user.id
    # 判断删除文章是不是当前用户的,防止越权操作,这种只能删除当前图片
    referer = request.headers['referer']
    article_user = Article.query.get(aid)
    del_user_id = article_user.user_id
    if user_id == del_user_id:
        db.session.delete(article_user)
        db.session.commit()
        return redirect(url_for('user.center'))
    else:
        return render_template('404.html', msg='危险越权操作!已记录您的IP', referer=referer)

#     aid = request.args.get('aid')
#     user_id = g.user.id
#     # 判断删除文章是不是当前用户的,防止越权操作,能删除当前用户的图片
#     user_art_list = Article.query.filter(Article.user_id == user_id).all()
#     referer = request.headers['referer']
#     for article in user_art_list:
#         if int(aid) == int(article.id):
#             flag = True
#             break
#         else:
#             flag = False
#     if flag:
#         article = Article.query.get(aid)
#         db.session.delete(article)
#         db.session.commit()
#         return redirect(url_for('user.center'))
#     else:
#         return render_template('404.html', msg='危险越权操作!已记录您的IP', referer=referer)

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OceanSec

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值