├── .gitignore ├── .makesite └── flask │ └── service │ └── flask_install_update.sh.tmpl ├── .travis.yml ├── Changelog ├── LICENSE ├── Makefile ├── README.md ├── base ├── __init__.py ├── app.py ├── auth │ ├── __init__.py │ ├── admin.py │ ├── forms.py │ ├── manage.py │ ├── models.py │ ├── oauth │ │ ├── __init__.py │ │ ├── base.py │ │ ├── facebook.py │ │ ├── github.py │ │ └── twitter.py │ ├── templates │ │ └── auth │ │ │ ├── profile.html │ │ │ └── register.html │ ├── tests.py │ ├── utils.py │ └── views.py ├── config │ ├── __init__.py │ ├── core.py │ ├── develop.py │ ├── production.py │ └── test.py ├── core │ ├── __init__.py │ ├── admin.py │ ├── ext.py │ ├── models.py │ ├── static │ │ ├── foundation.ico │ │ └── foundation.png │ ├── templates │ │ └── core │ │ │ ├── 404.html │ │ │ ├── 500.html │ │ │ ├── base.html │ │ │ ├── blocks │ │ │ ├── analytics.html │ │ │ ├── messages.html │ │ │ ├── metrika.html │ │ │ └── nav.html │ │ │ └── index.html │ ├── tests.py │ └── views.py ├── ext.py ├── loader.py ├── pages │ ├── __init__.py │ ├── admin.py │ ├── config.py │ ├── models.py │ ├── static │ │ ├── bootstrap-wysihtml5-0.0.2.min.js │ │ ├── bootstrap-wysihtml5.css │ │ ├── wysihtml5-0.4.0pre.min.js │ │ └── wysiwyg-color.css │ ├── templates │ │ └── pages │ │ │ ├── admin │ │ │ ├── create.html │ │ │ └── edit.html │ │ │ └── page.html │ ├── tests.py │ └── views.py └── translations │ ├── babel.ini │ ├── babel.pot │ └── ru │ └── LC_MESSAGES │ ├── messages.mo │ └── messages.po ├── makesite.ini ├── manage.py ├── migrate ├── README ├── __init__.py ├── develop.ini ├── env.py ├── production.ini ├── script.py.mako └── versions │ ├── 00000001_init_auth_models.py │ ├── 00000002_fill_admin_recv.py │ ├── 00000003_added_key_to_auth.py │ └── 00000004_init_pages.py ├── preview.png ├── requirements.txt └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | .DS_Store 3 | .db 4 | .env 5 | .ropeproject 6 | .tags 7 | .tests 8 | .vimrc 9 | _ 10 | base/config/local.py 11 | -------------------------------------------------------------------------------- /.makesite/flask/service/flask_install_update.sh.tmpl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PYTHON={{ virtualenvdir }}/bin/python 4 | SOURCEDIR={{ source_dir }} 5 | USER={{ site_user }} 6 | GROUP={{ site_group }} 7 | DEPLOYDIR={{ deploy_dir }} 8 | CONFIG=base.config.{{ mode }} 9 | 10 | 11 | cd $SOURCEDIR 12 | sudo $PYTHON $SOURCEDIR/manage.py collect -c $CONFIG 13 | sudo $PYTHON $SOURCEDIR/manage.py alembic upgrade head -c $CONFIG 14 | sudo chown $USER:$GROUP -R $DEPLOYDIR 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | 7 | branches: 8 | only: 9 | - master 10 | - develop 11 | 12 | install: 13 | - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib; fi" 14 | - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi" 15 | - pip install -IM pylama 16 | - pip install -IM -r requirements.txt 17 | 18 | script: 19 | - make db 20 | - make audit 21 | - make t 22 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | 2013-03-31 klen 2 | 3 | * Update requirements 4 | * Added 'pages' application 5 | * Version 0.6.4 6 | 7 | 2012-12-10 klen 8 | 9 | * Planned requirements update 10 | 11 | 2012-10-15 klen 12 | 13 | * Flask-Cache 0.8.0 14 | 15 | 2012-10-02 klen 16 | 17 | * Alembic 0.4.0 18 | 19 | 2012-10-01 klen 20 | 21 | * Flask-Cache 0.8.0 22 | 23 | 2012-09-29 klen 24 | 25 | * Flask-Mail 0.7.3 26 | * Add Foundation logo and favicon 27 | * Refactor OAuth support (Added facebook and Github as examples) 28 | * Respect environmet variable "MODE" 29 | 30 | 2012-09-25 klen 31 | 32 | * Flask-Script 0.5.0 33 | 34 | 2012-09-17 klen 35 | 36 | * Add dealer (project revision control) 37 | 38 | 2012-09-10 klen 39 | 40 | * Flask-Principal>=0.3.3 41 | * Flask-Mail>=0.7.1 42 | * Flask-Rauth 43 | 44 | 2012-09-03 klen 45 | 46 | * Added ability to run custom TestCase 47 | ./manage.py test -t base.core.tests.CoreTest -c base.config.develop 48 | 49 | 2012-09-01 klen 50 | 51 | * Flask-Script>=0.4.0 52 | * Flask-Mail>=0.7.0 53 | 54 | 2012-08-29 klen 55 | 56 | * Flask-Cache>=0.7.1 57 | 58 | 2012-08-28 klen 59 | 60 | * Update Flask-Admin to version 1.0.2 61 | 62 | 2012-08-26 klen 63 | 64 | * base.core.tests.FlaskTestCase haved assertNumQueries method 65 | 66 | 2012-08-24 klen 67 | 68 | * Add 'before_new', 'before_delete' model methods support 69 | * Add FlaskMail log handler for production 70 | * Load migrations by app config 71 | 72 | 2012-08-19 klen 73 | 74 | * Update alembic, flask-cache, flask-principal 75 | * Include Flask-DebugToolbar 76 | 77 | 2012-08-18 klen 78 | 79 | * Minor updates 80 | 81 | 2012-08-15 klen 82 | 83 | * Version 0.3.1 84 | * Integrate travic-ci 85 | 86 | 2012-08-10 klen 87 | 88 | * Version 0.3.0 89 | * Migrate to alembic 90 | 91 | 2012-08-05 klen 92 | 93 | * Added Flask-Collect 94 | * Release 0.2.18 95 | 96 | 2012-08-04 klen 97 | 98 | * Version 0.2.16 99 | * Add loader for control apps (admin, script) 100 | 101 | 2012-07-29 klen 102 | 103 | * Version 0.2.10 104 | * Added dirty release of oauth (twitter) 105 | 106 | 2012-07-17 klen 107 | 108 | * Version 0.2.9 109 | * Added Flask-Mail 110 | 111 | 2012-07-15 klen 112 | 113 | * Version 0.2.8 114 | * Added wsgi file 115 | 116 | 2012-07-14 klen 117 | 118 | * Version 0.2.4 119 | * Refactor blueprints 120 | 121 | 2012-07-13 klen 122 | 123 | * Version 0.2.3 124 | * Flask-testing 125 | * Flask-cache 126 | 127 | 2012-07-11 klen 128 | 129 | * Version 0.1.1 130 | * Initial release 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 by Kirill Klenov. See AUTHORS 2 | for more details. 3 | 4 | Some rights reserved. 5 | 6 | Redistribution and use in source and binary forms of the software as well 7 | as documentation, with or without modification, are permitted provided 8 | that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | 18 | * The names of the contributors may not be used to endorse or 19 | promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 24 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 26 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 29 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 33 | DAMAGE. 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ENVBIN=$(CURDIR)/.env/bin 2 | PIP=$(ENVBIN)/pip 3 | PYTHON=$(ENVBIN)/python 4 | PYBABEL=$(ENVBIN)/pybabel 5 | BABELDIR=$(CURDIR)/base/translations 6 | MODULE=base 7 | CONFIG=$(MODULE).config.develop 8 | 9 | all: .env db 10 | 11 | # target: help - Display callable targets 12 | help: 13 | @egrep "^# target:" [Mm]akefile 14 | 15 | .env: requirements.txt $(ENVBIN) 16 | $(PIP) install -M -r requirements.txt 17 | touch .env 18 | 19 | $(ENVBIN): 20 | virtualenv --no-site-packages .env 21 | 22 | # target: shell - Open application shell 23 | .PHONY: shell 24 | shell: .env/ manage.py 25 | $(PYTHON) manage.py shell -c $(CONFIG) 26 | 27 | 28 | # target: run - Run application server 29 | .PHONY: run 30 | run: .env/ manage.py 31 | $(PYTHON) manage.py runserver -c $(CONFIG) 32 | 33 | 34 | # target: db - Init and migrate application db 35 | .PHONY: db 36 | db: .env/ manage.py 37 | $(PYTHON) manage.py alembic upgrade head -c $(CONFIG) 38 | 39 | 40 | # target: audit - Audit source code 41 | .PHONY: audit 42 | audit: 43 | pylama $(MODULE) -i E501 44 | 45 | 46 | # target: test - Run tests 47 | .PHONY: t 48 | t: .env manage.py clean 49 | $(PYTHON) manage.py test -c $(MODULE).config.test 50 | 51 | 52 | # target: clean - Clean repo 53 | .PHONY: clean 54 | clean: 55 | rm -f *.py[co] *.orig 56 | rm -f */*.py[co] */*.orig 57 | 58 | 59 | # target: babel - Recompile language files 60 | .PHONY: babel 61 | babel: $(BABELDIR)/ru 62 | $(PYBABEL) extract -F $(BABELDIR)/babel.ini -k _gettext -k _ngettext -k lazy_gettext -o $(BABELDIR)/babel.pot --project Flask-base $(CURDIR) 63 | $(PYBABEL) update -i $(BABELDIR)/babel.pot -d $(BABELDIR) 64 | $(PYBABEL) compile -d $(BABELDIR) 65 | 66 | $(BABELDIR)/ru: 67 | $(PYBABEL) init -i $(BABELDIR)/babel.pot -d $(BABELDIR) -l ru 68 | 69 | .PHONY: chown 70 | chown: 71 | sudo chown $(USER):$(USER) -R $(CURDIR) 72 | 73 | .PHONY: upload 74 | upload: 75 | git push 76 | makesite update foundation -p /var/www -H ubuntu@foundation.node42.org 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flask-Foundation 2 | ================ 3 | 4 | [![Build Status](https://secure.travis-ci.org/klen/Flask-Foundation.png?branch=master)](http://travis-ci.org/klen/Flask-Foundation) 5 | 6 | > Quick start with Flask. 7 | > This is a template for a simple creating website with Flask. 8 | > Use it as a starting point to create something more unique. 9 | 10 | It includes: 11 | 12 | * [Flask-Admin](https://github.com/mrjoes/flask-admin/) (administration); 13 | * [Flask-Script](http://github.com/rduplain/flask-script) (administration); 14 | * [Flask-Cache](http://packages.python.org/Flask-Cache/) (cache); 15 | * [Flask-Debugtoolbar](https://github.com/mgood/flask-debugtoolbar) (debug); 16 | * [Flask-Babel](http://github.com/mitsuhiko/flask-babel) (i18n); 17 | * [Flask-WTF](http://github.com/rduplan/flask-wtf) (forms); 18 | * [Flask-Bootstrap](http://github.com/mbr/flask-bootstrap) (markup); 19 | * [Flask-Mail] (https://github.com/mattupstate/flask-mail) (mail); 20 | * [Flask-Squll](http://github.com/thrisp/flask-squll) (ORM); 21 | * [Alembic](http://pypi.python.org/pypi/alembic/0.3.5) (ORM); 22 | * [Flask-Testing](http://packages.python.org/Flask-Testing/) (testing); 23 | * Included user authentication system; 24 | * Usefull makefile shortcuts (make run, make db and etc); 25 | 26 | Preview 27 | ------- 28 | 29 | Working example: http://foundation.node42.org/ 30 | 31 | ![preview](https://raw.github.com/klen/Flask-Foundation/develop/preview.png) 32 | 33 | 34 | Requirements: 35 | ------------ 36 | 37 | * Python 2.6 (importlib, unittest2) 38 | * Python 2.7 39 | 40 | 41 | Instalation: 42 | ------------ 43 | 44 | $ git clone https://github.com/klen/Flask-Foundation.git 45 | $ cd Flask-Foundation 46 | $ make db 47 | 48 | 49 | Usage: 50 | ------ 51 | 52 | Run development server: 53 | 54 | $ make run 55 | 56 | See also: 57 | 58 | $ ./manage.py 59 | -------------------------------------------------------------------------------- /base/__init__.py: -------------------------------------------------------------------------------- 1 | " Project dir. " 2 | -------------------------------------------------------------------------------- /base/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from os import environ, path as op 3 | 4 | from .config import production 5 | 6 | 7 | def create_app(config=None, **settings): 8 | app = Flask(__name__) 9 | app.config.from_envvar("APP_SETTINGS", silent=True) 10 | app.config.from_object(config or production) 11 | 12 | # Settings from mode 13 | mode = environ.get('MODE') 14 | if mode: 15 | app.config.from_object('base.config.%s' % mode) 16 | 17 | # Local settings 18 | app.config.from_pyfile(op.join(op.dirname(production.__file__), 'local.py'), silent=True) 19 | 20 | # Overide settings 21 | app.config.update(settings) 22 | 23 | with app.test_request_context(): 24 | 25 | from .ext import config_extensions 26 | config_extensions(app) 27 | 28 | from .loader import loader 29 | loader.register(app) 30 | 31 | return app 32 | -------------------------------------------------------------------------------- /base/auth/__init__.py: -------------------------------------------------------------------------------- 1 | " base.auth " 2 | 3 | 4 | def loader_meta(app): 5 | " Configure application. " 6 | 7 | from .views import auth 8 | app.register_blueprint(auth) 9 | 10 | from .oauth import PROVIDERS 11 | map(lambda p: p.setup(app), PROVIDERS) 12 | 13 | loader_meta.priority = 1.0 14 | -------------------------------------------------------------------------------- /base/auth/admin.py: -------------------------------------------------------------------------------- 1 | from flask_admin.contrib.sqlamodel import ModelView 2 | from flask_wtf import PasswordField, required 3 | 4 | from ..core.ext import admin 5 | from .models import User, Role, Key 6 | 7 | 8 | class UserView(ModelView): 9 | column_filters = 'username', 'email' 10 | column_list = 'username', 'email', 'active', 'created_at', 'updated_at' 11 | form_excluded_columns = 'oauth_token', 'oauth_secret', '_pw_hash' 12 | 13 | def scaffold_form(self): 14 | form = super(UserView, self).scaffold_form() 15 | form.pw_hash = PasswordField(validators=[required()]) 16 | form.roles.kwargs['validators'] = [] 17 | return form 18 | 19 | admin.add_model(User, UserView) 20 | admin.add_model(Role) 21 | admin.add_model(Key) 22 | -------------------------------------------------------------------------------- /base/auth/forms.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flaskext.babel import lazy_gettext as _ 3 | from flask_wtf import Form, TextField, PasswordField, Required, Email, EqualTo, BooleanField, HiddenField, SubmitField 4 | 5 | 6 | class EmailFormMixin(): 7 | email = TextField(_('Email address'), 8 | validators=[Required(message=_("Email not provided")), 9 | Email(message=_("Invalid email address"))]) 10 | 11 | 12 | class PasswordFormMixin(): 13 | password = PasswordField(_("Password"), 14 | validators=[Required(message=_("Password not provided"))]) 15 | 16 | 17 | class PasswordConfirmFormMixin(): 18 | password_confirm = PasswordField(_("Retype Password"), 19 | validators=[EqualTo('password', message=_("Passwords do not match"))]) 20 | 21 | 22 | class LoginForm(Form, EmailFormMixin, PasswordFormMixin): 23 | " The default login form. " 24 | 25 | remember = BooleanField(_("Remember Me"), default=True) 26 | next = HiddenField() 27 | submit = SubmitField(_("Login")) 28 | 29 | def __init__(self, *args, **kwargs): 30 | super(LoginForm, self).__init__(*args, **kwargs) 31 | 32 | if request.method == 'GET': 33 | self.next.data = request.args.get('next', None) 34 | 35 | 36 | class ForgotPasswordForm(Form, EmailFormMixin): 37 | " The default forgot password form. " 38 | 39 | submit = SubmitField(_("Recover Password")) 40 | 41 | def to_dict(self): 42 | return dict(email=self.email.data) 43 | 44 | 45 | class RegisterForm(Form, EmailFormMixin, PasswordFormMixin, PasswordConfirmFormMixin): 46 | " The default register form. " 47 | 48 | username = TextField(_('UserName'), [Required(message=_('Required'))]) 49 | # submit = SubmitField(_("Register")) 50 | 51 | def to_dict(self): 52 | return dict(email=self.email.data, password=self.password.data) 53 | 54 | 55 | class ResetPasswordForm(Form, 56 | EmailFormMixin, 57 | PasswordFormMixin, 58 | PasswordConfirmFormMixin): 59 | " The default reset password form. " 60 | 61 | token = HiddenField(validators=[Required()]) 62 | 63 | submit = SubmitField(_("Reset Password")) 64 | 65 | def __init__(self, *args, **kwargs): 66 | super(ResetPasswordForm, self).__init__(*args, **kwargs) 67 | 68 | if request.method == 'GET': 69 | self.token.data = request.args.get('token', None) 70 | self.email.data = request.args.get('email', None) 71 | 72 | def to_dict(self): 73 | return dict(token=self.token.data, 74 | email=self.email.data, 75 | password=self.password.data) 76 | 77 | 78 | # pymode:lint_ignore=F0401 79 | -------------------------------------------------------------------------------- /base/auth/manage.py: -------------------------------------------------------------------------------- 1 | """Flask-Script support. 2 | """ 3 | from flask_script import prompt_pass, Manager 4 | 5 | 6 | auth_manager = Manager(usage='Authentication operations.') 7 | 8 | 9 | def loader_meta(manager): 10 | " Register submanager with loader as manager init. " 11 | 12 | manager.add_command('auth', auth_manager) 13 | 14 | 15 | @auth_manager.option('username') 16 | @auth_manager.option('email') 17 | @auth_manager.option('-a', '--active', dest='active', action='store_true') 18 | @auth_manager.option('-p', '--password', dest='password', default='') 19 | def create_user(username=None, email=None, active=False, password=''): 20 | " Create a user. " 21 | 22 | from .models import User 23 | from ..ext import db 24 | 25 | password = password or prompt_pass("Set password") 26 | user = User(username=username, 27 | email=email, 28 | pw_hash=password, 29 | active=active) 30 | 31 | db.session.add(user) 32 | db.session.commit() 33 | 34 | print 'User created successfully.' 35 | 36 | 37 | @auth_manager.option('name') 38 | def create_role(name): 39 | " Create a role. " 40 | 41 | from .models import Role 42 | from ..ext import db 43 | 44 | role = Role(name=name) 45 | 46 | db.session.add(role) 47 | db.session.commit() 48 | 49 | print 'Role "%s" created successfully.' % name 50 | 51 | 52 | @auth_manager.option('username') 53 | @auth_manager.option('role') 54 | def add_role(username, role): 55 | " Add a role to a user. " 56 | 57 | from .models import User, Role 58 | from ..ext import db 59 | 60 | u = User.query.filter_by(username=username).first() 61 | r = Role.query.filter_by(name=role).first() 62 | if u and r: 63 | u.roles.append(r) 64 | db.session.add(u) 65 | db.session.commit() 66 | print "Role '%s' added to user '%s' successfully" % ( 67 | role, username) 68 | 69 | 70 | @auth_manager.option('username') 71 | @auth_manager.option('role') 72 | def remove_role(username, role): 73 | " Remove a role from user. " 74 | 75 | from .models import User, Role 76 | from ..ext import db 77 | 78 | u = User.query.filter_by(username=username).first() 79 | r = Role.query.filter_by(name=role).first() 80 | if r in u.roles: 81 | u.roles.remove(r) 82 | db.session.add(u) 83 | db.session.commit() 84 | print "Role '%s' removed from user '%s' successfully" % ( 85 | role, username) 86 | 87 | # pymode:lint_ignore=F0401 88 | -------------------------------------------------------------------------------- /base/auth/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from flask_login import UserMixin 4 | from flask_principal import RoleNeed, Permission 5 | from flask_squll import _BoundDeclarativeMeta 6 | from random import choice 7 | from sqlalchemy.ext.declarative import declared_attr 8 | from sqlalchemy.ext.hybrid import hybrid_property 9 | from werkzeug import check_password_hash, generate_password_hash 10 | 11 | from ..core.models import BaseMixin 12 | from ..ext import db 13 | 14 | 15 | PSYMBOLS = 'abcdefghijklmnopqrstuvwxyz123456789' 16 | 17 | userroles = db.Table( 18 | 'auth_userroles', 19 | db.Column('user_id', db.Integer, db.ForeignKey('auth_user.id')), 20 | db.Column('role_id', db.Integer, db.ForeignKey('auth_role.id')) 21 | ) 22 | 23 | 24 | class Role(db.Model, BaseMixin): 25 | " User roles. " 26 | 27 | __tablename__ = 'auth_role' 28 | 29 | name = db.Column(db.String(19), nullable=False, unique=True) 30 | 31 | def __unicode__(self): 32 | return self.name 33 | 34 | def __repr__(self): 35 | return '' % (self.name) 36 | 37 | 38 | class UserMixinMeta(_BoundDeclarativeMeta): 39 | " Dynamic mixin from app configuration. " 40 | 41 | def __new__(mcs, name, bases, params): 42 | from flask import current_app 43 | from importlib import import_module 44 | 45 | if current_app and current_app.config.get('AUTH_USER_MIXINS'): 46 | for mixin in current_app.config.get('AUTH_USER_MIXINS'): 47 | mod, cls = mixin.rsplit('.', 1) 48 | mod = import_module(mod) 49 | cls = getattr(mod, cls) 50 | bases = bases + (cls, ) 51 | 52 | return super(UserMixinMeta, mcs).__new__(mcs, name, bases, params) 53 | 54 | 55 | class User(db.Model, UserMixin, BaseMixin): 56 | """ Main User database model. 57 | 58 | Extend that uses `AUTH_USER_MIXINS` option. 59 | """ 60 | 61 | __tablename__ = 'auth_user' 62 | __metaclass__ = UserMixinMeta 63 | 64 | username = db.Column(db.String(50), unique=True, nullable=False) 65 | email = db.Column(db.String(120)) 66 | active = db.Column(db.Boolean, default=True) 67 | _pw_hash = db.Column(db.String(199), nullable=False) 68 | 69 | @declared_attr 70 | def roles(self): 71 | assert self 72 | return db.relationship("Role", secondary=userroles, backref="users") 73 | 74 | @hybrid_property 75 | def pw_hash(self): 76 | """Simple getter function for the user's password.""" 77 | return self._pw_hash 78 | 79 | @pw_hash.setter 80 | def pw_hash(self, raw_password): 81 | """ Password setter, that handles the hashing 82 | in the database. 83 | """ 84 | self._pw_hash = generate_password_hash(raw_password) 85 | 86 | @staticmethod 87 | def permission(role): 88 | perm = Permission(RoleNeed(role)) 89 | return perm.can() 90 | 91 | def generate_password(self): 92 | self.pw_hash = ''.join(choice(PSYMBOLS) for c in xrange(8)) 93 | 94 | def check_password(self, password): 95 | return check_password_hash(self.pw_hash, password) 96 | 97 | def is_active(self): 98 | return self.active 99 | 100 | def __unicode__(self): 101 | return self.username 102 | 103 | def __repr__(self): 104 | return '' % (self.username) 105 | 106 | 107 | class Key(db.Model, BaseMixin): 108 | """ OAuth keys store. 109 | """ 110 | __tablename__ = 'auth_key' 111 | __table_args__ = db.UniqueConstraint('service_alias', 'service_id'), 112 | 113 | service_alias = db.Column(db.String) 114 | service_id = db.Column(db.String) 115 | 116 | access_token = db.Column(db.String) 117 | secret = db.Column(db.String) 118 | expires = db.Column(db.DateTime) 119 | refresh_token = db.Column(db.String) 120 | 121 | user_id = db.Column(db.Integer, db.ForeignKey('auth_user.id')) 122 | user = db.relationship('User', backref=db.backref('keys', lazy='dynamic')) 123 | 124 | def __unicode__(self): 125 | return self.service_alias 126 | 127 | def __repr__(self): 128 | return '' % (self.service_alias, self.service_id) 129 | 130 | def is_expired(self): 131 | return self.expires and self.expires < datetime.now() 132 | 133 | # pymode:lint_ignore=E0611,E0202 134 | -------------------------------------------------------------------------------- /base/auth/oauth/__init__.py: -------------------------------------------------------------------------------- 1 | from .twitter import TwitterOAuth 2 | from .github import GithubOAuth 3 | from .facebook import FacebookOAuth 4 | 5 | 6 | PROVIDERS = [TwitterOAuth, FacebookOAuth, GithubOAuth] 7 | -------------------------------------------------------------------------------- /base/auth/oauth/base.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from flask import url_for, request, flash, redirect 4 | from flask_login import current_user 5 | from flask_rauth import RauthOAuth2, RauthOAuth1 6 | from flaskext.babel import lazy_gettext as _ 7 | 8 | from ..models import db, Key, User 9 | from ..views import auth 10 | 11 | 12 | class AbstractRAuth(object): 13 | 14 | client = None 15 | 16 | @property 17 | def name(self): 18 | raise NotImplementedError 19 | 20 | @property 21 | def options(self): 22 | raise NotImplementedError 23 | 24 | @classmethod 25 | def get_credentials(cls, response, oauth_token): 26 | raise NotImplementedError 27 | 28 | @classmethod 29 | def authorize(cls, response, oauth_token): 30 | next_url = request.args.get('next') or url_for('urls.index') 31 | if response is None or 'denied' in request.args: 32 | flash(_(u'You denied the request to sign in.')) 33 | return redirect(next_url) 34 | 35 | try: 36 | credentials = cls.get_credentials(response, oauth_token) 37 | except Exception: 38 | return redirect(next_url) 39 | 40 | if credentials.get('expires'): 41 | expires_in = timedelta(seconds=int(credentials['expires'])) 42 | credentials['expires'] = datetime.now() + expires_in 43 | 44 | key = Key.query.filter( 45 | Key.service_alias == cls.name, 46 | Key.service_id == credentials['service_id'], 47 | ).first() 48 | 49 | user = current_user 50 | 51 | if key: 52 | 53 | if user.is_authenticated(): 54 | key.user = user 55 | 56 | else: 57 | user = key.user 58 | 59 | else: 60 | if not user.is_authenticated(): 61 | user = User(username=credentials['username']) 62 | user.generate_password() 63 | db.session.add(user) 64 | 65 | key = Key( 66 | service_alias=cls.name, 67 | user=user, 68 | service_id=credentials['service_id'], 69 | access_token=credentials['access_token'], 70 | secret=credentials.get('secret'), 71 | expires=credentials.get('expires'), 72 | ) 73 | db.session.add(key) 74 | 75 | db.session.commit() 76 | auth.login(user) 77 | flash(_('Welcome %(user)s', user=user.username)) 78 | return redirect(next_url) 79 | 80 | @classmethod 81 | def setup(cls, app): 82 | options = app.config.get('OAUTH_%s' % cls.name.upper()) 83 | if not options: 84 | return False 85 | 86 | params = dict() 87 | if 'params' in options: 88 | params = options.pop('params') 89 | 90 | app.logger.info('Init OAuth %s' % cls.name) 91 | cls.options.update(name=cls.name, **options) 92 | client_cls = RauthOAuth2 93 | if cls.options.get('request_token_url'): 94 | client_cls = RauthOAuth1 95 | 96 | cls.client = client_cls(**cls.options) 97 | 98 | login_name = 'oauth_%s_login' % cls.name 99 | authorize_name = 'oauth_%s_authorize' % cls.name 100 | 101 | @app.route('/%s' % login_name, endpoint=login_name) 102 | def login(): 103 | return cls.client.authorize( 104 | callback=( 105 | url_for(authorize_name, _external=True, 106 | next=request.args.get('next') or request.referrer) 107 | ), **params) 108 | 109 | cls.client.tokengetter_f = cls.get_token 110 | 111 | app.add_url_rule('/%s' % authorize_name, 112 | authorize_name, 113 | cls.client.authorized_handler(cls.authorize)) 114 | 115 | @classmethod 116 | def get_token(cls): 117 | if current_user.is_authenticated(): 118 | for key in current_user.keys: 119 | if key.service_alias == cls.name: 120 | return key.access_token 121 | -------------------------------------------------------------------------------- /base/auth/oauth/facebook.py: -------------------------------------------------------------------------------- 1 | from .base import AbstractRAuth 2 | 3 | 4 | class FacebookOAuth(AbstractRAuth): 5 | 6 | name = 'facebook' 7 | options = dict( 8 | base_url='https://graph.facebook.com', 9 | authorize_url='https://www.facebook.com/dialog/oauth', 10 | access_token_url='https://graph.facebook.com/oauth/access_token', 11 | ) 12 | 13 | @classmethod 14 | def get_credentials(cls, response, oauth_token): 15 | me = cls.client.get('/me', access_token=oauth_token) 16 | return dict( 17 | username=me.content['username'], 18 | access_token=oauth_token, 19 | expires=response.content['expires'], 20 | service_id=me.content['id'], 21 | ) 22 | -------------------------------------------------------------------------------- /base/auth/oauth/github.py: -------------------------------------------------------------------------------- 1 | from .base import AbstractRAuth 2 | 3 | 4 | class GithubOAuth(AbstractRAuth): 5 | 6 | name = 'github' 7 | options = dict( 8 | base_url='https://api.github.com/', 9 | authorize_url='https://github.com/login/oauth/authorize', 10 | access_token_url='https://github.com/login/oauth/access_token', 11 | ) 12 | 13 | @classmethod 14 | def get_credentials(cls, response, oauth_token): 15 | me = cls.client.get('/user', access_token=oauth_token) 16 | return dict( 17 | username=me.content['login'], 18 | access_token=oauth_token, 19 | service_id=me.content['id'], 20 | ) 21 | -------------------------------------------------------------------------------- /base/auth/oauth/twitter.py: -------------------------------------------------------------------------------- 1 | from .base import AbstractRAuth 2 | 3 | 4 | class TwitterOAuth(AbstractRAuth): 5 | 6 | name = 'twitter' 7 | options = dict( 8 | base_url='http://api.twitter.com/1/', 9 | authorize_url='http://api.twitter.com/oauth/authorize', 10 | access_token_url='http://api.twitter.com/oauth/access_token', 11 | request_token_url='http://api.twitter.com/oauth/request_token', 12 | ) 13 | 14 | @classmethod 15 | def get_credentials(cls, response, oauth_token): 16 | return dict( 17 | username=response.content['screen_name'], 18 | service_id=response.content['user_id'], 19 | access_token=response.content['oauth_token'], 20 | secret=response.content['oauth_token_secret'], 21 | ) 22 | -------------------------------------------------------------------------------- /base/auth/templates/auth/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "core/base.html" %} 2 | 3 | {% block page_content %} 4 |
5 |
6 |
7 |
8 |

{{ current_user.username }}

9 |

First Last Name

10 | 11 |

twitter twitter

12 |

facebook facebook

13 |

twitter github

14 | 15 | 8 messages 15 followers 16 |
17 |
18 |
19 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /base/auth/templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends "core/base.html" %} 2 | 3 | {% block page_content %} 4 |
5 |
6 | 7 | {% import "bootstrap_wtf.html" as wtf %} 8 | 9 |
10 | {{ _('New to Foundation? Sign up!') }} 11 | {{ form.hidden_tag() }} 12 | {{ wtf.horizontal_field(form.username) }} 13 | {{ wtf.horizontal_field(form.email) }} 14 | {{ wtf.horizontal_field(form.password) }} 15 | {{ wtf.horizontal_field(form.password_confirm) }} 16 |
17 | 18 |
19 |
20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /base/auth/tests.py: -------------------------------------------------------------------------------- 1 | from flask import url_for 2 | 3 | from ..core.tests import FlaskTest 4 | from ..ext import db 5 | 6 | 7 | class AuthTest(FlaskTest): 8 | 9 | def test_model_mixin(self): 10 | from .models import User 11 | self.assertTrue(User.do_true()) 12 | 13 | def test_users(self): 14 | from .models import User 15 | 16 | response = self.client.post('/auth/login/', data=dict()) 17 | self.assertRedirects(response, '/') 18 | 19 | user = User(username='test', pw_hash='test', email='test@test.com') 20 | db.session.add(user) 21 | db.session.commit() 22 | self.assertTrue(user.updated_at) 23 | 24 | response = self.client.post('/auth/login/', data=dict( 25 | email='test@test.com', 26 | action_save=True, 27 | password='test')) 28 | redirect_url = url_for(self.app.config.get('AUTH_PROFILE_VIEW', 'auth.profile')) 29 | self.assertRedirects(response, redirect_url) 30 | 31 | response = self.client.get('/auth/logout/') 32 | self.assertRedirects(response, '/') 33 | 34 | response = self.client.post('/auth/register/', data=dict( 35 | username='test2', 36 | email='test2@test.com', 37 | action_save=True, 38 | password='test', 39 | password_confirm='test', 40 | )) 41 | redirect_url = url_for(self.app.config.get('AUTH_PROFILE_VIEW', 'auth.profile')) 42 | self.assertRedirects(response, redirect_url) 43 | 44 | user = User.query.filter(User.username == 'test2').first() 45 | self.assertEqual(user.email, 'test2@test.com') 46 | 47 | def test_manager(self): 48 | from .models import Role, User 49 | from .manage import create_role, create_user, add_role 50 | 51 | create_role('test') 52 | role = Role.query.filter(Role.name == 'test').first() 53 | self.assertEqual(role.name, 'test') 54 | 55 | create_user('test', 'test@test.com', active=True, password='12345') 56 | user = User.query.filter(User.username == 'test').first() 57 | 58 | add_role('test', 'test') 59 | self.assertTrue(role in user.roles) 60 | 61 | def test_oauth(self): 62 | from flask import url_for 63 | 64 | if self.app.config.get('OAUTH_TWITTER'): 65 | self.assertTrue(url_for('oauth_twitter_login')) 66 | 67 | if self.app.config.get('OAUTH_GITHUB'): 68 | self.assertTrue(url_for('oauth_github_login')) 69 | 70 | if self.app.config.get('OAUTH_FACEBOOK'): 71 | self.assertTrue(url_for('oauth_facebook_login')) 72 | 73 | 74 | class TestUserMixin(object): 75 | 76 | @staticmethod 77 | def do_true(): 78 | return True 79 | -------------------------------------------------------------------------------- /base/auth/utils.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask_login import LoginManager, login_required, logout_user, login_user, current_user 3 | from flask_principal import Principal, identity_changed, Identity, AnonymousIdentity, identity_loaded, UserNeed, RoleNeed 4 | 5 | from ..ext import db 6 | from .models import User 7 | 8 | 9 | class UserManager(Blueprint): 10 | 11 | def __init__(self, *args, **kwargs): 12 | self._login_manager = None 13 | self._principal = None 14 | self.app = None 15 | super(UserManager, self).__init__(*args, **kwargs) 16 | 17 | def register(self, app, *args, **kwargs): 18 | " Activate loginmanager and principal. " 19 | 20 | if not self._login_manager or self.app != app: 21 | self._login_manager = LoginManager() 22 | self._login_manager.user_callback = self.user_loader 23 | self._login_manager.init_app(app) 24 | self._login_manager.login_view = app.config.get('AUTH_LOGIN_VIEW', 'code.index') 25 | self._login_manager.login_message = u'You need to be signed in for this page.' 26 | 27 | self.app = app 28 | 29 | if not self._principal: 30 | self._principal = Principal(app) 31 | identity_loaded.connect(self.identity_loaded) 32 | 33 | super(UserManager, self).register(app, *args, **kwargs) 34 | 35 | @staticmethod 36 | def user_loader(pk): 37 | return User.query.options(db.joinedload(User.roles)).get(pk) 38 | 39 | @staticmethod 40 | def login_required(fn): 41 | return login_required(fn) 42 | 43 | def logout(self): 44 | identity_changed.send(self.app, identity=AnonymousIdentity()) 45 | return logout_user() 46 | 47 | def login(self, user): 48 | identity_changed.send(self.app, identity=Identity(user.id)) 49 | return login_user(user) 50 | 51 | @staticmethod 52 | def identity_loaded(sender, identity): 53 | identity.user = current_user 54 | 55 | # Add the UserNeed to the identity 56 | if current_user.is_authenticated(): 57 | identity.provides.add(UserNeed(current_user.id)) 58 | 59 | # Assuming the User model has a list of roles, update the 60 | # identity with the roles that the user provides 61 | for role in current_user.roles: 62 | identity.provides.add(RoleNeed(role.name)) 63 | -------------------------------------------------------------------------------- /base/auth/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, render_template, flash, redirect, url_for, current_app 2 | from flaskext.babel import lazy_gettext as _ 3 | 4 | from ..ext import db 5 | from .forms import RegisterForm, LoginForm 6 | from .models import User 7 | from .utils import UserManager 8 | 9 | 10 | auth = UserManager( 11 | 'auth', __name__, url_prefix='/auth', template_folder='templates') 12 | 13 | 14 | if not current_app.config.get('AUTH_PROFILE_VIEW'): 15 | 16 | @auth.route('/profile/') 17 | @auth.login_required 18 | def profile(): 19 | return render_template("auth/profile.html") 20 | 21 | 22 | @auth.route('/login/', methods=['POST']) 23 | def login(): 24 | " View function which handles an authentication request. " 25 | form = LoginForm(request.form) 26 | # make sure data are valid, but doesn't validate password is right 27 | if form.validate_on_submit(): 28 | user = User.query.filter_by(email=form.email.data).first() 29 | # we use werzeug to validate user's password 30 | if user and user.check_password(form.password.data): 31 | auth.login(user) 32 | flash(_('Welcome %(user)s', user=user.username)) 33 | redirect_name = current_app.config.get('AUTH_PROFILE_VIEW', 'auth.profile') 34 | return redirect(url_for(redirect_name)) 35 | flash(_('Wrong email or password'), 'error-message') 36 | return redirect(request.referrer or url_for(auth._login_manager.login_view)) 37 | 38 | 39 | @auth.route('/logout/', methods=['GET']) 40 | @auth.login_required 41 | def logout(): 42 | " View function which handles a logout request. " 43 | auth.logout() 44 | return redirect(request.referrer or url_for(auth._login_manager.login_view)) 45 | 46 | 47 | @auth.route('/register/', methods=['GET', 'POST']) 48 | def register(): 49 | " Registration Form. " 50 | form = RegisterForm(request.form) 51 | if form.validate_on_submit(): 52 | # create an user instance not yet stored in the database 53 | user = User( 54 | username=form.username.data, 55 | email=form.email.data, 56 | pw_hash=form.password.data) 57 | 58 | # Insert the record in our database and commit it 59 | db.session.add(user) 60 | db.session.commit() 61 | 62 | auth.login(user) 63 | 64 | # flash will display a message to the user 65 | flash(_('Thanks for registering')) 66 | 67 | # redirect user to the 'home' method of the user module. 68 | redirect_name = current_app.config.get('AUTH_PROFILE_VIEW', 'auth.profile') 69 | return redirect(url_for(redirect_name)) 70 | 71 | return render_template("auth/register.html", form=form) 72 | 73 | 74 | # pymode:lint_ignore=F0401 75 | -------------------------------------------------------------------------------- /base/config/__init__.py: -------------------------------------------------------------------------------- 1 | from os import path as op 2 | 3 | 4 | ROOTDIR = op.abspath( 5 | op.dirname( 6 | op.dirname( 7 | op.dirname(__file__)))) 8 | -------------------------------------------------------------------------------- /base/config/core.py: -------------------------------------------------------------------------------- 1 | """ Immutable basic settings. 2 | """ 3 | 4 | import logging 5 | 6 | from base.config import op, ROOTDIR 7 | 8 | 9 | # Auth 10 | AUTH_USER_MIXINS = [] 11 | AUTH_LOGIN_VIEW = 'core.index' 12 | 13 | # Babel 14 | BABEL_LANGUAGES = ['en', 'ru'] 15 | BABEL_DEFAULT_LOCALE = 'en' 16 | 17 | # Database 18 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + op.join(ROOTDIR, '.db') 19 | 20 | # Cache 21 | CACHE_TYPE = 'simple' 22 | 23 | # Mail 24 | MAIL_SERVER = 'smtp.gmail.com' 25 | MAIL_PORT = 465 26 | MAIL_USE_SSL = True 27 | MAIL_USERNAME = None 28 | MAIL_PASSWORD = None 29 | DEFAULT_MAIL_SENDER = None 30 | 31 | # WTForms 32 | CSRF_ENABLED = True 33 | CSRF_SESSION_KEY = "somethingimpossibletoguess" 34 | 35 | logging.basicConfig( 36 | level=logging.DEBUG, 37 | format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', 38 | datefmt='%d.%m %H:%M:%S') 39 | logging.info("Core settings loaded.") 40 | -------------------------------------------------------------------------------- /base/config/develop.py: -------------------------------------------------------------------------------- 1 | """ Development settings. 2 | """ 3 | 4 | from .production import * 5 | 6 | 7 | MODE = 'develop' 8 | DEBUG = True 9 | DEBUG_TB_INTERCEPT_REDIRECTS = False 10 | SQLALCHEMY_ECHO = True 11 | 12 | logging.info("Develop settings loaded.") 13 | 14 | # pymode:lint_ignore=W0614,W404 15 | -------------------------------------------------------------------------------- /base/config/production.py: -------------------------------------------------------------------------------- 1 | " Production settings must be here. " 2 | 3 | from .core import * 4 | from os import path as op 5 | 6 | 7 | MODE = 'production' 8 | SECRET_KEY = 'SecretKeyForSessionSigning' 9 | ADMINS = MAIL_USERNAME and [MAIL_USERNAME] or None 10 | 11 | # flask.ext.collect 12 | # ----------------- 13 | COLLECT_STATIC_ROOT = op.join(op.dirname(ROOTDIR), 'static') 14 | 15 | # auth.oauth 16 | # ---------- 17 | OAUTH_TWITTER = dict( 18 | consumer_key='750sRyKzvdGPJjPd96yfgw', 19 | consumer_secret='UGcyjDCUOb1q44w1nUk8FA7aXxvwwj1BCbiFvYYI', 20 | ) 21 | 22 | OAUTH_FACEBOOK = dict( 23 | consumer_key='413457268707622', 24 | consumer_secret='48e9be9f4e8abccd3fb916a3f646dd3f', 25 | ) 26 | 27 | OAUTH_GITHUB = dict( 28 | consumer_key='8bdb217c5df1c20fe632', 29 | consumer_secret='a3aa972b2e66e3fac488b4544d55eda2aa2768b6', 30 | ) 31 | 32 | # dealer 33 | DEALER_PARAMS = dict( 34 | backends=('git', 'mercurial', 'simple', 'null') 35 | ) 36 | 37 | logging.info("Production settings loaded.") 38 | 39 | # pymode:lint_ignore=W0614,W404 40 | -------------------------------------------------------------------------------- /base/config/test.py: -------------------------------------------------------------------------------- 1 | " Settings for running tests. " 2 | 3 | from .production import * 4 | 5 | 6 | MODE = 'test' 7 | TESTING = True 8 | SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' 9 | CSRF_ENABLED = False 10 | CACHE_TYPE = 'simple' 11 | 12 | AUTH_USER_MIXINS += ['base.auth.tests.TestUserMixin'] 13 | 14 | logging.info("Test settings loaded.") 15 | 16 | # pymode:lint_ignore=W0614,W404 17 | -------------------------------------------------------------------------------- /base/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ Basic core functionality. 2 | 3 | * Setup admin interface; 4 | * Add alembic models in admin; 5 | 6 | """ 7 | from flask import request, current_app 8 | from flask_mail import Message 9 | from logging import Handler, ERROR 10 | 11 | from ..ext import mail 12 | 13 | 14 | def loader_meta(app=None): 15 | """ Configure core application. 16 | """ 17 | 18 | from .views import core 19 | app.register_blueprint(core) 20 | 21 | from .ext import admin 22 | admin.init_app(app) 23 | 24 | from flask_bootstrap import Bootstrap 25 | bootstrap = Bootstrap() 26 | bootstrap.init_app(app) 27 | 28 | from flask import render_template 29 | app.errorhandler(404)(lambda e: (render_template('core/404.html'), 404)) 30 | 31 | if not app.debug and app.config.get('ADMINS'): 32 | mailhandler = FlaskMailHandler(ERROR) 33 | app.logger.addHandler(mailhandler) 34 | 35 | loader_meta.priority = 100.0 36 | 37 | 38 | class FlaskMailHandler(Handler): 39 | """ Handle production errors on email. 40 | """ 41 | 42 | def emit(self, record): 43 | sbj = "APP ERROR: %s%s" % (request.host_url.rstrip('/'), request.path) 44 | body = self.format(record) or 'Wrong record.' 45 | 46 | msg = Message(sbj, body=body, recipients=current_app.config.get('ADMINS', [])) 47 | mail.send(msg) 48 | 49 | 50 | # pymode:lint_ignore=F0401 51 | -------------------------------------------------------------------------------- /base/core/admin.py: -------------------------------------------------------------------------------- 1 | from .ext import admin, ModelView 2 | from .models import Alembic 3 | 4 | 5 | class AlembicView(ModelView): 6 | column_filters = 'version_num', 7 | column_list = 'version_num', 8 | form_columns = 'version_num', 9 | 10 | admin.add_model(Alembic, AlembicView) 11 | -------------------------------------------------------------------------------- /base/core/ext.py: -------------------------------------------------------------------------------- 1 | from flask_admin import AdminIndexView, Admin 2 | from flask_admin.contrib.sqlamodel import ModelView 3 | from flask_login import current_user 4 | 5 | from ..ext import db 6 | 7 | 8 | class StaffAdminView(AdminIndexView): 9 | " Staff admin home page. " 10 | 11 | def is_accessible(self): 12 | return current_user.is_authenticated() and current_user.permission('staff') 13 | 14 | 15 | class AuthModelView(ModelView): 16 | def __init__(self, *args, **kwargs): 17 | self.role = kwargs.pop('role', None) or 'admin' 18 | super(AuthModelView, self).__init__(*args, **kwargs) 19 | 20 | def is_accessible(self): 21 | return current_user.is_authenticated() and current_user.permission(self.role) 22 | 23 | 24 | class FlaskAdmin(Admin): 25 | 26 | def __init__(self, **kwargs): 27 | super(FlaskAdmin, self).__init__(index_view=StaffAdminView(), **kwargs) 28 | 29 | def init_app(self, app): 30 | self.app = None 31 | super(FlaskAdmin, self).init_app(app) 32 | 33 | from ..loader import loader 34 | loader.register(submodule='admin') 35 | 36 | def add_model(self, model, view=None, **kwargs): 37 | view = view or AuthModelView 38 | self.add_view(view(model, db.session)) 39 | 40 | 41 | admin = FlaskAdmin() 42 | -------------------------------------------------------------------------------- /base/core/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import event 4 | from sqlalchemy.ext.declarative import declared_attr 5 | from sqlalchemy.orm.session import object_session 6 | 7 | from ..ext import db 8 | 9 | 10 | class UpdateMixin(object): 11 | """Provides the 'update' convenience function to allow class 12 | properties to be written via keyword arguments when the object is 13 | already initialised. 14 | 15 | .. code-block: python 16 | 17 | class Person(Base, UpdateMixin): 18 | name = db.Column(String(19)) 19 | 20 | >>> person = Person(name='foo') 21 | >>> person.update(**{'name': 'bar'}) 22 | >>> person.update(login='foo') 23 | 24 | """ 25 | 26 | def update(self, **kw): 27 | for k, v in kw.items(): 28 | if hasattr(self, k): 29 | setattr(self, k, v) 30 | 31 | 32 | class TimestampMixin(object): 33 | """Adds automatically updated created_at and updated_at timestamp 34 | columns to a table, that unsurprisingly are updated on record INSERT and 35 | UPDATE. UTC time is used in both cases. 36 | """ 37 | 38 | created_at = db.Column( 39 | db.DateTime, default=datetime.utcnow, nullable=False) 40 | updated_at = db.Column( 41 | db.DateTime, onupdate=datetime.utcnow, default=datetime.utcnow) 42 | 43 | 44 | class BaseMixin(UpdateMixin, TimestampMixin): 45 | """ Defines an id column to save on boring boilerplate. 46 | """ 47 | 48 | id = db.Column(db.Integer, primary_key=True) 49 | 50 | @declared_attr 51 | def __tablename__(self): 52 | """ Set default tablename. 53 | """ 54 | return self.__name__.lower() 55 | 56 | @property 57 | def __session__(self): 58 | return object_session(self) 59 | 60 | 61 | class Alembic(db.Model): 62 | __tablename__ = 'alembic_version' 63 | version_num = db.Column(db.String(32), nullable=False, primary_key=True) 64 | 65 | 66 | def before_signal(session, *args): 67 | map(lambda o: hasattr(o, 'before_new') and o.before_new(), session.new) 68 | map(lambda o: hasattr(o, 'before_delete') and o.before_delete(), session.deleted) 69 | 70 | event.listen(db.session.__class__, 'before_flush', before_signal) 71 | -------------------------------------------------------------------------------- /base/core/static/foundation.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klen/Flask-Foundation/d154886a8a4358a3bfb99d189a6401e422fea416/base/core/static/foundation.ico -------------------------------------------------------------------------------- /base/core/static/foundation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klen/Flask-Foundation/d154886a8a4358a3bfb99d189a6401e422fea416/base/core/static/foundation.png -------------------------------------------------------------------------------- /base/core/templates/core/404.html: -------------------------------------------------------------------------------- 1 | {% extends "core/base.html" %} 2 | 3 | {% block page_content %} 4 |

{{ _('Page not found.') }}

5 |

:(

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /base/core/templates/core/500.html: -------------------------------------------------------------------------------- 1 | {% extends "core/base.html" %} 2 | 3 | {% block page_content %} 4 |

{{ _('The server encountered an unexpected error.') }}

5 |

{{ _('Please try again later.') }}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /base/core/templates/core/base.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap_responsive.html" %} 2 | 3 | {% block style %} 4 | 5 | 6 | 9 | {%- endblock %} 10 | 11 | {%- block favicons %} 12 | 13 | {%- endblock %} 14 | 15 | {% block title %}{{ title or 'Foundation' }}{% endblock %} 16 | 17 | {% block body_content %} 18 | 19 | {# Navbar #} 20 | {% block navigation %}{% include 'core/blocks/nav.html' %}{% endblock %} 21 | 22 | {# Messages #} 23 | {% block messages %}{% include "core/blocks/messages.html" %}{% endblock %} 24 | 25 | {# Content #} 26 |
27 | {% block page_content %}{% endblock %} 28 |
29 | 30 | {# Footer #} 31 | {% block footer %} 32 | 39 |
40 |
41 |

© Company 2012

42 |
43 |
44 | {% endblock %} 45 | 46 | {% if config.get('FOUNDATION_METRIKA') %} {% include "core/blocks/metrika.html" %} {% endif %} 47 | {% if config.get('FOUNDATION_ANALYTICS') %} {% include "core/blocks/analytics.html" %} {% endif %} 48 | 49 | 50 | 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /base/core/templates/core/blocks/analytics.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /base/core/templates/core/blocks/messages.html: -------------------------------------------------------------------------------- 1 | {% with messages = get_flashed_messages(with_categories=true) %} 2 | {% if messages %} 3 | {% for category, message in messages %} 4 | {% if category == 'error' %} 5 | {% set icon = 'icon-exclamation-sign' %} 6 | {% elif category == 'success' %} 7 | {% set icon = 'icon-ok-sign' %} 8 | {% else %} 9 | {% set icon = 'icon-info-sign' %} 10 | {% endif %} 11 |
12 |   13 | × 14 | {{ message }} 15 |
16 | {% endfor %} 17 | {% endif %} 18 | {% endwith %} 19 | -------------------------------------------------------------------------------- /base/core/templates/core/blocks/metrika.html: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /base/core/templates/core/blocks/nav.html: -------------------------------------------------------------------------------- 1 | 73 | -------------------------------------------------------------------------------- /base/core/templates/core/index.html: -------------------------------------------------------------------------------- 1 | {% extends "core/base.html" %} 2 | 3 | {% block page_content %} 4 | 5 | 6 |
7 |

Quick start with Flask.

8 |

This is a template for a simple creating website with Flask. 9 | Use it as a starting point to create something more unique.

10 |

Learn more »

11 |
12 | 13 | 14 |
15 |
16 |

Batteries

17 | 18 | 31 |

View details »

32 |
33 |
34 |

Candies

35 |

Example user authentication system.

36 |

Example flat pages application.

37 |

Example OAuth authorization

38 |

Included usefull makefile shortcurs (make run, make db and etc).

39 |

View details »

40 |
41 |
42 |

Tests

43 |

Tested for python version 2.6, 2.7

44 |

Build Status

45 |

View details »

46 |
47 |
48 | 49 |
50 |
51 | 52 | {#

The same form rendered using quick_form

#} 53 | {# {{ wtf.quick_form(form) }} #} 54 | 55 |
56 |
57 | 58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /base/core/tests.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from flask_testing import TestCase 3 | from flask_mixer import Mixer 4 | 5 | from ..ext import db 6 | 7 | 8 | class QueriesContext(): 9 | """ Test's tool for check database queries. 10 | 11 | >>> with self.assertNumQueries(4): 12 | >>> do_something() 13 | """ 14 | 15 | def __init__(self, num, testcase): 16 | 17 | self.num = num 18 | self.echo = None 19 | self.testcase = testcase 20 | self.start = 0 21 | 22 | def __enter__(self): 23 | from flask_sqlalchemy import get_debug_queries 24 | 25 | self.start = len(get_debug_queries()) 26 | self.echo = db.engine.echo 27 | db.engine.echo = True 28 | 29 | def __exit__(self, exc_type, exc_value, traceback): 30 | db.engine.echo = self.echo 31 | if exc_type is not None: 32 | return 33 | 34 | from flask_sqlalchemy import get_debug_queries 35 | 36 | executed = len(get_debug_queries()) - self.start 37 | self.testcase.assertEqual( 38 | executed, self.num, "%d queries executed, %d expected" % ( 39 | executed, self.num 40 | ) 41 | ) 42 | 43 | 44 | class FlaskTest(TestCase): 45 | """ Base flask test class. 46 | 47 | Initialize database. 48 | Create objects generator. 49 | """ 50 | 51 | def create_app(self): 52 | return current_app 53 | 54 | def setUp(self): 55 | db.create_all() 56 | self.mixer = Mixer(self.app, session_commit=True) 57 | 58 | def tearDown(self): 59 | db.session.remove() 60 | db.drop_all() 61 | 62 | def assertNumQueries(self, num, func=None): 63 | " Check number of queries by flask_sqlalchemy. " 64 | 65 | context = QueriesContext(num, self) 66 | if func is None: 67 | return context 68 | 69 | with context: 70 | func() 71 | 72 | 73 | class CoreTest(FlaskTest): 74 | 75 | def test_home(self): 76 | response = self.client.get('/') 77 | self.assert200(response) 78 | 79 | def test_admin(self): 80 | with self.assertNumQueries(0): 81 | response = self.client.get('/admin/') 82 | self.assert404(response) 83 | 84 | def test_cache(self): 85 | from ..ext import cache 86 | 87 | cache.set('key', 'value') 88 | testkey = cache.get('key') 89 | self.assertEqual(testkey, 'value') 90 | 91 | def test_after_change(self): 92 | from .models import Alembic 93 | from mock import Mock 94 | Alembic.before_new = Mock() 95 | a = Alembic() 96 | a.version_num = '345' 97 | db.session.add(a) 98 | db.session.commit() 99 | self.assertEqual(Alembic.before_new.call_count, 1) 100 | 101 | def test_mail_handler(self): 102 | """ Handle errors by mail. 103 | """ 104 | from . import FlaskMailHandler 105 | from ..ext import mail 106 | 107 | mail.username = 'test@test.com' 108 | mail.password = 'test' 109 | self.app.config['ADMINS'] = ['test@test.com'] 110 | self.app.config['DEFAULT_MAIL_SENDER'] = 'test@test.com' 111 | self.app.logger.addHandler(FlaskMailHandler(40)) 112 | propagate_exceptions = self.app.config.get('PROPAGATE_EXCEPTIONS') 113 | self.app.config['PROPAGATE_EXCEPTIONS'] = False 114 | 115 | @self.app.route('/error') 116 | def error(): 117 | raise Exception('Error content') 118 | assert error 119 | 120 | with mail.record_messages() as outbox: 121 | self.app.logger.error('Attention!') 122 | self.assertTrue(outbox) 123 | msg = outbox.pop() 124 | self.assertEqual(msg.subject, 'APP ERROR: http://localhost/') 125 | 126 | self.client.get('/error') 127 | msg = outbox.pop() 128 | self.assertTrue('Error content', ) 129 | 130 | self.app.config['PROPAGATE_EXCEPTIONS'] = propagate_exceptions 131 | -------------------------------------------------------------------------------- /base/core/views.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, Blueprint, current_app 2 | 3 | from ..auth.forms import LoginForm 4 | 5 | 6 | core = Blueprint('core', __name__, 7 | template_folder='templates', 8 | static_url_path='/static/core', 9 | static_folder='static') 10 | 11 | 12 | if current_app and current_app.config.get('AUTH_LOGIN_VIEW') == 'core.index': 13 | 14 | @core.route('/') 15 | def index(): 16 | """ 17 | Main page. 18 | 19 | Redifine `AUTH_LOGIN_VIEW` for customize index page. 20 | """ 21 | 22 | return render_template('core/index.html', loginform=LoginForm()) 23 | -------------------------------------------------------------------------------- /base/ext.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_collect import Collect 3 | from flask_debugtoolbar import DebugToolbarExtension 4 | from flask_mail import Mail 5 | from flask_script import Manager 6 | from flask_squll import Squll 7 | from flaskext.babel import Babel 8 | from flask_cache import Cache 9 | from dealer.contrib.flask import Dealer 10 | 11 | from .app import create_app 12 | 13 | 14 | babel = Babel() 15 | cache = Cache() 16 | db = Squll() 17 | dealer = Dealer() 18 | mail = Mail() 19 | 20 | manager = Manager(create_app) 21 | manager.add_option("-c", "--config", dest="config", required=False) 22 | 23 | collect = Collect() 24 | collect.init_script(manager) 25 | 26 | 27 | def config_extensions(app): 28 | " Init application with extensions. " 29 | 30 | cache.init_app(app) 31 | collect.init_app(app) 32 | db.init_app(app) 33 | dealer.init_app(app) 34 | mail.init_app(app) 35 | 36 | DebugToolbarExtension(app) 37 | 38 | config_babel(app) 39 | 40 | 41 | def config_babel(app): 42 | " Init application with babel. " 43 | 44 | babel.init_app(app) 45 | 46 | def get_locale(): 47 | return request.accept_languages.best_match( 48 | app.config['BABEL_LANGUAGES']) 49 | babel.localeselector(get_locale) 50 | 51 | 52 | # pymode:lint_ignore=F0401 53 | -------------------------------------------------------------------------------- /base/loader.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | from straight.plugin.loaders import ModuleLoader 3 | 4 | 5 | class AppLoader(ModuleLoader): 6 | 7 | def __init__(self, subtype=None): 8 | self.subtype = None 9 | self._cache = [] 10 | super(AppLoader, self).__init__() 11 | 12 | def _fill_cache(self, namespace): 13 | super(AppLoader, self)._fill_cache(namespace) 14 | self._cache = filter(self._meta, self._cache) 15 | 16 | def register(self, *args, **kwargs): 17 | " Load and register modules. " 18 | 19 | result = [] 20 | 21 | submodule = kwargs.pop('submodule', None) 22 | logger = kwargs.pop('logger', None) 23 | 24 | for mod in self: 25 | 26 | if logger: 27 | logger.info("Register module: %s" % mod.__name__) 28 | 29 | if submodule: 30 | mod = self.import_module('%s.%s' % (mod.__name__, submodule)) 31 | 32 | meta = self._meta(mod) 33 | meta and meta(*args, **kwargs) 34 | 35 | result.append(mod) 36 | 37 | return result 38 | 39 | def __iter__(self): 40 | return iter(self._cache) 41 | 42 | @staticmethod 43 | def _meta(mod): 44 | return getattr(mod, 'loader_meta', None) 45 | 46 | @staticmethod 47 | def import_module(path): 48 | try: 49 | return import_module(path) 50 | except ImportError: 51 | return None 52 | 53 | 54 | loader = AppLoader() 55 | loader.load(__name__.split('.')[0]) 56 | 57 | 58 | # pymode:lint_ignore=F0401 59 | -------------------------------------------------------------------------------- /base/pages/__init__.py: -------------------------------------------------------------------------------- 1 | " base.pages " 2 | 3 | 4 | def loader_meta(app): 5 | " Configure application. " 6 | 7 | from .views import pages 8 | app.register_blueprint(pages) 9 | -------------------------------------------------------------------------------- /base/pages/admin.py: -------------------------------------------------------------------------------- 1 | from wtforms.fields import TextAreaField 2 | from wtforms.widgets import TextArea 3 | 4 | from ..core.ext import admin, AuthModelView 5 | from .models import Page 6 | 7 | 8 | class WysiwygWidget(TextArea): 9 | def __call__(self, field, **kwargs): 10 | kwargs['class'] = 'span8 textarea' 11 | return super(WysiwygWidget, self).__call__(field, **kwargs) 12 | 13 | 14 | class WysiwygTextAreaField(TextAreaField): 15 | widget = WysiwygWidget() 16 | 17 | 18 | class PageView(AuthModelView): 19 | create_template = 'pages/admin/create.html' 20 | edit_template = 'pages/admin/edit.html' 21 | form_overrides = dict(content=WysiwygTextAreaField) 22 | column_list = 'slug', 'active', 'created_at', 'updated_at' 23 | 24 | 25 | admin.add_model(Page, PageView) 26 | -------------------------------------------------------------------------------- /base/pages/config.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | 3 | 4 | ROOT = current_app.config.get('PAGES_ROOT', 'p').strip('/') 5 | TEMPLATE = current_app.config.get('PAGES_TEMPLATE', 'pages/page.html') 6 | -------------------------------------------------------------------------------- /base/pages/models.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | 3 | from ..core.models import BaseMixin, db 4 | from ..ext import cache 5 | from .config import TEMPLATE, ROOT 6 | 7 | 8 | class Page(db.Model, BaseMixin): 9 | """ Site pages. 10 | """ 11 | 12 | __tablename__ = 'pages_page' 13 | 14 | active = db.Column(db.Boolean, default=True) 15 | slug = db.Column(db.String(100), nullable=False, unique=True) 16 | link = db.Column(db.String(256)) 17 | content = db.Column(db.Text) 18 | 19 | parent_id = db.Column(db.Integer, db.ForeignKey('pages_page.id')) 20 | children = db.relation( 21 | 'Page', 22 | cascade='all', 23 | backref=db.backref('parent', remote_side='Page.id')) 24 | 25 | def __unicode__(self): 26 | return self.slug 27 | 28 | def render(self): 29 | return render_template(TEMPLATE, page=self) 30 | 31 | @property 32 | def uri(self): 33 | cache_key = 'pages.uri.{slug}'.format(slug=self.slug) 34 | uri = cache.get(cache_key) 35 | if not uri: 36 | parent = self.parent_id and self.parent.uri or ('/%s/' % ROOT) 37 | uri = "{parent}{slug}/".format(parent=parent, slug=self.slug) 38 | cache.set(cache_key, uri) 39 | return uri 40 | 41 | @classmethod 42 | def route(cls, slug): 43 | page = cls.query.filter_by(slug=slug).first() 44 | if page is None: 45 | return page 46 | 47 | return page.uri 48 | -------------------------------------------------------------------------------- /base/pages/static/bootstrap-wysihtml5-0.0.2.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"use strict";var c=function(a,b){var c={"font-styles":"",emphasis:"
  • "+"
  • ",lists:"
  • "+""+""+""+"
    "+"
  • ",link:"
  • "+""+"
  • ",image:"
  • "+""+"
  • ",html:"
  • "+"
    "+"
  • ",color:""};return c[a]},d=function(b,c){this.el=b,this.toolbar=this.createToolbar(b,c||f),this.editor=this.createEditor(c),window.editor=this.editor,a("iframe.wysihtml5-sandbox").each(function(b,c){a(c.contentWindow).off("focus.wysihtml5").on({"focus.wysihtml5":function(){a("li.dropdown").removeClass("open")}})})};d.prototype={constructor:d,createEditor:function(a){a=a||{},a.toolbar=this.toolbar[0];var c=new b.Editor(this.el[0],a);if(a&&a.events)for(var d in a.events)c.on(d,a.events[d]);return c},createToolbar:function(b,d){var e=this,h=a("
      ",{"class":"wysihtml5-toolbar",style:"display:none"}),i=d.locale||f.locale||"en";for(var j in f){var k=!1;d[j]!==undefined?d[j]===!0&&(k=!0):k=f[j],k===!0&&(h.append(c(j,g[i])),j==="html"&&this.initHtml(h),j==="link"&&this.initInsertLink(h),j==="image"&&this.initInsertImage(h))}if(d.toolbar)for(j in d.toolbar)h.append(d.toolbar[j]);return h.find("a[data-wysihtml5-command='formatBlock']").click(function(b){var c=b.target||b.srcElement,d=a(c);e.toolbar.find(".current-font").text(d.html())}),h.find("a[data-wysihtml5-command='foreColor']").click(function(b){var c=b.target||b.srcElement,d=a(c);e.toolbar.find(".current-color").text(d.html())}),this.el.before(h),h},initHtml:function(a){var b="a[data-wysihtml5-action='change_view']";a.find(b).click(function(c){a.find("a.btn").not(b).toggleClass("disabled")})},initInsertImage:function(b){var c=this,d=b.find(".bootstrap-wysihtml5-insert-image-modal"),e=d.find(".bootstrap-wysihtml5-insert-image-url"),f=d.find("a.btn-primary"),g=e.val(),h=function(){var a=e.val();e.val(g),c.editor.composer.commands.exec("insertImage",a)};e.keypress(function(a){a.which==13&&(h(),d.modal("hide"))}),f.click(h),d.on("shown",function(){e.focus()}),d.on("hide",function(){c.editor.currentView.element.focus()}),b.find("a[data-wysihtml5-command=insertImage]").click(function(){var b=a(this).hasClass("wysihtml5-command-active");return b?!0:(d.modal("show"),d.on("click.dismiss.modal",'[data-dismiss="modal"]',function(a){a.stopPropagation()}),!1)})},initInsertLink:function(b){var c=this,d=b.find(".bootstrap-wysihtml5-insert-link-modal"),e=d.find(".bootstrap-wysihtml5-insert-link-url"),f=d.find("a.btn-primary"),g=e.val(),h=function(){var a=e.val();e.val(g),c.editor.composer.commands.exec("createLink",{href:a,target:"_blank",rel:"nofollow"})},i=!1;e.keypress(function(a){a.which==13&&(h(),d.modal("hide"))}),f.click(h),d.on("shown",function(){e.focus()}),d.on("hide",function(){c.editor.currentView.element.focus()}),b.find("a[data-wysihtml5-command=createLink]").click(function(){var b=a(this).hasClass("wysihtml5-command-active");return b?!0:(d.appendTo("body").modal("show"),d.on("click.dismiss.modal",'[data-dismiss="modal"]',function(a){a.stopPropagation()}),!1)})}};var e={resetDefaults:function(){a.fn.wysihtml5.defaultOptions=a.extend(!0,{},a.fn.wysihtml5.defaultOptionsCache)},bypassDefaults:function(b){return this.each(function(){var c=a(this);c.data("wysihtml5",new d(c,b))})},shallowExtend:function(b){var c=a.extend({},a.fn.wysihtml5.defaultOptions,b||{}),d=this;return e.bypassDefaults.apply(d,[c])},deepExtend:function(b){var c=a.extend(!0,{},a.fn.wysihtml5.defaultOptions,b||{}),d=this;return e.bypassDefaults.apply(d,[c])},init:function(a){var b=this;return e.shallowExtend.apply(b,[a])}};a.fn.wysihtml5=function(b){if(e[b])return e[b].apply(this,Array.prototype.slice.call(arguments,1));if(typeof b=="object"||!b)return e.init.apply(this,arguments);a.error("Method "+b+" does not exist on jQuery.wysihtml5")},a.fn.wysihtml5.Constructor=d;var f=a.fn.wysihtml5.defaultOptions={"font-styles":!0,color:!1,emphasis:!0,lists:!0,html:!1,link:!0,image:!0,events:{},parserRules:{classes:{"wysiwyg-color-silver":1,"wysiwyg-color-gray":1,"wysiwyg-color-white":1,"wysiwyg-color-maroon":1,"wysiwyg-color-red":1,"wysiwyg-color-purple":1,"wysiwyg-color-fuchsia":1,"wysiwyg-color-green":1,"wysiwyg-color-lime":1,"wysiwyg-color-olive":1,"wysiwyg-color-yellow":1,"wysiwyg-color-navy":1,"wysiwyg-color-blue":1,"wysiwyg-color-teal":1,"wysiwyg-color-aqua":1,"wysiwyg-color-orange":1},tags:{b:{},i:{},br:{},ol:{},ul:{},li:{},h1:{},h2:{},h3:{},blockquote:{},u:1,img:{check_attributes:{width:"numbers",alt:"alt",src:"url",height:"numbers"}},a:{set_attributes:{target:"_blank",rel:"nofollow"},check_attributes:{href:"url"}},span:1,div:1}},locale:"en"};typeof a.fn.wysihtml5.defaultOptionsCache=="undefined"&&(a.fn.wysihtml5.defaultOptionsCache=a.extend(!0,{},a.fn.wysihtml5.defaultOptions));var g=a.fn.wysihtml5.locale={en:{font_styles:{normal:"Normal text",h1:"Heading 1",h2:"Heading 2",h3:"Heading 3"},emphasis:{bold:"Bold",italic:"Italic",underline:"Underline"},lists:{unordered:"Unordered list",ordered:"Ordered list",outdent:"Outdent",indent:"Indent"},link:{insert:"Insert link",cancel:"Cancel"},image:{insert:"Insert image",cancel:"Cancel"},html:{edit:"Edit HTML"},colours:{black:"Black",silver:"Silver",gray:"Grey",maroon:"Maroon",red:"Red",purple:"Purple",green:"Green",olive:"Olive",navy:"Navy",blue:"Blue",orange:"Orange"}}}}(window.jQuery,window.wysihtml5); 2 | -------------------------------------------------------------------------------- /base/pages/static/bootstrap-wysihtml5.css: -------------------------------------------------------------------------------- 1 | ul.wysihtml5-toolbar { 2 | margin: 0; 3 | padding: 0; 4 | display: block; 5 | } 6 | 7 | ul.wysihtml5-toolbar::after { 8 | clear: both; 9 | display: table; 10 | content: ""; 11 | } 12 | 13 | ul.wysihtml5-toolbar > li { 14 | float: left; 15 | display: list-item; 16 | list-style: none; 17 | margin: 0 5px 10px 0; 18 | } 19 | 20 | ul.wysihtml5-toolbar a[data-wysihtml5-command=bold] { 21 | font-weight: bold; 22 | } 23 | 24 | ul.wysihtml5-toolbar a[data-wysihtml5-command=italic] { 25 | font-style: italic; 26 | } 27 | 28 | ul.wysihtml5-toolbar a[data-wysihtml5-command=underline] { 29 | text-decoration: underline; 30 | } 31 | 32 | ul.wysihtml5-toolbar a.btn.wysihtml5-command-active { 33 | background-image: none; 34 | -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05); 35 | -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05); 36 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05); 37 | background-color: #E6E6E6; 38 | background-color: #D9D9D9 9; 39 | outline: 0; 40 | } 41 | 42 | ul.wysihtml5-commands-disabled .dropdown-menu { 43 | display: none !important; 44 | } 45 | -------------------------------------------------------------------------------- /base/pages/static/wysiwyg-color.css: -------------------------------------------------------------------------------- 1 | .wysiwyg-color-black { 2 | color: black; 3 | } 4 | 5 | .wysiwyg-color-silver { 6 | color: silver; 7 | } 8 | 9 | .wysiwyg-color-gray { 10 | color: gray; 11 | } 12 | 13 | .wysiwyg-color-white { 14 | color: white; 15 | } 16 | 17 | .wysiwyg-color-maroon { 18 | color: maroon; 19 | } 20 | 21 | .wysiwyg-color-red { 22 | color: red; 23 | } 24 | 25 | .wysiwyg-color-purple { 26 | color: purple; 27 | } 28 | 29 | .wysiwyg-color-fuchsia { 30 | color: fuchsia; 31 | } 32 | 33 | .wysiwyg-color-green { 34 | color: green; 35 | } 36 | 37 | .wysiwyg-color-lime { 38 | color: lime; 39 | } 40 | 41 | .wysiwyg-color-olive { 42 | color: olive; 43 | } 44 | 45 | .wysiwyg-color-yellow { 46 | color: yellow; 47 | } 48 | 49 | .wysiwyg-color-navy { 50 | color: navy; 51 | } 52 | 53 | .wysiwyg-color-blue { 54 | color: blue; 55 | } 56 | 57 | .wysiwyg-color-teal { 58 | color: teal; 59 | } 60 | 61 | .wysiwyg-color-aqua { 62 | color: aqua; 63 | } 64 | 65 | .wysiwyg-color-orange { 66 | color: orange; 67 | } -------------------------------------------------------------------------------- /base/pages/templates/pages/admin/create.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/model/create.html" %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | 7 | 12 | {% endblock %} 13 | 14 | {% block tail %} 15 | {{ super() }} 16 | 17 | 18 | 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /base/pages/templates/pages/admin/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/model/edit.html" %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | 7 | 12 | {% endblock %} 13 | 14 | {% block tail %} 15 | {{ super() }} 16 | 17 | 18 | 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /base/pages/templates/pages/page.html: -------------------------------------------------------------------------------- 1 | {% extends "core/base.html" %} 2 | 3 | {% block page_content %} 4 | {{ page.content|safe }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /base/pages/tests.py: -------------------------------------------------------------------------------- 1 | from ..core.tests import FlaskTest 2 | from .models import Page 3 | 4 | 5 | class PageTest(FlaskTest): 6 | 7 | def test_page(self): 8 | response = self.client.get('/p/unknown_page/') 9 | self.assert404(response) 10 | 11 | page1 = self.mixer.blend( 12 | 'base.pages.models.Page', content=self.mixer.random) 13 | self.assertEqual(Page.route(page1.slug), page1.uri) 14 | response = self.client.get(page1.uri) 15 | self.assert200(response) 16 | self.assertTrue(page1.content in response.data) 17 | 18 | page2 = self.mixer.blend( 19 | 'base.pages.models.Page', content=self.mixer.random, parent=page1) 20 | with self.assertNumQueries(2): 21 | self.assertEqual(page2.uri, '/p/{slug1}/{slug2}/'.format( 22 | slug1=page1.slug, slug2=page2.slug)) 23 | self.assertEqual(page1.uri, '/p/{slug}/'.format(slug=page1.slug)) 24 | 25 | self.assertEqual(Page.route(page2.slug), page2.uri) 26 | response = self.client.get(page2.uri) 27 | self.assertTrue(page2.content in response.data) 28 | 29 | page3 = self.mixer.blend( 30 | 'base.pages.models.Page', link='http://google.com', parent=page1) 31 | response = self.client.get(page3.uri) 32 | self.assertEqual(response.status_code, 302) 33 | self.assertTrue('http://google.com' in response.data) 34 | -------------------------------------------------------------------------------- /base/pages/views.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, abort, redirect 2 | 3 | from .config import ROOT 4 | from .models import Page 5 | 6 | 7 | pages = Blueprint('pages', __name__, 8 | template_folder='templates', 9 | static_url_path='/static/pages', 10 | static_folder='static') 11 | 12 | 13 | @pages.route('/%s/' % ROOT, methods=['GET']) 14 | def page(pages): 15 | current_page = pages.strip('/').split('/')[-1] 16 | current_page = Page.query.filter_by(slug=current_page).first() 17 | 18 | if current_page is None: 19 | return abort(404) 20 | 21 | if current_page.link: 22 | return redirect(current_page.link) 23 | 24 | return current_page.render() 25 | -------------------------------------------------------------------------------- /base/translations/babel.ini: -------------------------------------------------------------------------------- 1 | # Python 2 | [python: **/base/**.py] 3 | [python: **/flask_admin/**.py] 4 | # Jinja2 5 | [jinja2: **/templates/**.html] 6 | encoding = utf-8 7 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 8 | -------------------------------------------------------------------------------- /base/translations/babel.pot: -------------------------------------------------------------------------------- 1 | # Translations template for Flask-base. 2 | # Copyright (C) 2012 ORGANIZATION 3 | # This file is distributed under the same license as the Flask-base project. 4 | # FIRST AUTHOR , 2012. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Flask-base VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2012-07-09 16:01+0400\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.6\n" 19 | 20 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/base.py:216 21 | msgid "Home" 22 | msgstr "" 23 | 24 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/form.py:81 25 | msgid "Invalid time format" 26 | msgstr "" 27 | 28 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:32 29 | msgid "Invalid directory name" 30 | msgstr "" 31 | 32 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:40 33 | msgid "File to upload" 34 | msgstr "" 35 | 36 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:49 37 | msgid "File required." 38 | msgstr "" 39 | 40 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:54 41 | msgid "Invalid file type." 42 | msgstr "" 43 | 44 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:335 45 | msgid "File uploading is disabled." 46 | msgstr "" 47 | 48 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:344 49 | #, python-format 50 | msgid "File \"%(name)s\" already exists." 51 | msgstr "" 52 | 53 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:351 54 | #, python-format 55 | msgid "Failed to save file: %(error)s" 56 | msgstr "" 57 | 58 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:370 59 | msgid "Directory creation is disabled." 60 | msgstr "" 61 | 62 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:380 63 | #, python-format 64 | msgid "Failed to create directory: %(error)s" 65 | msgstr "" 66 | 67 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:402 68 | msgid "Deletion is disabled." 69 | msgstr "" 70 | 71 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:407 72 | msgid "Directory deletion is disabled." 73 | msgstr "" 74 | 75 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:412 76 | #, python-format 77 | msgid "Directory \"%s\" was successfully deleted." 78 | msgstr "" 79 | 80 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:414 81 | #, python-format 82 | msgid "Failed to delete directory: %(error)s" 83 | msgstr "" 84 | 85 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:418 86 | #, python-format 87 | msgid "File \"%(name)s\" was successfully deleted." 88 | msgstr "" 89 | 90 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:420 91 | #, python-format 92 | msgid "Failed to delete file: %(name)s" 93 | msgstr "" 94 | 95 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:439 96 | msgid "Renaming is disabled." 97 | msgstr "" 98 | 99 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:443 100 | msgid "Path does not exist." 101 | msgstr "" 102 | 103 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:454 104 | #, python-format 105 | msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\"" 106 | msgstr "" 107 | 108 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:457 109 | #, python-format 110 | msgid "Failed to rename: %(error)s" 111 | msgstr "" 112 | 113 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:35 114 | msgid "equals" 115 | msgstr "" 116 | 117 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:43 118 | msgid "not equal" 119 | msgstr "" 120 | 121 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:52 122 | msgid "contains" 123 | msgstr "" 124 | 125 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:61 126 | msgid "not contains" 127 | msgstr "" 128 | 129 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:69 130 | msgid "greater than" 131 | msgstr "" 132 | 133 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:77 134 | msgid "smaller than" 135 | msgstr "" 136 | 137 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/form.py:37 138 | msgid "Already exists." 139 | msgstr "" 140 | 141 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/view.py:518 142 | #, python-format 143 | msgid "Failed to create model. %(error)s" 144 | msgstr "" 145 | 146 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/view.py:533 147 | #, python-format 148 | msgid "Failed to update model. %(error)s" 149 | msgstr "" 150 | 151 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/view.py:548 152 | #, python-format 153 | msgid "Failed to delete model. %(error)s" 154 | msgstr "" 155 | 156 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/model/base.py:735 157 | msgid "Model was successfully created." 158 | msgstr "" 159 | 160 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/model/filters.py:82 161 | msgid "Yes" 162 | msgstr "" 163 | 164 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/model/filters.py:83 165 | msgid "No" 166 | msgstr "" 167 | 168 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/lib.html:105 169 | msgid "Submit" 170 | msgstr "" 171 | 172 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/lib.html:110 173 | msgid "Cancel" 174 | msgstr "" 175 | 176 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/list.html:7 177 | msgid "Root" 178 | msgstr "" 179 | 180 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/list.html:42 181 | #, python-format 182 | msgid "Are you sure you want to delete \\'%(name)s\\' recursively?" 183 | msgstr "" 184 | 185 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/list.html:50 186 | #, python-format 187 | msgid "Are you sure you want to delete \\'%(name)s\\'?" 188 | msgstr "" 189 | 190 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/list.html:75 191 | msgid "Upload File" 192 | msgstr "" 193 | 194 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/list.html:78 195 | msgid "Create Directory" 196 | msgstr "" 197 | 198 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/rename.html:5 199 | #, python-format 200 | msgid "Please provide new name for %(name)s" 201 | msgstr "" 202 | 203 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/create.html:11 204 | msgid "Save and Add" 205 | msgstr "" 206 | 207 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/create.html:16 208 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:12 209 | msgid "List" 210 | msgstr "" 211 | 212 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/create.html:19 213 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:16 214 | msgid "Create" 215 | msgstr "" 216 | 217 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:23 218 | msgid "Add Filter" 219 | msgstr "" 220 | 221 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:44 222 | msgid "Search" 223 | msgstr "" 224 | 225 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:57 226 | msgid "Apply" 227 | msgstr "" 228 | 229 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:59 230 | msgid "Reset Filters" 231 | msgstr "" 232 | 233 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:67 234 | msgid "Remove Filter" 235 | msgstr "" 236 | 237 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:128 238 | msgid "You sure you want to delete this item?" 239 | msgstr "" 240 | 241 | #: /home/klen/Projects/flask_template/base/templates/404.html:4 242 | msgid "Page not found." 243 | msgstr "" 244 | 245 | #: /home/klen/Projects/flask_template/base/templates/500.html:4 246 | msgid "The server encountered an unexpected error." 247 | msgstr "" 248 | 249 | #: /home/klen/Projects/flask_template/base/templates/500.html:5 250 | msgid "Please try again later." 251 | msgstr "" 252 | 253 | #: /home/klen/Projects/flask_template/base/templates/blocks/nav.html:29 254 | msgid "Admin" 255 | msgstr "" 256 | 257 | #: /home/klen/Projects/flask_template/base/templates/blocks/nav.html:31 258 | msgid "Profile" 259 | msgstr "" 260 | 261 | #: /home/klen/Projects/flask_template/base/templates/blocks/nav.html:32 262 | msgid "Logout" 263 | msgstr "" 264 | 265 | #: /home/klen/Projects/flask_template/base/templates/blocks/nav.html:37 266 | #: /home/klen/Projects/flask_template/base/templates/blocks/nav.html:46 267 | #: /home/klen/Projects/flask_template/base/users/forms.py:27 268 | #: /home/klen/Projects/flask_template/base/users/templates/users/login.html:8 269 | #: /home/klen/Projects/flask_template/base/users/templates/users/register.html:12 270 | msgid "Login" 271 | msgstr "" 272 | 273 | #: /home/klen/Projects/flask_template/base/users/forms.py:7 274 | msgid "Email address" 275 | msgstr "" 276 | 277 | #: /home/klen/Projects/flask_template/base/users/forms.py:8 278 | msgid "Email not provided" 279 | msgstr "" 280 | 281 | #: /home/klen/Projects/flask_template/base/users/forms.py:9 282 | msgid "Invalid email address" 283 | msgstr "" 284 | 285 | #: /home/klen/Projects/flask_template/base/users/forms.py:13 286 | msgid "Password" 287 | msgstr "" 288 | 289 | #: /home/klen/Projects/flask_template/base/users/forms.py:14 290 | msgid "Password not provided" 291 | msgstr "" 292 | 293 | #: /home/klen/Projects/flask_template/base/users/forms.py:18 294 | msgid "Retype Password" 295 | msgstr "" 296 | 297 | #: /home/klen/Projects/flask_template/base/users/forms.py:19 298 | msgid "Passwords do not match" 299 | msgstr "" 300 | 301 | #: /home/klen/Projects/flask_template/base/users/forms.py:25 302 | msgid "Remember Me" 303 | msgstr "" 304 | 305 | #: /home/klen/Projects/flask_template/base/users/forms.py:39 306 | msgid "Recover Password" 307 | msgstr "" 308 | 309 | #: /home/klen/Projects/flask_template/base/users/forms.py:50 310 | #: /home/klen/Projects/flask_template/base/users/templates/users/login.html:10 311 | #: /home/klen/Projects/flask_template/base/users/templates/users/register.html:10 312 | msgid "Register" 313 | msgstr "" 314 | 315 | #: /home/klen/Projects/flask_template/base/users/forms.py:64 316 | msgid "Reset Password" 317 | msgstr "" 318 | 319 | #: /home/klen/Projects/flask_template/base/users/views.py:29 320 | #, python-format 321 | msgid "Welcome %(user)s" 322 | msgstr "" 323 | 324 | #: /home/klen/Projects/flask_template/base/users/views.py:31 325 | msgid "Wrong email or password" 326 | msgstr "" 327 | 328 | #: /home/klen/Projects/flask_template/base/users/views.py:59 329 | msgid "Thanks for registering" 330 | msgstr "" 331 | 332 | #: /home/klen/Projects/flask_template/base/users/templates/users/profile.html:4 333 | msgid "Welcome" 334 | msgstr "" 335 | 336 | -------------------------------------------------------------------------------- /base/translations/ru/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klen/Flask-Foundation/d154886a8a4358a3bfb99d189a6401e422fea416/base/translations/ru/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /base/translations/ru/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Russian translations for Flask-base. 2 | # Copyright (C) 2012 ORGANIZATION 3 | # This file is distributed under the same license as the Flask-base project. 4 | # FIRST AUTHOR , 2012. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Flask-base VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2012-07-09 16:01+0400\n" 11 | "PO-Revision-Date: 2012-07-09 15:54+0400\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: ru \n" 14 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " 15 | "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 0.9.6\n" 20 | 21 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/base.py:216 22 | msgid "Home" 23 | msgstr "Домой" 24 | 25 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/form.py:81 26 | msgid "Invalid time format" 27 | msgstr "Неверный формат времени" 28 | 29 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:32 30 | msgid "Invalid directory name" 31 | msgstr "Неправильное имя директории" 32 | 33 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:40 34 | msgid "File to upload" 35 | msgstr "Загрузить фаайл" 36 | 37 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:49 38 | msgid "File required." 39 | msgstr "Файл не указан." 40 | 41 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:54 42 | msgid "Invalid file type." 43 | msgstr "Неправильный тип файла." 44 | 45 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:335 46 | msgid "File uploading is disabled." 47 | msgstr "Загрузка файлов отключена." 48 | 49 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:344 50 | #, python-format 51 | msgid "File \"%(name)s\" already exists." 52 | msgstr "Файл \"%(name)s\" существует." 53 | 54 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:351 55 | #, python-format 56 | msgid "Failed to save file: %(error)s" 57 | msgstr "Ошибка сохранения файла: %(error)s" 58 | 59 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:370 60 | msgid "Directory creation is disabled." 61 | msgstr "Создание директорий невозможно." 62 | 63 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:380 64 | #, python-format 65 | msgid "Failed to create directory: %(error)s" 66 | msgstr "Ошибка создания директории: %(error)s" 67 | 68 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:402 69 | msgid "Deletion is disabled." 70 | msgstr "Удаление отключено." 71 | 72 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:407 73 | msgid "Directory deletion is disabled." 74 | msgstr "Удаление директорий отключено." 75 | 76 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:412 77 | #, python-format 78 | msgid "Directory \"%s\" was successfully deleted." 79 | msgstr "Директория \"%s\" удалена." 80 | 81 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:414 82 | #, python-format 83 | msgid "Failed to delete directory: %(error)s" 84 | msgstr "Ошибка удаления директории: %(error)s" 85 | 86 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:418 87 | #, python-format 88 | msgid "File \"%(name)s\" was successfully deleted." 89 | msgstr "" 90 | 91 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:420 92 | #, python-format 93 | msgid "Failed to delete file: %(name)s" 94 | msgstr "" 95 | 96 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:439 97 | msgid "Renaming is disabled." 98 | msgstr "" 99 | 100 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:443 101 | msgid "Path does not exist." 102 | msgstr "" 103 | 104 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:454 105 | #, python-format 106 | msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\"" 107 | msgstr "" 108 | 109 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/fileadmin.py:457 110 | #, python-format 111 | msgid "Failed to rename: %(error)s" 112 | msgstr "" 113 | 114 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:35 115 | msgid "equals" 116 | msgstr "" 117 | 118 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:43 119 | msgid "not equal" 120 | msgstr "" 121 | 122 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:52 123 | msgid "contains" 124 | msgstr "" 125 | 126 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:61 127 | msgid "not contains" 128 | msgstr "" 129 | 130 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:69 131 | msgid "greater than" 132 | msgstr "" 133 | 134 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/filters.py:77 135 | msgid "smaller than" 136 | msgstr "" 137 | 138 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/form.py:37 139 | msgid "Already exists." 140 | msgstr "" 141 | 142 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/view.py:518 143 | #, python-format 144 | msgid "Failed to create model. %(error)s" 145 | msgstr "" 146 | 147 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/view.py:533 148 | #, python-format 149 | msgid "Failed to update model. %(error)s" 150 | msgstr "" 151 | 152 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/contrib/sqlamodel/view.py:548 153 | #, python-format 154 | msgid "Failed to delete model. %(error)s" 155 | msgstr "" 156 | 157 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/model/base.py:735 158 | msgid "Model was successfully created." 159 | msgstr "" 160 | 161 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/model/filters.py:82 162 | msgid "Yes" 163 | msgstr "" 164 | 165 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/model/filters.py:83 166 | msgid "No" 167 | msgstr "" 168 | 169 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/lib.html:105 170 | msgid "Submit" 171 | msgstr "" 172 | 173 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/lib.html:110 174 | msgid "Cancel" 175 | msgstr "" 176 | 177 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/list.html:7 178 | msgid "Root" 179 | msgstr "" 180 | 181 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/list.html:42 182 | #, python-format 183 | msgid "Are you sure you want to delete \\'%(name)s\\' recursively?" 184 | msgstr "" 185 | 186 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/list.html:50 187 | #, python-format 188 | msgid "Are you sure you want to delete \\'%(name)s\\'?" 189 | msgstr "" 190 | 191 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/list.html:75 192 | msgid "Upload File" 193 | msgstr "" 194 | 195 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/list.html:78 196 | msgid "Create Directory" 197 | msgstr "" 198 | 199 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/file/rename.html:5 200 | #, python-format 201 | msgid "Please provide new name for %(name)s" 202 | msgstr "" 203 | 204 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/create.html:11 205 | msgid "Save and Add" 206 | msgstr "Сохранить и добавить" 207 | 208 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/create.html:16 209 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:12 210 | msgid "List" 211 | msgstr "Список" 212 | 213 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/create.html:19 214 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:16 215 | msgid "Create" 216 | msgstr "Создать" 217 | 218 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:23 219 | msgid "Add Filter" 220 | msgstr "Добавить фильтер" 221 | 222 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:44 223 | msgid "Search" 224 | msgstr "Поиск" 225 | 226 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:57 227 | msgid "Apply" 228 | msgstr "Применить" 229 | 230 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:59 231 | msgid "Reset Filters" 232 | msgstr "Сбросить фильтры" 233 | 234 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:67 235 | msgid "Remove Filter" 236 | msgstr "Удалить фильтер" 237 | 238 | #: /home/klen/Projects/flask_template/.env/lib/python2.7/site-packages/flask_admin/templates/admin/model/list.html:128 239 | msgid "You sure you want to delete this item?" 240 | msgstr "Подтвердите удаление." 241 | 242 | #: /home/klen/Projects/flask_template/base/templates/404.html:4 243 | msgid "Page not found." 244 | msgstr "Страница не найдена." 245 | 246 | #: /home/klen/Projects/flask_template/base/templates/500.html:4 247 | msgid "The server encountered an unexpected error." 248 | msgstr "На сервере произошла непредвиденная ошибка." 249 | 250 | #: /home/klen/Projects/flask_template/base/templates/500.html:5 251 | msgid "Please try again later." 252 | msgstr "Пожалуйста, повторите позже" 253 | 254 | #: /home/klen/Projects/flask_template/base/templates/blocks/nav.html:29 255 | msgid "Admin" 256 | msgstr "Администрирование" 257 | 258 | #: /home/klen/Projects/flask_template/base/templates/blocks/nav.html:31 259 | msgid "Profile" 260 | msgstr "Профиль" 261 | 262 | #: /home/klen/Projects/flask_template/base/templates/blocks/nav.html:32 263 | msgid "Logout" 264 | msgstr "Выйти" 265 | 266 | #: /home/klen/Projects/flask_template/base/templates/blocks/nav.html:37 267 | #: /home/klen/Projects/flask_template/base/templates/blocks/nav.html:46 268 | #: /home/klen/Projects/flask_template/base/users/forms.py:27 269 | #: /home/klen/Projects/flask_template/base/users/templates/users/login.html:8 270 | #: /home/klen/Projects/flask_template/base/users/templates/users/register.html:12 271 | msgid "Login" 272 | msgstr "Войти" 273 | 274 | #: /home/klen/Projects/flask_template/base/users/forms.py:7 275 | msgid "Email address" 276 | msgstr "Электронная почта" 277 | 278 | #: /home/klen/Projects/flask_template/base/users/forms.py:8 279 | msgid "Email not provided" 280 | msgstr "Электронная почта не указана" 281 | 282 | #: /home/klen/Projects/flask_template/base/users/forms.py:9 283 | msgid "Invalid email address" 284 | msgstr "Неправильный адрес" 285 | 286 | #: /home/klen/Projects/flask_template/base/users/forms.py:13 287 | msgid "Password" 288 | msgstr "Пароль" 289 | 290 | #: /home/klen/Projects/flask_template/base/users/forms.py:14 291 | msgid "Password not provided" 292 | msgstr "Пароль не указан" 293 | 294 | #: /home/klen/Projects/flask_template/base/users/forms.py:18 295 | msgid "Retype Password" 296 | msgstr "Повторите пароль" 297 | 298 | #: /home/klen/Projects/flask_template/base/users/forms.py:19 299 | msgid "Passwords do not match" 300 | msgstr "Пароли не совпадают" 301 | 302 | #: /home/klen/Projects/flask_template/base/users/forms.py:25 303 | msgid "Remember Me" 304 | msgstr "Запомнить меня" 305 | 306 | #: /home/klen/Projects/flask_template/base/users/forms.py:39 307 | msgid "Recover Password" 308 | msgstr "Восстановить пароль" 309 | 310 | #: /home/klen/Projects/flask_template/base/users/forms.py:50 311 | #: /home/klen/Projects/flask_template/base/users/templates/users/login.html:10 312 | #: /home/klen/Projects/flask_template/base/users/templates/users/register.html:10 313 | msgid "Register" 314 | msgstr "Зарегистрироваться" 315 | 316 | #: /home/klen/Projects/flask_template/base/users/forms.py:64 317 | msgid "Reset Password" 318 | msgstr "Сброс пароля" 319 | 320 | #: /home/klen/Projects/flask_template/base/users/views.py:29 321 | #, python-format 322 | msgid "Welcome %(user)s" 323 | msgstr "Добро пожаловать %(user)s" 324 | 325 | #: /home/klen/Projects/flask_template/base/users/views.py:31 326 | msgid "Wrong email or password" 327 | msgstr "Неправильный адрес электронной почты или пароль" 328 | 329 | #: /home/klen/Projects/flask_template/base/users/views.py:59 330 | msgid "Thanks for registering" 331 | msgstr "Спасибо за регистрацию" 332 | 333 | #: /home/klen/Projects/flask_template/base/users/templates/users/profile.html:4 334 | msgid "Welcome" 335 | msgstr "Добро пожаловать" 336 | 337 | -------------------------------------------------------------------------------- /makesite.ini: -------------------------------------------------------------------------------- 1 | [Main] 2 | template=virtualenv,uwsgi,flask 3 | 4 | [Templates] 5 | flask=%(source_dir)s/.makesite/flask 6 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from base.ext import db, manager 4 | from base.loader import loader 5 | import sys 6 | 7 | 8 | # Load app scripts 9 | loader.register(manager, submodule='manage') 10 | 11 | 12 | @manager.shell 13 | def make_shell_context(): 14 | " Update shell. " 15 | 16 | from flask import current_app 17 | return dict(app=current_app, db=db) 18 | 19 | 20 | @manager.command 21 | def alembic(): 22 | " Alembic migration utils. " 23 | 24 | from flask import current_app 25 | from alembic.config import main 26 | from os import path as op 27 | 28 | global ARGV 29 | 30 | config = op.join(op.dirname(__file__), 'migrate', 'develop.ini' if current_app.debug else 'production.ini') 31 | 32 | ARGV = ['-c', config] + ARGV 33 | 34 | main(ARGV) 35 | 36 | 37 | @manager.command 38 | def test(testcase=''): 39 | " Run unittests. " 40 | 41 | try: 42 | from unittest2.loader import defaultTestLoader 43 | from unittest2.runner import TextTestRunner 44 | except ImportError: 45 | from unittest.loader import defaultTestLoader 46 | from unittest.runner import TextTestRunner 47 | 48 | if testcase: 49 | mod, case = testcase.rsplit('.', 1) 50 | mod = loader.import_module(mod) 51 | if not mod or not hasattr(mod, case): 52 | sys.stdout.write("Load case error: %s\n" % testcase) 53 | sys.exit(1) 54 | 55 | testcase = getattr(mod, case) 56 | suite = defaultTestLoader.loadTestsFromTestCase(testcase) 57 | else: 58 | cases = loader.register(submodule='tests') 59 | suites = [defaultTestLoader.loadTestsFromModule(mod) for mod in cases] 60 | suite = defaultTestLoader.suiteClass(suites) 61 | 62 | TextTestRunner().run(suite) 63 | 64 | 65 | ARGV = [] 66 | 67 | if __name__ == '__main__': 68 | argv = sys.argv[1:] 69 | if argv and argv[0] == 'alembic': 70 | ARGV = filter(lambda a: not a in ('-c', 'alembic') and not a.startswith('base.config.'), argv) 71 | argv = filter(lambda a: not a in ARGV, argv) 72 | sys.argv = [sys.argv[0] + ' alembic'] + argv 73 | 74 | manager.run() 75 | 76 | 77 | # pymode:lint_ignore=F0401,W801,W0603 78 | -------------------------------------------------------------------------------- /migrate/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klen/Flask-Foundation/d154886a8a4358a3bfb99d189a6401e422fea416/migrate/__init__.py -------------------------------------------------------------------------------- /migrate/develop.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrate 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # set to 'true' to run the environment during 11 | # the 'revision' command, regardless of autogenerate 12 | # revision_environment = false 13 | 14 | sqlalchemy.url = sqlite:///.db 15 | 16 | 17 | # Logging configuration 18 | [loggers] 19 | keys = root,sqlalchemy,alembic 20 | 21 | [handlers] 22 | keys = console 23 | 24 | [formatters] 25 | keys = generic 26 | 27 | [logger_root] 28 | level = WARN 29 | handlers = console 30 | qualname = 31 | 32 | [logger_sqlalchemy] 33 | level = WARN 34 | handlers = 35 | qualname = sqlalchemy.engine 36 | 37 | [logger_alembic] 38 | level = INFO 39 | handlers = 40 | qualname = alembic 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /migrate/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | 6 | # this is the Alembic Config object, which provides 7 | # access to the values within the .ini file in use. 8 | config = context.config 9 | 10 | # Interpret the config file for Python logging. 11 | # This line sets up loggers basically. 12 | fileConfig(config.config_file_name) 13 | 14 | # add your model's MetaData object here 15 | # for 'autogenerate' support 16 | # from myapp import mymodel 17 | # target_metadata = mymodel.Base.metadata 18 | target_metadata = None 19 | 20 | # other values from the config, defined by the needs of env.py, 21 | # can be acquired: 22 | # my_important_option = config.get_main_option("my_important_option") 23 | # ... etc. 24 | 25 | def run_migrations_offline(): 26 | """Run migrations in 'offline' mode. 27 | 28 | This configures the context with just a URL 29 | and not an Engine, though an Engine is acceptable 30 | here as well. By skipping the Engine creation 31 | we don't even need a DBAPI to be available. 32 | 33 | Calls to context.execute() here emit the given string to the 34 | script output. 35 | 36 | """ 37 | url = config.get_main_option("sqlalchemy.url") 38 | context.configure(url=url) 39 | 40 | with context.begin_transaction(): 41 | context.run_migrations() 42 | 43 | def run_migrations_online(): 44 | """Run migrations in 'online' mode. 45 | 46 | In this scenario we need to create an Engine 47 | and associate a connection with the context. 48 | 49 | """ 50 | engine = engine_from_config( 51 | config.get_section(config.config_ini_section), 52 | prefix='sqlalchemy.', 53 | poolclass=pool.NullPool) 54 | 55 | connection = engine.connect() 56 | context.configure( 57 | connection=connection, 58 | target_metadata=target_metadata 59 | ) 60 | 61 | try: 62 | with context.begin_transaction(): 63 | context.run_migrations() 64 | finally: 65 | connection.close() 66 | 67 | if context.is_offline_mode(): 68 | run_migrations_offline() 69 | else: 70 | run_migrations_online() 71 | 72 | -------------------------------------------------------------------------------- /migrate/production.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrate 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # set to 'true' to run the environment during 11 | # the 'revision' command, regardless of autogenerate 12 | # revision_environment = false 13 | 14 | sqlalchemy.url = sqlite:///.db 15 | 16 | 17 | # Logging configuration 18 | [loggers] 19 | keys = root,sqlalchemy,alembic 20 | 21 | [handlers] 22 | keys = console 23 | 24 | [formatters] 25 | keys = generic 26 | 27 | [logger_root] 28 | level = WARN 29 | handlers = console 30 | qualname = 31 | 32 | [logger_sqlalchemy] 33 | level = WARN 34 | handlers = 35 | qualname = sqlalchemy.engine 36 | 37 | [logger_alembic] 38 | level = INFO 39 | handlers = 40 | qualname = alembic 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /migrate/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | 13 | from alembic import op 14 | import sqlalchemy as db 15 | ${imports if imports else ""} 16 | 17 | def upgrade(): 18 | ${upgrades if upgrades else "pass"} 19 | 20 | 21 | def downgrade(): 22 | ${downgrades if downgrades else "pass"} 23 | -------------------------------------------------------------------------------- /migrate/versions/00000001_init_auth_models.py: -------------------------------------------------------------------------------- 1 | """Init auth models 2 | 3 | Create Date: 2012-08-10 17:29:18.996057 4 | 5 | """ 6 | 7 | # revision identifiers, used by Alembic. 8 | from datetime import datetime 9 | 10 | import sqlalchemy as db 11 | from alembic import op 12 | 13 | 14 | revision = '00000001' 15 | down_revision = None 16 | 17 | 18 | def upgrade(): 19 | 20 | op.create_table( 21 | 'auth_role', 22 | db.Column('id', db.Integer, primary_key=True), 23 | db.Column('created_at', db.DateTime, 24 | default=datetime.utcnow, nullable=False), 25 | db.Column('updated_at', db.DateTime, 26 | onupdate=datetime.utcnow, default=datetime.utcnow), 27 | db.Column( 28 | 'name', db.String(19), nullable=False, unique=True), 29 | ) 30 | 31 | op.create_table( 32 | 'auth_user', 33 | db.Column('id', db.Integer, primary_key=True), 34 | db.Column('created_at', db.DateTime, 35 | default=datetime.utcnow, nullable=False), 36 | db.Column('updated_at', db.DateTime, 37 | onupdate=datetime.utcnow, default=datetime.utcnow), 38 | db.Column('username', db.String(50), nullable=False, unique=True), 39 | db.Column('email', db.String(120)), 40 | db.Column('active', db.Boolean, default=True), 41 | db.Column('_pw_hash', db.String(199), nullable=False), 42 | db.Column('oauth_token', db.String(200)), 43 | db.Column('oauth_secret', db.String(200)), 44 | ) 45 | 46 | op.create_table( 47 | 'auth_userroles', 48 | db.Column('user_id', db.Integer, db.ForeignKey('auth_user.id')), 49 | db.Column('role_id', db.Integer, db.ForeignKey('auth_role.id')), 50 | ) 51 | 52 | 53 | def downgrade(): 54 | op.drop_table('auth_role') 55 | op.drop_table('auth_user') 56 | op.drop_table('auth_userroles') 57 | -------------------------------------------------------------------------------- /migrate/versions/00000002_fill_admin_recv.py: -------------------------------------------------------------------------------- 1 | """Fill admin recv 2 | 3 | Create Date: 2012-08-11 17:28:35.464047 4 | 5 | """ 6 | 7 | # revision identifiers, used by Alembic. 8 | from sqlalchemy.ext.declarative import declared_attr 9 | from werkzeug import generate_password_hash 10 | 11 | from base.core.models import BaseMixin 12 | from flask_sqlalchemy import SQLAlchemy 13 | from flask import current_app 14 | 15 | 16 | revision = '00000002' 17 | down_revision = '00000001' 18 | 19 | db = SQLAlchemy(current_app) 20 | 21 | 22 | class MigrateRole(db.Model, BaseMixin): 23 | __tablename__ = 'auth_role' 24 | __table_args__ = {'extend_existing': True} 25 | name = db.Column(db.String(19), nullable=False, unique=True) 26 | 27 | userroles = db.Table( 28 | 'auth_userroles', 29 | db.Column('user_id', db.Integer, db.ForeignKey('auth_user.id')), 30 | db.Column('role_id', db.Integer, db.ForeignKey('auth_role.id')), 31 | extend_existing=True, 32 | ) 33 | 34 | 35 | class MigrateUser(db.Model, BaseMixin): 36 | __tablename__ = 'auth_user' 37 | __table_args__ = {'extend_existing': True} 38 | username = db.Column(db.String(50), unique=True) 39 | email = db.Column(db.String(120)) 40 | active = db.Column(db.Boolean, default=True) 41 | _pw_hash = db.Column(db.String(199), nullable=False) 42 | 43 | @declared_attr 44 | def roles(self): 45 | assert self 46 | return db.relationship(MigrateRole, secondary=userroles, backref="auth") 47 | 48 | 49 | def upgrade(): 50 | admin = MigrateRole(name='admin') 51 | staff = MigrateRole(name='staff') 52 | user = MigrateUser(username='admin', 53 | email='admin@admin.com', 54 | _pw_hash=generate_password_hash('adminft7')) 55 | user.roles.append(admin) 56 | user.roles.append(staff) 57 | db.session.add(user) 58 | db.session.commit() 59 | 60 | 61 | def downgrade(): 62 | pass 63 | 64 | # pymode:lint_ignore=E0611,E0202 65 | -------------------------------------------------------------------------------- /migrate/versions/00000003_added_key_to_auth.py: -------------------------------------------------------------------------------- 1 | """Added Key to auth 2 | 3 | Revision ID: 00000003 4 | Revises: 00000002 5 | Create Date: 2012-09-29 20:54:46.332465 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '00000003' 11 | down_revision = '00000002' 12 | 13 | from alembic import op 14 | import sqlalchemy as db 15 | from datetime import datetime 16 | 17 | 18 | def upgrade(): 19 | op.create_table( 20 | 'auth_key', 21 | 22 | db.Column('id', db.Integer, primary_key=True), 23 | db.Column('created_at', db.DateTime, 24 | default=datetime.utcnow, nullable=False), 25 | db.Column('updated_at', db.DateTime, 26 | onupdate=datetime.utcnow, default=datetime.utcnow), 27 | 28 | db.Column('service_alias', db.String), 29 | db.Column('service_id', db.String), 30 | db.Column('access_token', db.String), 31 | db.Column('secret', db.String), 32 | db.Column('expires', db.DateTime), 33 | db.Column('refresh_token', db.String), 34 | db.Column('user_id', db.Integer, db.ForeignKey('auth_user.id')), 35 | 36 | db.UniqueConstraint('service_alias', 'service_id'), 37 | ) 38 | 39 | 40 | def downgrade(): 41 | op.drop_table('auth_key') 42 | -------------------------------------------------------------------------------- /migrate/versions/00000004_init_pages.py: -------------------------------------------------------------------------------- 1 | """init_pages 2 | 3 | Revision ID: 00000004 4 | Revises: 00000003 5 | Create Date: 2012-12-12 19:35:23.779969 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '00000004' 11 | down_revision = '00000003' 12 | 13 | from alembic import op 14 | import sqlalchemy as db 15 | from datetime import datetime 16 | 17 | 18 | def upgrade(): 19 | op.create_table( 20 | 'pages_page', 21 | 22 | db.Column('id', db.Integer, primary_key=True), 23 | db.Column('created_at', db.DateTime, 24 | default=datetime.utcnow, nullable=False), 25 | db.Column('updated_at', db.DateTime, 26 | onupdate=datetime.utcnow, default=datetime.utcnow), 27 | 28 | db.Column('active', db.Boolean, default=True), 29 | db.Column('slug', db.String(100), nullable=False, unique=True), 30 | db.Column('link', db.String(256)), 31 | db.Column('content', db.Text), 32 | db.Column('parent_id', db.Integer, db.ForeignKey('pages_page.id')), 33 | ) 34 | 35 | 36 | def downgrade(): 37 | op.drop_table('pages_page') 38 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klen/Flask-Foundation/d154886a8a4358a3bfb99d189a6401e422fea416/preview.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Flask template 2 | # -------------- 3 | # Freeze rauth==0.4.16 for Flask-Rauth==0.3.2 4 | rauth==0.4.16 5 | SQLAlchemy==0.8.0 6 | 7 | Flask-Admin==1.0.5 8 | Flask-Babel==0.8 9 | Flask-Bootstrap==2.3.1-1 10 | Flask-Cache==0.11 11 | Flask-DebugToolbar==0.8.0 12 | Flask-Login==0.1.3 13 | Flask-Mail==0.7.6 14 | Flask-Principal==0.3.4 15 | Flask-Rauth==0.3.2 16 | Flask-Script==0.5.3 17 | Flask-Squll==0.3.6 18 | Flask-Testing==0.4 19 | Flask==0.9 20 | alembic==0.4.2 21 | dealer==0.1.9 22 | flask-collect==0.1.6 23 | flask-mixer==0.2.2 24 | mock==1.0.1 25 | straight.plugin==1.4.0-post-1 26 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path as op 3 | 4 | from base.app import create_app 5 | 6 | 7 | APPROOT = op.abspath(op.join(op.dirname(__file__), 'base')) 8 | if not APPROOT in sys.path: 9 | sys.path.insert(0, APPROOT) 10 | 11 | application = create_app() 12 | --------------------------------------------------------------------------------