Flask笔记(个人博客)

3月11日

大型Flask项目目录结构

project/                                   # 项目根目录

         app/                                 # 存放整个的应用程序

                  static/                                # 静态资源

                          js/                              # JS脚本

                          css/                           # 层叠样式表

                          img/                           # 图片资源

                          favicon.ico                # 网站图标

                  templates/                         # 模板文件

                          common/                   # 通用模板文件

                          main/                         # 主蓝本模板文件

                          errors/                        # 错误模板文件

                          email/                        # 邮件模板文件

                          user/                         # 用户蓝本模板文件

                          posts/                       # 博客篮板模板文件

                  forms/                               # 存放所有表单

                  models/                            # 存放所有模型

                  views/                               # 存放视图函数

                  email.py                      # 邮件发送

                  extensions.py             # 所有的扩展

                  config.py                    # 配置文件

         migrations/                          # 数据库迁移脚本目录

         venv/                                      # 虚拟环境目录

         tests/                                   # 测试单元

         requirements.txt            # 所有的依赖包

         manage.py                            # 项目启动控制文件

如何快速复制一个虚拟环境:

1. 将当前环境依赖冷冻起来

   pip freeze > requirements.txt

2. 创建虚拟环境

   virtualenv venv

3. 安装冷冻的依赖包

   pip install -r requirements.txt

代码书写步骤

1. 书写配置即使用配置文件

   config.py、__ init __.py、manage.py

2. 配置相关扩展

   extensions.py、__ init __.py

3. 配置相关蓝本

   views目录、__ init __.py

4. 定制项目基础模板

   common/base.html

5. 自定义错误页面

   templates/errors/404.html、__ init __.py

6. 异步发送邮件

   email.py、templates/email/

7. 用户注册登录相关知识点

   密码要加密存储与校验:

   from werkzeug.security importgenerate_password_hash,check_password_hash

   @main.route('/jiami/')

   def jiami():

       return generate_password_hash('123456')

   @main.route('/check/<password>')

   def check(password):

       # 密码校验函数:加密后的值  密码

       # 正确:True,错误:False

       if check_password_hash('pbkdf2:sha256:50000$8tHnM54f$c1518c6e491e0a7c5ebd90beb8b56c1d3b03cef66ad940c566578e6a5cfd62ea', password):

           return '密码正确'

       else:

           return '密码错误'

   ```

   用户账户激活(token)

   ```python

   from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

   @main.route('/generate_token/')

   def generate_token():

       s = Serializer(current_app.config['SECRET_KEY'], expires_in=3600)

       # 加密指定的数据,已字典的形式传入

       return s.dumps({'id': 250})

   @main.route('/activate/<token>')

   def activate(token):

       s = Serializer(current_app.config['SECRET_KEY'])

       try:

           data = s.loads(token)

       except:

           return 'token有误'

       return str(data.get('id'))

3月12日

用户管理

# 用户管理

### 用户注册与激活

1. 在项目基础模板(common/base.html)中添加注册点击的链接

   ```html

   <li><a href="{{url_for('user.register')}}">注册</a></li>

   ```

   > 注意:由于所有的视图函数都是在蓝本中写的,因此在使用时要指定蓝本;否则可能会出问题,因为默认是当前蓝本的视图函数。

2. 在user蓝本中添加视图函数,如下:

   ```python

   @user.route('/register/')

   def register():

       return render_template('user/register.html')

   ```

3. 书写模板文件(templates/user/register.html),如下:

   ```html

   {% extends 'common/base.html' %}

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

   {% block page_content %}

   用户注册

   {% endblock %}

   ```

4. 创建用户注册表单类

   ```python

   # 导入表单基类

   from flask_wtf import FlaskForm

   # 导入相关字段

   from wtforms import StringField, PasswordField, SubmitField

   # 导入验证器类

   from wtforms.validators import DataRequired, EqualTo, Email, Length

   class RegisterForm(FlaskForm):

       username = StringField('用户名',

          validators=[DataRequired(),

          Length(6, 18, message='用户名必须在6~18个字符之间')])

       password = PasswordField('密码',

          validators=[DataRequired(),

          Length(6, 18, message='密码长度必须在6~18个字符之间')])

       confirm = PasswordField('确认密码',

          validators=[EqualTo('password',

                              message='两次密码不一致')])

       email = StringField('邮箱',

           validators=[Email(message='邮箱格式不正确')])

       submit = SubmitField('立即注册')

      

       # 自定义验证器,验证用户名

       def validate_username(self, field):

           if User.query.filter_by(username=field.data).first():

               raise ValidationError('该用户名已存在,请选用其它用户名')

       # 自定义验证器,验证邮箱

       def validate_email(self, field):

           if User.query.filter_by(email=field.data).first():

               raise ValidationError('该邮箱已使用,请选用其它邮箱')

   ```

5. 创建表单对象并渲染,如下:

   ```python

   @user.route('/register/')

   def register():

       form = RegisterForm()

       return render_template('user/register.html', form=form)

   ```

   模板中渲染表单

   ```html

   {% extends 'common/base.html' %}

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

   {% block page_content %}

       {{ wtf.quick_form(form) }}

   {% endblock %}

   ```

6. 用户的注册校验逻辑

7. 用户注册的邮件激活

   ```python

   @user.route('/register/', methods=['GET', 'POST'])

   def register():

       form = RegisterForm()

       if form.validate_on_submit():

           # 创建对象,写入数据库

           # 发送激活邮件

           s = Serializer(current_app.config['SECRET_KEY'],

                          expires_in=3600)

           token = s.dumps({'id': 250})

           # '[email protected]'

           send_mail(form.email.data, '账户激活',

                     'email/account_activate', token=token,

                     username=form.username.data)

           flash('激活邮件已发送,请点击链接完成用户激活')

           return redirect(url_for('main.index'))

       return render_template('user/register.html', form=form)

   @user.route('/activate/<token>')

   def activate(token):

       s = Serializer(current_app.config['SECRET_KEY'])

       try:

           data = s.loads(token)

       except:

           return 'token有误'

       return '%d号账户已经激活' % data.get('id')

   ```

8. 用户模型的设计,如下:

   ```python

   from flask import current_app

   from app.extensions import db

   # 生成token使用

   from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

   # 密码散列及校验

   from werkzeug.security import generate_password_hash, check_password_hash

   class User(db.Model):

       # 指定表名

       __tablename__ = 'users'

       id = db.Column(db.Integer, primary_key=True)

       username = db.Column(db.String(32), unique=True)

       password_hash = db.Column(db.String(128))

       email = db.Column(db.String(64), unique=True)

       confirmed = db.Column(db.Boolean, default=False)

       # 保护字段

       @property

       def password(self):

           raise AttributeError('密码是不可读属性')

       # 设置密码,加密存储

       @password.setter

       def password(self, password):

           self.password_hash = generate_password_hash(password)

       # 密码校验

       def verify_password(self, password):

           return check_password_hash(self.password_hash, password)

       # 生成用户激活的token

       def generate_activate_token(self, expires_in=3600):

           s = Serializer(current_app.config['SECRET_KEY'],

                          expires_in=expires_in)

           return s.dumps({'id': self.id})

       # 激活账户时的token校验,校验时还不知道用户信息,需要静态方法

       @staticmethod

       def check_activate_token(token):

           s = Serializer(current_app.config['SECRET_KEY'])

           try:

               data = s.loads(token)

           except:

               return False

           user = User.query.get(data.get('id'))

           if user is None:

               # 不存在此用户

               return False

           if not user.confirmed:

               # 账户没有激活时才激活

               user.confirmed = True

               db.session.add(user)

           return True

   ```

用户登录认证与退出

说明:用户登录认证及退出的逻辑可以自己实现,但是比较繁琐,推荐使用flask-login

安装:`pip install flask-login`

使用:

```python

from flask_login import LoginManager

login_manager = LoginManager()

def config_extensions(app):

      ...

    login_manager.init_app(app)

    # 会话保护级别:

    #  None不使用

    # 'basic'基本级别,默认级别

    # 'strong'用户信息更改立即退出

    login_manager.session_protection = 'strong'

    # 设置登录页面端点,当用户访问需要登录才能访问的页面,

    # 此时还没有登录,会自动跳转到此处

    login_manager.login_view = 'user.login'

    # 设置提示信息,默认是英文提示信息

    login_manager.login_message = '需要登录才可访问'

    # 在用户的Model中添加一个回调函数

    @login_manager.user_loader

    def loader_user(user_id):

        return User.query.get(int(user_id))

```

1. 基础模板中添加点击的链接,如下

   ```html

   <li><a href="{{url_for('user.login')}}">登录</a></li>

   ```

2. 在user蓝本中添加视图函数,使用flask-login认证,如下

   ```python

   @user.route('/login/', methods=['GET', 'POST'])

   def login():

       form = LoginForm()

       if form.validate_on_submit():

           u = User.query.filter_by(username=form.username.data).first()

           if u is None:

               flash('无效的用户名')

           elif u.verify_password(form.password.data):

               # 验证通过,用户登录,顺便可以完成'记住我'的功能

               login_user(u, remember=form.remember_me.data)

               # 如果有下一跳转地址就跳转到指定地址,没有跳转到首页

               return redirect(request.args.get('next') or

                               url_for('main.index'))

           else:

               flash('无效的密码')

       return render_template('user/login.html', form=form)

   ```

3. 添加一个login.html的模板文件,如下:

   ```

   {% extends 'common/base.html' %}

   {% block title %}用户登录{% endblock %}

   {% block page_content %}

   登录页面展示

   {% endblock %}

   ```

4. 使用flask-login退出登录

   ```python

   @user.route('/logout/')

   def logout():

       logout_user()

       flash('您已退出登录')

       return redirect(url_for('main.index'))

   ```

总结:扩展库提供了很多实用的功能

​         login_user:可以完成用户的登录,顺便还可以完成'记住我'的功能

​         logout_user:退出登录

​         current_user:在任意的地方都可使用,表示当前登录的用户,未登录是一个匿名用户

​                  is_authenticated:是否已登录

​                  is_anonymous:是否是匿名用户

​            login_required:保护指定的路由,针对需要登录才可访问的路由

3月13日

用户管理(修改头像)

1. 配置

   ```python

   # 最大上传文件大小

   MAX_CONTENT_LENGTH = 16 * 1024 * 1024

   # 上传文件存储位置

   UPLOADED_PHOTOS_DEST = os.path.join(base_dir, 'static/upload')

   ```

2. 添加flask-uploads扩展

   ```python

   # 导入类库及函数

   from flask_uploads import UploadSet, IMAGES

   from flask_uploads import configure_uploads, patch_request_class

   # 创建对象

   photos = UploadSet('photos', IMAGES)

   # 初始化

   configure_uploads(app, photos)

   patch_request_class(app, size=None)

   ```

3. 修改头像中使用上传文件

   1. 在基础模板中添加点击链接

      ```html

      <li><a href="{{url_for('user.change_icon')}}">修改头像</a></li>

      ```

   2. 添加视图函数

      ```python

      @user.route('/change_icon/')

      @login_required

      def change_icon():

          form = IconForm()

          return render_template('user/change_icon.html', form=form)

      ```

   3. 创建模板文件

      ```html

      {% extends 'common/base.html' %}

      {% block title %}修改头像{% endblock %}

      {% block page_content %}

         {{ wtf.quick_form(form) }}

      {% endblock %}

      ```

   4. 设计上传文件的表单

      ```python

      # 导入上传文件的字段及验证器

      from flask_wtf.file import FileField, FileRequired, FileAllowed

      from app.extensions import photos

      # 修改头像表单

      class IconForm(FlaskForm):

          icon = FileField('头像', validators=[FileRequired('请选择上传文件'), FileAllowed(photos, '只能上传图片')])

          submit = SubmitField('上传')

      ```

   5. 修改user数据模型

      ```python

      class User(UserMixin, db.Model):

                ...

          # 添加头像字段

          icon = db.Column(db.String(64), default='default.jpg')

      ```

      > 记得迁移数据库,顺便将默认值也修改了

   6. 修改模型后在信息展示和上传头像中测试
   7. 完整的上传头像并生成缩略图

      ```python

      import os

      from PIL import Image

      @user.route('/change_icon/', methods=['GET', 'POST'])

      @login_required

      def change_icon():

          form = IconForm()

          if form.validate_on_submit():

              # 生成随机的文件名

              suffix = os.path.splitext(form.icon.data.filename)[1]

              name = rand_str() + suffix

              # 保存上传头像

              photos.save(form.icon.data, name=name)

              # 生成缩略图

              pathname = os.path.join(os.path.join(current_app.config['UPLOADED_PHOTOS_DEST'], name))

              img = Image.open(pathname)

              img.thumbnail((64, 64))

              img.save(pathname)

              # 删除原有头像

              if current_user.icon != 'default.jpg':

                  # 第一次更换头像不删除,除此之外原来的头像都要删除

                  os.remove(os.path.join(current_app.config['UPLOADED_PHOTOS_DEST'], current_user.icon))

              # 更新新的头像名至数据库

              current_user.icon = name

              db.session.add(current_user)

              flash('头像已更换')

          return render_template('user/change_icon.html', form=form)

      # 生成随机的字符串

      def rand_str(length=32):

          import random

          base_str = 'abcdefghijklmnopqrstuvwxyz1234567890'

          return ''.join(random.choice(base_str) for i in range(length))

      ```

博客管理

1. 设计博客的模型

   ```python

   from app.extensions import db

   from datetime import datetime

   class Posts(db.Model):

       __tablename__ = 'posts'

       id = db.Column(db.Integer, primary_key=True)

       rid = db.Column(db.Integer, index=True, default=0)

       content = db.Column(db.Text)

       timestamp = db.Column(db.DateTime, default=datetime.utcnow)

       # 指定外键(表名.字段)

       uid = db.Column(db.Integer, db.ForeignKey('users.id'))

   ```

   为了关联查询,需要修改User模型,如下:

   ```python

   class User(UserMixin, db.Model):

     ...

     # 添加关联模型,相当于在关联的模型中动态的添加了一个字段

       # 参数说明:

       # 第一个参数:唯一一个必须的参数,关联的模型类名

       # backref:反向引用的字段名

       # lazy:指定加载关联数据的方式,dynamic:不加载记录,但是提供关联查询

       posts = db.relationship('Posts', backref='user', lazy='dynamic')

   ```

   > 数据模型添加或修改后,即可进行数据库迁移操作

2. 准备发表博客的表单

   ```python

   from flask_wtf import FlaskForm

   from wtforms import TextAreaField, SubmitField

   from wtforms.validators import DataRequired, Length

   class PostsForm(FlaskForm):

       # 如果想要设置字段的其它属性,可以通过render_kw完成

       content = TextAreaField('', render_kw={'placeholder': '这一刻的想法...'}, validators=[DataRequired(), Length(1, 128, message='说话要注意影响,不多不少最好')])

       submit = SubmitField('发表')

   ```

3. 添加视图函数

   ```python

   @main.route('/')

   def index():

       form = PostsForm()

       return render_template('main/index.html', form=form)

   ```

4. 渲染表单

   ```html

   {% extends 'common/base.html' %}

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

   {% block page_content %}

       {{ wtf.quick_form(form) }}

   {% endblock %}

   ```

5. 发表博客

   ```python

   @main.route('/', methods=['GET', 'POST'])

   def index():

       form = PostsForm()

       if form.validate_on_submit():

           # 判断是否登录

           if current_user.is_authenticated:

               u = current_user._get_current_object()

               # 根据表单提交的数据常见对象

               p = Posts(content=form.content.data, user=u)

               # 然后写入数据库

               db.session.add(p)

               return redirect(url_for('main.index'))

           else:

               flash('登录后才能发表博客')

               return redirect(url_for('user.login'))

       return render_template('main/index.html', form=form)

   ```

6. 展示博客

   ```

   @main.route('/', methods=['GET', 'POST'])

   def index():

       form = PostsForm()

       ...

       # 从数据库中读取博客,并分配到模板中,然后在模板中渲染

       # 安装发表时间,降序排列

       # 只获取发表的帖子,过滤回复的帖子

       posts = Posts.query.filter_by(rid=0).order_by(Posts.timestamp.desc()).all()

       return render_template('main/index.html', form=form, posts=posts)

   ```

   模板渲染

   ```html

   {# 展示博客内容 #}

   {% for p in posts %}

       <hr style="margin-top: 10px; margin-bottom: 10px;" />

       <div class="media">

           <div class="media-left">

               <a href="#">

                   <img class="media-object"

                        src="{{url_for('static',

                             filename='upload/'+p.user.icon)}}"

                        style="width: 64px; height: 64px;" alt="icon">

               </a>

           </div>

           <div class="media-body">

               <div style="float: right;">{{moment(p.timestamp).fromNow()}}</div>

               <h4 class="media-heading">{{p.user.username}}</h4>

               {{p.content}}

           </div>

       </div>

   {% endfor %}

   <hr />

   ```

7. 分页展示

   插叙数据时使用专门的分页函数:paginate,参数如下:

   ​  page:是唯一的必须参数,表示当前页数

   ​  per_page:每页显示的记录数,默认为20条

   ​  error_out:页码超出范围时是否显示404错误,默认为True

   函数的返回值是一个对象(Pagination),介绍如下:

   ​  属性:

   ​           items:当前页面的所有记录

   ​           page:当前的页码

   ​           pages:总页数

   ​           total:总记录数

   ​           prev_num:上一页的页码

   ​           next_num:下一页的页码

   ​           has_prev:是否有上一页,有返回True

   ​           has_next:是否有下一页,有返回True

   ​  方法:

   ​           iter_pages:是一个迭代器,每次返回一个在分页导航条上显示的页码

   ​           prev:上一页的分页对象

   ​           next:下一页的分页对象

8. 封装一个宏,专门负责分页显示

      ```html

      {% macro pagination_show(pagination, endpoint) %}

      <nav aria-label="Page navigation">

          <ul class="pagination">

              {# 上一页 #}

              <li {%if not pagination.has_prev %}class="disabled"{% endif %}>

                  <a href="{% if pagination.has_prev %}{{url_for(endpoint, page=pagination.prev_num, **kwargs)}}{% else %}#{% endif %}" aria-label="Previous">

                      <span aria-hidden="true">&laquo;</span>

                  </a>

              </li>

              {# 分页页码 #}

              {% for p in pagination.iter_pages() %}

                  {% if p %}

                      <li {% if pagination.page == p %}class="active"{% endif %}><a href="{{url_for(endpoint, page=p, **kwargs)}}">{{p}}</a></li>

                  {% else %}

                      <li><a href="#">&hellip;</a></li>

                  {% endif %}

              {% endfor %}

              {# 下一页 #}

              <li {% if not pagination.has_next %}class="disabled"{% endif %}>

                  <a href="{% if pagination.has_next %}{{url_for(endpoint, page=pagination.next_num, **kwargs)}}{% else %}#{% endif %}" aria-label="Next">

                      <span aria-hidden="true">&raquo;</span>

                  </a>

              </li>

          </ul>

      </nav>

      {% endmacro %}

      ```

9. 在视图函数中获取分页对象

      ```python

      @main.route('/', methods=['GET', 'POST'])

      def index():

          form = PostsForm()

          ...

          # 分页处理

          # 获取当前页码,没有认为是第一页

          page = request.args.get('page', 1, type=int)

          pagination = Posts.query.filter_by(rid=0)

                               .order_by(Posts.timestamp.desc())

                                      .paginate(page, per_page=3, error_out=False)

          posts = pagination.items

          return render_template('main/index.html', form=form,

                                 posts=posts, pagination=pagination)

      ```

10. 在模板中渲染分页导航条

       ```html

       {# 导入分页展示的宏 #}

       {% from 'common/macro.html' import pagination_show %}

       {# 展示分页导航条 #}

       {{ pagination_show(pagination, 'main.index') }}

       ```

 (学习自B站zhu_xuan0612老师,在此表示感谢!)

猜你喜欢

转载自www.cnblogs.com/yyxay/p/12504053.html
今日推荐