From 41e87aeaf46cc407f6b78bf122897f60f2e5a7b6 Mon Sep 17 00:00:00 2001 From: Michael Herman Date: Fri, 3 Jun 2022 13:17:18 -0500 Subject: [PATCH 1/3] updates --- LICENSE | 2 +- README.md | 117 ++++++++++++------------ create_db.py | 11 --- project/flaskr.db => flaskr.db | Bin 8192 -> 12288 bytes project/app.py | 157 ++++++++++++++++----------------- project/models.py | 14 --- project/schema.sql | 7 ++ project/templates/index.html | 60 ++++--------- project/templates/login.html | 54 ++++-------- project/templates/search.html | 65 -------------- requirements.txt | 9 +- runtime.txt | 2 +- test.db | Bin 16384 -> 12288 bytes tests/app_test.py | 19 ++-- 14 files changed, 190 insertions(+), 327 deletions(-) delete mode 100644 create_db.py rename project/flaskr.db => flaskr.db (63%) delete mode 100644 project/models.py create mode 100644 project/schema.sql delete mode 100644 project/templates/search.html diff --git a/LICENSE b/LICENSE index 14a9bd9..b3f8034 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Michael Herman +Copyright (c) 2022 Michael Herman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7292e07..665cfb4 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ As many of you know, Flaskr -- a mini-blog-like-app -- is the app that you build Also, if you're completely new to Flask and/or web development in general, it's important to grasp these basic fundamental concepts: -1. The difference between GET and POST requests and how functions within the app handle each. -1. What "requests" and "responses" are. +1. The difference between HTTP GET and POST requests and how functions within the app handle each. +1. What HTTP "requests" and "responses" are. 1. How HTML pages are rendered and/or returned to the end user. > This project is powered by **[TestDriven.io](https://testdriven.io/)**. Please support this open source project by purchasing one of our Flask courses. Learn how to build, test, and deploy microservices powered by Docker, Flask, and React! @@ -22,12 +22,14 @@ You'll be building a simple blogging app in this tutorial: ## Changelog -This tutorial was last updated on October 14th, 2020: +This tutorial was last updated on June 3rd, 2022: +- **06/03/2022**: + - Updated to Python 3.10.4 and bumped all other dependencies. - **10/14/2020**: - Renamed *app.test.py* to *app_test.py*. (Fixed issue #[58](https://github.com/mjhea0/flaskr-tdd/issues/58).) - Updated to Python 3.9 and bumped all other dependencies. - - Added pytest v6.1.1. (Fixed issue #[60](https://github.com/mjhea0/flaskr-tdd/issues/60)) + - Added pytest v7.1.2. (Fixed issue #[60](https://github.com/mjhea0/flaskr-tdd/issues/60)) - Migrated from `os.path` to `pathlib`. - **11/05/2019**: - Updated to Python 3.8.0, Flask 1.1.1, and Bootstrap 4.3.1. @@ -76,14 +78,14 @@ This tutorial was last updated on October 14th, 2020: This tutorial utilizes the following requirements: -1. Python v3.9.0 -1. Flask v1.1.2 +1. Python v3.10.4 +1. Flask v2.1.1 1. Flask-SQLAlchemy v2.5.1 -1. Gunicorn v20.0.4 -1. Psycopg2 v2.8.6 -1. Flake8 v3.8.4 -1. Black v20.8b1 -1. pytest v6.1.1 +1. Gunicorn v20.1.0 +1. Psycopg2 v3.0.14 +1. Flake8 v4.0.1 +1. Black v22.3.0 +1. pytest v7.1.2 ## Test Driven Development? @@ -104,14 +106,14 @@ TDD usually follows the "Red-Green-Refactor" cycle, as shown in the image above: Before beginning make sure you have the latest version of [Python 3.9](https://www.python.org/downloads/release/python-390/) installed, which you can download from [http://www.python.org/download/](http://www.python.org/download/). -> This tutorial uses Python v3.9.0. +> This tutorial uses Python v3.10.4. Along with Python, the following tools are also installed: - [pip](https://pip.pypa.io/en/stable/) - a [package management](http://en.wikipedia.org/wiki/Package_management_system) system for Python, similar to gem or npm for Ruby and Node, respectively. - [venv](https://docs.python.org/3/library/venv.html) - used to create isolated environments for development. This is standard practice. Always, always, ALWAYS utilize virtual environments. If you don't, you will eventually run into problems with dependency conflicts. -> Feel free to swap out virtualenv and Pip for [Poetry](https://python-poetry.org/) or [Pipenv](https://pipenv.pypa.io/en/latest/). +> Feel free to swap out virtualenv and Pip for [Poetry](https://python-poetry.org) or [Pipenv](https://github.com/pypa/pipenv). For more, review [Modern Python Environments](https://testdriven.io/blog/python-environments/). ## Project Setup @@ -125,7 +127,7 @@ $ cd flaskr-tdd Create and activate a virtual environment: ```sh -$ python3.9 -m venv env +$ python3.10 -m venv env $ source env/bin/activate (env)$ ``` @@ -135,7 +137,7 @@ $ source env/bin/activate Install Flask with pip: ```sh -(env)$ pip install flask==1.1.2 +(env)$ pip install flask==2.1.1 ``` ## First Test @@ -158,7 +160,7 @@ While the Python standard library comes with a unit testing framework called ùn Install it: ```sh -(env)$ pip install pytest==6.1.1 +(env)$ pip install pytest==7.1.2 ``` Open *tests/app_test.py* in your favorite text editor -- like [Visual Studio Code](https://code.visualstudio.com/), [Sublime Text](https://www.sublimetext.com/), or [PyCharm](https://www.jetbrains.com/pycharm/) -- and then add the following code: @@ -166,6 +168,7 @@ Open *tests/app_test.py* in your favorite text editor -- like [Visual Studio Cod ```python from project.app import app + def test_index(): tester = app.test_client() response = tester.get("/", content_type="html/text") @@ -224,14 +227,14 @@ Run the test again: ```sh (env)$ python -m pytest -==================================================== test session starts ===================================================== -platform darwin -- Python 3.9.0, pytest-6.1.1, py-1.9.0, pluggy-0.13.1 +=============================== test session starts =============================== +platform darwin -- Python 3.10.4, pytest-7.1.2, pluggy-1.0.0 rootdir: /Users/michael/repos/github/flaskr-tdd collected 1 item -tests/app_test.py . [100%] +tests/app_test.py . [100%] -===================================================== 1 passed in 0.11s ====================================================== +================================ 1 passed in 0.10s ================================ ``` Nice. @@ -244,6 +247,7 @@ Create a new file called *schema.sql* in "project" and add the following code: ```sql drop table if exists entries; + create table entries ( id integer primary key autoincrement, title text not null, @@ -251,7 +255,7 @@ create table entries ( ); ``` -This will set up a single table with three fields -- "id", "title", and "text". SQLite will be used for our RDMS since it's part of the standard Python library and requires no configuration. +This will set up a single table with three fields: "id", "title", and "text". SQLite will be used for our RDMS since it's part of the standard Python library and requires no configuration. Update *app.py*: @@ -340,6 +344,7 @@ Add the imports: ```python import sqlite3 + from flask import Flask, g ``` @@ -349,6 +354,7 @@ You should now have: ```python import sqlite3 + from flask import Flask, g @@ -547,27 +553,27 @@ Run the tests now: Three tests should fail: ```sh -==================================================== test session starts ===================================================== -platform darwin -- Python 3.9.0, pytest-6.1.1, py-1.9.0, pluggy-0.13.1 +=============================== test session starts =============================== +platform darwin -- Python 3.10.4, pytest-7.1.2, pluggy-1.0.0 rootdir: /Users/michael/repos/github/flaskr-tdd collected 5 items -tests/app_test.py ..FFF [100%] +tests/app_test.py ..FFF [100%] -========================================================== FAILURES ========================================================== -_______________________________________________________ test_empty_db ________________________________________________________ +==================================== FAILURES ===================================== +__________________________________ test_empty_db __________________________________ client = > def test_empty_db(client): """Ensure database is blank""" rv = client.get("/") -> assert b"No entries here so far" in rv.data +> assert b"No entries yet. Add some!" in rv.data E AssertionError: assert b'No entries yet. Add some!' in b'Hello, World!' -E + where b'Hello, World!' = .data +E + where b'Hello, World!' = .data tests/app_test.py:49: AssertionError -_____________________________________________________ test_login_logout ______________________________________________________ +________________________________ test_login_logout ________________________________ client = > @@ -577,7 +583,7 @@ client = > E KeyError: 'USERNAME' tests/app_test.py:54: KeyError -_______________________________________________________ test_messages ________________________________________________________ +__________________________________ test_messages __________________________________ client = > @@ -587,11 +593,12 @@ client = > E KeyError: 'USERNAME' tests/app_test.py:66: KeyError -================================================== short test summary info =================================================== -FAILED tests/app_test.py::test_empty_db - AssertionError: assert b'No entries yet. Add some!' in b'Hello, World!' +============================= short test summary info ============================= +FAILED tests/app_test.py::test_empty_db - + AssertionError: assert b'No entries yet. Add some!' in b'Hello, World!' FAILED tests/app_test.py::test_login_logout - KeyError: 'USERNAME' FAILED tests/app_test.py::test_messages - KeyError: 'USERNAME' -================================================ 3 failed, 2 passed in 0.27s ================================================= +=========================== 3 failed, 2 passed in 0.17s ========================== ``` Let's get these all green, one at a time... @@ -800,17 +807,17 @@ Add the appropriate imports: from flask import Flask, g, render_template, request, session, flash, redirect, url_for, abort ``` -Retest. +Retest: ```sh -==================================================== test session starts ===================================================== -platform darwin -- Python 3.9.0, pytest-6.1.1, py-1.9.0, pluggy-0.13.1 +=============================== test session starts =============================== +platform darwin -- Python 3.10.4, pytest-7.1.2, pluggy-1.0.0 rootdir: /Users/michael/repos/github/flaskr-tdd collected 5 items -tests/app_test.py ..... [100%] +tests/app_test.py ..... [100%] -===================================================== 5 passed in 0.26s ====================================================== +================================ 5 passed in 0.16s ================================ ``` Perfect. @@ -993,14 +1000,14 @@ Then run your automated test suite. It should pass: ```sh (env)$ python -m pytest -==================================================== test session starts ===================================================== -platform darwin -- Python 3.9.0, pytest-6.1.1, py-1.9.0, pluggy-0.13.1 +=============================== test session starts =============================== +platform darwin -- Python 3.10.4, pytest-7.1.2, pluggy-1.0.0 rootdir: /Users/michael/repos/github/flaskr-tdd collected 6 items -tests/app_test.py ...... [100%] +tests/app_test.py ...... [100%] -===================================================== 6 passed in 0.18s ====================================================== +================================ 6 passed in 0.17s ================================ ``` ## Deployment @@ -1034,9 +1041,9 @@ Create a *requirements.txt* file to specify the external dependencies that need Add the requirements: ``` -Flask==1.1.2 +Flask==2.1.1 gunicorn==20.0.4 -pytest==6.1.1 +pytest==7.1.2 ``` Create a *.gitignore* file in the project root: @@ -1058,7 +1065,7 @@ test.db To specify the correct Python runtime, add a new file to the project root called *runtime.txt*: ``` -python-3.9.0 +python-3.10.4 ``` Add a local Git repo: @@ -1442,7 +1449,7 @@ Ensure the tests pass: (env)$ python -m pytest ==================================================== test session starts ===================================================== -platform darwin -- Python 3.9.0, pytest-6.1.1, py-1.9.0, pluggy-0.13.1 +platform darwin -- Python 3.10.4, pytest-7.1.2, py-1.9.0, pluggy-0.13.1 rootdir: /Users/michael/repos/github/flaskr-tdd collected 6 items @@ -1456,10 +1463,10 @@ Manually test the app as well by running the server and logging in and out, addi If all is well, Update the requirements file: ``` -Flask==1.1.2 +Flask==2.1.1 Flask-SQLAlchemy==2.5.1 gunicorn==20.0.4 -pytest==6.1.1 +pytest==7.1.2 ``` Commit your code, and then push the new version to Heroku! @@ -1676,7 +1683,7 @@ Run the tests to ensure they still pass: (env)$ python -m pytest ==================================================== test session starts ===================================================== -platform darwin -- Python 3.9.0, pytest-6.1.1, py-1.9.0, pluggy-0.13.1 +platform darwin -- Python 3.10.4, pytest-7.1.2, py-1.9.0, pluggy-0.13.1 rootdir: /Users/michael/repos/github/flaskr-tdd collected 6 items @@ -1690,11 +1697,11 @@ Try logging in and out, adding a few new entries, and deleting old entries local Before updating Heroku, add [Psycopg2](http://initd.org/psycopg/) -- a Postgres database adapter for Python -- to the requirements file: ``` -Flask==1.1.2 +Flask==2.1.1 Flask-SQLAlchemy==2.5.1 gunicorn==20.0.4 -psycopg2-binary==2.8.6 -pytest==6.1.1 +psycopg2-binary==3.0.14 +pytest==7.1.2 ``` Commit and push your code up to Heroku. @@ -1712,8 +1719,8 @@ Test things out. Finally, we can lint and auto format our code with [Flake8](http://flake8.pycqa.org/) and [Black](https://black.readthedocs.io/), respectively: ```sh -(env)$ pip install flake8==3.8.4 -(env)$ pip install black==20.8b1 +(env)$ pip install flake8==4.0.1 +(env)$ pip install black==22.3.0 ``` Run Flake8 and correct any issues: @@ -1750,3 +1757,5 @@ Test everything out once last time! 1. Want something else added to this tutorial? Add an issue to the repo. > Did you enjoy this tutorial? Please [Share on Twitter](https://twitter.com/intent/tweet?text=Check%20out%20Flaskr%E2%80%94An%20intro%20to%20Flask%2C%20Test-Driven%20Development%2C%20and%20JavaScript%21%20https%3A%2F%2Fgithub.com%2Fmjhea0%2Fflaskr-tdd%20%23webdev%0A). + +add github actions diff --git a/create_db.py b/create_db.py deleted file mode 100644 index 89ce3af..0000000 --- a/create_db.py +++ /dev/null @@ -1,11 +0,0 @@ -# create_db.py - - -from project.app import db - - -# create the database and the db table -db.create_all() - -# commit the changes -db.session.commit() diff --git a/project/flaskr.db b/flaskr.db similarity index 63% rename from project/flaskr.db rename to flaskr.db index 884c0a0ae64b19c8fb8e75f62e841c045f05212e..ca1ea0eb8ad5178f1fb4ce1df0746fb0889bb087 100644 GIT binary patch delta 323 zcmZp0Xh@hK&C1Ncz`#6F!CsJ=LC-{i7szAcS7G2U<5$^OxP`AifQe09TbnVmBrz!` zwYV@Rvm`aXIJK}eH7^;R%j_KF>KNjx5aQ_Mg%%D$3ZHUy_)V zlUk5pTmr&O&Oxq@A+8D`j!r(V3Lp^$4KB{i6a`Pe5Lb8CAO%1F5Cy+bA0HhBF3yt7 zlAKh9FvlQg4@Wea)QS?U5&=P;zK%hW3S!=_kqR1_DVkiG5SM*n;Qzw^0_e7f{K8^D Tw*mndBMXDDa9VzTQeqJRLkBY~ diff --git a/project/app.py b/project/app.py index 81fca0e..70c147c 100644 --- a/project/app.py +++ b/project/app.py @@ -1,122 +1,113 @@ -import os -from functools import wraps -from pathlib import Path - -from flask import ( - Flask, - render_template, - request, - session, - flash, - redirect, - url_for, - jsonify, - abort, -) -from flask_sqlalchemy import SQLAlchemy - - -basedir = Path(__file__).resolve().parent +import sqlite3 + +from flask import Flask, g, render_template, request, session, flash, redirect, url_for, abort, jsonify + # configuration DATABASE = "flaskr.db" USERNAME = "admin" PASSWORD = "admin" SECRET_KEY = "change_me" -SQLALCHEMY_DATABASE_URI = os.getenv( - "DATABASE_URL", f"sqlite:///{Path(basedir).joinpath(DATABASE)}" -) -SQLALCHEMY_TRACK_MODIFICATIONS = False - # create and initialize a new Flask app app = Flask(__name__) + # load the config app.config.from_object(__name__) -# init sqlalchemy -db = SQLAlchemy(app) -from project import models +# connect to database +def connect_db(): + """Connects to the database.""" + rv = sqlite3.connect(app.config["DATABASE"]) + rv.row_factory = sqlite3.Row + return rv -def login_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - if not session.get("logged_in"): - flash("Please log in.") - return jsonify({"status": 0, "message": "Please log in."}), 401 - return f(*args, **kwargs) - return decorated_function +# create the database +def init_db(): + with app.app_context(): + db = get_db() + with app.open_resource("schema.sql", mode="r") as f: + db.cursor().executescript(f.read()) + db.commit() -@app.route("/") -def index(): - """Searches the database for entries, then displays them.""" - entries = db.session.query(models.Post) - return render_template("index.html", entries=entries) +# open database connection +def get_db(): + if not hasattr(g, "sqlite_db"): + g.sqlite_db = connect_db() + return g.sqlite_db -@app.route("/add", methods=["POST"]) -def add_entry(): - """Adds new post to the database.""" - if not session.get("logged_in"): - abort(401) - new_entry = models.Post(request.form["title"], request.form["text"]) - db.session.add(new_entry) - db.session.commit() - flash("New entry was successfully posted") - return redirect(url_for("index")) +# close database connection +@app.teardown_appcontext +def close_db(error): + if hasattr(g, "sqlite_db"): + g.sqlite_db.close() + + +@app.route('/') +def index(): + """Searches the database for entries, then displays them.""" + db = get_db() + cur = db.execute('select * from entries order by id desc') + entries = cur.fetchall() + return render_template('index.html', entries=entries) -@app.route("/login", methods=["GET", "POST"]) +@app.route('/login', methods=['GET', 'POST']) def login(): """User login/authentication/session management.""" error = None - if request.method == "POST": - if request.form["username"] != app.config["USERNAME"]: - error = "Invalid username" - elif request.form["password"] != app.config["PASSWORD"]: - error = "Invalid password" + if request.method == 'POST': + if request.form['username'] != app.config['USERNAME']: + error = 'Invalid username' + elif request.form['password'] != app.config['PASSWORD']: + error = 'Invalid password' else: - session["logged_in"] = True - flash("You were logged in") - return redirect(url_for("index")) - return render_template("login.html", error=error) + session['logged_in'] = True + flash('You were logged in') + return redirect(url_for('index')) + return render_template('login.html', error=error) -@app.route("/logout") +@app.route('/logout') def logout(): """User logout/authentication/session management.""" - session.pop("logged_in", None) - flash("You were logged out") - return redirect(url_for("index")) + session.pop('logged_in', None) + flash('You were logged out') + return redirect(url_for('index')) -@app.route("/delete/", methods=["GET"]) -@login_required +@app.route('/add', methods=['POST']) +def add_entry(): + """Add new post to database.""" + 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('index')) + + +@app.route('/delete/', methods=['GET']) def delete_entry(post_id): - """Deletes post from database.""" - result = {"status": 0, "message": "Error"} + """Delete post from database""" + result = {'status': 0, 'message': 'Error'} try: - new_id = post_id - db.session.query(models.Post).filter_by(id=new_id).delete() - db.session.commit() - result = {"status": 1, "message": "Post Deleted"} - flash("The entry was deleted.") + db = get_db() + db.execute('delete from entries where id=' + post_id) + db.commit() + result = {'status': 1, 'message': "Post Deleted"} except Exception as e: - result = {"status": 0, "message": repr(e)} + result = {'status': 0, 'message': repr(e)} return jsonify(result) -@app.route("/search/", methods=["GET"]) -def search(): - query = request.args.get("query") - entries = db.session.query(models.Post) - if query: - return render_template("search.html", entries=entries, query=query) - return render_template("search.html") - - if __name__ == "__main__": app.run() diff --git a/project/models.py b/project/models.py deleted file mode 100644 index 8434fa1..0000000 --- a/project/models.py +++ /dev/null @@ -1,14 +0,0 @@ -from project.app import db - - -class Post(db.Model): - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String, nullable=False) - text = db.Column(db.String, nullable=False) - - def __init__(self, title, text): - self.title = title - self.text = text - - def __repr__(self): - return f"" diff --git a/project/schema.sql b/project/schema.sql new file mode 100644 index 0000000..3e4e85b --- /dev/null +++ b/project/schema.sql @@ -0,0 +1,7 @@ +drop table if exists entries; + +create table entries ( + id integer primary key autoincrement, + title text not null, + text text not null +); diff --git a/project/templates/index.html b/project/templates/index.html index 6eca8d3..133689d 100644 --- a/project/templates/index.html +++ b/project/templates/index.html @@ -5,67 +5,39 @@ <link rel="stylesheet" type="text/css" - href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" + href="{{ url_for('static', filename='style.css') }}" /> </head> <body> - <div class="container"> - <br /><br /> - <h1>Flaskr</h1> - <a class="btn btn-info" role="button" href="{{ url_for('search') }}">Search</a> - <br /><br /> + <div class="page"> + <h1>Flaskr-TDD</h1> - {% if not session.logged_in %} - <a class="btn btn-success" role="button" href="{{ url_for('login') }}" - >log in</a - > - {% else %} - <a class="btn btn-warning" role="button" href="{{ url_for('logout') }}" - >log out</a - > - {% endif %} - - <br /><br /> + <div class="metanav"> + {% if not session.logged_in %} + <a href="{{ url_for('login') }}">log in</a> + {% else %} + <a href="{{ url_for('logout') }}">log out</a> + {% endif %} + </div> {% for message in get_flashed_messages() %} - <div class="flash alert alert-success col-sm-4" role="success"> - {{ message }} - </div> - {% endfor %} {% if session.logged_in %} + <div class="flash">{{ message }}</div> + {% endfor %} {% block body %}{% endblock %} {% if session.logged_in %} <form action="{{ url_for('add_entry') }}" method="post" - class="add-entry form-group" + class="add-entry" > <dl> <dt>Title:</dt> - <dd> - <input - type="text" - size="30" - name="title" - class="form-control col-sm-4" - /> - </dd> + <dd><input type="text" size="30" name="title" /></dd> <dt>Text:</dt> - <dd> - <textarea - name="text" - rows="5" - cols="40" - class="form-control col-sm-4" - ></textarea> - </dd> - <br /><br /> - <dd> - <input type="submit" class="btn btn-primary" value="Share" /> - </dd> + <dd><textarea name="text" rows="5" cols="40"></textarea></dd> + <dd><input type="submit" value="Share" /></dd> </dl> </form> {% endif %} - <br /> - <ul class="entries"> {% for entry in entries %} <li class="entry"> diff --git a/project/templates/login.html b/project/templates/login.html index e3a3f0a..1662281 100644 --- a/project/templates/login.html +++ b/project/templates/login.html @@ -5,58 +5,40 @@ <link rel="stylesheet" type="text/css" - href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" + href="{{ url_for('static', filename='style.css') }}" /> </head> <body> - <div class="container"> - <br /><br /> + <div class="page"> <h1>Flaskr</h1> - <br /><br /> - {% for message in get_flashed_messages() %} - <div class="flash alert alert-success col-sm-4" role="success"> - {{ message }} + <div class="metanav"> + {% if not session.logged_in %} + <a href="{{ url_for('login') }}">log in</a> + {% else %} + <a href="{{ url_for('logout') }}">log out</a> + {% endif %} </div> - {% endfor %} - <h3>Login</h3> + {% for message in get_flashed_messages() %} + <div class="flash">{{ message }}</div> + {% endfor %} {% block body %}{% endblock %} + + <h2>Login</h2> {% if error %} - <p class="alert alert-danger col-sm-4" role="danger"> - <strong>Error:</strong> {{ error }} - </p> + <p class="error"><strong>Error:</strong> {{ error }}</p> {% endif %} - <form action="{{ url_for('login') }}" method="post" class="form-group"> + <form action="{{ url_for('login') }}" method="post"> <dl> <dt>Username:</dt> - <dd> - <input - type="text" - name="username" - class="form-control col-sm-4" - /> - </dd> + <dd><input type="text" name="username" /></dd> <dt>Password:</dt> - <dd> - <input - type="password" - name="password" - class="form-control col-sm-4" - /> - </dd> - <br /><br /> - <dd> - <input type="submit" class="btn btn-primary" value="Login" /> - </dd> - <span>Use "admin" for username and password</span> + <dd><input type="password" name="password" /></dd> + <dd><input type="submit" value="Login" /></dd> </dl> </form> </div> - <script - type="text/javascript" - src="{{url_for('static', filename='main.js') }}" - ></script> </body> </html> diff --git a/project/templates/search.html b/project/templates/search.html deleted file mode 100644 index fd8c49a..0000000 --- a/project/templates/search.html +++ /dev/null @@ -1,65 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <title>Flaskr - - - -
-

-

Flaskr

-

- - - Home - - - {% if not session.logged_in %} - log in - {% else %} - log out - {% endif %} - -

- - {% for message in get_flashed_messages() %} -
- {{ message }} -
- {% endfor %} - -
-
-
Search:
-
- -
-
-
-
-
- -
    - {% for entry in entries %} {% if query.lower() in entry.title.lower() or - query.lower() in entry.text.lower() %} -
  • -

    {{ entry.title }}

    - {{ entry.text|safe }} -
  • - {% endif %} {% endfor %} -
-
- - - diff --git a/requirements.txt b/requirements.txt index 8fb45c4..b90811b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,3 @@ -Flask==1.1.2 -Flask-SQLAlchemy==2.5.1 +Flask==2.1.1 gunicorn==20.0.4 -psycopg2-binary==2.8.6 - -black==20.8b1 -flake8==3.8.4 -pytest==6.1.1 +pytest==7.1.2 diff --git a/runtime.txt b/runtime.txt index f72c511..73e24dc 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.9.0 +python-3.10.4 diff --git a/test.db b/test.db index 634462ba1239138ca81eb82bc3837f11033e3e76..469affda684920263788f801a58ffe5e411f948e 100644 GIT binary patch delta 56 zcmZo@U~EX3AT5~5z`(!^#4wOPQO6i4sAruAa1Dc=i2^T>$HcF~z^}qz#y5Lop$;Eo=fuL+ S%_{tN6gEo=Jm;U-APE4(1rLb; diff --git a/tests/app_test.py b/tests/app_test.py index 565a815..787e2ff 100644 --- a/tests/app_test.py +++ b/tests/app_test.py @@ -1,8 +1,10 @@ -import pytest import json +import os from pathlib import Path -from project.app import app, db +import pytest + +from project.app import app, init_db TEST_DB = "test.db" @@ -12,11 +14,10 @@ def client(): BASE_DIR = Path(__file__).resolve().parent.parent app.config["TESTING"] = True app.config["DATABASE"] = BASE_DIR.joinpath(TEST_DB) - app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{BASE_DIR.joinpath(TEST_DB)}" - db.create_all() # setup - yield app.test_client() # tests run here - db.drop_all() # teardown + init_db() # setup + yield app.test_client() # tests run here + init_db() # teardown def login(client, username, password): @@ -77,10 +78,6 @@ def test_messages(client): def test_delete_message(client): """Ensure the messages are being deleted""" - rv = client.get("/delete/1") - data = json.loads(rv.data) - assert data["status"] == 0 - login(client, app.config["USERNAME"], app.config["PASSWORD"]) - rv = client.get("/delete/1") + rv = client.get('/delete/1') data = json.loads(rv.data) assert data["status"] == 1 From 0f8b117b536aba6b19bc2a4ed1c57dde581d41aa Mon Sep 17 00:00:00 2001 From: Michael Herman Date: Fri, 3 Jun 2022 13:46:29 -0500 Subject: [PATCH 2/3] updates updates --- README.md | 43 ++++----- create_db.py | 11 +++ project/app.py | 160 ++++++++++++++++++--------------- flaskr.db => project/flaskr.db | Bin 12288 -> 8192 bytes project/models.py | 14 +++ project/schema.sql | 7 -- project/templates/index.html | 61 +++++++++---- project/templates/login.html | 54 +++++++---- project/templates/search.html | 65 ++++++++++++++ requirements.txt | 4 +- test.db | Bin 12288 -> 16384 bytes tests/app_test.py | 16 ++-- 12 files changed, 292 insertions(+), 143 deletions(-) create mode 100644 create_db.py rename flaskr.db => project/flaskr.db (63%) create mode 100644 project/models.py delete mode 100644 project/schema.sql create mode 100644 project/templates/search.html diff --git a/README.md b/README.md index 665cfb4..0a192e6 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ This tutorial utilizes the following requirements: 1. Flask v2.1.1 1. Flask-SQLAlchemy v2.5.1 1. Gunicorn v20.1.0 -1. Psycopg2 v3.0.14 +1. Psycopg2 v2.9.3 1. Flake8 v4.0.1 1. Black v22.3.0 1. pytest v7.1.2 @@ -1095,7 +1095,7 @@ First, remove the *style.css* stylesheet from both *index.html* and *login.html* ``` @@ -1111,7 +1111,7 @@ Replace the code in *login.html* with: @@ -1178,7 +1178,7 @@ And replace the code in *index.html* with: @@ -1410,7 +1410,7 @@ if __name__ == "__main__": app.run() ``` -Notice the changes in the config at the top as well as the means in which we're now accessing and manipulating the database in each view function -- via SQLAlchemy instead of vanilla SQL. +Notice the changes in the config at the top as well since the means in which we're now accessing and manipulating the database in each view function -- via SQLAlchemy instead of vanilla SQL. ### Create the DB @@ -1448,14 +1448,14 @@ Ensure the tests pass: ```sh (env)$ python -m pytest -==================================================== test session starts ===================================================== -platform darwin -- Python 3.10.4, pytest-7.1.2, py-1.9.0, pluggy-0.13.1 +=============================== test session starts =============================== +platform darwin -- Python 3.10.4, pytest-7.1.2, pluggy-1.0.0 rootdir: /Users/michael/repos/github/flaskr-tdd collected 6 items -tests/app_test.py ...... [100%] +tests/app_test.py ...... [100%] -===================================================== 6 passed in 0.28s ====================================================== +================================ 6 passed in 0.34s ================================ ``` Manually test the app as well by running the server and logging in and out, adding new entries, and deleting old entries. @@ -1507,7 +1507,7 @@ Now add the following code to *search.html*: @@ -1663,10 +1663,12 @@ DATABASE_URL: postgres://wqvcyzyveczscw:df14796eabbf0a1d9eb8a96a206bcd906101162c Next, update the `SQLALCHEMY_DATABASE_URI` variable in *app.py* like so: ```python -SQLALCHEMY_DATABASE_URI = os.getenv( - 'DATABASE_URL', - f'sqlite:///{Path(basedir).joinpath(DATABASE)}' -) +url = os.getenv('DATABASE_URL', f'sqlite:///{Path(basedir).joinpath(DATABASE)}') + +if url.startswith("postgres://"): + url = url.replace("postgres://", "postgresql://", 1) + +SQLALCHEMY_DATABASE_URI = url ``` So, `SQLALCHEMY_DATABASE_URI` now uses the value of the `DATABASE_URL` environment variable if it's available. Otherwise, it will use the SQLite URL. @@ -1682,14 +1684,14 @@ Run the tests to ensure they still pass: ```sh (env)$ python -m pytest -==================================================== test session starts ===================================================== -platform darwin -- Python 3.10.4, pytest-7.1.2, py-1.9.0, pluggy-0.13.1 +=============================== test session starts =============================== +platform darwin -- Python 3.10.4, pytest-7.1.2, pluggy-1.0.0 rootdir: /Users/michael/repos/github/flaskr-tdd collected 6 items -tests/app_test.py ...... [100%] +tests/app_test.py ...... [100%] -===================================================== 6 passed in 0.29s ====================================================== +================================ 6 passed in 0.32s ================================ ``` Try logging in and out, adding a few new entries, and deleting old entries locally. @@ -1699,8 +1701,8 @@ Before updating Heroku, add [Psycopg2](http://initd.org/psycopg/) -- a Postgres ``` Flask==2.1.1 Flask-SQLAlchemy==2.5.1 -gunicorn==20.0.4 -psycopg2-binary==3.0.14 +gunicorn==20.1.0 +psycopg2-binary==2.9.3 pytest==7.1.2 ``` @@ -1733,7 +1735,6 @@ Run Flake8 and correct any issues: ./project/app.py:2:1: F401 'sqlite3' imported but unused ./project/app.py:6:1: F401 'flask.g' imported but unused ./project/app.py:7:19: E126 continuation line over-indented for hanging indent -./project/app.py:56:9: F821 undefined name 'abort' ``` Update the code formatting per Black: diff --git a/create_db.py b/create_db.py new file mode 100644 index 0000000..89ce3af --- /dev/null +++ b/create_db.py @@ -0,0 +1,11 @@ +# create_db.py + + +from project.app import db + + +# create the database and the db table +db.create_all() + +# commit the changes +db.session.commit() diff --git a/project/app.py b/project/app.py index 70c147c..5b9dd77 100644 --- a/project/app.py +++ b/project/app.py @@ -1,113 +1,125 @@ -import sqlite3 - -from flask import Flask, g, render_template, request, session, flash, redirect, url_for, abort, jsonify - +import os +from functools import wraps +from pathlib import Path + +from flask import ( + Flask, + render_template, + request, + session, + flash, + redirect, + url_for, + abort, + jsonify, +) +from flask_sqlalchemy import SQLAlchemy + + +basedir = Path(__file__).resolve().parent # configuration DATABASE = "flaskr.db" USERNAME = "admin" PASSWORD = "admin" SECRET_KEY = "change_me" +url = os.getenv("DATABASE_URL", f"sqlite:///{Path(basedir).joinpath(DATABASE)}") + +if url.startswith("postgres://"): + url = url.replace("postgres://", "postgresql://", 1) + +SQLALCHEMY_DATABASE_URI = url +SQLALCHEMY_TRACK_MODIFICATIONS = False + # create and initialize a new Flask app app = Flask(__name__) - # load the config app.config.from_object(__name__) +# init sqlalchemy +db = SQLAlchemy(app) +from project import models -# connect to database -def connect_db(): - """Connects to the database.""" - rv = sqlite3.connect(app.config["DATABASE"]) - rv.row_factory = sqlite3.Row - return rv - - -# create the database -def init_db(): - with app.app_context(): - db = get_db() - with app.open_resource("schema.sql", mode="r") as f: - db.cursor().executescript(f.read()) - db.commit() - - -# open database connection -def get_db(): - if not hasattr(g, "sqlite_db"): - g.sqlite_db = connect_db() - return g.sqlite_db +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not session.get("logged_in"): + flash("Please log in.") + return jsonify({"status": 0, "message": "Please log in."}), 401 + return f(*args, **kwargs) -# close database connection -@app.teardown_appcontext -def close_db(error): - if hasattr(g, "sqlite_db"): - g.sqlite_db.close() + return decorated_function -@app.route('/') +@app.route("/") def index(): """Searches the database for entries, then displays them.""" - db = get_db() - cur = db.execute('select * from entries order by id desc') - entries = cur.fetchall() - return render_template('index.html', entries=entries) + entries = db.session.query(models.Post) + return render_template("index.html", entries=entries) -@app.route('/login', methods=['GET', 'POST']) +@app.route("/add", methods=["POST"]) +def add_entry(): + """Adds new post to the database.""" + if not session.get("logged_in"): + abort(401) + new_entry = models.Post(request.form["title"], request.form["text"]) + db.session.add(new_entry) + db.session.commit() + flash("New entry was successfully posted") + return redirect(url_for("index")) + + +@app.route("/login", methods=["GET", "POST"]) def login(): """User login/authentication/session management.""" error = None - if request.method == 'POST': - if request.form['username'] != app.config['USERNAME']: - error = 'Invalid username' - elif request.form['password'] != app.config['PASSWORD']: - error = 'Invalid password' + if request.method == "POST": + if request.form["username"] != app.config["USERNAME"]: + error = "Invalid username" + elif request.form["password"] != app.config["PASSWORD"]: + error = "Invalid password" else: - session['logged_in'] = True - flash('You were logged in') - return redirect(url_for('index')) - return render_template('login.html', error=error) + session["logged_in"] = True + flash("You were logged in") + return redirect(url_for("index")) + return render_template("login.html", error=error) -@app.route('/logout') +@app.route("/logout") def logout(): """User logout/authentication/session management.""" - session.pop('logged_in', None) - flash('You were logged out') - return redirect(url_for('index')) - - -@app.route('/add', methods=['POST']) -def add_entry(): - """Add new post to database.""" - 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('index')) + session.pop("logged_in", None) + flash("You were logged out") + return redirect(url_for("index")) -@app.route('/delete/', methods=['GET']) +@app.route("/delete/", methods=["GET"]) +@login_required def delete_entry(post_id): - """Delete post from database""" - result = {'status': 0, 'message': 'Error'} + """Deletes post from database.""" + result = {"status": 0, "message": "Error"} try: - db = get_db() - db.execute('delete from entries where id=' + post_id) - db.commit() - result = {'status': 1, 'message': "Post Deleted"} + new_id = post_id + db.session.query(models.Post).filter_by(id=new_id).delete() + db.session.commit() + result = {"status": 1, "message": "Post Deleted"} + flash("The entry was deleted.") except Exception as e: - result = {'status': 0, 'message': repr(e)} + result = {"status": 0, "message": repr(e)} return jsonify(result) +@app.route("/search/", methods=["GET"]) +def search(): + query = request.args.get("query") + entries = db.session.query(models.Post) + if query: + return render_template("search.html", entries=entries, query=query) + return render_template("search.html") + + if __name__ == "__main__": app.run() diff --git a/flaskr.db b/project/flaskr.db similarity index 63% rename from flaskr.db rename to project/flaskr.db index ca1ea0eb8ad5178f1fb4ce1df0746fb0889bb087..4f1e5810643fa584d11714527bb94f791901e329 100644 GIT binary patch delta 207 zcmZojXmFSy&C1Tez`!(7!JeO;LC-{i7szAeFK6H{-z+G=$zR{d#>g%%D$3ZHUy_)V zlUk5pTmr&O&Oxq@A+8D`j!r(V3Lp^$4KB{i6a`Pe5Lb8CAO%1F5Cy+bA0HhBF3yt7 zlAKh9FvlQg4@Wea)QS?U5&=P;zK%hW3S!=_kqR1_DVkiGAeS)+Ffj0c;lBfP+hu-6 Om5GkxoID_55C8x&uP|f) delta 323 zcmZp0Xh@hK&C1Ncz`#6F!CsJ=LC-{i7szAcS7G2U<5$^OxP`AifQe09TbnVmBrz!` zwYV@Rvm`aXIJK}eH7^;R%j_KF>KNjx5aQ_M" diff --git a/project/schema.sql b/project/schema.sql deleted file mode 100644 index 3e4e85b..0000000 --- a/project/schema.sql +++ /dev/null @@ -1,7 +0,0 @@ -drop table if exists entries; - -create table entries ( - id integer primary key autoincrement, - title text not null, - text text not null -); diff --git a/project/templates/index.html b/project/templates/index.html index 133689d..3547d2a 100644 --- a/project/templates/index.html +++ b/project/templates/index.html @@ -5,39 +5,68 @@ -
-

Flaskr-TDD

+
+

+

Flaskr

+

-
- {% if not session.logged_in %} - log in - {% else %} - log out - {% endif %} -
+ Search + + {% if not session.logged_in %} + log in + {% else %} + log out + {% endif %} + +

{% for message in get_flashed_messages() %} -
{{ message }}
- {% endfor %} {% block body %}{% endblock %} {% if session.logged_in %} +
+ {{ message }} +
+ {% endfor %} {% if session.logged_in %}
Title:
-
+
+ +
Text:
-
-
+
+ +
+

+
+ +
{% endif %} +
+
    {% for entry in entries %}
  • diff --git a/project/templates/login.html b/project/templates/login.html index 1662281..5328c3a 100644 --- a/project/templates/login.html +++ b/project/templates/login.html @@ -5,40 +5,58 @@ -
    +
    +

    Flaskr

    - -
    - {% if not session.logged_in %} - log in - {% else %} - log out - {% endif %} -
    +

    {% for message in get_flashed_messages() %} -
    {{ message }}
    - {% endfor %} {% block body %}{% endblock %} +
    + {{ message }} +
    + {% endfor %} -

    Login

    +

    Login

    {% if error %} -

    Error: {{ error }}

    +

    + Error: {{ error }} +

    {% endif %} -
    +
    Username:
    -
    +
    + +
    Password:
    -
    -
    +
    + +
    +

    +
    + +
    + Use "admin" for username and password
    + diff --git a/project/templates/search.html b/project/templates/search.html new file mode 100644 index 0000000..d8b8444 --- /dev/null +++ b/project/templates/search.html @@ -0,0 +1,65 @@ + + + + Flaskr + + + +
    +

    +

    Flaskr

    +

    + + + Home + + + {% if not session.logged_in %} + log in + {% else %} + log out + {% endif %} + +

    + + {% for message in get_flashed_messages() %} +
    + {{ message }} +
    + {% endfor %} + +
    +
    +
    Search:
    +
    + +
    +
    +
    +
    +
    + +
      + {% for entry in entries %} {% if query.lower() in entry.title.lower() or + query.lower() in entry.text.lower() %} +
    • +

      {{ entry.title }}

      + {{ entry.text|safe }} +
    • + {% endif %} {% endfor %} +
    +
    + + + diff --git a/requirements.txt b/requirements.txt index b90811b..3776422 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ Flask==2.1.1 -gunicorn==20.0.4 +Flask-SQLAlchemy==2.5.1 +gunicorn==20.1.0 +psycopg2-binary==2.9.3 pytest==7.1.2 diff --git a/test.db b/test.db index 469affda684920263788f801a58ffe5e411f948e..fb60dd6d0d68f361cfdee3ca25bf75dded697c8e 100644 GIT binary patch delta 70 zcmZojXlP)ZAT2n7fq{VqiWz})_e33I;Ry_SCJMYj9uvO`1HTG?8Q<)Ug*tqUof8XN SH>>d9QP?ag@SJ~QgCqdYsSm{f delta 56 zcmZo@U~EX3AT5~5z`(!^#4wOPQO6i4sAr Date: Fri, 3 Jun 2022 15:53:21 -0500 Subject: [PATCH 3/3] add github actions --- .github/workflows/main.yml | 29 +++++++++++++++++++++++++++++ .travis.yml | 14 -------------- requirements.txt | 3 +++ 3 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/main.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..9d0efda --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,29 @@ +name: Test + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Lint + run: | + flake8 --exclude env --ignore E402,E501 . + black --exclude=env --check . + - name: Test with pytest + run: | + python -m pytest diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4d6b344..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: python - -python: - # - 3.9 - - 3.8 - - 3.7 - -install: - - pip install -r requirements.txt - -script: - - python -m pytest - - flake8 --exclude env --ignore E402,E501 . - - black --exclude=env . diff --git a/requirements.txt b/requirements.txt index 3776422..cda737b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,7 @@ Flask==2.1.1 Flask-SQLAlchemy==2.5.1 gunicorn==20.1.0 psycopg2-binary==2.9.3 + +black==22.3.0 +flake8==4.0.1 pytest==7.1.2