| @ -0,0 +1,3 @@ | |||
| aws_reboot/static/.webassets-cache | |||
| aws_reboot/static/dist | |||
| node_modules | |||
| @ -0,0 +1,26 @@ | |||
| cookiecutter-flask-app | |||
| ====================== | |||
| My own personal `cookiecutter <https://cookiecutter.readthedocs.io/en/latest/>`_ template for writing `Flask <https://http://flask.pocoo.org/>`_ apps. | |||
| This cookiecutter sets up a starter Flask app using: | |||
| * `Flask <https://http://flask.pocoo.org/>`_ | |||
| * `Flask-Assets <https://flask-assets.readthedocs.io>`_ | |||
| * `Flask-Migrate <http://flask-migrate.readthedocs.io/>`_ | |||
| * `Flask-Script <http://flask-script.readthedocs.io/>`_ | |||
| * `Flask-Security <http://flask-security.readthedocs.io/>`_ | |||
| * `Flask-SQLAlchemy <http://flask-sqlalchemy.readthedocs.io/>`_ | |||
| * `libsass-python <https://hongminhee.org/libsass-python/>`_ | |||
| * `webassets-browserify <https://github.com/renstrom/webassets-browserify>`_ | |||
| Usage | |||
| ----- | |||
| To create a new starting project: | |||
| .. code-block:: bash | |||
| pip install cookiecutter | |||
| cookiecutter https://github.com/brettlangdon/cookiecutter-flask-app.git | |||
| @ -0,0 +1,7 @@ | |||
| { | |||
| "project_name": "flask_app", | |||
| "database_uri": "postgresql://localhost/{{cookiecutter.project_name}}", | |||
| "_copy_without_render": [ | |||
| "*.jinja" | |||
| ] | |||
| } | |||
| @ -0,0 +1,2 @@ | |||
| {{cookiecutter.project_name}} | |||
| === | |||
| @ -0,0 +1,3 @@ | |||
| #!/usr/bin/env python | |||
| from {{cookiecutter.project_name}}.manager import manager | |||
| manager.run() | |||
| @ -0,0 +1,45 @@ | |||
| # A generic, single database configuration. | |||
| [alembic] | |||
| # template used to generate migration files | |||
| # file_template = %%(rev)s_%%(slug)s | |||
| # set to 'true' to run the environment during | |||
| # the 'revision' command, regardless of autogenerate | |||
| # revision_environment = false | |||
| # Logging configuration | |||
| [loggers] | |||
| keys = root,sqlalchemy,alembic | |||
| [handlers] | |||
| keys = console | |||
| [formatters] | |||
| keys = generic | |||
| [logger_root] | |||
| level = WARN | |||
| handlers = console | |||
| qualname = | |||
| [logger_sqlalchemy] | |||
| level = WARN | |||
| handlers = | |||
| qualname = sqlalchemy.engine | |||
| [logger_alembic] | |||
| level = INFO | |||
| handlers = | |||
| qualname = alembic | |||
| [handler_console] | |||
| class = StreamHandler | |||
| args = (sys.stderr,) | |||
| level = NOTSET | |||
| formatter = generic | |||
| [formatter_generic] | |||
| format = %(levelname)-5.5s [%(name)s] %(message)s | |||
| datefmt = %H:%M:%S | |||
| @ -0,0 +1,87 @@ | |||
| from __future__ import with_statement | |||
| from alembic import context | |||
| from sqlalchemy import engine_from_config, pool | |||
| from logging.config import fileConfig | |||
| import logging | |||
| # this is the Alembic Config object, which provides | |||
| # access to the values within the .ini file in use. | |||
| config = context.config | |||
| # Interpret the config file for Python logging. | |||
| # This line sets up loggers basically. | |||
| fileConfig(config.config_file_name) | |||
| logger = logging.getLogger('alembic.env') | |||
| # add your model's MetaData object here | |||
| # for 'autogenerate' support | |||
| # from myapp import mymodel | |||
| # target_metadata = mymodel.Base.metadata | |||
| from flask import current_app | |||
| config.set_main_option('sqlalchemy.url', | |||
| current_app.config.get('SQLALCHEMY_DATABASE_URI')) | |||
| target_metadata = current_app.extensions['migrate'].db.metadata | |||
| # other values from the config, defined by the needs of env.py, | |||
| # can be acquired: | |||
| # my_important_option = config.get_main_option("my_important_option") | |||
| # ... etc. | |||
| def run_migrations_offline(): | |||
| """Run migrations in 'offline' mode. | |||
| This configures the context with just a URL | |||
| and not an Engine, though an Engine is acceptable | |||
| here as well. By skipping the Engine creation | |||
| we don't even need a DBAPI to be available. | |||
| Calls to context.execute() here emit the given string to the | |||
| script output. | |||
| """ | |||
| url = config.get_main_option("sqlalchemy.url") | |||
| context.configure(url=url) | |||
| with context.begin_transaction(): | |||
| context.run_migrations() | |||
| def run_migrations_online(): | |||
| """Run migrations in 'online' mode. | |||
| In this scenario we need to create an Engine | |||
| and associate a connection with the context. | |||
| """ | |||
| # this callback is used to prevent an auto-migration from being generated | |||
| # when there are no changes to the schema | |||
| # reference: http://alembic.readthedocs.org/en/latest/cookbook.html | |||
| def process_revision_directives(context, revision, directives): | |||
| if getattr(config.cmd_opts, 'autogenerate', False): | |||
| script = directives[0] | |||
| if script.upgrade_ops.is_empty(): | |||
| directives[:] = [] | |||
| logger.info('No changes in schema detected.') | |||
| engine = engine_from_config(config.get_section(config.config_ini_section), | |||
| prefix='sqlalchemy.', | |||
| poolclass=pool.NullPool) | |||
| connection = engine.connect() | |||
| context.configure(connection=connection, | |||
| target_metadata=target_metadata, | |||
| process_revision_directives=process_revision_directives, | |||
| **current_app.extensions['migrate'].configure_args) | |||
| try: | |||
| with context.begin_transaction(): | |||
| context.run_migrations() | |||
| finally: | |||
| connection.close() | |||
| if context.is_offline_mode(): | |||
| run_migrations_offline() | |||
| else: | |||
| run_migrations_online() | |||
| @ -0,0 +1,22 @@ | |||
| """${message} | |||
| Revision ID: ${up_revision} | |||
| Revises: ${down_revision} | |||
| Create Date: ${create_date} | |||
| """ | |||
| # revision identifiers, used by Alembic. | |||
| revision = ${repr(up_revision)} | |||
| down_revision = ${repr(down_revision)} | |||
| from alembic import op | |||
| import sqlalchemy as sa | |||
| ${imports if imports else ""} | |||
| def upgrade(): | |||
| ${upgrades if upgrades else "pass"} | |||
| def downgrade(): | |||
| ${downgrades if downgrades else "pass"} | |||
| @ -0,0 +1,49 @@ | |||
| """Setup user and role tables | |||
| Revision ID: 70bd704dcb5c | |||
| Revises: None | |||
| Create Date: 2016-07-26 10:07:41.130632 | |||
| """ | |||
| # revision identifiers, used by Alembic. | |||
| revision = '70bd704dcb5c' | |||
| down_revision = None | |||
| from alembic import op | |||
| import sqlalchemy as sa | |||
| from sqlalchemy.dialects.postgresql import UUID | |||
| def upgrade(): | |||
| op.create_table( | |||
| 'role', | |||
| sa.Column('id', UUID(), nullable=False), | |||
| sa.Column('name', sa.String(length=80), nullable=True), | |||
| sa.Column('description', sa.String(length=255), nullable=True), | |||
| sa.PrimaryKeyConstraint('id'), | |||
| sa.UniqueConstraint('name'), | |||
| ) | |||
| op.create_table( | |||
| 'user', | |||
| sa.Column('id', UUID(), nullable=False), | |||
| sa.Column('email', sa.String(length=255), nullable=True), | |||
| sa.Column('password', sa.String(length=255), nullable=True), | |||
| sa.Column('active', sa.Boolean(), nullable=True), | |||
| sa.Column('confirmed_at', sa.DateTime(), nullable=True), | |||
| sa.PrimaryKeyConstraint('id'), | |||
| sa.UniqueConstraint('email'), | |||
| ) | |||
| op.create_table( | |||
| 'user_role', | |||
| sa.Column('user_id', UUID(), nullable=True), | |||
| sa.Column('role_id', UUID(), nullable=True), | |||
| sa.ForeignKeyConstraint(['role_id'], ['role.id'], ), | |||
| sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), | |||
| ) | |||
| def downgrade(): | |||
| op.drop_table('user_role') | |||
| op.drop_table('user') | |||
| op.drop_table('role') | |||
| @ -0,0 +1,8 @@ | |||
| { | |||
| "name": "{{cookiecutter.project_name}}", | |||
| "private": true, | |||
| "dependencies": { | |||
| "browserify": "^13.1.0", | |||
| "bulma": "^0.1.0" | |||
| } | |||
| } | |||
| @ -0,0 +1,10 @@ | |||
| Flask==0.11.1 | |||
| # Waiting on next release for a fix for libsass | |||
| -e git+https://github.com/miracle2k/flask-assets@36da5c90cf99293ac632cfab58db52cab1c8cc83#egg=flask_assets | |||
| Flask-Migrate==1.8.1 | |||
| Flask-Script==2.0.5 | |||
| Flask-Security==1.7.5 | |||
| Flask-SQLAlchemy==2.1 | |||
| libsass==0.9.3 | |||
| psycopg2==2.6.2 | |||
| webassets-browserify==1.1.0 | |||
| @ -0,0 +1,35 @@ | |||
| #!/usr/bin/env python | |||
| from setuptools import setup | |||
| with open('README.rst') as readme_file: | |||
| readme = readme_file.read() | |||
| requirements = [] | |||
| with open('requirements.txt') as requirements_file: | |||
| requirements = [l.strip('\n') for l in requirements_file] | |||
| test_requirements = [] | |||
| with open('requirements-dev.txt') as test_requirements_file: | |||
| test_requirements = [l for l in test_requirements_file] | |||
| setup( | |||
| name='{{cookiecutter.project_name}}', | |||
| version='0.1.0', | |||
| long_description=readme, | |||
| packages=[ | |||
| '{{cookiecutter.project_name}}', | |||
| ], | |||
| package_dir={ | |||
| '{{cookiecutter.project_name}}': '{{cookiecutter.project_name}}' | |||
| }, | |||
| scripts=[ | |||
| 'bin/{{cookiecutter.project_name}}', | |||
| ], | |||
| include_package_data=True, | |||
| install_requires=requirements, | |||
| zip_safe=False, | |||
| test_suite='tests', | |||
| tests_require=test_requirements, | |||
| ) | |||
| @ -0,0 +1,30 @@ | |||
| from flask import Flask | |||
| from flask_assets import Bundle, Environment | |||
| from flask_security import Security, SQLAlchemyUserDatastore | |||
| from flask_sqlalchemy import SQLAlchemy | |||
| from webassets.filter import register_filter | |||
| from webassets_browserify import Browserify | |||
| app = Flask(__name__) | |||
| app.config.from_object('{{cookiecutter.project_name}}.configuration.EnvConfig') | |||
| db = SQLAlchemy(app) | |||
| # Configure static assets | |||
| assets = Environment(app) | |||
| css = Bundle('scss/app.scss', filters='libsass', output='dist/app.css', depends=['scss/**/*.scss']) | |||
| assets.register('css_all', css) | |||
| register_filter(Browserify) | |||
| js = Bundle('js/app.js', filters='browserify', output='dist/app.js', depends=['js/**/*.js']) | |||
| assets.register('js_all', js) | |||
| # Import models down here to ensure circular import works as expected | |||
| from .models.role import Role | |||
| from .models.user import User | |||
| user_datastore = SQLAlchemyUserDatastore(db, User, Role) | |||
| security = Security(app, user_datastore) | |||
| from .views import bind_routes | |||
| bind_routes(app) | |||
| @ -0,0 +1,2 @@ | |||
| from {{cookiecutter.project_name}}.manager import manager | |||
| manager.run() | |||
| @ -0,0 +1,38 @@ | |||
| import json | |||
| import os | |||
| HERE = os.path.abspath(os.path.dirname(__file__)) | |||
| ROOT = os.path.abspath(os.path.join(HERE, '..')) | |||
| NODE_MODULES = os.path.abspath(os.path.join(ROOT, 'node_modules')) | |||
| class MetaEnvConfig(type): | |||
| def __init__(cls, name, bases, dict): | |||
| super(MetaEnvConfig, cls).__init__(name, bases, dict) | |||
| # Override default configuration from environment variables | |||
| for key, value in os.environ.items(): | |||
| try: | |||
| value = json.loads(value) | |||
| except Exception: | |||
| pass | |||
| setattr(cls, key, value) | |||
| class EnvConfig(metaclass=MetaEnvConfig): | |||
| # Assets | |||
| BROWSERIFY_BIN = os.path.abspath(os.path.join(NODE_MODULES, '.bin', 'browserify')) | |||
| LIBSASS_INCLUDES = [os.path.abspath(os.path.join(NODE_MODULES, 'bulma'))] | |||
| LIBSASS_STYLE = 'compressed' | |||
| # User sessions | |||
| SECRET_KEY = 'super secret key' | |||
| SECURITY_PASSWORD_HASH = 'pbkdf2_sha512' | |||
| SECURITY_PASSWORD_SALT = 'super secret salt' | |||
| # User session templates | |||
| SECURITY_LOGIN_USER_TEMPLATE = 'security/login_user.jinja' | |||
| # Database | |||
| SQLALCHEMY_DATABASE_URI = '{{cookiecutter.database_uri}}' | |||
| SQLALCHEMY_TRACK_MODIFICATIONS = False | |||
| @ -0,0 +1,21 @@ | |||
| from flask_assets import ManageAssets | |||
| from flask_script import Manager | |||
| from flask_security.script import (CreateUserCommand, CreateRoleCommand, AddRoleCommand, | |||
| RemoveRoleCommand, DeactivateUserCommand, ActivateUserCommand) | |||
| from flask_migrate import Migrate, MigrateCommand | |||
| from . import app, assets, db | |||
| migrate = Migrate(app, db) | |||
| manager = Manager(app) | |||
| manager.add_command('assets', ManageAssets(assets)) | |||
| manager.add_command('db', MigrateCommand) | |||
| manager.add_command('create', CreateUserCommand, namespace='user') | |||
| manager.add_command('activate', ActivateUserCommand, namespace='user') | |||
| manager.add_command('deactivate', DeactivateUserCommand, namespace='user') | |||
| manager.add_command('add_role', AddRoleCommand, namespace='user') | |||
| manager.add_command('remove_role', RemoveRoleCommand, namespace='user') | |||
| manager.add_command('create', CreateRoleCommand, namespace='role') | |||
| @ -0,0 +1,10 @@ | |||
| import uuid | |||
| from sqlalchemy.ext.declarative import AbstractConcreteBase | |||
| class Base(AbstractConcreteBase): | |||
| def __init__(self, id=None, *args, **kwargs): | |||
| if id is None: | |||
| id = str(uuid.uuid4()) | |||
| super(Base, self).__init__(id=id, *args, **kwargs) | |||
| @ -0,0 +1,11 @@ | |||
| from flask_security import RoleMixin | |||
| from sqlalchemy.dialects.postgresql import UUID | |||
| from . import Base | |||
| from .. import db | |||
| class Role(Base, db.Model, RoleMixin): | |||
| id = db.Column(UUID(), primary_key=True) | |||
| name = db.Column(db.String(80), unique=True) | |||
| description = db.Column(db.String(255)) | |||
| @ -0,0 +1,16 @@ | |||
| from flask_security import UserMixin | |||
| from sqlalchemy.dialects.postgresql import UUID | |||
| from . import Base | |||
| from .. import db | |||
| from .user_role import user_role_map | |||
| class User(Base, db.Model, UserMixin): | |||
| id = db.Column(UUID(), primary_key=True) | |||
| email = db.Column(db.String(255), unique=True) | |||
| password = db.Column(db.String(255)) | |||
| active = db.Column(db.Boolean()) | |||
| confirmed_at = db.Column(db.DateTime()) | |||
| roles = db.relationship('Role', secondary=user_role_map, | |||
| backref=db.backref('users', lazy='dynamic')) | |||
| @ -0,0 +1,9 @@ | |||
| from sqlalchemy.dialects.postgresql import UUID | |||
| from .. import db | |||
| user_role_map = db.Table( | |||
| 'user_role', | |||
| db.Column('user_id', UUID(), db.ForeignKey('user.id')), | |||
| db.Column('role_id', UUID(), db.ForeignKey('role.id')), | |||
| ) | |||
| @ -0,0 +1,6 @@ | |||
| @charset "UTF-8"; | |||
| @import "https://fonts.googleapis.com/css?family=Lato"; | |||
| $family-sans-serif: "Lato", "Helvetica Neue", "Helvetica", "Arial", sans-serif; | |||
| @import "bulma"; | |||
| @ -0,0 +1,30 @@ | |||
| {% macro render_link_for(view_name, inner_text, class="") %} | |||
| {% set url = url_for(view_name) %} | |||
| {% if request.endpoint == view_name %} | |||
| {% set class = class + " is-active" %} | |||
| {% endif %} | |||
| <a href="{{ url }}" class="{{ class }}">{{ inner_text }}</a> | |||
| {% endmacro %} | |||
| {% macro render_field_with_errors(field, class="input") %} | |||
| <p class="control"> | |||
| {{ field.label(class="label") }} | |||
| {% if field.errors %} | |||
| {% set class = class + " is-danger" %} | |||
| {% endif %} | |||
| {{ field(class=class, **kwargs)|safe }} | |||
| {% if field.errors %} | |||
| {% for error in field.errors %} | |||
| <span class="help is-danger">{{ error }}</span> | |||
| {% endfor %} | |||
| {% endif %} | |||
| </p> | |||
| {% endmacro %} | |||
| {% macro render_field(field) %} | |||
| <p class="control"> | |||
| {{ field(**kwargs)|safe }} | |||
| </p> | |||
| {% endmacro %} | |||
| @ -0,0 +1,9 @@ | |||
| {%- with messages = get_flashed_messages(with_categories=true) -%} | |||
| {% if messages %} | |||
| {% for category, message in messages %} | |||
| <div class="notification"> | |||
| <p>{{ message }}</p> | |||
| </div> | |||
| {% endfor %} | |||
| {% endif %} | |||
| {%- endwith %} | |||
| @ -0,0 +1,47 @@ | |||
| {% from "_macros.jinja" import render_link_for %} | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <meta http-equiv="x-ua-compatible" content="ie=edge"> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||
| {% assets "css_all" %} | |||
| <link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}" /> | |||
| {% endassets %} | |||
| </head> | |||
| <body> | |||
| <nav class="nav has-shadow"> | |||
| <div class="container is-fluid"> | |||
| <div class="nav-left"> | |||
| <a class="nav-item" href="{{ url_for('root_index') }}"> | |||
| <h1>Flask app</h1> | |||
| </a> | |||
| </div> | |||
| <div class="nav-right nav-menu"> | |||
| {% if current_user.is_authenticated %} | |||
| <div class="nav-item"> | |||
| <a class="button nav-item" href="{{ url_for_security('logout') }}">Logout</a> | |||
| </div> | |||
| {% else %} | |||
| <div class="nav-item"> | |||
| <a class="button is-primary" href="{{ url_for_security('login') }}">Login</a> | |||
| </div> | |||
| {% endif %} | |||
| </div> | |||
| </div> | |||
| </nav> | |||
| <section class="section"> | |||
| <div class="container is-fluid"> | |||
| {% block content %}{% endblock %} | |||
| </div> | |||
| </section> | |||
| <footer class="footer"> | |||
| <div class="container is-fluid"> | |||
| <h2>Footer</h2> | |||
| </div> | |||
| </div> | |||
| {% assets "js_all" %} | |||
| <script type="text/javascript" src="{{ ASSET_URL }}"></script> | |||
| {% endassets %} | |||
| </body> | |||
| </html> | |||
| @ -0,0 +1,9 @@ | |||
| {% extends "layout.jinja" %} | |||
| {% from "_macros.jinja" import render_link_for %} | |||
| {% block content %} | |||
| <div class="content"> | |||
| <h1>Index</h1> | |||
| <p>Some page content</p> | |||
| </div> | |||
| {% endblock %} | |||
| @ -0,0 +1,19 @@ | |||
| {% extends "layout.jinja" %} | |||
| {% from "_macros.jinja" import render_field_with_errors, render_field %} | |||
| {% block content %} | |||
| <div class="content"> | |||
| <h1>Login</h1> | |||
| {% include "_messages.jinja" %} | |||
| <form action="{{ url_for_security('login') }}" method="POST" name="login_user_form"> | |||
| {{ login_user_form.hidden_tag() }} | |||
| {{ render_field_with_errors(login_user_form.email) }} | |||
| {{ render_field_with_errors(login_user_form.password) }} | |||
| {{ render_field_with_errors(login_user_form.remember, class="checkbox") }} | |||
| {{ render_field(login_user_form.next) }} | |||
| {{ render_field(login_user_form.submit, class="button is-primary") }} | |||
| </form> | |||
| {% include "security/_menu.html" %} | |||
| </div> | |||
| {% endblock %} | |||
| @ -0,0 +1,5 @@ | |||
| from . import root | |||
| def bind_routes(app): | |||
| app.add_url_rule('/', 'root_index', root.index) | |||
| @ -0,0 +1,7 @@ | |||
| from flask import render_template | |||
| from flask_security import login_required | |||
| @login_required | |||
| def index(): | |||
| return render_template('root.jinja') | |||