├── .gitignore ├── README.md ├── app.py ├── config.py ├── db_create.py ├── db_downgrade.py ├── db_migrate.py ├── db_upgrade.py ├── models ├── __init__.py ├── dbtools.py └── user.py ├── requirements.txt ├── run.py ├── static ├── css │ └── .gitkeep ├── files │ └── .gitkeep ├── img │ └── .gitkeep └── js │ └── .gitkeep ├── templates └── .gitkeep └── views ├── __init__.py ├── base.py ├── htttp_errors.py └── user.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime* 2 | *.pyc 3 | *.sublime-* 4 | .tox 5 | __pycache__ 6 | *.egg-info 7 | db_repository/ 8 | app.db 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlaskBase 2 | ## Base structure for a flask project with a little magic. 3 | 4 | This is a blueprint for every small-to-medium flask + SQLAlchemy projects. Designed to be cloned and used on the fly. 5 | 6 | ## Basic usage 7 | 8 | Use git to clone this repo, rename/reset the local clone and start building your project. 9 | ```shell 10 | git clone https://github.com/Hrabal/FlaskBase.git 11 | ``` 12 | If you want to later host your project on GitHub or similar, mirror this repo: 13 | 14 | ```shell 15 | git clone --bare https://github.com/Hrabal/FlaskBase.git 16 | 17 | mv FlaskBase.git YourProjectName.git 18 | cd YourProjectName.git 19 | git push --mirror https://github.com/YourGitHubUser/YourProjectRepository.git 20 | ``` 21 | 22 | Edit the `config.py` file to change the db settings (default is a SqlLite db named after the directory your project is in). 23 | 24 | Run the webapp: 25 | 26 | ```shell 27 | python3 run.py 28 | ``` 29 | 30 | Site routes and URIs should be defined in the views directory, avery SQLAlchemy models file should be placed in the models subdirectory. 31 | 32 | The static folder is divided into `css`, `files`, `img` and `js` sobfolders, store your static resources here. 33 | 34 | ## Db commodities 35 | 36 | Db migration scripts are based (well... stolen) from the great [Miguel Grinberg's The Flask Mega-Tutorial](https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-iv-database), read it to understand what's happening. 37 | 38 | For a quick start run the scripts: 39 | 40 | ```shell 41 | python3 db_create.py 42 | python3 db_migrate.py 43 | python3 db_upgrade.py 44 | ``` 45 | ## Views magic 46 | 47 | Every `.py` file in the views directory will be loaded and **executed**. That means that every `@app.route` registered in the views directory will be served. 48 | 49 | ## User Authentication 50 | 51 | This blueprint contains a baisc user login feature in the `views/user.py` view. 52 | The `/login` route handles renders a basic login form, replace TEMP_USER_FORM and all his usages in the file with your custom form to make it pretty, just be sure to add in the form a hidden input called "referrer" with the request.referrer value in it. 53 | 54 | The route manages redirection to the pre-login page, i.e: from x page the user clicks the login link and when authenticated he is redirected to the x page. It also manages basic login errors. 55 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import locale 3 | from flask import Flask 4 | from flask_sqlalchemy import SQLAlchemy 5 | from flask_login import LoginManager 6 | import logging 7 | 8 | locale.setlocale(locale.LC_TIME, locale.getlocale()) 9 | 10 | app = Flask(__name__) 11 | app.config.from_object("config") 12 | 13 | login_manager = LoginManager() 14 | login_manager.init_app(app) 15 | 16 | db = SQLAlchemy(app) 17 | 18 | logging.basicConfig() 19 | logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) 20 | 21 | import models 22 | import views 23 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | db_name = basedir.split("/")[-1] + ".db" 7 | SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(basedir, db_name) 8 | SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, "db_repository") 9 | SECRET_KEY = "tsftw" 10 | SQLALCHEMY_TRACK_MODIFICATIONS = False 11 | -------------------------------------------------------------------------------- /db_create.py: -------------------------------------------------------------------------------- 1 | #!flask/bin/python 2 | from migrate.versioning import api 3 | from config import SQLALCHEMY_DATABASE_URI 4 | from config import SQLALCHEMY_MIGRATE_REPO 5 | from app import db 6 | import os.path 7 | 8 | db.create_all() 9 | if not os.path.exists(SQLALCHEMY_MIGRATE_REPO): 10 | api.create(SQLALCHEMY_MIGRATE_REPO, "database repository") 11 | api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 12 | else: 13 | api.version_control( 14 | SQLALCHEMY_DATABASE_URI, 15 | SQLALCHEMY_MIGRATE_REPO, 16 | api.version(SQLALCHEMY_MIGRATE_REPO), 17 | ) 18 | -------------------------------------------------------------------------------- /db_downgrade.py: -------------------------------------------------------------------------------- 1 | #!flask/bin/python 2 | from migrate.versioning import api 3 | from config import SQLALCHEMY_DATABASE_URI 4 | from config import SQLALCHEMY_MIGRATE_REPO 5 | 6 | v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 7 | api.downgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, v - 1) 8 | v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 9 | print("Current database version: " + str(v)) 10 | -------------------------------------------------------------------------------- /db_migrate.py: -------------------------------------------------------------------------------- 1 | #!flask/bin/python 2 | import imp 3 | from migrate.versioning import api 4 | from app import db 5 | from config import SQLALCHEMY_DATABASE_URI 6 | from config import SQLALCHEMY_MIGRATE_REPO 7 | 8 | v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 9 | migration = SQLALCHEMY_MIGRATE_REPO + ("/versions/%03d_migration.py" % (v + 1)) 10 | tmp_module = imp.new_module("old_model") 11 | old_model = api.create_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 12 | exec(old_model, tmp_module.__dict__) 13 | script = api.make_update_script_for_model( 14 | SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, tmp_module.meta, db.metadata 15 | ) 16 | open(migration, "wt").write(script) 17 | api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 18 | v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 19 | print("New migration saved as " + migration) 20 | print("Current database version: " + str(v)) 21 | -------------------------------------------------------------------------------- /db_upgrade.py: -------------------------------------------------------------------------------- 1 | #!flask/bin/python 2 | from migrate.versioning import api 3 | from config import SQLALCHEMY_DATABASE_URI 4 | from config import SQLALCHEMY_MIGRATE_REPO 5 | 6 | api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 7 | v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 8 | print("Current database version: " + str(v)) 9 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | module_path = os.path.dirname(os.path.abspath(__file__)) 5 | models = [ 6 | f for f in os.listdir(module_path) if f.endswith(".py") and f != "__init__.py" 7 | ] 8 | __all__ = models 9 | print( 10 | "Imported models: %s" % ", ".join(models) 11 | if models 12 | else "No models avaiable in the models directory." 13 | ) 14 | -------------------------------------------------------------------------------- /models/dbtools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | from sqlalchemy.orm import RelationshipProperty 4 | 5 | 6 | def dump_datetime(value): 7 | if value is None: 8 | return None 9 | if isinstance(value, datetime): 10 | return value.strftime("%Y-%m-%d %H:%M:%S") 11 | raise TypeError 12 | 13 | 14 | class Dictable: 15 | def serialize(self, relations=True): 16 | item_id = getattr(self, "id", getattr(self, "code", None)) 17 | res = {"id": item_id} 18 | for k, v in self.iter_model(): 19 | if isinstance(v, datetime): 20 | res.setdefault("data", {})[k] = dump_datetime(v) 21 | elif isinstance(v, RelationshipProperty): 22 | if relations: 23 | res.setdefault("related", {})[k] = [i.serialize() for i in v] 24 | else: 25 | res.setdefault("data", {})[k] = v 26 | return res 27 | 28 | @property 29 | def serialize_many2many(self): 30 | return [item.serialize for item in self.many2many] 31 | 32 | def iter_model(self): 33 | for col in self.__class__.__table__.columns: 34 | if col.autoincrement is not True: 35 | yield col.key, self.__dict__.get(col.key, None) 36 | -------------------------------------------------------------------------------- /models/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import bcrypt 3 | from sqlalchemy.ext.hybrid import hybrid_property 4 | 5 | from app import db 6 | from models.dbtools import Dictable 7 | 8 | 9 | class User(db.Model, Dictable): 10 | user_id = db.Column(db.Integer, primary_key=True, autoincrement=True) 11 | username = db.Column(db.String(64), index=True, unique=True, nullable=False) 12 | password_hash = db.Column(db.String(300)) 13 | email = db.Column(db.String(300)) 14 | 15 | def __init__(self, username, password, email): 16 | self.username = username 17 | self.password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) 18 | self.email = email 19 | 20 | def validate_password(self, password): 21 | return bcrypt.checkpw(password.encode("utf-8"), self.password_hash) 22 | 23 | @hybrid_property 24 | def password(self): 25 | return self.password_hash 26 | 27 | @password.setter 28 | def password_setter(self, password): 29 | self.password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) 30 | 31 | @property 32 | def is_authenticated(self): 33 | return True 34 | 35 | @property 36 | def is_active(self): 37 | return True 38 | 39 | @property 40 | def is_anonymous(self): 41 | return False 42 | 43 | def get_id(self): 44 | return str(self.user_id) 45 | 46 | def __repr__(self): 47 | return "" % (self.username) 48 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bcrypt==3.1.4 2 | SQLAlchemy==1.1.14 3 | Flask==0.12.2 4 | Flask-Login==0.4.0 5 | Flask-SQLAlchemy==2.2 6 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from app import app 3 | 4 | app.run(debug=True) 5 | -------------------------------------------------------------------------------- /static/css/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hrabal/FlaskBase/c81940bfb46132e4281bd9d2ff257e50cc3388fa/static/css/.gitkeep -------------------------------------------------------------------------------- /static/files/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hrabal/FlaskBase/c81940bfb46132e4281bd9d2ff257e50cc3388fa/static/files/.gitkeep -------------------------------------------------------------------------------- /static/img/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hrabal/FlaskBase/c81940bfb46132e4281bd9d2ff257e50cc3388fa/static/img/.gitkeep -------------------------------------------------------------------------------- /static/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hrabal/FlaskBase/c81940bfb46132e4281bd9d2ff257e50cc3388fa/static/js/.gitkeep -------------------------------------------------------------------------------- /templates/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /views/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import importlib 4 | 5 | module_path = os.path.dirname(os.path.abspath(__file__)) 6 | views = [f for f in os.listdir(module_path) if f.endswith(".py") and f != "__init__.py"] 7 | __all__ = views 8 | for view in views: 9 | importlib.import_module("views.%s" % view[:-3]) 10 | 11 | print( 12 | "Imported views: %s" % ", ".join(views) 13 | if views 14 | else "No views avaiable in the views directory." 15 | ) 16 | -------------------------------------------------------------------------------- /views/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from app import app 3 | 4 | 5 | @app.route("/") 6 | def index(): 7 | return "Hello World" 8 | -------------------------------------------------------------------------------- /views/htttp_errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from app import app 3 | 4 | 5 | @app.errorhandler(400) 6 | def error_400(e): 7 | return "400, bad request.", 400 8 | 9 | 10 | @app.errorhandler(404) 11 | def error_404(e): 12 | return "404, page not found.", 404 13 | 14 | 15 | @app.errorhandler(403) 16 | def error_403(e): 17 | return "403, you are not authorized to see resource.", 403 18 | 19 | 20 | @app.errorhandler(405) 21 | def error_405(e): 22 | return "405, HTTP method not allowed.", 405 23 | 24 | 25 | @app.errorhandler(408) 26 | def error_408(e): 27 | return "408, your request is taking too long to be served.", 408 28 | 29 | 30 | @app.errorhandler(418) 31 | def error_418(e): 32 | return "Yes, I am a Teapot.", 418 33 | -------------------------------------------------------------------------------- /views/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from flask import g 3 | from flask_login import current_user, login_user, logout_user, login_required 4 | 5 | from app import app, login_manager 6 | from models.user import User 7 | 8 | TEMP_USER_FORM = """ 9 |
10 | {error} 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | """ 19 | 20 | 21 | @app.before_request 22 | def before_request(): 23 | g.user = current_user 24 | 25 | 26 | @app.route("/login", methods=["GET", "POST"]) 27 | def login(): 28 | if request.method == "POST": 29 | user = User.query.filter_by(username=request.form["username"]).first() 30 | if user: 31 | form_password = request.form["password"].encode("utf-8") 32 | if bcrypt.checkpw(form_password, user.password): 33 | login_user(user, remember=True) 34 | return redirect(request.form["referrer"]) 35 | else: 36 | login_page.set_form(request.form) 37 | return TEMP_USER_FORM.format( 38 | referrer=request.referrer, error="Wrong Password" 39 | ) 40 | else: 41 | return TEMP_USER_FORM.format( 42 | referrer=request.referrer, error="Wrong Username" 43 | ) 44 | return TEMP_USER_FORM.format(referrer=request.referrer, error="") 45 | 46 | 47 | @app.route("/logout") 48 | def logout(): 49 | logout_user() 50 | return redirect(request.referrer) 51 | --------------------------------------------------------------------------------