2018-02-06T15:11:28Z

The Flask Mega-Tutorial Part X: Email Support

This is the tenth installment of the Flask Mega-Tutorial series, in which I'm going to tell you how your application can send emails to your users, and how to build a password recovery feature on top of the email support.

For your reference, below is a list of the articles in this series.

Note 1: If you are looking for the legacy version of this tutorial, it's here.

Note 2: If you would like to support my work on this blog, or just don't have patience to wait for weekly articles, I am offering the complete version of this tutorial packaged as an ebook or a set of videos. For more information, visit courses.miguelgrinberg.com.

The application is doing pretty well on the database front now, so in this chapter I want to depart from that topic and add another important piece that most web applications need, which is the sending of emails.

Why does an application need to email its users? There are many reasons, but one common one is to solve authentication related problems. In this chapter I'm going to add a password reset feature for users that forget their password. When a user requests a password reset, the application will send an email with a specially crafted link. The user then needs to click that link to have access to a form in which to set a new password.

The GitHub links for this chapter are: Browse, Zip, Diff.

Introduction to Flask-Mail

As far as the actual sending of emails, Flask has a popular extension called Flask-Mail that can make the task very easy. As always, this extension is installed with pip:

(venv) $ pip install flask-mail

The password reset links will have a secure token in them. To generate these tokens, I'm going to use JSON Web Tokens, which also have a popular Python package:

(venv) $ pip install pyjwt

The Flask-Mail extension is configured from the app.config object. Remember when in Chapter 7 I added the email configuration for sending yourself an email whenever an error occurred in production? I did not tell you this then, but my choice of configuration variables was modeled after Flask-Mail's requirements, so there isn't really any additional work that is needed, the configuration variables are already in the application.

Like most Flask extensions, you need to create an instance right after the Flask application is created. In this case this is an object of class Mail:

app/__init__.py: Flask-Mail instance.

# ...
from flask_mail import Mail

app = Flask(__name__)
# ...
mail = Mail(app)

If you are planning to test sending of emails you have the same two options I mentioned in Chapter 7. If you want to use an emulated email server, Python provides one that is very handy that you can start in a second terminal with the following command:

(venv) $ python -m smtpd -n -c DebuggingServer localhost:8025

To configure for this server you will need to set two environment variables:

(venv) $ export MAIL_SERVER=localhost
(venv) $ export MAIL_PORT=8025

If you prefer to have emails sent for real, you need to use a real email server. If you have one, then you just need to set the MAIL_SERVER, MAIL_PORT, MAIL_USE_TLS, MAIL_USERNAME and MAIL_PASSWORD environment variables for it. If you want a quick solution, you can use a Gmail account to send email, with the following settings:

(venv) $ export MAIL_SERVER=smtp.googlemail.com
(venv) $ export MAIL_PORT=587
(venv) $ export MAIL_USE_TLS=1
(venv) $ export MAIL_USERNAME=<your-gmail-username>
(venv) $ export MAIL_PASSWORD=<your-gmail-password>

If you are using Microsoft Windows, you need to replace export with set in each of the export statements above.

Remember that the security features in your Gmail account may prevent the application from sending emails through it unless you explicitly allow "less secure apps" access to your Gmail account. You can read about this here, and if you are concerned about the security of your account, you can create a secondary account that you configure just for testing emails, or you can enable less secure apps only temporarily to run your tests and then revert back to the more secure default.

Flask-Mail Usage

To learn how Flask-Mail works, I'll show you how to send an email from a Python shell. So fire up Python with flask shell, and then run the following commands:

>>> from flask_mail import Message
>>> from app import mail
>>> msg = Message('test subject', sender=app.config['ADMINS'][0],
... recipients=['your-email@example.com'])
>>> msg.body = 'text body'
>>> msg.html = '<h1>HTML body</h1>'
>>> mail.send(msg)

The snippet of code above will send an email to a list of email addresses that you put in the recipients argument. I put the sender as the first configured admin (I've added the ADMINS configuration variable in Chapter 7). The email will have plain text and HTML versions, so depending on how your email client is configured you may see one or the other.

So as you see, this is pretty simple. Now let's integrate emails into the application.

A Simple Email Framework

I will begin by writing a helper function that sends an email, which is basically a generic version of the shell exercise from the previous section. I will put this function in a new module called app/email.py:

app/email.py: Email sending wrapper function.

from flask_mail import Message
from app import mail

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    mail.send(msg)

Flask-Mail supports some features that I'm not utilizing here such as Cc and Bcc lists. Be sure to check the Flask-Mail Documentation if you are interested in those options.

Requesting a Password Reset

As I mentioned above, I want users to have the option to request their password to be reset. For this purpose I'm going to add a link in the login page:

app/templates/login.html: Password reset link in login form.

    <p>
        Forgot Your Password?
        <a href="{{ url_for('reset_password_request') }}">Click to Reset It</a>
    </p>

When the user clicks the link, a new web form will appear that requests the user's email address as a way to initiate the password reset process. Here is the form class:

app/forms.py: Reset password request form.

class ResetPasswordRequestForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Email()])
    submit = SubmitField('Request Password Reset')

And here is the corresponding HTML template:

app/templates/reset_password_request.html: Reset password request template.

{% extends "base.html" %}

{% block content %}
    <h1>Reset Password</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.email.label }}<br>
            {{ form.email(size=64) }}<br>
            {% for error in form.email.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

I also need a view function to handle this form:

app/routes.py: Reset password request view function.

from app.forms import ResetPasswordRequestForm
from app.email import send_password_reset_email

@app.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = ResetPasswordRequestForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user:
            send_password_reset_email(user)
        flash('Check your email for the instructions to reset your password')
        return redirect(url_for('login'))
    return render_template('reset_password_request.html',
                           title='Reset Password', form=form)

This view function is fairly similar to others that process a form. I start by making sure the user is not logged in. If the user is logged in, then there is no point in using the password reset functionality, so I redirect to the index page.

When the form is submitted and valid, I look up the user by the email provided by the user in the form. If I find the user, I send a password reset email. I'm using a send_password_reset_email() helper function to do this. I will show you this function below.

After the email is sent, I flash a message directing the user to look for the email for further instructions, and then redirect back to the login page. You may notice that the flashed message is displayed even if the email provided by the user is unknown. This is so that clients cannot use this form to figure out if a given user is a member or not.

Password Reset Tokens

Before I implement the send_password_reset_email() function, I need to have a way to generate a password request link. This is going to be the link that is sent to the user via email. When the link is clicked, a page where a new password can be set is presented to the user. The tricky part of this plan is to make sure that only valid reset links can be used to reset an account's password.

The links are going to be provisioned with a token, and this token will be validated before allowing the password change, as proof that the user that requested the email has access to the email address on the account. A very popular token standard for this type of process is the JSON Web Token, or JWT. The nice thing about JWTs is that they are self contained. You can send a token to a user in an email, and when the user clicks the link that feeds the token back into the application, it can be verified on its own.

How do JWTs work? Nothing better than a quick Python shell session to understand them:

>>> import jwt
>>> token = jwt.encode({'a': 'b'}, 'my-secret', algorithm='HS256')
>>> token
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhIjoiYiJ9.dvOo58OBDHiuSHD4uW88nfJikhYAXc_sfUHq1mDi4G0'
>>> jwt.decode(token, 'my-secret', algorithms=['HS256'])
{'a': 'b'}

The {'a': 'b'} dictionary is an example payload that is going to be written into the token. To make the token secure, a secret key needs to be provided to be used in creating a cryptographic signature. For this example I have used the string 'my-secret', but with the application I'm going to use the SECRET_KEY from the configuration. The algorithm argument specifies how the token is to be generated. The HS256 algorithm is the most widely used.

As you can see the resulting token is a long sequence of characters. But do not think that this is an encrypted token. The contents of the token, including the payload, can be decoded easily by anyone (don't believe me? Copy the above token and then enter it in the JWT debugger to see its contents). What makes the token secure is that the payload is signed. If somebody tried to forge or tamper with the payload in a token, then the signature would be invalidated, and to generate a new signature the secret key is needed. When a token is verified, the contents of the payload are decoded and returned back to the caller. If the token's signature was validated, then the payload can be trusted as authentic.

The payload that I'm going to use for the password reset tokens is going to have the format {'reset_password': user_id, 'exp': token_expiration}. The exp field is standard for JWTs and if present it indicates an expiration time for the token. If a token has a valid signature, but it is past its expiration timestamp, then it will also be considered invalid. For the password reset feature, I'm going to give these tokens 10 minutes of life.

When the user clicks on the emailed link, the token is going to be sent back to the application as part of the URL, and the first thing the view function that handles this URL will do is to verify it. If the signature is valid, then the user can be identified by the ID stored in the payload. Once the user's identity is known, the application can ask for a new password and set it on the user's account.

Since these tokens belong to users, I'm going to write the token generation and verification functions as methods in the User model:

app/models.py: Reset password token methods.

from time import time
import jwt
from app import app

class User(UserMixin, db.Model):
    # ...

    def get_reset_password_token(self, expires_in=600):
        return jwt.encode(
            {'reset_password': self.id, 'exp': time() + expires_in},
            app.config['SECRET_KEY'], algorithm='HS256').decode('utf-8')

    @staticmethod
    def verify_reset_password_token(token):
        try:
            id = jwt.decode(token, app.config['SECRET_KEY'],
                            algorithms=['HS256'])['reset_password']
        except:
            return
        return User.query.get(id)

The get_reset_password_token() function generates a JWT token as a string. Note that the decode('utf-8') is necessary because the jwt.encode() function returns the token as a byte sequence, but in the application it is more convenient to have the token as a string.

The verify_reset_password_token() is a static method, which means that it can be invoked directly from the class. A static method is similar to a class method, with the only difference that static methods do not receive the class as a first argument. This method takes a token and attempts to decode it by invoking PyJWT's jwt.decode() function. If the token cannot be validated or is expired, an exception will be raised, and in that case I catch it to prevent the error, and then return None to the caller. If the token is valid, then the value of the reset_password key from the token's payload is the ID of the user, so I can load the user and return it.

Sending a Password Reset Email

Now that I have the tokens, I can generate the password reset emails. The send_password_reset_email() function relies on the send_email() function I wrote above.

app/email.py: Send password reset email function.

from flask import render_template
from app import app

# ...

def send_password_reset_email(user):
    token = user.get_reset_password_token()
    send_email('[Microblog] Reset Your Password',
               sender=app.config['ADMINS'][0],
               recipients=[user.email],
               text_body=render_template('email/reset_password.txt',
                                         user=user, token=token),
               html_body=render_template('email/reset_password.html',
                                         user=user, token=token))

The interesting part in this function is that the text and HTML content for the emails is generated from templates using the familiar render_template() function. The templates receive the user and the token as arguments, so that a personalized email message can be generated. Here is the text template for the reset password email:

app/templates/email/reset_password.txt: Text for password reset email.

Dear {{ user.username }},

To reset your password click on the following link:

{{ url_for('reset_password', token=token, _external=True) }}

If you have not requested a password reset simply ignore this message.

Sincerely,

The Microblog Team

And here is the nicer HTML version of the same email:

app/templates/email/reset_password.html: HTML for password reset email.

<p>Dear {{ user.username }},</p>
<p>
    To reset your password
    <a href="{{ url_for('reset_password', token=token, _external=True) }}">
        click here
    </a>.
</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('reset_password', token=token, _external=True) }}</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Microblog Team</p>

The reset_password route that is referenced in the url_for() call in these two email templates does not exist yet, this will be added in the next section. The _external=True argument that I included in the url_for() calls in both templates is also new. The URLs that are generated by url_for() by default are relative URLs, so for example, the url_for('user', username='susan') call would return /user/susan. This is normally sufficient for links that are generated in web pages, because the web browser takes the remaining parts of the URL from the current page. When sending a URL by email however, that context does not exist, so fully qualified URLs need to be used. When _external=True is passed as an argument, complete URLs are generated, so the previous example would return http://localhost:5000/user/susan, or the appropriate URL when the application is deployed on a domain name.

Resetting a User Password

When the user clicks on the email link, a second route associated with this feature is triggered. Here is the password request view function:

app/routes.py: Password reset view function.

from app.forms import ResetPasswordForm

@app.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    user = User.verify_reset_password_token(token)
    if not user:
        return redirect(url_for('index'))
    form = ResetPasswordForm()
    if form.validate_on_submit():
        user.set_password(form.password.data)
        db.session.commit()
        flash('Your password has been reset.')
        return redirect(url_for('login'))
    return render_template('reset_password.html', form=form)

In this view function I first make sure the user is not logged in, and then I determine who the user is by invoking the token verification method in the User class. This method returns the user if the token is valid, or None if not. If the token is invalid I redirect to the home page.

If the token is valid, then I present the user with a second form, in which the new password is requested. This form is processed in a way similar to previous forms, and as a result of a valid form submission, I invoke the set_password() method of User to change the password, and then redirect to the login page, where the user can now login.

Here is the ResetPasswordForm class:

app/forms.py: Password reset form.

class ResetPasswordForm(FlaskForm):
    password = PasswordField('Password', validators=[DataRequired()])
    password2 = PasswordField(
        'Repeat Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Request Password Reset')

And here is the corresponding HTML template:

app/templates/reset_password.html: Password reset form template.

{% extends "base.html" %}

{% block content %}
    <h1>Reset Your Password</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password2.label }}<br>
            {{ form.password2(size=32) }}<br>
            {% for error in form.password2.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

The password reset feature is now complete, so make sure you try it.

Asynchronous Emails

If you are using the simulated email server that Python provides you may not have noticed this, but sending an email slows the application down considerably. All the interactions that need to happen when sending an email make the task slow, it usually takes a few seconds to get an email out, and maybe more if the email server of the addressee is slow, or if there are multiple addressees.

What I really want is for the send_email() function to be asynchronous. What does that mean? It means that when this function is called, the task of sending the email is scheduled to happen in the background, freeing the send_email() to return immediately so that the application can continue running concurrently with the email being sent.

Python has support for running asynchronous tasks, actually in more than one way. The threading and multiprocessing modules can both do this. Starting a background thread for email being sent is much less resource intensive than starting a brand new process, so I'm going to go with that approach:

app/email.py: Send emails asynchronously.

from threading import Thread
# ...

def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)


def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    Thread(target=send_async_email, args=(app, msg)).start()

The send_async_email function now runs in a background thread, invoked via the Thread() class in the last line of send_email(). With this change, the sending of the email will run in the thread, and when the process completes the thread will end and clean itself up. If you have configured a real email server, you will definitely notice a speed improvement when you press the submit button on the password reset request form.

You probably expected that only the msg argument would be sent to the thread, but as you can see in the code, I'm also sending the application instance. When working with threads there is an important design aspect of Flask that needs to be kept in mind. Flask uses contexts to avoid having to pass arguments across functions. I'm not going to go into a lot of detail on this, but know that there are two types of contexts, the application context and the request context. In most cases, these contexts are automatically managed by the framework, but when the application starts custom threads, contexts for those threads may need to be manually created.

There are many extensions that require an application context to be in place to work, because that allows them to find the Flask application instance without it being passed as an argument. The reason many extensions need to know the application instance is because they have their configuration stored in the app.config object. This is exactly the situation with Flask-Mail. The mail.send() method needs to access the configuration values for the email server, and that can only be done by knowing what the application is. The application context that is created with the with app.app_context() call makes the application instance accessible via the current_app variable from Flask.

62 comments

  • #26 Mack said 2018-03-21T23:02:35Z

    Just a tip for anyone stuck and getting "SMTPServerDisconnected('please run connect() first')" related issues, try using the localhost method first (python -m smtpd -n -c DebuggingServer localhost:8025). It's also important that you export both mail server and mail port before you run your flask server. Of course this is all covered throughout the excellent tutorial, but just as a reminder this is what you should be doing: You need two terminal windows. 1. This is going to be running your local mail server that emulates your emails being sent $(venv) python -m smtpd -n -c DebuggingServer localhost:8025 2. Your main flask terminal window with the following required commands (FLASK_DEBUG=1 is optional but highly recommended for troubleshooting) $ export FLASK_APP=microblog.py $ export FLASK_DEBUG=1 $ export MAIL_SERVER=localhost $ export MAIL_PORT=8025 $ flask run Now try going to your app and resetting your password, it should work or at least the SMTP server issue should be gone. @Miguel Grinberg, I feel like it's easy to forget those prerequisite (export...) steps if learners are not doing the chapter in one sitting. So right after 10.7 where you mention "The password reset feature is now complete, so make sure you try it." it might be helpful to remind people about something new we learned in this chapter which is that if we forget to export mail_server and mail_port before running flask run, SMTP server disconnected error can arise. It's not hard to troubleshoot but to web dev newbies like myself, it's like what is this new error? I believe in one of the earlier chapters, you do remind learners to do $ export FLASK_APP=microblog.py because $ flask run won't work without that step. So just extending the same idea here.

  • #27 Miguel Grinberg said 2018-03-23T00:37:54Z

    @Thijs: You need to debug the route that changes the password. You can add print statements to ensure that the code is executing, for example.

  • #28 James said 2018-04-29T13:27:37Z

    Hi, thank you for your post. However, when I use an email that is not registered the posting goes on well, but when I use a registered email I get an error "raise SMTPServerDisconnected: please run connect() first". I getting this error despite having an internet connection.

  • #29 Miguel Grinberg said 2018-04-29T16:32:27Z

    @James: what do you mean by registered or not registered email addresses?

  • #30 James said 2018-05-07T15:00:16Z

    Sorry for coming back late. It relates to #28 above. Registered means an email I have registered in the system, and unregistered means the email that is not registered. In both cases, when the email is registered or otherwise, it should it should give out the flash message to go check in the email. The code breaks, when I use a registered email and I get the flash message when I use an email that is not registered in the system, I get the error: raise SMTPServerDisconnected(' please run connect() first') smtplib.SMTPServerDisconnected: please run connect() first

  • #31 Miguel Grinberg said 2018-05-07T19:15:40Z

    @James: check your email server configuration, there must be something there that is missing.

  • #32 Satinder said 2018-05-08T23:44:03Z

    Hi Miguel, First off, thanks so much for this tutorial, it's so incredibly helpful! I just have a quick question - how would you translate validation errors? To be more specific, let's say a user is on the login page and accidentally hits submit on the empty form. If this happens, page currently shows "please fill out this field" in English. How to make this in Spanish? Thank you, Satinder

  • #33 Miguel Grinberg said 2018-05-09T11:48:07Z

    @Satinder: Flask-Login has all the error messages translated to many languages. This is going to be activated later in the tutorial, when you add Flask-Babel to the application.

  • #34 Bruno said 2018-05-10T17:15:14Z

    @Mack: I was getting this error when I took a break and then accidentally forgot to set the "MAIL_..." environment variables. I was about to reply to Anthony when I noticed your comment, especially since your response is only on the second comment page (I thought the previous/next was the about the actual posts, not comments). I agree that a reminder near the end would be beneficial, even if it's just a mention of the configuration variables in the first section of this post.

  • #35 Pavel said 2018-05-19T19:49:19Z

    Hello, Miguel. I've caught a problem: Exception in thread Thread-2: #... File "C:\Users\Paberu\PycharmProjects\fit_app\app\email.py", line 7, in send_async_email with app.app_context(): #... RuntimeError: Working outside of application context. The code is the following: from flask_mail import Message from threading import Thread from . import mail, current_app def send_async_email(app, msg): with app.app_context(): mail.send(msg) def send_email(subject, sender, recipients, text_body, html_body): msg = Message(subject, sender=sender,recipients=recipients) msg.body = text_body msg.html = html_body Thread(target=send_async_email, args=(current_app, msg)).start() I'm using current_app because using from .. import app called ValueError: attempted relative import beyond top-level package I think that's because I don't fully understand what is the best way of getting application context in this situation. Oh, and one more thing: in my app structure email.py is inside of app folder and config.py and manage.py are outside.

  • #36 Miguel Grinberg said 2018-05-20T02:12:34Z

    @Pavel: you have a tiny mistake. In the line where you start the email sending thread, you have to pass "current_app._get_current_object()": Thread(target=send_async_email, args=(current_app._get_current_object(), msg)).start() The "current_app" object is a proxy object that looks for the application context in the current thread, if you pass the object to another thread that does not have an application context you'll get this error. Adding the _get_current_object() call forces the main thread to obtain the application instance, and then pass that to the other thread.

  • #37 Pavel said 2018-05-20T08:25:20Z

    Wow! The gmail application at my phone is saying I have an email from my flask application. Thank you so much. =)

  • #38 Dwaine said 2018-05-27T06:32:46Z

    Hello Miguel, I've verified the code to the best of my abilities, aside from straight copying and pasting that is. Where would I start to debug when the link in the email just redirects me to the home page? It seems to be redirecting at this point: user = User.verify_reset_password_token(token) if not user: return redirect(url_for('index')) So, does this mean that the token isn't verifying? Why wouldn't it verify if all the steps seem correct? Thank you for this excellent tutorial.

  • #39 Miguel Grinberg said 2018-05-27T16:46:50Z

    @Dwaine: you can print the token right before the verify_reset_password call, to see what you are getting. Once you print it, you can take it to the JWT debugger page (https://jwt.io) and paste it there to see what the contents of the JWT are and determine if they have the expected data.

  • #40 Dwaine said 2018-06-02T21:14:52Z

    @Miguel Thank you for the response Miguel. My solution was to change 'algorithms' to 'algorithm' under the verify_reset_password_token function in models.py. I don't know why this worked, but now the link in the email takes me to the correct page. Cheers!

  • #41 Miguel Grinberg said 2018-06-02T22:18:02Z

    @Dwaine: In terms of security features of your app, you should be very suspicious when things just work, so I strongly advice you to investigate this. The change that you made is wrong, the decode function needs to take the "algorithms" argument (plural), while the encode option takes the "algorithm" option (singular). The problem that you have is elsewhere, you just relaxed the security of your app.

  • #42 Andrés Garita said 2018-06-03T23:45:55Z

    Hi Miguel. In the documentation of Flask-Email, it's shown a particular configuration for Bulk email, in this way: with mail.connect() as conn: for user in users: message = '...' subject = "hello, %s" % user.name msg = Message(recipients=[user.email], body=message, subject=subject) conn.send(msg) Is this asynchronous? How can I do it?

  • #43 Miguel Grinberg said 2018-06-04T05:39:23Z

    @Andrés: that example is not directly asynchronous, but you can put that code in the send_async_email() function so that it runs in a background thread, replacing my implementation which sends a single email.

  • #44 Bill said 2018-06-14T14:49:46Z

    I'm really enjoying the tutorial. Made one small suggested change: @staticmethod def verify_reset_password_token(token): try: user_id = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])['reset_password'] except jwt.exceptions.ExpiredSignatureError: flash("Password reset token has expired") # Don't like this UI here, but... return except: current_app.logger.error('Unable to verify password reset token %s', str(token)) return return User.query.get(user_id)

  • #45 leon said 2018-06-28T09:24:23Z

    How does one catch an exception in the background thread? I was testing to prevent the data from committing the data in the database in case there is an error. No matter which function I add a Try and except block the data still gets committed.

  • #46 Miguel Grinberg said 2018-06-28T15:16:34Z

    @leon: the try/except needs to go in the thread itself.

  • #47 Fiodor said 2018-07-10T09:17:21Z

    Hi Miguel, thank you for that tutorial, is great and transparent. But there is some small issue I don't know how to solve. The problem is by sending reset token. After I fill the field with email for password reset and submitting the form, this error is appearing in console: 127.0.0.1 - - [10/Jul/2018 11:14:32] "GET /reset_password_request HTTP/1.1" 200 - [2018-07-10 11:14:41,745] ERROR in app: Exception on /reset_password_request [POST] Traceback (most recent call last): File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 2292, in wsgi_app response = self.full_dispatch_request() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 1815, in full_dispatch_request rv = self.handle_user_exception(e) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 1718, in handle_user_exception reraise(exc_type, exc_value, tb) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\_compat.py", line 35, in reraise raise value File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 1813, in full_dispatch_request rv = self.dispatch_request() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 1799, in dispatch_request return self.view_functions[rule.endpoint](**req.view_args) File "C:\Users\casus\PycharmProjects\juckquotes_01\app\routes.py", line 165, in reset_password_request send_password_reset_email(user) File "C:\Users\casus\PycharmProjects\juckquotes_01\app\email.py", line 26, in send_password_reset_email text_body=render_template('email/reset_password.txt', user=user, token=token), File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\templating.py", line 134, in render_template return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list), File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 869, in get_or_select_template return self.get_template(template_name_or_list, parent, globals) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 830, in get_template return self._load_template(name, self.make_globals(globals)) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 804, in _load_template template = self.loader.load(self, name, globals) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\loaders.py", line 125, in load code = environment.compile(source, name, filename) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 591, in compile self.handle_exception(exc_info, source_hint=source_hint) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 780, in handle_exception reraise(exc_type, exc_value, tb) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\_compat.py", line 37, in reraise raise value.with_traceback(tb) File "C:\Users\casus\PycharmProjects\juckquotes_01\app\templates\email\reset_password.txt", line 5, in template {{ url_for('reset_password', token=token, _external=True }} File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 497, in _parse return Parser(self, source, name, encode_filename(filename)).parse() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 901, in parse result = nodes.Template(self.subparse(), lineno=1) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 875, in subparse add_data(self.parse_tuple(with_condexpr=True)) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 620, in parse_tuple args.append(parse()) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 432, in parse_expression return self.parse_condexpr() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 437, in parse_condexpr expr1 = self.parse_or() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 450, in parse_or left = self.parse_and() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 459, in parse_and left = self.parse_not() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 470, in parse_not return self.parse_compare() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 474, in parse_compare expr = self.parse_math1() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 496, in parse_math1 left = self.parse_concat() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 507, in parse_concat args = [self.parse_math2()] File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 517, in parse_math2 left = self.parse_pow() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 528, in parse_pow left = self.parse_unary() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 547, in parse_unary node = self.parse_postfix(node) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 676, in parse_postfix node = self.parse_call(node) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 785, in parse_call value = self.parse_expression() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 432, in parse_expression return self.parse_condexpr() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 437, in parse_condexpr expr1 = self.parse_or() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 450, in parse_or left = self.parse_and() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 459, in parse_and left = self.parse_not() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 470, in parse_not return self.parse_compare() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 474, in parse_compare expr = self.parse_math1() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 496, in parse_math1 left = self.parse_concat() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 507, in parse_concat args = [self.parse_math2()] File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 517, in parse_math2 left = self.parse_pow() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 528, in parse_pow left = self.parse_unary() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 546, in parse_unary node = self.parse_primary() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 562, in parse_primary next(self.stream) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\lexer.py", line 359, in __next__ self.current = next(self._iter) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\lexer.py", line 562, in wrap for lineno, token, value in stream: File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\lexer.py", line 690, in tokeniter filename) jinja2.exceptions.TemplateSyntaxError: unexpected '}', expected ')' --- Logging error --- Traceback (most recent call last): File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 2292, in wsgi_app response = self.full_dispatch_request() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 1815, in full_dispatch_request rv = self.handle_user_exception(e) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 1718, in handle_user_exception reraise(exc_type, exc_value, tb) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\_compat.py", line 35, in reraise raise value File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 1813, in full_dispatch_request rv = self.dispatch_request() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 1799, in dispatch_request return self.view_functions[rule.endpoint](**req.view_args) File "C:\Users\casus\PycharmProjects\juckquotes_01\app\routes.py", line 165, in reset_password_request send_password_reset_email(user) File "C:\Users\casus\PycharmProjects\juckquotes_01\app\email.py", line 26, in send_password_reset_email text_body=render_template('email/reset_password.txt', user=user, token=token), File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\templating.py", line 134, in render_template return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list), File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 869, in get_or_select_template return self.get_template(template_name_or_list, parent, globals) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 830, in get_template return self._load_template(name, self.make_globals(globals)) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 804, in _load_template template = self.loader.load(self, name, globals) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\loaders.py", line 125, in load code = environment.compile(source, name, filename) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 591, in compile self.handle_exception(exc_info, source_hint=source_hint) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 780, in handle_exception reraise(exc_type, exc_value, tb) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\_compat.py", line 37, in reraise raise value.with_traceback(tb) File "C:\Users\casus\PycharmProjects\juckquotes_01\app\templates\email\reset_password.txt", line 5, in template {{ url_for('reset_password', token=token, _external=True }} File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\environment.py", line 497, in _parse return Parser(self, source, name, encode_filename(filename)).parse() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 901, in parse result = nodes.Template(self.subparse(), lineno=1) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 875, in subparse add_data(self.parse_tuple(with_condexpr=True)) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 620, in parse_tuple args.append(parse()) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 432, in parse_expression return self.parse_condexpr() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 437, in parse_condexpr expr1 = self.parse_or() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 450, in parse_or left = self.parse_and() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 459, in parse_and left = self.parse_not() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 470, in parse_not return self.parse_compare() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 474, in parse_compare expr = self.parse_math1() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 496, in parse_math1 left = self.parse_concat() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 507, in parse_concat args = [self.parse_math2()] File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 517, in parse_math2 left = self.parse_pow() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 528, in parse_pow left = self.parse_unary() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 547, in parse_unary node = self.parse_postfix(node) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 676, in parse_postfix node = self.parse_call(node) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 785, in parse_call value = self.parse_expression() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 432, in parse_expression return self.parse_condexpr() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 437, in parse_condexpr expr1 = self.parse_or() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 450, in parse_or left = self.parse_and() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 459, in parse_and left = self.parse_not() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 470, in parse_not return self.parse_compare() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 474, in parse_compare expr = self.parse_math1() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 496, in parse_math1 left = self.parse_concat() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 507, in parse_concat args = [self.parse_math2()] File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 517, in parse_math2 left = self.parse_pow() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 528, in parse_pow left = self.parse_unary() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 546, in parse_unary node = self.parse_primary() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\parser.py", line 562, in parse_primary next(self.stream) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\lexer.py", line 359, in __next__ self.current = next(self._iter) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\lexer.py", line 562, in wrap for lineno, token, value in stream: File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\jinja2\lexer.py", line 690, in tokeniter filename) jinja2.exceptions.TemplateSyntaxError: unexpected '}', expected ')' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\logging\handlers.py", line 71, in emit if self.shouldRollover(record): File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\logging\handlers.py", line 187, in shouldRollover msg = "%s\n" % self.format(record) File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\logging\__init__.py", line 839, in format return fmt.format(record) File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\logging\__init__.py", line 579, in format s = self.formatMessage(record) File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\logging\__init__.py", line 548, in formatMessage return self._style.format(record) File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\logging\__init__.py", line 391, in format return self._fmt % record.__dict__ ValueError: unsupported format character ':' (0x3a) at index 24 Call stack: File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\threading.py", line 884, in _bootstrap self._bootstrap_inner() File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\threading.py", line 916, in _bootstrap_inner self.run() File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\threading.py", line 864, in run self._target(*self._args, **self._kwargs) File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\socketserver.py", line 651, in process_request_thread self.finish_request(request, client_address) File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\socketserver.py", line 361, in finish_request self.RequestHandlerClass(request, client_address, self) File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\socketserver.py", line 721, in __init__ self.handle() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\werkzeug\serving.py", line 293, in handle rv = BaseHTTPRequestHandler.handle(self) File "C:\Users\casus\AppData\Local\Programs\Python\Python36\lib\http\server.py", line 418, in handle self.handle_one_request() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\werkzeug\serving.py", line 328, in handle_one_request return self.run_wsgi() File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\werkzeug\serving.py", line 270, in run_wsgi execute(self.server.app) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\werkzeug\serving.py", line 258, in execute application_iter = app(environ, start_response) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\cli.py", line 324, in __call__ return self._app(environ, start_response) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 2309, in __call__ return self.wsgi_app(environ, start_response) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 2295, in wsgi_app response = self.handle_exception(e) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 1745, in handle_exception self.log_exception((exc_type, exc_value, tb)) File "C:\Users\casus\.virtualenvs\juckquotes_01\lib\site-packages\flask\app.py", line 1761, in log_exception ), exc_info=exc_info) Message: 'Exception on /reset_password_request [POST]' Arguments: () 127.0.0.1 - - [10/Jul/2018 11:14:41] "POST /reset_password_request HTTP/1.1" 500 - the code for this route is looking like: @app.route('/reset_password_request', methods=['GET', 'POST']) def reset_password_request(): if current_user.is_authenticated: return redirect(url_for('index')) form = ResetPasswordRequestForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() if user: send_password_reset_email(user) flash('Check your email for the instructions to reset your password') return redirect(url_for('login')) return render_template('reset_password_request.html', title='Reset Password', form=form) Thanks in advance.

  • #48 Miguel Grinberg said 2018-07-11T04:51:58Z

    @Fiodor: you have a "}" instead of a ")" in one of the email templates that you are using on this route. Sometimes it is tricky to find the error when you get such a long stack trace, but when you do, the problem is pretty clear: jinja2.exceptions.TemplateSyntaxError: unexpected '}', expected ')'

  • #49 Alex said 2018-07-16T21:02:06Z

    How would we access the <head> tag after we've changed base.html to inherit from bootstrap/base.html? For example, I want to include a favicon by placing this code in my <head> section: <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">.

  • #50 Miguel Grinberg said 2018-07-17T05:59:01Z

    @Alex: there is a "head" block that you can use, but remember to add a super() call so that you don't override the definitions added by Flask-Bootstrap itself in this block.

Leave a Comment