├── .gitignore ├── README.md ├── flask_skeleton.py ├── requirements.txt ├── skeleton ├── .python-version ├── .travis.yml ├── LICENSE ├── README.md ├── 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 │ │ ├── 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 └── templates ├── .gitignore ├── brief.jinja2 └── config.jinja2 /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | *.pyc 3 | __pycache__/ 4 | *.DS_Store 5 | _old 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scaffold a Flask Project 2 | 3 | This article details how to build a scaffolding tool used to generate a Flask boilerplate. 4 | 5 | Blog post: https://realpython.com/blog/python/scaffold-a-flask-project/ 6 | -------------------------------------------------------------------------------- /flask_skeleton.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import os 5 | import argparse 6 | import jinja2 7 | import codecs 8 | import subprocess 9 | import shutil 10 | 11 | if sys.version_info < (3, 0): 12 | from shutilwhich import which 13 | else: 14 | from shutil import which 15 | 16 | import platform 17 | 18 | 19 | # Globals # 20 | 21 | cwd = os.getcwd() 22 | script_dir = os.path.dirname(os.path.realpath(__file__)) 23 | 24 | # Jinja2 environment 25 | template_loader = jinja2.FileSystemLoader( 26 | searchpath=os.path.join(script_dir, "templates")) 27 | template_env = jinja2.Environment(loader=template_loader) 28 | 29 | 30 | def get_arguments(argv): 31 | parser = argparse.ArgumentParser(description='Scaffold a Flask Skeleton.') 32 | parser.add_argument('appname', help='The application name') 33 | parser.add_argument('-s', '--skeleton', help='The skeleton folder to use.') 34 | parser.add_argument('-b', '--bower', help='Install dependencies via bower') 35 | parser.add_argument('-v', '--virtualenv', action='store_true') 36 | parser.add_argument('-g', '--git', action='store_true') 37 | args = parser.parse_args() 38 | return args 39 | 40 | 41 | def generate_brief(args): 42 | template_var = { 43 | 'pyversion': platform.python_version(), 44 | 'appname': args.appname, 45 | 'bower': args.bower, 46 | 'virtualenv': args.virtualenv, 47 | 'skeleton': args.skeleton, 48 | 'path': os.path.join(cwd, args.appname), 49 | 'git': args.git 50 | } 51 | template = template_env.get_template('brief.jinja2') 52 | return template.render(template_var) 53 | 54 | 55 | def main(args): 56 | 57 | print("\nScaffolding...") 58 | 59 | # Variables # 60 | 61 | appname = args.appname 62 | fullpath = os.path.join(cwd, appname) 63 | skeleton_dir = args.skeleton 64 | 65 | # Tasks # 66 | 67 | # Copy files and folders 68 | print("Copying files and folders...") 69 | shutil.copytree(os.path.join(script_dir, skeleton_dir), fullpath) 70 | 71 | # Create config.py 72 | print("Creating the config...") 73 | secret_key = codecs.encode(os.urandom(32), 'hex').decode('utf-8') 74 | template = template_env.get_template('config.jinja2') 75 | template_var = { 76 | 'secret_key': secret_key, 77 | } 78 | with open(os.path.join(fullpath, 'project', 'config.py'), 'w') as fd: 79 | fd.write(template.render(template_var)) 80 | 81 | # Add bower dependencies 82 | if args.bower: 83 | print("Adding bower dependencies...") 84 | bower = args.bower.split(',') 85 | bower_exe = which('bower') 86 | if bower_exe: 87 | os.chdir(os.path.join(fullpath, 'project', 'client', 'static')) 88 | for dependency in bower: 89 | output, error = subprocess.Popen( 90 | [bower_exe, 'install', dependency], 91 | stdout=subprocess.PIPE, 92 | stderr=subprocess.PIPE 93 | ).communicate() 94 | if error: 95 | print("An error occurred with Bower") 96 | print(error) 97 | else: 98 | print("Could not find bower. Ignoring.") 99 | 100 | # Add a virtualenv 101 | virtualenv = args.virtualenv 102 | if virtualenv: 103 | print("Adding a virtualenv...") 104 | virtualenv_exe = which('pyvenv') 105 | if virtualenv_exe: 106 | output, error = subprocess.Popen( 107 | [virtualenv_exe, os.path.join(fullpath, 'env')], 108 | stdout=subprocess.PIPE, 109 | stderr=subprocess.PIPE 110 | ).communicate() 111 | if error: 112 | with open('virtualenv_error.log', 'w') as fd: 113 | fd.write(error.decode('utf-8')) 114 | print("An error occurred with virtualenv") 115 | sys.exit(2) 116 | venv_bin = os.path.join(fullpath, 'env/bin') 117 | output, error = subprocess.Popen( 118 | [ 119 | os.path.join(venv_bin, 'pip'), 120 | 'install', 121 | '-r', 122 | os.path.join(fullpath, 'requirements.txt') 123 | ], 124 | stdout=subprocess.PIPE, 125 | stderr=subprocess.PIPE 126 | ).communicate() 127 | if error: 128 | with open('pip_error.log', 'w') as fd: 129 | fd.write(error.decode('utf-8')) 130 | sys.exit(2) 131 | else: 132 | print("Could not find virtualenv executable. Ignoring") 133 | 134 | # Git init 135 | if args.git: 136 | print("Initializing Git...") 137 | output, error = subprocess.Popen( 138 | ['git', 'init', fullpath], 139 | stdout=subprocess.PIPE, 140 | stderr=subprocess.PIPE 141 | ).communicate() 142 | if error: 143 | with open('git_error.log', 'w') as fd: 144 | fd.write(error.decode('utf-8')) 145 | print("Error with git init") 146 | sys.exit(2) 147 | shutil.copyfile( 148 | os.path.join(script_dir, 'templates', '.gitignore'), 149 | os.path.join(fullpath, '.gitignore') 150 | ) 151 | 152 | 153 | if __name__ == '__main__': 154 | arguments = get_arguments(sys.argv) 155 | print(generate_brief(arguments)) 156 | if sys.version_info < (3, 0): 157 | input = raw_input 158 | proceed = input("\nProceed (yes/no)? ") 159 | valid = ["yes", "y", "no", "n"] 160 | while True: 161 | if proceed.lower() in valid: 162 | if proceed.lower() == "yes" or proceed.lower() == "y": 163 | main(arguments) 164 | print("Done!") 165 | break 166 | else: 167 | print("Goodbye!") 168 | break 169 | else: 170 | print("Please respond with 'yes' or 'no' (or 'y' or 'n').") 171 | proceed = input("\nProceed (yes/no)? ") 172 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2==2.8 2 | MarkupSafe==0.23 3 | shutilwhich==1.1.0 4 | -------------------------------------------------------------------------------- /skeleton/.python-version: -------------------------------------------------------------------------------- 1 | 3.5.2 2 | -------------------------------------------------------------------------------- /skeleton/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.5" 5 | - "3.4" 6 | - "2.7" 7 | 8 | install: 9 | - pip install -r requirements.txt 10 | - pip install coveralls 11 | 12 | script: 13 | - python manage.py cov 14 | 15 | after_success: 16 | coveralls 17 | -------------------------------------------------------------------------------- /skeleton/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /skeleton/README.md: -------------------------------------------------------------------------------- 1 | # Flask Skeleton 2 | 3 | Flask starter project... 4 | 5 | [![Build Status](https://travis-ci.org/realpython/flask-skeleton.svg?branch=master)](https://travis-ci.org/realpython/flask-skeleton) 6 | 7 | ## Quick Start 8 | 9 | ### Basics 10 | 11 | 1. Activate a virtualenv 12 | 1. Install the requirements 13 | 14 | ### Set Environment Variables 15 | 16 | Update *project/server/config.py*, and then run: 17 | 18 | ```sh 19 | $ export APP_SETTINGS="project.server.config.DevelopmentConfig" 20 | ``` 21 | 22 | or 23 | 24 | ```sh 25 | $ export APP_SETTINGS="project.server.config.ProductionConfig" 26 | ``` 27 | 28 | ### Create DB 29 | 30 | ```sh 31 | $ python manage.py create_db 32 | $ python manage.py db init 33 | $ python manage.py db migrate 34 | $ python manage.py create_admin 35 | $ python manage.py create_data 36 | ``` 37 | 38 | ### Run the Application 39 | 40 | ```sh 41 | $ python manage.py runserver 42 | ``` 43 | 44 | So access the application at the address [http://localhost:5000/](http://localhost:5000/) 45 | 46 | > Want to specify a different port? 47 | 48 | > ```sh 49 | > $ python manage.py runserver -h 0.0.0.0 -p 8080 50 | > ``` 51 | 52 | ### Testing 53 | 54 | Without coverage: 55 | 56 | ```sh 57 | $ python manage.py test 58 | ``` 59 | 60 | With coverage: 61 | 62 | ```sh 63 | $ python manage.py cov 64 | ``` 65 | -------------------------------------------------------------------------------- /skeleton/manage.py: -------------------------------------------------------------------------------- 1 | # manage.py 2 | 3 | 4 | import os 5 | import unittest 6 | import coverage 7 | 8 | from flask_script import Manager 9 | from flask_migrate import Migrate, MigrateCommand 10 | 11 | COV = coverage.coverage( 12 | branch=True, 13 | include='project/*', 14 | omit=[ 15 | 'project/tests/*', 16 | 'project/server/config.py', 17 | 'project/server/*/__init__.py' 18 | ] 19 | ) 20 | COV.start() 21 | 22 | from project.server import app, db 23 | from project.server.models import User 24 | 25 | 26 | migrate = Migrate(app, db) 27 | manager = Manager(app) 28 | 29 | # migrations 30 | manager.add_command('db', MigrateCommand) 31 | 32 | 33 | @manager.command 34 | def test(): 35 | """Runs the unit tests without test coverage.""" 36 | tests = unittest.TestLoader().discover('project/tests', pattern='test*.py') 37 | result = unittest.TextTestRunner(verbosity=2).run(tests) 38 | if result.wasSuccessful(): 39 | return 0 40 | return 1 41 | 42 | 43 | @manager.command 44 | def cov(): 45 | """Runs the unit tests with coverage.""" 46 | tests = unittest.TestLoader().discover('project/tests') 47 | result = unittest.TextTestRunner(verbosity=2).run(tests) 48 | if result.wasSuccessful(): 49 | COV.stop() 50 | COV.save() 51 | print('Coverage Summary:') 52 | COV.report() 53 | basedir = os.path.abspath(os.path.dirname(__file__)) 54 | covdir = os.path.join(basedir, 'tmp/coverage') 55 | COV.html_report(directory=covdir) 56 | print('HTML version: file://%s/index.html' % covdir) 57 | COV.erase() 58 | return 0 59 | return 1 60 | 61 | 62 | @manager.command 63 | def create_db(): 64 | """Creates the db tables.""" 65 | db.create_all() 66 | 67 | 68 | @manager.command 69 | def drop_db(): 70 | """Drops the db tables.""" 71 | db.drop_all() 72 | 73 | 74 | @manager.command 75 | def create_admin(): 76 | """Creates the admin user.""" 77 | db.session.add(User(email='ad@min.com', password='admin', admin=True)) 78 | db.session.commit() 79 | 80 | 81 | @manager.command 82 | def create_data(): 83 | """Creates sample data.""" 84 | pass 85 | 86 | 87 | if __name__ == '__main__': 88 | manager.run() 89 | -------------------------------------------------------------------------------- /skeleton/project/__init__.py: -------------------------------------------------------------------------------- 1 | # project/__init__.py 2 | -------------------------------------------------------------------------------- /skeleton/project/client/static/main.css: -------------------------------------------------------------------------------- 1 | /* custom css */ 2 | 3 | .site-content { 4 | padding-top: 75px; 5 | } -------------------------------------------------------------------------------- /skeleton/project/client/static/main.js: -------------------------------------------------------------------------------- 1 | // custom javascript 2 | 3 | $( document ).ready(function() { 4 | console.log('Sanity Check!'); 5 | }); -------------------------------------------------------------------------------- /skeleton/project/client/templates/_base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flask Skeleton{% 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 | 43 | {% block content %}{% endblock %} 44 | 45 |
46 | 47 | 48 | {% if error %} 49 |

Error: {{ error }}

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

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

401

9 |

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

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

401

9 |

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

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

404

9 |

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

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

500

9 |

Sorry. Something went terribly wrong. Go home.

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

About

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

Welcome!

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

Please login

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

27 |

Need to Register?

28 |
29 | 30 |
31 | 32 | 33 | {% endblock content %} 34 | -------------------------------------------------------------------------------- /skeleton/project/client/templates/user/members.html: -------------------------------------------------------------------------------- 1 | {% extends "_base.html" %} 2 | {% block content %} 3 |

Welcome, {{ current_user.email }}!

4 |

This is the members-only page.

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

Please Register

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

28 |

Already have an account? Sign in.

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

Please Register

\n', response.data) 99 | 100 | def test_user_registration(self): 101 | # Ensure registration behaves correctlys. 102 | with self.client: 103 | response = self.client.post( 104 | '/register', 105 | data=dict(email="test@tester.com", password="testing", 106 | confirm="testing"), 107 | follow_redirects=True 108 | ) 109 | self.assertIn(b'Welcome', response.data) 110 | self.assertTrue(current_user.email == "test@tester.com") 111 | self.assertTrue(current_user.is_active()) 112 | self.assertEqual(response.status_code, 200) 113 | 114 | 115 | if __name__ == '__main__': 116 | unittest.main() 117 | -------------------------------------------------------------------------------- /skeleton/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.8.6 2 | bcrypt==3.1.0 3 | blinker==1.4 4 | cffi==1.7.0 5 | click==6.6 6 | coverage==4.1 7 | dominate==2.2.1 8 | Flask==0.11.1 9 | Flask-Bcrypt==0.7.1 10 | Flask-Bootstrap==3.3.6.0 11 | Flask-DebugToolbar==0.10.0 12 | Flask-Login==0.3.2 13 | Flask-Migrate==1.8.1 14 | Flask-Script==2.0.5 15 | Flask-SQLAlchemy==2.1 16 | Flask-Testing==0.5.0 17 | Flask-WTF==0.12 18 | itsdangerous==0.24 19 | Jinja2==2.8 20 | Mako==1.0.4 21 | MarkupSafe==0.23 22 | pycparser==2.14 23 | python-editor==1.0.1 24 | six==1.10.0 25 | SQLAlchemy==1.0.14 26 | visitor==0.1.3 27 | Werkzeug==0.11.10 28 | WTForms==2.1 29 | -------------------------------------------------------------------------------- /templates/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /templates/brief.jinja2: -------------------------------------------------------------------------------- 1 | Welcome! The following settings will be used to create your application: 2 | 3 | Python Version: {{ pyversion }} 4 | Project Name: {{ appname }} 5 | Project Path: {{ path }} 6 | Virtualenv: {% if virtualenv %}Enabled{% else %}Disabled{% endif %} 7 | Skeleton: {{ skeleton }} 8 | Git: {% if git %}Yes{% else %}{{ disabled }}No{% endif %} 9 | Bower: {% if bower %}Enabled{% else %}Disabled{% endif %} 10 | {% if bower %}Bower Dependencies: {% for dependency in bower %}{{ dependency }}{% endfor %}{% endif %} 11 | -------------------------------------------------------------------------------- /templates/config.jinja2: -------------------------------------------------------------------------------- 1 | # config.jinja2 2 | 3 | import os 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class BaseConfig(object): 8 | """Base configuration.""" 9 | SECRET_KEY = '{{ secret_key }}' 10 | DEBUG = False 11 | BCRYPT_LOG_ROUNDS = 13 12 | WTF_CSRF_ENABLED = True 13 | DEBUG_TB_ENABLED = False 14 | DEBUG_TB_INTERCEPT_REDIRECTS = False 15 | 16 | 17 | class DevelopmentConfig(BaseConfig): 18 | """Development configuration.""" 19 | DEBUG = True 20 | BCRYPT_LOG_ROUNDS = 13 21 | WTF_CSRF_ENABLED = False 22 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'dev.sqlite') 23 | DEBUG_TB_ENABLED = True 24 | 25 | 26 | class TestingConfig(BaseConfig): 27 | """Testing configuration.""" 28 | DEBUG = True 29 | TESTING = True 30 | BCRYPT_LOG_ROUNDS = 13 31 | WTF_CSRF_ENABLED = False 32 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' 33 | DEBUG_TB_ENABLED = False 34 | 35 | 36 | class ProductionConfig(BaseConfig): 37 | """Production configuration.""" 38 | SECRET_KEY = '{{ secret_key }}' 39 | DEBUG = False 40 | SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/example' 41 | DEBUG_TB_ENABLED = False 42 | --------------------------------------------------------------------------------