├── .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 |
14 |
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)
--------------------------------------------------------------------------------