├── .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 | [](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 |
9 |
10 |
11 |
12 |
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 |
9 |
10 |
11 |
12 |
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 |
--------------------------------------------------------------------------------