Initial commit
This commit is contained in:
76
flaskbase/__init__.py
Normal file
76
flaskbase/__init__.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import flask
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from flaskbase.extensions import db
|
||||
from flaskbase.middleware import MethodRewriteMiddleware
|
||||
|
||||
_initializers = set()
|
||||
|
||||
|
||||
def import_submodules(app, submodule):
|
||||
ret = list()
|
||||
|
||||
for mod_name in app.config['ENABLED_MODULES']:
|
||||
try:
|
||||
# FIXME support for mod_name directing to submodule
|
||||
mod = __import__('%s.%s' % (mod_name, submodule))
|
||||
ret.append((mod_name, getattr(mod, submodule)))
|
||||
except ImportError:
|
||||
# Check if error occured right in that try clause
|
||||
if sys.exc_info()[2].tb_next:
|
||||
logging.warning('Import failed for %s.%s', mod_name, submodule,
|
||||
exc_info=True)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def create_app(settings_object=None, settings={}):
|
||||
app = flask.Flask(
|
||||
__name__,
|
||||
template_folder='../templates',
|
||||
static_folder='../static'
|
||||
)
|
||||
|
||||
if 'SETTINGS_MODULE' in os.environ:
|
||||
app.config.from_envvar('SETTINGS_MODULE')
|
||||
else:
|
||||
if not settings_object:
|
||||
settings_object = 'flaskbase.settings.{}'.format(
|
||||
os.environ.get('ENV', 'production'))
|
||||
|
||||
app.config.from_object(settings_object)
|
||||
|
||||
app.config.update(settings)
|
||||
|
||||
# FIXME https://github.com/mattupstate/flask-social/issues/34
|
||||
app.wsgi_app = MethodRewriteMiddleware(app.wsgi_app)
|
||||
|
||||
for m_name, m in import_submodules(app, 'extensions'):
|
||||
m.init_app(app)
|
||||
|
||||
import_submodules(app, 'models')
|
||||
|
||||
for m_name, m in import_submodules(app, 'views'):
|
||||
app.register_blueprint(
|
||||
m.blueprint,
|
||||
url_prefix=app.config['URLS'].get(m_name, '/' + m.blueprint.name),
|
||||
)
|
||||
|
||||
import_submodules(app, 'admin')
|
||||
|
||||
for fun in _initializers:
|
||||
fun(app)
|
||||
|
||||
logging.info('Starting...')
|
||||
return app
|
||||
|
||||
|
||||
def initializer(f):
|
||||
_initializers.add(f)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def init_db(app):
|
||||
db.create_all()
|
||||
26
flaskbase/admin.py
Normal file
26
flaskbase/admin.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from flask_admin.contrib.sqla import ModelView as _ModelView
|
||||
from flask_admin import BaseView as _BaseView
|
||||
from flask_security import current_user
|
||||
from flask_principal import RoleNeed, Permission
|
||||
|
||||
|
||||
# FIXME admin homepage is always accessible
|
||||
class AdminProtect(object):
|
||||
roles = []
|
||||
|
||||
def is_accessible(self):
|
||||
return current_user.is_authenticated() and \
|
||||
(current_user.is_superuser or
|
||||
(self.roles and
|
||||
Permission(*[RoleNeed(r) for r in self.roles]).can()))
|
||||
|
||||
|
||||
class ModelView(AdminProtect, _ModelView):
|
||||
def __init__(self, model, *args, **kwargs):
|
||||
kwargs.setdefault('endpoint', model.__name__.lower() + '_model')
|
||||
return super(ModelView, self).__init__(model, *args, **kwargs)
|
||||
named_filter_urls = True
|
||||
|
||||
|
||||
class BaseView(AdminProtect, _BaseView):
|
||||
pass
|
||||
52
flaskbase/extensions.py
Normal file
52
flaskbase/extensions.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
from flask import url_for, render_template
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_security import Security, SQLAlchemyUserDatastore
|
||||
from flask_social import Social
|
||||
from flask_social.datastore import SQLAlchemyConnectionDatastore
|
||||
from flask_mail import Mail
|
||||
from flask_migrate import Migrate
|
||||
from flask_admin import Admin
|
||||
from flask_wtf.csrf import CsrfProtect
|
||||
from flask.ext.markdown import Markdown
|
||||
from flask_pagedown import PageDown
|
||||
|
||||
db = SQLAlchemy()
|
||||
security = Security()
|
||||
social = Social()
|
||||
mail = Mail()
|
||||
migrate = Migrate()
|
||||
admin = Admin(template_mode='bootstrap3')
|
||||
|
||||
|
||||
def init_app(app):
|
||||
db.init_app(app)
|
||||
mail.init_app(app)
|
||||
admin.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
|
||||
@app.context_processor
|
||||
def utility_processor():
|
||||
return {
|
||||
'static': lambda fn: url_for('static', filename=fn)
|
||||
}
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
if app.config.get('LOGGING'):
|
||||
logging.config.dictConfig(app.config['LOGGING'])
|
||||
|
||||
from auth.models import User, Role, Connection
|
||||
security.init_app(app, SQLAlchemyUserDatastore(db, User, Role))
|
||||
social._state = social.init_app(
|
||||
app, SQLAlchemyConnectionDatastore(db, Connection))
|
||||
|
||||
CsrfProtect(app)
|
||||
Markdown(app)
|
||||
PageDown(app)
|
||||
8
flaskbase/factories.py
Normal file
8
flaskbase/factories.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from factory.alchemy import SQLAlchemyModelFactory
|
||||
from flaskbase.extensions import db
|
||||
|
||||
|
||||
class BaseFactory(SQLAlchemyModelFactory):
|
||||
class Meta:
|
||||
abstract = True
|
||||
sqlalchemy_session = db.session
|
||||
13
flaskbase/forms.py
Normal file
13
flaskbase/forms.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from flask_wtf import Form
|
||||
from wtforms_alchemy import model_form_factory
|
||||
# The variable db here is a SQLAlchemy object instance from
|
||||
# Flask-SQLAlchemy package
|
||||
from flaskbase.extensions import db
|
||||
|
||||
BaseModelForm = model_form_factory(Form)
|
||||
|
||||
|
||||
class ModelForm(BaseModelForm):
|
||||
@classmethod
|
||||
def get_session(self):
|
||||
return db.session
|
||||
16
flaskbase/middleware.py
Normal file
16
flaskbase/middleware.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from werkzeug import url_decode
|
||||
|
||||
|
||||
class MethodRewriteMiddleware(object):
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
if 'METHOD_OVERRIDE' in environ.get('QUERY_STRING', ''):
|
||||
args = url_decode(environ['QUERY_STRING'])
|
||||
method = args.get('__METHOD_OVERRIDE__')
|
||||
if method:
|
||||
method = method.encode('ascii', 'replace')
|
||||
environ['REQUEST_METHOD'] = method
|
||||
return self.app(environ, start_response)
|
||||
0
flaskbase/settings/__init__.py
Normal file
0
flaskbase/settings/__init__.py
Normal file
76
flaskbase/settings/base.py
Normal file
76
flaskbase/settings/base.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import os.path
|
||||
|
||||
PROJECT_NAME = 'flaskbase'.capitalize()
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
DEBUG = True
|
||||
|
||||
SECRET_KEY = 'nothing'
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||
|
||||
ENABLED_MODULES = [
|
||||
'flaskbase',
|
||||
'auth',
|
||||
'flatpages',
|
||||
]
|
||||
|
||||
URLS = {
|
||||
'flaskbase': '',
|
||||
'flatpages': '/p',
|
||||
}
|
||||
|
||||
SECURITY_REGISTERABLE = True
|
||||
SECURITY_RECOVERABLE = True
|
||||
SECURITY_CHANGEABLE = True
|
||||
SECURITY_CONFIRMABLE = True
|
||||
SECURITY_TRACKABLE = True
|
||||
|
||||
SECURITY_URL_PREFIX = '/auth'
|
||||
SECURITY_PASSWORD_HASH = 'bcrypt'
|
||||
SECURITY_PASSWORD_SALT = SECRET_KEY
|
||||
|
||||
SECURITY_EMAIL_SUBJECT_REGISTER = 'Welcome to ' + PROJECT_NAME
|
||||
|
||||
SECURITY_INVITE_WITHIN = '60 days'
|
||||
|
||||
MAIL_SUPPRESS_SEND = DEBUG
|
||||
|
||||
# Logging setup
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'standard': {
|
||||
'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'default': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||
'filename': os.path.join(BASE_DIR, 'logs', 'app.log'),
|
||||
'formatter': 'standard',
|
||||
'when': 'midnight',
|
||||
},
|
||||
'console': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'standard'
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'': {
|
||||
'handlers': ['default', 'console'],
|
||||
'level': 'INFO',
|
||||
'propagate': True
|
||||
},
|
||||
'django.request': {
|
||||
'handlers': ['default'],
|
||||
'level': 'WARN',
|
||||
'propagate': False
|
||||
},
|
||||
}
|
||||
}
|
||||
21
flaskbase/settings/development.py
Normal file
21
flaskbase/settings/development.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from flaskbase.settings.base import *
|
||||
|
||||
#SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://postgres:postgres@postgres'
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/flaskbase.db'
|
||||
|
||||
SOCIAL_FACEBOOK = {
|
||||
'consumer_key': 'APP-ID',
|
||||
'consumer_secret': 'APP-SECRET',
|
||||
'request_token_params': {
|
||||
'scope': 'email,user_friends',
|
||||
}
|
||||
}
|
||||
|
||||
from flask_mail import email_dispatched
|
||||
|
||||
|
||||
def log_message(message, app):
|
||||
app.logger.debug('subject:%s', message.subject)
|
||||
app.logger.debug('body:%s', message.body)
|
||||
|
||||
email_dispatched.connect(log_message)
|
||||
4
flaskbase/settings/testing.py
Normal file
4
flaskbase/settings/testing.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from flaskbase.settings.base import *
|
||||
|
||||
TESTING = True
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://postgres:postgres@postgres/testing'
|
||||
27
flaskbase/tests.py
Normal file
27
flaskbase/tests.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import unittest
|
||||
import flaskbase
|
||||
import contextlib
|
||||
|
||||
from flaskbase.extensions import db
|
||||
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
"""
|
||||
Base testcase class
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.flask_app = flaskbase.create_app(settings={
|
||||
'SQLALCHEMY_DATABASE_URI': 'sqlite://',
|
||||
})
|
||||
|
||||
self.app = self.flask_app.test_client()
|
||||
flaskbase.init_db(self.flask_app)
|
||||
|
||||
def tearDown(self):
|
||||
db.session.remove()
|
||||
with contextlib.closing(db.engine.connect()) as con:
|
||||
trans = con.begin()
|
||||
for table in reversed(db.Model.metadata.sorted_tables):
|
||||
con.execute(table.delete())
|
||||
trans.commit()
|
||||
9
flaskbase/utils.py
Normal file
9
flaskbase/utils.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from sqlalchemy.orm import exc
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
|
||||
def get_object_or_404(model, *criterion):
|
||||
try:
|
||||
return model.query.filter(*criterion).one()
|
||||
except exc.NoResultFound, exc.MultipleResultsFound:
|
||||
abort(404)
|
||||
8
flaskbase/views.py
Normal file
8
flaskbase/views.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
blueprint = Blueprint('core', __name__)
|
||||
|
||||
|
||||
@blueprint.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
3
flaskbase/wsgi.py
Normal file
3
flaskbase/wsgi.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from flaskbase import create_app
|
||||
|
||||
application = create_app()
|
||||
Reference in New Issue
Block a user