Add an option to fix imports before linting, make all code conform to the linting rules

master
Wagner Augusto Andreoli 9 years ago
parent 34c6faf630
commit 2986743a8c
  1. 2
      {{cookiecutter.app_name}}/README.rst
  2. 32
      {{cookiecutter.app_name}}/manage.py
  3. 1
      {{cookiecutter.app_name}}/tests/__init__.py
  4. 6
      {{cookiecutter.app_name}}/tests/conftest.py
  5. 16
      {{cookiecutter.app_name}}/tests/factories.py
  6. 5
      {{cookiecutter.app_name}}/tests/test_config.py
  7. 14
      {{cookiecutter.app_name}}/tests/test_forms.py
  8. 42
      {{cookiecutter.app_name}}/tests/test_functional.py
  9. 23
      {{cookiecutter.app_name}}/tests/test_models.py
  10. 1
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/__init__.py
  11. 22
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py
  12. 21
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/assets.py
  13. 8
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/compat.py
  14. 22
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py
  15. 20
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py
  16. 3
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/__init__.py
  17. 9
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py
  18. 36
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/views.py
  19. 10
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py
  20. 2
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/__init__.py
  21. 15
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/forms.py
  22. 22
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/models.py
  23. 9
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/views.py
  24. 5
      {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/utils.py

@ -20,7 +20,7 @@ Then run the following commands to bootstrap your environment.
:: ::
git clone https://github.com/{{cookiecutter.github_username}}/{{ cookiecutter.app_name }} git clone https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.app_name}}
cd {{cookiecutter.app_name}} cd {{cookiecutter.app_name}}
pip install -r requirements/dev.txt pip install -r requirements/dev.txt
python manage.py server python manage.py server

@ -2,18 +2,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Management script.""" """Management script."""
import os import os
from glob import glob
from subprocess import call from subprocess import call
from flask_script import Manager, Shell, Server, Command
from flask_script.commands import Clean, ShowUrls
from flask_migrate import MigrateCommand from flask_migrate import MigrateCommand
from flask_script import Command, Manager, Option, Server, Shell
from flask_script.commands import Clean, ShowUrls
from {{cookiecutter.app_name}}.app import create_app from {{cookiecutter.app_name}}.app import create_app
from {{cookiecutter.app_name}}.user.models import User
from {{cookiecutter.app_name}}.settings import DevConfig, ProdConfig
from {{cookiecutter.app_name}}.database import db from {{cookiecutter.app_name}}.database import db
from {{cookiecutter.app_name}}.settings import DevConfig, ProdConfig
from {{cookiecutter.app_name}}.user.models import User
if os.environ.get('{{cookiecutter.app_name | upper}}_ENV') == 'prod': if os.environ.get('MYFLASKAPP_ENV') == 'prod':
app = create_app(ProdConfig) app = create_app(ProdConfig)
else: else:
app = create_app(DevConfig) app = create_app(DevConfig)
@ -40,16 +41,29 @@ def test():
class Lint(Command): class Lint(Command):
"""Lint and check code style.""" """Lint and check code style."""
def run(self): def get_options(self):
"""Command line options."""
return (
Option('-f', '--fix',
action='store_true',
dest='fix_imports',
default=False,
help='Fix imports before linting'),
)
def run(self, fix_imports):
"""Run command.""" """Run command."""
skip = ['requirements'] skip = ['requirements']
files = [name for name in next(os.walk('.'))[1] if not name.startswith('.') and name not in skip] root_files = glob('*.py')
root_directories = [name for name in next(os.walk('.'))[1] if not name.startswith('.')]
arguments = [arg for arg in root_files + root_directories if arg not in skip]
command_line = ['isort', '-rc'] + files if fix_imports:
command_line = ['isort', '-rc'] + arguments
print('Fixing import order: %s' % ' '.join(command_line)) print('Fixing import order: %s' % ' '.join(command_line))
call(command_line) call(command_line)
command_line = ['flake8'] + files command_line = ['flake8'] + arguments
print('Checking code style: %s' % ' '.join(command_line)) print('Checking code style: %s' % ' '.join(command_line))
exit(call(command_line)) exit(call(command_line))

@ -1,19 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Defines fixtures available to all tests.""" """Defines fixtures available to all tests."""
import os
import pytest import pytest
from webtest import TestApp from webtest import TestApp
from {{ cookiecutter.app_name }}.settings import TestConfig
from {{cookiecutter.app_name}}.app import create_app from {{cookiecutter.app_name}}.app import create_app
from {{cookiecutter.app_name}}.database import db as _db from {{cookiecutter.app_name}}.database import db as _db
from {{cookiecutter.app_name}}.settings import TestConfig
from .factories import UserFactory from .factories import UserFactory
@pytest.yield_fixture(scope='function') @pytest.yield_fixture(scope='function')
def app(): def app():
"""An application for the tests."""
_app = create_app(TestConfig) _app = create_app(TestConfig)
ctx = _app.test_request_context() ctx = _app.test_request_context()
ctx.push() ctx.push()
@ -31,6 +31,7 @@ def testapp(app):
@pytest.yield_fixture(scope='function') @pytest.yield_fixture(scope='function')
def db(app): def db(app):
"""A database for the tests."""
_db.app = app _db.app = app
with app.app_context(): with app.app_context():
_db.create_all() _db.create_all()
@ -42,6 +43,7 @@ def db(app):
@pytest.fixture @pytest.fixture
def user(db): def user(db):
"""A user for the tests."""
user = UserFactory(password='myprecious') user = UserFactory(password='myprecious')
db.session.commit() db.session.commit()
return user return user

@ -1,23 +1,31 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from factory import Sequence, PostGenerationMethodCall """Factories to help in tests."""
from factory import PostGenerationMethodCall, Sequence
from factory.alchemy import SQLAlchemyModelFactory from factory.alchemy import SQLAlchemyModelFactory
from {{cookiecutter.app_name}}.user.models import User
from {{cookiecutter.app_name}}.database import db from {{cookiecutter.app_name}}.database import db
from {{cookiecutter.app_name}}.user.models import User
class BaseFactory(SQLAlchemyModelFactory): class BaseFactory(SQLAlchemyModelFactory):
"""Base factory."""
class Meta: class Meta:
"""Factory configuration."""
abstract = True abstract = True
sqlalchemy_session = db.session sqlalchemy_session = db.session
class UserFactory(BaseFactory): class UserFactory(BaseFactory):
username = Sequence(lambda n: "user{0}".format(n)) """User factory."""
email = Sequence(lambda n: "user{0}@example.com".format(n))
username = Sequence(lambda n: 'user{0}'.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:
"""Factory configuration."""
model = User model = User

@ -1,9 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Test configs."""
from {{cookiecutter.app_name}}.app import create_app from {{cookiecutter.app_name}}.app import create_app
from {{cookiecutter.app_name}}.settings import ProdConfig, DevConfig from {{cookiecutter.app_name}}.settings import DevConfig, ProdConfig
def test_production_config(): def test_production_config():
"""Production config."""
app = create_app(ProdConfig) app = create_app(ProdConfig)
assert app.config['ENV'] == 'prod' assert app.config['ENV'] == 'prod'
assert app.config['DEBUG'] is False assert app.config['DEBUG'] is False
@ -12,6 +14,7 @@ def test_production_config():
def test_dev_config(): def test_dev_config():
"""Development config."""
app = create_app(DevConfig) app = create_app(DevConfig)
assert app.config['ENV'] == 'dev' assert app.config['ENV'] == 'dev'
assert app.config['DEBUG'] is True assert app.config['DEBUG'] is True

@ -1,15 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pytest """Test forms."""
from {{cookiecutter.app_name}}.public.forms import LoginForm from {{cookiecutter.app_name}}.public.forms import LoginForm
from {{cookiecutter.app_name}}.user.forms import RegisterForm from {{cookiecutter.app_name}}.user.forms import RegisterForm
from .factories import UserFactory
class TestRegisterForm: class TestRegisterForm:
"""Register form."""
def test_validate_user_already_registered(self, user): def test_validate_user_already_registered(self, user):
# Enters username that is already registered """Enter username that is already registered."""
form = RegisterForm(username=user.username, email='foo@bar.com', form = RegisterForm(username=user.username, email='foo@bar.com',
password='example', confirm='example') password='example', confirm='example')
@ -17,7 +17,7 @@ class TestRegisterForm:
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):
# enters email that is already registered """Enter email that is already registered."""
form = RegisterForm(username='unique', email=user.email, form = RegisterForm(username='unique', email=user.email,
password='example', confirm='example') password='example', confirm='example')
@ -25,14 +25,17 @@ class TestRegisterForm:
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."""
form = RegisterForm(username='newusername', email='new@test.test', form = RegisterForm(username='newusername', email='new@test.test',
password='example', confirm='example') password='example', confirm='example')
assert form.validate() is True assert form.validate() is True
class TestLoginForm: class TestLoginForm:
"""Login form."""
def test_validate_success(self, user): def test_validate_success(self, user):
"""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')
@ -40,12 +43,14 @@ class TestLoginForm:
assert form.user == user assert form.user == user
def test_validate_unknown_username(self, db): 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 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."""
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')
@ -53,6 +58,7 @@ class TestLoginForm:
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."""
user.active = False user.active = False
user.set_password('example') user.set_password('example')
user.save() user.save()

@ -3,19 +3,20 @@
See: http://webtest.readthedocs.org/ See: http://webtest.readthedocs.org/
""" """
import pytest
from flask import url_for from flask import url_for
from {{cookiecutter.app_name}}.user.models import User from {{cookiecutter.app_name}}.user.models import User
from .factories import UserFactory from .factories import UserFactory
class TestLoggingIn: class TestLoggingIn:
"""Login."""
def test_can_log_in_returns_200(self, user, testapp): def test_can_log_in_returns_200(self, user, testapp):
"""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
@ -25,7 +26,8 @@ class TestLoggingIn:
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):
res = testapp.get("/") """Show alert on logout."""
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
@ -37,8 +39,9 @@ class TestLoggingIn:
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."""
# 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
@ -46,11 +49,12 @@ class TestLoggingIn:
# 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."""
# 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'
@ -58,19 +62,21 @@ class TestLoggingIn:
# 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:
"""Register a user."""
def test_can_register(self, user, testapp): def test_can_register(self, user, testapp):
"""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'
@ -82,10 +88,11 @@ class TestRegistering:
assert len(User.query.all()) == old_count + 1 assert len(User.query.all()) == old_count + 1
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."""
# 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'
@ -93,15 +100,16 @@ class TestRegistering:
# 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."""
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'
@ -109,4 +117,4 @@ class TestRegistering:
# Submits # Submits
res = form.submit() res = form.submit()
# sees error # sees error
assert "Username already registered" in res assert 'Username already registered' in res

@ -4,14 +4,17 @@ import datetime as dt
import pytest import pytest
from {{ cookiecutter.app_name }}.user.models import User, Role 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."""
def test_get_by_id(self): def test_get_by_id(self):
"""Get user by ID."""
user = User('foo', 'foo@bar.com') user = User('foo', 'foo@bar.com')
user.save() user.save()
@ -19,18 +22,21 @@ class TestUser:
assert retrieved == user assert retrieved == user
def test_created_at_defaults_to_datetime(self): 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() 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."""
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):
user = UserFactory(password="myprecious") """Test user factory."""
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)
@ -40,16 +46,19 @@ class TestUser:
assert user.check_password('myprecious') assert user.check_password('myprecious')
def test_check_password(self): def test_check_password(self):
user = User.create(username="foo", email="foo@bar.com", """Check password."""
password="foobarbaz123") user = User.create(username='foo', email='foo@bar.com',
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 = UserFactory(first_name="Foo", last_name="Bar") """User full name."""
assert user.full_name == "Foo Bar" user = UserFactory(first_name='Foo', last_name='Bar')
assert user.full_name == 'Foo Bar'
def test_roles(self): def test_roles(self):
"""Add a role to a user."""
role = Role(name='admin') role = Role(name='admin')
role.save() role.save()
u = UserFactory() u = UserFactory()

@ -2,22 +2,14 @@
"""The app module, containing the app factory function.""" """The app module, containing the app factory function."""
from flask import Flask, render_template from flask import Flask, render_template
from {{cookiecutter.app_name}}.settings import ProdConfig
from {{cookiecutter.app_name}}.assets import assets
from {{cookiecutter.app_name}}.extensions import (
bcrypt,
cache,
db,
login_manager,
migrate,
debug_toolbar,
)
from {{cookiecutter.app_name}} import public, user from {{cookiecutter.app_name}} import public, user
from {{cookiecutter.app_name}}.assets import assets
from {{cookiecutter.app_name}}.extensions import bcrypt, cache, db, debug_toolbar, login_manager, migrate
from {{cookiecutter.app_name}}.settings import ProdConfig
def create_app(config_object=ProdConfig): def create_app(config_object=ProdConfig):
"""An application factory, as explained here: """An application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
http://flask.pocoo.org/docs/patterns/appfactories/
:param config_object: The configuration object to use. :param config_object: The configuration object to use.
""" """
@ -30,6 +22,7 @@ def create_app(config_object=ProdConfig):
def register_extensions(app): def register_extensions(app):
"""Register Flask extensions."""
assets.init_app(app) assets.init_app(app)
bcrypt.init_app(app) bcrypt.init_app(app)
cache.init_app(app) cache.init_app(app)
@ -41,16 +34,19 @@ def register_extensions(app):
def register_blueprints(app): def register_blueprints(app):
"""Register Flask blueprints."""
app.register_blueprint(public.views.blueprint) app.register_blueprint(public.views.blueprint)
app.register_blueprint(user.views.blueprint) app.register_blueprint(user.views.blueprint)
return None return None
def register_errorhandlers(app): def register_errorhandlers(app):
"""Register error handlers."""
def render_error(error): def render_error(error):
"""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

@ -1,22 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Application assets."""
from flask_assets import Bundle, Environment from flask_assets import Bundle, Environment
css = Bundle( css = Bundle(
"libs/bootstrap/dist/css/bootstrap.css", 'libs/bootstrap/dist/css/bootstrap.css',
"css/style.css", 'css/style.css',
filters="cssmin", filters='cssmin',
output="public/css/common.css" output='public/css/common.css'
) )
js = Bundle( js = Bundle(
"libs/jQuery/dist/jquery.js", 'libs/jQuery/dist/jquery.js',
"libs/bootstrap/dist/js/bootstrap.js", 'libs/bootstrap/dist/js/bootstrap.js',
"js/plugins.js", 'js/plugins.js',
filters='jsmin', filters='jsmin',
output="public/js/common.js" output='public/js/common.js'
) )
assets = Environment() assets = Environment()
assets.register("js_all", js) assets.register('js_all', js)
assets.register("css_all", css) assets.register('css_all', css)

@ -7,11 +7,11 @@ import sys
PY2 = int(sys.version[0]) == 2 PY2 = int(sys.version[0]) == 2
if PY2: if PY2:
text_type = unicode text_type = unicode # noqa
binary_type = str binary_type = str
string_types = (str, unicode) string_types = (str, unicode) # noqa
unicode = unicode unicode = unicode # noqa
basestring = basestring basestring = basestring # noqa
else: else:
text_type = str text_type = str
binary_type = bytes binary_type = bytes

@ -1,11 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Database module, including the SQLAlchemy database object and DB-related """Database module, including the SQLAlchemy database object and DB-related utilities."""
utilities.
"""
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .extensions import db
from .compat import basestring from .compat import basestring
from .extensions import db
# Alias common SQLAlchemy names # Alias common SQLAlchemy names
Column = db.Column Column = db.Column
@ -13,9 +11,7 @@ relationship = relationship
class CRUDMixin(object): class CRUDMixin(object):
"""Mixin that adds convenience methods for CRUD (create, read, update, delete) """Mixin that adds convenience methods for CRUD (create, read, update, delete) operations."""
operations.
"""
@classmethod @classmethod
def create(cls, **kwargs): def create(cls, **kwargs):
@ -41,23 +37,25 @@ class CRUDMixin(object):
db.session.delete(self) db.session.delete(self)
return commit and db.session.commit() return commit and db.session.commit()
class Model(CRUDMixin, db.Model): class Model(CRUDMixin, db.Model):
"""Base model class that includes CRUD convenience methods.""" """Base model class that includes CRUD convenience methods."""
__abstract__ = True __abstract__ = True
# From Mike Bayer's "Building the app" talk # From Mike Bayer's "Building the app" talk
# https://speakerdeck.com/zzzeek/building-the-app # https://speakerdeck.com/zzzeek/building-the-app
class SurrogatePK(object): class SurrogatePK(object):
"""A mixin that adds a surrogate integer 'primary key' column named """A mixin that adds a surrogate integer 'primary key' column named ``id`` to any declarative-mapped class."""
``id`` to any declarative-mapped class.
"""
__table_args__ = {'extend_existing': True} __table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@classmethod @classmethod
def get_by_id(cls, id): def get_by_id(cls, id):
"""Get record by ID."""
if any( if any(
(isinstance(id, basestring) and id.isdigit(), (isinstance(id, basestring) and id.isdigit(),
isinstance(id, (int, float))), isinstance(id, (int, float))),
@ -66,7 +64,7 @@ class SurrogatePK(object):
return None return None
def ReferenceCol(tablename, nullable=False, pk_name='id', **kwargs): def ReferenceCol(tablename, nullable=False, pk_name='id', **kwargs): # noqa
"""Column that adds primary key foreign key reference. """Column that adds primary key foreign key reference.
Usage: :: Usage: ::
@ -75,5 +73,5 @@ def ReferenceCol(tablename, nullable=False, pk_name='id', **kwargs):
category = relationship('Category', backref='categories') category = relationship('Category', backref='categories')
""" """
return db.Column( return db.Column(
db.ForeignKey("{0}.{1}".format(tablename, pk_name)), db.ForeignKey('{0}.{1}'.format(tablename, pk_name)),
nullable=nullable, **kwargs) nullable=nullable, **kwargs)

@ -1,22 +1,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Extensions module. Each extension is initialized in the app factory located """Extensions module. Each extension is initialized in the app factory located in app.py."""
in app.py
"""
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
bcrypt = Bcrypt() from flask_cache import Cache
from flask_debugtoolbar import DebugToolbarExtension
from flask_login import LoginManager from flask_login import LoginManager
login_manager = LoginManager() from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
from flask_migrate import Migrate bcrypt = Bcrypt()
login_manager = LoginManager()
db = SQLAlchemy()
migrate = Migrate() migrate = Migrate()
from flask_cache import Cache
cache = Cache() cache = Cache()
from flask_debugtoolbar import DebugToolbarExtension
debug_toolbar = DebugToolbarExtension() debug_toolbar = DebugToolbarExtension()

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""The public module, including the homepage and user auth.""" """The public module, including the homepage and user auth."""
from . import views # noqa
from . import views

@ -1,20 +1,25 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Public forms."""
from flask_wtf import Form from flask_wtf import Form
from wtforms import TextField, PasswordField from wtforms import PasswordField, StringField
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
from {{cookiecutter.app_name}}.user.models import User from {{cookiecutter.app_name}}.user.models import User
class LoginForm(Form): class LoginForm(Form):
username = TextField('Username', validators=[DataRequired()]) """Login form."""
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."""
super(LoginForm, self).__init__(*args, **kwargs) super(LoginForm, self).__init__(*args, **kwargs)
self.user = None self.user = None
def validate(self): def validate(self):
"""Validate the form."""
initial_validation = super(LoginForm, self).validate() initial_validation = super(LoginForm, self).validate()
if not initial_validation: if not initial_validation:
return False return False

@ -1,63 +1,63 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Public section, including homepage and signup.""" """Public section, including homepage and signup."""
from flask import (Blueprint, request, render_template, flash, url_for, from flask import Blueprint, flash, redirect, render_template, request, url_for
redirect, session) from flask_login import login_required, login_user, logout_user
from flask_login import login_user, login_required, logout_user
from {{cookiecutter.app_name}}.extensions import login_manager from {{cookiecutter.app_name}}.extensions import login_manager
from {{cookiecutter.app_name}}.user.models import User
from {{cookiecutter.app_name}}.public.forms import LoginForm from {{cookiecutter.app_name}}.public.forms import LoginForm
from {{cookiecutter.app_name}}.user.forms import RegisterForm from {{cookiecutter.app_name}}.user.forms import RegisterForm
from {{cookiecutter.app_name}}.user.models import User
from {{cookiecutter.app_name}}.utils import flash_errors from {{cookiecutter.app_name}}.utils import flash_errors
from {{cookiecutter.app_name}}.database import db
blueprint = Blueprint('public', __name__, static_folder="../static") blueprint = Blueprint('public', __name__, static_folder='../static')
@login_manager.user_loader @login_manager.user_loader
def load_user(id): def load_user(id):
"""Load user by ID."""
return User.get_by_id(int(id)) return User.get_by_id(int(id))
@blueprint.route("/", methods=["GET", "POST"]) @blueprint.route('/', methods=['GET', 'POST'])
def home(): def home():
"""Home page."""
form = LoginForm(request.form) form = LoginForm(request.form)
# 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_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."""
form = RegisterForm(request.form, csrf_enabled=False) form = RegisterForm(request.form, csrf_enabled=False)
if form.validate_on_submit(): if form.validate_on_submit():
new_user = User.create(username=form.username.data, User.create(username=form.username.data, email=form.email.data, password=form.password.data, active=True)
email=form.email.data, flash('Thank you for registering. You can now log in.', 'success')
password=form.password.data,
active=True)
flash("Thank you for registering. You can now log in.", 'success')
return redirect(url_for('public.home')) 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."""
form = LoginForm(request.form) form = LoginForm(request.form)
return render_template("public/about.html", form=form) return render_template('public/about.html', form=form)

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Application configuration."""
import os import os
os_env = os.environ os_env = os.environ
class Config(object): class Config(object):
SECRET_KEY = os_env.get('{{cookiecutter.app_name | upper}}_SECRET', 'secret-key') # TODO: Change me """Base configuration."""
SECRET_KEY = os_env.get('MYFLASKAPP_SECRET', 'secret-key') # TODO: Change me
APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory
PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir))
BCRYPT_LOG_ROUNDS = 13 BCRYPT_LOG_ROUNDS = 13
@ -13,10 +16,12 @@ class Config(object):
DEBUG_TB_ENABLED = False # Disable Debug toolbar DEBUG_TB_ENABLED = False # Disable Debug toolbar
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
class ProdConfig(Config): class ProdConfig(Config):
"""Production configuration.""" """Production configuration."""
ENV = 'prod' ENV = 'prod'
DEBUG = False DEBUG = False
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/example' # TODO: Change me SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/example' # TODO: Change me
@ -25,6 +30,7 @@ class ProdConfig(Config):
class DevConfig(Config): class DevConfig(Config):
"""Development configuration.""" """Development configuration."""
ENV = 'dev' ENV = 'dev'
DEBUG = True DEBUG = True
DB_NAME = 'dev.db' DB_NAME = 'dev.db'
@ -37,6 +43,8 @@ class DevConfig(Config):
class TestConfig(Config): class TestConfig(Config):
"""Test configuration."""
TESTING = True TESTING = True
DEBUG = True DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite://' SQLALCHEMY_DATABASE_URI = 'sqlite://'

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""The user module.""" """The user module."""
from . import views from . import views # noqa

@ -1,15 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""User forms."""
from flask_wtf import Form from flask_wtf import Form
from wtforms import TextField, PasswordField from wtforms import PasswordField, StringField
from wtforms.validators import DataRequired, Email, EqualTo, Length from wtforms.validators import DataRequired, Email, EqualTo, Length
from .models import User from .models import User
class RegisterForm(Form): class RegisterForm(Form):
username = TextField('Username', """Register form."""
username = StringField('Username',
validators=[DataRequired(), Length(min=3, max=25)]) validators=[DataRequired(), Length(min=3, max=25)])
email = TextField('Email', email = StringField('Email',
validators=[DataRequired(), Email(), Length(min=6, max=40)]) validators=[DataRequired(), Email(), Length(min=6, max=40)])
password = PasswordField('Password', password = PasswordField('Password',
validators=[DataRequired(), Length(min=6, max=40)]) validators=[DataRequired(), Length(min=6, max=40)])
@ -17,19 +20,21 @@ class RegisterForm(Form):
[DataRequired(), EqualTo('password', message='Passwords must match')]) [DataRequired(), EqualTo('password', message='Passwords must match')])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Create instance."""
super(RegisterForm, self).__init__(*args, **kwargs) super(RegisterForm, self).__init__(*args, **kwargs)
self.user = None self.user = None
def validate(self): def validate(self):
"""Validate the form."""
initial_validation = super(RegisterForm, self).validate() initial_validation = super(RegisterForm, self).validate()
if not initial_validation: if not initial_validation:
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

@ -1,33 +1,32 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""User models."""
import datetime as dt import datetime as dt
from flask_login import UserMixin from flask_login import UserMixin
from {{cookiecutter.app_name}}.database import Column, Model, ReferenceCol, SurrogatePK, db, relationship
from {{cookiecutter.app_name}}.extensions import bcrypt from {{cookiecutter.app_name}}.extensions import bcrypt
from {{cookiecutter.app_name}}.database import (
Column,
db,
Model,
ReferenceCol,
relationship,
SurrogatePK,
)
class Role(SurrogatePK, Model): class Role(SurrogatePK, Model):
"""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 = ReferenceCol('users', nullable=True) user_id = ReferenceCol('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."""
db.Model.__init__(self, name=name, **kwargs) db.Model.__init__(self, name=name, **kwargs)
def __repr__(self): 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): class User(UserMixin, SurrogatePK, Model):
"""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)
@ -41,6 +40,7 @@ class User(UserMixin, SurrogatePK, Model):
is_admin = Column(db.Boolean(), default=False) is_admin = Column(db.Boolean(), default=False)
def __init__(self, username, email, password=None, **kwargs): def __init__(self, username, email, password=None, **kwargs):
"""Create instance."""
db.Model.__init__(self, username=username, email=email, **kwargs) db.Model.__init__(self, username=username, email=email, **kwargs)
if password: if password:
self.set_password(password) self.set_password(password)
@ -48,14 +48,18 @@ class User(UserMixin, SurrogatePK, Model):
self.password = None self.password = None
def set_password(self, password): def set_password(self, password):
"""Set password."""
self.password = bcrypt.generate_password_hash(password) self.password = bcrypt.generate_password_hash(password)
def check_password(self, value): def check_password(self, value):
"""Check password."""
return bcrypt.check_password_hash(self.password, value) return bcrypt.check_password_hash(self.password, value)
@property @property
def full_name(self): def full_name(self):
return "{0} {1}".format(self.first_name, self.last_name) """Full user name."""
return '{0} {1}'.format(self.first_name, self.last_name)
def __repr__(self): 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)

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""User views."""
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', blueprint = Blueprint('user', __name__, url_prefix='/users', static_folder='../static')
static_folder="../static")
@blueprint.route("/") @blueprint.route('/')
@login_required @login_required
def members(): def members():
return render_template("users/members.html") """List members."""
return render_template('users/members.html')

@ -3,9 +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}" flash('{0} - {1}'.format(getattr(form, field).label.text, error), category)
.format(getattr(form, field).label.text, error), category)

Loading…
Cancel
Save