main
Michael Herman 2015-01-21 09:32:17 -07:00
commit 82dcc03a07
36 changed files with 910 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
env
temp
tmp
migrations
*.pyc
*.sqlite
*.coverage
.DS_Store

7
.travis.yml Normal file
View File

@ -0,0 +1,7 @@
# Config file for testing at travis-ci.org
language: python
python:
- "2.7"
install: "pip install -r requirements.txt"
script: nosetests

35
README.md Normal file
View File

@ -0,0 +1,35 @@
# Flask Skeleton
Flask starter...
## Quick Start
### Set Environment Variables
Update the configuration setting files in "/project/config" and then run:
```sh
$ export APP_SETTINGS="project.config.development_config"
```
or
```sh
$ export APP_SETTINGS="project.config.production_config"
```
### Create DB
```sh
$ python manage.py create_db
$ python manage.py db init
$ python manage.py db migrate
$ python manage.py create_admin
$ python manage.py create_data
```
### Run the Application
```sh
$ python manage.py runserver
```

BIN
dev.db Normal file

Binary file not shown.

77
manage.py Normal file
View File

@ -0,0 +1,77 @@
# manage.py
import os
import unittest
import coverage
from flask.ext.script import Manager
from flask.ext.migrate import Migrate, MigrateCommand
from project import app, db
from project.models import User
migrate = Migrate(app, db)
manager = Manager(app)
# migrations
manager.add_command('db', MigrateCommand)
@manager.command
def test():
"""Runs the unit tests without coverage."""
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
@manager.command
def cov():
"""Runs the unit tests with coverage."""
cov = coverage.coverage(
branch=True,
include='project/*',
omit=['*/__init__.py', '*/config/*']
)
cov.start()
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
cov.stop()
cov.save()
print 'Coverage Summary:'
cov.report()
basedir = os.path.abspath(os.path.dirname(__file__))
covdir = os.path.join(basedir, 'tmp/coverage')
cov.html_report(directory=covdir)
print('HTML version: file://%s/index.html' % covdir)
cov.erase()
@manager.command
def create_db():
"""Creates the db tables."""
db.create_all()
@manager.command
def drop_db():
"""Drops the db tables."""
db.drop_all()
@manager.command
def create_admin():
"""Creates the admin user."""
db.session.add(User(email='ad@min.com', password='admin', admin=True))
db.session.commit()
@manager.command
def create_data():
"""Creates sample data."""
pass
if __name__ == '__main__':
manager.run()

79
project/__init__.py Normal file
View File

@ -0,0 +1,79 @@
# project/__init__.py
#################
#### imports ####
#################
import os
from flask import Flask, render_template
from flask.ext.login import LoginManager
from flask.ext.bcrypt import Bcrypt
from flask.ext.debugtoolbar import DebugToolbarExtension
from flask_bootstrap import Bootstrap
from flask.ext.sqlalchemy import SQLAlchemy
################
#### config ####
################
app = Flask(__name__)
app.config.from_object(os.environ['APP_SETTINGS'])
####################
#### extensions ####
####################
login_manager = LoginManager()
login_manager.init_app(app)
bcrypt = Bcrypt(app)
toolbar = DebugToolbarExtension(app)
bootstrap = Bootstrap(app)
db = SQLAlchemy(app)
###################
### blueprints ####
###################
from project.user.views import user_blueprint
from project.main.views import main_blueprint
app.register_blueprint(user_blueprint)
app.register_blueprint(main_blueprint)
###################
### flask-login ####
###################
from models import User
login_manager.login_view = "user.login"
login_manager.login_message_category = 'danger'
@login_manager.user_loader
def load_user(user_id):
return User.query.filter(User.id == int(user_id)).first()
########################
#### error handlers ####
########################
@app.errorhandler(403)
def forbidden_page(error):
return render_template("errors/403.html"), 403
@app.errorhandler(404)
def page_not_found(error):
return render_template("errors/404.html"), 404
@app.errorhandler(500)
def server_error_page(error):
return render_template("errors/500.html"), 500

View File

@ -0,0 +1 @@
# project/config/__init__.py

View File

@ -0,0 +1,14 @@
# project/config/development_config.py
import os
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
SECRET_KEY = 'my_precious'
DEBUG = True
BCRYPT_LOG_ROUNDS = 13
WTF_CSRF_ENABLED = False
DEBUG_TB_ENABLED = True
DEBUG_TB_INTERCEPT_REDIRECTS = False
CACHE_TYPE = 'simple'
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'dev.sqlite')

View File

@ -0,0 +1,11 @@
# project/config/production_config.py
SECRET_KEY = 'my_precious'
DEBUG = False
BCRYPT_LOG_ROUNDS = 13
WTF_CSRF_ENABLED = True
DEBUG_TB_ENABLED = False
DEBUG_TB_INTERCEPT_REDIRECTS = False
CACHE_TYPE = 'simple'
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/example'

View File

@ -0,0 +1,12 @@
# project/config/test_config.py
TESTING = True
SECRET_KEY = 'my_precious'
DEBUG = True
BCRYPT_LOG_ROUNDS = 1
WTF_CSRF_ENABLED = False
DEBUG_TB_ENABLED = False
DEBUG_TB_INTERCEPT_REDIRECTS = False
CACHE_TYPE = 'simple'
SQLALCHEMY_DATABASE_URI = 'sqlite://'

1
project/main/__init__.py Normal file
View File

@ -0,0 +1 @@
# project/public/__init__.py

30
project/main/views.py Normal file
View File

@ -0,0 +1,30 @@
# project/main/views.py
#################
#### imports ####
#################
from flask import render_template, Blueprint
################
#### config ####
################
main_blueprint = Blueprint('main', __name__,)
################
#### routes ####
################
@main_blueprint.route('/')
def home():
return render_template('main/home.html')
@main_blueprint.route("/about/")
def about():
return render_template("main/about.html")

38
project/models.py Normal file
View File

@ -0,0 +1,38 @@
# project/models.py
import datetime
from project import db, bcrypt
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
email = db.Column(db.String(255), unique=True, nullable=False)
password = db.Column(db.String(255), nullable=False)
registered_on = db.Column(db.DateTime, nullable=False)
admin = db.Column(db.Boolean, nullable=False, default=False)
def __init__(self, email, password, admin=False):
self.email = email
self.password = bcrypt.generate_password_hash(password)
self.registered_on = datetime.datetime.now()
self.admin = admin
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return unicode(self.id)
def __repr__(self):
return '<User {0}>'.format(self.email)

5
project/static/main.css Normal file
View File

@ -0,0 +1,5 @@
/* custom css */
.site-content {
padding-top: 75px;
}

1
project/static/main.js Normal file
View File

@ -0,0 +1 @@
// custom javascript

View File

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Flask Skeleton{% block title %}{% endblock %}</title>
<!-- meta -->
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width,initial-scale=1">
<!-- styles -->
<link href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.1/yeti/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="{{url_for('static', filename='main.css')}}" rel="stylesheet" media="screen">
{% block css %}{% endblock %}
</head>
<body>
{% include 'header.html' %}
<div class="site-content">
<div class="container">
<!-- messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<br>
<div class="row">
<div class="col-md-12">
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
<a class="close" title="Close" href="#" data-dismiss="alert">&times;</a>
{{message}}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
<!-- child template -->
{% block content %}{% endblock %}
<br>
<!-- errors -->
{% if error %}
<p class="error"><strong>Error:</strong> {{ error }}</p>
{% endif %}
</div>
</div>
<br><br>
{% include 'footer.html' %}
<!-- scripts -->
<script src="//code.jquery.com/jquery-1.11.2.min.js" type="text/javascript"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js" type="text/javascript"></script>
<script src="{{url_for('static', filename='js/main.js')}}" type="text/javascript"></script>
{% block js %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,12 @@
{% extends "_base.html" %}
{% block page_title %}- Unauthorized{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="text-center">
<h1>401</h1>
<p>You are not authorized to view this page. Please <a href="{{ url_for('main.login')}}">log in</a>.</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "_base.html" %}
{% block page_title %}- Page Not Found{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="text-center">
<h1>404</h1>
<p>Sorry. The requested page doesn't exist. Go <a href="{{ url_for('main.home')}}">home</a>.</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "_base.html" %}
{% block page_title %}- Server Error{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="text-center">
<h1>500</h1>
<p>Sorry. Something went terribly wrong. Go <a href="{{ url_for('main.home')}}">home</a>.</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,7 @@
<footer>
<div class="container">
<small>
<span>© <a href="mailto:michael@realpython.com">Michael Herman</a></span>
</small>
</div>
</footer>

View File

@ -0,0 +1,35 @@
<header class="site-header">
<!-- Navigation -->
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ url_for('main.home') }}">Flask-Skeleton</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li><a href="{{ url_for('main.about') }}">About</a></li>
{% if current_user.is_authenticated() %}
<li><a href="{{ url_for('user.members') }}">Members</a></li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated() %}
<li><a href="{{ url_for('user.logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ url_for('user.login') }}"><span class="glyphicon glyphicon-user"></span>&nbsp;Register/Login</a></li>
{% endif %}
</ul>
</div>
<!-- /.navbar-collapse -->
</div>
<!-- /.container -->
</nav>
</header>

View File

@ -0,0 +1,11 @@
{% extends "_base.html" %}
{% block content %}
<div class="body-content">
<div class="row">
<h1>About</h1>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "_base.html" %}
{% block content %}
<div class="body-content">
<div class="row">
<h1>Welcome!</h1>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends '_base.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
<header class="content-header">
<h1>Please login</h1>
</header>
<br>
<form class="form" role="form" method="post" action="">
{{ form.csrf_token }}
{{ form.hidden_tag() }}
{{ wtf.form_errors(form, hiddens="only") }}
<div class="col-lg-4 col-sm-4">
{{ wtf.form_field(form.email) }}
{{ wtf.form_field(form.password) }}
<button class="btn btn-success" type="submit">Sign In!</button>
<br><br>
<p>Need to <a href="{{ url_for('user.register') }}">Register</a>?</p>
</div>
</form>
{% endblock content %}

View File

@ -0,0 +1,5 @@
{% extends "_base.html" %}
{% block content %}
<h1>Welcome, <em>{{ current_user.email }}</em>!</h1>
<h3>This is the members-only page.</h3>
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends '_base.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
<header class="content-header">
<h1>Please Register</h1>
</header>
<br>
<form class="form" role="form" method="post" action="">
{{ form.csrf_token }}
{{ form.hidden_tag() }}
{{ wtf.form_errors(form, hiddens="only") }}
<div class="col-lg-4 col-sm-4">
{{ wtf.form_field(form.email) }}
{{ wtf.form_field(form.password) }}
{{ wtf.form_field(form.confirm) }}
<button class="btn btn-success" type="submit">Register!</button>
<br><br>
<p>Already have an account? <a href="{{ url_for('user.login') }}">Sign in</a>.</p>
</div>
</form>
{% endblock content %}

1
project/user/__init__.py Normal file
View File

@ -0,0 +1 @@
# project/user/__init__.py

28
project/user/forms.py Normal file
View File

@ -0,0 +1,28 @@
# project/user/forms.py
from flask_wtf import Form
from wtforms import TextField, PasswordField
from wtforms.validators import DataRequired, Email, Length, EqualTo
class LoginForm(Form):
email = TextField('Email Address', [DataRequired(), Email()])
password = PasswordField('Password', [DataRequired()])
class RegisterForm(Form):
email = TextField(
'Email Address',
validators=[DataRequired(), Email(message=None), Length(min=6, max=40)])
password = PasswordField(
'Password',
validators=[DataRequired(), Length(min=6, max=25)]
)
confirm = PasswordField(
'Confirm password',
validators=[
DataRequired(),
EqualTo('password', message='Passwords must match.')
]
)

73
project/user/views.py Normal file
View File

@ -0,0 +1,73 @@
# project/user/views.py
#################
#### imports ####
#################
from flask import render_template, Blueprint, url_for, \
redirect, flash, request
from flask.ext.login import login_user, logout_user, login_required
from project import bcrypt, db
from project.models import User
from project.user.forms import LoginForm, RegisterForm
################
#### config ####
################
user_blueprint = Blueprint('user', __name__,)
################
#### routes ####
################
@user_blueprint.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if form.validate_on_submit():
user = User(
email=form.email.data,
password=form.password.data
)
db.session.add(user)
db.session.commit()
login_user(user)
flash('Thank you for registering.', 'success')
return redirect(url_for("user.members"))
return render_template('user/register.html', form=form)
@user_blueprint.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm(request.form)
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and bcrypt.check_password_hash(
user.password, request.form['password']):
login_user(user)
flash('You are logged in. Welcome!', 'success')
return redirect(url_for('user.members'))
else:
flash('Invalid email and/or password.', 'danger')
return render_template('user/login.html', form=form)
return render_template('user/login.html', title='Please Login', form=form)
@user_blueprint.route('/logout')
@login_required
def logout():
logout_user()
flash('You were logged out. Bye!', 'success')
return redirect(url_for('main.home'))
@user_blueprint.route('/members')
@login_required
def members():
return render_template('user/members.html')

22
requirements.txt Normal file
View File

@ -0,0 +1,22 @@
Flask==0.10.1
Flask-Bcrypt==0.6.0
Flask-Bootstrap==3.3.0.1
Flask-DebugToolbar==0.9.0
Flask-Login==0.2.11
Flask-Migrate==1.2.0
Flask-SQLAlchemy==2.0
Flask-Script==2.0.5
Flask-Testing==0.4.2
Flask-WTF==0.10.3
Jinja2==2.7.3
Mako==1.0.0
MarkupSafe==0.23
SQLAlchemy==0.9.8
WTForms==2.0.2
Werkzeug==0.9.6
alembic==0.7.4
blinker==1.3
coverage==4.0a2
itsdangerous==0.24
py-bcrypt==0.4
wsgiref==0.1.2

0
tests/__init__.py Normal file
View File

24
tests/base.py Normal file
View File

@ -0,0 +1,24 @@
# tests/base.py
from flask.ext.testing import TestCase
from project import app, db
from project.models import User
class BaseTestCase(TestCase):
def create_app(self):
app.config.from_object('project.config.test_config')
return app
def setUp(self):
db.create_all()
user = User(email="ad@min.com", password="admin_user")
db.session.add(user)
db.session.commit()
def tearDown(self):
db.session.remove()
db.drop_all()

1
tests/helpers.py Normal file
View File

@ -0,0 +1 @@
# tests/helpers.py

54
tests/test__config.py Normal file
View File

@ -0,0 +1,54 @@
# tests/test_config.py
import unittest
from flask import current_app
from flask.ext.testing import TestCase
from project import app
class TestDevelopmentConfig(TestCase):
def create_app(self):
app.config.from_object('project.config.development_config')
return app
def test_app_is_development(self):
self.assertFalse(current_app.config['TESTING'])
self.assertTrue(app.config['DEBUG'] is True)
self.assertTrue(app.config['WTF_CSRF_ENABLED'] is False)
self.assertTrue(app.config['DEBUG_TB_ENABLED'] is True)
self.assertFalse(current_app is None)
class TestTestingConfig(TestCase):
def create_app(self):
app.config.from_object('project.config.test_config')
return app
def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])
self.assertTrue(app.config['DEBUG'] is True)
self.assertTrue(app.config['BCRYPT_LOG_ROUNDS'] == 1)
self.assertTrue(app.config['WTF_CSRF_ENABLED'] is False)
class TestProductionConfig(TestCase):
def create_app(self):
app.config.from_object('project.config.production_config')
return app
def test_app_is_production(self):
self.assertFalse(current_app.config['TESTING'])
self.assertTrue(app.config['DEBUG'] is False)
self.assertTrue(app.config['DEBUG_TB_ENABLED'] is False)
self.assertTrue(app.config['WTF_CSRF_ENABLED'] is True)
self.assertTrue(app.config['BCRYPT_LOG_ROUNDS'] == 13)
if __name__ == '__main__':
unittest.main()

32
tests/test_main.py Normal file
View File

@ -0,0 +1,32 @@
# tests/test_main.py
import unittest
from base import BaseTestCase
class TestMainBlueprint(BaseTestCase):
def test_index(self):
# Ensure Flask is setup.
response = self.client.get('/', follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertIn('Welcome!', response.data)
self.assertIn('Register/Login', response.data)
def test_about(self):
# Ensure about route behaves correctly.
response = self.client.get('/about', follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertIn('About', response.data)
def test_404(self):
# Ensure 404 error is handled.
response = self.client.get('/404')
self.assert404(response)
self.assertTemplateUsed('errors/404.html')
if __name__ == '__main__':
unittest.main()

116
tests/test_user.py Normal file
View File

@ -0,0 +1,116 @@
# tests/test_user.py
import datetime
import unittest
from flask.ext.login import current_user
from base import BaseTestCase
from project import bcrypt
from project.models import User
from project.user.forms import LoginForm
class TestUserBlueprint(BaseTestCase):
def test_correct_login(self):
# Ensure login behaves correctly with correct credentials.
with self.client:
response = self.client.post(
'/login',
data=dict(email="ad@min.com", password="admin_user"),
follow_redirects=True
)
self.assertIn('Welcome', response.data)
self.assertIn('Logout', response.data)
self.assertIn('Members', response.data)
self.assertTrue(current_user.email == "ad@min.com")
self.assertTrue(current_user.is_active())
self.assertEqual(response.status_code, 200)
def test_logout_behaves_correctly(self):
# Ensure logout behaves correctly - regarding the session.
with self.client:
self.client.post(
'/login',
data=dict(email="ad@min.com", password="admin_user"),
follow_redirects=True
)
response = self.client.get('/logout', follow_redirects=True)
self.assertIn('You were logged out. Bye!', response.data)
self.assertFalse(current_user.is_active())
def test_logout_route_requires_login(self):
# Ensure logout route requres logged in user.
response = self.client.get('/logout', follow_redirects=True)
self.assertIn('Please log in to access this page', response.data)
def test_member_route_requires_login(self):
# Ensure member route requres logged in user.
response = self.client.get('/members', follow_redirects=True)
self.assertIn('Please log in to access this page', response.data)
def test_validate_success_login_form(self):
# Ensure correct data validates.
form = LoginForm(email='ad@min.com', password='admin_user')
self.assertTrue(form.validate())
def test_validate_invalid_email_format(self):
# Ensure invalid email format throws error.
form = LoginForm(email='unknown', password='example')
self.assertFalse(form.validate())
def test_get_by_id(self):
# Ensure id is correct for the current/logged in user.
with self.client:
self.client.post('/login', data=dict(
email='ad@min.com', password='admin_user'
), follow_redirects=True)
self.assertTrue(current_user.id == 1)
def test_registered_on_defaults_to_datetime(self):
# Ensure that registered_on is a datetime.
with self.client:
self.client.post('/login', data=dict(
email='ad@min.com', password='admin_user'
), follow_redirects=True)
user = User.query.filter_by(email='ad@min.com').first()
self.assertIsInstance(user.registered_on, datetime.datetime)
def test_check_password(self):
# Ensure given password is correct after unhashing.
user = User.query.filter_by(email='ad@min.com').first()
self.assertTrue(bcrypt.check_password_hash(user.password, 'admin_user'))
self.assertFalse(bcrypt.check_password_hash(user.password, 'foobar'))
def test_validate_invalid_password(self):
# Ensure user can't login when the pasword is incorrect.
with self.client:
response = self.client.post('/login', data=dict(
email='ad@min.com', password='foo_bar'
), follow_redirects=True)
self.assertIn('Invalid email and/or password.', response.data)
def test_register_route(self):
# Ensure about route behaves correctly.
response = self.client.get('/register', follow_redirects=True)
self.assertIn('<h1>Please Register</h1>\n', response.data)
def test_user_registration(self):
# Ensure registration behaves correctlys.
with self.client:
response = self.client.post(
'/register',
data=dict(email="test@tester.com", password="testing",
confirm="testing"),
follow_redirects=True
)
self.assertIn('Welcome', response.data)
self.assertTrue(current_user.email == "test@tester.com")
self.assertTrue(current_user.is_active())
self.assertEqual(response.status_code, 200)
if __name__ == '__main__':
unittest.main()