├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── clean.sh
├── create.sh
├── manage.py
├── project
├── __init__.py
├── config.py
├── config
│ ├── .gitignore
│ ├── README.md
│ └── production.cfg.sample
├── decorators.py
├── email.py
├── main
│ ├── __init__.py
│ └── views.py
├── models.py
├── static
│ ├── main.css
│ └── main.js
├── templates
│ ├── _base.html
│ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── main
│ │ └── index.html
│ ├── navigation.html
│ └── user
│ │ ├── activate.html
│ │ ├── forgot.html
│ │ ├── forgot_new.html
│ │ ├── login.html
│ │ ├── profile.html
│ │ ├── register.html
│ │ ├── reset.html
│ │ └── unconfirmed.html
├── token.py
├── user
│ ├── __init__.py
│ ├── forms.py
│ └── views.py
└── util.py
├── readme.md
├── requirements.txt
└── tests
├── __init__.py
├── test_config.py
├── test_main.py
└── test_user.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.sqlite
3 | .DS_Store
4 | .coverage
5 | env
6 | venv
7 | migrations
8 | tmp
9 | env.sh
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "2.7"
4 | - "3.4"
5 | install:
6 | - pip install -r requirements.txt
7 | script:
8 | python manage.py test
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | How to contribute
2 | -----------------
3 |
4 | ## env.sh
5 |
6 | You can create a file `env.sh` with the settings needed to setup the needed development (or testing) environment variables.
7 |
8 | ```sh
9 | #!/usr/bin/env bash
10 |
11 | function app_clean_env() {
12 | unset APP_MAIL_SERVER
13 | unset APP_MAIL_PORT
14 | unset APP_MAIL_USE_TLS
15 | unset APP_MAIL_USE_SSL
16 | unset APP_MAIL_USERNAME
17 | unset APP_MAIL_PASSWORD
18 | unset APP_MAIL_DEFAULT_SENDER
19 | unset APP_SQLALCHEMY_DATABASE_URI
20 | }
21 |
22 | app_clean_env
23 |
24 | if [[ "$APP_SETTINGS" == 'project.config.DevelopmentConfig' ]]; then
25 | echo "Apply DevelopmentConfig settings"
26 |
27 | # mail setting
28 | export APP_MAIL_SERVER=''
29 | export APP_MAIL_PORT=''
30 | export APP_MAIL_USE_TLS=''
31 | export APP_MAIL_USE_SSL=''
32 |
33 | # mail authentication
34 | export APP_MAIL_USERNAME=''
35 | export APP_MAIL_PASSWORD=''
36 |
37 | elif [[ "$APP_SETTINGS" == 'project.config.TestingConfig' ]]; then
38 | echo "Apply TestingConfig settings"
39 | # put your testing settings here
40 |
41 | elif [[ "$APP_SETTINGS" == 'project.config.ProductionConfig' ]]; then
42 | echo "Apply ProductionConfig settings"
43 | # put your production settings here
44 |
45 | else
46 | (>&2 echo "Unrecognized setting")
47 | fi
48 | ```
49 |
50 | ## Sending and debugging emails
51 |
52 | To send email for development, testing and production you can use your own hosted SMTP server or other solutions. Here's two examples:
53 |
54 | * [Mailgun](https://mailgun.com)
55 | You can use Mailgun if you want to send emails in production.
56 |
57 | * [Debug Mail](https://mailgun.com)
58 | Debug Mail is a tool to debut email, you can use it for developing testing.
59 |
60 | Both services offer free plans that you can use for developing and testing `flask-registration`.
61 |
62 | ## Develop locally
63 |
64 | You can use the scripts `clean.sh` and `create.sh` to setup the environment.
65 |
66 | You need to set up the `APP_SETTINGS` variable:
67 |
68 | ```sh
69 | export APP_SETTINGS=project.config.DevelopmentConfig
70 | ```
71 |
72 | To run the development server:
73 | ```bash
74 | ./clean.sh; source env.sh && ./create.sh && python manage.py runserver
75 | ```
76 |
77 | To run the tests:
78 | ```bash
79 | ./clean.sh; source env.sh && python manage.py test
80 | ```
81 |
82 | ## Use Travis to run tests
83 |
84 | 1. Fork the repository
85 | 2. if you don't have an account on [Travis](http://travis-ci.org/), create it (it's free)
86 | 3. Add your `flask-registration` repository on the Travis settings page.
87 | 4. In Travis, click of the cog icon next to the repository and add the following [environment variables](https://docs.travis-ci.com/user/environment-variables/#Defining-Variables-in-Repository-Settings):
88 |
89 | ```
90 | APP_SETTINGS: project.config.TestingConfig
91 | APP_MAIL_SERVER:
92 | APP_MAIL_PORT:
93 | APP_MAIL_USERNAME:
94 | APP_MAIL_PASSWORD:
95 | ```
96 |
97 | You can use any valid SMTP credentials, the defaults are the following:
98 |
99 | ```
100 | MAIL_SERVER = 'smtp.googlemail.com'
101 | MAIL_PORT = 465
102 | MAIL_USE_TLS = False
103 | MAIL_USE_SSL = True
104 | ```
105 |
106 | When you commit to your repository, travis will automatically build the application with the testing settings (see the file `.travis.yml`.
107 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Real Python
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/clean.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | function clean_env() {
4 | unset APP_MAIL_SERVER
5 | unset APP_MAIL_PORT
6 | unset APP_MAIL_USE_TLS
7 | unset APP_MAIL_USE_SSL
8 | unset APP_MAIL_USERNAME
9 | unset APP_MAIL_PASSWORD
10 | unset APP_MAIL_DEFAULT_SENDER
11 | unset APP_SQLALCHEMY_DATABASE_URI
12 | }
13 |
14 | clean_env
15 |
16 | rm -rf migrations
17 | rm -rf tmp
18 | rm -f project/dev.sqlite
19 |
--------------------------------------------------------------------------------
/create.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | python manage.py create_db
4 | python manage.py db init
5 | python manage.py db migrate
6 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | # manage.py
2 |
3 |
4 | import os
5 | import unittest
6 | import coverage
7 | import datetime
8 |
9 | from flask_script import Manager
10 | from flask_migrate import Migrate, MigrateCommand
11 |
12 | COV = coverage.coverage(
13 | branch=True,
14 | include='project/*',
15 | omit=['*/__init__.py', '*/config/*']
16 | )
17 | COV.start()
18 |
19 | from project import app, db
20 | from project.models import User
21 |
22 | app.config.from_object(os.environ['APP_SETTINGS'])
23 |
24 | migrate = Migrate(app, db)
25 | manager = Manager(app)
26 |
27 | # migrations
28 | manager.add_command('db', MigrateCommand)
29 |
30 |
31 | @manager.command
32 | def test():
33 | """Runs the unit tests without coverage."""
34 | tests = unittest.TestLoader().discover('tests')
35 | result = unittest.TextTestRunner(verbosity=2).run(tests)
36 | if result.wasSuccessful():
37 | return 0
38 | else:
39 | return 1
40 |
41 |
42 | @manager.command
43 | def cov():
44 | """Runs the unit tests with coverage."""
45 | tests = unittest.TestLoader().discover('tests')
46 | unittest.TextTestRunner(verbosity=2).run(tests)
47 | COV.stop()
48 | COV.save()
49 | print('Coverage Summary:')
50 | COV.report()
51 | basedir = os.path.abspath(os.path.dirname(__file__))
52 | covdir = os.path.join(basedir, 'tmp/coverage')
53 | COV.html_report(directory=covdir)
54 | print('HTML version: file://%s/index.html' % covdir)
55 | COV.erase()
56 |
57 |
58 | @manager.command
59 | def create_db():
60 | """Creates the db tables."""
61 | db.create_all()
62 |
63 |
64 | @manager.command
65 | def drop_db():
66 | """Drops the db tables."""
67 | db.drop_all()
68 |
69 |
70 | @manager.command
71 | def create_admin():
72 | """Creates the admin user."""
73 | db.session.add(User(
74 | email="ad@min.com",
75 | password="admin",
76 | admin=True,
77 | confirmed=True,
78 | confirmed_on=datetime.datetime.now())
79 | )
80 | db.session.commit()
81 |
82 |
83 | if __name__ == '__main__':
84 | manager.run()
85 |
--------------------------------------------------------------------------------
/project/__init__.py:
--------------------------------------------------------------------------------
1 | # project/__init__.py
2 |
3 |
4 | #################
5 | #### imports ####
6 | #################
7 |
8 | import os
9 |
10 | from flask import Flask, render_template
11 | from flask.ext.login import LoginManager
12 | from flask.ext.bcrypt import Bcrypt
13 | from flask_mail import Mail
14 | from flask.ext.debugtoolbar import DebugToolbarExtension
15 | from flask.ext.sqlalchemy import SQLAlchemy
16 |
17 |
18 | ################
19 | #### config ####
20 | ################
21 |
22 | def _check_config_variables_are_set(config):
23 | assert config['MAIL_USERNAME'] is not None,\
24 | 'MAIL_USERNAME is not set, set the env variable APP_MAIL_USERNAME '\
25 | 'or MAIL_USERNAME in the production config file.'
26 | assert config['MAIL_PASSWORD'] is not None,\
27 | 'MAIL_PASSWORD is not set, set the env variable APP_MAIL_PASSWORD '\
28 | 'or MAIL_PASSWORD in the production config file.'
29 |
30 | assert config['SECRET_KEY'] is not None,\
31 | 'SECRET_KEY is not set, set it in the production config file.'
32 | assert config['SECURITY_PASSWORD_SALT'] is not None,\
33 | 'SECURITY_PASSWORD_SALT is not set, '\
34 | 'set it in the production config file.'
35 |
36 | assert config['SQLALCHEMY_DATABASE_URI'] is not None,\
37 | 'SQLALCHEMY_DATABASE_URI is not set, '\
38 | 'set it in the production config file.'
39 |
40 | if os.environ['APP_SETTINGS'] == 'project.config.ProductionConfig':
41 | assert config['STRIPE_SECRET_KEY'] is not None,\
42 | 'STRIPE_SECRET_KEY is not set, '\
43 | 'set it in the production config file.'
44 | assert config['STRIPE_PUBLISHABLE_KEY'] is not None,\
45 | 'STRIPE_PUBLISHABLE_KEY is not set, '\
46 | 'set it in the production config file.'
47 |
48 |
49 | app = Flask(__name__)
50 |
51 | app.config.from_object(os.environ['APP_SETTINGS'])
52 |
53 | _check_config_variables_are_set(app.config)
54 |
55 | ####################
56 | #### extensions ####
57 | ####################
58 |
59 | login_manager = LoginManager()
60 | login_manager.init_app(app)
61 | bcrypt = Bcrypt(app)
62 | mail = Mail(app)
63 | toolbar = DebugToolbarExtension(app)
64 | db = SQLAlchemy(app)
65 |
66 |
67 | ####################
68 | #### blueprints ####
69 | ####################
70 |
71 | from project.main.views import main_blueprint
72 | from project.user.views import user_blueprint
73 | app.register_blueprint(main_blueprint)
74 | app.register_blueprint(user_blueprint)
75 |
76 |
77 | ####################
78 | #### flask-login ####
79 | ####################
80 |
81 | from project.models import User
82 |
83 | login_manager.login_view = "user.login"
84 | login_manager.login_message_category = "danger"
85 |
86 |
87 | @login_manager.user_loader
88 | def load_user(user_id):
89 | return User.query.filter(User.id == int(user_id)).first()
90 |
91 |
92 | ########################
93 | #### error handlers ####
94 | ########################
95 |
96 | @app.errorhandler(403)
97 | def forbidden_page(error):
98 | return render_template("errors/403.html"), 403
99 |
100 |
101 | @app.errorhandler(404)
102 | def page_not_found(error):
103 | return render_template("errors/404.html"), 404
104 |
105 |
106 | @app.errorhandler(500)
107 | def server_error_page(error):
108 | return render_template("errors/500.html"), 500
109 |
--------------------------------------------------------------------------------
/project/config.py:
--------------------------------------------------------------------------------
1 | # project/config.py
2 |
3 | import os
4 | try:
5 | # Python 2.7
6 | import ConfigParser as configparser
7 | except ImportError:
8 | # Python 3
9 | import configparser
10 |
11 | basedir = os.path.abspath(os.path.dirname(__file__))
12 |
13 |
14 | def _get_bool_env_var(varname, default=None):
15 |
16 | value = os.environ.get(varname, default)
17 |
18 | if value is None:
19 | return False
20 | elif isinstance(value, str) and value.lower() == 'false':
21 | return False
22 | elif bool(value) is False:
23 | return False
24 | else:
25 | return bool(value)
26 |
27 |
28 | class BaseConfig(object):
29 | """Base configuration."""
30 |
31 | # main config
32 | SECRET_KEY = 'my_precious'
33 | SECURITY_PASSWORD_SALT = 'my_precious_two'
34 | DEBUG = False
35 | BCRYPT_LOG_ROUNDS = 13
36 | WTF_CSRF_ENABLED = True
37 | DEBUG_TB_ENABLED = False
38 | DEBUG_TB_INTERCEPT_REDIRECTS = False
39 |
40 | # mail settings
41 | # defaults are:
42 | # - MAIL_SERVER = 'smtp.googlemail.com'
43 | # - MAIL_PORT = 465
44 | # - MAIL_USE_TLS = False
45 | # - MAIL_USE_SSL = True
46 | MAIL_SERVER = os.environ.get('APP_MAIL_SERVER', 'smtp.googlemail.com')
47 | MAIL_PORT = int(os.environ.get('APP_MAIL_PORT', 465))
48 | MAIL_USE_TLS = _get_bool_env_var('APP_MAIL_USE_TLS', False)
49 | MAIL_USE_SSL = _get_bool_env_var('APP_MAIL_USE_SSL', True)
50 |
51 | # mail authentication
52 | MAIL_USERNAME = os.environ.get('APP_MAIL_USERNAME', None)
53 | MAIL_PASSWORD = os.environ.get('APP_MAIL_PASSWORD', None)
54 |
55 | # mail accounts
56 | MAIL_DEFAULT_SENDER = 'from@example.com'
57 |
58 |
59 | class DevelopmentConfig(BaseConfig):
60 | """Development configuration."""
61 | DEBUG = True
62 | WTF_CSRF_ENABLED = False
63 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'dev.sqlite')
64 | DEBUG_TB_ENABLED = True
65 |
66 |
67 | class TestingConfig(BaseConfig):
68 | """Testing configuration."""
69 | LOGIN_DISABLED=False
70 | TESTING = True
71 | DEBUG = False
72 | BCRYPT_LOG_ROUNDS = 1
73 | WTF_CSRF_ENABLED = False
74 | DEBUG_TB_ENABLED = False
75 | SQLALCHEMY_DATABASE_URI = 'sqlite://'
76 |
77 |
78 | class ProductionConfig(BaseConfig):
79 | """Production configuration."""
80 | DEBUG = False
81 | DEBUG_TB_ENABLED = False
82 |
83 | SECRET_KEY = None
84 | SECURITY_PASSWORD_SALT = None
85 |
86 | STRIPE_SECRET_KEY = None
87 | STRIPE_PUBLISHABLE_KEY = None
88 |
89 | SQLALCHEMY_DATABASE_URI = None
90 |
91 | # production config takes precedence over env variables
92 |
93 | # production config file at ./project/config/production.cfg
94 | config_path = os.path.join(basedir, 'config', 'production.cfg')
95 |
96 | # if config file exists, read it:
97 | if os.path.isfile(config_path):
98 | config = configparser.ConfigParser()
99 |
100 | with open(config_path) as configfile:
101 | config.readfp(configfile)
102 |
103 | SECRET_KEY = config.get('keys', 'SECRET_KEY')
104 | SECURITY_PASSWORD_SALT = config.get('keys', 'SECRET_KEY')
105 |
106 | # mail settings
107 | MAIL_SERVER = config.get('mail', 'MAIL_SERVER')
108 | MAIL_PORT = config.getint('mail', 'MAIL_PORT')
109 | MAIL_USE_TLS = config.getboolean('mail', 'MAIL_USE_TLS')
110 | MAIL_USE_SSL = config.getboolean('mail', 'MAIL_USE_SSL')
111 |
112 | # mail authentication and sender
113 | MAIL_USERNAME = config.get('mail', 'MAIL_USERNAME')
114 | MAIL_PASSWORD = config.get('mail', 'MAIL_PASSWORD')
115 | MAIL_DEFAULT_SENDER = config.get('mail', 'MAIL_DEFAULT_SENDER')
116 |
117 | # database URI
118 | SQLALCHEMY_DATABASE_URI = config.get('db', 'SQLALCHEMY_DATABASE_URI')
119 |
120 | # stripe keys
121 | STRIPE_SECRET_KEY = config.get('stripe', 'STRIPE_SECRET_KEY')
122 | STRIPE_PUBLISHABLE_KEY = config.get('stripe', 'STRIPE_PUBLISHABLE_KEY')
123 |
--------------------------------------------------------------------------------
/project/config/.gitignore:
--------------------------------------------------------------------------------
1 | *.cfg
2 |
--------------------------------------------------------------------------------
/project/config/README.md:
--------------------------------------------------------------------------------
1 | Config file directory
2 | ---------------------
3 |
4 | You can put here your `production.cfg` for your production settings.
5 | Start from the template in `production.cfg.sample`.
6 |
7 | All files ending with `.cfg` are ignored by Git.
8 |
--------------------------------------------------------------------------------
/project/config/production.cfg.sample:
--------------------------------------------------------------------------------
1 | # This is a sample config file.
2 | # Create from this template a config named:
3 | # production.cfg
4 | #
5 | # for your production settings.
6 | #
7 | # You don't need quotes for strings.
8 | # Lines starting with # are comments and are ignored
9 |
10 | [keys]
11 | SECRET_KEY = my_precious
12 | SECURITY_PASSWORD_SALT = my_precious_two
13 |
14 | [mail]
15 | MAIL_SERVER = smtp.gmail.com
16 | MAIL_PORT = 465
17 | MAIL_USE_TLS = False
18 | MAIL_USE_SSL = True
19 |
20 | # mail authentication
21 | MAIL_USERNAME = youremail@gmail.com
22 | MAIL_PASSWORD = your-gmail-password
23 |
24 | # mail sender
25 | MAIL_DEFAULT_SENDER = dev@example.org
26 |
27 | [db]
28 | # the database URL is specified as follows:
29 | # dialect+driver://username:password@host:port/database
30 | SQLALCHEMY_DATABASE_URI = sqlite://
31 |
32 | [stripe]
33 | STRIPE_SECRET_KEY = foo
34 | STRIPE_PUBLISHABLE_KEY = bar
35 |
--------------------------------------------------------------------------------
/project/decorators.py:
--------------------------------------------------------------------------------
1 | # project/decorators.py
2 |
3 |
4 | from functools import wraps
5 |
6 | from flask import flash, redirect, url_for
7 | from flask.ext.login import current_user
8 |
9 |
10 | def check_confirmed(func):
11 | @wraps(func)
12 | def decorated_function(*args, **kwargs):
13 | if current_user.confirmed is False:
14 | flash('Please confirm your account!', 'warning')
15 | return redirect(url_for('user.unconfirmed'))
16 | return func(*args, **kwargs)
17 |
18 | return decorated_function
19 |
--------------------------------------------------------------------------------
/project/email.py:
--------------------------------------------------------------------------------
1 | # project/email.py
2 |
3 | from flask.ext.mail import Message
4 |
5 | from project import app, mail
6 |
7 |
8 | def send_email(to, subject, template):
9 | msg = Message(
10 | subject,
11 | recipients=[to],
12 | html=template,
13 | sender=app.config['MAIL_DEFAULT_SENDER']
14 | )
15 | mail.send(msg)
16 |
--------------------------------------------------------------------------------
/project/main/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realpython/flask-registration/aeb66980a55465169c7484c1b04a3689db71d100/project/main/__init__.py
--------------------------------------------------------------------------------
/project/main/views.py:
--------------------------------------------------------------------------------
1 | # project/main/views.py
2 |
3 |
4 | #################
5 | #### imports ####
6 | #################
7 |
8 | from flask import render_template
9 | from flask import Blueprint
10 | from flask_login import current_user
11 | from flask_login import login_required
12 |
13 | ################
14 | #### config ####
15 | ################
16 |
17 | main_blueprint = Blueprint('main', __name__,)
18 |
19 |
20 | ################
21 | #### routes ####
22 | ################
23 |
24 | @main_blueprint.route('/')
25 | def home():
26 | return render_template('main/index.html', current_user=current_user)
27 |
--------------------------------------------------------------------------------
/project/models.py:
--------------------------------------------------------------------------------
1 | # project/models.py
2 |
3 |
4 | import datetime
5 |
6 | from project import db, bcrypt
7 |
8 |
9 | class User(db.Model):
10 |
11 | __tablename__ = "users"
12 |
13 | id = db.Column(db.Integer, primary_key=True)
14 | email = db.Column(db.String, unique=True, nullable=False)
15 | password = db.Column(db.String, nullable=False)
16 | registered_on = db.Column(db.DateTime, nullable=False)
17 | admin = db.Column(db.Boolean, nullable=False, default=False)
18 | confirmed = db.Column(db.Boolean, nullable=False, default=False)
19 | confirmed_on = db.Column(db.DateTime, nullable=True)
20 | password_reset_token = db.Column(db.String, nullable=True)
21 |
22 | def __init__(self, email, password, confirmed,
23 | admin=False, confirmed_on=None,
24 | password_reset_token=None):
25 | self.email = email
26 | self.password = bcrypt.generate_password_hash(password)
27 | self.registered_on = datetime.datetime.now()
28 | self.admin = admin
29 | self.confirmed = confirmed
30 | self.confirmed_on = confirmed_on
31 | self.password_reset_token = password_reset_token
32 |
33 | def is_authenticated(self):
34 | return True
35 |
36 | def is_active(self):
37 | return True
38 |
39 | def is_anonymous(self):
40 | return False
41 |
42 | def get_id(self):
43 | return self.id
44 |
45 | def __repr__(self):
46 | return '
2 |
3 |
4 |
5 | Flask User Management
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {% block css %}{% endblock %}
14 |
15 |
16 |
17 | {% include "navigation.html" %}
18 |
19 |
20 |
21 |
22 |
23 |
24 | {% with messages = get_flashed_messages(with_categories=true) %}
25 | {% if messages %}
26 |
27 |
28 | {% for category, message in messages %}
29 |
30 |
×
31 | {{message}}
32 |
33 | {% endfor %}
34 |
35 |
36 | {% endif %}
37 | {% endwith %}
38 |
39 |
40 | {% block content %}{% endblock %}
41 |
42 |
43 |
44 |
45 | {% if error %}
46 |
Error: {{ error }}
47 | {% endif %}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {% block js %}{% endblock %}
56 |
57 |
58 |
--------------------------------------------------------------------------------
/project/templates/errors/403.html:
--------------------------------------------------------------------------------
1 | {% extends "_base.html" %}
2 | {% block content %}
3 | 403
4 | Run along!
5 | Return Home?
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/project/templates/errors/404.html:
--------------------------------------------------------------------------------
1 | {% extends "_base.html" %}
2 | {% block content %}
3 | 404
4 | There's nothing here!
5 | Return Home?
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/project/templates/errors/500.html:
--------------------------------------------------------------------------------
1 | {% extends "_base.html" %}
2 | {% block content %}
3 | 500
4 | Something's wrong! We are on the job.
5 | Return Home?
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/project/templates/main/index.html:
--------------------------------------------------------------------------------
1 | {% extends "_base.html" %}
2 | {% block content %}
3 |
4 | {% if current_user.is_authenticated() %}
5 | Welcome, {{ current_user.email }}
6 | {% else %}
7 | Welcome!
8 | {% endif %}
9 |
10 |
11 | {% if current_user.is_authenticated() %}
12 | You are authenticated
13 |
16 | {% else %}
17 | You are not authenticated:
18 |
23 | {% endif %}
24 |
25 | {% endblock %}
--------------------------------------------------------------------------------
/project/templates/navigation.html:
--------------------------------------------------------------------------------
1 |
2 |
36 |
--------------------------------------------------------------------------------
/project/templates/user/activate.html:
--------------------------------------------------------------------------------
1 | Welcome! Thanks for signing up. Please follow this link to activate your account:
2 | {{ confirm_url }}
3 |
4 | Cheers!
--------------------------------------------------------------------------------
/project/templates/user/forgot.html:
--------------------------------------------------------------------------------
1 | {% extends "_base.html" %}
2 |
3 | {% block content %}
4 |
5 | Forgot Your Password?
6 |
7 |
18 |
19 |
20 |
24 | {% endblock %}
--------------------------------------------------------------------------------
/project/templates/user/forgot_new.html:
--------------------------------------------------------------------------------
1 | {% extends "_base.html" %}
2 |
3 | {% block content %}
4 |
5 | Forgot Your Password?
6 |
7 |
32 |
33 | {% endblock %}
--------------------------------------------------------------------------------
/project/templates/user/login.html:
--------------------------------------------------------------------------------
1 | {% extends "_base.html" %}
2 |
3 | {% block content %}
4 |
5 | Please login
6 |
7 |
36 |
37 | {% endblock %}
--------------------------------------------------------------------------------
/project/templates/user/profile.html:
--------------------------------------------------------------------------------
1 | {% extends "_base.html" %}
2 |
3 | {% block content %}
4 |
5 | Your Profile
6 |
7 |
8 | {% if current_user.is_authenticated() %}
9 | Email: {{current_user.email}}
10 | {% endif %}
11 |
12 | Change Password
13 |
14 |
38 |
39 |
40 | {% endblock %}
--------------------------------------------------------------------------------
/project/templates/user/register.html:
--------------------------------------------------------------------------------
1 | {% extends "_base.html" %}
2 |
3 | {% block content %}
4 |
5 | Please Register
6 |
7 |
42 |
43 | {% endblock %}
--------------------------------------------------------------------------------
/project/templates/user/reset.html:
--------------------------------------------------------------------------------
1 | Hi! Somobody requested a password reset for the account {{ username }}.
2 | Click on the following link to reset your password:
3 | {{ reset_url }}
4 | If you didn't request it, you can ignore this email.
5 |
6 | Cheers!
--------------------------------------------------------------------------------
/project/templates/user/unconfirmed.html:
--------------------------------------------------------------------------------
1 | {% extends "_base.html" %}
2 |
3 | {% block content %}
4 |
5 | Welcome!
6 |
7 | You have not confirmed your account. Please check your inbox (and your spam folder) - you should have received an email with a confirmation link.
8 | Didn't get the email? Resend.
9 |
10 | {% endblock %}
--------------------------------------------------------------------------------
/project/token.py:
--------------------------------------------------------------------------------
1 | # project/token.py
2 |
3 | from itsdangerous import URLSafeTimedSerializer
4 |
5 | from project import app
6 |
7 |
8 | def generate_confirmation_token(email):
9 | serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
10 | return serializer.dumps(email, salt=app.config['SECURITY_PASSWORD_SALT'])
11 |
12 |
13 | def confirm_token(token, expiration=3600):
14 | serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
15 | try:
16 | email = serializer.loads(
17 | token,
18 | salt=app.config['SECURITY_PASSWORD_SALT'],
19 | max_age=expiration
20 | )
21 | except:
22 | return False
23 | return email
24 |
--------------------------------------------------------------------------------
/project/user/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realpython/flask-registration/aeb66980a55465169c7484c1b04a3689db71d100/project/user/__init__.py
--------------------------------------------------------------------------------
/project/user/forms.py:
--------------------------------------------------------------------------------
1 | # project/user/forms.py
2 |
3 |
4 | from flask_wtf import Form
5 | from wtforms import TextField, PasswordField
6 | from wtforms.validators import DataRequired, Email, Length, EqualTo
7 |
8 | from project.models import User
9 |
10 |
11 | class LoginForm(Form):
12 | email = TextField('email', validators=[DataRequired(), Email()])
13 | password = PasswordField('password', validators=[DataRequired()])
14 |
15 |
16 | class RegisterForm(Form):
17 | email = TextField(
18 | 'email',
19 | validators=[DataRequired(), Email(message=None), Length(min=6, max=255)])
20 | password = PasswordField(
21 | 'password',
22 | validators=[DataRequired(), Length(min=6, max=255)]
23 | )
24 | confirm = PasswordField(
25 | 'Repeat password',
26 | validators=[
27 | DataRequired(),
28 | EqualTo('password', message='Passwords must match.')
29 | ]
30 | )
31 |
32 | def validate(self):
33 | initial_validation = super(RegisterForm, self).validate()
34 | if not initial_validation:
35 | return False
36 | user = User.query.filter_by(email=self.email.data).first()
37 | if user:
38 | self.email.errors.append("Email already registered")
39 | return False
40 | return True
41 |
42 |
43 | class ForgotForm(Form):
44 | email = TextField(
45 | 'email',
46 | validators=[DataRequired(), Email(message=None), Length(min=6, max=255)])
47 |
48 | def validate(self):
49 | initial_validation = super(ForgotForm, self).validate()
50 | if not initial_validation:
51 | return False
52 | user = User.query.filter_by(email=self.email.data).first()
53 | if not user:
54 | self.email.errors.append("This email is not registered")
55 | return False
56 | return True
57 |
58 |
59 | class ChangePasswordForm(Form):
60 | password = PasswordField(
61 | 'password',
62 | validators=[DataRequired(), Length(min=6, max=255)]
63 | )
64 | confirm = PasswordField(
65 | 'Repeat password',
66 | validators=[
67 | DataRequired(),
68 | EqualTo('password', message='Passwords must match.')
69 | ]
70 | )
71 |
--------------------------------------------------------------------------------
/project/user/views.py:
--------------------------------------------------------------------------------
1 | # project/user/views.py
2 |
3 |
4 | #################
5 | #### imports ####
6 | #################
7 |
8 | import datetime
9 |
10 | from flask import render_template, Blueprint, url_for, \
11 | redirect, flash, request
12 | from flask_login import login_user, logout_user, \
13 | login_required, current_user
14 |
15 | from project.models import User
16 | from project.email import send_email
17 | from project.token import generate_confirmation_token, confirm_token
18 | from project.decorators import check_confirmed
19 | from project import db, bcrypt
20 | from .forms import LoginForm, RegisterForm, ChangePasswordForm, ForgotForm
21 |
22 |
23 | ################
24 | #### config ####
25 | ################
26 |
27 | user_blueprint = Blueprint('user', __name__,)
28 |
29 |
30 | ################
31 | #### routes ####
32 | ################
33 |
34 | @user_blueprint.route('/register', methods=['GET', 'POST'])
35 | def register():
36 | form = RegisterForm(request.form)
37 | if form.validate_on_submit():
38 | user = User(
39 | email=form.email.data,
40 | password=form.password.data,
41 | confirmed=False
42 | )
43 | db.session.add(user)
44 | db.session.commit()
45 |
46 | token = generate_confirmation_token(user.email)
47 | confirm_url = url_for('user.confirm_email', token=token, _external=True)
48 | html = render_template('user/activate.html', confirm_url=confirm_url)
49 | subject = "Please confirm your email"
50 | send_email(user.email, subject, html)
51 |
52 | login_user(user)
53 |
54 | flash('A confirmation email has been sent via email.', 'success')
55 | return redirect(url_for("user.unconfirmed"))
56 |
57 | return render_template('user/register.html', form=form)
58 |
59 |
60 | @user_blueprint.route('/login', methods=['GET', 'POST'])
61 | def login():
62 | form = LoginForm(request.form)
63 | if form.validate_on_submit():
64 | user = User.query.filter_by(email=form.email.data).first()
65 | if user and bcrypt.check_password_hash(
66 | user.password, request.form['password']):
67 | login_user(user)
68 | flash('Welcome.', 'success')
69 | return redirect(url_for('main.home'))
70 | else:
71 | flash('Invalid email and/or password.', 'danger')
72 | return render_template('user/login.html', form=form)
73 | return render_template('user/login.html', form=form)
74 |
75 |
76 | @user_blueprint.route('/logout')
77 | @login_required
78 | def logout():
79 | logout_user()
80 | flash('You were logged out.', 'success')
81 | return redirect(url_for('user.login'))
82 |
83 |
84 | @user_blueprint.route('/profile', methods=['GET', 'POST'])
85 | @login_required
86 | @check_confirmed
87 | def profile():
88 | form = ChangePasswordForm(request.form)
89 | if form.validate_on_submit():
90 | user = User.query.filter_by(email=current_user.email).first()
91 | if user:
92 | user.password = bcrypt.generate_password_hash(form.password.data)
93 | db.session.commit()
94 | flash('Password successfully changed.', 'success')
95 | return redirect(url_for('user.profile'))
96 | else:
97 | flash('Password change was unsuccessful.', 'danger')
98 | return redirect(url_for('user.profile'))
99 | return render_template('user/profile.html', form=form)
100 |
101 |
102 | @user_blueprint.route('/confirm/')
103 | @login_required
104 | def confirm_email(token):
105 | if current_user.confirmed:
106 | flash('Account already confirmed. Please login.', 'success')
107 | return redirect(url_for('main.home'))
108 | email = confirm_token(token)
109 | user = User.query.filter_by(email=current_user.email).first_or_404()
110 | if user.email == email:
111 | user.confirmed = True
112 | user.confirmed_on = datetime.datetime.now()
113 | db.session.add(user)
114 | db.session.commit()
115 | flash('You have confirmed your account. Thanks!', 'success')
116 | else:
117 | flash('The confirmation link is invalid or has expired.', 'danger')
118 | return redirect(url_for('main.home'))
119 |
120 |
121 | @user_blueprint.route('/unconfirmed')
122 | @login_required
123 | def unconfirmed():
124 | if current_user.confirmed:
125 | return redirect(url_for('main.home'))
126 | flash('Please confirm your account!', 'warning')
127 | return render_template('user/unconfirmed.html')
128 |
129 |
130 | @user_blueprint.route('/resend')
131 | @login_required
132 | def resend_confirmation():
133 | token = generate_confirmation_token(current_user.email)
134 | confirm_url = url_for('user.confirm_email', token=token, _external=True)
135 | html = render_template('user/activate.html', confirm_url=confirm_url)
136 | subject = "Please confirm your email"
137 | send_email(current_user.email, subject, html)
138 | flash('A new confirmation email has been sent.', 'success')
139 | return redirect(url_for('user.unconfirmed'))
140 |
141 | @user_blueprint.route('/forgot', methods=['GET', 'POST'])
142 | def forgot():
143 | form = ForgotForm(request.form)
144 | if form.validate_on_submit():
145 |
146 | user = User.query.filter_by(email=form.email.data).first()
147 | token = generate_confirmation_token(user.email)
148 |
149 | user.password_reset_token = token
150 | db.session.commit()
151 |
152 | reset_url = url_for('user.forgot_new', token=token, _external=True)
153 | html = render_template('user/reset.html',
154 | username=user.email,
155 | reset_url=reset_url)
156 | subject = "Reset your password"
157 | send_email(user.email, subject, html)
158 |
159 | flash('A password reset email has been sent via email.', 'success')
160 | return redirect(url_for("main.home"))
161 |
162 | return render_template('user/forgot.html', form=form)
163 |
164 |
165 | @user_blueprint.route('/forgot/new/', methods=['GET', 'POST'])
166 | def forgot_new(token):
167 |
168 | email = confirm_token(token)
169 | user = User.query.filter_by(email=email).first_or_404()
170 |
171 | if user.password_reset_token is not None:
172 | form = ChangePasswordForm(request.form)
173 | if form.validate_on_submit():
174 | user = User.query.filter_by(email=email).first()
175 | if user:
176 | user.password = bcrypt.generate_password_hash(form.password.data)
177 | user.password_reset_token = None
178 | db.session.commit()
179 |
180 | login_user(user)
181 |
182 | flash('Password successfully changed.', 'success')
183 | return redirect(url_for('user.profile'))
184 |
185 | else:
186 | flash('Password change was unsuccessful.', 'danger')
187 | return redirect(url_for('user.profile'))
188 | else:
189 | flash('You can now change your password.', 'success')
190 | return render_template('user/forgot_new.html', form=form)
191 | else:
192 | flash('Can not reset the password, try again.', 'danger')
193 |
194 | return redirect(url_for('main.home'))
195 |
--------------------------------------------------------------------------------
/project/util.py:
--------------------------------------------------------------------------------
1 | # project/util.py
2 |
3 |
4 | from flask.ext.testing import TestCase
5 |
6 | from project import app, db
7 | from project.models import User
8 |
9 |
10 | class BaseTestCase(TestCase):
11 |
12 | def create_app(self):
13 | app.config.from_object('project.config.TestingConfig')
14 | return app
15 |
16 | @classmethod
17 | def setUpClass(self):
18 | db.create_all()
19 | user = User(
20 | email="test@user.com",
21 | password="just_a_test_user",
22 | confirmed=False
23 | )
24 | db.session.add(user)
25 | db.session.commit()
26 |
27 | @classmethod
28 | def tearDownClass(self):
29 | db.session.remove()
30 | db.drop_all()
31 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Flask Registration
2 |
3 | [](https://travis-ci.org/realpython/flask-registration)
4 |
5 | Starter app for managing users - login/logout, registration, and email confirmation.
6 |
7 | **Blog posts:**
8 |
9 | - Part 1: [Handling Email Confirmation During Registration in Flask](https://realpython.com/blog/python/handling-email-confirmation-in-flask)
10 | - Part 2: [The Minimum Viable Test Suite](https://realpython.com/blog/python/the-minimum-viable-test-suite/)
11 |
12 | ## QuickStart
13 |
14 | ### Set Environment Variables
15 |
16 | Development Example (with [Debug Mail](https://debugmail.io)):
17 |
18 | ```sh
19 | $ export APP_SETTINGS="project.config.DevelopmentConfig"
20 | $ export APP_MAIL_SERVER=debugmail.io
21 | $ export APP_MAIL_PORT=25
22 | $ export APP_MAIL_USE_TLS=true
23 | $ export APP_MAIL_USE_SSL=false
24 | $ export APP_MAIL_USERNAME=ADDYOUROWN
25 | $ export APP_MAIL_PASSWORD=ADDYOUROWN
26 | ```
27 |
28 | Production Example:
29 |
30 | ```sh
31 | $ export APP_SETTINGS="project.config.ProductionConfig"
32 | $ export APP_MAIL_SERVER=ADDYOUROWN
33 | $ export APP_MAIL_PORT=ADDYOUROWN
34 | $ export APP_MAIL_USE_TLS=ADDYOUROWN
35 | $ export APP_MAIL_USE_SSL=ADDYOUROWN
36 | $ export APP_MAIL_USERNAME=ADDYOUROWN
37 | $ export APP_MAIL_PASSWORD=ADDYOUROWN
38 | ```
39 |
40 | ### Update Settings in Production
41 |
42 | 1. `SECRET_KEY`
43 | 1. `SQLALCHEMY_DATABASE_URI`
44 |
45 | ### Create DB
46 |
47 | Run:
48 |
49 | ```sh
50 | $ sh create.sh
51 | ```
52 |
53 | Or:
54 |
55 | ```sh
56 | $ python manage.py create_db
57 | $ python manage.py db init
58 | $ python manage.py db migrate
59 | $ python manage.py create_admin
60 | ```
61 |
62 | Want to clean the environment? Run:
63 |
64 | ```sh
65 | sh clean.sh
66 | ```
67 |
68 | ### Run
69 |
70 | ```sh
71 | $ python manage.py runserver
72 | ```
73 |
74 | ### Testing
75 |
76 | Without coverage:
77 |
78 | ```sh
79 | $ python manage.py test
80 | ```
81 |
82 | With coverage:
83 |
84 | ```sh
85 | $ python manage.py cov
86 | ```
87 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==0.10.1
2 | Flask-Bcrypt==0.6.0
3 | Flask-DebugToolbar==0.9.0
4 | Flask-Login==0.2.11
5 | Flask-Mail==0.9.1
6 | Flask-Migrate==1.2.0
7 | Flask-SQLAlchemy==2.0
8 | Flask-Script==2.0.5
9 | Flask-Testing==0.4.2
10 | Flask-WTF==0.10.2
11 | Jinja2==2.7.3
12 | Mako==1.0.0
13 | MarkupSafe==0.23
14 | SQLAlchemy==0.9.8
15 | WTForms==2.0.1
16 | Werkzeug==0.9.6
17 | alembic==0.6.7
18 | blinker==1.3
19 | coverage==4.0a1
20 | ecdsa==0.11
21 | httplib2==0.9
22 | itsdangerous==0.24
23 | paramiko==1.15.1
24 | psycopg2==2.5.4
25 | py-bcrypt==0.4
26 | pycrypto==2.6.1
27 | requests==2.6.2
28 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/realpython/flask-registration/aeb66980a55465169c7484c1b04a3689db71d100/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | # tests/test_config.py
2 |
3 |
4 | import unittest
5 |
6 | from flask import current_app
7 | from flask_testing import TestCase
8 |
9 | from project import app
10 |
11 |
12 | class TestDevelopmentConfig(TestCase):
13 |
14 | def create_app(self):
15 | app.config.from_object('project.config.DevelopmentConfig')
16 | return app
17 |
18 | def test_app_is_development(self):
19 | self.assertTrue(app.config['DEBUG'] is True)
20 | self.assertTrue(app.config['WTF_CSRF_ENABLED'] is False)
21 | self.assertTrue(app.config['DEBUG_TB_ENABLED'] is True)
22 |
23 |
24 | class TestTestingConfig(TestCase):
25 |
26 | def create_app(self):
27 | app.config.from_object('project.config.TestingConfig')
28 | return app
29 |
30 | def test_app_is_testing(self):
31 | self.assertTrue(current_app.config['TESTING'])
32 | self.assertTrue(app.config['DEBUG'] is False)
33 | self.assertTrue(app.config['BCRYPT_LOG_ROUNDS'] == 1)
34 | self.assertTrue(app.config['WTF_CSRF_ENABLED'] is False)
35 |
36 |
37 | class TestProductionConfig(TestCase):
38 |
39 | def create_app(self):
40 | app.config.from_object('project.config.ProductionConfig')
41 | return app
42 |
43 | def test_app_is_production(self):
44 | self.assertTrue(app.config['DEBUG'] is False)
45 | self.assertTrue(app.config['DEBUG_TB_ENABLED'] is False)
46 | self.assertTrue(app.config['WTF_CSRF_ENABLED'] is True)
47 | self.assertTrue(app.config['BCRYPT_LOG_ROUNDS'] == 13)
48 |
49 |
50 | if __name__ == '__main__':
51 | unittest.main()
52 |
--------------------------------------------------------------------------------
/tests/test_main.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from project.util import BaseTestCase
4 |
5 |
6 | class TestMainViews(BaseTestCase):
7 |
8 | def test_main_route_does_not_require_login(self):
9 | # Ensure main route requres a logged in user.
10 | response = self.client.get('/', follow_redirects=True)
11 | self.assertTrue(response.status_code == 200)
12 | self.assertTemplateUsed('main/index.html')
13 |
14 |
15 | if __name__ == '__main__':
16 | unittest.main()
17 |
--------------------------------------------------------------------------------
/tests/test_user.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import unittest
3 |
4 | from flask_login import current_user
5 |
6 | from project import db
7 | from project.models import User
8 | from project.util import BaseTestCase
9 | from project.user.forms import RegisterForm, \
10 | LoginForm, ChangePasswordForm, ForgotForm
11 | from project.token import generate_confirmation_token, confirm_token
12 |
13 |
14 | class TestUserForms(BaseTestCase):
15 |
16 | def test_validate_success_register_form(self):
17 | # Ensure correct data validates.
18 | form = RegisterForm(
19 | email='new@test.test',
20 | password='example', confirm='example')
21 | self.assertTrue(form.validate())
22 |
23 | def test_validate_invalid_password_format(self):
24 | # Ensure incorrect data does not validate.
25 | form = RegisterForm(
26 | email='new@test.test',
27 | password='example', confirm='')
28 | self.assertFalse(form.validate())
29 |
30 | def test_validate_email_already_registered(self):
31 | # Ensure user can't register when a duplicate email is used
32 | form = RegisterForm(
33 | email='test@user.com',
34 | password='just_a_test_user',
35 | confirm='just_a_test_user'
36 | )
37 | self.assertFalse(form.validate())
38 |
39 | def test_validate_success_login_form(self):
40 | # Ensure correct data validates.
41 | form = LoginForm(email='test@user.com', password='just_a_test_user')
42 | self.assertTrue(form.validate())
43 |
44 | def test_validate_invalid_email_format(self):
45 | # Ensure invalid email format throws error.
46 | form = LoginForm(email='unknown', password='example')
47 | self.assertFalse(form.validate())
48 |
49 | def test_validate_success_change_password_form(self):
50 | # Ensure correct data validates.
51 | form = ChangePasswordForm(password='update', confirm='update')
52 | self.assertTrue(form.validate())
53 |
54 | def test_validate_invalid_change_password(self):
55 | # Ensure passwords must match.
56 | form = ChangePasswordForm(password='update', confirm='unknown')
57 | self.assertFalse(form.validate())
58 |
59 | def test_validate_invalid_change_password_format(self):
60 | # Ensure invalid email format throws error.
61 | form = ChangePasswordForm(password='123', confirm='123')
62 | self.assertFalse(form.validate())
63 |
64 | def test_validate_success_forgot_password(self):
65 | # Ensure invalid email format throws error.
66 | form = ForgotForm(email='test@user.com')
67 | self.assertTrue(form.validate())
68 |
69 | def test_validate_invalid_forgot_password_format(self):
70 | # Ensure invalid email format throws error.
71 | form = ForgotForm(email='unknown')
72 | self.assertFalse(form.validate())
73 |
74 | def test_validate_invalid_forgot_password_no_such_user(self):
75 | # Ensure invalid email format throws error.
76 | form = ForgotForm(email='not@correct.com')
77 | self.assertFalse(form.validate())
78 |
79 |
80 | class TestUserViews(BaseTestCase):
81 |
82 | def test_correct_login(self):
83 | # Ensure login behaves correctly with correct credentials.
84 | with self.client:
85 | response = self.client.post(
86 | '/login',
87 | data=dict(email="test@user.com", password="just_a_test_user"),
88 | follow_redirects=True
89 | )
90 | self.assertTrue(response.status_code == 200)
91 | self.assertTrue(current_user.email == "test@user.com")
92 | self.assertTrue(current_user.is_active())
93 | self.assertTrue(current_user.is_authenticated())
94 | self.assertTemplateUsed('main/index.html')
95 |
96 | def test_incorrect_login(self):
97 | # Ensure login behaves correctly with incorrect credentials.
98 | with self.client:
99 | response = self.client.post(
100 | '/login',
101 | data=dict(email="not@correct.com", password="incorrect"),
102 | follow_redirects=True
103 | )
104 | self.assertTrue(response.status_code == 200)
105 | self.assertIn(b'Invalid email and/or password.', response.data)
106 | self.assertFalse(current_user.is_active())
107 | self.assertFalse(current_user.is_authenticated())
108 | self.assertTemplateUsed('user/login.html')
109 |
110 | def test_profile_route_requires_login(self):
111 | # Ensure profile route requires logged in user.
112 | self.client.get('/profile', follow_redirects=True)
113 | self.assertTemplateUsed('user/login.html')
114 |
115 | def test_confirm_token_route_requires_login(self):
116 | # Ensure confirm/ route requires logged in user.
117 | self.client.get('/confirm/blah', follow_redirects=True)
118 | self.assertTemplateUsed('user/login.html')
119 |
120 | def test_confirm_token_route_valid_token(self):
121 | # Ensure user can confirm account with valid token.
122 | with self.client:
123 | self.client.post('/login', data=dict(
124 | email='test@user.com', password='just_a_test_user'
125 | ), follow_redirects=True)
126 | token = generate_confirmation_token('test@user.com')
127 | response = self.client.get(
128 | '/confirm/'+token, follow_redirects=True)
129 | self.assertIn(
130 | b'You have confirmed your account. Thanks!', response.data)
131 | self.assertTemplateUsed('main/index.html')
132 | user = User.query.filter_by(email='test@user.com').first_or_404()
133 | self.assertIsInstance(user.confirmed_on, datetime.datetime)
134 | self.assertTrue(user.confirmed)
135 |
136 | def test_confirm_token_route_invalid_token(self):
137 | # Ensure user cannot confirm account with invalid token.
138 | token = generate_confirmation_token('test@test1.com')
139 | with self.client:
140 | self.client.post('/login', data=dict(
141 | email='test@user.com', password='just_a_test_user'
142 | ), follow_redirects=True)
143 | response = self.client.get('/confirm/'+token,
144 | follow_redirects=True)
145 | self.assertIn(
146 | b'The confirmation link is invalid or has expired.',
147 | response.data
148 | )
149 |
150 | def test_confirm_token_route_expired_token(self):
151 | # Ensure user cannot confirm account with expired token.
152 | user = User(email='test@test1.com', password='test1', confirmed=False)
153 | db.session.add(user)
154 | db.session.commit()
155 | token = generate_confirmation_token('test@test1.com')
156 | self.assertFalse(confirm_token(token, -1))
157 |
158 | def test_forgot_password_does_not_require_login(self):
159 | # Ensure user can request new password without login.
160 | self.client.get('/forgot', follow_redirects=True)
161 | self.assertTemplateUsed('user/forgot.html')
162 |
163 | def test_correct_forgot_password_request(self):
164 | # Ensure login behaves correctly with correct credentials.
165 | with self.client:
166 | response = self.client.post(
167 | '/forgot',
168 | data=dict(email="test@user.com"),
169 | follow_redirects=True
170 | )
171 | self.assertTrue(response.status_code == 200)
172 | self.assertTemplateUsed('main/index.html')
173 |
174 | def test_reset_forgotten_password_valid_token(self):
175 | # Ensure user can confirm account with valid token.
176 | with self.client:
177 | self.client.post('/forgot', data=dict(
178 | email='test@user.com',
179 | ), follow_redirects=True)
180 | token = generate_confirmation_token('test@user.com')
181 | response = self.client.get('/forgot/new/'+token, follow_redirects=True)
182 | self.assertTemplateUsed('user/forgot_new.html')
183 | self.assertIn(
184 | b'You can now change your password.',
185 | response.data
186 | )
187 | self.assertFalse(current_user.is_authenticated())
188 |
189 | def test_reset_forgotten_password_valid_token_correct_login(self):
190 | # Ensure user can confirm account with valid token.
191 | with self.client:
192 | self.client.post('/forgot', data=dict(
193 | email='test@user.com',
194 | ), follow_redirects=True)
195 | token = generate_confirmation_token('test@user.com')
196 | response = self.client.get('/forgot/new/'+token, follow_redirects=True)
197 | self.assertTemplateUsed('user/forgot_new.html')
198 | self.assertIn(
199 | b'You can now change your password.',
200 | response.data
201 | )
202 | response = self.client.post(
203 | '/forgot/new/'+token,
204 | data=dict(password="new-password", confirm="new-password"),
205 | follow_redirects=True
206 | )
207 | self.assertIn(
208 | b'Password successfully changed.',
209 | response.data
210 | )
211 | self.assertTemplateUsed('user/profile.html')
212 | self.assertTrue(current_user.is_authenticated())
213 | self.client.get('/logout')
214 | self.assertFalse(current_user.is_authenticated())
215 |
216 | response = self.client.post(
217 | '/login',
218 | data=dict(email="test@user.com", password="new-password"),
219 | follow_redirects=True
220 | )
221 | self.assertTrue(response.status_code == 200)
222 | self.assertTrue(current_user.email == "test@user.com")
223 | self.assertTrue(current_user.is_active())
224 | self.assertTrue(current_user.is_authenticated())
225 | self.assertTemplateUsed('main/index.html')
226 |
227 | def test_reset_forgotten_password_valid_token_invalid_login(self):
228 | # Ensure user can confirm account with valid token.
229 | with self.client:
230 | self.client.post('/forgot', data=dict(
231 | email='test@user.com',
232 | ), follow_redirects=True)
233 | token = generate_confirmation_token('test@user.com')
234 | response = self.client.get('/forgot/new/'+token, follow_redirects=True)
235 | self.assertTemplateUsed('user/forgot_new.html')
236 | self.assertIn(
237 | b'You can now change your password.',
238 | response.data
239 | )
240 | response = self.client.post(
241 | '/forgot/new/'+token,
242 | data=dict(password="new-password", confirm="new-password"),
243 | follow_redirects=True
244 | )
245 | self.assertIn(
246 | b'Password successfully changed.',
247 | response.data
248 | )
249 | self.assertTemplateUsed('user/profile.html')
250 | self.assertTrue(current_user.is_authenticated())
251 | self.client.get('/logout')
252 | self.assertFalse(current_user.is_authenticated())
253 |
254 | response = self.client.post(
255 | '/login',
256 | data=dict(email="test@user.com", password="just_a_test_user"),
257 | follow_redirects=True
258 | )
259 | self.assertTrue(response.status_code == 200)
260 | self.assertFalse(current_user.is_authenticated())
261 | self.assertIn(
262 | b'Invalid email and/or password.',
263 | response.data
264 | )
265 | self.assertTemplateUsed('user/login.html')
266 |
267 |
268 | if __name__ == '__main__':
269 | unittest.main()
270 |
--------------------------------------------------------------------------------