rewrite tutorial docs and example
This commit is contained in:
parent
16d83d6bb4
commit
c3dd7b8e4c
103 changed files with 3327 additions and 2224 deletions
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{% extends "pages/layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
Hello
|
||||
{% endblock %}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{% extends "pages/layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
Blueprint example page
|
||||
{% endblock %}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{% extends "pages/layout.html" %}
|
||||
{% block body %}
|
||||
World
|
||||
{% endblock %}
|
||||
|
|
@ -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
|
||||
2
examples/flaskr/.gitignore
vendored
2
examples/flaskr/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
flaskr.db
|
||||
.eggs/
|
||||
|
|
@ -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.
|
||||
|
|
@ -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'))
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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; }
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
[aliases]
|
||||
test=pytest
|
||||
|
|
@ -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',
|
||||
],
|
||||
)
|
||||
|
|
@ -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'<Hello>' in rv.data
|
||||
assert b'<strong>HTML</strong> allowed here' in rv.data
|
||||
|
|
@ -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()
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
2
examples/minitwit/.gitignore
vendored
2
examples/minitwit/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
minitwit.db
|
||||
.eggs/
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
graft minitwit/templates
|
||||
graft minitwit/static
|
||||
include minitwit/schema.sql
|
||||
|
|
@ -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.
|
||||
|
|
@ -1 +0,0 @@
|
|||
from .minitwit import app
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 — A Flask Application
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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 %}
|
||||
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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>— {{ message.pub_date|datetimeformat }}</small>
|
||||
{% else %}
|
||||
<li><em>There's no message so far.</em>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
[aliases]
|
||||
test=pytest
|
||||
|
|
@ -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',
|
||||
],
|
||||
)
|
||||
|
|
@ -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'<test message 2>' 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 "foo"' 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 "foo"' 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
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='yourapplication',
|
||||
packages=['yourapplication'],
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
'flask',
|
||||
],
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
14
examples/tutorial/.gitignore
vendored
Normal 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
31
examples/tutorial/LICENSE
Normal 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.
|
||||
|
|
@ -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
|
||||
76
examples/tutorial/README.rst
Normal file
76
examples/tutorial/README.rst
Normal 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
|
||||
48
examples/tutorial/flaskr/__init__.py
Normal file
48
examples/tutorial/flaskr/__init__.py
Normal 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
|
||||
108
examples/tutorial/flaskr/auth.py
Normal file
108
examples/tutorial/flaskr/auth.py
Normal 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'))
|
||||
119
examples/tutorial/flaskr/blog.py
Normal file
119
examples/tutorial/flaskr/blog.py
Normal 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'))
|
||||
54
examples/tutorial/flaskr/db.py
Normal file
54
examples/tutorial/flaskr/db.py
Normal 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)
|
||||
20
examples/tutorial/flaskr/schema.sql
Normal file
20
examples/tutorial/flaskr/schema.sql
Normal 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)
|
||||
);
|
||||
134
examples/tutorial/flaskr/static/style.css
Normal file
134
examples/tutorial/flaskr/static/style.css
Normal 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;
|
||||
}
|
||||
15
examples/tutorial/flaskr/templates/auth/login.html
Normal file
15
examples/tutorial/flaskr/templates/auth/login.html
Normal 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 %}
|
||||
15
examples/tutorial/flaskr/templates/auth/register.html
Normal file
15
examples/tutorial/flaskr/templates/auth/register.html
Normal 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 %}
|
||||
24
examples/tutorial/flaskr/templates/base.html
Normal file
24
examples/tutorial/flaskr/templates/base.html
Normal 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>
|
||||
15
examples/tutorial/flaskr/templates/blog/create.html
Normal file
15
examples/tutorial/flaskr/templates/blog/create.html
Normal 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 %}
|
||||
28
examples/tutorial/flaskr/templates/blog/index.html
Normal file
28
examples/tutorial/flaskr/templates/blog/index.html
Normal 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 %}
|
||||
19
examples/tutorial/flaskr/templates/blog/update.html
Normal file
19
examples/tutorial/flaskr/templates/blog/update.html
Normal 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 %}
|
||||
13
examples/tutorial/setup.cfg
Normal file
13
examples/tutorial/setup.cfg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[metadata]
|
||||
license_file = LICENSE
|
||||
|
||||
[bdist_wheel]
|
||||
universal = False
|
||||
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
source =
|
||||
flaskr
|
||||
23
examples/tutorial/setup.py
Normal file
23
examples/tutorial/setup.py
Normal 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',
|
||||
],
|
||||
)
|
||||
64
examples/tutorial/tests/conftest.py
Normal file
64
examples/tutorial/tests/conftest.py
Normal 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)
|
||||
8
examples/tutorial/tests/data.sql
Normal file
8
examples/tutorial/tests/data.sql
Normal 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');
|
||||
66
examples/tutorial/tests/test_auth.py
Normal file
66
examples/tutorial/tests/test_auth.py
Normal 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
|
||||
92
examples/tutorial/tests/test_blog.py
Normal file
92
examples/tutorial/tests/test_blog.py
Normal 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
|
||||
28
examples/tutorial/tests/test_db.py
Normal file
28
examples/tutorial/tests/test_db.py
Normal 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
|
||||
12
examples/tutorial/tests/test_factory.py
Normal file
12
examples/tutorial/tests/test_factory.py
Normal 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!'
|
||||
Loading…
Add table
Add a link
Reference in a new issue