From 2986743a8c4da82f0e2d71339ed808384bde65ff Mon Sep 17 00:00:00 2001 From: Wagner Augusto Andreoli Date: Fri, 6 Nov 2015 03:10:01 +0100 Subject: [PATCH] Add an option to fix imports before linting, make all code conform to the linting rules --- {{cookiecutter.app_name}}/README.rst | 2 +- {{cookiecutter.app_name}}/manage.py | 36 +++++++++++----- {{cookiecutter.app_name}}/tests/__init__.py | 1 + {{cookiecutter.app_name}}/tests/conftest.py | 6 ++- {{cookiecutter.app_name}}/tests/factories.py | 16 +++++-- .../tests/test_config.py | 5 ++- {{cookiecutter.app_name}}/tests/test_forms.py | 20 +++++---- .../tests/test_functional.py | 42 +++++++++++-------- .../tests/test_models.py | 23 ++++++---- .../{{cookiecutter.app_name}}/__init__.py | 1 + .../{{cookiecutter.app_name}}/app.py | 22 ++++------ .../{{cookiecutter.app_name}}/assets.py | 21 +++++----- .../{{cookiecutter.app_name}}/compat.py | 8 ++-- .../{{cookiecutter.app_name}}/database.py | 26 ++++++------ .../{{cookiecutter.app_name}}/extensions.py | 20 ++++----- .../public/__init__.py | 3 +- .../{{cookiecutter.app_name}}/public/forms.py | 9 +++- .../{{cookiecutter.app_name}}/public/views.py | 36 ++++++++-------- .../{{cookiecutter.app_name}}/settings.py | 10 ++++- .../user/__init__.py | 2 +- .../{{cookiecutter.app_name}}/user/forms.py | 23 ++++++---- .../{{cookiecutter.app_name}}/user/models.py | 22 ++++++---- .../{{cookiecutter.app_name}}/user/views.py | 9 ++-- .../{{cookiecutter.app_name}}/utils.py | 5 +-- 24 files changed, 215 insertions(+), 153 deletions(-) diff --git a/{{cookiecutter.app_name}}/README.rst b/{{cookiecutter.app_name}}/README.rst index 9e11961..25ac2d3 100644 --- a/{{cookiecutter.app_name}}/README.rst +++ b/{{cookiecutter.app_name}}/README.rst @@ -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}} pip install -r requirements/dev.txt python manage.py server diff --git a/{{cookiecutter.app_name}}/manage.py b/{{cookiecutter.app_name}}/manage.py index 6fbb022..eea8d24 100644 --- a/{{cookiecutter.app_name}}/manage.py +++ b/{{cookiecutter.app_name}}/manage.py @@ -2,18 +2,19 @@ # -*- coding: utf-8 -*- """Management script.""" import os +from glob import glob 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_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}}.user.models import User -from {{cookiecutter.app_name}}.settings import DevConfig, ProdConfig 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) else: app = create_app(DevConfig) @@ -40,16 +41,29 @@ def test(): class Lint(Command): """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.""" 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 - print('Fixing import order: %s' % ' '.join(command_line)) - call(command_line) + if fix_imports: + command_line = ['isort', '-rc'] + arguments + print('Fixing import order: %s' % ' '.join(command_line)) + call(command_line) - command_line = ['flake8'] + files + command_line = ['flake8'] + arguments print('Checking code style: %s' % ' '.join(command_line)) exit(call(command_line)) diff --git a/{{cookiecutter.app_name}}/tests/__init__.py b/{{cookiecutter.app_name}}/tests/__init__.py index e69de29..b0c2ab8 100644 --- a/{{cookiecutter.app_name}}/tests/__init__.py +++ b/{{cookiecutter.app_name}}/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the app.""" diff --git a/{{cookiecutter.app_name}}/tests/conftest.py b/{{cookiecutter.app_name}}/tests/conftest.py index 31118e7..f2472b5 100644 --- a/{{cookiecutter.app_name}}/tests/conftest.py +++ b/{{cookiecutter.app_name}}/tests/conftest.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- """Defines fixtures available to all tests.""" -import os import pytest from webtest import TestApp -from {{ cookiecutter.app_name }}.settings import TestConfig from {{cookiecutter.app_name}}.app import create_app from {{cookiecutter.app_name}}.database import db as _db +from {{cookiecutter.app_name}}.settings import TestConfig from .factories import UserFactory @pytest.yield_fixture(scope='function') def app(): + """An application for the tests.""" _app = create_app(TestConfig) ctx = _app.test_request_context() ctx.push() @@ -31,6 +31,7 @@ def testapp(app): @pytest.yield_fixture(scope='function') def db(app): + """A database for the tests.""" _db.app = app with app.app_context(): _db.create_all() @@ -42,6 +43,7 @@ def db(app): @pytest.fixture def user(db): + """A user for the tests.""" user = UserFactory(password='myprecious') db.session.commit() return user diff --git a/{{cookiecutter.app_name}}/tests/factories.py b/{{cookiecutter.app_name}}/tests/factories.py index 10b98ab..33efe7a 100644 --- a/{{cookiecutter.app_name}}/tests/factories.py +++ b/{{cookiecutter.app_name}}/tests/factories.py @@ -1,23 +1,31 @@ # -*- coding: utf-8 -*- -from factory import Sequence, PostGenerationMethodCall +"""Factories to help in tests.""" +from factory import PostGenerationMethodCall, Sequence from factory.alchemy import SQLAlchemyModelFactory -from {{cookiecutter.app_name}}.user.models import User from {{cookiecutter.app_name}}.database import db +from {{cookiecutter.app_name}}.user.models import User class BaseFactory(SQLAlchemyModelFactory): + """Base factory.""" class Meta: + """Factory configuration.""" + abstract = True sqlalchemy_session = db.session class UserFactory(BaseFactory): - username = Sequence(lambda n: "user{0}".format(n)) - email = Sequence(lambda n: "user{0}@example.com".format(n)) + """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') active = True class Meta: + """Factory configuration.""" + model = User diff --git a/{{cookiecutter.app_name}}/tests/test_config.py b/{{cookiecutter.app_name}}/tests/test_config.py index c945dc7..4223730 100644 --- a/{{cookiecutter.app_name}}/tests/test_config.py +++ b/{{cookiecutter.app_name}}/tests/test_config.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- +"""Test configs.""" 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(): + """Production config.""" app = create_app(ProdConfig) assert app.config['ENV'] == 'prod' assert app.config['DEBUG'] is False @@ -12,6 +14,7 @@ def test_production_config(): def test_dev_config(): + """Development config.""" app = create_app(DevConfig) assert app.config['ENV'] == 'dev' assert app.config['DEBUG'] is True diff --git a/{{cookiecutter.app_name}}/tests/test_forms.py b/{{cookiecutter.app_name}}/tests/test_forms.py index 53e5bd0..590d07f 100644 --- a/{{cookiecutter.app_name}}/tests/test_forms.py +++ b/{{cookiecutter.app_name}}/tests/test_forms.py @@ -1,38 +1,41 @@ # -*- coding: utf-8 -*- -import pytest +"""Test forms.""" from {{cookiecutter.app_name}}.public.forms import LoginForm from {{cookiecutter.app_name}}.user.forms import RegisterForm -from .factories import UserFactory class TestRegisterForm: + """Register form.""" 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', - password='example', confirm='example') + password='example', confirm='example') assert form.validate() is False assert 'Username already registered' in form.username.errors 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, - password='example', confirm='example') + password='example', confirm='example') assert form.validate() is False 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') + password='example', confirm='example') assert form.validate() is True class TestLoginForm: + """Login form.""" def test_validate_success(self, user): + """Login successful.""" user.set_password('example') user.save() form = LoginForm(username=user.username, password='example') @@ -40,12 +43,14 @@ class TestLoginForm: assert form.user == user def test_validate_unknown_username(self, db): + """Unknown username.""" form = LoginForm(username='unknown', password='example') assert form.validate() is False 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.save() form = LoginForm(username=user.username, password='wrongpassword') @@ -53,6 +58,7 @@ class TestLoginForm: assert 'Invalid password' in form.password.errors def test_validate_inactive_user(self, user): + """Inactive user.""" user.active = False user.set_password('example') user.save() diff --git a/{{cookiecutter.app_name}}/tests/test_functional.py b/{{cookiecutter.app_name}}/tests/test_functional.py index ac489cd..6b9151f 100644 --- a/{{cookiecutter.app_name}}/tests/test_functional.py +++ b/{{cookiecutter.app_name}}/tests/test_functional.py @@ -3,19 +3,20 @@ See: http://webtest.readthedocs.org/ """ -import pytest from flask import url_for - from {{cookiecutter.app_name}}.user.models import User + from .factories import UserFactory class TestLoggingIn: + """Login.""" 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 @@ -25,7 +26,8 @@ class TestLoggingIn: assert res.status_code == 200 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 form = res.forms['loginForm'] form['username'] = user.username @@ -37,8 +39,9 @@ class TestLoggingIn: 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 @@ -46,11 +49,12 @@ class TestLoggingIn: # 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' @@ -58,19 +62,21 @@ class TestLoggingIn: # Submits res = form.submit() # sees error - assert "Unknown user" in res + assert 'Unknown user' in res class TestRegistering: + """Register a user.""" def test_can_register(self, user, testapp): + """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 = res.forms['registerForm'] form['username'] = 'foobar' form['email'] = 'foo@bar.com' form['password'] = 'secret' @@ -82,10 +88,11 @@ class TestRegistering: assert len(User.query.all()) == old_count + 1 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 = res.forms['registerForm'] form['username'] = 'foobar' form['email'] = 'foo@bar.com' form['password'] = 'secret' @@ -93,15 +100,16 @@ class TestRegistering: # 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 = res.forms['registerForm'] form['username'] = user.username form['email'] = 'foo@bar.com' form['password'] = 'secret' @@ -109,4 +117,4 @@ class TestRegistering: # Submits res = form.submit() # sees error - assert "Username already registered" in res + assert 'Username already registered' in res diff --git a/{{cookiecutter.app_name}}/tests/test_models.py b/{{cookiecutter.app_name}}/tests/test_models.py index feb7078..d321f5f 100644 --- a/{{cookiecutter.app_name}}/tests/test_models.py +++ b/{{cookiecutter.app_name}}/tests/test_models.py @@ -4,14 +4,17 @@ import datetime as dt import pytest -from {{ cookiecutter.app_name }}.user.models import User, Role +from {{cookiecutter.app_name}}.user.models import Role, User + from .factories import UserFactory @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.save() @@ -19,18 +22,21 @@ class TestUser: assert retrieved == user def test_created_at_defaults_to_datetime(self): + """Test creation date.""" 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.save() assert user.password is None def test_factory(self, db): - user = UserFactory(password="myprecious") + """Test user factory.""" + user = UserFactory(password='myprecious') db.session.commit() assert bool(user.username) assert bool(user.email) @@ -40,16 +46,19 @@ class TestUser: assert user.check_password('myprecious') def test_check_password(self): - user = User.create(username="foo", email="foo@bar.com", - password="foobarbaz123") + """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 + assert user.check_password('barfoobaz') is False def test_full_name(self): - user = UserFactory(first_name="Foo", last_name="Bar") - assert user.full_name == "Foo Bar" + """User full name.""" + 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.save() u = UserFactory() diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/__init__.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/__init__.py index e69de29..5b52f8c 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/__init__.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/__init__.py @@ -0,0 +1 @@ +"""Main application package.""" diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py index c945411..62c54ff 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py @@ -2,22 +2,14 @@ """The app module, containing the app factory function.""" 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}}.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): - """An application factory, as explained here: - http://flask.pocoo.org/docs/patterns/appfactories/ + """An application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/. :param config_object: The configuration object to use. """ @@ -30,6 +22,7 @@ def create_app(config_object=ProdConfig): def register_extensions(app): + """Register Flask extensions.""" assets.init_app(app) bcrypt.init_app(app) cache.init_app(app) @@ -41,16 +34,19 @@ def register_extensions(app): def register_blueprints(app): + """Register Flask blueprints.""" app.register_blueprint(public.views.blueprint) app.register_blueprint(user.views.blueprint) return None 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 + return render_template('{0}.html'.format(error_code)), error_code for errcode in [401, 404, 500]: app.errorhandler(errcode)(render_error) return None diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/assets.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/assets.py index b425a85..833e2e3 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/assets.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/assets.py @@ -1,22 +1,23 @@ # -*- coding: utf-8 -*- +"""Application assets.""" from flask_assets import Bundle, Environment css = Bundle( - "libs/bootstrap/dist/css/bootstrap.css", - "css/style.css", - filters="cssmin", - output="public/css/common.css" + 'libs/bootstrap/dist/css/bootstrap.css', + 'css/style.css', + filters='cssmin', + output='public/css/common.css' ) js = Bundle( - "libs/jQuery/dist/jquery.js", - "libs/bootstrap/dist/js/bootstrap.js", - "js/plugins.js", + 'libs/jQuery/dist/jquery.js', + 'libs/bootstrap/dist/js/bootstrap.js', + 'js/plugins.js', filters='jsmin', - output="public/js/common.js" + output='public/js/common.js' ) assets = Environment() -assets.register("js_all", js) -assets.register("css_all", css) +assets.register('js_all', js) +assets.register('css_all', css) diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/compat.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/compat.py index 863f2a0..f749d7c 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/compat.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/compat.py @@ -7,11 +7,11 @@ import sys PY2 = int(sys.version[0]) == 2 if PY2: - text_type = unicode + text_type = unicode # noqa binary_type = str - string_types = (str, unicode) - unicode = unicode - basestring = basestring + string_types = (str, unicode) # noqa + unicode = unicode # noqa + basestring = basestring # noqa else: text_type = str binary_type = bytes diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py index 5d88d4d..8265fb8 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- -"""Database module, including the SQLAlchemy database object and DB-related -utilities. -""" +"""Database module, including the SQLAlchemy database object and DB-related utilities.""" from sqlalchemy.orm import relationship -from .extensions import db from .compat import basestring +from .extensions import db # Alias common SQLAlchemy names Column = db.Column @@ -13,9 +11,7 @@ relationship = relationship class CRUDMixin(object): - """Mixin that adds convenience methods for CRUD (create, read, update, delete) - operations. - """ + """Mixin that adds convenience methods for CRUD (create, read, update, delete) operations.""" @classmethod def create(cls, **kwargs): @@ -41,32 +37,34 @@ class CRUDMixin(object): db.session.delete(self) return commit and db.session.commit() + class Model(CRUDMixin, db.Model): """Base model class that includes CRUD convenience methods.""" + __abstract__ = True # From Mike Bayer's "Building the app" talk # https://speakerdeck.com/zzzeek/building-the-app 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} id = db.Column(db.Integer, primary_key=True) @classmethod def get_by_id(cls, id): + """Get record by ID.""" if any( - (isinstance(id, basestring) and id.isdigit(), - isinstance(id, (int, float))), + (isinstance(id, basestring) and id.isdigit(), + isinstance(id, (int, float))), ): return cls.query.get(int(id)) 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. Usage: :: @@ -75,5 +73,5 @@ def ReferenceCol(tablename, nullable=False, pk_name='id', **kwargs): category = relationship('Category', backref='categories') """ return db.Column( - db.ForeignKey("{0}.{1}".format(tablename, pk_name)), + db.ForeignKey('{0}.{1}'.format(tablename, pk_name)), nullable=nullable, **kwargs) diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py index af8ece3..7d86238 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py @@ -1,22 +1,16 @@ # -*- coding: utf-8 -*- -"""Extensions module. Each extension is initialized in the app factory located -in app.py -""" +"""Extensions module. Each extension is initialized in the app factory located in app.py.""" from flask_bcrypt import Bcrypt -bcrypt = Bcrypt() - +from flask_cache import Cache +from flask_debugtoolbar import DebugToolbarExtension from flask_login import LoginManager -login_manager = LoginManager() - +from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy -db = SQLAlchemy() -from flask_migrate import Migrate +bcrypt = Bcrypt() +login_manager = LoginManager() +db = SQLAlchemy() migrate = Migrate() - -from flask_cache import Cache cache = Cache() - -from flask_debugtoolbar import DebugToolbarExtension debug_toolbar = DebugToolbarExtension() diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/__init__.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/__init__.py index 4260a43..234a4e5 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/__init__.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- """The public module, including the homepage and user auth.""" - -from . import views +from . import views # noqa diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py index 43da1de..ed7f1fe 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py @@ -1,20 +1,25 @@ # -*- coding: utf-8 -*- +"""Public forms.""" from flask_wtf import Form -from wtforms import TextField, PasswordField +from wtforms import PasswordField, StringField from wtforms.validators import DataRequired from {{cookiecutter.app_name}}.user.models import User class LoginForm(Form): - username = TextField('Username', validators=[DataRequired()]) + """Login form.""" + + username = StringField('Username', validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()]) def __init__(self, *args, **kwargs): + """Create instance.""" super(LoginForm, self).__init__(*args, **kwargs) self.user = None def validate(self): + """Validate the form.""" initial_validation = super(LoginForm, self).validate() if not initial_validation: return False diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/views.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/views.py index 879291b..381e1cd 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/views.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/views.py @@ -1,63 +1,63 @@ # -*- coding: utf-8 -*- """Public section, including homepage and signup.""" -from flask import (Blueprint, request, render_template, flash, url_for, - redirect, session) -from flask_login import login_user, login_required, logout_user +from flask import Blueprint, 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 -from {{cookiecutter.app_name}}.user.models import User from {{cookiecutter.app_name}}.public.forms import LoginForm 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}}.database import db -blueprint = Blueprint('public', __name__, static_folder="../static") +blueprint = Blueprint('public', __name__, static_folder='../static') @login_manager.user_loader def load_user(id): + """Load user by ID.""" return User.get_by_id(int(id)) -@blueprint.route("/", methods=["GET", "POST"]) +@blueprint.route('/', methods=['GET', 'POST']) def home(): + """Home page.""" form = LoginForm(request.form) # Handle logging in 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/') @login_required def logout(): + """Logout.""" logout_user() 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, csrf_enabled=False) if form.validate_on_submit(): - new_user = 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') + 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) -@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) diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py index ce70006..f94b81e 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- +"""Application configuration.""" import os os_env = os.environ 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 PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) BCRYPT_LOG_ROUNDS = 13 @@ -13,10 +16,12 @@ class Config(object): DEBUG_TB_ENABLED = False # Disable Debug toolbar DEBUG_TB_INTERCEPT_REDIRECTS = False CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc. + SQLALCHEMY_TRACK_MODIFICATIONS = False class ProdConfig(Config): """Production configuration.""" + ENV = 'prod' DEBUG = False SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/example' # TODO: Change me @@ -25,6 +30,7 @@ class ProdConfig(Config): class DevConfig(Config): """Development configuration.""" + ENV = 'dev' DEBUG = True DB_NAME = 'dev.db' @@ -37,6 +43,8 @@ class DevConfig(Config): class TestConfig(Config): + """Test configuration.""" + TESTING = True DEBUG = True SQLALCHEMY_DATABASE_URI = 'sqlite://' diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/__init__.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/__init__.py index a655bf1..c11ac5e 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/__init__.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """The user module.""" -from . import views +from . import views # noqa diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/forms.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/forms.py index f4ba798..1ba4166 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/forms.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/forms.py @@ -1,35 +1,40 @@ # -*- coding: utf-8 -*- +"""User forms.""" from flask_wtf import Form -from wtforms import TextField, PasswordField +from wtforms import PasswordField, StringField from wtforms.validators import DataRequired, Email, EqualTo, Length from .models import User class RegisterForm(Form): - username = TextField('Username', - validators=[DataRequired(), Length(min=3, max=25)]) - email = TextField('Email', - validators=[DataRequired(), Email(), Length(min=6, max=40)]) + """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)]) + validators=[DataRequired(), Length(min=6, max=40)]) confirm = PasswordField('Verify password', - [DataRequired(), EqualTo('password', message='Passwords must match')]) + [DataRequired(), EqualTo('password', message='Passwords must match')]) def __init__(self, *args, **kwargs): + """Create instance.""" super(RegisterForm, self).__init__(*args, **kwargs) self.user = None def validate(self): + """Validate the form.""" initial_validation = super(RegisterForm, self).validate() if not initial_validation: 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 diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/models.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/models.py index 06ad15a..7fb50ef 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/models.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/models.py @@ -1,33 +1,32 @@ # -*- coding: utf-8 -*- +"""User models.""" import datetime as dt 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}}.database import ( - Column, - db, - Model, - ReferenceCol, - relationship, - SurrogatePK, -) class Role(SurrogatePK, Model): + """A role for a user.""" + __tablename__ = 'roles' name = Column(db.String(80), unique=True, nullable=False) user_id = ReferenceCol('users', nullable=True) user = relationship('User', backref='roles') def __init__(self, name, **kwargs): + """Create instance.""" db.Model.__init__(self, name=name, **kwargs) def __repr__(self): + """Represent instance as a unique string.""" return ''.format(name=self.name) class User(UserMixin, SurrogatePK, Model): + """A user of the app.""" __tablename__ = 'users' 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) def __init__(self, username, email, password=None, **kwargs): + """Create instance.""" db.Model.__init__(self, username=username, email=email, **kwargs) if password: self.set_password(password) @@ -48,14 +48,18 @@ class User(UserMixin, SurrogatePK, Model): self.password = None def set_password(self, password): + """Set password.""" self.password = bcrypt.generate_password_hash(password) def check_password(self, value): + """Check password.""" return bcrypt.check_password_hash(self.password, value) @property 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): + """Represent instance as a unique string.""" return ''.format(username=self.username) diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/views.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/views.py index ca99cc8..2dff555 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/views.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/views.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- +"""User views.""" 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(): - return render_template("users/members.html") + """List members.""" + return render_template('users/members.html') diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/utils.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/utils.py index 2def30c..1bafe98 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/utils.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/utils.py @@ -3,9 +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)