Merge pull request #503 from cookiecutter-flask/add-black-formatting

Add black formatting
master
James Curtin 6 years ago committed by GitHub
commit a3e168e3fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .travis.yml
  2. 2
      cookiecutter.json
  3. 16
      hooks/post_gen_project.py
  4. 1
      hooks/pre_gen_project.py
  5. 45
      tasks.py
  6. 2
      {{cookiecutter.app_name}}/.isort.cfg
  7. 6
      {{cookiecutter.app_name}}/.travis.yml
  8. 3
      {{cookiecutter.app_name}}/Pipfile
  9. 9
      {{cookiecutter.app_name}}/README.rst
  10. 4
      {{cookiecutter.app_name}}/package.json
  11. 4
      {{cookiecutter.app_name}}/requirements/dev.txt
  12. 10
      {{cookiecutter.app_name}}/setup.cfg
  13. 12
      {{cookiecutter.app_name}}/tests/conftest.py
  14. 6
      {{cookiecutter.app_name}}/tests/factories.py
  15. 14
      {{cookiecutter.app_name}}/tests/settings.py
  16. 45
      {{cookiecutter.app_name}}/tests/test_forms.py
  17. 82
      {{cookiecutter.app_name}}/tests/test_functional.py
  18. 25
      {{cookiecutter.app_name}}/tests/test_models.py
  19. 6
      {{cookiecutter.app_name}}/webpack.config.js
  20. 28
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py
  21. 92
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/commands.py
  22. 18
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py
  23. 10
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py
  24. 49
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/views.py
  25. 14
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py
  26. 25
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/forms.py
  27. 23
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/models.py
  28. 6
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/views.py
  29. 4
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/utils.py

@ -2,7 +2,6 @@
dist: xenial dist: xenial
language: python language: python
python: python:
- 3.5
- 3.6 - 3.6
- 3.7 - 3.7

@ -6,6 +6,6 @@
"app_name": "{{cookiecutter.project_name.lower().replace('-', '_').replace(' ', '_')}}", "app_name": "{{cookiecutter.project_name.lower().replace('-', '_').replace(' ', '_')}}",
"project_short_description": "A flasky app.", "project_short_description": "A flasky app.",
"use_pipenv": ["no", "yes"], "use_pipenv": ["no", "yes"],
"python_version": ["3.7", "3.6", "3.5"], "python_version": ["3.7", "3.6"],
"node_version": ["12", "10", "8"] "node_version": ["12", "10", "8"]
} }

@ -9,13 +9,13 @@ import sys
def clean_extra_package_management_files(): def clean_extra_package_management_files():
"""Removes either requirements files and folder or the Pipfile.""" """Removes either requirements files and folder or the Pipfile."""
use_pipenv = '{{cookiecutter.use_pipenv}}' use_pipenv = "{{cookiecutter.use_pipenv}}"
to_delete = [] to_delete = []
if use_pipenv == 'yes': if use_pipenv == "yes":
to_delete = to_delete + ['requirements.txt', 'requirements'] to_delete = to_delete + ["requirements.txt", "requirements"]
else: else:
to_delete.append('Pipfile') to_delete.append("Pipfile")
try: try:
for file_or_dir in to_delete: for file_or_dir in to_delete:
@ -25,11 +25,9 @@ def clean_extra_package_management_files():
shutil.rmtree(file_or_dir) shutil.rmtree(file_or_dir)
sys.exit(0) sys.exit(0)
except OSError as e: except OSError as e:
sys.stdout.write( sys.stdout.write("While attempting to remove file(s) an error occurred")
'While attempting to remove file(s) an error occurred' sys.stdout.write("Error: {}".format(e))
)
sys.stdout.write('Error: {}'.format(e))
if __name__ == '__main__': if __name__ == "__main__":
clean_extra_package_management_files() clean_extra_package_management_files()

@ -1,7 +1,6 @@
import re import re
import sys import sys
MODULE_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$" MODULE_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$"

@ -1,34 +1,34 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Invoke tasks.""" """Invoke tasks."""
import os
import json import json
import os
import shutil import shutil
import webbrowser import webbrowser
from invoke import task from invoke import task
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(HERE, 'cookiecutter.json'), 'r') as fp: with open(os.path.join(HERE, "cookiecutter.json"), "r") as fp:
COOKIECUTTER_SETTINGS = json.load(fp) COOKIECUTTER_SETTINGS = json.load(fp)
# Match default value of app_name from cookiecutter.json # Match default value of app_name from cookiecutter.json
COOKIECUTTER_SETTINGS["app_name"] = 'my_flask_app' COOKIECUTTER_SETTINGS["app_name"] = "my_flask_app"
COOKIE = os.path.join(HERE, COOKIECUTTER_SETTINGS['app_name']) COOKIE = os.path.join(HERE, COOKIECUTTER_SETTINGS["app_name"])
REQUIREMENTS = os.path.join(COOKIE, 'requirements', 'dev.txt') REQUIREMENTS = os.path.join(COOKIE, "requirements", "dev.txt")
def _run_npm_command(ctx, command): def _run_npm_command(ctx, command):
os.chdir(COOKIE) os.chdir(COOKIE)
ctx.run('npm {0}'.format(command), echo=True) ctx.run("npm {0}".format(command), echo=True)
os.chdir(HERE) os.chdir(HERE)
@task @task
def build(ctx): def build(ctx):
"""Build the cookiecutter.""" """Build the cookiecutter."""
ctx.run('cookiecutter {0} --no-input'.format(HERE)) ctx.run("cookiecutter {0} --no-input".format(HERE))
_run_npm_command(ctx, 'install') _run_npm_command(ctx, "install")
_run_npm_command(ctx, 'run build') _run_npm_command(ctx, "run build")
@task @task
@ -36,33 +36,34 @@ def clean(ctx):
"""Clean out generated cookiecutter.""" """Clean out generated cookiecutter."""
if os.path.exists(COOKIE): if os.path.exists(COOKIE):
shutil.rmtree(COOKIE) shutil.rmtree(COOKIE)
print('Removed {0}'.format(COOKIE)) print("Removed {0}".format(COOKIE))
else: else:
print('App directory does not exist. Skipping.') print("App directory does not exist. Skipping.")
def _run_flask_command(ctx, command): def _run_flask_command(ctx, command, *args):
os.chdir(COOKIE) os.chdir(COOKIE)
ctx.run('flask {0}'.format(command), echo=True) flask_command = "flask {0}".format(command)
if args:
flask_command = "{0} {1}".format(flask_command, " ".join(args))
ctx.run(flask_command, echo=True)
@task(pre=[clean, build]) @task(pre=[clean, build])
def test(ctx): def test(ctx):
"""Run lint commands and tests.""" """Run lint commands and tests."""
ctx.run('pip install -r {0} --ignore-installed'.format(REQUIREMENTS), ctx.run("pip install -r {0} --ignore-installed".format(REQUIREMENTS), echo=True)
echo=True) _run_npm_command(ctx, "run lint")
_run_npm_command(ctx, 'run lint')
os.chdir(COOKIE) os.chdir(COOKIE)
shutil.copyfile(os.path.join(COOKIE, '.env.example'), shutil.copyfile(os.path.join(COOKIE, ".env.example"), os.path.join(COOKIE, ".env"))
os.path.join(COOKIE, '.env'))
os.environ["FLASK_ENV"] = "production" os.environ["FLASK_ENV"] = "production"
os.environ["FLASK_DEBUG"] = "0" os.environ["FLASK_DEBUG"] = "0"
_run_flask_command(ctx, 'lint') _run_flask_command(ctx, "lint", "--check")
_run_flask_command(ctx, 'test') _run_flask_command(ctx, "test")
@task @task
def readme(ctx, browse=False): def readme(ctx, browse=False):
ctx.run('rst2html.py README.rst > README.html') ctx.run("rst2html.py README.rst > README.html")
if browse: if browse:
webbrowser.open_new_tab('README.html') webbrowser.open_new_tab("README.html")

@ -1,2 +0,0 @@
[settings]
line_length=120

@ -1,11 +1,9 @@
# Config file for automatic testing at travis-ci.org # Config file for automatic testing at travis-ci.org
dist: xenial
language: python language: python
env: env:
- FLASK_APP=autoapp.py FLASK_DEBUG=1 - FLASK_APP=autoapp.py FLASK_DEBUG=1
python: python:
- 2.7
- 3.4
- 3.5
- 3.6 - 3.6
- 3.7 - 3.7
install: install:
@ -16,5 +14,5 @@ install:
before_script: before_script:
- npm run lint - npm run lint
- npm run build - npm run build
- flask lint - flask lint --check
script: flask test script: flask test

@ -53,12 +53,11 @@ factory-boy = "==2.12.*"
pdbpp = "==0.10.0" pdbpp = "==0.10.0"
# Lint and code style # Lint and code style
black = "==19.3b0"
flake8 = "==3.7.7" flake8 = "==3.7.7"
flake8-blind-except = "==0.1.1" flake8-blind-except = "==0.1.1"
flake8-debugger = "==3.1.0" flake8-debugger = "==3.1.0"
flake8-docstrings = "==1.3.0" flake8-docstrings = "==1.3.0"
flake8-isort = "==2.7.0" flake8-isort = "==2.7.0"
flake8-quotes = "==2.0.1"
isort = "==4.3.20" isort = "==4.3.20"
pep8-naming = "==0.8.2" pep8-naming = "==0.8.2"

@ -57,13 +57,18 @@ To open the interactive shell, run ::
By default, you will have access to the flask ``app``. By default, you will have access to the flask ``app``.
Running Tests Running Tests/Linter
------------- --------------------
To run all tests, run :: To run all tests, run ::
flask test flask test
To run the linter, run ::
flask lint
The ``lint`` command will attempt to fix any linting/style errors in the code. If you only want to know if the code will pass CI and do not wish for the linter to make changes, add the ``--check`` argument.
Migrations Migrations
---------- ----------

@ -15,7 +15,9 @@
}, },
"author": "{{cookiecutter.full_name}}", "author": "{{cookiecutter.full_name}}",
"license": "MIT", "license": "MIT",
"engines": { "node" : ">={{cookiecutter.node_version}}" }, "engines": {
"node": ">={{cookiecutter.node_version}}"
},
"bugs": { "bugs": {
"url": "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.app_name}}/issues" "url": "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.app_name}}/issues"
}, },

@ -8,11 +8,11 @@ factory-boy==2.11.1
pdbpp==0.10.0 pdbpp==0.10.0
# Lint and code style # Lint and code style
flake8==3.5.0 black==19.3b0
flake8==3.7.7
flake8-blind-except==0.1.1 flake8-blind-except==0.1.1
flake8-debugger==3.1.0 flake8-debugger==3.1.0
flake8-docstrings==1.3.0 flake8-docstrings==1.3.0
flake8-isort==2.5 flake8-isort==2.5
flake8-quotes==1.0.0
isort==4.3.4 isort==4.3.4
pep8-naming==0.7.0 pep8-naming==0.7.0

@ -1,3 +1,11 @@
[flake8] [flake8]
ignore = D401 ignore = D401,D202,E226,E302,E41
max-line-length=120 max-line-length=120
exclude = migrations/*
max-complexity = 10
[isort]
line_length=88
multi_line_output=3
skip=migrations/*
include_trailing_comma=true

@ -12,8 +12,8 @@ from .factories import UserFactory
@pytest.fixture @pytest.fixture
def app(): def app():
"""An application for the tests.""" """Create application for the tests."""
_app = create_app('tests.settings') _app = create_app("tests.settings")
ctx = _app.test_request_context() ctx = _app.test_request_context()
ctx.push() ctx.push()
@ -24,13 +24,13 @@ def app():
@pytest.fixture @pytest.fixture
def testapp(app): def testapp(app):
"""A Webtest app.""" """Create Webtest app."""
return TestApp(app) return TestApp(app)
@pytest.fixture @pytest.fixture
def db(app): def db(app):
"""A database for the tests.""" """Create database for the tests."""
_db.app = app _db.app = app
with app.app_context(): with app.app_context():
_db.create_all() _db.create_all()
@ -44,7 +44,7 @@ def db(app):
@pytest.fixture @pytest.fixture
def user(db): def user(db):
"""A user for the tests.""" """Create user for the tests."""
user = UserFactory(password='myprecious') user = UserFactory(password="myprecious")
db.session.commit() db.session.commit()
return user return user

@ -20,9 +20,9 @@ class BaseFactory(SQLAlchemyModelFactory):
class UserFactory(BaseFactory): class UserFactory(BaseFactory):
"""User factory.""" """User factory."""
username = Sequence(lambda n: 'user{0}'.format(n)) username = Sequence(lambda n: "user{0}".format(n))
email = Sequence(lambda n: 'user{0}@example.com'.format(n)) email = Sequence(lambda n: "user{0}@example.com".format(n))
password = PostGenerationMethodCall('set_password', 'example') password = PostGenerationMethodCall("set_password", "example")
active = True active = True
class Meta: class Meta:

@ -1,11 +1,13 @@
"""Settings module for test app.""" """Settings module for test app."""
ENV = 'development' ENV = "development"
TESTING = True TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite://' SQLALCHEMY_DATABASE_URI = "sqlite://"
SECRET_KEY = 'not-so-secret-in-tests' SECRET_KEY = "not-so-secret-in-tests"
BCRYPT_LOG_ROUNDS = 4 # For faster tests; needs at least 4 to avoid "ValueError: Invalid rounds" BCRYPT_LOG_ROUNDS = (
4
) # For faster tests; needs at least 4 to avoid "ValueError: Invalid rounds"
DEBUG_TB_ENABLED = False DEBUG_TB_ENABLED = False
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc. CACHE_TYPE = "simple" # Can be "memcached", "redis", etc.
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
WEBPACK_MANIFEST_PATH = 'webpack/manifest.json' WEBPACK_MANIFEST_PATH = "webpack/manifest.json"
WTF_CSRF_ENABLED = False # Allows form testing WTF_CSRF_ENABLED = False # Allows form testing

@ -10,24 +10,33 @@ class TestRegisterForm:
def test_validate_user_already_registered(self, user): def test_validate_user_already_registered(self, user):
"""Enter username that is already registered.""" """Enter username that is already registered."""
form = RegisterForm(username=user.username, email='foo@bar.com', form = RegisterForm(
password='example', confirm='example') username=user.username,
email="foo@bar.com",
password="example",
confirm="example",
)
assert form.validate() is False assert form.validate() is False
assert 'Username already registered' in form.username.errors assert "Username already registered" in form.username.errors
def test_validate_email_already_registered(self, user): def test_validate_email_already_registered(self, user):
"""Enter email that is already registered.""" """Enter email that is already registered."""
form = RegisterForm(username='unique', email=user.email, form = RegisterForm(
password='example', confirm='example') username="unique", email=user.email, password="example", confirm="example"
)
assert form.validate() is False assert form.validate() is False
assert 'Email already registered' in form.email.errors assert "Email already registered" in form.email.errors
def test_validate_success(self, db): def test_validate_success(self, db):
"""Register with success.""" """Register with success."""
form = RegisterForm(username='newusername', email='new@test.test', form = RegisterForm(
password='example', confirm='example') username="newusername",
email="new@test.test",
password="example",
confirm="example",
)
assert form.validate() is True assert form.validate() is True
@ -36,33 +45,33 @@ class TestLoginForm:
def test_validate_success(self, user): def test_validate_success(self, user):
"""Login successful.""" """Login successful."""
user.set_password('example') user.set_password("example")
user.save() user.save()
form = LoginForm(username=user.username, password='example') form = LoginForm(username=user.username, password="example")
assert form.validate() is True assert form.validate() is True
assert form.user == user assert form.user == user
def test_validate_unknown_username(self, db): def test_validate_unknown_username(self, db):
"""Unknown username.""" """Unknown username."""
form = LoginForm(username='unknown', password='example') form = LoginForm(username="unknown", password="example")
assert form.validate() is False assert form.validate() is False
assert 'Unknown username' in form.username.errors assert "Unknown username" in form.username.errors
assert form.user is None assert form.user is None
def test_validate_invalid_password(self, user): def test_validate_invalid_password(self, user):
"""Invalid password.""" """Invalid password."""
user.set_password('example') user.set_password("example")
user.save() user.save()
form = LoginForm(username=user.username, password='wrongpassword') form = LoginForm(username=user.username, password="wrongpassword")
assert form.validate() is False assert form.validate() is False
assert 'Invalid password' in form.password.errors assert "Invalid password" in form.password.errors
def test_validate_inactive_user(self, user): def test_validate_inactive_user(self, user):
"""Inactive user.""" """Inactive user."""
user.active = False user.active = False
user.set_password('example') user.set_password("example")
user.save() user.save()
# Correct username and password, but user is not activated # Correct username and password, but user is not activated
form = LoginForm(username=user.username, password='example') form = LoginForm(username=user.username, password="example")
assert form.validate() is False assert form.validate() is False
assert 'User not activated' in form.username.errors assert "User not activated" in form.username.errors

@ -16,53 +16,53 @@ class TestLoggingIn:
def test_can_log_in_returns_200(self, user, testapp): def test_can_log_in_returns_200(self, user, testapp):
"""Login successful.""" """Login successful."""
# Goes to homepage # Goes to homepage
res = testapp.get('/') res = testapp.get("/")
# Fills out login form in navbar # Fills out login form in navbar
form = res.forms['loginForm'] form = res.forms["loginForm"]
form['username'] = user.username form["username"] = user.username
form['password'] = 'myprecious' form["password"] = "myprecious"
# Submits # Submits
res = form.submit().follow() res = form.submit().follow()
assert res.status_code == 200 assert res.status_code == 200
def test_sees_alert_on_log_out(self, user, testapp): def test_sees_alert_on_log_out(self, user, testapp):
"""Show alert on logout.""" """Show alert on logout."""
res = testapp.get('/') res = testapp.get("/")
# Fills out login form in navbar # Fills out login form in navbar
form = res.forms['loginForm'] form = res.forms["loginForm"]
form['username'] = user.username form["username"] = user.username
form['password'] = 'myprecious' form["password"] = "myprecious"
# Submits # Submits
res = form.submit().follow() res = form.submit().follow()
res = testapp.get(url_for('public.logout')).follow() res = testapp.get(url_for("public.logout")).follow()
# sees alert # sees alert
assert 'You are logged out.' in res assert "You are logged out." in res
def test_sees_error_message_if_password_is_incorrect(self, user, testapp): def test_sees_error_message_if_password_is_incorrect(self, user, testapp):
"""Show error if password is incorrect.""" """Show error if password is incorrect."""
# Goes to homepage # Goes to homepage
res = testapp.get('/') res = testapp.get("/")
# Fills out login form, password incorrect # Fills out login form, password incorrect
form = res.forms['loginForm'] form = res.forms["loginForm"]
form['username'] = user.username form["username"] = user.username
form['password'] = 'wrong' form["password"] = "wrong"
# Submits # Submits
res = form.submit() res = form.submit()
# sees error # sees error
assert 'Invalid password' in res assert "Invalid password" in res
def test_sees_error_message_if_username_doesnt_exist(self, user, testapp): def test_sees_error_message_if_username_doesnt_exist(self, user, testapp):
"""Show error if username doesn't exist.""" """Show error if username doesn't exist."""
# Goes to homepage # Goes to homepage
res = testapp.get('/') res = testapp.get("/")
# Fills out login form, password incorrect # Fills out login form, password incorrect
form = res.forms['loginForm'] form = res.forms["loginForm"]
form['username'] = 'unknown' form["username"] = "unknown"
form['password'] = 'myprecious' form["password"] = "myprecious"
# Submits # Submits
res = form.submit() res = form.submit()
# sees error # sees error
assert 'Unknown user' in res assert "Unknown user" in res
class TestRegistering: class TestRegistering:
@ -72,15 +72,15 @@ class TestRegistering:
"""Register a new user.""" """Register a new user."""
old_count = len(User.query.all()) old_count = len(User.query.all())
# Goes to homepage # Goes to homepage
res = testapp.get('/') res = testapp.get("/")
# Clicks Create Account button # Clicks Create Account button
res = res.click('Create account') res = res.click("Create account")
# Fills out the form # Fills out the form
form = res.forms['registerForm'] form = res.forms["registerForm"]
form['username'] = 'foobar' form["username"] = "foobar"
form['email'] = 'foo@bar.com' form["email"] = "foo@bar.com"
form['password'] = 'secret' form["password"] = "secret"
form['confirm'] = 'secret' form["confirm"] = "secret"
# Submits # Submits
res = form.submit().follow() res = form.submit().follow()
assert res.status_code == 200 assert res.status_code == 200
@ -90,31 +90,31 @@ class TestRegistering:
def test_sees_error_message_if_passwords_dont_match(self, user, testapp): def test_sees_error_message_if_passwords_dont_match(self, user, testapp):
"""Show error if passwords don't match.""" """Show error if passwords don't match."""
# Goes to registration page # Goes to registration page
res = testapp.get(url_for('public.register')) res = testapp.get(url_for("public.register"))
# Fills out form, but passwords don't match # Fills out form, but passwords don't match
form = res.forms['registerForm'] form = res.forms["registerForm"]
form['username'] = 'foobar' form["username"] = "foobar"
form['email'] = 'foo@bar.com' form["email"] = "foo@bar.com"
form['password'] = 'secret' form["password"] = "secret"
form['confirm'] = 'secrets' form["confirm"] = "secrets"
# Submits # Submits
res = form.submit() res = form.submit()
# sees error message # sees error message
assert 'Passwords must match' in res assert "Passwords must match" in res
def test_sees_error_message_if_user_already_registered(self, user, testapp): def test_sees_error_message_if_user_already_registered(self, user, testapp):
"""Show error if user already registered.""" """Show error if user already registered."""
user = UserFactory(active=True) # A registered user user = UserFactory(active=True) # A registered user
user.save() user.save()
# Goes to registration page # Goes to registration page
res = testapp.get(url_for('public.register')) res = testapp.get(url_for("public.register"))
# Fills out form, but username is already registered # Fills out form, but username is already registered
form = res.forms['registerForm'] form = res.forms["registerForm"]
form['username'] = user.username form["username"] = user.username
form['email'] = 'foo@bar.com' form["email"] = "foo@bar.com"
form['password'] = 'secret' form["password"] = "secret"
form['confirm'] = 'secret' form["confirm"] = "secret"
# Submits # Submits
res = form.submit() res = form.submit()
# sees error # sees error
assert 'Username already registered' in res assert "Username already registered" in res

@ -9,13 +9,13 @@ from {{cookiecutter.app_name}}.user.models import Role, User
from .factories import UserFactory from .factories import UserFactory
@pytest.mark.usefixtures('db') @pytest.mark.usefixtures("db")
class TestUser: class TestUser:
"""User tests.""" """User tests."""
def test_get_by_id(self): def test_get_by_id(self):
"""Get user by ID.""" """Get user by ID."""
user = User('foo', 'foo@bar.com') user = User("foo", "foo@bar.com")
user.save() user.save()
retrieved = User.get_by_id(user.id) retrieved = User.get_by_id(user.id)
@ -23,43 +23,42 @@ class TestUser:
def test_created_at_defaults_to_datetime(self): def test_created_at_defaults_to_datetime(self):
"""Test creation date.""" """Test creation date."""
user = User(username='foo', email='foo@bar.com') user = User(username="foo", email="foo@bar.com")
user.save() user.save()
assert bool(user.created_at) assert bool(user.created_at)
assert isinstance(user.created_at, dt.datetime) assert isinstance(user.created_at, dt.datetime)
def test_password_is_nullable(self): def test_password_is_nullable(self):
"""Test null password.""" """Test null password."""
user = User(username='foo', email='foo@bar.com') user = User(username="foo", email="foo@bar.com")
user.save() user.save()
assert user.password is None assert user.password is None
def test_factory(self, db): def test_factory(self, db):
"""Test user factory.""" """Test user factory."""
user = UserFactory(password='myprecious') user = UserFactory(password="myprecious")
db.session.commit() db.session.commit()
assert bool(user.username) assert bool(user.username)
assert bool(user.email) assert bool(user.email)
assert bool(user.created_at) assert bool(user.created_at)
assert user.is_admin is False assert user.is_admin is False
assert user.active is True assert user.active is True
assert user.check_password('myprecious') assert user.check_password("myprecious")
def test_check_password(self): def test_check_password(self):
"""Check password.""" """Check password."""
user = User.create(username='foo', email='foo@bar.com', user = User.create(username="foo", email="foo@bar.com", password="foobarbaz123")
password='foobarbaz123') assert user.check_password("foobarbaz123") is True
assert user.check_password('foobarbaz123') is True assert user.check_password("barfoobaz") is False
assert user.check_password('barfoobaz') is False
def test_full_name(self): def test_full_name(self):
"""User full name.""" """User full name."""
user = UserFactory(first_name='Foo', last_name='Bar') user = UserFactory(first_name="Foo", last_name="Bar")
assert user.full_name == 'Foo Bar' assert user.full_name == "Foo Bar"
def test_roles(self): def test_roles(self):
"""Add a role to a user.""" """Add a role to a user."""
role = Role(name='admin') role = Role(name="admin")
role.save() role.save()
user = UserFactory() user = UserFactory()
user.roles.append(role) user.roles.append(role)

@ -67,8 +67,10 @@ module.exports = {
}, },
{ test: /\.html$/, loader: 'raw-loader' }, { test: /\.html$/, loader: 'raw-loader' },
{ test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff' } }, { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff' } },
{ test: /\.(ttf|eot|svg|png|jpe?g|gif|ico)(\?.*)?$/i, {
loader: `file-loader?context=${rootAssetPath}&name=[path][name].[hash].[ext]` }, test: /\.(ttf|eot|svg|png|jpe?g|gif|ico)(\?.*)?$/i,
loader: `file-loader?context=${rootAssetPath}&name=[path][name].[hash].[ext]`
},
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', query: { presets: ['env'], cacheDirectory: true } }, { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', query: { presets: ['env'], cacheDirectory: true } },
], ],
}, },

@ -6,15 +6,24 @@ import sys
from flask import Flask, render_template from flask import Flask, render_template
from {{cookiecutter.app_name}} import commands, public, user from {{cookiecutter.app_name}} import commands, public, user
from {{cookiecutter.app_name}}.extensions import bcrypt, cache, csrf_protect, db, debug_toolbar, login_manager, migrate, webpack from {{cookiecutter.app_name}}.extensions import (
bcrypt,
cache,
csrf_protect,
db,
debug_toolbar,
login_manager,
migrate,
webpack,
)
def create_app(config_object='{{cookiecutter.app_name}}.settings'): def create_app(config_object="{{cookiecutter.app_name}}.settings"):
"""An application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/. """Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
:param config_object: The configuration object to use. :param config_object: The configuration object to use.
""" """
app = Flask(__name__.split('.')[0]) app = Flask(__name__.split(".")[0])
app.config.from_object(config_object) app.config.from_object(config_object)
register_extensions(app) register_extensions(app)
register_blueprints(app) register_blueprints(app)
@ -47,11 +56,13 @@ def register_blueprints(app):
def register_errorhandlers(app): def register_errorhandlers(app):
"""Register error handlers.""" """Register error handlers."""
def render_error(error): def render_error(error):
"""Render error template.""" """Render error template."""
# If a HTTPException, pull the `code` attribute; default to 500 # If a HTTPException, pull the `code` attribute; default to 500
error_code = getattr(error, 'code', 500) error_code = getattr(error, "code", 500)
return render_template('{0}.html'.format(error_code)), error_code return render_template("{0}.html".format(error_code)), error_code
for errcode in [401, 404, 500]: for errcode in [401, 404, 500]:
app.errorhandler(errcode)(render_error) app.errorhandler(errcode)(render_error)
return None return None
@ -59,11 +70,10 @@ def register_errorhandlers(app):
def register_shellcontext(app): def register_shellcontext(app):
"""Register shell context objects.""" """Register shell context objects."""
def shell_context(): def shell_context():
"""Shell context objects.""" """Shell context objects."""
return { return {"db": db, "User": user.models.User}
'db': db,
'User': user.models.User}
app.shell_context_processor(shell_context) app.shell_context_processor(shell_context)

@ -11,40 +11,61 @@ from werkzeug.exceptions import MethodNotAllowed, NotFound
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
PROJECT_ROOT = os.path.join(HERE, os.pardir) PROJECT_ROOT = os.path.join(HERE, os.pardir)
TEST_PATH = os.path.join(PROJECT_ROOT, 'tests') TEST_PATH = os.path.join(PROJECT_ROOT, "tests")
@click.command() @click.command()
def test(): def test():
"""Run the tests.""" """Run the tests."""
import pytest import pytest
rv = pytest.main([TEST_PATH, '--verbose'])
rv = pytest.main([TEST_PATH, "--verbose"])
exit(rv) exit(rv)
@click.command() @click.command()
@click.option('-f', '--fix-imports', default=False, is_flag=True, @click.option(
help='Fix imports using isort, before linting') "-f",
def lint(fix_imports): "--fix-imports",
"""Lint and check code style with flake8 and isort.""" default=True,
skip = ['node_modules', 'requirements'] is_flag=True,
root_files = glob('*.py') help="Fix imports using isort, before linting",
)
@click.option(
"-c",
"--check",
default=False,
is_flag=True,
help="Don't make any changes to files, just confirm they are formatted correctly",
)
def lint(fix_imports, check):
"""Lint and check code style with black, flake8 and isort."""
skip = ["node_modules", "requirements", "migrations"]
root_files = glob("*.py")
root_directories = [ root_directories = [
name for name in next(os.walk('.'))[1] if not name.startswith('.')] name for name in next(os.walk("."))[1] if not name.startswith(".")
]
files_and_directories = [ files_and_directories = [
arg for arg in root_files + root_directories if arg not in skip] arg for arg in root_files + root_directories if arg not in skip
]
def execute_tool(description, *args): def execute_tool(description, *args):
"""Execute a checking tool with its arguments.""" """Execute a checking tool with its arguments."""
command_line = list(args) + files_and_directories command_line = list(args) + files_and_directories
click.echo('{}: {}'.format(description, ' '.join(command_line))) click.echo("{}: {}".format(description, " ".join(command_line)))
rv = call(command_line) rv = call(command_line)
if rv != 0: if rv != 0:
exit(rv) exit(rv)
isort_args = ["-rc"]
black_args = []
if check:
isort_args.append("-c")
black_args.append("--check")
if fix_imports: if fix_imports:
execute_tool('Fixing import order', 'isort', '-rc') execute_tool("Fixing import order", "isort", *isort_args)
execute_tool('Checking code style', 'flake8') execute_tool("Formatting style", "black", *black_args)
execute_tool("Checking code style", "flake8")
@click.command() @click.command()
@ -53,19 +74,19 @@ def clean():
Borrowed from Flask-Script, converted to use Click. Borrowed from Flask-Script, converted to use Click.
""" """
for dirpath, dirnames, filenames in os.walk('.'): for dirpath, dirnames, filenames in os.walk("."):
for filename in filenames: for filename in filenames:
if filename.endswith('.pyc') or filename.endswith('.pyo'): if filename.endswith(".pyc") or filename.endswith(".pyo"):
full_pathname = os.path.join(dirpath, filename) full_pathname = os.path.join(dirpath, filename)
click.echo('Removing {}'.format(full_pathname)) click.echo("Removing {}".format(full_pathname))
os.remove(full_pathname) os.remove(full_pathname)
@click.command() @click.command()
@click.option('--url', default=None, @click.option("--url", default=None, help="Url to test (ex. /static/image.png)")
help='Url to test (ex. /static/image.png)') @click.option(
@click.option('--order', default='rule', "--order", default="rule", help="Property on Rule to order by (default: rule)"
help='Property on Rule to order by (default: rule)') )
@with_appcontext @with_appcontext
def urls(url, order): def urls(url, order):
"""Display all of the url matching routes for the project. """Display all of the url matching routes for the project.
@ -74,53 +95,50 @@ def urls(url, order):
""" """
rows = [] rows = []
column_length = 0 column_length = 0
column_headers = ('Rule', 'Endpoint', 'Arguments') column_headers = ("Rule", "Endpoint", "Arguments")
if url: if url:
try: try:
rule, arguments = ( rule, arguments = current_app.url_map.bind("localhost").match(
current_app.url_map url, return_rule=True
.bind('localhost') )
.match(url, return_rule=True))
rows.append((rule.rule, rule.endpoint, arguments)) rows.append((rule.rule, rule.endpoint, arguments))
column_length = 3 column_length = 3
except (NotFound, MethodNotAllowed) as e: except (NotFound, MethodNotAllowed) as e:
rows.append(('<{}>'.format(e), None, None)) rows.append(("<{}>".format(e), None, None))
column_length = 1 column_length = 1
else: else:
rules = sorted( rules = sorted(
current_app.url_map.iter_rules(), current_app.url_map.iter_rules(), key=lambda rule: getattr(rule, order)
key=lambda rule: getattr(rule, order)) )
for rule in rules: for rule in rules:
rows.append((rule.rule, rule.endpoint, None)) rows.append((rule.rule, rule.endpoint, None))
column_length = 2 column_length = 2
str_template = '' str_template = ""
table_width = 0 table_width = 0
if column_length >= 1: if column_length >= 1:
max_rule_length = max(len(r[0]) for r in rows) max_rule_length = max(len(r[0]) for r in rows)
max_rule_length = max_rule_length if max_rule_length > 4 else 4 max_rule_length = max_rule_length if max_rule_length > 4 else 4
str_template += '{:' + str(max_rule_length) + '}' str_template += "{:" + str(max_rule_length) + "}"
table_width += max_rule_length table_width += max_rule_length
if column_length >= 2: if column_length >= 2:
max_endpoint_length = max(len(str(r[1])) for r in rows) max_endpoint_length = max(len(str(r[1])) for r in rows)
# max_endpoint_length = max(rows, key=len) # max_endpoint_length = max(rows, key=len)
max_endpoint_length = ( max_endpoint_length = max_endpoint_length if max_endpoint_length > 8 else 8
max_endpoint_length if max_endpoint_length > 8 else 8) str_template += " {:" + str(max_endpoint_length) + "}"
str_template += ' {:' + str(max_endpoint_length) + '}'
table_width += 2 + max_endpoint_length table_width += 2 + max_endpoint_length
if column_length >= 3: if column_length >= 3:
max_arguments_length = max(len(str(r[2])) for r in rows) max_arguments_length = max(len(str(r[2])) for r in rows)
max_arguments_length = ( max_arguments_length = max_arguments_length if max_arguments_length > 9 else 9
max_arguments_length if max_arguments_length > 9 else 9) str_template += " {:" + str(max_arguments_length) + "}"
str_template += ' {:' + str(max_arguments_length) + '}'
table_width += 2 + max_arguments_length table_width += 2 + max_arguments_length
click.echo(str_template.format(*column_headers[:column_length])) click.echo(str_template.format(*column_headers[:column_length]))
click.echo('-' * table_width) click.echo("-" * table_width)
for row in rows: for row in rows:
click.echo(str_template.format(*row[:column_length])) click.echo(str_template.format(*row[:column_length]))

@ -47,7 +47,7 @@ class Model(CRUDMixin, db.Model):
class SurrogatePK(object): class SurrogatePK(object):
"""A mixin that adds a surrogate integer 'primary key' column named ``id`` to any declarative-mapped class.""" """A mixin that adds a surrogate integer 'primary key' column named ``id`` to any declarative-mapped class."""
__table_args__ = {'extend_existing': True} __table_args__ = {"extend_existing": True}
id = Column(db.Integer, primary_key=True) id = Column(db.Integer, primary_key=True)
@ -55,14 +55,18 @@ class SurrogatePK(object):
def get_by_id(cls, record_id): def get_by_id(cls, record_id):
"""Get record by ID.""" """Get record by ID."""
if any( if any(
(isinstance(record_id, basestring) and record_id.isdigit(), (
isinstance(record_id, (int, float))), isinstance(record_id, basestring) and record_id.isdigit(),
isinstance(record_id, (int, float)),
)
): ):
return cls.query.get(int(record_id)) return cls.query.get(int(record_id))
return None return None
def reference_col(tablename, nullable=False, pk_name='id', foreign_key_kwargs=None, column_kwargs=None): def reference_col(
tablename, nullable=False, pk_name="id", foreign_key_kwargs=None, column_kwargs=None
):
"""Column that adds primary key foreign key reference. """Column that adds primary key foreign key reference.
Usage: :: Usage: ::
@ -74,5 +78,7 @@ def reference_col(tablename, nullable=False, pk_name='id', foreign_key_kwargs=No
column_kwargs = column_kwargs or {} column_kwargs = column_kwargs or {}
return Column( return Column(
db.ForeignKey('{0}.{1}'.format(tablename, pk_name), **foreign_key_kwargs), db.ForeignKey("{0}.{1}".format(tablename, pk_name), **foreign_key_kwargs),
nullable=nullable, **column_kwargs) nullable=nullable,
**column_kwargs
)

@ -10,8 +10,8 @@ from {{cookiecutter.app_name}}.user.models import User
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
"""Login form.""" """Login form."""
username = StringField('Username', validators=[DataRequired()]) username = StringField("Username", validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()]) password = PasswordField("Password", validators=[DataRequired()])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Create instance.""" """Create instance."""
@ -26,14 +26,14 @@ class LoginForm(FlaskForm):
self.user = User.query.filter_by(username=self.username.data).first() self.user = User.query.filter_by(username=self.username.data).first()
if not self.user: if not self.user:
self.username.errors.append('Unknown username') self.username.errors.append("Unknown username")
return False return False
if not self.user.check_password(self.password.data): if not self.user.check_password(self.password.data):
self.password.errors.append('Invalid password') self.password.errors.append("Invalid password")
return False return False
if not self.user.active: if not self.user.active:
self.username.errors.append('User not activated') self.username.errors.append("User not activated")
return False return False
return True return True

@ -1,6 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Public section, including homepage and signup.""" """Public section, including homepage and signup."""
from flask import Blueprint, current_app, flash, redirect, render_template, request, url_for from flask import (
Blueprint,
current_app,
flash,
redirect,
render_template,
request,
url_for,
)
from flask_login import login_required, login_user, logout_user from flask_login import login_required, login_user, logout_user
from {{cookiecutter.app_name}}.extensions import login_manager from {{cookiecutter.app_name}}.extensions import login_manager
@ -9,7 +17,7 @@ from {{cookiecutter.app_name}}.user.forms import RegisterForm
from {{cookiecutter.app_name}}.user.models import User from {{cookiecutter.app_name}}.user.models import User
from {{cookiecutter.app_name}}.utils import flash_errors from {{cookiecutter.app_name}}.utils import flash_errors
blueprint = Blueprint('public', __name__, static_folder='../static') blueprint = Blueprint("public", __name__, static_folder="../static")
@login_manager.user_loader @login_manager.user_loader
@ -18,47 +26,52 @@ def load_user(user_id):
return User.get_by_id(int(user_id)) return User.get_by_id(int(user_id))
@blueprint.route('/', methods=['GET', 'POST']) @blueprint.route("/", methods=["GET", "POST"])
def home(): def home():
"""Home page.""" """Home page."""
form = LoginForm(request.form) form = LoginForm(request.form)
current_app.logger.info('Hello from the home page!') current_app.logger.info("Hello from the home page!")
# Handle logging in # Handle logging in
if request.method == 'POST': if request.method == "POST":
if form.validate_on_submit(): if form.validate_on_submit():
login_user(form.user) login_user(form.user)
flash('You are logged in.', 'success') flash("You are logged in.", "success")
redirect_url = request.args.get('next') or url_for('user.members') redirect_url = request.args.get("next") or url_for("user.members")
return redirect(redirect_url) return redirect(redirect_url)
else: else:
flash_errors(form) flash_errors(form)
return render_template('public/home.html', form=form) return render_template("public/home.html", form=form)
@blueprint.route('/logout/') @blueprint.route("/logout/")
@login_required @login_required
def logout(): def logout():
"""Logout.""" """Logout."""
logout_user() logout_user()
flash('You are logged out.', 'info') flash("You are logged out.", "info")
return redirect(url_for('public.home')) return redirect(url_for("public.home"))
@blueprint.route('/register/', methods=['GET', 'POST']) @blueprint.route("/register/", methods=["GET", "POST"])
def register(): def register():
"""Register new user.""" """Register new user."""
form = RegisterForm(request.form) form = RegisterForm(request.form)
if form.validate_on_submit(): if form.validate_on_submit():
User.create(username=form.username.data, email=form.email.data, password=form.password.data, active=True) User.create(
flash('Thank you for registering. You can now log in.', 'success') username=form.username.data,
return redirect(url_for('public.home')) email=form.email.data,
password=form.password.data,
active=True,
)
flash("Thank you for registering. You can now log in.", "success")
return redirect(url_for("public.home"))
else: else:
flash_errors(form) flash_errors(form)
return render_template('public/register.html', form=form) return render_template("public/register.html", form=form)
@blueprint.route('/about/') @blueprint.route("/about/")
def about(): def about():
"""About page.""" """About page."""
form = LoginForm(request.form) form = LoginForm(request.form)
return render_template('public/about.html', form=form) return render_template("public/about.html", form=form)

@ -11,13 +11,13 @@ from environs import Env
env = Env() env = Env()
env.read_env() env.read_env()
ENV = env.str('FLASK_ENV', default='production') ENV = env.str("FLASK_ENV", default="production")
DEBUG = ENV == 'development' DEBUG = ENV == "development"
SQLALCHEMY_DATABASE_URI = env.str('DATABASE_URL') SQLALCHEMY_DATABASE_URI = env.str("DATABASE_URL")
SECRET_KEY = env.str('SECRET_KEY') SECRET_KEY = env.str("SECRET_KEY")
BCRYPT_LOG_ROUNDS = env.int('BCRYPT_LOG_ROUNDS', default=13) BCRYPT_LOG_ROUNDS = env.int("BCRYPT_LOG_ROUNDS", default=13)
DEBUG_TB_ENABLED = DEBUG DEBUG_TB_ENABLED = DEBUG
DEBUG_TB_INTERCEPT_REDIRECTS = False DEBUG_TB_INTERCEPT_REDIRECTS = False
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc. CACHE_TYPE = "simple" # Can be "memcached", "redis", etc.
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
WEBPACK_MANIFEST_PATH = 'webpack/manifest.json' WEBPACK_MANIFEST_PATH = "webpack/manifest.json"

@ -10,14 +10,19 @@ from .models import User
class RegisterForm(FlaskForm): class RegisterForm(FlaskForm):
"""Register form.""" """Register form."""
username = StringField('Username', username = StringField(
validators=[DataRequired(), Length(min=3, max=25)]) "Username", validators=[DataRequired(), Length(min=3, max=25)]
email = StringField('Email', )
validators=[DataRequired(), Email(), Length(min=6, max=40)]) email = StringField(
password = PasswordField('Password', "Email", validators=[DataRequired(), Email(), Length(min=6, max=40)]
validators=[DataRequired(), Length(min=6, max=40)]) )
confirm = PasswordField('Verify password', password = PasswordField(
[DataRequired(), EqualTo('password', message='Passwords must match')]) "Password", validators=[DataRequired(), Length(min=6, max=40)]
)
confirm = PasswordField(
"Verify password",
[DataRequired(), EqualTo("password", message="Passwords must match")],
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Create instance.""" """Create instance."""
@ -31,10 +36,10 @@ class RegisterForm(FlaskForm):
return False return False
user = User.query.filter_by(username=self.username.data).first() user = User.query.filter_by(username=self.username.data).first()
if user: if user:
self.username.errors.append('Username already registered') self.username.errors.append("Username already registered")
return False return False
user = User.query.filter_by(email=self.email.data).first() user = User.query.filter_by(email=self.email.data).first()
if user: if user:
self.email.errors.append('Email already registered') self.email.errors.append("Email already registered")
return False return False
return True return True

@ -4,17 +4,24 @@ import datetime as dt
from flask_login import UserMixin from flask_login import UserMixin
from {{cookiecutter.app_name}}.database import Column, Model, SurrogatePK, db, reference_col, relationship from {{cookiecutter.app_name}}.database import (
Column,
Model,
SurrogatePK,
db,
reference_col,
relationship,
)
from {{cookiecutter.app_name}}.extensions import bcrypt from {{cookiecutter.app_name}}.extensions import bcrypt
class Role(SurrogatePK, Model): class Role(SurrogatePK, Model):
"""A role for a user.""" """A role for a user."""
__tablename__ = 'roles' __tablename__ = "roles"
name = Column(db.String(80), unique=True, nullable=False) name = Column(db.String(80), unique=True, nullable=False)
user_id = reference_col('users', nullable=True) user_id = reference_col("users", nullable=True)
user = relationship('User', backref='roles') user = relationship("User", backref="roles")
def __init__(self, name, **kwargs): def __init__(self, name, **kwargs):
"""Create instance.""" """Create instance."""
@ -22,13 +29,13 @@ class Role(SurrogatePK, Model):
def __repr__(self): def __repr__(self):
"""Represent instance as a unique string.""" """Represent instance as a unique string."""
return '<Role({name})>'.format(name=self.name) return "<Role({name})>".format(name=self.name)
class User(UserMixin, SurrogatePK, Model): class User(UserMixin, SurrogatePK, Model):
"""A user of the app.""" """A user of the app."""
__tablename__ = 'users' __tablename__ = "users"
username = Column(db.String(80), unique=True, nullable=False) username = Column(db.String(80), unique=True, nullable=False)
email = Column(db.String(80), unique=True, nullable=False) email = Column(db.String(80), unique=True, nullable=False)
#: The hashed password #: The hashed password
@ -58,8 +65,8 @@ class User(UserMixin, SurrogatePK, Model):
@property @property
def full_name(self): def full_name(self):
"""Full user name.""" """Full user name."""
return '{0} {1}'.format(self.first_name, self.last_name) return "{0} {1}".format(self.first_name, self.last_name)
def __repr__(self): def __repr__(self):
"""Represent instance as a unique string.""" """Represent instance as a unique string."""
return '<User({username!r})>'.format(username=self.username) return "<User({username!r})>".format(username=self.username)

@ -3,11 +3,11 @@
from flask import Blueprint, render_template from flask import Blueprint, render_template
from flask_login import login_required from flask_login import login_required
blueprint = Blueprint('user', __name__, url_prefix='/users', static_folder='../static') blueprint = Blueprint("user", __name__, url_prefix="/users", static_folder="../static")
@blueprint.route('/') @blueprint.route("/")
@login_required @login_required
def members(): def members():
"""List members.""" """List members."""
return render_template('users/members.html') return render_template("users/members.html")

@ -3,8 +3,8 @@
from flask import flash from flask import flash
def flash_errors(form, category='warning'): def flash_errors(form, category="warning"):
"""Flash all errors for a form.""" """Flash all errors for a form."""
for field, errors in form.errors.items(): for field, errors in form.errors.items():
for error in errors: for error in errors:
flash('{0} - {1}'.format(getattr(form, field).label.text, error), category) flash("{0} - {1}".format(getattr(form, field).label.text, error), category)

Loading…
Cancel
Save