Initial commit
This commit is contained in:
commit
94f0009144
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
*.py[co]
|
||||
*.swp
|
||||
app.db
|
||||
db.sqlite3
|
||||
.coverage
|
||||
.coverage_report
|
||||
.bash_history
|
||||
.ropeproject
|
||||
ssl
|
||||
flaskbase/settings/production.py
|
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@ -0,0 +1,29 @@
|
||||
FROM ubuntu
|
||||
|
||||
RUN useradd -d /app app
|
||||
RUN apt-get update && \
|
||||
apt-get -y install python \
|
||||
python-dev \
|
||||
python-pip \
|
||||
libpq-dev \
|
||||
uwsgi \
|
||||
uwsgi-plugin-python \
|
||||
git
|
||||
|
||||
# We use that to cache last requirements version
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
RUN pip install -r /app/requirements.txt
|
||||
|
||||
RUN apt-get -y install python-psycopg2
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
EXPOSE 8000
|
||||
USER app
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
CMD ./manage.py db upgrade && \
|
||||
uwsgi --http-socket :8000 --processes 4 --master --plugin python \
|
||||
--pythonpath /app --module flaskbase.wsgi:application \
|
||||
--touch-reload '/app/flaskbase/wsgi.py'
|
29
README.md
Normal file
29
README.md
Normal file
@ -0,0 +1,29 @@
|
||||
A simple go-to Flask template project incorporating many cool module, eg:
|
||||
|
||||
* Flask-Security for user and permissions management
|
||||
* Flask-Social for social login
|
||||
* Flask-WTF for form management
|
||||
* Flask-SQLAlchemy for database access
|
||||
* Flask-Migrate for migrations
|
||||
* Flask-Admin for admin panel
|
||||
* Flask-Script for management commands
|
||||
* Admin is only accessible for `is_superuser`'s
|
||||
* Social authorization automatically creates user model and allows him to make
|
||||
sure email fetched from social (Facebook only right now) is correct.
|
||||
|
||||
After all, when you look at it, it's just a Django-for-Flask... :)
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
* Clone repository
|
||||
* `mv flaskbase yourproject`
|
||||
* `find . -type f -print0 | xargs -0 sed -i 's/flaskbase/yourproject/g'`
|
||||
* ...
|
||||
* PROFIT!
|
||||
|
||||
|
||||
TODO
|
||||
----
|
||||
* Migrate to Flask-Social-Blueprint. Much cleaner.
|
0
auth/__init__.py
Normal file
0
auth/__init__.py
Normal file
90
auth/admin.py
Normal file
90
auth/admin.py
Normal file
@ -0,0 +1,90 @@
|
||||
from flaskbase.extensions import admin, db
|
||||
from flaskbase.admin import ModelView
|
||||
from auth.models import User, Role, Connection
|
||||
from jinja2 import Markup
|
||||
|
||||
from flask import flash, redirect
|
||||
from flask_admin.babel import gettext
|
||||
from flask_admin.base import expose
|
||||
from flask_admin.helpers import (get_redirect_target, flash_errors)
|
||||
from flask_security import login_user
|
||||
|
||||
|
||||
class UserModelView(ModelView):
|
||||
column_searchable_list = (
|
||||
'email', 'current_login_ip', 'last_login_ip',
|
||||
)
|
||||
|
||||
column_list = (
|
||||
'email', 'active', 'is_superuser', 'last_login_at', 'last_login_ip',
|
||||
'display_name'
|
||||
)
|
||||
|
||||
column_filters = column_list
|
||||
column_sortable_list = column_list
|
||||
|
||||
column_labels = {
|
||||
'is_superuser': 'SU',
|
||||
}
|
||||
|
||||
column_descriptions = {
|
||||
'is_superuser': 'Superuser',
|
||||
}
|
||||
|
||||
inline_models = (Connection,)
|
||||
|
||||
list_template = 'admin/user_list.html'
|
||||
can_impersonate = True
|
||||
|
||||
@expose('/impersonate/', methods=('POST',))
|
||||
def impersonate_view(self):
|
||||
"""
|
||||
Impersonate user view. Only POST method is allowed.
|
||||
"""
|
||||
return_url = get_redirect_target() or self.get_url('.index_view')
|
||||
|
||||
if not self.can_impersonate:
|
||||
return redirect(return_url)
|
||||
|
||||
form = self.delete_form()
|
||||
|
||||
if self.validate_form(form):
|
||||
# id is Required()
|
||||
id = form.id.data
|
||||
|
||||
model = self.get_one(id)
|
||||
|
||||
if model is None:
|
||||
return redirect(return_url)
|
||||
|
||||
if login_user(model):
|
||||
flash(gettext('You are now %s' % (model,)))
|
||||
return redirect('/')
|
||||
else:
|
||||
flash_errors(form, message='Failed to impersonate. %(error)s')
|
||||
|
||||
return redirect(return_url)
|
||||
|
||||
|
||||
class ConnectionModelView(ModelView):
|
||||
column_list = (
|
||||
'user',
|
||||
'provider_id',
|
||||
'full_name',
|
||||
'email',
|
||||
)
|
||||
|
||||
column_formatters = {
|
||||
'full_name':
|
||||
lambda v, c, m, n: Markup(
|
||||
'<a href="%s" target="_blank"><img src="%s" /> %s</a>') % (
|
||||
m.profile_url, m.image_url, m.full_name)
|
||||
}
|
||||
|
||||
column_filters = column_list
|
||||
column_sortable_list = column_list
|
||||
|
||||
|
||||
admin.add_view(UserModelView(User, db.session))
|
||||
admin.add_view(ModelView(Role, db.session))
|
||||
admin.add_view(ConnectionModelView(Connection, db.session))
|
21
auth/commands.py
Normal file
21
auth/commands.py
Normal file
@ -0,0 +1,21 @@
|
||||
from flask_script import Manager
|
||||
from auth.models import User
|
||||
from flaskbase.extensions import db
|
||||
import datetime
|
||||
|
||||
|
||||
manager = Manager()
|
||||
|
||||
|
||||
@manager.option('-e', '--email', help='User email', default='root')
|
||||
@manager.option('-p', '--password', help='Password', default='t00r')
|
||||
def create_superuser(email, password):
|
||||
try:
|
||||
u = User(email=email, password=password, active=True,
|
||||
is_superuser=True, confirmed_at=datetime.datetime.utcnow())
|
||||
db.session.add(u)
|
||||
db.session.commit()
|
||||
|
||||
print('User %s successfuly created.' % (email,))
|
||||
except Exception, e:
|
||||
print('User creation failed: %s' % (e,))
|
12
auth/factories.py
Normal file
12
auth/factories.py
Normal file
@ -0,0 +1,12 @@
|
||||
from flaskbase.factories import BaseFactory
|
||||
from factory import Sequence
|
||||
from auth.models import User
|
||||
|
||||
|
||||
class UserFactory(BaseFactory):
|
||||
email = Sequence(lambda n: 'user{0}@example.com'.format(n))
|
||||
password = Sequence(lambda n: 'UserPassword{0}'.format(n))
|
||||
active = True
|
||||
|
||||
class Meta:
|
||||
model = User
|
6
auth/forms.py
Normal file
6
auth/forms.py
Normal file
@ -0,0 +1,6 @@
|
||||
from flask_wtf import Form
|
||||
from flask_security.forms import UniqueEmailFormMixin
|
||||
|
||||
|
||||
class SocialLoginConfirmForm(Form, UniqueEmailFormMixin):
|
||||
pass
|
120
auth/models.py
Normal file
120
auth/models.py
Normal file
@ -0,0 +1,120 @@
|
||||
from flask import g, session, url_for
|
||||
from flask_security import RoleMixin, UserMixin
|
||||
from flask_login import user_logged_in
|
||||
from flaskbase.extensions import db, social
|
||||
from importlib import import_module
|
||||
|
||||
# Define models
|
||||
roles_users = db.Table(
|
||||
'roles_users',
|
||||
db.Column('user_id', db.Integer(), db.ForeignKey('users.id')),
|
||||
db.Column('role_id', db.Integer(), db.ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
|
||||
class Role(db.Model, RoleMixin):
|
||||
__tablename__ = 'roles'
|
||||
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
name = db.Column(db.String(80), unique=True)
|
||||
description = db.Column(db.String(255))
|
||||
|
||||
def __unicode__(self):
|
||||
return '{0.description} ({0.name})'.format(self)
|
||||
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(255), unique=True)
|
||||
password = db.Column(db.String(255))
|
||||
|
||||
active = db.Column(db.Boolean())
|
||||
is_superuser = db.Column(db.Boolean(), default=False)
|
||||
|
||||
last_login_ip = db.Column(db.String(128))
|
||||
last_login_at = db.Column(db.DateTime())
|
||||
current_login_ip = db.Column(db.String(128))
|
||||
current_login_at = db.Column(db.DateTime())
|
||||
login_count = db.Column(db.Integer)
|
||||
|
||||
invited_by_id = db.Column(db.Integer, db.ForeignKey('users.id',
|
||||
ondelete='SET NULL'))
|
||||
invited_by = db.relationship('User', remote_side=[id],
|
||||
backref='invited_users')
|
||||
|
||||
confirmed_at = db.Column(db.DateTime())
|
||||
roles = db.relationship('Role', secondary=roles_users,
|
||||
backref=db.backref('users', lazy='dynamic'))
|
||||
|
||||
display_name = db.Column(db.String(128), info={
|
||||
'label': 'Display name',
|
||||
'description': 'Your email will be used instead if empty.',
|
||||
})
|
||||
|
||||
def __repr__(self):
|
||||
return '<User {0.email}>'.format(self)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.display_name or self.email
|
||||
|
||||
@property
|
||||
def image_url(self):
|
||||
if self.connections:
|
||||
return self.connections[0].image_url
|
||||
|
||||
return None
|
||||
|
||||
def first_login(self):
|
||||
"""Checks if user has just logged in (used in base template)"""
|
||||
g.first_login = session.pop('first_login', False) or \
|
||||
g.get('first_login', False)
|
||||
return g.first_login
|
||||
|
||||
@property
|
||||
def profile_url(self):
|
||||
return url_for('profile.public', id=self.id, _external=True)
|
||||
|
||||
@property
|
||||
def is_ghost(self):
|
||||
return not self.confirmed_at
|
||||
|
||||
|
||||
@user_logged_in.connect
|
||||
def track_first_login(app, user):
|
||||
"""Tracks first user login"""
|
||||
if not user.login_count:
|
||||
session['first_login'] = True
|
||||
|
||||
|
||||
class Connection(db.Model):
|
||||
__tablename__ = 'connections'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
user = db.relationship('User', backref='connections')
|
||||
provider_id = db.Column(db.String(255))
|
||||
provider_user_id = db.Column(db.String(255))
|
||||
access_token = db.Column(db.String(255))
|
||||
secret = db.Column(db.String(255))
|
||||
email = db.Column(db.String(255))
|
||||
full_name = db.Column(db.String(255))
|
||||
display_name = db.Column(db.String(255))
|
||||
profile_url = db.Column(db.String(512))
|
||||
image_url = db.Column(db.String(512))
|
||||
rank = db.Column(db.Integer)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Connection {0.provider_id}:{0.provider_user_id} {0.user}>' \
|
||||
.format(self)
|
||||
|
||||
def get_api(self):
|
||||
provider = social.providers.get(self.provider_id)
|
||||
if not provider:
|
||||
return None
|
||||
|
||||
module = import_module(provider.module)
|
||||
return module.get_api(connection=self,
|
||||
consumer_key=provider.consumer_key,
|
||||
consumer_secret=provider.consumer_secret)
|
80
auth/views.py
Normal file
80
auth/views.py
Normal file
@ -0,0 +1,80 @@
|
||||
from flask import Blueprint, render_template, \
|
||||
redirect, url_for, session, abort
|
||||
from flask import current_app as app
|
||||
from flask_security import login_user
|
||||
from flask_security.confirmable import confirm_user
|
||||
from flask_social import connection_created, login_completed, login_failed
|
||||
from flask_social.utils import get_connection_values_from_oauth_response, \
|
||||
get_provider_or_404
|
||||
from flask_social.views import connect_handler
|
||||
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from flaskbase.extensions import db
|
||||
|
||||
from auth.forms import SocialLoginConfirmForm
|
||||
|
||||
blueprint = Blueprint('auth', __name__)
|
||||
|
||||
_security = LocalProxy(lambda: app.extensions['security'])
|
||||
|
||||
|
||||
@connection_created.connect
|
||||
@login_completed.connect
|
||||
def on_connection_created(app, connection=None, user=None, provider=None):
|
||||
if not connection:
|
||||
connection = provider.get_connection()
|
||||
|
||||
if not connection.user and user:
|
||||
connection.user = user
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@login_failed.connect
|
||||
def on_login_failed(app, provider, oauth_response):
|
||||
session['failed_login_connection'] = \
|
||||
get_connection_values_from_oauth_response(provider, oauth_response)
|
||||
|
||||
abort(redirect(url_for('auth.confirm_social')))
|
||||
|
||||
|
||||
@blueprint.route('/confirm_social', methods=['GET', 'POST'])
|
||||
def confirm_social():
|
||||
connection_values = session.get('failed_login_connection', None)
|
||||
|
||||
if not connection_values:
|
||||
return redirect('/')
|
||||
|
||||
form = SocialLoginConfirmForm(
|
||||
email=connection_values.get('email', ''),
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
# Prevent Flask-Security form sending confirmation email
|
||||
_security.confirmable = False
|
||||
|
||||
kwargs = {'email': form.email.data}
|
||||
if connection_values.get('full_name'):
|
||||
kwargs['display_name'] = connection_values['full_name']
|
||||
|
||||
# Create and login user
|
||||
user = _security.datastore.create_user(**kwargs)
|
||||
confirm_user(user)
|
||||
_security.datastore.commit()
|
||||
|
||||
login_user(user)
|
||||
|
||||
# TODO: possibly move it to user_logged_in signal handler?
|
||||
|
||||
# Process pending social connection
|
||||
connection_values = session.pop('failed_login_connection', None)
|
||||
connection_values['user_id'] = user.id
|
||||
connect_handler(
|
||||
connection_values,
|
||||
get_provider_or_404(connection_values['provider_id']))
|
||||
|
||||
_security.datastore.commit()
|
||||
return redirect('/')
|
||||
|
||||
return render_template('security/confirm_social.html', form=form)
|
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@ -0,0 +1,29 @@
|
||||
postgresdata:
|
||||
image: postgres
|
||||
volumes:
|
||||
- /var/lib/postgres
|
||||
command: /bin/true
|
||||
|
||||
postgres:
|
||||
image: postgres
|
||||
volumes_from:
|
||||
- postgresdata
|
||||
|
||||
web:
|
||||
build: .
|
||||
links:
|
||||
- postgres
|
||||
volumes:
|
||||
- ".:/app"
|
||||
environment:
|
||||
VIRTUAL_HOST: flaskbase.local
|
||||
ENV: development
|
||||
|
||||
proxy:
|
||||
image: jwilder/nginx-proxy
|
||||
volumes:
|
||||
- /var/run/docker.sock:/tmp/docker.sock
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
DEFAULT_HOST: flaskbase.local
|
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()
|
0
flatpages/__init__.py
Normal file
0
flatpages/__init__.py
Normal file
22
flatpages/admin.py
Normal file
22
flatpages/admin.py
Normal file
@ -0,0 +1,22 @@
|
||||
from flaskbase.admin import ModelView
|
||||
from flaskbase.extensions import admin, db
|
||||
from flatpages.models import Page
|
||||
from flask_pagedown.fields import PageDownField
|
||||
|
||||
|
||||
class PageModelView(ModelView):
|
||||
roles = ['editor']
|
||||
|
||||
column_list = ('slug', 'title', 'updated')
|
||||
form_columns = ('title', 'slug', 'content')
|
||||
form_extra_fields = {
|
||||
'content': PageDownField('Content'),
|
||||
}
|
||||
|
||||
form_widget_args = {
|
||||
'content': {
|
||||
'rows': 10,
|
||||
}
|
||||
}
|
||||
|
||||
admin.add_view(PageModelView(Page, db.session))
|
22
flatpages/models.py
Normal file
22
flatpages/models.py
Normal file
@ -0,0 +1,22 @@
|
||||
from flaskbase.extensions import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import foreign, remote
|
||||
|
||||
|
||||
class Page(db.Model):
|
||||
__tablename__ = 'pages'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
slug = db.Column(db.String(64), unique=True)
|
||||
title = db.Column(db.String(256))
|
||||
content = db.Column(db.Text)
|
||||
|
||||
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated = db.Column(db.DateTime, default=datetime.utcnow, nullable=False,
|
||||
onupdate=datetime.utcnow)
|
||||
|
||||
subpages = db.relationship(
|
||||
'Page', primaryjoin=remote(foreign(slug)).like(slug.concat('/%')),
|
||||
viewonly=True)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Page {0.slug} "{0.title}">'.format(self)
|
13
flatpages/views.py
Normal file
13
flatpages/views.py
Normal file
@ -0,0 +1,13 @@
|
||||
from flask import Blueprint, render_template
|
||||
from flaskbase.utils import get_object_or_404
|
||||
from flatpages.models import Page
|
||||
|
||||
blueprint = Blueprint('flatpages', __name__)
|
||||
|
||||
|
||||
@blueprint.route('/<path:slug>')
|
||||
def show(slug):
|
||||
return render_template(
|
||||
'flatpages/show.html',
|
||||
page=get_object_or_404(Page, Page.slug == slug),
|
||||
)
|
2
logs/.gitignore
vendored
Normal file
2
logs/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
57
manage.py
Executable file
57
manage.py
Executable file
@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from flask_script import Manager
|
||||
from flask_migrate import MigrateCommand
|
||||
|
||||
from flaskbase import create_app, import_submodules
|
||||
from flaskbase.extensions import db
|
||||
|
||||
app = create_app()
|
||||
manager = Manager(app)
|
||||
manager.add_command('db', MigrateCommand)
|
||||
|
||||
for m_name, m in import_submodules(app, 'commands'):
|
||||
manager.add_command(m_name.replace('mod_', ''), m.manager)
|
||||
|
||||
|
||||
@manager.command
|
||||
def syncdb():
|
||||
db.create_all()
|
||||
|
||||
try:
|
||||
from flask.ext.zen import Test, ZenTest
|
||||
from flask_script import Option
|
||||
from coverage import coverage
|
||||
|
||||
class CoverageTest(Test):
|
||||
def get_options(self):
|
||||
return super(CoverageTest, self).get_options() + [
|
||||
Option('-H', '--html', dest='html', default=None,
|
||||
help='Save HTML report to directory'),
|
||||
]
|
||||
|
||||
def run(self, html=None, *args, **kwargs):
|
||||
cov = coverage(branch=True, source=['.'],
|
||||
omit=['*/tests.py', 'tests.py', 'manage.py',
|
||||
'flaskbase/settings/*', '*/admin.py'])
|
||||
cov.start()
|
||||
|
||||
super(CoverageTest, self).run(*args, **kwargs)
|
||||
|
||||
cov.stop()
|
||||
cov.save()
|
||||
print
|
||||
cov.report()
|
||||
|
||||
if html:
|
||||
cov.html_report(directory=html)
|
||||
print '\nHTML coverage report saved to', html
|
||||
|
||||
manager.add_command('test', Test())
|
||||
manager.add_command('coverage', CoverageTest())
|
||||
manager.add_command('zen', ZenTest())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
manager.run()
|
1
migrations/README
Executable file
1
migrations/README
Executable file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
45
migrations/alembic.ini
Normal file
45
migrations/alembic.ini
Normal 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
|
73
migrations/env.py
Executable file
73
migrations/env.py
Executable file
@ -0,0 +1,73 @@
|
||||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
|
||||
# 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)
|
||||
|
||||
# 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.
|
||||
|
||||
"""
|
||||
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,
|
||||
**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
migrations/script.py.mako
Executable file
22
migrations/script.py.mako
Executable 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"}
|
86
migrations/versions/31d442356906_initial_migration.py
Normal file
86
migrations/versions/31d442356906_initial_migration.py
Normal file
@ -0,0 +1,86 @@
|
||||
"""Initial migration
|
||||
|
||||
Revision ID: 31d442356906
|
||||
Revises: None
|
||||
Create Date: 2018-03-31 18:18:51.582410
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '31d442356906'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('pages',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('slug', sa.String(length=64), nullable=True),
|
||||
sa.Column('title', sa.String(length=256), nullable=True),
|
||||
sa.Column('content', sa.Text(), nullable=True),
|
||||
sa.Column('created', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated', sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('slug')
|
||||
)
|
||||
op.create_table('roles',
|
||||
sa.Column('id', sa.Integer(), 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('users',
|
||||
sa.Column('id', sa.Integer(), 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('is_superuser', sa.Boolean(), nullable=True),
|
||||
sa.Column('last_login_ip', sa.String(length=128), nullable=True),
|
||||
sa.Column('last_login_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('current_login_ip', sa.String(length=128), nullable=True),
|
||||
sa.Column('current_login_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('login_count', sa.Integer(), nullable=True),
|
||||
sa.Column('invited_by_id', sa.Integer(), nullable=True),
|
||||
sa.Column('confirmed_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('display_name', sa.String(length=128), nullable=True),
|
||||
sa.ForeignKeyConstraint(['invited_by_id'], ['users.id'], ondelete='SET NULL'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('email')
|
||||
)
|
||||
op.create_table('connections',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('provider_id', sa.String(length=255), nullable=True),
|
||||
sa.Column('provider_user_id', sa.String(length=255), nullable=True),
|
||||
sa.Column('access_token', sa.String(length=255), nullable=True),
|
||||
sa.Column('secret', sa.String(length=255), nullable=True),
|
||||
sa.Column('email', sa.String(length=255), nullable=True),
|
||||
sa.Column('full_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('display_name', sa.String(length=255), nullable=True),
|
||||
sa.Column('profile_url', sa.String(length=512), nullable=True),
|
||||
sa.Column('image_url', sa.String(length=512), nullable=True),
|
||||
sa.Column('rank', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('roles_users',
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], )
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('roles_users')
|
||||
op.drop_table('connections')
|
||||
op.drop_table('users')
|
||||
op.drop_table('roles')
|
||||
op.drop_table('pages')
|
||||
### end Alembic commands ###
|
40
requirements.txt
Normal file
40
requirements.txt
Normal file
@ -0,0 +1,40 @@
|
||||
Flask==0.10.1
|
||||
Flask-Admin==1.2.0
|
||||
Flask-Login==0.2.11
|
||||
Flask-Mail==0.9.1
|
||||
Flask-Markdown==0.3
|
||||
Flask-Migrate==1.5.0
|
||||
Flask-OAuth==0.12
|
||||
Flask-PageDown==0.2.2
|
||||
Flask-Principal==0.4.0
|
||||
Flask-SQLAlchemy==2.0
|
||||
Flask-Script==2.0.5
|
||||
Flask-Security==1.7.4
|
||||
Flask-Social==1.6.2
|
||||
Flask-WTF==0.12
|
||||
Jinja2==2.8
|
||||
Mako==1.0.1
|
||||
Markdown==2.6.11
|
||||
MarkupSafe==0.23
|
||||
SQLAlchemy==1.0.8
|
||||
WTForms==2.0.2
|
||||
Werkzeug==0.10.4
|
||||
alembic==0.7.7
|
||||
argparse==1.2.1
|
||||
bcrypt==3.1.4
|
||||
blinker==1.4
|
||||
certifi==2018.1.18
|
||||
cffi==1.11.5
|
||||
chardet==3.0.4
|
||||
facebook-sdk==2.0.0
|
||||
httplib2==0.9.1
|
||||
idna==2.6
|
||||
itsdangerous==0.24
|
||||
oauth2==1.5.211
|
||||
passlib==1.6.5
|
||||
psycopg2==2.7.4
|
||||
pycparser==2.18
|
||||
requests==2.18.4
|
||||
six==1.11.0
|
||||
urllib3==1.22
|
||||
wsgiref==0.1.2
|
7500
static/css/bootstrap.css
vendored
Normal file
7500
static/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
templates/404.html
Normal file
6
templates/404.html
Normal file
@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% set page_title = '404' %}
|
||||
|
||||
{% block title_actions %}
|
||||
<small>Page could not be found</small>
|
||||
{% endblock %}
|
56
templates/_formhelpers.html
Normal file
56
templates/_formhelpers.html
Normal file
@ -0,0 +1,56 @@
|
||||
{% macro render_field(field, prefix=None, suffix=None, layout=True, label=True) %}
|
||||
{% if field.type == 'HiddenField' %}
|
||||
{{ field(**kwargs) }}
|
||||
{% else %}
|
||||
{% if layout %}
|
||||
<div class="form-group{% if field.errors %} has-error{% endif %}">
|
||||
{% if field.type == 'BooleanField' %}
|
||||
<div class="col-xs-3"></div>
|
||||
{% elif label %}
|
||||
{{ field.label(class_='col-xs-3 control-label') }}
|
||||
{% endif %}
|
||||
<div class="col-xs-9">
|
||||
{% endif %}
|
||||
|
||||
{{ render_field_inner(field, prefix, suffix, label=label, **kwargs) }}
|
||||
|
||||
{% if layout %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_field_inner(field, prefix=None, suffix=None, label=True) %}
|
||||
{% if field.type == 'BooleanField' %}<div class="checkbox"><label for="{{ field.id }}">{% endif %}
|
||||
{% if prefix or suffix %}<div class="input-group">{% endif %}
|
||||
{% if prefix %}<span class="input-group-addon">{{ prefix }}</span>{% endif %}
|
||||
{% if field.type == 'BooleanField' %}
|
||||
{{ field(**kwargs) }} {% if label %}{{ field.label.text }}{% endif %}
|
||||
{% else %}
|
||||
{{ field(class_='form-control', **kwargs) }}
|
||||
{% endif %}
|
||||
{% if suffix %}<span class="input-group-addon">{{ suffix }}</span>{% endif %}
|
||||
{% if prefix or suffix %}</div>{% endif %}
|
||||
{% if field.description %}
|
||||
<span class="help-block">{{ field.description }}</span>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}
|
||||
<span class="help-block">{{ error }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if field.type == 'BooleanField' %}</label></div>{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_submit(label='Submit', class_='btn btn-primary', layout=True) %}
|
||||
{% if layout %}
|
||||
<div class="form-group">
|
||||
<div class="col-xs-9 col-xs-offset-3">
|
||||
{% endif %}
|
||||
<button type="submit" class="{{ class_ }}">{{ label }}</button>
|
||||
{% if layout %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
32
templates/_socialhelpers.html
Normal file
32
templates/_socialhelpers.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% macro social_register(provider_id, display_name) %}
|
||||
<form action="{{ url_for('social.login', provider_id=provider_id) }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<button type="submit" class="btn btn-primary btn-large">Register with {{ display_name }}</button>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro social_login(provider_id, display_name) %}
|
||||
<form action="{{ url_for('social.login', provider_id=provider_id) }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<button type="submit" class="btn btn-primary btn-large">Login with {{ display_name }}</button>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro show_provider_button(provider_id, display_name, conn, btn_type='primary') %}
|
||||
{% if conn %}
|
||||
{#
|
||||
<form action="{{ url_for('social.remove_connection', provider_id=conn.provider_id, provider_user_id=conn.provider_user_id) }}?__METHOD_OVERRIDE__=DELETE" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button type="submit" class="btn btn-{{ btn_type }}">Disconnect {{ display_name }}</button>
|
||||
</div>
|
||||
</form>
|
||||
#}
|
||||
<button type="button" class="btn btn-default invite-friends">Invite {{ display_name }} friends <i class="glyphicon glyphicon-share-alt"></i></button>
|
||||
{% else %}
|
||||
<form action="{{ url_for('social.connect', provider_id=provider_id) }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<button type="submit" class="btn btn-{{ btn_type }}">Connect {{ display_name }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
32
templates/admin/actions.html
Normal file
32
templates/admin/actions.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% import 'admin/static.html' as admin_static with context %}
|
||||
|
||||
{% macro dropdown(actions, btn_class='dropdown-toggle') -%}
|
||||
<a class="{{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)"><b>{{ _gettext('With selected') }}</b><b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
{% for p in actions %}
|
||||
<li>
|
||||
<a href="javascript:void(0)" onclick="return modelActions.execute('{{ p[0] }}');">{{ _gettext(p[1]) }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro form(actions, url) %}
|
||||
{% if actions %}
|
||||
<form id="action_form" action="{{ url }}" method="POST" style="display: none">
|
||||
{% if csrf_token %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
{% endif %}
|
||||
<input type="hidden" id="action" name="action" />
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro script(message, actions, actions_confirmation) %}
|
||||
{% if actions %}
|
||||
<script src="{{ admin_static.url(filename='admin/js/actions-1.0.0.js') }}"></script>
|
||||
<script language="javascript">
|
||||
var modelActions = new AdminModelActions({{ message|tojson|safe }}, {{ actions_confirmation|tojson|safe }});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
27
templates/admin/base.html
Normal file
27
templates/admin/base.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends 'base.html' %}
|
||||
{% import 'admin/layout.html' as layout with context -%}
|
||||
{% import 'admin/static.html' as admin_static with context %}
|
||||
|
||||
{% block head_css %}{{ super() }}
|
||||
<link href="{{ admin_static.url(filename='admin/css/bootstrap3/admin.css') }}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-md-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
{{ layout.menu() }}
|
||||
{{ layout.menu_links() }}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-10">
|
||||
{% set render_ctx = h.resolve_ctx() %}
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail_js %}{{ super() }}
|
||||
<script src="{{ admin_static.url(filename='vendor/moment-2.8.4.min.js') }}" type="text/javascript"></script>
|
||||
<script src="{{ admin_static.url(filename='vendor/select2/select2.min.js') }}" type="text/javascript"></script>
|
||||
{{ pagedown.include_pagedown() }}
|
||||
{% endblock %}
|
18
templates/admin/user_list.html
Normal file
18
templates/admin/user_list.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends 'admin/model/list.html' %}
|
||||
|
||||
{% block list_row_actions %}{{ super() }}
|
||||
{% if admin_view.can_impersonate %}
|
||||
<form class="icon" method="POST" action="{{ get_url('.impersonate_view') }}">
|
||||
{{ delete_form.id(value=get_pk_value(row)) }}
|
||||
{{ delete_form.url(value=return_url) }}
|
||||
{% if delete_form.csrf_token %}
|
||||
{{ delete_form.csrf_token }}
|
||||
{% elif csrf_token %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
{% endif %}
|
||||
<button onclick="return confirm('{{ _gettext('Are you sure you want to impersonate as this user?') }}');" title="Impersonate user">
|
||||
<span class="glyphicon glyphicon-eye-open"></span>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
123
templates/base.html
Normal file
123
templates/base.html
Normal file
@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
{% block head_meta %}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
{% endblock %}
|
||||
<title>{{ config['PROJECT_NAME'] }}{% if page_title %} - {{ page_title }}{% endif %}</title>
|
||||
{% block head_css %}
|
||||
<link rel="stylesheet" href="{{ static('css/bootstrap.css') }}" media="screen">
|
||||
<link rel="stylesheet" href="{{ static('css/base.css') }}" media="screen">
|
||||
{% endblock -%}
|
||||
{%- block head %}{% endblock -%}
|
||||
{%- block head_tail -%}{%- endblock -%}
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a href="/" class="navbar-brand"><i class="glyphicon glyphicon-stats"></i> {{ config['PROJECT_NAME'] }}
|
||||
{%- if config['DEBUG'] or config.get('BRAND_NOTE') %} <small>{{ config.get('BRAND_NOTE', 'development') }}</small>{% endif -%}
|
||||
</a>
|
||||
<button class="navbar-toggle" type="button" data-toggle="collapse" data-target="#navbar-main">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse" id="navbar-main">
|
||||
{% macro navbar_link(href, title=None) %}
|
||||
<li{% if href == request.endpoint and request.view_args == kwargs %} class="active"{% endif %}>
|
||||
<a href="{{ url_for(href, **kwargs) }}">{% if not title %}{{ caller() }}{% else %}{{ title }}{% endif %}</a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
{% block main_menu %}
|
||||
{# Here add your main menu items, eg: #}
|
||||
{# navbar_link('friends.index', 'Friends') #}
|
||||
{% endblock %}
|
||||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{{ navbar_link('flatpages.show', 'FAQ', slug='faq') }}
|
||||
{% if current_user.is_authenticated() %}
|
||||
{% if current_user.is_superuser or current_user.roles %}
|
||||
{{ navbar_link('admin.index', 'Admin') }}
|
||||
{% endif %}
|
||||
<p class="navbar-text">{{ current_user }}</p>
|
||||
{{ navbar_link('security.logout', 'Logout') }}
|
||||
{% else %}
|
||||
{{ navbar_link('security.register', 'Sign up') }}
|
||||
{{ navbar_link('security.login', 'Login') }}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% block messages %}
|
||||
{% with messages = get_flashed_messages(with_categories=True) +
|
||||
config.get('GLOBAL_FLASHED_MESSAGES', []) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
{% set category = 'info' if category == 'message' else category %}
|
||||
<div class="alert alert-{{ category }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_title %}
|
||||
{% if page_title %}
|
||||
<h1 class="page-header">{{ page_title }}
|
||||
{% block title_actions %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="container text-right">
|
||||
{{ config['PROJECT_NAME'] }}
|
||||
</footer>
|
||||
{% block tail_js %}
|
||||
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
|
||||
<script src="{{ static('js/bootstrap.min.js') }}"></script>
|
||||
<script src="{{ static('js/scripts.js') }}"></script>
|
||||
|
||||
{% if config.get('SOCIAL_FACEBOOK') %}
|
||||
<!-- Facebook SDK START -->
|
||||
<script>
|
||||
window.fbAsyncInit = function() {
|
||||
FB.init({
|
||||
appId : {{ config['SOCIAL_FACEBOOK']['consumer_key'] }},
|
||||
cookie : true,
|
||||
xfbml : true,
|
||||
version : 'v2.3',
|
||||
});
|
||||
};
|
||||
|
||||
// Load the SDK asynchronously
|
||||
(function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) return;
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "//connect.facebook.net/en_US/sdk.js";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));
|
||||
</script>
|
||||
<!-- Facebook SDK END -->
|
||||
{% endif %}
|
||||
{% endblock -%}
|
||||
|
||||
{%- block tail %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
17
templates/flatpages/show.html
Normal file
17
templates/flatpages/show.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% set page_title = page.title %}
|
||||
{% block content %}
|
||||
{% if page.content %}
|
||||
{{ page.content|markdown }}
|
||||
{% endif %}
|
||||
|
||||
{% if page.subpages %}
|
||||
<hr>
|
||||
<ul>
|
||||
{% for sub in page.subpages %}
|
||||
<li><a href="{{ url_for('flatpages.show', slug=sub.slug) }}">{{ sub.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
4
templates/index.html
Normal file
4
templates/index.html
Normal file
@ -0,0 +1,4 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content -%}
|
||||
{%- endblock %}
|
13
templates/security/change_password.html
Normal file
13
templates/security/change_password.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
{% set page_title = 'Change password' %}
|
||||
{% from "_formhelpers.html" import render_field, render_submit %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{{ url_for_security('change_password') }}" method="POST" class="form-horizontal col-md-6 col-md-offset-3">
|
||||
{{ change_password_form.hidden_tag() }}
|
||||
{{ render_field(change_password_form.password) }}
|
||||
{{ render_field(change_password_form.new_password) }}
|
||||
{{ render_field(change_password_form.new_password_confirm) }}
|
||||
{{ render_submit() }}
|
||||
</form>
|
||||
{% endblock %}
|
15
templates/security/confirm_social.html
Normal file
15
templates/security/confirm_social.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% set page_title = 'Registration' %}
|
||||
{% from "_formhelpers.html" import render_field, render_submit %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{{ url_for('friends.confirm_social') }}" method="POST" class="form-horizontal col-md-6 col-md-offset-3">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="col-xs-9 col-xs-offset-3">
|
||||
<span class="help-block">You will be registered as <strong>{{ session['failed_login_connection']['full_name'] }}</strong>.</span>
|
||||
<span class="help-block">Please confirm your address.</span>
|
||||
</div>
|
||||
{{ render_field(form.email) }}
|
||||
{{ render_submit() }}
|
||||
</form>
|
||||
{% endblock %}
|
11
templates/security/forgot_password.html
Normal file
11
templates/security/forgot_password.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% set page_title = 'Password reset' %}
|
||||
{% from "_formhelpers.html" import render_field, render_submit %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{{ url_for_security('forgot_password') }}" method="POST" class="form-horizontal col-md-6 col-md-offset-3">
|
||||
{{ forgot_password_form.hidden_tag() }}
|
||||
{{ render_field(forgot_password_form.email) }}
|
||||
{{ render_submit() }}
|
||||
</form>
|
||||
{% endblock %}
|
12
templates/security/invited.html
Normal file
12
templates/security/invited.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
{% set page_title = 'Invitation' %}
|
||||
{% from "_formhelpers.html" import render_field, render_submit %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{{ url_for('friends.invited', token=invite_token) }}" method="POST" class="form-horizontal col-md-6 col-md-offset-3">
|
||||
{{ invite_form.hidden_tag() }}
|
||||
{{ render_field(invite_form.password) }}
|
||||
{{ render_field(invite_form.password_confirm) }}
|
||||
{{ render_submit() }}
|
||||
</form>
|
||||
{% endblock %}
|
23
templates/security/login_user.html
Normal file
23
templates/security/login_user.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% set page_title = 'Login' %}
|
||||
{% from "_formhelpers.html" import render_field, render_submit %}
|
||||
{% from "_socialhelpers.html" import social_login %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{{ url_for_security('login') }}" method="POST" class="form-horizontal col-md-6 col-md-offset-3">
|
||||
{{ login_user_form.hidden_tag() }}
|
||||
{{ render_field(login_user_form.email) }}
|
||||
{{ render_field(login_user_form.password) }}
|
||||
{{ render_field(login_user_form.remember) }}
|
||||
{{ render_field(login_user_form.next) }}
|
||||
<div class="form-group">
|
||||
<div class="col-xs-9 col-xs-offset-3">
|
||||
{{ render_submit(layout=False) }}
|
||||
<a href="{{ url_for_security('forgot_password') }}" class="btn btn-default">Forgot password?</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-md-3">
|
||||
{{ social_login('facebook', 'Facebook') }}
|
||||
</div>
|
||||
{% endblock %}
|
20
templates/security/register_user.html
Normal file
20
templates/security/register_user.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% set page_title = 'Registration' %}
|
||||
{% from "_formhelpers.html" import render_field, render_submit %}
|
||||
{% from "_socialhelpers.html" import social_register %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="{{ url_for_security('register') }}" method="POST" class="form-horizontal">
|
||||
{{ register_user_form.hidden_tag() }}
|
||||
{{ render_field(register_user_form.email) }}
|
||||
{{ render_field(register_user_form.password) }}
|
||||
{{ render_submit() }}
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ social_register('facebook', 'Facebook') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
12
templates/security/reset_password.html
Normal file
12
templates/security/reset_password.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
{% set page_title = 'Password reset' %}
|
||||
{% from "_formhelpers.html" import render_field, render_submit %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" class="form-horizontal col-md-6 col-md-offset-3">
|
||||
{{ reset_password_form.hidden_tag() }}
|
||||
{{ render_field(reset_password_form.password) }}
|
||||
{{ render_field(reset_password_form.password_confirm) }}
|
||||
{{ render_submit() }}
|
||||
</form>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user