├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.MD ├── README.rst ├── codefresh.yml ├── docker-flask-codefresh.jpg ├── flaskr ├── __init__.py ├── auth.py ├── blog.py ├── db.py ├── schema.sql ├── static │ └── style.css └── templates │ ├── auth │ ├── login.html │ └── register.html │ ├── base.html │ └── blog │ ├── create.html │ ├── index.html │ └── update.html ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── conftest.py ├── data.sql ├── test_auth.py ├── test_blog.py ├── test_db.py └── test_factory.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .git -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eggs 2 | .pytest_cache/ 3 | minitwit.egg-info 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.2-alpine3.11 2 | 3 | ENV FLASK_APP=flaskr 4 | ENV FLASK_ENV=development 5 | 6 | COPY . /app 7 | 8 | WORKDIR /app 9 | 10 | RUN pip install --editable . 11 | 12 | RUN flask init-db 13 | 14 | # Unit tests 15 | # RUN pip install pytest && pytest 16 | 17 | EXPOSE 5000 18 | 19 | CMD [ "flask", "run", "--host=0.0.0.0" ] 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2010 by the Pallets team. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms of the software as 6 | well as documentation, with or without modification, are permitted 7 | provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 21 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 22 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 23 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 26 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 27 | USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 30 | THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF 31 | SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include flaskr/schema.sql 3 | graft flaskr/static 4 | graft flaskr/templates 5 | graft tests 6 | global-exclude *.pyc 7 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ## Flaskr an example application written in Python/Flask 2 | 3 | ![Flask plus Codefresh](docker-flask-codefresh.jpg) 4 | 5 | Original source code from https://github.com/pallets/flask/tree/master/examples/tutorial 6 | 7 | ## Docker instructions 8 | 9 | To create a docker image execute: 10 | 11 | `docker build . -t flaskr` 12 | 13 | To run the docker image execute: 14 | 15 | `docker run -p 5000:5000 flaskr` and visit with your browser http://localhost:5000 16 | 17 | To run unit tests inside the container execute: 18 | 19 | `docker run -it flaskr /bin/sh` 20 | 21 | and then in the new command promt run 22 | 23 | `pip install pytest && pytest` 24 | 25 | ## To use this project in Codefresh 26 | 27 | There is also a [codefresh.yml](codefresh.yml) for easy usage with the [Codefresh](codefresh.io) CI/CD platform. 28 | 29 | More details can be found in [Codefresh documentation](https://codefresh.io/docs/docs/getting-started/create-a-basic-pipeline/) 30 | 31 | #Edit 3 32 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flaskr 2 | ====== 3 | 4 | The basic blog app built in the Flask `tutorial`_. 5 | 6 | .. _tutorial: https://flask.palletsprojects.com/tutorial/ 7 | 8 | 9 | Install 10 | ------- 11 | 12 | **Be sure to use the same version of the code as the version of the docs 13 | you're reading.** You probably want the latest tagged version, but the 14 | default Git version is the master branch. :: 15 | 16 | # clone the repository 17 | $ git clone https://github.com/pallets/flask 18 | $ cd flask 19 | # checkout the correct version 20 | $ git tag # shows the tagged versions 21 | $ git checkout latest-tag-found-above 22 | $ cd examples/tutorial 23 | 24 | Create a virtualenv and activate it:: 25 | 26 | $ python3 -m venv venv 27 | $ . venv/bin/activate 28 | 29 | Or on Windows cmd:: 30 | 31 | $ py -3 -m venv venv 32 | $ venv\Scripts\activate.bat 33 | 34 | Install Flaskr:: 35 | 36 | $ pip install -e . 37 | 38 | Or if you are using the master branch, install Flask from source before 39 | installing Flaskr:: 40 | 41 | $ pip install -e ../.. 42 | $ pip install -e . 43 | 44 | 45 | Run 46 | --- 47 | 48 | :: 49 | 50 | $ export FLASK_APP=flaskr 51 | $ export FLASK_ENV=development 52 | $ flask init-db 53 | $ flask run 54 | 55 | Or on Windows cmd:: 56 | 57 | > set FLASK_APP=flaskr 58 | > set FLASK_ENV=development 59 | > flask init-db 60 | > flask run 61 | 62 | Open http://127.0.0.1:5000 in a browser. 63 | 64 | 65 | Test 66 | ---- 67 | 68 | :: 69 | 70 | $ pip install '.[test]' 71 | $ pytest 72 | 73 | Run with coverage report:: 74 | 75 | $ coverage run -m pytest 76 | $ coverage report 77 | $ coverage html # open htmlcov/index.html in a browser 78 | -------------------------------------------------------------------------------- /codefresh.yml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | stages: 3 | - checkout 4 | - package 5 | - test 6 | steps: 7 | main_clone: 8 | title: Cloning main repository... 9 | type: git-clone 10 | repo: '${{CF_REPO_OWNER}}/${{CF_REPO_NAME}}' 11 | revision: '${{CF_REVISION}}' 12 | stage: checkout 13 | MyAppDockerImage: 14 | title: Building Docker Image 15 | type: build 16 | stage: package 17 | image_name: my-app-image 18 | working_directory: ./ 19 | tag: v1.0.1 20 | dockerfile: Dockerfile 21 | disable_push: true 22 | MyUnitTests: 23 | title: Running Unit tests 24 | image: '${{MyAppDockerImage}}' 25 | stage: test 26 | commands: 27 | - pip install pytest 28 | - pytest 29 | -------------------------------------------------------------------------------- /docker-flask-codefresh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefresh-contrib/python-flask-sample-app/b5c6cd160a2b469ca337b47269d926f2464c2c61/docker-flask-codefresh.jpg -------------------------------------------------------------------------------- /flaskr/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | 5 | 6 | def create_app(test_config=None): 7 | """Create and configure an instance of the Flask application.""" 8 | app = Flask(__name__, instance_relative_config=True) 9 | app.config.from_mapping( 10 | # a default secret that should be overridden by instance config 11 | SECRET_KEY="dev", 12 | # store the database in the instance folder 13 | DATABASE=os.path.join(app.instance_path, "flaskr.sqlite"), 14 | ) 15 | 16 | if test_config is None: 17 | # load the instance config, if it exists, when not testing 18 | app.config.from_pyfile("config.py", silent=True) 19 | else: 20 | # load the test config if passed in 21 | app.config.update(test_config) 22 | 23 | # ensure the instance folder exists 24 | try: 25 | os.makedirs(app.instance_path) 26 | except OSError: 27 | pass 28 | 29 | @app.route("/hello") 30 | def hello(): 31 | return "Hello, World!" 32 | 33 | # register the database commands 34 | from flaskr import db 35 | 36 | db.init_app(app) 37 | 38 | # apply the blueprints to the app 39 | from flaskr import auth, blog 40 | 41 | app.register_blueprint(auth.bp) 42 | app.register_blueprint(blog.bp) 43 | 44 | # make url_for('index') == url_for('blog.index') 45 | # in another app, you might define a separate main index here with 46 | # app.route, while giving the blog blueprint a url_prefix, but for 47 | # the tutorial the blog will be the main index 48 | app.add_url_rule("/", endpoint="index") 49 | 50 | return app 51 | -------------------------------------------------------------------------------- /flaskr/auth.py: -------------------------------------------------------------------------------- 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 | from werkzeug.security import check_password_hash 12 | from werkzeug.security import generate_password_hash 13 | 14 | from flaskr.db import get_db 15 | 16 | bp = Blueprint("auth", __name__, url_prefix="/auth") 17 | 18 | 19 | def login_required(view): 20 | """View decorator that redirects anonymous users to the login page.""" 21 | 22 | @functools.wraps(view) 23 | def wrapped_view(**kwargs): 24 | if g.user is None: 25 | return redirect(url_for("auth.login")) 26 | 27 | return view(**kwargs) 28 | 29 | return wrapped_view 30 | 31 | 32 | @bp.before_app_request 33 | def load_logged_in_user(): 34 | """If a user id is stored in the session, load the user object from 35 | the database into ``g.user``.""" 36 | user_id = session.get("user_id") 37 | 38 | if user_id is None: 39 | g.user = None 40 | else: 41 | g.user = ( 42 | get_db().execute("SELECT * FROM user WHERE id = ?", (user_id,)).fetchone() 43 | ) 44 | 45 | 46 | @bp.route("/register", methods=("GET", "POST")) 47 | def register(): 48 | """Register a new user. 49 | 50 | Validates that the username is not already taken. Hashes the 51 | password for security. 52 | """ 53 | if request.method == "POST": 54 | username = request.form["username"] 55 | password = request.form["password"] 56 | db = get_db() 57 | error = None 58 | 59 | if not username: 60 | error = "Username is required." 61 | elif not password: 62 | error = "Password is required." 63 | elif ( 64 | db.execute("SELECT id FROM user WHERE username = ?", (username,)).fetchone() 65 | is not None 66 | ): 67 | error = "User {0} is already registered.".format(username) 68 | 69 | if error is None: 70 | # the name is available, store it in the database and go to 71 | # the login page 72 | db.execute( 73 | "INSERT INTO user (username, password) VALUES (?, ?)", 74 | (username, generate_password_hash(password)), 75 | ) 76 | db.commit() 77 | return redirect(url_for("auth.login")) 78 | 79 | flash(error) 80 | 81 | return render_template("auth/register.html") 82 | 83 | 84 | @bp.route("/login", methods=("GET", "POST")) 85 | def login(): 86 | """Log in a registered user by adding the user id to the session.""" 87 | if request.method == "POST": 88 | username = request.form["username"] 89 | password = request.form["password"] 90 | db = get_db() 91 | error = None 92 | user = db.execute( 93 | "SELECT * FROM user WHERE username = ?", (username,) 94 | ).fetchone() 95 | 96 | if user is None: 97 | error = "Incorrect username." 98 | elif not check_password_hash(user["password"], password): 99 | error = "Incorrect password." 100 | 101 | if error is None: 102 | # store the user id in a new session and return to the index 103 | session.clear() 104 | session["user_id"] = user["id"] 105 | return redirect(url_for("index")) 106 | 107 | flash(error) 108 | 109 | return render_template("auth/login.html") 110 | 111 | 112 | @bp.route("/logout") 113 | def logout(): 114 | """Clear the current session, including the stored user id.""" 115 | session.clear() 116 | return redirect(url_for("index")) 117 | -------------------------------------------------------------------------------- /flaskr/blog.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask import flash 3 | from flask import g 4 | from flask import redirect 5 | from flask import render_template 6 | from flask import request 7 | from flask import url_for 8 | from werkzeug.exceptions import abort 9 | 10 | from flaskr.auth import login_required 11 | from flaskr.db import get_db 12 | 13 | bp = Blueprint("blog", __name__) 14 | 15 | 16 | @bp.route("/") 17 | def index(): 18 | """Show all the posts, most recent first.""" 19 | db = get_db() 20 | posts = db.execute( 21 | "SELECT p.id, title, body, created, author_id, username" 22 | " FROM post p JOIN user u ON p.author_id = u.id" 23 | " ORDER BY created DESC" 24 | ).fetchall() 25 | return render_template("blog/index.html", posts=posts) 26 | 27 | 28 | def get_post(id, check_author=True): 29 | """Get a post and its author by id. 30 | 31 | Checks that the id exists and optionally that the current user is 32 | the author. 33 | 34 | :param id: id of post to get 35 | :param check_author: require the current user to be the author 36 | :return: the post with author information 37 | :raise 404: if a post with the given id doesn't exist 38 | :raise 403: if the current user isn't the author 39 | """ 40 | post = ( 41 | get_db() 42 | .execute( 43 | "SELECT p.id, title, body, created, author_id, username" 44 | " FROM post p JOIN user u ON p.author_id = u.id" 45 | " WHERE p.id = ?", 46 | (id,), 47 | ) 48 | .fetchone() 49 | ) 50 | 51 | if post is None: 52 | abort(404, "Post id {0} doesn't exist.".format(id)) 53 | 54 | if check_author and post["author_id"] != g.user["id"]: 55 | abort(403) 56 | 57 | return post 58 | 59 | 60 | @bp.route("/create", methods=("GET", "POST")) 61 | @login_required 62 | def create(): 63 | """Create a new post for the current user.""" 64 | if request.method == "POST": 65 | title = request.form["title"] 66 | body = request.form["body"] 67 | error = None 68 | 69 | if not title: 70 | error = "Title is required." 71 | 72 | if error is not None: 73 | flash(error) 74 | else: 75 | db = get_db() 76 | db.execute( 77 | "INSERT INTO post (title, body, author_id) VALUES (?, ?, ?)", 78 | (title, body, g.user["id"]), 79 | ) 80 | db.commit() 81 | return redirect(url_for("blog.index")) 82 | 83 | return render_template("blog/create.html") 84 | 85 | 86 | @bp.route("//update", methods=("GET", "POST")) 87 | @login_required 88 | def update(id): 89 | """Update a post if the current user is the author.""" 90 | post = get_post(id) 91 | 92 | if request.method == "POST": 93 | title = request.form["title"] 94 | body = request.form["body"] 95 | error = None 96 | 97 | if not title: 98 | error = "Title is required." 99 | 100 | if error is not None: 101 | flash(error) 102 | else: 103 | db = get_db() 104 | db.execute( 105 | "UPDATE post SET title = ?, body = ? WHERE id = ?", (title, body, id) 106 | ) 107 | db.commit() 108 | return redirect(url_for("blog.index")) 109 | 110 | return render_template("blog/update.html", post=post) 111 | 112 | 113 | @bp.route("//delete", methods=("POST",)) 114 | @login_required 115 | def delete(id): 116 | """Delete a post. 117 | 118 | Ensures that the post exists and that the logged in user is the 119 | author of the post. 120 | """ 121 | get_post(id) 122 | db = get_db() 123 | db.execute("DELETE FROM post WHERE id = ?", (id,)) 124 | db.commit() 125 | return redirect(url_for("blog.index")) 126 | -------------------------------------------------------------------------------- /flaskr/db.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | import click 4 | from flask import current_app 5 | from flask import g 6 | from flask.cli import with_appcontext 7 | 8 | 9 | def get_db(): 10 | """Connect to the application's configured database. The connection 11 | is unique for each request and will be reused if this is called 12 | again. 13 | """ 14 | if "db" not in g: 15 | g.db = sqlite3.connect( 16 | current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES 17 | ) 18 | g.db.row_factory = sqlite3.Row 19 | 20 | return g.db 21 | 22 | 23 | def close_db(e=None): 24 | """If this request connected to the database, close the 25 | connection. 26 | """ 27 | db = g.pop("db", None) 28 | 29 | if db is not None: 30 | db.close() 31 | 32 | 33 | def init_db(): 34 | """Clear existing data and create new tables.""" 35 | db = get_db() 36 | 37 | with current_app.open_resource("schema.sql") as f: 38 | db.executescript(f.read().decode("utf8")) 39 | 40 | 41 | @click.command("init-db") 42 | @with_appcontext 43 | def init_db_command(): 44 | """Clear existing data and create new tables.""" 45 | init_db() 46 | click.echo("Initialized the database.") 47 | 48 | 49 | def init_app(app): 50 | """Register database functions with the Flask app. This is called by 51 | the application factory. 52 | """ 53 | app.teardown_appcontext(close_db) 54 | app.cli.add_command(init_db_command) 55 | -------------------------------------------------------------------------------- /flaskr/schema.sql: -------------------------------------------------------------------------------- 1 | -- Initialize the database. 2 | -- Drop any existing data and create empty tables. 3 | 4 | DROP TABLE IF EXISTS user; 5 | DROP TABLE IF EXISTS post; 6 | 7 | CREATE TABLE user ( 8 | id INTEGER PRIMARY KEY AUTOINCREMENT, 9 | username TEXT UNIQUE NOT NULL, 10 | password TEXT NOT NULL 11 | ); 12 | 13 | CREATE TABLE post ( 14 | id INTEGER PRIMARY KEY AUTOINCREMENT, 15 | author_id INTEGER NOT NULL, 16 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | title TEXT NOT NULL, 18 | body TEXT NOT NULL, 19 | FOREIGN KEY (author_id) REFERENCES user (id) 20 | ); 21 | -------------------------------------------------------------------------------- /flaskr/static/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | background: #eee; 4 | padding: 1rem; 5 | } 6 | 7 | body { 8 | max-width: 960px; 9 | margin: 0 auto; 10 | background: white; 11 | } 12 | 13 | h1, h2, h3, h4, h5, h6 { 14 | font-family: serif; 15 | color: #377ba8; 16 | margin: 1rem 0; 17 | } 18 | 19 | a { 20 | color: #377ba8; 21 | } 22 | 23 | hr { 24 | border: none; 25 | border-top: 1px solid lightgray; 26 | } 27 | 28 | nav { 29 | background: lightgray; 30 | display: flex; 31 | align-items: center; 32 | padding: 0 0.5rem; 33 | } 34 | 35 | nav h1 { 36 | flex: auto; 37 | margin: 0; 38 | } 39 | 40 | nav h1 a { 41 | text-decoration: none; 42 | padding: 0.25rem 0.5rem; 43 | } 44 | 45 | nav ul { 46 | display: flex; 47 | list-style: none; 48 | margin: 0; 49 | padding: 0; 50 | } 51 | 52 | nav ul li a, nav ul li span, header .action { 53 | display: block; 54 | padding: 0.5rem; 55 | } 56 | 57 | .content { 58 | padding: 0 1rem 1rem; 59 | } 60 | 61 | .content > header { 62 | border-bottom: 1px solid lightgray; 63 | display: flex; 64 | align-items: flex-end; 65 | } 66 | 67 | .content > header h1 { 68 | flex: auto; 69 | margin: 1rem 0 0.25rem 0; 70 | } 71 | 72 | .flash { 73 | margin: 1em 0; 74 | padding: 1em; 75 | background: #cae6f6; 76 | border: 1px solid #377ba8; 77 | } 78 | 79 | .post > header { 80 | display: flex; 81 | align-items: flex-end; 82 | font-size: 0.85em; 83 | } 84 | 85 | .post > header > div:first-of-type { 86 | flex: auto; 87 | } 88 | 89 | .post > header h1 { 90 | font-size: 1.5em; 91 | margin-bottom: 0; 92 | } 93 | 94 | .post .about { 95 | color: slategray; 96 | font-style: italic; 97 | } 98 | 99 | .post .body { 100 | white-space: pre-line; 101 | } 102 | 103 | .content:last-child { 104 | margin-bottom: 0; 105 | } 106 | 107 | .content form { 108 | margin: 1em 0; 109 | display: flex; 110 | flex-direction: column; 111 | } 112 | 113 | .content label { 114 | font-weight: bold; 115 | margin-bottom: 0.5em; 116 | } 117 | 118 | .content input, .content textarea { 119 | margin-bottom: 1em; 120 | } 121 | 122 | .content textarea { 123 | min-height: 12em; 124 | resize: vertical; 125 | } 126 | 127 | input.danger { 128 | color: #cc2f2e; 129 | } 130 | 131 | input[type=submit] { 132 | align-self: start; 133 | min-width: 10em; 134 | } 135 | -------------------------------------------------------------------------------- /flaskr/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Log In{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /flaskr/templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Register{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /flaskr/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% block title %}{% endblock %} - Flaskr 3 | 4 | 16 |
17 |
18 | {% block header %}{% endblock %} 19 |
20 | {% for message in get_flashed_messages() %} 21 |
{{ message }}
22 | {% endfor %} 23 | {% block content %}{% endblock %} 24 |
25 | -------------------------------------------------------------------------------- /flaskr/templates/blog/create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}New Post{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /flaskr/templates/blog/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Posts{% endblock %}

5 | {% if g.user %} 6 | New 7 | {% endif %} 8 | {% endblock %} 9 | 10 | {% block content %} 11 | {% for post in posts %} 12 |
13 |
14 |
15 |

{{ post['title'] }}

16 |
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
17 |
18 | {% if g.user['id'] == post['author_id'] %} 19 | Edit 20 | {% endif %} 21 |
22 |

{{ post['body'] }}

23 |
24 | {% if not loop.last %} 25 |
26 | {% endif %} 27 | {% endfor %} 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /flaskr/templates/blog/update.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block header %} 4 |

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.0 # via flask 2 | flask==1.1.1 # via flaskr (setup.py) 3 | itsdangerous==1.1.0 # via flask 4 | jinja2==2.11.1 # via flask 5 | markupsafe==1.1.1 # via jinja2 6 | werkzeug==1.0.0 # via flask 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | 4 | [bdist_wheel] 5 | universal = True 6 | 7 | [tool:pytest] 8 | testpaths = tests 9 | 10 | [coverage:run] 11 | branch = True 12 | source = 13 | flaskr 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | with io.open("README.rst", "rt", encoding="utf8") as f: 7 | readme = f.read() 8 | 9 | setup( 10 | name="flaskr", 11 | version="1.0.0", 12 | url="https://flask.palletsprojects.com/tutorial/", 13 | license="BSD", 14 | maintainer="Pallets team", 15 | maintainer_email="contact@palletsprojects.com", 16 | description="The basic blog app built in the Flask tutorial.", 17 | long_description=readme, 18 | packages=find_packages(), 19 | include_package_data=True, 20 | zip_safe=False, 21 | install_requires=["flask"], 22 | extras_require={"test": ["pytest", "coverage"]}, 23 | ) 24 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | import pytest 5 | 6 | from flaskr import create_app 7 | from flaskr.db import get_db 8 | from flaskr.db import init_db 9 | 10 | # read in SQL for populating test data 11 | with open(os.path.join(os.path.dirname(__file__), "data.sql"), "rb") as f: 12 | _data_sql = f.read().decode("utf8") 13 | 14 | 15 | @pytest.fixture 16 | def app(): 17 | """Create and configure a new app instance for each test.""" 18 | # create a temporary file to isolate the database for each test 19 | db_fd, db_path = tempfile.mkstemp() 20 | # create the app with common test config 21 | app = create_app({"TESTING": True, "DATABASE": db_path}) 22 | 23 | # create the database and load test data 24 | with app.app_context(): 25 | init_db() 26 | get_db().executescript(_data_sql) 27 | 28 | yield app 29 | 30 | # close and remove the temporary database 31 | os.close(db_fd) 32 | os.unlink(db_path) 33 | 34 | 35 | @pytest.fixture 36 | def client(app): 37 | """A test client for the app.""" 38 | return app.test_client() 39 | 40 | 41 | @pytest.fixture 42 | def runner(app): 43 | """A test runner for the app's Click commands.""" 44 | return app.test_cli_runner() 45 | 46 | 47 | class AuthActions(object): 48 | def __init__(self, client): 49 | self._client = client 50 | 51 | def login(self, username="test", password="test"): 52 | return self._client.post( 53 | "/auth/login", data={"username": username, "password": password} 54 | ) 55 | 56 | def logout(self): 57 | return self._client.get("/auth/logout") 58 | 59 | 60 | @pytest.fixture 61 | def auth(client): 62 | return AuthActions(client) 63 | -------------------------------------------------------------------------------- /tests/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO user (username, password) 2 | VALUES 3 | ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), 4 | ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); 5 | 6 | INSERT INTO post (title, body, author_id, created) 7 | VALUES 8 | ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); 9 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask import g 3 | from flask import session 4 | 5 | from flaskr.db import get_db 6 | 7 | 8 | def test_register(client, app): 9 | # test that viewing the page renders without template errors 10 | assert client.get("/auth/register").status_code == 200 11 | 12 | # test that successful registration redirects to the login page 13 | response = client.post("/auth/register", data={"username": "a", "password": "a"}) 14 | assert "http://localhost/auth/login" == response.headers["Location"] 15 | 16 | # test that the user was inserted into the database 17 | with app.app_context(): 18 | assert ( 19 | get_db().execute("select * from user where username = 'a'").fetchone() 20 | is not None 21 | ) 22 | 23 | 24 | @pytest.mark.parametrize( 25 | ("username", "password", "message"), 26 | ( 27 | ("", "", b"Username is required."), 28 | ("a", "", b"Password is required."), 29 | ("test", "test", b"already registered"), 30 | ), 31 | ) 32 | def test_register_validate_input(client, username, password, message): 33 | response = client.post( 34 | "/auth/register", data={"username": username, "password": password} 35 | ) 36 | assert message in response.data 37 | 38 | 39 | def test_login(client, auth): 40 | # test that viewing the page renders without template errors 41 | assert client.get("/auth/login").status_code == 200 42 | 43 | # test that successful login redirects to the index page 44 | response = auth.login() 45 | assert response.headers["Location"] == "http://localhost/" 46 | 47 | # login request set the user_id in the session 48 | # check that the user is loaded from the session 49 | with client: 50 | client.get("/") 51 | assert session["user_id"] == 1 52 | assert g.user["username"] == "test" 53 | 54 | 55 | @pytest.mark.parametrize( 56 | ("username", "password", "message"), 57 | (("a", "test", b"Incorrect username."), ("test", "a", b"Incorrect password.")), 58 | ) 59 | def test_login_validate_input(auth, username, password, message): 60 | response = auth.login(username, password) 61 | assert message in response.data 62 | 63 | 64 | def test_logout(client, auth): 65 | auth.login() 66 | 67 | with client: 68 | auth.logout() 69 | assert "user_id" not in session 70 | -------------------------------------------------------------------------------- /tests/test_blog.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flaskr.db import get_db 4 | 5 | 6 | def test_index(client, auth): 7 | response = client.get("/") 8 | assert b"Log In" in response.data 9 | assert b"Register" in response.data 10 | 11 | auth.login() 12 | response = client.get("/") 13 | assert b"test title" in response.data 14 | assert b"by test on 2018-01-01" in response.data 15 | assert b"test\nbody" in response.data 16 | assert b'href="/1/update"' in response.data 17 | 18 | 19 | @pytest.mark.parametrize("path", ("/create", "/1/update", "/1/delete")) 20 | def test_login_required(client, path): 21 | response = client.post(path) 22 | assert response.headers["Location"] == "http://localhost/auth/login" 23 | 24 | 25 | def test_author_required(app, client, auth): 26 | # change the post author to another user 27 | with app.app_context(): 28 | db = get_db() 29 | db.execute("UPDATE post SET author_id = 2 WHERE id = 1") 30 | db.commit() 31 | 32 | auth.login() 33 | # current user can't modify other user's post 34 | assert client.post("/1/update").status_code == 403 35 | assert client.post("/1/delete").status_code == 403 36 | # current user doesn't see edit link 37 | assert b'href="/1/update"' not in client.get("/").data 38 | 39 | 40 | @pytest.mark.parametrize("path", ("/2/update", "/2/delete")) 41 | def test_exists_required(client, auth, path): 42 | auth.login() 43 | assert client.post(path).status_code == 404 44 | 45 | 46 | def test_create(client, auth, app): 47 | auth.login() 48 | assert client.get("/create").status_code == 200 49 | client.post("/create", data={"title": "created", "body": ""}) 50 | 51 | with app.app_context(): 52 | db = get_db() 53 | count = db.execute("SELECT COUNT(id) FROM post").fetchone()[0] 54 | assert count == 2 55 | 56 | 57 | def test_update(client, auth, app): 58 | auth.login() 59 | assert client.get("/1/update").status_code == 200 60 | client.post("/1/update", data={"title": "updated", "body": ""}) 61 | 62 | with app.app_context(): 63 | db = get_db() 64 | post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() 65 | assert post["title"] == "updated" 66 | 67 | 68 | @pytest.mark.parametrize("path", ("/create", "/1/update")) 69 | def test_create_update_validate(client, auth, path): 70 | auth.login() 71 | response = client.post(path, data={"title": "", "body": ""}) 72 | assert b"Title is required." in response.data 73 | 74 | 75 | def test_delete(client, auth, app): 76 | auth.login() 77 | response = client.post("/1/delete") 78 | assert response.headers["Location"] == "http://localhost/" 79 | 80 | with app.app_context(): 81 | db = get_db() 82 | post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() 83 | assert post is None 84 | -------------------------------------------------------------------------------- /tests/test_db.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | import pytest 4 | 5 | from flaskr.db import get_db 6 | 7 | 8 | def test_get_close_db(app): 9 | with app.app_context(): 10 | db = get_db() 11 | assert db is get_db() 12 | 13 | with pytest.raises(sqlite3.ProgrammingError) as e: 14 | db.execute("SELECT 1") 15 | 16 | assert "closed" in str(e.value) 17 | 18 | 19 | def test_init_db_command(runner, monkeypatch): 20 | class Recorder(object): 21 | called = False 22 | 23 | def fake_init_db(): 24 | Recorder.called = True 25 | 26 | monkeypatch.setattr("flaskr.db.init_db", fake_init_db) 27 | result = runner.invoke(args=["init-db"]) 28 | assert "Initialized" in result.output 29 | assert Recorder.called 30 | -------------------------------------------------------------------------------- /tests/test_factory.py: -------------------------------------------------------------------------------- 1 | from flaskr import create_app 2 | 3 | 4 | def test_config(): 5 | """Test create_app without passing test config.""" 6 | assert not create_app().testing 7 | assert create_app({"TESTING": True}).testing 8 | 9 | 10 | def test_hello(client): 11 | response = client.get("/hello") 12 | assert response.data == b"Hello, World!" 13 | --------------------------------------------------------------------------------