| @ -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') | |||||