diff --git a/README.rst b/README.rst index 5174f923a994c0632c64a28e626d3588fc5dc7ad..6e07ca139aced110a703e3d1d737ab6e3995f5b3 100644 --- a/README.rst +++ b/README.rst @@ -16,6 +16,7 @@ Features - A simple ``manage.py`` script. - CSS and JS minification using Flask-Assets - Easily switch between development and production environments through the MYFLASKAPP_ENV system variable. +- Utilizes best practices: `Blueprints <http://flask.pocoo.org/docs/blueprints/>`_ and `Application Factory <http://flask.pocoo.org/docs/patterns/appfactories/>`_ patterns Screenshots ----------- @@ -42,9 +43,10 @@ Inspiration ----------- - `Building Websites in Python with Flask <http://maximebf.com/blog/2012/10/building-websites-in-python-with-flask/>`_ +- `Getting Bigger with Flask <http://maximebf.com/blog/2012/11/getting-bigger-with-flask/>`_ - `Structuring Flask Apps <http://charlesleifer.com/blog/structuring-flask-apps-a-how-to-for-those-coming-from-django/>`_ -- `Flask-Foundation <https://github.com/JackStouffer/Flask-Foundation>`_ -- `flask-basic-registration <https://github.com/mjhea0/flask-basic-registration>`_ +- `Flask-Foundation <https://github.com/JackStouffer/Flask-Foundation>`_ by `@JackStouffer <https://github.com/JackStouffer>`_ +- `flask-basic-registration <https://github.com/mjhea0/flask-basic-registration>`_ by `@mjhea0 <https://github.com/mjhea0>`_ - `Flask Official Documentation <http://flask.pocoo.org/docs/>`_ diff --git a/{{cookiecutter.repo_name}}/manage.py b/{{cookiecutter.repo_name}}/manage.py index a9b19b53679631124d72d47472c068cbf3eb4ec0..4fe3a76e933b3a528d6f8014e662b088ea0ca6be 100644 --- a/{{cookiecutter.repo_name}}/manage.py +++ b/{{cookiecutter.repo_name}}/manage.py @@ -1,9 +1,16 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- +import os import sys import subprocess from flask.ext.script import Manager, Shell, Server -from {{ cookiecutter.repo_name }} import models -from {{ cookiecutter.repo_name }}.main import app, db +from {{cookiecutter.repo_name }} import models +from {{cookiecutter.repo_name }}.app import create_app +from {{cookiecutter.repo_name}}.models import db + +env = os.environ.get("{{cookiecutter.repo_name | upper }}_ENV", 'prod') +app = create_app("{{cookiecutter.repo_name}}.settings.{0}Config" + .format(env.capitalize()), env) manager = Manager(app) TEST_CMD = "nosetests" diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/app.py b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/app.py index 88947a0892e77c8af69b2caa051ae3b684be3d19..f83f4390212962fefa5e0b5fc4ac1b3bd83d8719 100644 --- a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/app.py +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/app.py @@ -6,19 +6,31 @@ from flask.ext.assets import Environment from webassets.loaders import PythonLoader from {{cookiecutter.repo_name}} import assets +from {{cookiecutter.repo_name}}.models import db -app = Flask(__name__) -# The environment variable, either 'prod' or 'dev' -env = os.environ.get("{{cookiecutter.repo_name | upper}}_ENV", "prod") -# Use the appropriate environment-specific settings -app.config.from_object('{{cookiecutter.repo_name}}.settings.{env}Config' - .format(env=env.capitalize())) -app.config['ENV'] = env -db = SQLAlchemy(app) - -# Register asset bundles assets_env = Environment() -assets_env.init_app(app) -assets_loader = PythonLoader(assets) -for name, bundle in assets_loader.load_bundles().iteritems(): - assets_env.register(name, bundle) + +def create_app(config_object, env): + '''An application factory, as explained here: + http://flask.pocoo.org/docs/patterns/appfactories/ + + :param config_object: The configuration object to use. + :param env: A string, the current environment. Either "dev" or "prod" + ''' + app = Flask(__name__) + app.config.from_object('{{cookiecutter.repo_name}}.settings.{env}Config' + .format(env=env.capitalize())) + app.config['ENV'] = env + # Initialize SQLAlchemy + db.init_app(app) + # Register asset bundles + assets_env.init_app(app) + assets_loader = PythonLoader(assets) + for name, bundle in assets_loader.load_bundles().iteritems(): + assets_env.register(name, bundle) + # Register blueprints + from {{cookiecutter.repo_name}}.modules import public, member + app.register_blueprint(public.blueprint) + app.register_blueprint(member.blueprint) + + return app diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/main.py b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/main.py index 153bad06908f85b4eb8bd5a1369c12d71e08da00..56528350794af0b45590d9e55c01e005a8e75b08 100644 --- a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/main.py +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/main.py @@ -4,11 +4,12 @@ Entry point for all things, to avoid circular imports. """ import os -from .app import app, db, assets_env -from .models import * -from .views import * - +from .app import create_app +from .models import User, db +import {{cookiecutter.repo_name}}.modules as modules if __name__ == '__main__': - port = int(os.environ.get('PORT', 5000)) - app.run(host='0.0.0.0', port=port) + # Get the environment setting from the system environment variable + env = os.environ.get("{{cookiecutter.repo_name | upper}}_ENV", "prod") + app = create_app("{{cookiecutter.repo_name}}.settings.{env}Config" + .format(env=env.capitalize()), env) diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/models.py b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/models.py index fa200d2a57e9fc5881af31962be71f2457938c9f..fe93bd5b0b8d9aeca2a19d46c7ff8d83f6b399f8 100644 --- a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/models.py +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/models.py @@ -3,7 +3,9 @@ """ {{cookiecutter.project_name}} models. """ -from .app import db +from flask.ext.sqlalchemy import SQLAlchemy + +db = SQLAlchemy() class User(db.Model): @@ -22,5 +24,3 @@ class User(db.Model): def __repr__(self): return '<User "{username}">'.format(username=self.username) - -db.create_all() diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/modules/__init__.py b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/modules/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..33bdd6a83587b4b9fd908dc91f2fd7011725f74f --- /dev/null +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/modules/__init__.py @@ -0,0 +1 @@ +'''Blueprint modules for {{cookiecutter.repo_name}}.''' diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/modules/member.py b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/modules/member.py new file mode 100644 index 0000000000000000000000000000000000000000..e732f6ea5c010d5d802c0130ca5e5dfecbf1701e --- /dev/null +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/modules/member.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +'''Members-only module, typically including the app itself. +''' +from flask import Blueprint, render_template +from {{cookiecutter.repo_name}}.utils import login_required + +blueprint = Blueprint('member', __name__, + static_folder="../static", + template_folder="../templates") + +@blueprint.route("/members/") +@login_required +def members(): + return render_template("members.html") diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/views.py b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/modules/public.py similarity index 54% rename from {{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/views.py rename to {{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/modules/public.py index 6ed52085befe2d882f5e8f3c367708633749fb5f..d69025df38512e5697d3c1539836d6b80431a731 100644 --- a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/views.py +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/modules/public.py @@ -1,29 +1,20 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals -from flask import render_template, session, request, flash, redirect, url_for -from functools import wraps -from .app import app, db -from .models import User -from .forms import RegisterForm, LoginForm +'''Public section, including homepage and signup.''' +from flask import (Blueprint, request, render_template, flash, url_for, + redirect, session) from sqlalchemy.exc import IntegrityError -def flash_errors(form): - for field, errors in form.errors.items(): - for error in errors: - flash("Error in the {0} field - {1}" - .format(getattr(form, field).label.text, error), 'error') +from {{cookiecutter.repo_name}}.models import User +from {{cookiecutter.repo_name}}.forms import RegisterForm, LoginForm +from {{cookiecutter.repo_name}}.utils import flash_errors +from {{cookiecutter.repo_name}}.models import db + +blueprint = Blueprint('public', __name__, + static_folder="../static", + template_folder="../templates") -def login_required(test): - @wraps(test) - def wrap(*args, **kwargs): - if 'logged_in' in session: - return test(*args, **kwargs) - else: - flash('You need to log in first.') - return redirect(url_for('home')) - return wrap -@app.route("/", methods=["GET", "POST"]) +@blueprint.route("/", methods=["GET", "POST"]) def home(): form = LoginForm(request.form) if request.method == 'POST': @@ -31,29 +22,22 @@ def home(): password=request.form['password']).first() if u is None: error = 'Invalid username or password.' - flash(error, 'error') + flash(error, 'warning') else: session['logged_in'] = True session['username'] = u.username flash("You are logged in.", 'success') - return redirect(url_for("members")) + return redirect(url_for("member.members")) return render_template("home.html", form=form) - -@app.route("/members/") -@login_required -def members(): - return render_template("members.html") - - -@app.route('/logout/') +@blueprint.route('/logout/') def logout(): session.pop('logged_in', None) session.pop('username', None) flash('You are logged out.', 'info') - return redirect(url_for('home')) + return redirect(url_for('public.home')) -@app.route("/register/", methods=['GET', 'POST']) +@blueprint.route("/register/", methods=['GET', 'POST']) def register(): form = RegisterForm(request.form, csrf_enabled=False) if form.validate_on_submit(): @@ -62,21 +46,19 @@ def register(): db.session.add(new_user) db.session.commit() flash("Thank you for registering. You can now log in.", 'success') - return redirect(url_for('home')) + return redirect(url_for('public.home')) except IntegrityError as err: print(err) - flash("That username and/or email already exists. Try again.", 'error') + flash("That username and/or email already exists. Try again.", 'warning') else: flash_errors(form) return render_template('register.html', form=form) - -@app.route("/about/") +@blueprint.route("/about/") def about(): form = LoginForm(request.form) return render_template("about.html", form=form) - -@app.errorhandler(404) +@blueprint.errorhandler(404) def page_not_found(e): return render_template("404.html"), 404 diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/settings.py b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/settings.py index 574df755b1bc0c506f8978f809a3a50bcebeb8f6..243bda4e872144a098fc489649e96a479b421b71 100644 --- a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/settings.py +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/settings.py @@ -18,6 +18,3 @@ class DevConfig(Config): DB_PATH = os.path.join(Config.PROJECT_ROOT, DB_NAME) SQLALCHEMY_DATABASE_URI = "sqlite:///{0}".format(DB_PATH) SQLALCHEMY_ECHO = True - - - diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/templates/_layouts/footer.html b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/templates/_layouts/footer.html index da563ebfa2e9041151a3433b8cb00ba3d26fa873..5f4bf3af82213f02ab615275bd2474055e52bf68 100644 --- a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/templates/_layouts/footer.html +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/templates/_layouts/footer.html @@ -6,9 +6,9 @@ <ul class="footer-nav"> {% raw %} - <li><a href="{{ url_for('about') }}">About</a></li> + <li><a href="{{ url_for('public.about') }}">About</a></li> {% endraw %} <li><a href="mailto:{{ cookiecutter.email }}">Contact</a></li> </ul> </small> -</footer> \ No newline at end of file +</footer> diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/templates/_layouts/nav.html b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/templates/_layouts/nav.html index 479b970c1a26857e84ccd65227e8edbf4c6e2e0c..7406f4984f6313d5fdfa9dd14b731bbc36ee7c89 100644 --- a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/templates/_layouts/nav.html +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/templates/_layouts/nav.html @@ -8,7 +8,7 @@ <span class="icon-bar"></span> <span class="icon-bar"></span> </button> - <a class="navbar-brand" href="{{ url_for('home') }}"> + <a class="navbar-brand" href="{{ url_for('public.home') }}"> {% endraw %} {{cookiecutter.project_name}} {% raw %} @@ -17,17 +17,17 @@ <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse navbar-ex1-collapse"> <ul class="nav navbar-nav"> - <li class="active"><a href="{{ url_for('home') }}">Home</a></li> - <li><a href="{{ url_for('about') }}">About</a></li> + <li class="active"><a href="{{ url_for('public.home') }}">Home</a></li> + <li><a href="{{ url_for('public.about') }}">About</a></li> </ul> {% if session.logged_in %} - <a class="btn btn-default btn-sm navbar-btn navbar-right" href="{{ url_for('logout') }}">Log out</a> + <a class="btn btn-default btn-sm navbar-btn navbar-right" href="{{ url_for('public.logout') }}">Log out</a> <ul class="nav navbar-nav navbar-right"> - <li><a href="{{ url_for('members') }}">Logged in as {{ session.username }}</a></li> + <li><a href="{{ url_for('member.members') }}">Logged in as {{ session.username }}</a></li> </ul> {% elif form %} <ul class="nav navbar-nav navbar-right"> - <li><a href="{{ url_for('register') }}">Create account</a></li> + <li><a href="{{ url_for('public.register') }}">Create account</a></li> </ul> <form method="POST" class="navbar-form form-inline navbar-right" action="" role="login"> {{ form.hidden_tag() }} diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/tests/unit_tests.py b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/tests/unit_tests.py index 653984a08079161f168f450e2af6a8a08182b6fc..51fbb28f0a5106b88c0dc5ed9951d9205047369e 100644 --- a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/tests/unit_tests.py +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/tests/unit_tests.py @@ -7,11 +7,12 @@ except ImportError: import sys print('nose required. Run "pip install nose".') -from {{cookiecutter.repo_name}}.main import app +from {{cookiecutter.repo_name}}.main import create_app class Test{{cookiecutter.repo_name | capitalize}}(unittest.TestCase): def setUp(self): + app = create_app("{{cookiecutter.repo_name}}.settings.DevConfig", 'dev') app.config['TESTING'] = True self.app = app.test_client() diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/utils.py b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..61cbe740ea8c91ac8f9bc7e351a1be38314e3c6f --- /dev/null +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/utils.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +'''Helper utilities and decorators.''' +from flask import session, flash, redirect, url_for +from functools import wraps + +def flash_errors(form): + '''Flash all errors for a form.''' + for field, errors in form.errors.items(): + for error in errors: + flash("Error in the {0} field - {1}" + .format(getattr(form, field).label.text, error), 'warning') + +def login_required(test): + '''Decorator that makes a view require authentication.''' + @wraps(test) + def wrap(*args, **kwargs): + if 'logged_in' in session: + return test(*args, **kwargs) + else: + flash('You need to log in first.') + return redirect(url_for('home')) + return wrap