├── .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 | [](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 |
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/skeleton/project/client/templates/main/home.html:
--------------------------------------------------------------------------------
1 | {% extends "_base.html" %}
2 |
3 | {% block content %}
4 |
5 |
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 |
9 |
10 |
11 |
12 |
13 |
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 |
9 |
10 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------