├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── app ├── __init__.py ├── api_1_0 │ ├── __init__.py │ └── users.py ├── main │ ├── __init__.py │ ├── errors.py │ ├── forms.py │ └── views.py ├── models.py ├── static │ ├── css │ │ └── .gitkeep │ ├── favicon.ico │ └── js │ │ └── .gitkeep └── templates │ ├── admin │ └── index.html │ └── index.html ├── config.py ├── homepage-admin.png ├── manage.py ├── requirements.txt └── runtime.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # SQLite databases 39 | *.sqlite 40 | 41 | # Virtual environment 42 | venv/ 43 | myenv/ 44 | .env 45 | 46 | backup 47 | .idea 48 | 49 | # 必须有migrations目录,不然Heroku上不能创建新的目录 50 | # migrations 51 | 52 | 53 | # webpack map 54 | *.map 55 | hooks/ 56 | platforms/ 57 | plugins/ 58 | webpack/ 59 | www/ 60 | node_modules/ 61 | platforms/ 62 | dump.rdb 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kevin ZHANG Qing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn manage:app 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask app template - Advanced 2 | 3 | > an advanced Flask app template, integrated bunch of Flask functions/extensions for Admin, Security, blueprint, RESTful structure. 4 | 5 | > fit for both python2 & python3 6 | 7 | > Thanks to: Miguel Grinberg "Flask Web Development" 8 | 9 | ![snapshot](homepage-admin.png "snapshot") 10 | 11 | ## Features: 12 | - configurations in `/config.py` 13 | - Flask_Script: manage application, deployment, command line 14 | - Flask_sqlalchemy: powerful ORM 15 | - Flask_Migrate: manage database, upgrade/downgrade 16 | - Flask_Security: manage Registration/Login/Session/Token 17 | - Flask_Admin: admin dashboard for managing database, full CURD function 18 | - Blueprint for main and api, easy for expansion 19 | 20 | 21 | ## Install 22 | 23 | ``` bash 24 | # git clone 25 | # create virtual env 26 | python3 -m venv venv 27 | source venv/bin/activate 28 | # install python modules 29 | pip3 install -r requirements.txt 30 | ``` 31 | 32 | ## Start up 33 | ``` 34 | # setup database 35 | python manage.py deploy 36 | # create roles and Admin user 37 | python manage.py initrole 38 | # start development server 39 | python manage.py runserver 40 | ``` 41 | Bingo! Check app in your web browser at: http://localhost:5000, and http://localhost:5000/admin 42 | 43 | ## deploy to Heroku Server 44 | ready for deploy to [Heroku](https://www.heroku.com), `Procfile` and `runtime.txt` are included. 45 | ``` 46 | # create app in heroku 47 | # git push to heroku 48 | # configure env. variables 49 | # init database by using manage.py 50 | ``` 51 | for details, refer to: https://devcenter.heroku.com/articles/getting-started-with-python 52 | 53 | ## Expansion 54 | For production app, you can easily expand functions as you wish, such as: 55 | - Flask_Compress 56 | - Flask_Cache 57 | - Flask_Redis 58 | 59 | 60 | > For a detailed explanation on how things work, check out the [guide (CHN)](https://www.jianshu.com/p/f37871e31231). 61 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from flask import Flask, abort, redirect, url_for, request 3 | from flask_sqlalchemy import SQLAlchemy 4 | from config import config 5 | from flask_security import Security, SQLAlchemyUserDatastore, current_user, \ 6 | UserMixin, RoleMixin, login_required, auth_token_required, http_auth_required 7 | from flask_security.utils import encrypt_password 8 | from flask_admin.contrib import sqla 9 | from flask_admin import Admin, helpers as admin_helpers 10 | 11 | db = SQLAlchemy() 12 | 13 | # models must be imported after db/login_manager 14 | from .models import User, Role 15 | 16 | user_datastore = SQLAlchemyUserDatastore(db, User, Role) 17 | security = Security() 18 | 19 | # Create admin 20 | admin = Admin(name=u'Admin Board') 21 | 22 | def create_app(config_name): 23 | app = Flask(__name__) 24 | app.config.from_object(config[config_name]) 25 | config[config_name].init_app(app) 26 | 27 | db.init_app(app) 28 | # datastore must be set after app created 29 | security.init_app(app, datastore=user_datastore) 30 | admin.init_app(app) 31 | 32 | from .main import main as main_blueprint 33 | app.register_blueprint(main_blueprint) 34 | 35 | from .api_1_0 import api as api_1_0_blueprint 36 | app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0') 37 | 38 | return app 39 | -------------------------------------------------------------------------------- /app/api_1_0/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | api = Blueprint('api', __name__) 4 | 5 | from . import users 6 | -------------------------------------------------------------------------------- /app/api_1_0/users.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from flask import jsonify, request, current_app, url_for 3 | from . import api 4 | from ..models import User 5 | from flask_security import auth_token_required 6 | from .. import db 7 | 8 | @api.route('/protected') 9 | @auth_token_required 10 | def token_protected(): 11 | return 'you\'re logged in by Token!' 12 | 13 | @api.route('/users/') 14 | @auth_token_required 15 | def get_user(id): 16 | user = User.query.get_or_404(id) 17 | return jsonify(user.to_json()) 18 | 19 | @api.route('/users//posts/') 20 | def get_user_posts(id): 21 | user = User.query.get_or_404(id) 22 | page = request.args.get('page', 1, type=int) 23 | pagination = user.posts.order_by(Post.timestamp.desc()).paginate( 24 | page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], 25 | error_out=False) 26 | posts = pagination.items 27 | prev = None 28 | if pagination.has_prev: 29 | prev = url_for('api.get_user_posts', page=page-1, _external=True) 30 | next = None 31 | if pagination.has_next: 32 | next = url_for('api.get_user_posts', page=page+1, _external=True) 33 | return jsonify({ 34 | 'posts': [post.to_json() for post in posts], 35 | 'prev': prev, 36 | 'next': next, 37 | 'count': pagination.total 38 | }) 39 | 40 | @api.route('/users//timeline/') 41 | def get_user_followed_posts(id): 42 | user = User.query.get_or_404(id) 43 | page = request.args.get('page', 1, type=int) 44 | pagination = user.followed_posts.order_by(Post.timestamp.desc()).paginate( 45 | page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], 46 | error_out=False) 47 | posts = pagination.items 48 | prev = None 49 | if pagination.has_prev: 50 | prev = url_for('api.get_user_followed_posts', page=page-1, 51 | _external=True) 52 | next = None 53 | if pagination.has_next: 54 | next = url_for('api.get_user_followed_posts', page=page+1, 55 | _external=True) 56 | return jsonify({ 57 | 'posts': [post.to_json() for post in posts], 58 | 'prev': prev, 59 | 'next': next, 60 | 'count': pagination.total 61 | }) 62 | -------------------------------------------------------------------------------- /app/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | main = Blueprint('main', __name__) 4 | 5 | from . import views 6 | -------------------------------------------------------------------------------- /app/main/errors.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request, jsonify 2 | from . import main 3 | 4 | 5 | @main.app_errorhandler(403) 6 | def forbidden(e): 7 | if request.accept_mimetypes.accept_json and \ 8 | not request.accept_mimetypes.accept_html: 9 | response = jsonify({'error': 'forbidden'}) 10 | response.status_code = 403 11 | return response 12 | return render_template('403.html'), 403 13 | 14 | 15 | @main.app_errorhandler(404) 16 | def page_not_found(e): 17 | if request.accept_mimetypes.accept_json and \ 18 | not request.accept_mimetypes.accept_html: 19 | response = jsonify({'error': 'not found'}) 20 | response.status_code = 404 21 | return response 22 | return render_template('404.html'), 404 23 | 24 | 25 | @main.app_errorhandler(500) 26 | def internal_server_error(e): 27 | if request.accept_mimetypes.accept_json and \ 28 | not request.accept_mimetypes.accept_html: 29 | response = jsonify({'error': 'internal server error'}) 30 | response.status_code = 500 31 | return response 32 | return render_template('500.html'), 500 33 | -------------------------------------------------------------------------------- /app/main/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import Form 2 | from wtforms import StringField, TextAreaField, BooleanField, SelectField,\ 3 | SubmitField 4 | from wtforms.validators import Required, Length, Email, Regexp 5 | from wtforms import ValidationError 6 | from flask_pagedown.fields import PageDownField 7 | from ..models import Role, User 8 | 9 | 10 | class NameForm(Form): 11 | name = StringField('What is your name?', validators=[Required()]) 12 | submit = SubmitField('Submit') 13 | 14 | 15 | class EditProfileForm(Form): 16 | name = StringField('Real name', validators=[Length(0, 64)]) 17 | location = StringField('Location', validators=[Length(0, 64)]) 18 | about_me = TextAreaField('About me') 19 | submit = SubmitField('Submit') 20 | 21 | 22 | class EditProfileAdminForm(Form): 23 | email = StringField('Email', validators=[Required(), Length(1, 64), 24 | Email()]) 25 | username = StringField('Username', validators=[ 26 | Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 27 | 'Usernames must have only letters, ' 28 | 'numbers, dots or underscores')]) 29 | confirmed = BooleanField('Confirmed') 30 | role = SelectField('Role', coerce=int) 31 | name = StringField('Real name', validators=[Length(0, 64)]) 32 | location = StringField('Location', validators=[Length(0, 64)]) 33 | about_me = TextAreaField('About me') 34 | submit = SubmitField('Submit') 35 | 36 | def __init__(self, user, *args, **kwargs): 37 | super(EditProfileAdminForm, self).__init__(*args, **kwargs) 38 | self.role.choices = [(role.id, role.name) 39 | for role in Role.query.order_by(Role.name).all()] 40 | self.user = user 41 | 42 | def validate_email(self, field): 43 | if field.data != self.user.email and \ 44 | User.query.filter_by(email=field.data).first(): 45 | raise ValidationError('Email already registered.') 46 | 47 | def validate_username(self, field): 48 | if field.data != self.user.username and \ 49 | User.query.filter_by(username=field.data).first(): 50 | raise ValidationError('Username already in use.') 51 | 52 | 53 | class PostForm(Form): 54 | body = PageDownField("What's on your mind?", validators=[Required()]) 55 | submit = SubmitField('Submit') 56 | 57 | 58 | class CommentForm(Form): 59 | body = StringField('Enter your comment', validators=[Required()]) 60 | submit = SubmitField('Submit') 61 | -------------------------------------------------------------------------------- /app/main/views.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from flask import render_template, redirect, url_for, abort, flash, request,\ 3 | current_app, make_response, jsonify 4 | from flask_security import Security, SQLAlchemyUserDatastore, current_user, AnonymousUser, \ 5 | UserMixin, RoleMixin, login_required, auth_token_required, http_auth_required 6 | from flask_security.utils import logout_user 7 | from flask_sqlalchemy import get_debug_queries 8 | from . import main 9 | from .. import db, admin 10 | from flask_admin import Admin, BaseView, expose 11 | from flask_admin.contrib.sqla import ModelView 12 | from flask_admin._backwards import ObsoleteAttr 13 | from ..models import User, Role, roles_users 14 | from flask_admin import helpers as admin_helpers 15 | 16 | # admin dashboard customized homepage 17 | class MyView(BaseView): 18 | @expose('/') 19 | def index(self): 20 | return self.render('admin/index.html') 21 | 22 | # Create customized model view class 23 | class MyModelViewBase(ModelView): 24 | # column formatter 25 | # `view` is current administrative view 26 | # `context` is instance of jinja2.runtime.Context 27 | # `model` is model instance 28 | # `name` is property name 29 | column_display_pk = True # optional, but I like to see the IDs in the list 30 | # column_list = ('id', 'name', 'parent') 31 | column_auto_select_related = ObsoleteAttr('column_auto_select_related', 32 | 'auto_select_related', 33 | True) 34 | column_display_all_relations = True 35 | 36 | def is_accessible(self): 37 | if not current_user.is_active or not current_user.is_authenticated: 38 | return False 39 | if current_user.has_role('superuser'): 40 | return True 41 | return False 42 | 43 | def _handle_view(self, name, **kwargs): 44 | """ 45 | Override builtin _handle_view in order to redirect users when a view is not accessible. 46 | """ 47 | if not self.is_accessible(): 48 | if current_user.is_authenticated: 49 | # permission denied 50 | abort(403) 51 | else: 52 | # login 53 | return redirect(url_for('security.login', next=request.url)) 54 | 55 | class MyModelViewUser(MyModelViewBase): 56 | column_formatters = dict( 57 | password=lambda v, c, m, p: '**'+m.password[-6:], 58 | ) 59 | column_searchable_list = (User.email, ) 60 | 61 | # Role/User Tab: login required 62 | admin.add_view(MyModelViewBase(Role, db.session)) 63 | admin.add_view(MyModelViewUser(User, db.session)) 64 | 65 | @main.route('/', methods=['GET', 'POST']) 66 | def index(): 67 | return render_template('index.html') 68 | 69 | @main.route('/__webpack_hmr') 70 | def npm(): 71 | return redirect('http://localhost:8080/__webpack_hmr') 72 | 73 | @main.route('/protected') 74 | @login_required 75 | def protected(): 76 | if current_user != AnonymousUser and not current_user.is_active: 77 | logout_user() 78 | return "you've been logged out!" 79 | return "Success, it's protected view!" 80 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from datetime import datetime 3 | import hashlib 4 | from werkzeug.security import generate_password_hash, check_password_hash 5 | from flask import current_app, request, url_for, jsonify 6 | from flask_security import Security, SQLAlchemyUserDatastore, \ 7 | UserMixin, RoleMixin, login_required, auth_token_required, http_auth_required 8 | from . import db 9 | 10 | roles_users = db.Table('roles_users', 11 | db.Column('user_id', db.Integer(), db.ForeignKey('users.id')), 12 | db.Column('role_id', db.Integer(), db.ForeignKey('roles.id'))) 13 | 14 | # superuser, admin, author, editor, user 15 | class Role(db.Model, RoleMixin): 16 | __tablename__ = 'roles' 17 | id = db.Column(db.Integer(), primary_key=True) 18 | name = db.Column(db.String(80), unique=True) 19 | description = db.Column(db.String(255)) 20 | 21 | def __repr__(self): 22 | return '' % self.name 23 | 24 | class User(UserMixin, db.Model): 25 | __tablename__ = 'users' 26 | id = db.Column(db.Integer, primary_key=True) 27 | email = db.Column(db.String(255), unique=True) 28 | username = db.Column(db.String(64), unique=True, index=True) 29 | password = db.Column(db.String(255)) 30 | active = db.Column(db.Boolean()) 31 | confirmed_at = db.Column(db.DateTime()) 32 | last_login_at = db.Column(db.DateTime()) 33 | current_login_at = db.Column(db.DateTime()) 34 | last_login_ip = db.Column(db.String(63)) 35 | current_login_ip = db.Column(db.String(63)) 36 | login_count = db.Column(db.Integer) 37 | roles = db.relationship('Role', secondary=roles_users, 38 | backref=db.backref('users'))#, lazy='dynamic')) 39 | member_since = db.Column(db.DateTime(), default=datetime.utcnow) 40 | 41 | def to_json(self): 42 | json_user = { 43 | 'url': url_for('api.get_user', id=self.id, _external=True), 44 | 'username': self.username, 45 | 'member_since': self.member_since, 46 | 'last_seen': self.last_seen, 47 | } 48 | return json_user 49 | 50 | def __repr__(self): 51 | return '' % (self.id, self.email) 52 | 53 | class Alembic(db.Model): 54 | __tablename__ = 'alembic_version' 55 | version_num = db.Column(db.String(32), primary_key=True, nullable=False) 56 | 57 | @staticmethod 58 | def clear_A(): 59 | for a in Alembic.query.all(): 60 | print(a.version_num) 61 | db.session.delete(a) 62 | db.session.commit() 63 | print('======== data in Table: Alembic cleared!') 64 | -------------------------------------------------------------------------------- /app/static/css/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinqqnj/flask-template-advanced/86fb5955f1e2894c5157a199559bd6b3959ac357/app/static/css/.gitkeep -------------------------------------------------------------------------------- /app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinqqnj/flask-template-advanced/86fb5955f1e2894c5157a199559bd6b3959ac357/app/static/favicon.ico -------------------------------------------------------------------------------- /app/static/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinqqnj/flask-template-advanced/86fb5955f1e2894c5157a199559bd6b3959ac357/app/static/js/.gitkeep -------------------------------------------------------------------------------- /app/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% block body %} 3 |
4 |
Welcome to Admin dashboard
5 |
6 |

You need to Login to access

7 |
8 |

Back to Homepage

9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Flask advanced template 10 | 11 | 12 | 13 | 14 | 15 |
16 |

Flask advanced template

17 |
18 |

admin dashboard

19 |

login

20 |

logout

21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os, datetime 2 | basedir = os.path.abspath(os.path.dirname(__file__)) 3 | 4 | class Config: 5 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' 6 | # allow tracing login activities 7 | SECURITY_TRACKABLE = True 8 | SECURITY_PASSWORD_HASH = 'pbkdf2_sha512' 9 | SECURITY_PASSWORD_SALT = 'super-secret' 10 | # allow register 11 | SECURITY_REGISTERABLE = True 12 | SECURITY_SEND_REGISTER_EMAIL = False 13 | SSL_DISABLE = False 14 | 15 | MAIL_SERVER = 'smtp.googlemail.com' 16 | MAIL_PORT = 587 17 | MAIL_USE_TLS = True 18 | MAIL_USERNAME = os.environ.get('MAIL_USERNAME') 19 | MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') 20 | FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]' 21 | FLASKY_MAIL_SENDER = 'Flasky Admin ' 22 | FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN') 23 | FLASKY_POSTS_PER_PAGE = 20 24 | FLASKY_FOLLOWERS_PER_PAGE = 50 25 | FLASKY_COMMENTS_PER_PAGE = 30 26 | FLASKY_SLOW_DB_QUERY_TIME=0.5 27 | 28 | SQLALCHEMY_TRACK_MODIFICATIONS = True 29 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 30 | SQLALCHEMY_RECORD_QUERIES = True 31 | REDISTOGO_URL = os.getenv('REDIS_URL', 'redis://localhost:6379') 32 | REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379') 33 | 34 | @staticmethod 35 | def init_app(app): 36 | pass 37 | 38 | class DevelopmentConfig(Config): 39 | DEBUG = True 40 | SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \ 41 | 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite') 42 | # 'postgresql://yourid:password@localhost/' 43 | 44 | class TestingConfig(Config): 45 | TESTING = True 46 | SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \ 47 | 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite') 48 | WTF_CSRF_ENABLED = False 49 | 50 | class ProductionConfig(Config): 51 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 52 | 'sqlite:///' + os.path.join(basedir, 'data.sqlite') 53 | 54 | @classmethod 55 | def init_app(cls, app): 56 | Config.init_app(app) 57 | 58 | # email errors to the administrators 59 | import logging 60 | from logging.handlers import SMTPHandler 61 | credentials = None 62 | secure = None 63 | if getattr(cls, 'MAIL_USERNAME', None) is not None: 64 | credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD) 65 | if getattr(cls, 'MAIL_USE_TLS', None): 66 | secure = () 67 | mail_handler = SMTPHandler( 68 | mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT), 69 | fromaddr=cls.FLASKY_MAIL_SENDER, 70 | toaddrs=[cls.FLASKY_ADMIN], 71 | subject=cls.FLASKY_MAIL_SUBJECT_PREFIX + ' Application Error', 72 | credentials=credentials, 73 | secure=secure) 74 | mail_handler.setLevel(logging.ERROR) 75 | app.logger.addHandler(mail_handler) 76 | 77 | class HerokuConfig(ProductionConfig): 78 | SSL_DISABLE = bool(os.environ.get('SSL_DISABLE')) 79 | 80 | @classmethod 81 | def init_app(cls, app): 82 | ProductionConfig.init_app(app) 83 | 84 | # handle proxy server headers 85 | from werkzeug.contrib.fixers import ProxyFix 86 | app.wsgi_app = ProxyFix(app.wsgi_app) 87 | 88 | # log to stderr 89 | import logging 90 | from logging import StreamHandler 91 | file_handler = StreamHandler() 92 | file_handler.setLevel(logging.WARNING) 93 | app.logger.addHandler(file_handler) 94 | 95 | class UnixConfig(ProductionConfig): 96 | @classmethod 97 | def init_app(cls, app): 98 | ProductionConfig.init_app(app) 99 | 100 | # log to syslog 101 | import logging 102 | from logging.handlers import SysLogHandler 103 | syslog_handler = SysLogHandler() 104 | syslog_handler.setLevel(logging.WARNING) 105 | app.logger.addHandler(syslog_handler) 106 | 107 | config = { 108 | 'development': DevelopmentConfig, 109 | 'testing': TestingConfig, 110 | 'production': ProductionConfig, 111 | 'heroku': HerokuConfig, 112 | 'unix': UnixConfig, 113 | 'default': DevelopmentConfig 114 | } 115 | -------------------------------------------------------------------------------- /homepage-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinqqnj/flask-template-advanced/86fb5955f1e2894c5157a199559bd6b3959ac357/homepage-admin.png -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | #!/usr/bin/env python 3 | import os 4 | 5 | COV = None 6 | if os.environ.get('FLASK_COVERAGE'): 7 | import coverage 8 | COV = coverage.coverage(branch=True, include='app/*') 9 | COV.start() 10 | 11 | if os.path.exists('.env'): 12 | print('Importing environment from .env...') 13 | for line in open('.env'): 14 | var = line.strip().split('=') 15 | if len(var) == 2: 16 | os.environ[var[0]] = var[1] 17 | 18 | from app import create_app, db 19 | from app.models import User, Role, roles_users, Alembic 20 | from flask_script import Manager, Shell 21 | from flask_migrate import Migrate, MigrateCommand 22 | from flask_security.utils import encrypt_password 23 | 24 | app = create_app(os.getenv('FLASK_CONFIG') or 'default') 25 | manager = Manager(app) 26 | migrate = Migrate(app, db) 27 | 28 | def make_shell_context(): 29 | return dict(app=app, db=db, User=User, Role=Role, roles_users=roles_users) 30 | manager.add_command("shell", Shell(make_context=make_shell_context)) 31 | manager.add_command('db', MigrateCommand) 32 | 33 | @manager.command 34 | def test(coverage=False): 35 | """Run the unit tests.""" 36 | if coverage and not os.environ.get('FLASK_COVERAGE'): 37 | import sys 38 | os.environ['FLASK_COVERAGE'] = '1' 39 | os.execvp(sys.executable, [sys.executable] + sys.argv) 40 | import unittest 41 | tests = unittest.TestLoader().discover('tests') 42 | unittest.TextTestRunner(verbosity=2).run(tests) 43 | if COV: 44 | COV.stop() 45 | COV.save() 46 | print('Coverage Summary:') 47 | COV.report() 48 | basedir = os.path.abspath(os.path.dirname(__file__)) 49 | covdir = os.path.join(basedir, 'tmp/coverage') 50 | COV.html_report(directory=covdir) 51 | print('HTML version: file://%s/index.html' % covdir) 52 | COV.erase() 53 | 54 | @manager.command 55 | def profile(length=25, profile_dir=None): 56 | """Start the application under the code profiler.""" 57 | from werkzeug.contrib.profiler import ProfilerMiddleware 58 | app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length], 59 | profile_dir=profile_dir) 60 | app.run() 61 | 62 | @manager.command 63 | def deploy(): 64 | """Run deployment tasks.""" 65 | from flask_migrate import init, migrate, upgrade 66 | 67 | # migrate database to latest revision 68 | try: init() 69 | except: pass 70 | migrate() 71 | upgrade() 72 | 73 | @manager.command 74 | def dropall(): 75 | db.drop_all() 76 | print("all tables dropped! remember to delete directory: migrations") 77 | 78 | @manager.command 79 | def clear_A(): 80 | # heroku db upgrade failed due to Alembic version mismatch 81 | Alembic.clear_A() 82 | print("Alembic content cleared") 83 | 84 | @manager.command 85 | def initrole(): 86 | db.session.add(Role(name="superuser")) 87 | db.session.add(Role(name="admin")) 88 | db.session.add(Role(name="editor")) 89 | db.session.add(Role(name="author")) 90 | db.session.add(Role(name="user")) 91 | pwd = os.getenv('FLASK_ADMIN_PWD') or input("Pls input Flask admin pwd:") 92 | db.session.add(User(email="admin", password=encrypt_password(pwd), active=True)) 93 | db.session.commit() 94 | ins=roles_users.insert().values(user_id="1", role_id="1") 95 | db.session.execute(ins) 96 | db.session.commit() 97 | print("Roles added!") 98 | 99 | if __name__ == '__main__': 100 | manager.run() 101 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.9.6 2 | Babel==2.5.1 3 | beautifulsoup4==4.6.0 4 | blinker==1.4 5 | certifi==2017.11.5 6 | chardet==3.0.4 7 | click==6.7 8 | Flask==0.12.2 9 | Flask-Admin==1.5.0 10 | Flask-BabelEx==0.9.3 11 | Flask-Login==0.4.1 12 | Flask-Mail==0.9.1 13 | Flask-Migrate==2.1.1 14 | Flask-Principal==0.4.0 15 | Flask-Script==2.0.6 16 | Flask-Security==3.0.0 17 | Flask-SQLAlchemy==2.3.2 18 | Flask-WTF==0.14.2 19 | gunicorn==19.7.1 20 | idna==2.6 21 | itsdangerous==0.24 22 | Jinja2==2.10 23 | Mako==1.0.7 24 | MarkupSafe==1.0 25 | passlib==1.7.1 26 | psycopg2==2.7.3.2 27 | python-dateutil==2.6.1 28 | python-editor==1.0.3 29 | pytz==2017.3 30 | requests==2.18.4 31 | six==1.11.0 32 | speaklater==1.3 33 | SQLAlchemy==1.2.0 34 | urllib3==1.22 35 | Werkzeug==0.15.3 36 | WTForms==2.1 37 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.4 2 | --------------------------------------------------------------------------------