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
language: python
python:
- 3.5
- 3.6
- 3.7

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

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

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

@ -1,34 +1,34 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Invoke tasks."""
import os
import json
import os
import shutil
import webbrowser
from invoke import task
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)
# Match default value of app_name from cookiecutter.json
COOKIECUTTER_SETTINGS["app_name"] = 'my_flask_app'
COOKIE = os.path.join(HERE, COOKIECUTTER_SETTINGS['app_name'])
REQUIREMENTS = os.path.join(COOKIE, 'requirements', 'dev.txt')
COOKIECUTTER_SETTINGS["app_name"] = "my_flask_app"
COOKIE = os.path.join(HERE, COOKIECUTTER_SETTINGS["app_name"])
REQUIREMENTS = os.path.join(COOKIE, "requirements", "dev.txt")
def _run_npm_command(ctx, command):
os.chdir(COOKIE)
ctx.run('npm {0}'.format(command), echo=True)
ctx.run("npm {0}".format(command), echo=True)
os.chdir(HERE)
@task
def build(ctx):
"""Build the cookiecutter."""
ctx.run('cookiecutter {0} --no-input'.format(HERE))
_run_npm_command(ctx, 'install')
_run_npm_command(ctx, 'run build')
ctx.run("cookiecutter {0} --no-input".format(HERE))
_run_npm_command(ctx, "install")
_run_npm_command(ctx, "run build")
@task
@ -36,33 +36,34 @@ def clean(ctx):
"""Clean out generated cookiecutter."""
if os.path.exists(COOKIE):
shutil.rmtree(COOKIE)
print('Removed {0}'.format(COOKIE))
print("Removed {0}".format(COOKIE))
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)
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])
def test(ctx):
"""Run lint commands and tests."""
ctx.run('pip install -r {0} --ignore-installed'.format(REQUIREMENTS),
echo=True)
_run_npm_command(ctx, 'run lint')
ctx.run("pip install -r {0} --ignore-installed".format(REQUIREMENTS), echo=True)
_run_npm_command(ctx, "run lint")
os.chdir(COOKIE)
shutil.copyfile(os.path.join(COOKIE, '.env.example'),
os.path.join(COOKIE, '.env'))
shutil.copyfile(os.path.join(COOKIE, ".env.example"), os.path.join(COOKIE, ".env"))
os.environ["FLASK_ENV"] = "production"
os.environ["FLASK_DEBUG"] = "0"
_run_flask_command(ctx, 'lint')
_run_flask_command(ctx, 'test')
_run_flask_command(ctx, "lint", "--check")
_run_flask_command(ctx, "test")
@task
def readme(ctx, browse=False):
ctx.run('rst2html.py README.rst > README.html')
ctx.run("rst2html.py README.rst > README.html")
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
dist: xenial
language: python
env:
- FLASK_APP=autoapp.py FLASK_DEBUG=1
python:
- 2.7
- 3.4
- 3.5
- 3.6
- 3.7
install:
@ -16,5 +14,5 @@ install:
before_script:
- npm run lint
- npm run build
- flask lint
- flask lint --check
script: flask test

@ -53,12 +53,11 @@ factory-boy = "==2.12.*"
pdbpp = "==0.10.0"
# Lint and code style
black = "==19.3b0"
flake8 = "==3.7.7"
flake8-blind-except = "==0.1.1"
flake8-debugger = "==3.1.0"
flake8-docstrings = "==1.3.0"
flake8-isort = "==2.7.0"
flake8-quotes = "==2.0.1"
isort = "==4.3.20"
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``.
Running Tests
-------------
Running Tests/Linter
--------------------
To run all tests, run ::
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
----------

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

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

@ -1,3 +1,11 @@
[flake8]
ignore = D401
ignore = D401,D202,E226,E302,E41
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
def app():
"""An application for the tests."""
_app = create_app('tests.settings')
"""Create application for the tests."""
_app = create_app("tests.settings")
ctx = _app.test_request_context()
ctx.push()
@ -24,13 +24,13 @@ def app():
@pytest.fixture
def testapp(app):
"""A Webtest app."""
"""Create Webtest app."""
return TestApp(app)
@pytest.fixture
def db(app):
"""A database for the tests."""
"""Create database for the tests."""
_db.app = app
with app.app_context():
_db.create_all()
@ -44,7 +44,7 @@ def db(app):
@pytest.fixture
def user(db):
"""A user for the tests."""
user = UserFactory(password='myprecious')
"""Create user for the tests."""
user = UserFactory(password="myprecious")
db.session.commit()
return user

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

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

@ -10,24 +10,33 @@ class TestRegisterForm:
def test_validate_user_already_registered(self, user):
"""Enter username that is already registered."""
form = RegisterForm(username=user.username, email='foo@bar.com',
password='example', confirm='example')
form = RegisterForm(
username=user.username,
email="foo@bar.com",
password="example",
confirm="example",
)
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):
"""Enter email that is already registered."""
form = RegisterForm(username='unique', email=user.email,
password='example', confirm='example')
form = RegisterForm(
username="unique", email=user.email, password="example", confirm="example"
)
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):
"""Register with success."""
form = RegisterForm(username='newusername', email='new@test.test',
password='example', confirm='example')
form = RegisterForm(
username="newusername",
email="new@test.test",
password="example",
confirm="example",
)
assert form.validate() is True
@ -36,33 +45,33 @@ class TestLoginForm:
def test_validate_success(self, user):
"""Login successful."""
user.set_password('example')
user.set_password("example")
user.save()
form = LoginForm(username=user.username, password='example')
form = LoginForm(username=user.username, password="example")
assert form.validate() is True
assert form.user == user
def test_validate_unknown_username(self, db):
"""Unknown username."""
form = LoginForm(username='unknown', password='example')
form = LoginForm(username="unknown", password="example")
assert form.validate() is False
assert 'Unknown username' in form.username.errors
assert "Unknown username" in form.username.errors
assert form.user is None
def test_validate_invalid_password(self, user):
"""Invalid password."""
user.set_password('example')
user.set_password("example")
user.save()
form = LoginForm(username=user.username, password='wrongpassword')
form = LoginForm(username=user.username, password="wrongpassword")
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):
"""Inactive user."""
user.active = False
user.set_password('example')
user.set_password("example")
user.save()
# 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 '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):
"""Login successful."""
# Goes to homepage
res = testapp.get('/')
res = testapp.get("/")
# Fills out login form in navbar
form = res.forms['loginForm']
form['username'] = user.username
form['password'] = 'myprecious'
form = res.forms["loginForm"]
form["username"] = user.username
form["password"] = "myprecious"
# Submits
res = form.submit().follow()
assert res.status_code == 200
def test_sees_alert_on_log_out(self, user, testapp):
"""Show alert on logout."""
res = testapp.get('/')
res = testapp.get("/")
# Fills out login form in navbar
form = res.forms['loginForm']
form['username'] = user.username
form['password'] = 'myprecious'
form = res.forms["loginForm"]
form["username"] = user.username
form["password"] = "myprecious"
# Submits
res = form.submit().follow()
res = testapp.get(url_for('public.logout')).follow()
res = testapp.get(url_for("public.logout")).follow()
# 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):
"""Show error if password is incorrect."""
# Goes to homepage
res = testapp.get('/')
res = testapp.get("/")
# Fills out login form, password incorrect
form = res.forms['loginForm']
form['username'] = user.username
form['password'] = 'wrong'
form = res.forms["loginForm"]
form["username"] = user.username
form["password"] = "wrong"
# Submits
res = form.submit()
# sees error
assert 'Invalid password' in res
assert "Invalid password" in res
def test_sees_error_message_if_username_doesnt_exist(self, user, testapp):
"""Show error if username doesn't exist."""
# Goes to homepage
res = testapp.get('/')
res = testapp.get("/")
# Fills out login form, password incorrect
form = res.forms['loginForm']
form['username'] = 'unknown'
form['password'] = 'myprecious'
form = res.forms["loginForm"]
form["username"] = "unknown"
form["password"] = "myprecious"
# Submits
res = form.submit()
# sees error
assert 'Unknown user' in res
assert "Unknown user" in res
class TestRegistering:
@ -72,15 +72,15 @@ class TestRegistering:
"""Register a new user."""
old_count = len(User.query.all())
# Goes to homepage
res = testapp.get('/')
res = testapp.get("/")
# Clicks Create Account button
res = res.click('Create account')
res = res.click("Create account")
# Fills out the form
form = res.forms['registerForm']
form['username'] = 'foobar'
form['email'] = 'foo@bar.com'
form['password'] = 'secret'
form['confirm'] = 'secret'
form = res.forms["registerForm"]
form["username"] = "foobar"
form["email"] = "foo@bar.com"
form["password"] = "secret"
form["confirm"] = "secret"
# Submits
res = form.submit().follow()
assert res.status_code == 200
@ -90,31 +90,31 @@ class TestRegistering:
def test_sees_error_message_if_passwords_dont_match(self, user, testapp):
"""Show error if passwords don't match."""
# 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
form = res.forms['registerForm']
form['username'] = 'foobar'
form['email'] = 'foo@bar.com'
form['password'] = 'secret'
form['confirm'] = 'secrets'
form = res.forms["registerForm"]
form["username"] = "foobar"
form["email"] = "foo@bar.com"
form["password"] = "secret"
form["confirm"] = "secrets"
# Submits
res = form.submit()
# 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):
"""Show error if user already registered."""
user = UserFactory(active=True) # A registered user
user.save()
# 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
form = res.forms['registerForm']
form['username'] = user.username
form['email'] = 'foo@bar.com'
form['password'] = 'secret'
form['confirm'] = 'secret'
form = res.forms["registerForm"]
form["username"] = user.username
form["email"] = "foo@bar.com"
form["password"] = "secret"
form["confirm"] = "secret"
# Submits
res = form.submit()
# 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
@pytest.mark.usefixtures('db')
@pytest.mark.usefixtures("db")
class TestUser:
"""User tests."""
def test_get_by_id(self):
"""Get user by ID."""
user = User('foo', 'foo@bar.com')
user = User("foo", "foo@bar.com")
user.save()
retrieved = User.get_by_id(user.id)
@ -23,43 +23,42 @@ class TestUser:
def test_created_at_defaults_to_datetime(self):
"""Test creation date."""
user = User(username='foo', email='foo@bar.com')
user = User(username="foo", email="foo@bar.com")
user.save()
assert bool(user.created_at)
assert isinstance(user.created_at, dt.datetime)
def test_password_is_nullable(self):
"""Test null password."""
user = User(username='foo', email='foo@bar.com')
user = User(username="foo", email="foo@bar.com")
user.save()
assert user.password is None
def test_factory(self, db):
"""Test user factory."""
user = UserFactory(password='myprecious')
user = UserFactory(password="myprecious")
db.session.commit()
assert bool(user.username)
assert bool(user.email)
assert bool(user.created_at)
assert user.is_admin is False
assert user.active is True
assert user.check_password('myprecious')
assert user.check_password("myprecious")
def test_check_password(self):
"""Check password."""
user = User.create(username='foo', email='foo@bar.com',
password='foobarbaz123')
assert user.check_password('foobarbaz123') is True
assert user.check_password('barfoobaz') is False
user = User.create(username="foo", email="foo@bar.com", password="foobarbaz123")
assert user.check_password("foobarbaz123") is True
assert user.check_password("barfoobaz") is False
def test_full_name(self):
"""User full name."""
user = UserFactory(first_name='Foo', last_name='Bar')
assert user.full_name == 'Foo Bar'
user = UserFactory(first_name="Foo", last_name="Bar")
assert user.full_name == "Foo Bar"
def test_roles(self):
"""Add a role to a user."""
role = Role(name='admin')
role = Role(name="admin")
role.save()
user = UserFactory()
user.roles.append(role)

@ -67,8 +67,10 @@ module.exports = {
},
{ 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: /\.(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 } },
],
},

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

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

@ -47,7 +47,7 @@ class Model(CRUDMixin, db.Model):
class SurrogatePK(object):
"""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)
@ -55,14 +55,18 @@ class SurrogatePK(object):
def get_by_id(cls, record_id):
"""Get record by ID."""
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 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.
Usage: ::
@ -74,5 +78,7 @@ def reference_col(tablename, nullable=False, pk_name='id', foreign_key_kwargs=No
column_kwargs = column_kwargs or {}
return Column(
db.ForeignKey('{0}.{1}'.format(tablename, pk_name), **foreign_key_kwargs),
nullable=nullable, **column_kwargs)
db.ForeignKey("{0}.{1}".format(tablename, pk_name), **foreign_key_kwargs),
nullable=nullable,
**column_kwargs
)

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

@ -1,6 +1,14 @@
# -*- coding: utf-8 -*-
"""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 {{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}}.utils import flash_errors
blueprint = Blueprint('public', __name__, static_folder='../static')
blueprint = Blueprint("public", __name__, static_folder="../static")
@login_manager.user_loader
@ -18,47 +26,52 @@ def load_user(user_id):
return User.get_by_id(int(user_id))
@blueprint.route('/', methods=['GET', 'POST'])
@blueprint.route("/", methods=["GET", "POST"])
def home():
"""Home page."""
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
if request.method == 'POST':
if request.method == "POST":
if form.validate_on_submit():
login_user(form.user)
flash('You are logged in.', 'success')
redirect_url = request.args.get('next') or url_for('user.members')
flash("You are logged in.", "success")
redirect_url = request.args.get("next") or url_for("user.members")
return redirect(redirect_url)
else:
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
def logout():
"""Logout."""
logout_user()
flash('You are logged out.', 'info')
return redirect(url_for('public.home'))
flash("You are logged out.", "info")
return redirect(url_for("public.home"))
@blueprint.route('/register/', methods=['GET', 'POST'])
@blueprint.route("/register/", methods=["GET", "POST"])
def register():
"""Register new user."""
form = RegisterForm(request.form)
if form.validate_on_submit():
User.create(username=form.username.data, 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'))
User.create(
username=form.username.data,
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:
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():
"""About page."""
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.read_env()
ENV = env.str('FLASK_ENV', default='production')
DEBUG = ENV == 'development'
SQLALCHEMY_DATABASE_URI = env.str('DATABASE_URL')
SECRET_KEY = env.str('SECRET_KEY')
BCRYPT_LOG_ROUNDS = env.int('BCRYPT_LOG_ROUNDS', default=13)
ENV = env.str("FLASK_ENV", default="production")
DEBUG = ENV == "development"
SQLALCHEMY_DATABASE_URI = env.str("DATABASE_URL")
SECRET_KEY = env.str("SECRET_KEY")
BCRYPT_LOG_ROUNDS = env.int("BCRYPT_LOG_ROUNDS", default=13)
DEBUG_TB_ENABLED = DEBUG
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
WEBPACK_MANIFEST_PATH = 'webpack/manifest.json'
WEBPACK_MANIFEST_PATH = "webpack/manifest.json"

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

@ -4,17 +4,24 @@ import datetime as dt
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
class Role(SurrogatePK, Model):
"""A role for a user."""
__tablename__ = 'roles'
__tablename__ = "roles"
name = Column(db.String(80), unique=True, nullable=False)
user_id = reference_col('users', nullable=True)
user = relationship('User', backref='roles')
user_id = reference_col("users", nullable=True)
user = relationship("User", backref="roles")
def __init__(self, name, **kwargs):
"""Create instance."""
@ -22,13 +29,13 @@ class Role(SurrogatePK, Model):
def __repr__(self):
"""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):
"""A user of the app."""
__tablename__ = 'users'
__tablename__ = "users"
username = Column(db.String(80), unique=True, nullable=False)
email = Column(db.String(80), unique=True, nullable=False)
#: The hashed password
@ -58,8 +65,8 @@ class User(UserMixin, SurrogatePK, Model):
@property
def full_name(self):
"""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):
"""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_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
def members():
"""List members."""
return render_template('users/members.html')
return render_template("users/members.html")

@ -3,8 +3,8 @@
from flask import flash
def flash_errors(form, category='warning'):
def flash_errors(form, category="warning"):
"""Flash all errors for a form."""
for field, errors in form.errors.items():
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