Browse Source

Initial commit

master
Brett Langdon 10 years ago
commit
3b840a840c
31 changed files with 566 additions and 0 deletions
  1. +3
    -0
      .gitignore
  2. +26
    -0
      README.rst
  3. +7
    -0
      cookiecutter.json
  4. +2
    -0
      {{cookiecutter.project_name}}/README.rst
  5. +3
    -0
      {{cookiecutter.project_name}}/bin/{{cookiecutter.project_name}}
  6. +45
    -0
      {{cookiecutter.project_name}}/migrations/alembic.ini
  7. +87
    -0
      {{cookiecutter.project_name}}/migrations/env.py
  8. +22
    -0
      {{cookiecutter.project_name}}/migrations/script.py.mako
  9. +49
    -0
      {{cookiecutter.project_name}}/migrations/versions/70bd704dcb5c_setup_user_and_role_tables.py
  10. +8
    -0
      {{cookiecutter.project_name}}/package.json
  11. +0
    -0
      {{cookiecutter.project_name}}/requirements-dev.txt
  12. +10
    -0
      {{cookiecutter.project_name}}/requirements.txt
  13. +35
    -0
      {{cookiecutter.project_name}}/setup.py
  14. +30
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__init__.py
  15. +2
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py
  16. +38
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/configuration.py
  17. +0
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/forms/__init__.py
  18. +21
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/manager.py
  19. +10
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/models/__init__.py
  20. +11
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/models/role.py
  21. +16
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/models/user.py
  22. +9
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/models/user_role.py
  23. +0
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/static/js/app.js
  24. +6
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/static/scss/app.scss
  25. +30
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/templates/_macros.jinja
  26. +9
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/templates/_messages.jinja
  27. +47
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/templates/layout.jinja
  28. +9
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/templates/root.jinja
  29. +19
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/templates/security/login_user.jinja
  30. +5
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/views/__init__.py
  31. +7
    -0
      {{cookiecutter.project_name}}/{{cookiecutter.project_name}}/views/root.py

+ 3
- 0
.gitignore View File

@ -0,0 +1,3 @@
aws_reboot/static/.webassets-cache
aws_reboot/static/dist
node_modules

+ 26
- 0
README.rst View File

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

+ 7
- 0
cookiecutter.json View File

@ -0,0 +1,7 @@
{
"project_name": "flask_app",
"database_uri": "postgresql://localhost/{{cookiecutter.project_name}}",
"_copy_without_render": [
"*.jinja"
]
}

+ 2
- 0
{{cookiecutter.project_name}}/README.rst View File

@ -0,0 +1,2 @@
{{cookiecutter.project_name}}
===

+ 3
- 0
{{cookiecutter.project_name}}/bin/{{cookiecutter.project_name}} View File

@ -0,0 +1,3 @@
#!/usr/bin/env python
from {{cookiecutter.project_name}}.manager import manager
manager.run()

+ 45
- 0
{{cookiecutter.project_name}}/migrations/alembic.ini View File

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

+ 87
- 0
{{cookiecutter.project_name}}/migrations/env.py View File

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

+ 22
- 0
{{cookiecutter.project_name}}/migrations/script.py.mako View File

@ -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"}

+ 49
- 0
{{cookiecutter.project_name}}/migrations/versions/70bd704dcb5c_setup_user_and_role_tables.py View File

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

+ 8
- 0
{{cookiecutter.project_name}}/package.json View File

@ -0,0 +1,8 @@
{
"name": "{{cookiecutter.project_name}}",
"private": true,
"dependencies": {
"browserify": "^13.1.0",
"bulma": "^0.1.0"
}
}

+ 0
- 0
{{cookiecutter.project_name}}/requirements-dev.txt View File


+ 10
- 0
{{cookiecutter.project_name}}/requirements.txt View File

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

+ 35
- 0
{{cookiecutter.project_name}}/setup.py View File

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

+ 30
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__init__.py View File

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

+ 2
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py View File

@ -0,0 +1,2 @@
from {{cookiecutter.project_name}}.manager import manager
manager.run()

+ 38
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/configuration.py View File

@ -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
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/forms/__init__.py View File


+ 21
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/manager.py View File

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

+ 10
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/models/__init__.py View File

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

+ 11
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/models/role.py View File

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

+ 16
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/models/user.py View File

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

+ 9
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/models/user_role.py View File

@ -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
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/static/js/app.js View File


+ 6
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/static/scss/app.scss View File

@ -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";

+ 30
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/templates/_macros.jinja View File

@ -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 %}

+ 9
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/templates/_messages.jinja View File

@ -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 %}

+ 47
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/templates/layout.jinja View File

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

+ 9
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/templates/root.jinja View File

@ -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 %}

+ 19
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/templates/security/login_user.jinja View File

@ -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 %}

+ 5
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/views/__init__.py View File

@ -0,0 +1,5 @@
from . import root
def bind_routes(app):
app.add_url_rule('/', 'root_index', root.index)

+ 7
- 0
{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/views/root.py View File

@ -0,0 +1,7 @@
from flask import render_template
from flask_security import login_required
@login_required
def index():
return render_template('root.jinja')

Loading…
Cancel
Save