Writing in the front

Yesterday I made a random poetry generator using Python+ lots and lots of poetry data. I worked on it for a day, but at the end of the day, there was no rhyme in the poem written by the program. I just gave up and wrote the forum again.

In zhuanlan.zhihu.com/p/113539585 complete directory

Please note that

This essay is a bit long (32,150 words) and may take some time. Read it in sections. (I heard that Zhihu has a word limit of 20,000 characters, but I didn’t find it.)

Get into the business

In zhuanlan.zhihu.com/p/113477674, we have created the database, now is the time to let the user register, login. First, open app/auth/init.py and change one code:

# app/auth/__init__.py

from flask import Blueprint

auth = Blueprint('auth', __name__, url_prefix='/auth')  Set the URL prefix

from . import views
Copy the code

Here I changed the part where I created Blueprint to add a URL prefix so that all view urls in auth blueprints start with /auth, so I don’t have to type it anymore.

Now, write the view function:

# app/auth/views.py

from . import auth
from app.models import User
from flask import render_template


@auth.route('/register/')
def register() :
    return render_template('auth/register.html')

Copy the code

Pycharm was not shown yesterday that I had no template files in the main view, but I restarted Pycharm today and it was shown that I had created the template.

All right, back to business. In this view, we just render the template, not the database, but we’ll do it later. I used flask-wtf (I struggled with this for a long time because WTF rendered forms were too restrictive, but I used them anyway because I was lazy).

Install it first:

pip install flask-wtf
Copy the code

Initialization:

No initialization, out of the box

Create a new forms.py in the auth directory to store all forms:

# app/auth/forms.py

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Regexp, Email, Length


class RegistrationForm(FlaskForm) :  # RegistrationForm inherits from FlaskForm
    # StringField is used to get character data, validators are validators, and Regexp is filled with regular expressions
    username = StringField('Username', validators=[DataRequired(message='Please enter a user name'), Regexp('^[A-Za-z][A-Za-z0-9_.]*$'.0.'Username can only contain letters, numbers, dots.'
                                                                                        'or underline'), Length(1.64)])
    The Email validator is used to verify that input is in mailbox format, but does not ensure that a mailbox exists
    email = StringField('email', validators=[DataRequired(message='Please enter email address'), Email(message='Please enter your real email address')])
    password = PasswordField('password', validators=[DataRequired(message='Please enter your password')])
    submit = SubmitField('registered')

Copy the code

Now, change the Register view:

# app/auth/views.py

from . import auth
from app.models import User
from flask import render_template
from .forms import RegistrationForm  # import form


@auth.route('/register/')
def register() :
    form = RegistrationForm()  # instantiate the form
    return render_template('auth/register.html', form=form)

Copy the code

Now that you’ve finished writing your view, go ahead and write your HTML template. In templates, create a new auth/register.html:

<! --app/templates/register.html-->{% extends 'base.html' %} {% from 'bootstrap/form.html' import render_form %} {% block title %} register - AttributeError{% endblock %} {% block content %}<div class="container">
        {{ render_form(form) }}
    </div>
{% endblock %}
Copy the code

Here we use the macro rendering template that comes with Bootstrap-flask. Let’s run it:

RuntimeError! This is because I didn’t set the SECRET_KEY. Add two lines to config.py:

# app/config.py

import os


class DevelopmentConfig:
    #...
    # Do not track changes
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SECRET_KEY = 'this is my secret key!! '  # set key


class ProductionConfig:
    #...
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'secret key'  The production environment first uses the keys in the environment variables


#...
Copy the code

Now running the program, it should be normal:

However, the submit button on the form is not what we want it to be. According to plan, it should be yellow (primary), but it is grey! After Bing for a long time, I found that in the official document:

That is, when button_map is not specified, it defaults to using the button of the default class! Let’s change to primary and see:

<! --app/templates/register.html-->{% extends 'base.html' %} {% from 'bootstrap/form.html' import render_form %} {% block title %} register - AttributeError{% endblock %} {% block content %}<div class="container">
        <h1>registered</h1>
        <hr>Render_form (form, button_map={'submit': 'primary'})}} {</div>
{% endblock %}
Copy the code

However, Flask returns the Method not allowed error when we submit the form! Look at the flask-Bootstrap documentation again and find:

But no! Render_form uses post by default, so what happens? The post mode is not specified in my view.

# app/auth/views.py

from . import auth
from app.models import User
from flask import render_template
from .forms import RegistrationForm  # import form


@auth.route('/register/', methods=['GET'.'POST'])
def register() :
    form = RegistrationForm()  # instantiate the form
    return render_template('auth/register.html', form=form)
Copy the code

Refresh the page again, bug fixed. However, you may have noticed that the DataRequired validator in the form returns the same error information as the browser default when submitting empty information. I searched for a long time but could not find a solution, so I had no choice but to put up an issue on GitHub. There is no reply yet, I will update this post as soon as possible. Let’s ignore the glitch for a moment.

Now, let’s print the information from the form again in the console:

# app/auth/views.py

from . import auth
from app.models import User
from flask import render_template
from .forms import RegistrationForm  # import form


@auth.route('/register/', methods=['GET'.'POST'])
def register() :
    form = RegistrationForm()  # instantiate the form
    if form.validate_on_submit():  Execute when the form is submitted
        print(form.username.data, form.email.data, form.password.data)  Get form information
    return render_template('auth/register.html', form=form)
Copy the code

After resubmitting, something similar to the following should appear in the console:

Sam [email protected] 123
Copy the code

Username, email address, and password. Here, it’s very easy to type in your password only once when registering, so we should add another area to verify your password:

# app/auth/forms.py
#...
class RegistrationForm(FlaskForm) :  # RegistrationForm inherits from FlaskForm
    #...
    password = PasswordField('password', validators=[DataRequired(message='Please enter your password')])
    password2 = PasswordField('Confirm password', validators=[DataRequired(message='Please enter your password confirmation'), EqualTo('password', 
                                                                                           message='Inconsistent passwords')])
    submit = SubmitField('registered')
Copy the code

Refresh the page, and something strange happens. The e original DataRequired error information is actually fixed in Password2, but the other forms are left intact. All right, leave him alone. We could start writing the actual registration function now, but putting passwords directly in the database is not a good idea. However, with encryption, we can solve this problem. I have found the flask – bcrypt. Readthedocs. IO/en/latest/used to encrypt the password. Download and initialize it first:

PIP Install flask-bcryptCopy the code

Initialization:

# app/extensions.py

#...
from flask_bcrypt import Bcrypt

# instantiate the extension
#...
bcrypt = Bcrypt()
Copy the code
# app/__init__.py

#...


def create_app() :
    app = Flask(__name__)  # create app instance
    #...
    bcrypt.init_app(app)

    #...

    return app  # to return to the app

Copy the code

To use flask-bcrypt, edit the User class:

# app/models.py

from .extensions import db, bcrypt  # import SQLAlchemy

#...

class User(db.Model) :  The # User class inherits from db.model
    #...
    password = db.Column(db.String(255))  # save password

    def __init__(self, password, **kwargs) :
        super().__init__(password=password, **kwargs)
        self.password = self.set_password(password)  Unencrypted passwords are encrypted during initialization

    def set_password(self, password) :
        return bcrypt.generate_password_hash(password)  Call the flask-bcrypt built-in function to generate a password hash

    def check_password(self, password) :
        return bcrypt.check_password_hash(self.password, password)  Check if the password matches the hash value

    #...

Copy the code

To prepare for production, we can also create a function in the Role class that automatically generates the required roles:

# app/models.py

from .extensions import db, bcrypt  # import SQLAlchemy


class Role(db.Model) :
    __tablename__ = 'role'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64))
    users = db.relationship('User', backref='role', lazy='dynamic')  Create an association
    
    @staticmethod
    def insert_role() :
        print('Inserting roles... ', end=' ')
        roles = ['Ordinary user'.'Associate'.'Administrator']
        for role in roles:
            if Role.query.filter_by(name=role).first() is None:
                role = Role(name=role)
                db.session.add(role)
        db.session.commit()
        for user in User.query.all() :if user.role is None:
                user.role = Role.query.filter_by(name='Ordinary user').first()
                db.session.add(user)
        db.session.commit()
        print('done')

    def __repr__(self) :
        return '<Role %s>' % self.name


class User(db.Model) :  The # User class inherits from db.model
    #...

Copy the code

For easy development and deployment, we can make a one-click deployment command and automatically import each model class into the Flask shell:

# app.py

from app import create_app  # import create_app
from app.models import Role, User
from app.extensions import db

app = create_app()  # create an app


@app.shell_context_processor  Flask built-in shell context decorator
def make_shell_context() :
    return dict(db=db, Role=Role, User=User)  # return a dictionary containing all models


@app.cli.command()  # Flask integrates Click, and we can use its commands to easily create command-line commands
def deploy() :
    """Deploy the application"""
    Role.insert_role()


if __name__ == '__main__':
    app.run(debug=True)  Run the app and enable debug mode

Copy the code

Here, when I look at all of flask’s commands, deploy does not appear. But I’ll rename app.py to attributeError.py (or whatever you want to call it), and that’s it. I also found that the flask shell has been in a production mode, I was in the root directory. Add the FLASK_ENV env = development this way, when to deploy the change back again. Now, let’s delete the users and roles we tested earlier and open the Flask shell:

Python 3.81. (v38.1.:1b293b6006, Dec 18 2019.14: 08:53) 
[Clang 6.0 (clang-600.057.)] on darwin
App: app [development]
Instance: /Users/sam/Desktop/Python/AttributeError/instance
>>> for user in User.query.all() :# Import is not used here because we have already defined it in context
.    db.session.delete(user)  # loop delete
.
>>> db.session.commit()  # commit changes
>>> for role in Role.query.all() :.    db.session.delete(role)
.
>>> db.session.commit()
Copy the code

Now we can deploy with the previous command:

(venv) flask deploy Inserting roles... doneCopy the code

Finally, we can continue to write views:

# app/auth/views.py

from . import auth
from app.models import User, Role
from flask import render_template, flash
from .forms import RegistrationForm  # import form
from app.extensions import db


@auth.route('/register/', methods=['GET'.'POST'])
def register() :
    form = RegistrationForm()  # instantiate the form
    if form.validate_on_submit():  Execute when the form is submitted
        Get form information
        username = form.username.data
        email = form.email.data
        password = form.password.data
        user = User(username=username, email=email, password=password, role=Role.query.filter_by(name='Ordinary user').first())
        db.session.add(user)
        db.session.commit()
        flash('Registration successful'.'success')
    return render_template('auth/register.html', form=form)

Copy the code

In order for flash messages to be displayed, we need to change base.html:

<! --app/templates/base.html-->{% from 'Bootstrap /nav. HTML' import render_nav_item %} {% from 'Bootstrap /utils.html' import render_messages %}<! DOCTYPEhtml>
<html lang="en">
<head>
    <! -... -->
</head>
<body>
    <! -... -->
    <br>{{render_messages(container=True, distransmissible =True, dismiss_animate=True)}}<br>{% block content %}{% endBlock %}{# block content #}</body>{% block scripts %} {# JS code block #} {{bootstrap.load_js()}} {# bootstrap-flask built-in JavaScript #} {% endblock %}</html>
Copy the code

Now, run the program and see:

When a user name or email address has already been registered, the program will prompt us. The program will also prompt us if the password and confirmation password are inconsistent. The program will also tell us when the registration is successful. Now we are ready to write the login functionality. First, install flask-Login, which integrates things you need for login, such as Remember me and visitor mode:

pip install flask-login
Copy the code

Initialization:

# app/extensions.py

from flask_bootstrap import Bootstrap  # import the Bootstrap - Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_bcrypt import Bcrypt
from flask_login import LoginManager

# instantiate the extension
bootstrap = Bootstrap()
db = SQLAlchemy()
migrate = Migrate()
bcrypt = Bcrypt()
login_manager = LoginManager()

Copy the code
# app/__init__.py

#...

def create_app() :
    app = Flask(__name__)  # create app instance
    #...
    login_manager.init_app(app)

    #...

    return app  # to return to the app

Copy the code

However, flask-login also needs to set up the user_Loader to load the user. Let’s define it:

# app/extensions.py

#...
from flask_login import LoginManager
from app.models import User

# instantiate the extension
#...
login_manager = LoginManager()
login_manager.login_view = "auth.login"  Flask-login login view
login_manager.login_message = "Please log in and return to this page"  Flask-login = 'Please login to access this page.'


@login_manager.user_loader  Define the user loader
def load_user(id) :
    return User.query.get(id)
Copy the code

But that’s not enough. Flask-login also needs the User class to inherit from the UserMixin base class, so let’s change models.py:

# app/models.py

from .extensions import db, bcrypt
from flask_login import UserMixin

#...

class User(db.Model, UserMixin) :  The # User class inherits from db.model
    #...
Copy the code

Now let’s write the login view, creating the form first:

# app/auth/forms.py

#...

class RegistrationForm(FlaskForm) :  # RegistrationForm inherits from FlaskForm
    #...
        

class LoginForm(FlaskForm) :
    username = StringField('Username', validators=[DataRequired(message='Please enter a user name')])
    password = PasswordField('password', validators=[DataRequired(message='Please enter your password')])
Copy the code

Now open auth/views.py and write the view function:

@auth.route('/login/', methods=['GET'.'POST'])
def login() :
    form = LoginForm()
    if form.validate_on_submit():
        username = form.username.data
        password = form.password.data
        remember = form.remember_me.data
        user = User.query.filter_by(username=username).first()
        if user and user.check_password(password):
            login_user(user, remember=remember)
            flash('Login successful! '.'success')
            return redirect(url_for('main.index'))
        flash('Incorrect username or password'.'warning')
    return render_template('auth/login.html', form=form)
Copy the code

Next, write the HTML template:

<! --app/templates/login.html-->{% extends 'base.html' %} {% from 'bootstrap/form.html' import render_form %} {% block title %} Login - AttributeError{% endblock %} {% block content %}<div class="container">
        <h1>The login</h1>
        <p>Don't have an account yet?<a href="{{ url_for('auth.register') }}">Click here to</a>registered</p>
        <hr>
        {{ render_form(form, button_map={'submit': 'primary'}) }}
    </div>
{% endblock %}
Copy the code

In addition, we should also provide the login link on the registration page:

<! --app/templates/register.html-->{% extends 'base.html' %} {% from 'bootstrap/form.html' import render_form %} {% block title %} register - AttributeError{% endblock %} {% block content %}<div class="container">
        <h1>registered</h1>
        <p>Existing account?<a href="{{ url_for('auth.login') }}">Click here to</a>The login</p>
        <hr>Render_form (form, button_map={'submit': 'primary'})}} {</div>
{% endblock %}
Copy the code

Most websites also display a login/registration link in the navigation bar. We also add:

<! --app/templates/base.html-->{% from 'Bootstrap /nav. HTML' import render_nav_item %} {% from 'Bootstrap /utils.html' import render_messages %}<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% title %}{% endBlock %}{# title block #}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='Bootstrap/bootstrap.css') }}">{# introduce custom Bootstrap CSS #}</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container">
            <! -... -->
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav">{{render_nav_item('main.index', 'home ')}} {# use bootstrap-flask to render navigation links #}</ul>
                <ul class="navbar-nav ml-auto">{% if not current_user.is_authenticated %} {{ render_nav_item('auth.login', 'login')}} {{render_nav_item (' auth. Register ', 'registered')}}} else {% %<span class="navbar-text">
                            Hi, {{ current_user.username }}
                        </span>
                    {% endif %}
                </ul>
            </div>
        </div>
    </nav>
    <! -... -->
</html>
Copy the code

Here, if the user is logged in, the right side of the navigation bar will display Hi, user name, otherwise login and registration links will be displayed.

In addition to logging in and registering, we also need to log out. Flask-login is done for us, we just need to call it:

# app/auth/views.py

from . import auth
from app.models import User, Role
from flask import render_template, flash, redirect, url_for
from .forms import RegistrationForm, LoginForm  # import form
from app.extensions import db
from flask_login import login_user, login_required, logout_user, current_user

#...

@auth.route('/logout/')
@login_required  Only the user has logged in can log out
def logout() :
    logout_user()
    flash('You've logged out'.'success')
    return redirect(url_for('main.index'))

Copy the code

Now, let’s display the link in the navigation bar:

<! --app/templates/base.html-->
<! -... -->
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container">
            <a class="navbar-brand" href="/">AttributeError</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav">{{render_nav_item('main.index', 'home ')}} {# use bootstrap-flask to render navigation links #}</ul>
                <ul class="navbar-nav ml-auto">{% if not current_user.is_authenticated %} {{ render_nav_item('auth.login', 'login')}} {{render_nav_item (' auth. Register ', 'registered')}}} else {% %<li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                Hi, {{ current_user.username }}
                            </a>
                            <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
                                <a class="dropdown-item" href="{{ url_for('auth.logout') }}">logout</a>
                            </div>
                        </li>
                    {% endif %}
                </ul>
            </div>
        </div>
    </nav>
    <! -... -->
</html>
Copy the code

Users can now log in and out. However, we have not verified that the user’s email address is real. Most websites use the method of verifying email, and we adopt this method too. First, you need to register an account at www.sendgrid.com/, then create an ApiKey in the control panel and either copy it or put it in a file. Now, let’s use it to authenticate the user. First, add the confirmed row to the User model:

# app/models.py

#...


class User(db.Model, UserMixin) :  The # User class inherits from db.model
    #...
    confirmed = db.Column(db.Boolean, default=False)

    #...

Copy the code

Then migrate the database:

(venv) flask db migrate
# ... 
(venv) flask db upgrade
Copy the code

However, we also want to limit the use of unauthenticated users. If the user is not authenticated (Confirmed =False), deny the user access to all pages except the view in the Auth blueprint. Instead, a page will remind users of their authentication. Let’s finish this function first :(insert a small episode here, just as I am writing this article, at 5:37, zhihu reported error 502 except the home page, surprised)

# app/main/views.py

from flask import render_template, request  # import render template function
from . import main  # import blueprint
from flask_login import current_user


@main.route('/')  # define route
def index() :
    return render_template('main/index.html')  Return to the rendered body of the page

@auth.route('/unconfirmed/')
@login_required
def unconfirmed() :
    if current_user.confirmed:
        return redirect(url_for('main.index'))
    return render_template('auth/unconfirmed.html')


@main.before_app_request  The function executed before each request is executed by the application
def before_request() :
    if not current_user.confirmed andrequest.blueprint ! ='auth':
        return redirect(url_for('auth.unconfirmed'))

Copy the code
<! --app/templates/auth/unconfirmed.html-->{% extends 'base.html' %} {% block title %} Authenticate your account - AttributeError{% endBlock %} {% block content %}<div class="container">
        <h1>Hi, {{ current_user.username }}!</h1>
        <hr>
        <p>You haven't authenticated your account yet!</p>
    </div>
{% endblock %}
Copy the code

Now run the program, if you logged out last time or did not select the remember me option, you may see this error:

Traceback (most recent call last): The File "/ Users/Sam/Desktop/Python/AttributeError/venv/lib/python3.8 / site - packages/flask/app. Py", line 2463, in __call__ return self.wsgi_app(environ, Start_response) File "/ Users/Sam/Desktop/Python/AttributeError/venv/lib/python3.8 / site - packages/flask/app. Py", line 2449, in wsgi_app response = self.handle_exception(e) File "/ Users/Sam/Desktop/Python/AttributeError/venv/lib/python3.8 / site - packages/flask/app. Py", line 1866, in handle_exception reraise(exc_type, exc_value, TB) File "/ Users/Sam/Desktop/Python/AttributeError/venv/lib/python3.8 / site - packages/flask / _compat. Py", line 39, In reraise raise value File "/ Users/Sam/Desktop/Python/AttributeError/venv/lib/python3.8 / site - packages/flask/app. Py", line 2446, in wsgi_app response = self.full_dispatch_request() File "/ Users/Sam/Desktop/Python/AttributeError/venv/lib/python3.8 / site - packages/flask/app. Py", line 1951, in full_dispatch_request rv = self.handle_user_exception(e) File "/ Users/Sam/Desktop/Python/AttributeError/venv/lib/python3.8 / site - packages/flask/app. Py", line 1820, in handle_user_exception reraise(exc_type, exc_value, TB) File "/ Users/Sam/Desktop/Python/AttributeError/venv/lib/python3.8 / site - packages/flask / _compat. Py", line 39, In reraise raise value File "/ Users/Sam/Desktop/Python/AttributeError/venv/lib/python3.8 / site - packages/flask/app. Py", line 1947, in full_dispatch_request rv = self.preprocess_request() File "/ Users/Sam/Desktop/Python/AttributeError/venv/lib/python3.8 / site - packages/flask/app. Py", line 2241, in preprocess_request rv = func() File "/Users/sam/Desktop/Python/AttributeError/app/main/views.py", line 15, in before_request if not current_user.confirmed and request.blueprint ! = 'auth': The File "/ Users/Sam/Desktop/Python/AttributeError/venv/lib/python3.8 / site - werkzeug/local/packages. Py", line 347, in __getattr__ return getattr(self._get_current_object(), name) AttributeError: 'AnonymousUserMixin' object has no attribute 'confirmed'Copy the code

This time it really is AttributeError. No, it has to be fixed, or we’ll lose the name. Flask-login AnonymousUserMixin doesn’t have confirmed, so we’ll just add this to it:

@main.before_app_request  The function executed before each request is executed by the application
def before_request() :
    if not current_user.is_anonymous and not current_user.confirmed andrequest.blueprint ! ='auth':
        return redirect(url_for('auth.unconfirmed'))
Copy the code

Well, problem solved. Now let’s write the mail function first. First, we need flask-mail to simplify things. Install it first:

(venv) pip install flask-mail
Copy the code

Initialization:

# app/extensions.py

#...
from flask_mail import Mail

# instantiate the extension
#...
mail = Mail()

Copy the code
# app/__init__.py

#...


def create_app() :
    app = Flask(__name__)  # create app instance
    #...
    mail.init_app(app)

    #...

    return app  # to return to the app

Copy the code

Before we write the function that sends the mail, we need to add some Settings. Open the. Env file in the root directory and add the following:

MAIL_PASSWORD=<your-apikey>
Copy the code

Replace the < your-apiKey > above with the apikey you just generated in Sendgrid, then open config.py: replace the <your-apikey> above with the apikey you just generated in Sendgrid, then open config.py:

# app/config.py

import os


class DevelopmentConfig:
    DEBUG = True  Set to debug mode
    Set the database location
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URI') or 'mysql+pymysql://root:%s@localhost:3306/%s' \
                                                                    '? charset'\
                                                                    '=utf8mb4' % (os.environ.get('DEV_DATABASE_PASS'),
                                                                                  os.environ.get('DEV_DATABASE_NAME'))
    # Do not track changes
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SECRET_KEY = 'this is my secret key!! '  # set key
    MAIL_USERNAME = 'apikey'
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    MAIL_SERVER = 'smtp.sendgrid.net'
    MAIL_PORT = 465
    MAIL_USE_SSL = True


class ProductionConfig:
    DEBUG = False  # disable debugging
    Set up the database to use in the production environment
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'mysql+pymysql://root:%s@localhost:3306/%s? charset' \
                               '=utf8mb4' % (os.environ.get('DATABASE_PASS'), os.environ.get('DATABASE_NAME'))
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'secret key'  The production environment first uses the keys in the environment variables
    MAIL_USERNAME = 'apikey'
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    MAIL_SERVER = 'smtp.sendgrid.net'
    MAIL_PORT = 465
    MAIL_USE_SSL = True


Set the config to be used in different cases
config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,

    'default': DevelopmentConfig
}

Copy the code

Here we set the username for the mailbox (sendGrid is all “apikey”), password (the real APIkey, retrieved from the environment variable), mailbox server (smtp.sendgrid.net), mailbox sending port (465, the only SSL port for SendGrid), And whether the email sending server uses SSL (True). Flask-mail is now used to send flask-mail, creating app/utils.py:

# app/utils.py

from flask_mail import Message
from flask import render_template
from .extensions import mail


def send_email(from_address, to_address, title, template=None, **kwargs) :
    msg = Message(title, sender=from_address, recipients=[to_address])  # create message
    if template:
        msg.body = render_template('%s.txt' % template, **kwargs)  # Render the text body
        msg.html = render_template('%s.html' % template, **kwargs)  Render the HTML body
    else:
        msg.body = title
    mail.send(msg)  # Send email

Copy the code

Now let’s test this by opening the Flask shell:

Python 3.81. (v38.1.:1b293b6006, Dec 18 2019.14: 08:53) 
[Clang 6.0 (clang-600.057.)] on darwin
App: app [development]
Instance: /Users/sam/Desktop/Python/AttributeError/instance
>>> from app.utils import send_email  # import this function
>>> The first parameter is the sender address, the second parameter is the recipient address, and the last parameter is the body of the message.
>>> send_email('[email protected]'.'[email protected]'.'Hi, User! This is a test email and sent by Python. Can you see it? ')
send: 'the ehlo / 127.0.0.1 \ r \ n'
reply: b'250-smtp.sendgrid.net\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250-PIPELINING\r\n'
reply: b'250-SIZE 31457280\r\n'
reply: b'250-AUTH PLAIN LOGIN\r\n'
reply: b'250 AUTH=PLAIN LOGIN\r\n'
reply: retcode (250); Msg: b'smtp.sendgrid.net\n8BITMIME\nPIPELINING\nSIZE 31457280\nAUTH PLAIN LOGIN\nAUTH=PLAIN LOGIN'
send: 'AUTH PLAIN AGFwaWtleQBTRy55bWRPMVBYSlFmMkZTZDZjcG9jMVRRLjZVU09jVjBJOHRuWUg1ZF9sMkg2OEZXUVFnSW0wRzFVZ1R0QTI5di04bDQ=\r\n'
reply: b'235 Authentication successful\r\n'
reply: retcode (235); Msg: b'Authentication successful'
send: 'mail FROM:<[email protected]> size=402\r\n'
reply: b'250 Sender address accepted\r\n'
reply: retcode (250); Msg: b'Sender address accepted'
send: 'rcpt TO:<[email protected]>\r\n'
reply: b'250 Recipient address accepted\r\n'
reply: retcode (250); Msg: b'Recipient address accepted'
send: 'data\r\n'
reply: b'354 Continue\r\n'
reply: retcode (354); Msg: b'Continue'
data: (354.b'Continue')
send: b'Content-Type: text/plain; Charset =" utF-8 "\r\ nMIME-version: 1.0\r\ nContent-transfer-Encoding: 7bit\r\nSubject: Hi, User! This is a test email and sent by Python. Can you see it? \r\nFrom: [email protected]\r\nTo: [email protected]\r\nDate: Sat, Mar 2020 19:01:13 + 0800 21 \ r \ nMessage - ID: < 158478847246.17263.7630660357922288153 @ bogon > \ r \ n \ r \ nHi, the User! This is a test email and sent by Python. Can you see it? \r\n.\r\n'
reply: b'250 Ok: queued as wkngCyhORL6Q5SKRAqPjuQ\r\n'
reply: retcode (250); Msg: b'Ok: queued as wkngCyhORL6Q5SKRAqPjuQ'
data: (250.b'Ok: queued as wkngCyhORL6Q5SKRAqPjuQ')
send: 'quit\r\n'
reply: b'221 See you later\r\n'
reply: retcode (221); Msg: b'See you later'
Copy the code

Now, let’s write a function that validates the user. First, we need to generate the token. Itsdangerous gives us this functionality, so we just need to call it (although we still don’t know why, from github.com/miguelgrinb… Copy over, also please god guidance) :

# app/models.py
from flask import current_app
from itsdangerous import Serializer

from .extensions import db, bcrypt
from flask_login import UserMixin


#...

class User(db.Model, UserMixin) :  The # User class inherits from db.model
    #...

    def generate_confirmation_token(self, expiration=3600) :
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'confirm': self.id}).decode('utf-8')

    def confirm(self, token) :
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token.encode('utf-8'))
        except:
            return False
        if data.get('confirm') != self.id:
            return False
        self.confirmed = True
        db.session.add(self)
        return True

    #...

Copy the code

We are now ready to write an authenticated view:

# app/auth/views.py

#...


@auth.route('/confirm/<token>/')
@login_required
def confirm_user(token) :
    user = current_user._get_current_object()
    if user.confirm(token):
        db.session.commit()
        flash('Verify user succeeded! '.'success')
        return redirect(url_for('main.index'))
    flash('Incorrect token, validation failed'.'error')
    return redirect(url_for('main.index'))


#...

Copy the code

But that’s not enough. We must generate the authentication token at registration and send the authentication email to the user. Change views.py again:

# app/auth/views.py

#...
from flask_login import login_user, login_required, logout_user, current_user
from app.utils import send_email


@auth.route('/register/', methods=['GET'.'POST'])
def register() :
    form = RegistrationForm()  # instantiate the form
    if form.validate_on_submit():  Execute when the form is submitted
        Get form information
        username = form.username.data
        email = form.email.data
        password = form.password.data
        user = User(username=username, email=email, password=password, role=Role.query.filter_by(name='Ordinary user').first())
        db.session.add(user)
        db.session.commit()
        token = user.generate_confirmation_token()
        send_email('[email protected]', user.email, 'Authenticate your Account', template='auth/email/confirm', user=user, 
                   token=token)
        flash('Registration successful'.'success')
    return render_template('auth/register.html', form=form)

#...

Copy the code

Now we can test this feature by deleting the previously created user and creating a new one.

However, sometimes the user may not receive the message or accidentally delete it, so we need to provide a resend button. First, create the view:

# app/auth/views.py

#...

@auth.route('/confirm/re-send/')
@login_required
def re_send_confirm() :
    user = current_user._get_current_object()
    token = user.generate_confirmation_token()
    send_email('[email protected]', user.email, 'Authenticate your Account', template='auth/email/confirm', user=user,
               token=token)
    flash('A new authentication message has been sent successfully! '.'success')
    return redirect(url_for('main.index'))

#...

Copy the code

Then, change unconfirmed.html to allow the user to resend the email:

<! --app/templates/auth/unconfirmed.html-->{% extends 'base.html' %} {% block title %} Authenticate your account - AttributeError{% endBlock %} {% block content %}<div class="container">
        <h1>Hi, {{ current_user.username }}!</h1>
        <hr>
        <p>You haven't authenticated your account yet!</p>
        <p>Didn't receive the verification email? Please check your spam, or<a href="{{ url_for('auth.re_send_confirm') }}">Click here to</a>Resend the verification email!</p>
    </div>
{% endblock %}
Copy the code

You might notice that when you send an email now, the web page doesn’t respond for a second or two, that’s the application sending the email. We can solve this problem by sending emails asynchronously:

# app/utils.py

from flask_mail import Message
from flask import render_template, current_app
from .extensions import mail
import threading


def send_async_email(msg) :
    with current_app.app_context():
        mail.send(msg)


def send_email(from_address, to_address, title, template=None, **kwargs) :
    msg = Message(title, sender=from_address, recipients=[to_address])  # create message
    if template:
        msg.body = render_template('%s.txt' % template, **kwargs)  # Render the text body
        msg.html = render_template('%s.html' % template, **kwargs)  Render the HTML body
    else:
        msg.body = title
    threading.Thread(target=send_async_email, args=[msg])  Send email asynchronously
Copy the code

You may have noticed that our sign-up page now doesn’t have a jump to the login page, which is very inconvenient. Let’s add it to:

# app/auth/views.py

#...

@auth.route('/register/', methods=['GET'.'POST'])
def register() :
    form = RegistrationForm()  # instantiate the form
    if form.validate_on_submit():  Execute when the form is submitted
        Get form information
        username = form.username.data
        email = form.email.data
        password = form.password.data
        user = User(username=username, email=email, password=password, role=Role.query.filter_by(name='Ordinary user').first())
        db.session.add(user)
        db.session.commit()
        token = user.generate_confirmation_token()
        send_email('[email protected]', user.email, 'Authenticate your Account', template='auth/email/confirm', user=user,
                   token=token)
        flash('Registered successfully, you can log in now'.'success')
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html', form=form)

#...

Copy the code

However, once a user enters a page that requires a login, he goes to the login page first and then directly to the home page instead of the page he just visited. Flask-login already tells us in the address bar what page the user is coming from, so we just jump to it. However, we must verify that the URL is secure. Add some code to utils.py:

# app/utils.py
from urllib.parse import urlparse, urljoin

from flask_mail import Message
from flask import render_template, current_app, request
from .extensions import mail
import threading


#...

def is_safe_url(target) :
    ref_url = urlparse(request.host_url)  Get the host URL in the program
    test_url = urlparse(urljoin(request.host_url, target))  Convert the destination URl to an absolute path
    return test_url.scheme in ('http'.'https') and ref_url.netloc == test_url.netloc  Verify that this is an internal URL

Copy the code

Next, the implementation of the jump function:

# app/auth/views.py

#...
from app.utils import send_email, is_safe_url


#...

@auth.route('/login/', methods=['GET'.'POST'])
def login() :
    form = LoginForm()
    if form.validate_on_submit():
        username = form.username.data
        password = form.password.data
        remember = form.remember_me.data
        user = User.query.filter_by(username=username).first()
        if user and user.check_password(password):
            login_user(user, remember=remember)
            next = request.args.get('next')
            flash('Login successful! '.'success')
            if is_safe_url(next) :return redirect(next)
            return redirect(url_for('main.index'))
        flash('Incorrect username or password'.'warning')
    return render_template('auth/login.html', form=form)

Copy the code

So far, we have completed the functions of user login, logout, registration, and email authentication. It took me nearly 5 days to write this article. Probably no one will be able to read it, but I’ll take notes anyway.

If you see this and you don’t want to code, clone my repository on GitHub :(version 694e079) github.com/samzhangjy/…

Don’t forget to execute PIP install -r requiretions. TXT and flask DB upgrade to download the Flask extension and update the database! Finally, you should also update your. Env file with your SendGrid information.

Write in the last

Ah, finally. If there is a mistake, please advise!

Also, I haven’t replied to the issue I mentioned in Bootstrap-flask, I will update the article if there is.