rewrite tutorial docs and example

This commit is contained in:
David Lord 2018-02-09 14:39:05 -08:00
parent 16d83d6bb4
commit c3dd7b8e4c
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
103 changed files with 3327 additions and 2224 deletions

View file

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
"""
Blueprint Example
~~~~~~~~~~~~~~~~~
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
from flask import Flask
from simple_page.simple_page import simple_page
app = Flask(__name__)
app.register_blueprint(simple_page)
# Blueprint can be registered many times
app.register_blueprint(simple_page, url_prefix='/pages')
if __name__=='__main__':
app.run()

View file

@ -1,13 +0,0 @@
from flask import Blueprint, render_template, abort
from jinja2 import TemplateNotFound
simple_page = Blueprint('simple_page', __name__,
template_folder='templates')
@simple_page.route('/', defaults={'page': 'index'})
@simple_page.route('/<page>')
def show(page):
try:
return render_template('pages/%s.html' % page)
except TemplateNotFound:
abort(404)

View file

@ -1,5 +0,0 @@
{% extends "pages/layout.html" %}
{% block body %}
Hello
{% endblock %}

View file

@ -1,5 +0,0 @@
{% extends "pages/layout.html" %}
{% block body %}
Blueprint example page
{% endblock %}

View file

@ -1,20 +0,0 @@
<!doctype html>
<title>Simple Page Blueprint</title>
<div class="page">
<h1>This is blueprint example</h1>
<p>
A simple page blueprint is registered under / and /pages
you can access it using this URLs:
<ul>
<li><a href="{{ url_for('simple_page.show', page='hello') }}">/hello</a>
<li><a href="{{ url_for('simple_page.show', page='world') }}">/world</a>
</ul>
<p>
Also you can register the same blueprint under another path
<ul>
<li><a href="/pages/hello">/pages/hello</a>
<li><a href="/pages/world">/pages/world</a>
</ul>
{% block body %}{% endblock %}
</div>

View file

@ -1,4 +0,0 @@
{% extends "pages/layout.html" %}
{% block body %}
World
{% endblock %}

View file

@ -1,35 +0,0 @@
# -*- coding: utf-8 -*-
"""
Blueprint Example Tests
~~~~~~~~~~~~~~~~~~~~~~~
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
import pytest
import blueprintexample
@pytest.fixture
def client():
return blueprintexample.app.test_client()
def test_urls(client):
r = client.get('/')
assert r.status_code == 200
r = client.get('/hello')
assert r.status_code == 200
r = client.get('/world')
assert r.status_code == 200
# second blueprint instance
r = client.get('/pages/hello')
assert r.status_code == 200
r = client.get('/pages/world')
assert r.status_code == 200

View file

@ -1,2 +0,0 @@
flaskr.db
.eggs/

View file

@ -1,40 +0,0 @@
/ Flaskr /
a minimal blog application
~ What is Flaskr?
A sqlite powered thumble blog application
~ How do I use it?
1. edit the configuration in the factory.py file or
export a FLASKR_SETTINGS environment variable
pointing to a configuration file or pass in a
dictionary with config values using the create_app
function.
2. install the app from the root of the project directory
pip install --editable .
3. instruct flask to use the right application
export FLASK_APP="flaskr.factory:create_app()"
4. initialize the database with this command:
flask initdb
5. now you can run flaskr:
flask run
the application will greet you on
http://localhost:5000/
~ Is it tested?
You betcha. Run `python setup.py test` to see
the tests pass.

View file

@ -1,85 +0,0 @@
# -*- coding: utf-8 -*-
"""
Flaskr
~~~~~~
A microblog example application written as Flask tutorial with
Flask and sqlite3.
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
from sqlite3 import dbapi2 as sqlite3
from flask import Blueprint, request, session, g, redirect, url_for, abort, \
render_template, flash, current_app
# create our blueprint :)
bp = Blueprint('flaskr', __name__)
def connect_db():
"""Connects to the specific database."""
rv = sqlite3.connect(current_app.config['DATABASE'])
rv.row_factory = sqlite3.Row
return rv
def init_db():
"""Initializes the database."""
db = get_db()
with current_app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
def get_db():
"""Opens a new database connection if there is none yet for the
current application context.
"""
if not hasattr(g, 'sqlite_db'):
g.sqlite_db = connect_db()
return g.sqlite_db
@bp.route('/')
def show_entries():
db = get_db()
cur = db.execute('select title, text from entries order by id desc')
entries = cur.fetchall()
return render_template('show_entries.html', entries=entries)
@bp.route('/add', methods=['POST'])
def add_entry():
if not session.get('logged_in'):
abort(401)
db = get_db()
db.execute('insert into entries (title, text) values (?, ?)',
[request.form['title'], request.form['text']])
db.commit()
flash('New entry was successfully posted')
return redirect(url_for('flaskr.show_entries'))
@bp.route('/login', methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
if request.form['username'] != current_app.config['USERNAME']:
error = 'Invalid username'
elif request.form['password'] != current_app.config['PASSWORD']:
error = 'Invalid password'
else:
session['logged_in'] = True
flash('You were logged in')
return redirect(url_for('flaskr.show_entries'))
return render_template('login.html', error=error)
@bp.route('/logout')
def logout():
session.pop('logged_in', None)
flash('You were logged out')
return redirect(url_for('flaskr.show_entries'))

View file

@ -1,64 +0,0 @@
# -*- coding: utf-8 -*-
"""
Flaskr
~~~~~~
A microblog example application written as Flask tutorial with
Flask and sqlite3.
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
import os
from flask import Flask, g
from werkzeug.utils import find_modules, import_string
from flaskr.blueprints.flaskr import init_db
def create_app(config=None):
app = Flask('flaskr')
app.config.update(dict(
DATABASE=os.path.join(app.root_path, 'flaskr.db'),
DEBUG=True,
SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/',
USERNAME='admin',
PASSWORD='default'
))
app.config.update(config or {})
app.config.from_envvar('FLASKR_SETTINGS', silent=True)
register_blueprints(app)
register_cli(app)
register_teardowns(app)
return app
def register_blueprints(app):
"""Register all blueprint modules
Reference: Armin Ronacher, "Flask for Fun and for Profit" PyBay 2016.
"""
for name in find_modules('flaskr.blueprints'):
mod = import_string(name)
if hasattr(mod, 'bp'):
app.register_blueprint(mod.bp)
return None
def register_cli(app):
@app.cli.command('initdb')
def initdb_command():
"""Creates the database tables."""
init_db()
print('Initialized the database.')
def register_teardowns(app):
@app.teardown_appcontext
def close_db(error):
"""Closes the database again at the end of the request."""
if hasattr(g, 'sqlite_db'):
g.sqlite_db.close()

View file

@ -1,6 +0,0 @@
drop table if exists entries;
create table entries (
id integer primary key autoincrement,
title text not null,
'text' text not null
);

View file

@ -1,18 +0,0 @@
body { font-family: sans-serif; background: #eee; }
a, h1, h2 { color: #377BA8; }
h1, h2 { font-family: 'Georgia', serif; margin: 0; }
h1 { border-bottom: 2px solid #eee; }
h2 { font-size: 1.2em; }
.page { margin: 2em auto; width: 35em; border: 5px solid #ccc;
padding: 0.8em; background: white; }
.entries { list-style: none; margin: 0; padding: 0; }
.entries li { margin: 0.8em 1.2em; }
.entries li h2 { margin-left: -1em; }
.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; }
.add-entry dl { font-weight: bold; }
.metanav { text-align: right; font-size: 0.8em; padding: 0.3em;
margin-bottom: 1em; background: #fafafa; }
.flash { background: #CEE5F5; padding: 0.5em;
border: 1px solid #AACBE2; }
.error { background: #F0D6D6; padding: 0.5em; }

View file

@ -1,17 +0,0 @@
<!doctype html>
<title>Flaskr</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
<div class="page">
<h1>Flaskr</h1>
<div class="metanav">
{% if not session.logged_in %}
<a href="{{ url_for('flaskr.login') }}">log in</a>
{% else %}
<a href="{{ url_for('flaskr.logout') }}">log out</a>
{% endif %}
</div>
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% block body %}{% endblock %}
</div>

View file

@ -1,14 +0,0 @@
{% extends "layout.html" %}
{% block body %}
<h2>Login</h2>
{% if error %}<p class="error"><strong>Error:</strong> {{ error }}{% endif %}
<form action="{{ url_for('flaskr.login') }}" method="post">
<dl>
<dt>Username:
<dd><input type="text" name="username">
<dt>Password:
<dd><input type="password" name="password">
<dd><input type="submit" value="Login">
</dl>
</form>
{% endblock %}

View file

@ -1,21 +0,0 @@
{% extends "layout.html" %}
{% block body %}
{% if session.logged_in %}
<form action="{{ url_for('flaskr.add_entry') }}" method="post" class="add-entry">
<dl>
<dt>Title:
<dd><input type="text" size="30" name="title">
<dt>Text:
<dd><textarea name="text" rows="5" cols="40"></textarea>
<dd><input type="submit" value="Share">
</dl>
</form>
{% endif %}
<ul class="entries">
{% for entry in entries %}
<li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}</li>
{% else %}
<li><em>Unbelievable. No entries here so far</em></li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -1,2 +0,0 @@
[aliases]
test=pytest

View file

@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
"""
Flaskr Tests
~~~~~~~~~~~~
Tests the Flaskr application.
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
from setuptools import setup, find_packages
setup(
name='flaskr',
packages=find_packages(),
include_package_data=True,
install_requires=[
'flask',
],
setup_requires=[
'pytest-runner',
],
tests_require=[
'pytest',
],
)

View file

@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
"""
Flaskr Tests
~~~~~~~~~~~~
Tests the Flaskr application.
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
import os
import tempfile
import pytest
from flaskr.factory import create_app
from flaskr.blueprints.flaskr import init_db
@pytest.fixture
def app():
db_fd, db_path = tempfile.mkstemp()
config = {
'DATABASE': db_path,
'TESTING': True,
}
app = create_app(config=config)
with app.app_context():
init_db()
yield app
os.close(db_fd)
os.unlink(db_path)
@pytest.fixture
def client(app):
return app.test_client()
def login(client, username, password):
return client.post('/login', data=dict(
username=username,
password=password
), follow_redirects=True)
def logout(client):
return client.get('/logout', follow_redirects=True)
def test_empty_db(client):
"""Start with a blank database."""
rv = client.get('/')
assert b'No entries here so far' in rv.data
def test_login_logout(client, app):
"""Make sure login and logout works"""
rv = login(client, app.config['USERNAME'],
app.config['PASSWORD'])
assert b'You were logged in' in rv.data
rv = logout(client)
assert b'You were logged out' in rv.data
rv = login(client,app.config['USERNAME'] + 'x',
app.config['PASSWORD'])
assert b'Invalid username' in rv.data
rv = login(client, app.config['USERNAME'],
app.config['PASSWORD'] + 'x')
assert b'Invalid password' in rv.data
def test_messages(client, app):
"""Test that messages work"""
login(client, app.config['USERNAME'],
app.config['PASSWORD'])
rv = client.post('/add', data=dict(
title='<Hello>',
text='<strong>HTML</strong> allowed here'
), follow_redirects=True)
assert b'No entries here so far' not in rv.data
assert b'&lt;Hello&gt;' in rv.data
assert b'<strong>HTML</strong> allowed here' in rv.data

View file

@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
"""
jQuery Example
~~~~~~~~~~~~~~
A simple application that shows how Flask and jQuery get along.
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
from flask import Flask, jsonify, render_template, request
app = Flask(__name__)
@app.route('/_add_numbers')
def add_numbers():
"""Add two numbers server side, ridiculous but well..."""
a = request.args.get('a', 0, type=int)
b = request.args.get('b', 0, type=int)
return jsonify(result=a + b)
@app.route('/')
def index():
return render_template('index.html')
if __name__ == '__main__':
app.run()

View file

@ -1,33 +0,0 @@
{% extends "layout.html" %}
{% block body %}
<script type="text/javascript">
$(function() {
var submit_form = function(e) {
$.getJSON($SCRIPT_ROOT + '/_add_numbers', {
a: $('input[name="a"]').val(),
b: $('input[name="b"]').val()
}, function(data) {
$('#result').text(data.result);
$('input[name=a]').focus().select();
});
return false;
};
$('a#calculate').bind('click', submit_form);
$('input[type=text]').bind('keydown', function(e) {
if (e.keyCode == 13) {
submit_form(e);
}
});
$('input[name=a]').focus();
});
</script>
<h1>jQuery Example</h1>
<p>
<input type="text" size="5" name="a"> +
<input type="text" size="5" name="b"> =
<span id="result">?</span>
<p><a href=# id="calculate">calculate server side</a>
{% endblock %}

View file

@ -1,8 +0,0 @@
<!doctype html>
<title>jQuery Example</title>
<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript">
var $SCRIPT_ROOT = {{ request.script_root|tojson|safe }};
</script>
{% block body %}{% endblock %}

View file

@ -1,2 +0,0 @@
minitwit.db
.eggs/

View file

@ -1,3 +0,0 @@
graft minitwit/templates
graft minitwit/static
include minitwit/schema.sql

View file

@ -1,39 +0,0 @@
/ MiniTwit /
because writing todo lists is not fun
~ What is MiniTwit?
A SQLite and Flask powered twitter clone
~ How do I use it?
1. edit the configuration in the minitwit.py file or
export an MINITWIT_SETTINGS environment variable
pointing to a configuration file.
2. install the app from the root of the project directory
pip install --editable .
3. tell flask about the right application:
export FLASK_APP=minitwit
4. fire up a shell and run this:
flask initdb
5. now you can run minitwit:
flask run
the application will greet you on
http://localhost:5000/
~ Is it tested?
You betcha. Run the `python setup.py test` file to
see the tests pass.

View file

@ -1 +0,0 @@
from .minitwit import app

View file

@ -1,256 +0,0 @@
# -*- coding: utf-8 -*-
"""
MiniTwit
~~~~~~~~
A microblogging application written with Flask and sqlite3.
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
import time
from sqlite3 import dbapi2 as sqlite3
from hashlib import md5
from datetime import datetime
from flask import Flask, request, session, url_for, redirect, \
render_template, abort, g, flash, _app_ctx_stack
from werkzeug import check_password_hash, generate_password_hash
# configuration
DATABASE = '/tmp/minitwit.db'
PER_PAGE = 30
DEBUG = True
SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/'
# create our little application :)
app = Flask('minitwit')
app.config.from_object(__name__)
app.config.from_envvar('MINITWIT_SETTINGS', silent=True)
def get_db():
"""Opens a new database connection if there is none yet for the
current application context.
"""
top = _app_ctx_stack.top
if not hasattr(top, 'sqlite_db'):
top.sqlite_db = sqlite3.connect(app.config['DATABASE'])
top.sqlite_db.row_factory = sqlite3.Row
return top.sqlite_db
@app.teardown_appcontext
def close_database(exception):
"""Closes the database again at the end of the request."""
top = _app_ctx_stack.top
if hasattr(top, 'sqlite_db'):
top.sqlite_db.close()
def init_db():
"""Initializes the database."""
db = get_db()
with app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
@app.cli.command('initdb')
def initdb_command():
"""Creates the database tables."""
init_db()
print('Initialized the database.')
def query_db(query, args=(), one=False):
"""Queries the database and returns a list of dictionaries."""
cur = get_db().execute(query, args)
rv = cur.fetchall()
return (rv[0] if rv else None) if one else rv
def get_user_id(username):
"""Convenience method to look up the id for a username."""
rv = query_db('select user_id from user where username = ?',
[username], one=True)
return rv[0] if rv else None
def format_datetime(timestamp):
"""Format a timestamp for display."""
return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M')
def gravatar_url(email, size=80):
"""Return the gravatar image for the given email address."""
return 'https://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \
(md5(email.strip().lower().encode('utf-8')).hexdigest(), size)
@app.before_request
def before_request():
g.user = None
if 'user_id' in session:
g.user = query_db('select * from user where user_id = ?',
[session['user_id']], one=True)
@app.route('/')
def timeline():
"""Shows a users timeline or if no user is logged in it will
redirect to the public timeline. This timeline shows the user's
messages as well as all the messages of followed users.
"""
if not g.user:
return redirect(url_for('public_timeline'))
return render_template('timeline.html', messages=query_db('''
select message.*, user.* from message, user
where message.author_id = user.user_id and (
user.user_id = ? or
user.user_id in (select whom_id from follower
where who_id = ?))
order by message.pub_date desc limit ?''',
[session['user_id'], session['user_id'], PER_PAGE]))
@app.route('/public')
def public_timeline():
"""Displays the latest messages of all users."""
return render_template('timeline.html', messages=query_db('''
select message.*, user.* from message, user
where message.author_id = user.user_id
order by message.pub_date desc limit ?''', [PER_PAGE]))
@app.route('/<username>')
def user_timeline(username):
"""Display's a users tweets."""
profile_user = query_db('select * from user where username = ?',
[username], one=True)
if profile_user is None:
abort(404)
followed = False
if g.user:
followed = query_db('''select 1 from follower where
follower.who_id = ? and follower.whom_id = ?''',
[session['user_id'], profile_user['user_id']],
one=True) is not None
return render_template('timeline.html', messages=query_db('''
select message.*, user.* from message, user where
user.user_id = message.author_id and user.user_id = ?
order by message.pub_date desc limit ?''',
[profile_user['user_id'], PER_PAGE]), followed=followed,
profile_user=profile_user)
@app.route('/<username>/follow')
def follow_user(username):
"""Adds the current user as follower of the given user."""
if not g.user:
abort(401)
whom_id = get_user_id(username)
if whom_id is None:
abort(404)
db = get_db()
db.execute('insert into follower (who_id, whom_id) values (?, ?)',
[session['user_id'], whom_id])
db.commit()
flash('You are now following "%s"' % username)
return redirect(url_for('user_timeline', username=username))
@app.route('/<username>/unfollow')
def unfollow_user(username):
"""Removes the current user as follower of the given user."""
if not g.user:
abort(401)
whom_id = get_user_id(username)
if whom_id is None:
abort(404)
db = get_db()
db.execute('delete from follower where who_id=? and whom_id=?',
[session['user_id'], whom_id])
db.commit()
flash('You are no longer following "%s"' % username)
return redirect(url_for('user_timeline', username=username))
@app.route('/add_message', methods=['POST'])
def add_message():
"""Registers a new message for the user."""
if 'user_id' not in session:
abort(401)
if request.form['text']:
db = get_db()
db.execute('''insert into message (author_id, text, pub_date)
values (?, ?, ?)''', (session['user_id'], request.form['text'],
int(time.time())))
db.commit()
flash('Your message was recorded')
return redirect(url_for('timeline'))
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Logs the user in."""
if g.user:
return redirect(url_for('timeline'))
error = None
if request.method == 'POST':
user = query_db('''select * from user where
username = ?''', [request.form['username']], one=True)
if user is None:
error = 'Invalid username'
elif not check_password_hash(user['pw_hash'],
request.form['password']):
error = 'Invalid password'
else:
flash('You were logged in')
session['user_id'] = user['user_id']
return redirect(url_for('timeline'))
return render_template('login.html', error=error)
@app.route('/register', methods=['GET', 'POST'])
def register():
"""Registers the user."""
if g.user:
return redirect(url_for('timeline'))
error = None
if request.method == 'POST':
if not request.form['username']:
error = 'You have to enter a username'
elif not request.form['email'] or \
'@' not in request.form['email']:
error = 'You have to enter a valid email address'
elif not request.form['password']:
error = 'You have to enter a password'
elif request.form['password'] != request.form['password2']:
error = 'The two passwords do not match'
elif get_user_id(request.form['username']) is not None:
error = 'The username is already taken'
else:
db = get_db()
db.execute('''insert into user (
username, email, pw_hash) values (?, ?, ?)''',
[request.form['username'], request.form['email'],
generate_password_hash(request.form['password'])])
db.commit()
flash('You were successfully registered and can login now')
return redirect(url_for('login'))
return render_template('register.html', error=error)
@app.route('/logout')
def logout():
"""Logs the user out."""
flash('You were logged out')
session.pop('user_id', None)
return redirect(url_for('public_timeline'))
# add some filters to jinja
app.jinja_env.filters['datetimeformat'] = format_datetime
app.jinja_env.filters['gravatar'] = gravatar_url

View file

@ -1,21 +0,0 @@
drop table if exists user;
create table user (
user_id integer primary key autoincrement,
username text not null,
email text not null,
pw_hash text not null
);
drop table if exists follower;
create table follower (
who_id integer,
whom_id integer
);
drop table if exists message;
create table message (
message_id integer primary key autoincrement,
author_id integer not null,
text text not null,
pub_date integer
);

View file

@ -1,178 +0,0 @@
body {
background: #CAECE9;
font-family: 'Trebuchet MS', sans-serif;
font-size: 14px;
}
a {
color: #26776F;
}
a:hover {
color: #333;
}
input[type="text"],
input[type="password"] {
background: white;
border: 1px solid #BFE6E2;
padding: 2px;
font-family: 'Trebuchet MS', sans-serif;
font-size: 14px;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
color: #105751;
}
input[type="submit"] {
background: #105751;
border: 1px solid #073B36;
padding: 1px 3px;
font-family: 'Trebuchet MS', sans-serif;
font-size: 14px;
font-weight: bold;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
color: white;
}
div.page {
background: white;
border: 1px solid #6ECCC4;
width: 700px;
margin: 30px auto;
}
div.page h1 {
background: #6ECCC4;
margin: 0;
padding: 10px 14px;
color: white;
letter-spacing: 1px;
text-shadow: 0 0 3px #24776F;
font-weight: normal;
}
div.page div.navigation {
background: #DEE9E8;
padding: 4px 10px;
border-top: 1px solid #ccc;
border-bottom: 1px solid #eee;
color: #888;
font-size: 12px;
letter-spacing: 0.5px;
}
div.page div.navigation a {
color: #444;
font-weight: bold;
}
div.page h2 {
margin: 0 0 15px 0;
color: #105751;
text-shadow: 0 1px 2px #ccc;
}
div.page div.body {
padding: 10px;
}
div.page div.footer {
background: #eee;
color: #888;
padding: 5px 10px;
font-size: 12px;
}
div.page div.followstatus {
border: 1px solid #ccc;
background: #E3EBEA;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
padding: 3px;
font-size: 13px;
}
div.page ul.messages {
list-style: none;
margin: 0;
padding: 0;
}
div.page ul.messages li {
margin: 10px 0;
padding: 5px;
background: #F0FAF9;
border: 1px solid #DBF3F1;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
min-height: 48px;
}
div.page ul.messages p {
margin: 0;
}
div.page ul.messages li img {
float: left;
padding: 0 10px 0 0;
}
div.page ul.messages li small {
font-size: 0.9em;
color: #888;
}
div.page div.twitbox {
margin: 10px 0;
padding: 5px;
background: #F0FAF9;
border: 1px solid #94E2DA;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
div.page div.twitbox h3 {
margin: 0;
font-size: 1em;
color: #2C7E76;
}
div.page div.twitbox p {
margin: 0;
}
div.page div.twitbox input[type="text"] {
width: 585px;
}
div.page div.twitbox input[type="submit"] {
width: 70px;
margin-left: 5px;
}
ul.flashes {
list-style: none;
margin: 10px 10px 0 10px;
padding: 0;
}
ul.flashes li {
background: #B9F3ED;
border: 1px solid #81CEC6;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
padding: 4px;
font-size: 13px;
}
div.error {
margin: 10px 0;
background: #FAE4E4;
border: 1px solid #DD6F6F;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
padding: 4px;
font-size: 13px;
}

View file

@ -1,32 +0,0 @@
<!doctype html>
<title>{% block title %}Welcome{% endblock %} | MiniTwit</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
<div class="page">
<h1>MiniTwit</h1>
<div class="navigation">
{% if g.user %}
<a href="{{ url_for('timeline') }}">my timeline</a> |
<a href="{{ url_for('public_timeline') }}">public timeline</a> |
<a href="{{ url_for('logout') }}">sign out [{{ g.user.username }}]</a>
{% else %}
<a href="{{ url_for('public_timeline') }}">public timeline</a> |
<a href="{{ url_for('register') }}">sign up</a> |
<a href="{{ url_for('login') }}">sign in</a>
{% endif %}
</div>
{% with flashes = get_flashed_messages() %}
{% if flashes %}
<ul class="flashes">
{% for message in flashes %}
<li>{{ message }}
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<div class="body">
{% block body %}{% endblock %}
</div>
<div class="footer">
MiniTwit &mdash; A Flask Application
</div>
</div>

View file

@ -1,16 +0,0 @@
{% extends "layout.html" %}
{% block title %}Sign In{% endblock %}
{% block body %}
<h2>Sign In</h2>
{% if error %}<div class="error"><strong>Error:</strong> {{ error }}</div>{% endif %}
<form action="" method="post">
<dl>
<dt>Username:
<dd><input type="text" name="username" size="30" value="{{ request.form.username }}">
<dt>Password:
<dd><input type="password" name="password" size="30">
</dl>
<div class="actions"><input type="submit" value="Sign In"></div>
</form>
{% endblock %}

View file

@ -1,19 +0,0 @@
{% extends "layout.html" %}
{% block title %}Sign Up{% endblock %}
{% block body %}
<h2>Sign Up</h2>
{% if error %}<div class="error"><strong>Error:</strong> {{ error }}</div>{% endif %}
<form action="" method="post">
<dl>
<dt>Username:
<dd><input type="text" name="username" size="30" value="{{ request.form.username }}">
<dt>E-Mail:
<dd><input type="text" name="email" size="30" value="{{ request.form.email }}">
<dt>Password:
<dd><input type="password" name="password" size="30">
<dt>Password <small>(repeat)</small>:
<dd><input type="password" name="password2" size="30">
</dl>
<div class="actions"><input type="submit" value="Sign Up"></div>
</form>
{% endblock %}

View file

@ -1,49 +0,0 @@
{% extends "layout.html" %}
{% block title %}
{% if request.endpoint == 'public_timeline' %}
Public Timeline
{% elif request.endpoint == 'user_timeline' %}
{{ profile_user.username }}'s Timeline
{% else %}
My Timeline
{% endif %}
{% endblock %}
{% block body %}
<h2>{{ self.title() }}</h2>
{% if g.user %}
{% if request.endpoint == 'user_timeline' %}
<div class="followstatus">
{% if g.user.user_id == profile_user.user_id %}
This is you!
{% elif followed %}
You are currently following this user.
<a class="unfollow" href="{{ url_for('unfollow_user', username=profile_user.username)
}}">Unfollow user</a>.
{% else %}
You are not yet following this user.
<a class="follow" href="{{ url_for('follow_user', username=profile_user.username)
}}">Follow user</a>.
{% endif %}
</div>
{% elif request.endpoint == 'timeline' %}
<div class="twitbox">
<h3>What's on your mind {{ g.user.username }}?</h3>
<form action="{{ url_for('add_message') }}" method="post">
<p><input type="text" name="text" size="60"><!--
--><input type="submit" value="Share">
</form>
</div>
{% endif %}
{% endif %}
<ul class="messages">
{% for message in messages %}
<li><img src="{{ message.email|gravatar(size=48) }}"><p>
<strong><a href="{{ url_for('user_timeline', username=message.username)
}}">{{ message.username }}</a></strong>
{{ message.text }}
<small>&mdash; {{ message.pub_date|datetimeformat }}</small>
{% else %}
<li><em>There's no message so far.</em>
{% endfor %}
</ul>
{% endblock %}

View file

@ -1,2 +0,0 @@
[aliases]
test=pytest

View file

@ -1,16 +0,0 @@
from setuptools import setup
setup(
name='minitwit',
packages=['minitwit'],
include_package_data=True,
install_requires=[
'flask',
],
setup_requires=[
'pytest-runner',
],
tests_require=[
'pytest',
],
)

View file

@ -1,150 +0,0 @@
# -*- coding: utf-8 -*-
"""
MiniTwit Tests
~~~~~~~~~~~~~~
Tests the MiniTwit application.
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
import os
import tempfile
import pytest
from minitwit import minitwit
@pytest.fixture
def client():
db_fd, minitwit.app.config['DATABASE'] = tempfile.mkstemp()
client = minitwit.app.test_client()
with minitwit.app.app_context():
minitwit.init_db()
yield client
os.close(db_fd)
os.unlink(minitwit.app.config['DATABASE'])
def register(client, username, password, password2=None, email=None):
"""Helper function to register a user"""
if password2 is None:
password2 = password
if email is None:
email = username + '@example.com'
return client.post('/register', data={
'username': username,
'password': password,
'password2': password2,
'email': email,
}, follow_redirects=True)
def login(client, username, password):
"""Helper function to login"""
return client.post('/login', data={
'username': username,
'password': password
}, follow_redirects=True)
def register_and_login(client, username, password):
"""Registers and logs in in one go"""
register(client, username, password)
return login(client, username, password)
def logout(client):
"""Helper function to logout"""
return client.get('/logout', follow_redirects=True)
def add_message(client, text):
"""Records a message"""
rv = client.post('/add_message', data={'text': text},
follow_redirects=True)
if text:
assert b'Your message was recorded' in rv.data
return rv
def test_register(client):
"""Make sure registering works"""
rv = register(client, 'user1', 'default')
assert b'You were successfully registered ' \
b'and can login now' in rv.data
rv = register(client, 'user1', 'default')
assert b'The username is already taken' in rv.data
rv = register(client, '', 'default')
assert b'You have to enter a username' in rv.data
rv = register(client, 'meh', '')
assert b'You have to enter a password' in rv.data
rv = register(client, 'meh', 'x', 'y')
assert b'The two passwords do not match' in rv.data
rv = register(client, 'meh', 'foo', email='broken')
assert b'You have to enter a valid email address' in rv.data
def test_login_logout(client):
"""Make sure logging in and logging out works"""
rv = register_and_login(client, 'user1', 'default')
assert b'You were logged in' in rv.data
rv = logout(client)
assert b'You were logged out' in rv.data
rv = login(client, 'user1', 'wrongpassword')
assert b'Invalid password' in rv.data
rv = login(client, 'user2', 'wrongpassword')
assert b'Invalid username' in rv.data
def test_message_recording(client):
"""Check if adding messages works"""
register_and_login(client, 'foo', 'default')
add_message(client, 'test message 1')
add_message(client, '<test message 2>')
rv = client.get('/')
assert b'test message 1' in rv.data
assert b'&lt;test message 2&gt;' in rv.data
def test_timelines(client):
"""Make sure that timelines work"""
register_and_login(client, 'foo', 'default')
add_message(client, 'the message by foo')
logout(client)
register_and_login(client, 'bar', 'default')
add_message(client, 'the message by bar')
rv = client.get('/public')
assert b'the message by foo' in rv.data
assert b'the message by bar' in rv.data
# bar's timeline should just show bar's message
rv = client.get('/')
assert b'the message by foo' not in rv.data
assert b'the message by bar' in rv.data
# now let's follow foo
rv = client.get('/foo/follow', follow_redirects=True)
assert b'You are now following &#34;foo&#34;' in rv.data
# we should now see foo's message
rv = client.get('/')
assert b'the message by foo' in rv.data
assert b'the message by bar' in rv.data
# but on the user's page we only want the user's message
rv = client.get('/bar')
assert b'the message by foo' not in rv.data
assert b'the message by bar' in rv.data
rv = client.get('/foo')
assert b'the message by foo' in rv.data
assert b'the message by bar' not in rv.data
# now unfollow and check if that worked
rv = client.get('/foo/unfollow', follow_redirects=True)
assert b'You are no longer following &#34;foo&#34;' in rv.data
rv = client.get('/')
assert b'the message by foo' not in rv.data
assert b'the message by bar' in rv.data

View file

@ -1,10 +0,0 @@
from setuptools import setup
setup(
name='yourapplication',
packages=['yourapplication'],
include_package_data=True,
install_requires=[
'flask',
],
)

View file

@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
"""
Larger App Tests
~~~~~~~~~~~~~~~~
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
from yourapplication import app
import pytest
@pytest.fixture
def client():
app.config['TESTING'] = True
client = app.test_client()
return client
def test_index(client):
rv = client.get('/')
assert b"Hello World!" in rv.data

View file

@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
"""
yourapplication
~~~~~~~~~~~~~~~
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
from flask import Flask
app = Flask('yourapplication')
import yourapplication.views

View file

@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
"""
yourapplication.views
~~~~~~~~~~~~~~~~~~~~~
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
from yourapplication import app
@app.route('/')
def index():
return 'Hello World!'

14
examples/tutorial/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
venv/
*.pyc
__pycache__/
instance/
.cache/
.pytest_cache/
.coverage
htmlcov/
dist/
build/
*.egg-info/
.idea/
*.swp
*~

31
examples/tutorial/LICENSE Normal file
View file

@ -0,0 +1,31 @@
Copyright © 2010 by the Pallets team.
Some rights reserved.
Redistribution and use in source and binary forms of the software as
well as documentation, with or without modification, are permitted
provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.

View file

@ -1,3 +1,6 @@
graft flaskr/templates
graft flaskr/static
include LICENSE
include flaskr/schema.sql
graft flaskr/static
graft flaskr/templates
graft tests
global-exclude *.pyc

View file

@ -0,0 +1,76 @@
Flaskr
======
The basic blog app built in the Flask `tutorial`_.
.. _tutorial: http://flask.pocoo.org/docs/tutorial/
Install
-------
**Be sure to use the same version of the code as the version of the docs
you're reading.** You probably want the latest tagged version, but the
default Git version is the master branch. ::
# clone the repository
git clone https://github.com/pallets/flask
cd flask
# checkout the correct version
git tag # shows the tagged versions
git checkout latest-tag-found-above
cd examples/tutorial
Create a virtualenv and activate it::
python3 -m venv venv
. venv/bin/activate
Or on Windows cmd::
py -3 -m venv venv
venv\Scripts\activate.bat
Install Flaskr::
pip install -e .
Or if you are using the master branch, install Flask from source before
installing Flaskr::
pip install -e ../..
pip install -e .
Run
---
::
export FLASK_APP=flaskr
export FLASK_ENV=development
flask run
Or on Windows cmd::
set FLASK_APP=flaskr
set FLASK_ENV=development
flask run
Open http://127.0.0.1:5000 in a browser.
Test
----
::
pip install pytest
pytest
Run with coverage report::
pip install pytest coverage
coverage run -m pytest
coverage report
coverage html # open htmlcov/index.html in a browser

View file

@ -0,0 +1,48 @@
import os
from flask import Flask
def create_app(test_config=None):
"""Create and configure an instance of the Flask application."""
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
# a default secret that should be overridden by instance config
SECRET_KEY='dev',
# store the database in the instance folder
DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'),
)
if test_config is None:
# load the instance config, if it exists, when not testing
app.config.from_pyfile('config.py', silent=True)
else:
# load the test config if passed in
app.config.update(test_config)
# ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
@app.route('/hello')
def hello():
return 'Hello, World!'
# register the database commands
from flaskr import db
db.init_app(app)
# apply the blueprints to the app
from flaskr import auth, blog
app.register_blueprint(auth.bp)
app.register_blueprint(blog.bp)
# make url_for('index') == url_for('blog.index')
# in another app, you might define a separate main index here with
# app.route, while giving the blog blueprint a url_prefix, but for
# the tutorial the blog will be the main index
app.add_url_rule('/', endpoint='index')
return app

View file

@ -0,0 +1,108 @@
import functools
from flask import (
Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash
from flaskr.db import get_db
bp = Blueprint('auth', __name__, url_prefix='/auth')
def login_required(view):
"""View decorator that redirects anonymous users to the login page."""
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
return redirect(url_for('auth.login'))
return view(**kwargs)
return wrapped_view
@bp.before_app_request
def load_logged_in_user():
"""If a user id is stored in the session, load the user object from
the database into ``g.user``."""
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = get_db().execute(
'SELECT * FROM user WHERE id = ?', (user_id,)
).fetchone()
@bp.route('/register', methods=('GET', 'POST'))
def register():
"""Register a new user.
Validates that the username is not already taken. Hashes the
password for security.
"""
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
if not username:
error = 'Username is required.'
elif not password:
error = 'Password is required.'
elif db.execute(
'SELECT id FROM user WHERE username = ?', (username,)
).fetchone() is not None:
error = 'User {} is already registered.'.format(username)
if error is None:
# the name is available, store it in the database and go to
# the login page
db.execute(
'INSERT INTO user (username, password) VALUES (?, ?)',
(username, generate_password_hash(password))
)
db.commit()
return redirect(url_for('auth.login'))
flash(error)
return render_template('auth/register.html')
@bp.route('/login', methods=('GET', 'POST'))
def login():
"""Log in a registered user by adding the user id to the session."""
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
error = None
user = db.execute(
'SELECT * FROM user WHERE username = ?', (username,)
).fetchone()
if user is None:
error = 'Incorrect username.'
elif not check_password_hash(user['password'], password):
error = 'Incorrect password.'
if error is None:
# store the user id in a new session and return to the index
session.clear()
session['user_id'] = user['id']
return redirect(url_for('index'))
flash(error)
return render_template('auth/login.html')
@bp.route('/logout')
def logout():
"""Clear the current session, including the stored user id."""
session.clear()
return redirect(url_for('index'))

View file

@ -0,0 +1,119 @@
from flask import (
Blueprint, flash, g, redirect, render_template, request, url_for
)
from werkzeug.exceptions import abort
from flaskr.auth import login_required
from flaskr.db import get_db
bp = Blueprint('blog', __name__)
@bp.route('/')
def index():
"""Show all the posts, most recent first."""
db = get_db()
posts = db.execute(
'SELECT p.id, title, body, created, author_id, username'
' FROM post p JOIN user u ON p.author_id = u.id'
' ORDER BY created DESC'
).fetchall()
return render_template('blog/index.html', posts=posts)
def get_post(id, check_author=True):
"""Get a post and its author by id.
Checks that the id exists and optionally that the current user is
the author.
:param id: id of post to get
:param check_author: require the current user to be the author
:return: the post with author information
:raise 404: if a post with the given id doesn't exist
:raise 403: if the current user isn't the author
"""
post = get_db().execute(
'SELECT p.id, title, body, created, author_id, username'
' FROM post p JOIN user u ON p.author_id = u.id'
' WHERE p.id = ?',
(id,)
).fetchone()
if post is None:
abort(404, "Post id {0} doesn't exist.".format(id))
if check_author and post['author_id'] != g.user['id']:
abort(403)
return post
@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
"""Create a new post for the current user."""
if request.method == 'POST':
title = request.form['title']
body = request.form['body']
error = None
if not title:
error = 'Title is required.'
if error is not None:
flash(error)
else:
db = get_db()
db.execute(
'INSERT INTO post (title, body, author_id)'
' VALUES (?, ?, ?)',
(title, body, g.user['id'])
)
db.commit()
return redirect(url_for('blog.index'))
return render_template('blog/create.html')
@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
"""Update a post if the current user is the author."""
post = get_post(id)
if request.method == 'POST':
title = request.form['title']
body = request.form['body']
error = None
if not title:
error = 'Title is required.'
if error is not None:
flash(error)
else:
db = get_db()
db.execute(
'UPDATE post SET title = ?, body = ? WHERE id = ?',
(title, body, id)
)
db.commit()
return redirect(url_for('blog.index'))
return render_template('blog/update.html', post=post)
@bp.route('/<int:id>/delete', methods=('POST',))
@login_required
def delete(id):
"""Delete a post.
Ensures that the post exists and that the logged in user is the
author of the post.
"""
get_post(id)
db = get_db()
db.execute('DELETE FROM post WHERE id = ?', (id,))
db.commit()
return redirect(url_for('blog.index'))

View file

@ -0,0 +1,54 @@
import sqlite3
import click
from flask import current_app, g
from flask.cli import with_appcontext
def get_db():
"""Connect to the application's configured database. The connection
is unique for each request and will be reused if this is called
again.
"""
if 'db' not in g:
g.db = sqlite3.connect(
current_app.config['DATABASE'],
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
return g.db
def close_db(e=None):
"""If this request connected to the database, close the
connection.
"""
db = g.pop('db', None)
if db is not None:
db.close()
def init_db():
"""Clear existing data and create new tables."""
db = get_db()
with current_app.open_resource('schema.sql') as f:
db.executescript(f.read().decode('utf8'))
@click.command('init-db')
@with_appcontext
def init_db_command():
"""Clear existing data and create new tables."""
init_db()
click.echo('Initialized the database.')
def init_app(app):
"""Register database functions with the Flask app. This is called by
the application factory.
"""
app.teardown_appcontext(close_db)
app.cli.add_command(init_db_command)

View file

@ -0,0 +1,20 @@
-- Initialize the database.
-- Drop any existing data and create empty tables.
DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS post;
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE post (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author_id INTEGER NOT NULL,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
title TEXT NOT NULL,
body TEXT NOT NULL,
FOREIGN KEY (author_id) REFERENCES user (id)
);

View file

@ -0,0 +1,134 @@
html {
font-family: sans-serif;
background: #eee;
padding: 1rem;
}
body {
max-width: 960px;
margin: 0 auto;
background: white;
}
h1, h2, h3, h4, h5, h6 {
font-family: serif;
color: #377ba8;
margin: 1rem 0;
}
a {
color: #377ba8;
}
hr {
border: none;
border-top: 1px solid lightgray;
}
nav {
background: lightgray;
display: flex;
align-items: center;
padding: 0 0.5rem;
}
nav h1 {
flex: auto;
margin: 0;
}
nav h1 a {
text-decoration: none;
padding: 0.25rem 0.5rem;
}
nav ul {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
nav ul li a, nav ul li span, header .action {
display: block;
padding: 0.5rem;
}
.content {
padding: 0 1rem 1rem;
}
.content > header {
border-bottom: 1px solid lightgray;
display: flex;
align-items: flex-end;
}
.content > header h1 {
flex: auto;
margin: 1rem 0 0.25rem 0;
}
.flash {
margin: 1em 0;
padding: 1em;
background: #cae6f6;
border: 1px solid #377ba8;
}
.post > header {
display: flex;
align-items: flex-end;
font-size: 0.85em;
}
.post > header > div:first-of-type {
flex: auto;
}
.post > header h1 {
font-size: 1.5em;
margin-bottom: 0;
}
.post .about {
color: slategray;
font-style: italic;
}
.post .body {
white-space: pre-line;
}
.content:last-child {
margin-bottom: 0;
}
.content form {
margin: 1em 0;
display: flex;
flex-direction: column;
}
.content label {
font-weight: bold;
margin-bottom: 0.5em;
}
.content input, .content textarea {
margin-bottom: 1em;
}
.content textarea {
min-height: 12em;
resize: vertical;
}
input.danger {
color: #cc2f2e;
}
input[type=submit] {
align-self: start;
min-width: 10em;
}

View file

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Log In{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="username">Username</label>
<input name="username" id="username" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<input type="submit" value="Log In">
</form>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Register{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="username">Username</label>
<input name="username" id="username" required>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<input type="submit" value="Register">
</form>
{% endblock %}

View file

@ -0,0 +1,24 @@
<!doctype html>
<title>{% block title %}{% endblock %} - Flaskr</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<nav>
<h1><a href="{{ url_for('index') }}">Flaskr</a></h1>
<ul>
{% if g.user %}
<li><span>{{ g.user['username'] }}</span>
<li><a href="{{ url_for('auth.logout') }}">Log Out</a>
{% else %}
<li><a href="{{ url_for('auth.register') }}">Register</a>
<li><a href="{{ url_for('auth.login') }}">Log In</a>
{% endif %}
</ul>
</nav>
<section class="content">
<header>
{% block header %}{% endblock %}
</header>
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</section>

View file

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}New Post{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="title">Title</label>
<input name="title" id="title" value="{{ request.form['title'] }}" required>
<label for="body">Body</label>
<textarea name="body" id="body">{{ request.form['body'] }}</textarea>
<input type="submit" value="Save">
</form>
{% endblock %}

View file

@ -0,0 +1,28 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Posts{% endblock %}</h1>
{% if g.user %}
<a class="action" href="{{ url_for('blog.create') }}">New</a>
{% endif %}
{% endblock %}
{% block content %}
{% for post in posts %}
<article class="post">
<header>
<div>
<h1>{{ post['title'] }}</h1>
<div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
</div>
{% if g.user['id'] == post['author_id'] %}
<a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>
{% endif %}
</header>
<p class="body">{{ post['body'] }}</p>
</article>
{% if not loop.last %}
<hr>
{% endif %}
{% endfor %}
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}
{% block content %}
<form method="post">
<label for="title">Title</label>
<input name="title" id="title" value="{{ request.form['title'] or post['title'] }}" required>
<label for="body">Body</label>
<textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
<input type="submit" value="Save">
</form>
<hr>
<form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
<input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
</form>
{% endblock %}

View file

@ -0,0 +1,13 @@
[metadata]
license_file = LICENSE
[bdist_wheel]
universal = False
[tool:pytest]
testpaths = tests
[coverage:run]
branch = True
source =
flaskr

View file

@ -0,0 +1,23 @@
import io
from setuptools import find_packages, setup
with io.open('README.rst', 'rt', encoding='utf8') as f:
readme = f.read()
setup(
name='flaskr',
version='1.0.0',
url='http://flask.pocoo.org/docs/tutorial/',
license='BSD',
maintainer='Pallets team',
maintainer_email='contact@palletsprojects.com',
description='The basic blog app built in the Flask tutorial.',
long_description=readme,
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=[
'flask',
],
)

View file

@ -0,0 +1,64 @@
import os
import tempfile
import pytest
from flaskr import create_app
from flaskr.db import get_db, init_db
# read in SQL for populating test data
with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f:
_data_sql = f.read().decode('utf8')
@pytest.fixture
def app():
"""Create and configure a new app instance for each test."""
# create a temporary file to isolate the database for each test
db_fd, db_path = tempfile.mkstemp()
# create the app with common test config
app = create_app({
'TESTING': True,
'DATABASE': db_path,
})
# create the database and load test data
with app.app_context():
init_db()
get_db().executescript(_data_sql)
yield app
# close and remove the temporary database
os.close(db_fd)
os.unlink(db_path)
@pytest.fixture
def client(app):
"""A test client for the app."""
return app.test_client()
@pytest.fixture
def runner(app):
"""A test runner for the app's Click commands."""
return app.test_cli_runner()
class AuthActions(object):
def __init__(self, client):
self._client = client
def login(self, username='test', password='test'):
return self._client.post(
'/auth/login',
data={'username': username, 'password': password}
)
def logout(self):
return self._client.get('/auth/logout')
@pytest.fixture
def auth(client):
return AuthActions(client)

View file

@ -0,0 +1,8 @@
INSERT INTO user (username, password)
VALUES
('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'),
('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79');
INSERT INTO post (title, body, author_id, created)
VALUES
('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00');

View file

@ -0,0 +1,66 @@
import pytest
from flask import g, session
from flaskr.db import get_db
def test_register(client, app):
# test that viewing the page renders without template errors
assert client.get('/auth/register').status_code == 200
# test that successful registration redirects to the login page
response = client.post(
'/auth/register', data={'username': 'a', 'password': 'a'}
)
assert 'http://localhost/auth/login' == response.headers['Location']
# test that the user was inserted into the database
with app.app_context():
assert get_db().execute(
"select * from user where username = 'a'",
).fetchone() is not None
@pytest.mark.parametrize(('username', 'password', 'message'), (
('', '', b'Username is required.'),
('a', '', b'Password is required.'),
('test', 'test', b'already registered'),
))
def test_register_validate_input(client, username, password, message):
response = client.post(
'/auth/register',
data={'username': username, 'password': password}
)
assert message in response.data
def test_login(client, auth):
# test that viewing the page renders without template errors
assert client.get('/auth/login').status_code == 200
# test that successful login redirects to the index page
response = auth.login()
assert response.headers['Location'] == 'http://localhost/'
# login request set the user_id in the session
# check that the user is loaded from the session
with client:
client.get('/')
assert session['user_id'] == 1
assert g.user['username'] == 'test'
@pytest.mark.parametrize(('username', 'password', 'message'), (
('a', 'test', b'Incorrect username.'),
('test', 'a', b'Incorrect password.'),
))
def test_login_validate_input(auth, username, password, message):
response = auth.login(username, password)
assert message in response.data
def test_logout(client, auth):
auth.login()
with client:
auth.logout()
assert 'user_id' not in session

View file

@ -0,0 +1,92 @@
import pytest
from flaskr.db import get_db
def test_index(client, auth):
response = client.get('/')
assert b"Log In" in response.data
assert b"Register" in response.data
auth.login()
response = client.get('/')
assert b'test title' in response.data
assert b'by test on 2018-01-01' in response.data
assert b'test\nbody' in response.data
assert b'href="/1/update"' in response.data
@pytest.mark.parametrize('path', (
'/create',
'/1/update',
'/1/delete',
))
def test_login_required(client, path):
response = client.post(path)
assert response.headers['Location'] == 'http://localhost/auth/login'
def test_author_required(app, client, auth):
# change the post author to another user
with app.app_context():
db = get_db()
db.execute('UPDATE post SET author_id = 2 WHERE id = 1')
db.commit()
auth.login()
# current user can't modify other user's post
assert client.post('/1/update').status_code == 403
assert client.post('/1/delete').status_code == 403
# current user doesn't see edit link
assert b'href="/1/update"' not in client.get('/').data
@pytest.mark.parametrize('path', (
'/2/update',
'/2/delete',
))
def test_exists_required(client, auth, path):
auth.login()
assert client.post(path).status_code == 404
def test_create(client, auth, app):
auth.login()
assert client.get('/create').status_code == 200
client.post('/create', data={'title': 'created', 'body': ''})
with app.app_context():
db = get_db()
count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0]
assert count == 2
def test_update(client, auth, app):
auth.login()
assert client.get('/1/update').status_code == 200
client.post('/1/update', data={'title': 'updated', 'body': ''})
with app.app_context():
db = get_db()
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone()
assert post['title'] == 'updated'
@pytest.mark.parametrize('path', (
'/create',
'/1/update',
))
def test_create_update_validate(client, auth, path):
auth.login()
response = client.post(path, data={'title': '', 'body': ''})
assert b'Title is required.' in response.data
def test_delete(client, auth, app):
auth.login()
response = client.post('/1/delete')
assert response.headers['Location'] == 'http://localhost/'
with app.app_context():
db = get_db()
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone()
assert post is None

View file

@ -0,0 +1,28 @@
import sqlite3
import pytest
from flaskr.db import get_db
def test_get_close_db(app):
with app.app_context():
db = get_db()
assert db is get_db()
with pytest.raises(sqlite3.ProgrammingError) as e:
db.execute('SELECT 1')
assert 'closed' in str(e)
def test_init_db_command(runner, monkeypatch):
class Recorder(object):
called = False
def fake_init_db():
Recorder.called = True
monkeypatch.setattr('flaskr.db.init_db', fake_init_db)
result = runner.invoke(args=['init-db'])
assert 'Initialized' in result.output
assert Recorder.called

View file

@ -0,0 +1,12 @@
from flaskr import create_app
def test_config():
"""Test create_app without passing test config."""
assert not create_app().testing
assert create_app({'TESTING': True}).testing
def test_hello(client):
response = client.get('/hello')
assert response.data == b'Hello, World!'