Skip to content

Commit 0045123

Browse files
authored
Merge pull request pallets-eco#720 from pallets/flaskr
adapt flaskr example to sqlalchemy
2 parents da6c7cc + 391c3b8 commit 0045123

29 files changed

+982
-0
lines changed

examples/flaskr/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
venv/
2+
*.pyc
3+
instance/
4+
.pytest_cache/
5+
.coverage
6+
htmlcov/
7+
dist/
8+
build/
9+
*.egg-info/
10+
.idea/

examples/flaskr/LICENSE.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Copyright 2010 Pallets
2+
3+
Redistribution and use in source and binary forms, with or without
4+
modification, are permitted provided that the following conditions are
5+
met:
6+
7+
1. Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
10+
2. Redistributions in binary form must reproduce the above copyright
11+
notice, this list of conditions and the following disclaimer in the
12+
documentation and/or other materials provided with the distribution.
13+
14+
3. Neither the name of the copyright holder nor the names of its
15+
contributors may be used to endorse or promote products derived from
16+
this software without specific prior written permission.
17+
18+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
21+
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
24+
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

examples/flaskr/MANIFEST.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
graft flaskr/static
2+
graft flaskr/templates
3+
graft tests
4+
global-exclude *.pyc

examples/flaskr/README.rst

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
Flaskr
2+
======
3+
4+
The basic blog app built in the Flask `tutorial`_, modified to use
5+
Flask-SQLAlchemy instead of plain SQL.
6+
7+
.. _tutorial: http://flask.pocoo.org/docs/tutorial/
8+
9+
10+
Install
11+
-------
12+
13+
**Be sure to use the same version of the code as the version of the docs
14+
you're reading.** You probably want the latest tagged version, but the
15+
default Git version is the master branch.
16+
17+
.. code-block:: text
18+
19+
# clone the repository
20+
$ git clone https://github.com/pallets/flask-sqlalchemy
21+
$ cd flask-sqlalchemy/examples/flaskr
22+
# checkout the correct version
23+
$ git checkout correct-version-tag
24+
25+
Create a virtualenv and activate it:
26+
27+
.. code-block:: text
28+
29+
$ python3 -m venv venv
30+
$ . venv/bin/activate
31+
32+
Or on Windows cmd:
33+
34+
.. code-block:: text
35+
36+
$ py -3 -m venv venv
37+
$ venv\Scripts\activate.bat
38+
39+
Install Flaskr:
40+
41+
.. code-block:: text
42+
43+
$ pip install -e .
44+
45+
Or if you are using the master branch, install Flask-SQLAlchemy from
46+
source before installing Flaskr:
47+
48+
.. code-block:: text
49+
50+
$ pip install -e ../..
51+
$ pip install -e .
52+
53+
54+
Run
55+
---
56+
57+
.. code-block:: text
58+
59+
$ export FLASK_APP=flaskr
60+
$ export FLASK_ENV=development
61+
$ flask init-db
62+
$ flask run
63+
64+
Or on Windows cmd:
65+
66+
.. code-block:: text
67+
68+
> set FLASK_APP=flaskr
69+
> set FLASK_ENV=development
70+
> flask init-db
71+
> flask run
72+
73+
Open http://127.0.0.1:5000 in a browser.
74+
75+
76+
Test
77+
----
78+
79+
.. code-block:: text
80+
81+
$ pip install -e '.[test]'
82+
$ pytest
83+
84+
Run with coverage report:
85+
86+
.. code-block:: text
87+
88+
$ coverage run -m pytest
89+
$ coverage report
90+
$ coverage html # open htmlcov/index.html in a browser

examples/flaskr/flaskr/__init__.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import os
2+
3+
import click
4+
from flask import Flask
5+
from flask.cli import with_appcontext
6+
from flask_sqlalchemy import SQLAlchemy
7+
8+
__version__ = (1, 0, 0, "dev")
9+
10+
db = SQLAlchemy()
11+
12+
13+
def create_app(test_config=None):
14+
"""Create and configure an instance of the Flask application."""
15+
app = Flask(__name__, instance_relative_config=True)
16+
17+
# some deploy systems set the database url in the environ
18+
db_url = os.environ.get("DATABASE_URL")
19+
20+
if db_url is None:
21+
# default to a sqlite database in the instance folder
22+
db_url = "sqlite:///" + os.path.join(app.instance_path, "flaskr.sqlite")
23+
# ensure the instance folder exists
24+
os.makedirs(app.instance_path, exist_ok=True)
25+
26+
app.config.from_mapping(
27+
# default secret that should be overridden in environ or config
28+
SECRET_KEY=os.environ.get("SECRET_KEY", "dev"),
29+
SQLALCHEMY_DATABASE_URI=db_url,
30+
SQLALCHEMY_TRACK_MODIFICATIONS=False,
31+
)
32+
33+
if test_config is None:
34+
# load the instance config, if it exists, when not testing
35+
app.config.from_pyfile("config.py", silent=True)
36+
else:
37+
# load the test config if passed in
38+
app.config.update(test_config)
39+
40+
# initialize Flask-SQLAlchemy and the init-db command
41+
db.init_app(app)
42+
app.cli.add_command(init_db_command)
43+
44+
# apply the blueprints to the app
45+
from flaskr import auth, blog
46+
47+
app.register_blueprint(auth.bp)
48+
app.register_blueprint(blog.bp)
49+
50+
# make "index" point at "/", which is handled by "blog.index"
51+
app.add_url_rule("/", endpoint="index")
52+
53+
return app
54+
55+
56+
def init_db():
57+
db.drop_all()
58+
db.create_all()
59+
60+
61+
@click.command("init-db")
62+
@with_appcontext
63+
def init_db_command():
64+
"""Clear existing data and create new tables."""
65+
init_db()
66+
click.echo("Initialized the database.")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .views import bp

examples/flaskr/flaskr/auth/models.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from sqlalchemy.ext.hybrid import hybrid_property
2+
from werkzeug.security import check_password_hash
3+
from werkzeug.security import generate_password_hash
4+
5+
from flaskr import db
6+
7+
8+
class User(db.Model):
9+
id = db.Column(db.Integer, primary_key=True)
10+
username = db.Column(db.String, unique=True, nullable=False)
11+
_password = db.Column("password", db.String, nullable=False)
12+
13+
@hybrid_property
14+
def password(self):
15+
return self._password
16+
17+
@password.setter
18+
def password(self, value):
19+
"""Store the password as a hash for security."""
20+
self._password = generate_password_hash(value)
21+
22+
def check_password(self, value):
23+
return check_password_hash(self.password, value)

examples/flaskr/flaskr/auth/views.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import functools
2+
3+
from flask import Blueprint
4+
from flask import flash
5+
from flask import g
6+
from flask import redirect
7+
from flask import render_template
8+
from flask import request
9+
from flask import session
10+
from flask import url_for
11+
12+
from flaskr import db
13+
from flaskr.auth.models import User
14+
15+
bp = Blueprint("auth", __name__, url_prefix="/auth")
16+
17+
18+
def login_required(view):
19+
"""View decorator that redirects anonymous users to the login page."""
20+
21+
@functools.wraps(view)
22+
def wrapped_view(**kwargs):
23+
if g.user is None:
24+
return redirect(url_for("auth.login"))
25+
26+
return view(**kwargs)
27+
28+
return wrapped_view
29+
30+
31+
@bp.before_app_request
32+
def load_logged_in_user():
33+
"""If a user id is stored in the session, load the user object from
34+
the database into ``g.user``."""
35+
user_id = session.get("user_id")
36+
g.user = User.query.get(user_id) if user_id is not None else None
37+
38+
39+
@bp.route("/register", methods=("GET", "POST"))
40+
def register():
41+
"""Register a new user.
42+
43+
Validates that the username is not already taken. Hashes the
44+
password for security.
45+
"""
46+
if request.method == "POST":
47+
username = request.form["username"]
48+
password = request.form["password"]
49+
error = None
50+
51+
if not username:
52+
error = "Username is required."
53+
elif not password:
54+
error = "Password is required."
55+
elif db.session.query(
56+
User.query.filter_by(username=username).exists()
57+
).scalar():
58+
error = f"User {username} is already registered."
59+
60+
if error is None:
61+
# the name is available, create the user and go to the login page
62+
db.session.add(User(username=username, password=password))
63+
db.session.commit()
64+
return redirect(url_for("auth.login"))
65+
66+
flash(error)
67+
68+
return render_template("auth/register.html")
69+
70+
71+
@bp.route("/login", methods=("GET", "POST"))
72+
def login():
73+
"""Log in a registered user by adding the user id to the session."""
74+
if request.method == "POST":
75+
username = request.form["username"]
76+
password = request.form["password"]
77+
error = None
78+
user = User.query.filter_by(username=username).first()
79+
80+
if user is None:
81+
error = "Incorrect username."
82+
elif not user.check_password(password):
83+
error = "Incorrect password."
84+
85+
if error is None:
86+
# store the user id in a new session and return to the index
87+
session.clear()
88+
session["user_id"] = user.id
89+
return redirect(url_for("index"))
90+
91+
flash(error)
92+
93+
return render_template("auth/login.html")
94+
95+
96+
@bp.route("/logout")
97+
def logout():
98+
"""Clear the current session, including the stored user id."""
99+
session.clear()
100+
return redirect(url_for("index"))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .views import bp

examples/flaskr/flaskr/blog/models.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from flask import url_for
2+
3+
from flaskr import db
4+
from flaskr.auth.models import User
5+
6+
7+
class Post(db.Model):
8+
id = db.Column(db.Integer, primary_key=True)
9+
author_id = db.Column(db.ForeignKey(User.id), nullable=False)
10+
created = db.Column(
11+
db.DateTime, nullable=False, server_default=db.func.current_timestamp()
12+
)
13+
title = db.Column(db.String, nullable=False)
14+
body = db.Column(db.String, nullable=False)
15+
16+
# User object backed by author_id
17+
# lazy="joined" means the user is returned with the post in one query
18+
author = db.relationship(User, lazy="joined", backref="posts")
19+
20+
@property
21+
def update_url(self):
22+
return url_for("blog.update", id=self.id)
23+
24+
@property
25+
def delete_url(self):
26+
return url_for("blog.delete", id=self.id)

0 commit comments

Comments
 (0)