├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cookiecutter.json ├── docker-compose.sh └── {{cookiecutter.app_slug}} ├── .env ├── Dockerfile ├── Pipfile ├── README.md ├── docker-compose.yml ├── entrypoint.sh ├── manage.py ├── project ├── __init__.py ├── client │ ├── static │ │ ├── main.css │ │ └── main.js │ └── templates │ │ ├── _base.html │ │ ├── errors │ │ ├── 401.html │ │ ├── 403.html │ │ ├── 404.html │ │ └── 500.html │ │ ├── footer.html │ │ ├── header.html │ │ ├── main │ │ ├── about.html │ │ └── home.html │ │ └── user │ │ ├── login.html │ │ ├── members.html │ │ └── register.html ├── server │ ├── __init__.py │ ├── config.py │ ├── db │ │ ├── Dockerfile │ │ └── create.sql │ ├── main │ │ ├── __init__.py │ │ └── views.py │ ├── models.py │ └── user │ │ ├── __init__.py │ │ ├── forms.py │ │ └── views.py └── tests │ ├── __init__.py │ ├── base.py │ ├── helpers.py │ ├── test__config.py │ ├── test_main.py │ └── test_user.py ├── requirements.txt ├── setup-with-docker.md ├── setup-without-docker.md └── setup.cfg /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | 3 | *.coverage 4 | *.db 5 | *.pyc 6 | 7 | htmlcov 8 | migrations 9 | 10 | env 11 | venv 12 | 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.8" 5 | - "3.7" 6 | - "3.6" 7 | - "3.5" 8 | 9 | sudo: required 10 | 11 | services: 12 | - docker 13 | 14 | env: 15 | global: 16 | - DOCKER_COMPOSE_VERSION=1.18.0 17 | - BLACK_VERSION=19.10b0 18 | 19 | before_install: 20 | - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then bash docker-compose.sh; fi 21 | - "cd {{cookiecutter.app_slug}}" 22 | 23 | install: 24 | - "pip install -r requirements.txt" 25 | 26 | before_script: 27 | - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then docker-compose up --build -d; fi 28 | - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then pip install black==$BLACK_VERSION; fi 29 | 30 | script: 31 | - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then black --line-length 79 --check --diff .; fi 32 | - flake8 . 33 | - python manage.py cov 34 | - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then docker-compose run web python manage.py cov; fi 35 | 36 | after_script: 37 | - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then docker-compose down; fi 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Michael Herman 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is no longer maintained. You can find the maintained version at [https://github.com/testdrivenio/cookiecutter-flask-skeleton](https://github.com/testdrivenio/cookiecutter-flask-skeleton) 2 | 3 | # Flask Skeleton 4 | 5 | Flask starter project for [Cookiecutter](https://github.com/audreyr/cookiecutter). 6 | 7 | [![Build Status](https://travis-ci.org/realpython/cookiecutter-flask-skeleton.svg?branch=master)](https://travis-ci.org/realpython/cookiecutter-flask-skeleton) 8 | 9 | ## Quick Start 10 | 11 | Install Cookiecutter globally: 12 | 13 | ```sh 14 | $ pip install cookiecutter 15 | ``` 16 | 17 | Generate the boilerplate: 18 | 19 | ```sh 20 | $ cookiecutter https://github.com/realpython/cookiecutter-flask-skeleton.git 21 | ``` 22 | 23 | Once generated, review the setup guides, within the newly created project directory, to configure the app: 24 | 25 | 1. [setup-with-docker.md](%7B%7Bcookiecutter.app_slug%7D%7D/setup-with-docker.md) 26 | 1. [setup-without-docker.md](%7B%7Bcookiecutter.app_slug%7D%7D/setup-without-docker.md) 27 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_name": "Flask Skeleton", 3 | "app_slug": "{{ cookiecutter.app_name.lower()|replace(' ', '_')|replace('-', '_') }}", 4 | "_copy_without_render": [ 5 | "*.html" 6 | ] 7 | } -------------------------------------------------------------------------------- /docker-compose.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo rm /usr/local/bin/docker-compose 4 | curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 5 | chmod +x docker-compose 6 | sudo mv docker-compose /usr/local/bin 7 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/.env: -------------------------------------------------------------------------------- 1 | APP_NAME="{{cookiecutter.app_name}}" 2 | APP_SETTINGS="project.server.config.ProductionConfig" 3 | FLASK_DEBUG=0 4 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.1-slim 2 | 3 | # install netcat 4 | RUN apt-get update \ 5 | && apt-get -y install netcat \ 6 | && apt-get clean 7 | 8 | # set working directory 9 | RUN mkdir -p /usr/src/app 10 | WORKDIR /usr/src/app 11 | 12 | # add requirements 13 | COPY ./requirements.txt /usr/src/app/requirements.txt 14 | 15 | # install requirements 16 | RUN pip install -r requirements.txt 17 | 18 | # add entrypoint.sh 19 | COPY ./entrypoint.sh /usr/src/app/entrypoint.sh 20 | 21 | # add app 22 | COPY . /usr/src/app 23 | 24 | # run server 25 | CMD ["./entrypoint.sh"] 26 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [packages] 9 | 10 | flask = "*" 11 | flask-bcrypt = "*" 12 | flask-bootstrap = "*" 13 | flask-login = "*" 14 | flask-migrate = "*" 15 | flask-sqlalchemy = "*" 16 | flask-wtf = "*" 17 | psycopg2 = "*" 18 | 19 | 20 | [dev-packages] 21 | 22 | coverage = "*" 23 | flask-debugtoolbar = "*" 24 | flask-testing = "*" 25 | "flake8" = "*" 26 | 27 | 28 | [requires] 29 | 30 | python_version = "3.8" 31 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/README.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.app_name}} 2 | 3 | ## Quick Start 4 | 5 | Install Cookiecutter globally: 6 | 7 | ```sh 8 | $ pip install cookiecutter 9 | ``` 10 | 11 | Generate the boilerplate: 12 | 13 | ```sh 14 | $ cookiecutter https://github.com/realpython/cookiecutter-flask-skeleton.git 15 | ``` 16 | 17 | Review the set up guides to configure the app: 18 | 19 | 1. [setup-with-docker.md](setup-with-docker.md) 20 | 1. [setup-without-docker.md](setup-without-docker.md) 21 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | web: 6 | image: web 7 | build: 8 | context: ./ 9 | dockerfile: Dockerfile 10 | volumes: 11 | - '.:/usr/src/app' 12 | ports: 13 | - 5002:5000 14 | environment: 15 | - APP_NAME={{cookiecutter.app_name}} 16 | - FLASK_DEBUG=1 17 | - PYTHONUNBUFFERED=0 18 | - APP_SETTINGS=project.server.config.ProductionConfig 19 | - DATABASE_URL=postgres://postgres:postgres@web-db:5432/users_dev 20 | - DATABASE_TEST_URL=postgres://postgres:postgres@web-db:5432/users_test 21 | - SECRET_KEY=change_me_in_prod 22 | depends_on: 23 | - web-db 24 | 25 | web-db: 26 | container_name: web-db 27 | build: 28 | context: ./project/server/db 29 | dockerfile: Dockerfile 30 | ports: 31 | - 5435:5432 32 | environment: 33 | - POSTGRES_USER=postgres 34 | - POSTGRES_PASSWORD=postgres 35 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Waiting for postgres..." 4 | 5 | while ! nc -z web-db 5432; do 6 | sleep 0.1 7 | done 8 | 9 | echo "PostgreSQL started" 10 | 11 | python manage.py run -h 0.0.0.0 12 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/manage.py: -------------------------------------------------------------------------------- 1 | # manage.py 2 | 3 | 4 | import unittest 5 | 6 | import coverage 7 | 8 | from flask.cli import FlaskGroup 9 | 10 | from project.server import create_app, db 11 | from project.server.models import User 12 | import subprocess 13 | import sys 14 | 15 | app = create_app() 16 | cli = FlaskGroup(create_app=create_app) 17 | 18 | # code coverage 19 | COV = coverage.coverage( 20 | branch=True, 21 | include="project/*", 22 | omit=[ 23 | "project/tests/*", 24 | "project/server/config.py", 25 | "project/server/*/__init__.py", 26 | ], 27 | ) 28 | COV.start() 29 | 30 | 31 | @cli.command() 32 | def create_db(): 33 | db.drop_all() 34 | db.create_all() 35 | db.session.commit() 36 | 37 | 38 | @cli.command() 39 | def drop_db(): 40 | """Drops the db tables.""" 41 | db.drop_all() 42 | 43 | 44 | @cli.command() 45 | def create_admin(): 46 | """Creates the admin user.""" 47 | db.session.add(User(email="ad@min.com", password="admin", admin=True)) 48 | db.session.commit() 49 | 50 | 51 | @cli.command() 52 | def create_data(): 53 | """Creates sample data.""" 54 | pass 55 | 56 | 57 | @cli.command() 58 | def test(): 59 | """Runs the unit tests without test coverage.""" 60 | tests = unittest.TestLoader().discover("project/tests", pattern="test*.py") 61 | result = unittest.TextTestRunner(verbosity=2).run(tests) 62 | if result.wasSuccessful(): 63 | sys.exit(0) 64 | else: 65 | sys.exit(1) 66 | 67 | 68 | @cli.command() 69 | def cov(): 70 | """Runs the unit tests with coverage.""" 71 | tests = unittest.TestLoader().discover("project/tests") 72 | result = unittest.TextTestRunner(verbosity=2).run(tests) 73 | if result.wasSuccessful(): 74 | COV.stop() 75 | COV.save() 76 | print("Coverage Summary:") 77 | COV.report() 78 | COV.html_report() 79 | COV.erase() 80 | sys.exit(0) 81 | else: 82 | sys.exit(1) 83 | 84 | 85 | @cli.command() 86 | def flake(): 87 | """Runs flake8 on the project.""" 88 | subprocess.run(["flake8", "project"]) 89 | 90 | 91 | if __name__ == "__main__": 92 | cli() 93 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/__init__.py: -------------------------------------------------------------------------------- 1 | # project/__init__.py 2 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/static/main.css: -------------------------------------------------------------------------------- 1 | /* custom css */ 2 | 3 | .site-content { 4 | padding-top: 6rem; 5 | } -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/static/main.js: -------------------------------------------------------------------------------- 1 | // custom javascript 2 | 3 | $( document ).ready(function() { 4 | console.log('Sanity Check!'); 5 | }); -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/_base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ config.APP_NAME }}{% block title %}{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% block css %}{% endblock %} 15 | 16 | 17 | 18 | 19 | {% include 'header.html' %} 20 | 21 |
22 |
23 | 24 | 25 | {% with messages = get_flashed_messages(with_categories=true) %} 26 | {% if messages %} 27 |
28 |
29 |
30 | {% for category, message in messages %} 31 |
32 | × 33 | {{message}} 34 |
35 | {% endfor %} 36 |
37 |
38 | {% endif %} 39 | {% endwith %} 40 | 41 | 42 | {% block content %}{% endblock %} 43 | 44 |
45 | 46 | 47 | {% if error %} 48 |

Error: {{ error }}

49 | {% endif %} 50 | 51 |
52 |
53 | 54 |

55 | 56 | {% include 'footer.html' %} 57 | 58 | 59 | 60 | 61 | 62 | 63 | {% block js %}{% endblock %} 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/errors/401.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | {% block page_title %}- Unauthorized{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 |

401

8 |

You are not authorized to view this page. Please log in.

9 |
10 |
11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/errors/403.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | {% block page_title %}- Forbidden{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 |

403

8 |

You are forbidden from viewing this page. Please log in.

9 |
10 |
11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | {% block page_title %}- Page Not Found{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 |

404

8 |

Sorry. The requested page doesn't exist. Go home.

9 |
10 |
11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | {% block page_title %}- Server Error{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 |

500

8 |

Sorry. Something went terribly wrong. Go home.

9 |
10 |
11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/footer.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 34 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/main/about.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | {% block content %} 3 | 4 |
5 |
6 |

About

7 |

8 |

Add some content here!

9 |
10 |
11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/main/home.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | {% block content %} 3 | 4 |
5 |

Welcome!

6 |

7 |

Add some content here!

8 |
9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/user/login.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% block content %} 4 | 5 |
6 |

Login

7 |

8 |
9 | 10 |
11 | 12 |
13 | {{ form.csrf_token }} 14 | {{ form.hidden_tag() }} 15 | {{ wtf.form_errors(form, hiddens="only") }} 16 | 17 |
18 | {{ wtf.form_field(form.email) }} 19 | {{ wtf.form_field(form.password) }} 20 | 21 | 22 |

23 |

Need to Register?

24 |
25 |
26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/user/members.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | {% block content %} 3 | 4 |

Welcome, {{ current_user.email }}!

5 |

This is the members-only page.

6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/client/templates/user/register.html: -------------------------------------------------------------------------------- 1 | {% extends '_base.html' %} 2 | {% import "bootstrap/wtf.html" as wtf %} 3 | {% block content %} 4 | 5 |
6 |

Register

7 |

8 |
9 | 10 |
11 | 12 |
13 | {{ form.csrf_token }} 14 | {{ form.hidden_tag() }} 15 | {{ wtf.form_errors(form, hiddens="only") }} 16 | 17 |
18 | {{ wtf.form_field(form.email) }} 19 | {{ wtf.form_field(form.password) }} 20 | {{ wtf.form_field(form.confirm) }} 21 | 22 | 23 |

24 |

Already have an account? Sign in.

25 |
26 |
27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/server/__init__.py: -------------------------------------------------------------------------------- 1 | # project/server/__init__.py 2 | 3 | 4 | import os 5 | 6 | from flask import Flask, render_template 7 | from flask_login import LoginManager 8 | from flask_bcrypt import Bcrypt 9 | from flask_debugtoolbar import DebugToolbarExtension 10 | from flask_bootstrap import Bootstrap 11 | from flask_sqlalchemy import SQLAlchemy 12 | from flask_migrate import Migrate 13 | 14 | 15 | # instantiate the extensions 16 | login_manager = LoginManager() 17 | bcrypt = Bcrypt() 18 | toolbar = DebugToolbarExtension() 19 | bootstrap = Bootstrap() 20 | db = SQLAlchemy() 21 | migrate = Migrate() 22 | 23 | 24 | def create_app(script_info=None): 25 | 26 | # instantiate the app 27 | app = Flask( 28 | __name__, 29 | template_folder="../client/templates", 30 | static_folder="../client/static", 31 | ) 32 | 33 | # set config 34 | app_settings = os.getenv( 35 | "APP_SETTINGS", "project.server.config.ProductionConfig" 36 | ) 37 | app.config.from_object(app_settings) 38 | 39 | # set up extensions 40 | login_manager.init_app(app) 41 | bcrypt.init_app(app) 42 | toolbar.init_app(app) 43 | bootstrap.init_app(app) 44 | db.init_app(app) 45 | migrate.init_app(app, db) 46 | 47 | # register blueprints 48 | from project.server.user.views import user_blueprint 49 | from project.server.main.views import main_blueprint 50 | 51 | app.register_blueprint(user_blueprint) 52 | app.register_blueprint(main_blueprint) 53 | 54 | # flask login 55 | from project.server.models import User 56 | 57 | login_manager.login_view = "user.login" 58 | login_manager.login_message_category = "danger" 59 | 60 | @login_manager.user_loader 61 | def load_user(user_id): 62 | return User.query.filter(User.id == int(user_id)).first() 63 | 64 | # error handlers 65 | @app.errorhandler(401) 66 | def unauthorized_page(error): 67 | return render_template("errors/401.html"), 401 68 | 69 | @app.errorhandler(403) 70 | def forbidden_page(error): 71 | return render_template("errors/403.html"), 403 72 | 73 | @app.errorhandler(404) 74 | def page_not_found(error): 75 | return render_template("errors/404.html"), 404 76 | 77 | @app.errorhandler(500) 78 | def server_error_page(error): 79 | return render_template("errors/500.html"), 500 80 | 81 | # shell context for flask cli 82 | @app.shell_context_processor 83 | def ctx(): 84 | return {"app": app, "db": db} 85 | 86 | return app 87 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/server/config.py: -------------------------------------------------------------------------------- 1 | # project/server/config.py 2 | 3 | import os 4 | 5 | basedir = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | 8 | class BaseConfig(object): 9 | """Base configuration.""" 10 | 11 | APP_NAME = os.getenv("APP_NAME", "{{cookiecutter.app_name}}") 12 | BCRYPT_LOG_ROUNDS = 4 13 | DEBUG_TB_ENABLED = False 14 | SECRET_KEY = os.getenv("SECRET_KEY", "my_precious") 15 | SQLALCHEMY_TRACK_MODIFICATIONS = False 16 | WTF_CSRF_ENABLED = False 17 | 18 | 19 | class DevelopmentConfig(BaseConfig): 20 | """Development configuration.""" 21 | 22 | DEBUG_TB_ENABLED = True 23 | DEBUG_TB_INTERCEPT_REDIRECTS = False 24 | SQLALCHEMY_DATABASE_URI = os.environ.get( 25 | "DATABASE_URL", "sqlite:///{0}".format(os.path.join(basedir, "dev.db")) 26 | ) 27 | 28 | 29 | class TestingConfig(BaseConfig): 30 | """Testing configuration.""" 31 | 32 | PRESERVE_CONTEXT_ON_EXCEPTION = False 33 | SQLALCHEMY_DATABASE_URI = "sqlite:///" 34 | SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_TEST_URL", "sqlite:///") 35 | TESTING = True 36 | 37 | 38 | class ProductionConfig(BaseConfig): 39 | """Production configuration.""" 40 | 41 | BCRYPT_LOG_ROUNDS = 13 42 | SQLALCHEMY_DATABASE_URI = os.environ.get( 43 | "DATABASE_URL", 44 | "sqlite:///{0}".format(os.path.join(basedir, "prod.db")), 45 | ) 46 | WTF_CSRF_ENABLED = True 47 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/server/db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:10.4-alpine 2 | 3 | # run create.sql on init 4 | ADD create.sql /docker-entrypoint-initdb.d 5 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/server/db/create.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE users_prod; 2 | CREATE DATABASE users_stage; 3 | CREATE DATABASE users_dev; 4 | CREATE DATABASE users_test; 5 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/server/main/__init__.py: -------------------------------------------------------------------------------- 1 | # project/server/main/__init__.py 2 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/server/main/views.py: -------------------------------------------------------------------------------- 1 | # project/server/main/views.py 2 | 3 | 4 | from flask import render_template, Blueprint 5 | 6 | 7 | main_blueprint = Blueprint("main", __name__) 8 | 9 | 10 | @main_blueprint.route("/") 11 | def home(): 12 | return render_template("main/home.html") 13 | 14 | 15 | @main_blueprint.route("/about/") 16 | def about(): 17 | return render_template("main/about.html") 18 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/server/models.py: -------------------------------------------------------------------------------- 1 | # project/server/models.py 2 | 3 | 4 | import datetime 5 | 6 | from flask import current_app 7 | 8 | from project.server import db, bcrypt 9 | 10 | 11 | class User(db.Model): 12 | 13 | __tablename__ = "users" 14 | 15 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 16 | email = db.Column(db.String(255), unique=True, nullable=False) 17 | password = db.Column(db.String(255), nullable=False) 18 | registered_on = db.Column(db.DateTime, nullable=False) 19 | admin = db.Column(db.Boolean, nullable=False, default=False) 20 | 21 | def __init__(self, email, password, admin=False): 22 | self.email = email 23 | self.password = bcrypt.generate_password_hash( 24 | password, current_app.config.get("BCRYPT_LOG_ROUNDS") 25 | ).decode("utf-8") 26 | self.registered_on = datetime.datetime.now() 27 | self.admin = admin 28 | 29 | def is_authenticated(self): 30 | return True 31 | 32 | def is_active(self): 33 | return True 34 | 35 | def is_anonymous(self): 36 | return False 37 | 38 | def get_id(self): 39 | return self.id 40 | 41 | def __repr__(self): 42 | return "".format(self.email) 43 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/server/user/__init__.py: -------------------------------------------------------------------------------- 1 | # project/server/user/__init__.py 2 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/server/user/forms.py: -------------------------------------------------------------------------------- 1 | # project/server/user/forms.py 2 | 3 | 4 | from flask_wtf import FlaskForm 5 | from wtforms import StringField, PasswordField 6 | from wtforms.validators import DataRequired, Email, Length, EqualTo 7 | 8 | 9 | class LoginForm(FlaskForm): 10 | email = StringField("Email Address", [DataRequired(), Email()]) 11 | password = PasswordField("Password", [DataRequired()]) 12 | 13 | 14 | class RegisterForm(FlaskForm): 15 | email = StringField( 16 | "Email Address", 17 | validators=[ 18 | DataRequired(), 19 | Email(message=None), 20 | Length(min=6, max=40), 21 | ], 22 | ) 23 | password = PasswordField( 24 | "Password", validators=[DataRequired(), Length(min=6, max=25)] 25 | ) 26 | confirm = PasswordField( 27 | "Confirm password", 28 | validators=[ 29 | DataRequired(), 30 | EqualTo("password", message="Passwords must match."), 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/server/user/views.py: -------------------------------------------------------------------------------- 1 | # project/server/user/views.py 2 | 3 | 4 | from flask import render_template, Blueprint, url_for, redirect, flash, request 5 | from flask_login import login_user, logout_user, login_required 6 | 7 | from project.server import bcrypt, db 8 | from project.server.models import User 9 | from project.server.user.forms import LoginForm, RegisterForm 10 | 11 | 12 | user_blueprint = Blueprint("user", __name__) 13 | 14 | 15 | @user_blueprint.route("/register", methods=["GET", "POST"]) 16 | def register(): 17 | form = RegisterForm(request.form) 18 | if form.validate_on_submit(): 19 | user = User(email=form.email.data, password=form.password.data) 20 | db.session.add(user) 21 | db.session.commit() 22 | 23 | login_user(user) 24 | 25 | flash("Thank you for registering.", "success") 26 | return redirect(url_for("user.members")) 27 | 28 | return render_template("user/register.html", form=form) 29 | 30 | 31 | @user_blueprint.route("/login", methods=["GET", "POST"]) 32 | def login(): 33 | form = LoginForm(request.form) 34 | if form.validate_on_submit(): 35 | user = User.query.filter_by(email=form.email.data).first() 36 | if user and bcrypt.check_password_hash( 37 | user.password, request.form["password"] 38 | ): 39 | login_user(user) 40 | flash("You are logged in. Welcome!", "success") 41 | return redirect(url_for("user.members")) 42 | else: 43 | flash("Invalid email and/or password.", "danger") 44 | return render_template("user/login.html", form=form) 45 | return render_template("user/login.html", title="Please Login", form=form) 46 | 47 | 48 | @user_blueprint.route("/logout") 49 | @login_required 50 | def logout(): 51 | logout_user() 52 | flash("You were logged out. Bye!", "success") 53 | return redirect(url_for("main.home")) 54 | 55 | 56 | @user_blueprint.route("/members") 57 | @login_required 58 | def members(): 59 | return render_template("user/members.html") 60 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # project/server/tests/__init__.py 2 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/tests/base.py: -------------------------------------------------------------------------------- 1 | # project/server/tests/base.py 2 | 3 | 4 | from flask_testing import TestCase 5 | 6 | from project.server import db, create_app 7 | from project.server.models import User 8 | 9 | app = create_app() 10 | 11 | 12 | class BaseTestCase(TestCase): 13 | def create_app(self): 14 | app.config.from_object("project.server.config.TestingConfig") 15 | return app 16 | 17 | def setUp(self): 18 | db.create_all() 19 | user = User(email="ad@min.com", password="admin_user") 20 | db.session.add(user) 21 | db.session.commit() 22 | 23 | def tearDown(self): 24 | db.session.remove() 25 | db.drop_all() 26 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/tests/helpers.py: -------------------------------------------------------------------------------- 1 | # tests/helpers.py 2 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/tests/test__config.py: -------------------------------------------------------------------------------- 1 | # project/server/tests/test_config.py 2 | 3 | 4 | import unittest 5 | import os 6 | 7 | from flask import current_app 8 | from flask_testing import TestCase 9 | 10 | from project.server import create_app 11 | 12 | app = create_app() 13 | 14 | 15 | class TestDevelopmentConfig(TestCase): 16 | def create_app(self): 17 | app.config.from_object("project.server.config.DevelopmentConfig") 18 | return app 19 | 20 | def test_app_is_development(self): 21 | self.assertFalse(current_app.config["TESTING"]) 22 | self.assertTrue(app.config["WTF_CSRF_ENABLED"] is False) 23 | self.assertTrue(app.config["DEBUG_TB_ENABLED"] is True) 24 | self.assertFalse(current_app is None) 25 | 26 | 27 | class TestTestingConfig(TestCase): 28 | def create_app(self): 29 | app.config.from_object("project.server.config.TestingConfig") 30 | return app 31 | 32 | def test_app_is_testing(self): 33 | self.assertTrue(current_app.config["TESTING"]) 34 | self.assertTrue(app.config["BCRYPT_LOG_ROUNDS"] == 4) 35 | self.assertTrue(app.config["WTF_CSRF_ENABLED"] is False) 36 | 37 | 38 | class TestProductionConfig(TestCase): 39 | def create_app(self): 40 | app.config.from_object("project.server.config.ProductionConfig") 41 | return app 42 | 43 | def test_app_is_production(self): 44 | self.assertFalse(current_app.config["TESTING"]) 45 | self.assertTrue(app.config["DEBUG_TB_ENABLED"] is False) 46 | self.assertTrue(app.config["WTF_CSRF_ENABLED"] is True) 47 | self.assertTrue(app.config["BCRYPT_LOG_ROUNDS"] == 13) 48 | 49 | def test_secret_key_has_been_set(self): 50 | self.assertTrue( 51 | app.secret_key == os.getenv("SECRET_KEY", default="my_precious") 52 | ) 53 | 54 | 55 | if __name__ == "__main__": 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/tests/test_main.py: -------------------------------------------------------------------------------- 1 | # project/server/tests/test_main.py 2 | 3 | 4 | import unittest 5 | 6 | from base import BaseTestCase 7 | 8 | 9 | class TestMainBlueprint(BaseTestCase): 10 | def test_index(self): 11 | # Ensure Flask is setup. 12 | response = self.client.get("/", follow_redirects=True) 13 | self.assertEqual(response.status_code, 200) 14 | self.assertIn(b"Welcome!", response.data) 15 | self.assertIn(b"Login", response.data) 16 | 17 | def test_about(self): 18 | # Ensure about route behaves correctly. 19 | response = self.client.get("/about", follow_redirects=True) 20 | self.assertEqual(response.status_code, 200) 21 | self.assertIn(b"About", response.data) 22 | 23 | def test_404(self): 24 | # Ensure 404 error is handled. 25 | response = self.client.get("/404") 26 | self.assert404(response) 27 | self.assertTemplateUsed("errors/404.html") 28 | 29 | 30 | if __name__ == "__main__": 31 | unittest.main() 32 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/project/tests/test_user.py: -------------------------------------------------------------------------------- 1 | # project/server/tests/test_user.py 2 | 3 | 4 | import datetime 5 | import unittest 6 | 7 | from flask_login import current_user 8 | 9 | from base import BaseTestCase 10 | from project.server import bcrypt 11 | from project.server.models import User 12 | from project.server.user.forms import LoginForm 13 | 14 | 15 | class TestUserBlueprint(BaseTestCase): 16 | def test_correct_login(self): 17 | # Ensure login behaves correctly with correct credentials. 18 | with self.client: 19 | response = self.client.post( 20 | "/login", 21 | data=dict(email="ad@min.com", password="admin_user"), 22 | follow_redirects=True, 23 | ) 24 | self.assertIn(b"Welcome", response.data) 25 | self.assertIn(b"Logout", response.data) 26 | self.assertIn(b"Members", response.data) 27 | self.assertTrue(current_user.email == "ad@min.com") 28 | self.assertTrue(current_user.is_active()) 29 | self.assertEqual(response.status_code, 200) 30 | 31 | def test_logout_behaves_correctly(self): 32 | # Ensure logout behaves correctly - regarding the session. 33 | with self.client: 34 | self.client.post( 35 | "/login", 36 | data=dict(email="ad@min.com", password="admin_user"), 37 | follow_redirects=True, 38 | ) 39 | response = self.client.get("/logout", follow_redirects=True) 40 | self.assertIn(b"You were logged out. Bye!", response.data) 41 | self.assertFalse(current_user.is_active) 42 | 43 | def test_logout_route_requires_login(self): 44 | # Ensure logout route requres logged in user. 45 | response = self.client.get("/logout", follow_redirects=True) 46 | self.assertIn(b"Please log in to access this page", response.data) 47 | 48 | def test_member_route_requires_login(self): 49 | # Ensure member route requres logged in user. 50 | response = self.client.get("/members", follow_redirects=True) 51 | self.assertIn(b"Please log in to access this page", response.data) 52 | 53 | def test_validate_success_login_form(self): 54 | # Ensure correct data validates. 55 | form = LoginForm(email="ad@min.com", password="admin_user") 56 | self.assertTrue(form.validate()) 57 | 58 | def test_validate_invalid_email_format(self): 59 | # Ensure invalid email format throws error. 60 | form = LoginForm(email="unknown", password="example") 61 | self.assertFalse(form.validate()) 62 | 63 | def test_get_by_id(self): 64 | # Ensure id is correct for the current/logged in user. 65 | with self.client: 66 | self.client.post( 67 | "/login", 68 | data=dict(email="ad@min.com", password="admin_user"), 69 | follow_redirects=True, 70 | ) 71 | self.assertTrue(current_user.id == 1) 72 | 73 | def test_registered_on_defaults_to_datetime(self): 74 | # Ensure that registered_on is a datetime. 75 | with self.client: 76 | self.client.post( 77 | "/login", 78 | data=dict(email="ad@min.com", password="admin_user"), 79 | follow_redirects=True, 80 | ) 81 | user = User.query.filter_by(email="ad@min.com").first() 82 | self.assertIsInstance(user.registered_on, datetime.datetime) 83 | 84 | def test_check_password(self): 85 | # Ensure given password is correct after unhashing. 86 | user = User.query.filter_by(email="ad@min.com").first() 87 | self.assertTrue( 88 | bcrypt.check_password_hash(user.password, "admin_user") 89 | ) 90 | self.assertFalse(bcrypt.check_password_hash(user.password, "foobar")) 91 | 92 | def test_validate_invalid_password(self): 93 | # Ensure user can't login when the pasword is incorrect. 94 | with self.client: 95 | response = self.client.post( 96 | "/login", 97 | data=dict(email="ad@min.com", password="foo_bar"), 98 | follow_redirects=True, 99 | ) 100 | self.assertIn(b"Invalid email and/or password.", response.data) 101 | 102 | def test_register_route(self): 103 | # Ensure about route behaves correctly. 104 | response = self.client.get("/register", follow_redirects=True) 105 | self.assertIn(b"

Register

\n", response.data) 106 | 107 | def test_user_registration(self): 108 | # Ensure registration behaves correctlys. 109 | with self.client: 110 | response = self.client.post( 111 | "/register", 112 | data=dict( 113 | email="test@tester.com", 114 | password="testing", 115 | confirm="testing", 116 | ), 117 | follow_redirects=True, 118 | ) 119 | self.assertIn(b"Welcome", response.data) 120 | self.assertTrue(current_user.email == "test@tester.com") 121 | self.assertTrue(current_user.is_active()) 122 | self.assertEqual(response.status_code, 200) 123 | 124 | 125 | if __name__ == "__main__": 126 | unittest.main() 127 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/requirements.txt: -------------------------------------------------------------------------------- 1 | coverage>=4.5.4 2 | flake8==3.7.9 3 | Flask==1.1.1 4 | Flask-Bcrypt==0.7.1 5 | Flask-Bootstrap==3.3.7.1 6 | Flask-DebugToolbar==0.11.0 7 | Flask-Login==0.5.0 8 | Flask-Migrate==2.5.2 9 | Flask-SQLAlchemy==2.4.1 10 | Flask-Testing==0.7.1 11 | Flask-WTF==0.14.3 12 | psycopg2-binary==2.8.4 13 | Werkzeug==0.16.1 14 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/setup-with-docker.md: -------------------------------------------------------------------------------- 1 | # Docker Setup 2 | 3 | Use this guide if you want to use Docker in your project. 4 | 5 | > Built with Docker v18.03.1-ce. 6 | 7 | ## Getting Started 8 | 9 | Update the environment variables in *docker-compose.yml*, and then build the images and spin up the containers: 10 | 11 | ```sh 12 | $ docker-compose up -d --build 13 | ``` 14 | 15 | By default the app is set to use the production configuration. If you would like to use the development configuration, you can alter the `APP_SETTINGS` environment variable: 16 | 17 | ``` 18 | APP_SETTINGS="project.server.config.DevelopmentConfig" 19 | ``` 20 | 21 | 22 | Create the database: 23 | - 24 | ```sh 25 | $ docker-compose run web python manage.py create-db 26 | $ docker-compose run web python manage.py db init 27 | $ docker-compose run web python manage.py db migrate 28 | $ docker-compose run web python manage.py create-admin 29 | $ docker-compose run web python manage.py create-data 30 | ``` 31 | 32 | Access the application at the address [http://localhost:5002/](http://localhost:5002/) 33 | 34 | ### Testing 35 | 36 | Test without coverage: 37 | 38 | ```sh 39 | $ docker-compose run web python manage.py test 40 | ``` 41 | 42 | Test with coverage: 43 | 44 | ```sh 45 | $ docker-compose run web python manage.py cov 46 | ``` 47 | 48 | Lint: 49 | 50 | ```sh 51 | $ docker-compose run web flake8 project 52 | ``` 53 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/setup-without-docker.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Use this guide if you do NOT want to use Docker in your project. 4 | 5 | ## Getting Started 6 | 7 | Create and activate a virtual environment, and then install the requirements. 8 | 9 | ### Set Environment Variables 10 | 11 | Update *project/server/config.py*, and then run: 12 | 13 | ```sh 14 | $ export APP_NAME="{{cookiecutter.app_name}}" 15 | $ export APP_SETTINGS="project.server.config.ProductionConfig" 16 | $ export FLASK_DEBUG=0 17 | ``` 18 | By default the app is set to use the production configuration. If you would like to use the development configuration, you can alter the `APP_SETTINGS` environment variable: 19 | 20 | ```sh 21 | $ export APP_SETTINGS="project.server.config.DevelopmentConfig" 22 | ``` 23 | 24 | Using [Pipenv](https://docs.pipenv.org/) or [python-dotenv](https://github.com/theskumar/python-dotenv)? Use the *.env* file to set environment variables: 25 | 26 | ``` 27 | APP_NAME="{{cookiecutter.app_name}}" 28 | APP_SETTINGS="project.server.config.DevelopmentConfig" 29 | FLASK_DEBUG=1 30 | ``` 31 | 32 | ### Create DB 33 | 34 | ```sh 35 | $ python manage.py create-db 36 | $ python manage.py db init 37 | $ python manage.py db migrate 38 | $ python manage.py create-admin 39 | $ python manage.py create-data 40 | ``` 41 | 42 | ### Run the Application 43 | 44 | 45 | ```sh 46 | $ python manage.py run 47 | ``` 48 | 49 | Access the application at the address [http://localhost:5000/](http://localhost:5000/) 50 | 51 | ### Testing 52 | 53 | Without coverage: 54 | 55 | ```sh 56 | $ python manage.py test 57 | ``` 58 | 59 | With coverage: 60 | 61 | ```sh 62 | $ python manage.py cov 63 | ``` 64 | 65 | Run flake8 on the app: 66 | 67 | ```sh 68 | $ python manage.py flake 69 | ``` 70 | 71 | or 72 | 73 | ```sh 74 | $ flake8 project 75 | ``` 76 | -------------------------------------------------------------------------------- /{{cookiecutter.app_slug}}/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=79 3 | exclude=migrations 4 | --------------------------------------------------------------------------------