├── .gitignore ├── app ├── __init__.py ├── app_factory.py ├── default_config.py ├── models │ ├── __init__.py │ ├── core.py │ └── user.py ├── static │ ├── css │ │ └── main.css │ └── js │ │ ├── home.js │ │ └── main.js ├── templates │ ├── about.html │ ├── index.html │ ├── layout.html │ ├── layout_sections │ │ ├── layout_assets_at_bottom.html │ │ ├── layout_assets_at_top.html │ │ └── top_navbar.html │ └── macros.html └── views │ ├── __init__.py │ ├── main_api_bp.py │ └── main_pages_bp.py ├── cli.py ├── dbmanager.py ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 3a542da63280_.py │ └── 490681539ae8_.py ├── requirements.txt └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | instance/ -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuryaSankar/flask_pattern_minimal_with_auth/759e32a32812622b19ae31be7f9a519ace08480c/app/__init__.py -------------------------------------------------------------------------------- /app/app_factory.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from .models.core import db 3 | from .models.user import user_datastore 4 | from . import views 5 | from flask_security import Security 6 | 7 | 8 | def register_blueprints_on_app(app): 9 | app.register_blueprint(views.main_pages_bp) 10 | app.register_blueprint(views.main_api_bp, url_prefix='/api') 11 | 12 | 13 | def create_app(register_blueprints=True): 14 | app = Flask(__name__, instance_relative_config=True) 15 | 16 | app.config.from_object('app.default_config') 17 | app.config.from_pyfile('application.cfg.py') 18 | 19 | app.config['SQLALCHEMY_DATABASE_URI'] = \ 20 | "{db_prefix}://{user}:{passwd}@{server}/{db}".format( 21 | db_prefix=app.config['SQLALCHEMY_DB_PREFIX'], 22 | user=app.config['DB_USERNAME'], 23 | passwd=app.config['DB_PASSWORD'], 24 | server=app.config['DB_SERVER'], 25 | db=app.config['DB_NAME']) 26 | db.init_app(app) 27 | 28 | if register_blueprints: 29 | register_blueprints_on_app(app) 30 | 31 | Security( 32 | app, user_datastore) 33 | 34 | return app 35 | -------------------------------------------------------------------------------- /app/default_config.py: -------------------------------------------------------------------------------- 1 | SERVER_NAME = 'localhost:5000' 2 | 3 | DB_USERNAME = '' 4 | DB_PASSWORD = '' 5 | DB_SERVER = 'localhost' 6 | DB_NAME = '' 7 | SQLALCHEMY_DB_PREFIX = "mysql+mysqldb" 8 | 9 | SECRET_KEY = "" 10 | SECURITY_PASSWORD_SALT = "" 11 | 12 | SECURITY_REGISTERABLE = True 13 | SECURITY_SEND_REGISTER_EMAIL = False 14 | SECURITY_CHANGEABLE = True -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import db 2 | from .user import User, user_datastore -------------------------------------------------------------------------------- /app/models/core.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | db = SQLAlchemy() 3 | -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | from .core import db 2 | from flask_security import ( 3 | SQLAlchemyUserDatastore, UserMixin, RoleMixin) 4 | from sqlalchemy import func 5 | 6 | 7 | class UserRole(db.Model): 8 | id = db.Column(db.Integer, primary_key=True, unique=True) 9 | created_on = db.Column(db.DateTime(), default=func.now()) 10 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 11 | role_id = db.Column(db.Integer, db.ForeignKey('role.id')) 12 | 13 | 14 | class User(db.Model, UserMixin): 15 | id = db.Column(db.Integer, primary_key=True, unique=True) 16 | created_on = db.Column(db.DateTime(), default=func.now()) 17 | name = db.Column(db.Unicode(100)) 18 | email = db.Column(db.Unicode(100), unique=True) 19 | phone = db.Column(db.Unicode(30), unique=True) 20 | password = db.Column(db.String(200)) 21 | active = db.Column(db.Boolean, default=False) 22 | 23 | roles = db.relationship( 24 | "Role", secondary=UserRole.__table__) 25 | 26 | def __repr__(self): 27 | if self.name: 28 | return "{} <{}>".format( 29 | self.name, self.email) 30 | return self.email 31 | 32 | 33 | class Role(db.Model, RoleMixin): 34 | 35 | id = db.Column(db.Integer, primary_key=True, unique=True) 36 | created_on = db.Column(db.DateTime(), default=func.now()) 37 | name = db.Column(db.String(30), unique=True) 38 | description = db.Column(db.String(200)) 39 | 40 | users = db.relationship( 41 | "User", secondary=UserRole.__table__) 42 | 43 | def __repr__(self): 44 | return self.name 45 | 46 | 47 | user_datastore = SQLAlchemyUserDatastore(db, User, Role) 48 | -------------------------------------------------------------------------------- /app/static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 5rem; 3 | } 4 | .starter-template { 5 | padding: 3rem 1.5rem; 6 | text-align: center; 7 | } -------------------------------------------------------------------------------- /app/static/js/home.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | console.log("In Home Page"); 3 | $("#home-page-cta").click(function(){ 4 | alert("Hello, you are in home page"); 5 | }); 6 | }); -------------------------------------------------------------------------------- /app/static/js/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | console.log("In Main js"); 3 | alert("This message will be displayed on all pages"); 4 | }); -------------------------------------------------------------------------------- /app/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | 4 | {% block page_content %} 5 |
6 |

About us page

7 | 8 | 9 |
10 | {% endblock %} 11 | 12 | {% block page_assets_at_bottom %} 13 | {{ macros.static_js_tag("js/about.js")}} 14 | {% endblock %} 15 | 16 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | 4 | {% block page_content %} 5 |
6 |

Home Page Title

7 | 8 | {% if current_user.is_authenticated %} 9 |

Logged in as {{current_user}}

10 | {% else %} 11 |
12 | Login 13 |
14 |
15 | Register 16 |
17 | {% endif %} 18 | 19 |
20 | {% endblock %} 21 | 22 | {% block page_assets_at_bottom %} 23 | {{ macros.static_js_tag("js/home.js")}} 24 | {% endblock %} 25 | 26 | -------------------------------------------------------------------------------- /app/templates/layout.html: -------------------------------------------------------------------------------- 1 | {% import "macros.html" as macros %} 2 | 3 | 4 | 5 | 6 | Flask Boilerplate Demo 7 | 8 | 9 | {% include "layout_sections/layout_assets_at_top.html" %} 10 | 11 | {{ macros.static_css_tag("css/main.css")}} 12 | 13 | {% block page_assets_at_top %} 14 | {% endblock %} 15 | 16 | 17 | {% include "layout_sections/top_navbar.html" %} 18 |
19 | 20 | {% block page_content %} 21 | {% endblock %} 22 | 23 | 24 |
25 | 26 | 27 | {% include "layout_sections/layout_assets_at_bottom.html" %} 28 | 29 | {{ macros.static_js_tag("js/main.js")}} 30 | 31 | {% block page_assets_at_bottom %} 32 | {% endblock %} 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/templates/layout_sections/layout_assets_at_bottom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/templates/layout_sections/layout_assets_at_top.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/templates/layout_sections/top_navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/macros.html: -------------------------------------------------------------------------------- 1 | {% macro static_js_tag(filepath) %} 2 | 6 | {% endmacro %} 7 | 8 | {% macro static_css_tag(filepath) %} 9 | 13 | 14 | {% endmacro %} -------------------------------------------------------------------------------- /app/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .main_api_bp import main_api_bp 2 | from .main_pages_bp import main_pages_bp 3 | -------------------------------------------------------------------------------- /app/views/main_api_bp.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify 2 | 3 | main_api_bp = Blueprint('main_api_bp', __name__) 4 | 5 | 6 | @main_api_bp.route("/") 7 | def index(): 8 | return jsonify({ 9 | "status": "success" 10 | }) 11 | -------------------------------------------------------------------------------- /app/views/main_pages_bp.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | 3 | main_pages_bp = Blueprint('main_pages_bp', __name__) 4 | 5 | 6 | @main_pages_bp.route("/") 7 | def index(): 8 | return render_template("index.html") 9 | 10 | 11 | @main_pages_bp.route("/about") 12 | def about(): 13 | return render_template("about.html") -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | from app.app_factory import create_app 2 | import app.models as models 3 | import IPython 4 | 5 | 6 | def run_interactive_shell(): 7 | app = create_app() 8 | 9 | # Needed for making the console work in app request context 10 | ctx = app.test_request_context() 11 | ctx.push() 12 | 13 | IPython.embed() 14 | 15 | 16 | if __name__ == '__main__': 17 | run_interactive_shell() 18 | -------------------------------------------------------------------------------- /dbmanager.py: -------------------------------------------------------------------------------- 1 | from flask_script import Manager 2 | from flask_migrate import Migrate, MigrateCommand 3 | from app.models import db 4 | from app.app_factory import create_app 5 | 6 | 7 | def create_db_manager(): 8 | app = create_app(register_blueprints=False) 9 | migrate = Migrate(app, db) 10 | manager = Manager(app) 11 | manager.add_command('db', MigrateCommand) 12 | return manager 13 | 14 | 15 | if __name__ == '__main__': 16 | create_db_manager().run() 17 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option( 26 | 'sqlalchemy.url', 27 | str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%')) 28 | target_metadata = current_app.extensions['migrate'].db.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | 65 | # this callback is used to prevent an auto-migration from being generated 66 | # when there are no changes to the schema 67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 68 | def process_revision_directives(context, revision, directives): 69 | if getattr(config.cmd_opts, 'autogenerate', False): 70 | script = directives[0] 71 | if script.upgrade_ops.is_empty(): 72 | directives[:] = [] 73 | logger.info('No changes in schema detected.') 74 | 75 | connectable = engine_from_config( 76 | config.get_section(config.config_ini_section), 77 | prefix='sqlalchemy.', 78 | poolclass=pool.NullPool, 79 | ) 80 | 81 | with connectable.connect() as connection: 82 | context.configure( 83 | connection=connection, 84 | target_metadata=target_metadata, 85 | process_revision_directives=process_revision_directives, 86 | **current_app.extensions['migrate'].configure_args 87 | ) 88 | 89 | with context.begin_transaction(): 90 | context.run_migrations() 91 | 92 | 93 | if context.is_offline_mode(): 94 | run_migrations_offline() 95 | else: 96 | run_migrations_online() 97 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/3a542da63280_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 3a542da63280 4 | Revises: 490681539ae8 5 | Create Date: 2020-04-13 00:47:25.994021 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '3a542da63280' 14 | down_revision = '490681539ae8' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('role', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('created_on', sa.DateTime(), nullable=True), 24 | sa.Column('name', sa.String(length=30), nullable=True), 25 | sa.Column('description', sa.String(length=200), nullable=True), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('id'), 28 | sa.UniqueConstraint('name') 29 | ) 30 | op.create_table('user_role', 31 | sa.Column('id', sa.Integer(), nullable=False), 32 | sa.Column('created_on', sa.DateTime(), nullable=True), 33 | sa.Column('user_id', sa.Integer(), nullable=True), 34 | sa.Column('role_id', sa.Integer(), nullable=True), 35 | sa.ForeignKeyConstraint(['role_id'], ['role.id'], ), 36 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 37 | sa.PrimaryKeyConstraint('id'), 38 | sa.UniqueConstraint('id') 39 | ) 40 | op.add_column('user', sa.Column('active', sa.Boolean(), nullable=True)) 41 | op.add_column('user', sa.Column('created_on', sa.DateTime(), nullable=True)) 42 | op.add_column('user', sa.Column('password', sa.String(length=200), nullable=True)) 43 | # ### end Alembic commands ### 44 | 45 | 46 | def downgrade(): 47 | # ### commands auto generated by Alembic - please adjust! ### 48 | op.drop_column('user', 'password') 49 | op.drop_column('user', 'created_on') 50 | op.drop_column('user', 'active') 51 | op.drop_table('user_role') 52 | op.drop_table('role') 53 | # ### end Alembic commands ### 54 | -------------------------------------------------------------------------------- /migrations/versions/490681539ae8_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 490681539ae8 4 | Revises: 5 | Create Date: 2020-04-12 20:16:01.603537 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '490681539ae8' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('user', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.Unicode(length=100), nullable=True), 24 | sa.Column('email', sa.Unicode(length=100), nullable=True), 25 | sa.Column('phone', sa.Unicode(length=30), nullable=True), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('email'), 28 | sa.UniqueConstraint('id'), 29 | sa.UniqueConstraint('phone') 30 | ) 31 | # ### end Alembic commands ### 32 | 33 | 34 | def downgrade(): 35 | # ### commands auto generated by Alembic - please adjust! ### 36 | op.drop_table('user') 37 | # ### end Alembic commands ### 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | sqlalchemy 3 | flask_sqlalchemy 4 | flask_script 5 | flask_migrate 6 | flask_security 7 | mysqlclient 8 | bcrypt 9 | ipython 10 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | from werkzeug.serving import run_simple 2 | from app import app_factory 3 | 4 | application = app_factory.create_app() 5 | 6 | 7 | if __name__ == "__main__": 8 | run_simple( 9 | '0.0.0.0', 5000, 10 | application, 11 | use_reloader=True, use_debugger=True, threaded=True) --------------------------------------------------------------------------------