{"id":13421688,"url":"https://github.com/mjhea0/flaskr-tdd","last_synced_at":"2025-05-14T02:08:54.989Z","repository":{"id":1017815,"uuid":"14267375","full_name":"mjhea0/flaskr-tdd","owner":"mjhea0","description":" Flaskr: Intro to Flask, Test-Driven Development (TDD), and JavaScript","archived":false,"fork":false,"pushed_at":"2025-03-22T03:04:40.000Z","size":528,"stargazers_count":2328,"open_issues_count":3,"forks_count":502,"subscribers_count":62,"default_branch":"main","last_synced_at":"2025-04-12T15:56:20.810Z","etag":null,"topics":["flask","javascript","python","tdd","test-driven-development"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mjhea0.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"mjhea0"}},"created_at":"2013-11-09T23:41:32.000Z","updated_at":"2025-04-08T01:22:42.000Z","dependencies_parsed_at":"2025-02-28T13:09:53.186Z","dependency_job_id":"4fae17b5-f60c-46f7-ac2a-ed64eb513380","html_url":"https://github.com/mjhea0/flaskr-tdd","commit_stats":{"total_commits":127,"total_committers":18,"mean_commits":7.055555555555555,"dds":0.2755905511811023,"last_synced_commit":"23e9e849f88d0509cdc6d57048e4612e0cadc9a8"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjhea0%2Fflaskr-tdd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjhea0%2Fflaskr-tdd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjhea0%2Fflaskr-tdd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mjhea0%2Fflaskr-tdd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mjhea0","download_url":"https://codeload.github.com/mjhea0/flaskr-tdd/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254053216,"owners_count":22006717,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["flask","javascript","python","tdd","test-driven-development"],"created_at":"2024-07-30T23:00:28.286Z","updated_at":"2025-05-14T02:08:49.968Z","avatar_url":"https://github.com/mjhea0.png","language":"Python","funding_links":["https://github.com/sponsors/mjhea0"],"categories":["Python","Resources","介绍","Tutorials"],"sub_categories":["Tutorials"],"readme":"# Flaskr - Intro to Flask, Test-Driven Development, and JavaScript\n\nAs many of you know, Flaskr -- a mini-blog-like-app -- is the app that you build for the official Flask [tutorial](https://flask.palletsprojects.com/en/3.0.x/tutorial/). I've gone through the tutorial more times than I care to admit. Anyway, I wanted to take the tutorial a step further by adding [Test-Driven Development](https://testdriven.io/test-driven-development/) (TDD), a bit of JavaScript, and deployment. This article is that tutorial. Enjoy.\n\nAlso, if you're completely new to Flask and/or web development in general, it's important to grasp these basic fundamental concepts:\n\n1. The difference between HTTP GET and POST requests and how functions within the app handle each.\n1. What HTTP \"requests\" and \"responses\" are.\n1. How HTML pages are rendered and/or returned to the end user.\n\n\u003e 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!\n\n## What you're building\n\nYou'll be building a simple blogging app in this tutorial:\n\n![flaskr app](/flaskr-app.png)\n\n## Changelog\n\nThis tutorial was last updated on October 17th, 2023:\n\n- **10/17/2023**:\n  - Updated to Python 3.12.0 and bumped all other dependencies.\n- **06/03/2022**:\n  - Updated to Python 3.10.4 and bumped all other dependencies.\n- **10/14/2020**:\n  - Renamed *app.test.py* to *app_test.py*. (Fixed issue #[58](https://github.com/mjhea0/flaskr-tdd/issues/58).)\n  - Updated to Python 3.9 and bumped all other dependencies.\n  - Added pytest v7.1.2. (Fixed issue #[60](https://github.com/mjhea0/flaskr-tdd/issues/60))\n  - Migrated from `os.path` to `pathlib`.\n- **11/05/2019**:\n  - Updated to Python 3.8.0, Flask 1.1.1, and Bootstrap 4.3.1.\n  - Replaced jQuery with vanilla JavaScript.\n  - Added Black and Flake8.\n  - Used Postgres in production.\n  - Restricted post delete requests.\n- **10/07/2018**: Updated to Python 3.7.0.\n- **05/10/2018**: Updated to Python 3.6.5, Flask 1.0.2, Bootstrap 4.1.1.\n- **10/16/2017**:\n  - Updated to Python 3.6.2.\n  - Updated to Bootstrap 4.\n- **10/10/2017**: Added a search feature.\n- **07/03/2017**: Updated to Python 3.6.1.\n- **01/24/2016**: Updated to Python 3 (v3.5.1)!\n- **08/24/2014**: PEP8 updates.\n- **02/25/2014**: Upgraded to SQLAlchemy.\n- **02/20/2014**: Completed AJAX.\n- **12/06/2013**: Added Bootstrap 3 styles\n- **11/29/2013**: Updated unit tests.\n- **11/19/2013**: Fixed typo. Updated unit tests.\n- **11/11/2013**: Added information on requests.\n\n## Contents\n\n1. [Test Driven Development?](#test-driven-development)\n1. [Download Python](#download-python)\n1. [Project Setup](#project-setup)\n1. [First Test](#first-test)\n1. [Flaskr Setup](#flaskr-setup)\n1. [Second Test](#second-test)\n1. [Database Setup](#database-setup)\n1. [Templates and Views](#templates-and-views)\n1. [Add Some Style](#add-some-style)\n1. [JavaScript](#javascript)\n1. [Deployment](#deployment)\n1. [Bootstrap](#bootstrap)\n1. [SQLAlchemy](#sqlalchemy)\n1. [Search Page](#search-page)\n1. [Login Required](#login-required)\n1. [Postgres Heroku](#postgres-heroku)\n1. [Linting and Code Formatting](#linting-and-code-formatting)\n1. [Conclusion](#conclusion)\n\n## Requirements\n\nThis tutorial utilizes the following requirements:\n\n1. Python v3.12.0\n1. Flask v3.0.0\n1. Flask-SQLAlchemy v3.1.1\n1. Gunicorn v21.2.0\n1. Psycopg2 v2.9.9\n1. Flake8 v6.1.0\n1. Black v23.10.0\n1. pytest v7.4.2\n\n## Test Driven Development?\n\n![tdd](https://raw.githubusercontent.com/mjhea0/flaskr-tdd/master/tdd.png)\n\nTest-Driven Development (TDD) is an iterative development cycle that emphasizes writing automated tests before writing the actual feature or function. Put another way, TDD combines building and testing. This process not only helps ensure correctness of the code -- but also helps to indirectly evolve the design and architecture of the project at hand.\n\nTDD usually follows the \"Red-Green-Refactor\" cycle, as shown in the image above:\n\n1. Write a test\n1. Run the test (it should fail)\n1. Write just enough code for the test to pass\n1. Refactor code and retest, again and again (if necessary)\n\n\u003e For more, check out [What is Test-Driven Development?](https://testdriven.io/test-driven-development/).\n\n## Download Python\n\nBefore beginning make sure you have the latest version of [Python 3.12](https://www.python.org/downloads/release/python-3120/) installed, which you can download from [http://www.python.org/download/](http://www.python.org/download/).\n\n\u003e This tutorial uses Python v3.12.0.\n\nAlong with Python, the following tools are also installed:\n\n- [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.\n- [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'll eventually run into problems with dependency conflicts.\n\n\u003e 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/).\n\n## Project Setup\n\nCreate a new directory to store the project:\n\n```sh\n$ mkdir flaskr-tdd\n$ cd flaskr-tdd\n```\n\nCreate and activate a virtual environment:\n\n```sh\n$ python3.12 -m venv env\n$ source env/bin/activate\n(env)$\n```\n\n\u003e You know that you're in a virtual environment when `env` is displayed before the `$` in your terminal: `(env)$`. To exit the virtual environment, use the command `deactivate`. You can reactivate by navigating back to the project directory and running `source env/bin/activate`.\n\nInstall Flask with pip:\n\n```sh\n(env)$ pip install flask==3.0.0\n```\n\n## First Test\n\nLet's start with a simple \"hello, world\" app.\n\nCreate the following files and folders:\n\n```sh\n├── project\n│   ├── __init__.py\n│   ├── app.py\n└── tests\n    ├── __init__.py\n    └── app_test.py\n```\n\nWhile the Python standard library comes with a unit testing framework called ùnittest, [pytest](https://pytest.org/) is the go-to testing framework for testing Python code.\n\n\u003e For more on pytest, check out [Pytest for Beginners](https://testdriven.io/blog/pytest-for-beginners/).\n\nInstall it:\n\n```sh\n(env)$ pip install pytest==7.4.2\n```\n\nOpen *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:\n\n```python\nfrom project.app import app\n\n\ndef test_index():\n    tester = app.test_client()\n    response = tester.get(\"/\", content_type=\"html/text\")\n\n    assert response.status_code == 200\n    assert response.data == b\"Hello, World!\"\n```\n\nEssentially, we're testing whether the response that we get back has a status code of \"200\" and that \"Hello, World!\" is displayed.\n\nRun the test:\n\n```sh\n(env)$ python -m pytest\n```\n\nIf all goes well, this test will fail:\n\n```sh\nImportError: cannot import name 'app' from 'project.app\n```\n\nNow add the code for this to pass to *project/app.py*:\n\n```python\nfrom flask import Flask\n\n\n# create and initialize a new Flask app\napp = Flask(__name__)\n\n\n@app.route(\"/\")\ndef hello():\n    return \"Hello, World!\"\n\n\nif __name__ == \"__main__\":\n    app.run()\n```\n\nRun the app:\n\n```sh\n(env)$ FLASK_APP=project/app.py python -m flask run -p 5001\n```\n\n\u003e The `FLASK_APP` environment variable is used to tell Flask to look for the application in a different module.\n\nThen, navigate to [http://localhost:5001/](http://localhost:5001/) in your browser of choice. You should see \"Hello, World!\" on your screen.\n\nReturn to the terminal. Kill the server with Ctrl+C.\n\nRun the test again:\n\n```sh\n(env)$ python -m pytest\n\n=============================== test session starts ===============================\nplatform darwin -- Python 3.10.4, pytest-7.4.2, pluggy-1.0.0\nrootdir: /Users/michael/repos/github/flaskr-tdd\ncollected 1 item\n\ntests/app_test.py .                                                         [100%]\n\n================================ 1 passed in 0.10s ================================\n```\n\nNice.\n\n## Database Setup\n\nEssentially, we want to open a database connection, create the database based on a defined schema if it doesn't already exist, and then close the connection each time a test is ran.\n\nCreate a new file called *schema.sql* in \"project\" and add the following code:\n\n```sql\ndrop table if exists entries;\n\ncreate table entries (\n  id integer primary key autoincrement,\n  title text not null,\n  text text not null\n);\n```\n\nThis 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.\n\nUpdate *app.py*:\n\n```python\nfrom flask import Flask\n\n\n# configuration\nDATABASE = \"flaskr.db\"\n\n# create and initialize a new Flask app\napp = Flask(__name__)\n\n# load the config\napp.config.from_object(__name__)\n\n\n@app.route(\"/\")\ndef hello():\n    return \"Hello, World!\"\n\n\nif __name__ == \"__main__\":\n    app.run()\n```\n\nHere, we created a configuration section for config variables (with the name of the future SQLite database) and loaded the config after app initialization.\n\nHow do we test for the existence of a file? Update *app_test.py* like so:\n\n```python\nfrom pathlib import Path\n\nfrom project.app import app\n\n\ndef test_index():\n    tester = app.test_client()\n    response = tester.get(\"/\", content_type=\"html/text\")\n\n    assert response.status_code == 200\n    assert response.data == b\"Hello, World!\"\n\n\ndef test_database():\n    assert Path(\"flaskr.db\").is_file()\n```\n\nRun it to make sure it fails, indicating that the database does not exist.\n\nNow add the following code to *app.py*, just before the `hello` view function:\n\n```python\n# connect to database\ndef connect_db():\n    \"\"\"Connects to the database.\"\"\"\n    rv = sqlite3.connect(app.config[\"DATABASE\"])\n    rv.row_factory = sqlite3.Row\n    return rv\n\n\n# create the database\ndef init_db():\n    with app.app_context():\n        db = get_db()\n        with app.open_resource(\"schema.sql\", mode=\"r\") as f:\n            db.cursor().executescript(f.read())\n        db.commit()\n\n\n# open database connection\ndef get_db():\n    if not hasattr(g, \"sqlite_db\"):\n        g.sqlite_db = connect_db()\n    return g.sqlite_db\n\n\n# close database connection\n@app.teardown_appcontext\ndef close_db(error):\n    if hasattr(g, \"sqlite_db\"):\n        g.sqlite_db.close()\n```\n\nAdd the imports:\n\n```python\nimport sqlite3\n\nfrom flask import Flask, g\n```\n\n\u003e Curious about `g` object? Check out the [Understanding the Application and Request Contexts in Flask](https://testdriven.io/blog/flask-contexts/) for more.\n\nYou should now have:\n\n```python\nimport sqlite3\n\nfrom flask import Flask, g\n\n\n# configuration\nDATABASE = \"flaskr.db\"\n\n# create and initialize a new Flask app\napp = Flask(__name__)\n\n# load the config\napp.config.from_object(__name__)\n\n\n# connect to database\ndef connect_db():\n    \"\"\"Connects to the database.\"\"\"\n    rv = sqlite3.connect(app.config[\"DATABASE\"])\n    rv.row_factory = sqlite3.Row\n    return rv\n\n\n# create the database\ndef init_db():\n    with app.app_context():\n        db = get_db()\n        with app.open_resource(\"schema.sql\", mode=\"r\") as f:\n            db.cursor().executescript(f.read())\n        db.commit()\n\n\n# open database connection\ndef get_db():\n    if not hasattr(g, \"sqlite_db\"):\n        g.sqlite_db = connect_db()\n    return g.sqlite_db\n\n\n# close database connection\n@app.teardown_appcontext\ndef close_db(error):\n    if hasattr(g, \"sqlite_db\"):\n        g.sqlite_db.close()\n\n\n@app.route(\"/\")\ndef hello():\n    return \"Hello, World!\"\n\n\nif __name__ == \"__main__\":\n    app.run()\n```\n\nNow, create a database by starting up a Python shell and importing and then calling the `init_db` function:\n\n```python\n\u003e\u003e\u003e from project.app import init_db\n\u003e\u003e\u003e init_db()\n```\n\nClose the shell, then run the test again. Does it pass? It should. Now we know that the database has been created.\n\nYou can also call `init_db` within the test, to ensure that the test can be ran independently:\n\n```python\nfrom pathlib import Path\n\nfrom project.app import app, init_db\n\n\ndef test_index():\n    tester = app.test_client()\n    response = tester.get(\"/\", content_type=\"html/text\")\n\n    assert response.status_code == 200\n    assert response.data == b\"Hello, World!\"\n\n\ndef test_database():\n    init_db()\n    assert Path(\"flaskr.db\").is_file()\n```\n\nUpdated structure:\n\n```sh\n├── flaskr.db\n├── project\n│   ├── __init__.py\n│   ├── app.py\n│   └── schema.sql\n└── tests\n    ├── __init__.py\n    └── app_test.py\n```\n\n## Templates and Views\n\nNext, we need to set up the templates and the associated views, which define the routes. Think about this from a user's standpoint:\n\n1. Users should be able to log in and out.\n1. Once logged in, users should be able to post new messages.\n1. Finally, users should be able to view the messages.\n\nWrite some tests for this first.\n\n### Tests\n\nTake a look at the final code below. I added docstrings for explanation.\n\n```python\nimport os\nimport pytest\nfrom pathlib import Path\n\nfrom project.app import app, init_db\n\nTEST_DB = \"test.db\"\n\n\n@pytest.fixture\ndef client():\n    BASE_DIR = Path(__file__).resolve().parent.parent\n    app.config[\"TESTING\"] = True\n    app.config[\"DATABASE\"] = BASE_DIR.joinpath(TEST_DB)\n\n    init_db() # setup\n    yield app.test_client() # tests run here\n    init_db() # teardown\n\n\ndef login(client, username, password):\n    \"\"\"Login helper function\"\"\"\n    return client.post(\n        \"/login\",\n        data=dict(username=username, password=password),\n        follow_redirects=True,\n    )\n\n\ndef logout(client):\n    \"\"\"Logout helper function\"\"\"\n    return client.get(\"/logout\", follow_redirects=True)\n\n\ndef test_index(client):\n    response = client.get(\"/\", content_type=\"html/text\")\n    assert response.status_code == 200\n\n\ndef test_database(client):\n    \"\"\"initial test. ensure that the database exists\"\"\"\n    tester = Path(\"test.db\").is_file()\n    assert tester\n\n\ndef test_empty_db(client):\n    \"\"\"Ensure database is blank\"\"\"\n    rv = client.get(\"/\")\n    assert b\"No entries yet. Add some!\" in rv.data\n\n\ndef test_login_logout(client):\n    \"\"\"Test login and logout using helper functions\"\"\"\n    rv = login(client, app.config[\"USERNAME\"], app.config[\"PASSWORD\"])\n    assert b\"You were logged in\" in rv.data\n    rv = logout(client)\n    assert b\"You were logged out\" in rv.data\n    rv = login(client, app.config[\"USERNAME\"] + \"x\", app.config[\"PASSWORD\"])\n    assert b\"Invalid username\" in rv.data\n    rv = login(client, app.config[\"USERNAME\"], app.config[\"PASSWORD\"] + \"x\")\n    assert b\"Invalid password\" in rv.data\n\n\ndef test_messages(client):\n    \"\"\"Ensure that user can post messages\"\"\"\n    login(client, app.config[\"USERNAME\"], app.config[\"PASSWORD\"])\n    rv = client.post(\n        \"/add\",\n        data=dict(title=\"\u003cHello\u003e\", text=\"\u003cstrong\u003eHTML\u003c/strong\u003e allowed here\"),\n        follow_redirects=True,\n    )\n    assert b\"No entries here so far\" not in rv.data\n    assert b\"\u0026lt;Hello\u0026gt;\" in rv.data\n    assert b\"\u003cstrong\u003eHTML\u003c/strong\u003e allowed here\" in rv.data\n```\n\nTake note of the `client` function. This is a pytest [fixture](https://docs.pytest.org/en/stable/fixture.html), which sets up a known state for each test function before the test runs.\n\nRun the tests now:\n\n```sh\n(env)$ python -m pytest\n```\n\nThree tests should fail:\n\n```sh\n=============================== test session starts ===============================\nplatform darwin -- Python 3.10.4, pytest-7.4.2, pluggy-1.0.0\nrootdir: /Users/michael/repos/github/flaskr-tdd\ncollected 5 items\n\ntests/app_test.py ..FFF                                                     [100%]\n\n==================================== FAILURES =====================================\n__________________________________ test_empty_db __________________________________\n\nclient = \u003cFlaskClient \u003cFlask 'project.app'\u003e\u003e\n\n    def test_empty_db(client):\n        \"\"\"Ensure database is blank\"\"\"\n        rv = client.get(\"/\")\n\u003e       assert b\"No entries yet. Add some!\" in rv.data\nE       AssertionError: assert b'No entries yet. Add some!' in b'Hello, World!'\nE        +  where b'Hello, World!' = \u003cWrapperTestResponse 13 bytes [200 OK]\u003e.data\n\ntests/app_test.py:49: AssertionError\n________________________________ test_login_logout ________________________________\n\nclient = \u003cFlaskClient \u003cFlask 'project.app'\u003e\u003e\n\n    def test_login_logout(client):\n        \"\"\"Test login and logout using helper functions\"\"\"\n\u003e       rv = login(client, app.config[\"USERNAME\"], app.config[\"PASSWORD\"])\nE       KeyError: 'USERNAME'\n\ntests/app_test.py:54: KeyError\n__________________________________ test_messages __________________________________\n\nclient = \u003cFlaskClient \u003cFlask 'project.app'\u003e\u003e\n\n    def test_messages(client):\n        \"\"\"Ensure that user can post messages\"\"\"\n\u003e       login(client, app.config[\"USERNAME\"], app.config[\"PASSWORD\"])\nE       KeyError: 'USERNAME'\n\ntests/app_test.py:66: KeyError\n============================= short test summary info =============================\nFAILED tests/app_test.py::test_empty_db -\n    AssertionError: assert b'No entries yet. Add some!' in b'Hello, World!'\nFAILED tests/app_test.py::test_login_logout - KeyError: 'USERNAME'\nFAILED tests/app_test.py::test_messages - KeyError: 'USERNAME'\n=========================== 3 failed, 2 passed in 0.17s ==========================\n```\n\nLet's get these all green, one at a time...\n\n### Show Entries\n\nFirst, replace the `hello` view with the following view function for displaying the entries to *app.py*:\n\n```python\n@app.route('/')\ndef index():\n    \"\"\"Searches the database for entries, then displays them.\"\"\"\n    db = get_db()\n    cur = db.execute('select * from entries order by id desc')\n    entries = cur.fetchall()\n    return render_template('index.html', entries=entries)\n```\n\nImport in `render_template`:\n\n```python\nfrom flask import Flask, g, render_template\n```\n\nThen, create a new folder called \"templates\" inside of \"project\", and add an *index.html* template file to it:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003ctitle\u003eFlaskr\u003c/title\u003e\n    \u003clink\n      rel=\"stylesheet\"\n      type=\"text/css\"\n      href=\"{{ url_for('static', filename='style.css') }}\"\n    /\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv class=\"page\"\u003e\n      \u003ch1\u003eFlaskr-TDD\u003c/h1\u003e\n\n      \u003cdiv class=\"metanav\"\u003e\n        {% if not session.logged_in %}\n        \u003ca href=\"{{ url_for('login') }}\"\u003elog in\u003c/a\u003e\n        {% else %}\n        \u003ca href=\"{{ url_for('logout') }}\"\u003elog out\u003c/a\u003e\n        {% endif %}\n      \u003c/div\u003e\n\n      {% for message in get_flashed_messages() %}\n      \u003cdiv class=\"flash\"\u003e{{ message }}\u003c/div\u003e\n      {% endfor %} {% block body %}{% endblock %} {% if session.logged_in %}\n      \u003cform\n        action=\"{{ url_for('add_entry') }}\"\n        method=\"post\"\n        class=\"add-entry\"\n      \u003e\n        \u003cdl\u003e\n          \u003cdt\u003eTitle:\u003c/dt\u003e\n          \u003cdd\u003e\u003cinput type=\"text\" size=\"30\" name=\"title\" /\u003e\u003c/dd\u003e\n          \u003cdt\u003eText:\u003c/dt\u003e\n          \u003cdd\u003e\u003ctextarea name=\"text\" rows=\"5\" cols=\"40\"\u003e\u003c/textarea\u003e\u003c/dd\u003e\n          \u003cdd\u003e\u003cinput type=\"submit\" value=\"Share\" /\u003e\u003c/dd\u003e\n        \u003c/dl\u003e\n      \u003c/form\u003e\n      {% endif %}\n\n      \u003cul class=\"entries\"\u003e\n        {% for entry in entries %}\n        \u003cli\u003e\n          \u003ch2\u003e{{ entry.title }}\u003c/h2\u003e\n          {{ entry.text|safe }}\n        \u003c/li\u003e\n        {% else %}\n        \u003cli\u003e\u003cem\u003eNo entries yet. Add some!\u003c/em\u003e\u003c/li\u003e\n        {% endfor %}\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n### User Login and Logout\n\nUpdate *app.py*:\n\n```python\n@app.route('/login', methods=['GET', 'POST'])\ndef login():\n    \"\"\"User login/authentication/session management.\"\"\"\n    error = None\n    if request.method == 'POST':\n        if request.form['username'] != app.config['USERNAME']:\n            error = 'Invalid username'\n        elif request.form['password'] != app.config['PASSWORD']:\n            error = 'Invalid password'\n        else:\n            session['logged_in'] = True\n            flash('You were logged in')\n            return redirect(url_for('index'))\n    return render_template('login.html', error=error)\n\n\n@app.route('/logout')\ndef logout():\n    \"\"\"User logout/authentication/session management.\"\"\"\n    session.pop('logged_in', None)\n    flash('You were logged out')\n    return redirect(url_for('index'))\n```\n\nIn the above `login` function, the decorator indicates that the route can accept either a GET or POST request. Put simply, a request is initiated by the end user when they access the `/login` URL. The difference between these requests is simple: GET is used for accessing a webpage, while POST is used when information is sent to the server. Thus, when a user accesses the `/login` URL, they are using a GET request, but when they attempt to log in, a POST request is used.\n\nUpdate the config as well:\n\n```python\n# configuration\nDATABASE = \"flaskr.db\"\nUSERNAME = \"admin\"\nPASSWORD = \"admin\"\nSECRET_KEY = \"change_me\"\n```\n\nAdd the appropriate imports:\n\n```python\nfrom flask import Flask, g, render_template, request, session, flash, redirect, url_for\n```\n\nAdd the *login.html* template:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003ctitle\u003eFlaskr-TDD | Login\u003c/title\u003e\n    \u003clink\n      rel=\"stylesheet\"\n      type=\"text/css\"\n      href=\"{{ url_for('static', filename='style.css') }}\"\n    /\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv class=\"page\"\u003e\n      \u003ch1\u003eFlaskr\u003c/h1\u003e\n\n      \u003cdiv class=\"metanav\"\u003e\n        {% if not session.logged_in %}\n        \u003ca href=\"{{ url_for('login') }}\"\u003elog in\u003c/a\u003e\n        {% else %}\n        \u003ca href=\"{{ url_for('logout') }}\"\u003elog out\u003c/a\u003e\n        {% endif %}\n      \u003c/div\u003e\n\n      {% for message in get_flashed_messages() %}\n      \u003cdiv class=\"flash\"\u003e{{ message }}\u003c/div\u003e\n      {% endfor %} {% block body %}{% endblock %}\n\n      \u003ch2\u003eLogin\u003c/h2\u003e\n\n      {% if error %}\n      \u003cp class=\"error\"\u003e\u003cstrong\u003eError:\u003c/strong\u003e {{ error }}\u003c/p\u003e\n      {% endif %}\n\n      \u003cform action=\"{{ url_for('login') }}\" method=\"post\"\u003e\n        \u003cdl\u003e\n          \u003cdt\u003eUsername:\u003c/dt\u003e\n          \u003cdd\u003e\u003cinput type=\"text\" name=\"username\" /\u003e\u003c/dd\u003e\n          \u003cdt\u003ePassword:\u003c/dt\u003e\n          \u003cdd\u003e\u003cinput type=\"password\" name=\"password\" /\u003e\u003c/dd\u003e\n          \u003cdd\u003e\u003cinput type=\"submit\" value=\"Login\" /\u003e\u003c/dd\u003e\n        \u003c/dl\u003e\n      \u003c/form\u003e\n    \u003c/div\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\nRun the tests again. You should see two errors:\n\n```sh\nE           werkzeug.routing.BuildError: Could not build url for endpoint 'add_entry'. Did you mean 'login' instead?\n```\n\nNext, add in a view for adding entries:\n\n```python\n@app.route('/add', methods=['POST'])\ndef add_entry():\n    \"\"\"Add new post to database.\"\"\"\n    if not session.get('logged_in'):\n        abort(401)\n    db = get_db()\n    db.execute(\n        'insert into entries (title, text) values (?, ?)',\n        [request.form['title'], request.form['text']]\n    )\n    db.commit()\n    flash('New entry was successfully posted')\n    return redirect(url_for('index'))\n```\n\nAdd the appropriate imports:\n\n```python\nfrom flask import Flask, g, render_template, request, session, flash, redirect, url_for, abort\n```\n\nRetest:\n\n```sh\n=============================== test session starts ===============================\nplatform darwin -- Python 3.10.4, pytest-7.4.2, pluggy-1.0.0\nrootdir: /Users/michael/repos/github/flaskr-tdd\ncollected 5 items\n\ntests/app_test.py .....                                                     [100%]\n\n================================ 5 passed in 0.16s ================================\n```\n\nPerfect.\n\n## Add Some Style\n\nSave the following styles to a new file called *style.css* in a new folder called \"project/static\":\n\n```css\nbody {\n  font-family: sans-serif;\n  background: #eee;\n}\n\na,\nh1,\nh2 {\n  color: #377ba8;\n}\n\nh1,\nh2 {\n  font-family: \"Georgia\", serif;\n  margin: 0;\n}\n\nh1 {\n  border-bottom: 2px solid #eee;\n}\n\nh2 {\n  font-size: 1.2em;\n}\n\n.page {\n  margin: 2em auto;\n  width: 35em;\n  border: 5px solid #ccc;\n  padding: 0.8em;\n  background: white;\n}\n\n.entries {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\n.entries li {\n  margin: 0.8em 1.2em;\n}\n\n.entries li h2 {\n  margin-left: -1em;\n}\n\n.add-entry {\n  font-size: 0.9em;\n  border-bottom: 1px solid #ccc;\n}\n\n.add-entry dl {\n  font-weight: bold;\n}\n\n.metanav {\n  text-align: right;\n  font-size: 0.8em;\n  padding: 0.3em;\n  margin-bottom: 1em;\n  background: #fafafa;\n}\n\n.flash {\n  background: #cee5f5;\n  padding: 0.5em;\n  border: 1px solid #aacbe2;\n}\n\n.error {\n  background: #f0d6d6;\n  padding: 0.5em;\n}\n```\n\nRun your app, log in (username/password = \"admin\"), add a post, log out.\n\n## JavaScript\n\nNext, let's add some JavaScript to make the app slightly more interactive.\n\nOpen *index.html* and update the first `\u003cli\u003e` like so:\n\n```html\n\u003cli class=\"entry\"\u003e\n  \u003ch2 id=\"{{ entry.id }}\"\u003e{{ entry.title }}\u003c/h2\u003e\n  {{ entry.text|safe }}\n\u003c/li\u003e\n```\n\nNow, we can use JavaScript to target each `\u003cli\u003e`. First, we need to add the following script to the document just before the closing body tag:\n\n```html\n\u003cscript\n  type=\"text/javascript\"\n  src=\"{{url_for('static', filename='main.js') }}\"\n\u003e\u003c/script\u003e\n```\n\nCreate a *main.js* file in your \"static\" directory and add the following code:\n\n```javascript\n(function () {\n  console.log(\"ready!\"); // sanity check\n})();\n\nconst postElements = document.getElementsByClassName(\"entry\");\n\nfor (var i = 0; i \u003c postElements.length; i++) {\n  postElements[i].addEventListener(\"click\", function () {\n    const postId = this.getElementsByTagName(\"h2\")[0].getAttribute(\"id\");\n    const node = this;\n    fetch(`/delete/${postId}`)\n      .then(function (resp) {\n        return resp.json();\n      })\n      .then(function (result) {\n        if (result.status === 1) {\n          node.parentNode.removeChild(node);\n          console.log(result);\n        }\n        location.reload();\n      })\n      .catch(function (err) {\n        console.log(err);\n      });\n  });\n}\n```\n\nAdd a new function in *app.py* to remove the post from the database:\n\n```python\n@app.route('/delete/\u003cpost_id\u003e', methods=['GET'])\ndef delete_entry(post_id):\n    \"\"\"Delete post from database\"\"\"\n    result = {'status': 0, 'message': 'Error'}\n    try:\n        db = get_db()\n        db.execute('delete from entries where id=' + post_id)\n        db.commit()\n        result = {'status': 1, 'message': \"Post Deleted\"}\n    except Exception as e:\n        result = {'status': 0, 'message': repr(e)}\n    return jsonify(result)\n```\n\nUpdate the imports:\n\n```python\nfrom flask import Flask, g, render_template, request, session, flash, redirect, url_for, abort, jsonify\n```\n\nFinally, add a new test:\n\n```python\ndef test_delete_message(client):\n    \"\"\"Ensure the messages are being deleted\"\"\"\n    rv = client.get('/delete/1')\n    data = json.loads(rv.data)\n    assert data[\"status\"] == 1\n```\n\nMake sure to add the following import as well: `import json`.\n\nManually test this out by running the server and adding two new entries. Click on one of them. It should be removed from the DOM as well as the database. Double check this.\n\nThen run your automated test suite. It should pass:\n\n```sh\n(env)$ python -m pytest\n\n=============================== test session starts ===============================\nplatform darwin -- Python 3.10.4, pytest-7.4.2, pluggy-1.0.0\nrootdir: /Users/michael/repos/github/flaskr-tdd\ncollected 6 items\n\ntests/app_test.py ......                                                    [100%]\n\n================================ 6 passed in 0.17s ================================\n```\n\n## Deployment\n\nWith the app in a working state, let's shift gears and deploy the app to [Heroku](https://www.heroku.com). To do this, first [sign up](https://signup.heroku.com/), and then install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli).\n\nNext, install a production-grade WSGI web server called [Gunicorn](http://gunicorn.org/):\n\n```sh\n(env)$ pip install gunicorn==21.2.0\n```\n\nCreate a [Procfile](https://devcenter.heroku.com/articles/procfile) in the project root:\n\n```sh\n(env)$ touch Procfile\n```\n\nAnd add the following code:\n\n```sh\nweb: gunicorn project.app:app\n```\n\nCreate a *requirements.txt* file to specify the external dependencies that need to be installed for the app to work:\n\n```sh\n(env)$ touch requirements.txt\n```\n\nAdd the requirements:\n\n```\nFlask==3.0.0\ngunicorn==21.2.0\npytest==7.4.2\n```\n\nCreate a *.gitignore* file in the project root:\n\n```sh\n(env)$ touch .gitignore\n```\n\nAnd include the following files and folders (so they are not included in version control):\n\n```sh\nenv\n*.pyc\n*.DS_Store\n__pycache__\ntest.db\n```\n\nTo specify the correct Python runtime, add a new file to the project root called *runtime.txt*:\n\n```\npython-3.12.0\n```\n\nAdd a local Git repo:\n\n```sh\n(env)$ git init\n(env)$ git add -A\n(env)$ git commit -m \"initial\"\n```\n\nDeploy to Heroku:\n\n```sh\n(env)$ heroku create\n(env)$ git push heroku main\n```\n\nLet's test this in the cloud. Run `heroku open` to open the app in your default web browser.\n\n## Bootstrap\n\nLet's update the styles with [Bootstrap](http://getbootstrap.com/).\n\nFirst, remove the *style.css* stylesheet from both *index.html* and *login.html*. Then add this stylesheet to both files:\n\n```html\n\u003clink\n  rel=\"stylesheet\"\n  type=\"text/css\"\n  href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\"\n/\u003e\n```\n\nNow, we have full access to all of the Bootstrap helper classes.\n\nReplace the code in *login.html* with:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003ctitle\u003eFlaskr-TDD | Login\u003c/title\u003e\n    \u003clink\n      rel=\"stylesheet\"\n      type=\"text/css\"\n      href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\"\n    /\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv class=\"container\"\u003e\n      \u003cbr /\u003e\u003cbr /\u003e\n      \u003ch1\u003eFlaskr\u003c/h1\u003e\n      \u003cbr /\u003e\u003cbr /\u003e\n\n      {% for message in get_flashed_messages() %}\n      \u003cdiv class=\"flash alert alert-success col-sm-4\" role=\"success\"\u003e\n        {{ message }}\n      \u003c/div\u003e\n      {% endfor %}\n\n      \u003ch3\u003eLogin\u003c/h3\u003e\n\n      {% if error %}\n      \u003cp class=\"alert alert-danger col-sm-4\" role=\"danger\"\u003e\n        \u003cstrong\u003eError:\u003c/strong\u003e {{ error }}\n      \u003c/p\u003e\n      {% endif %}\n\n      \u003cform action=\"{{ url_for('login') }}\" method=\"post\" class=\"form-group\"\u003e\n        \u003cdl\u003e\n          \u003cdt\u003eUsername:\u003c/dt\u003e\n          \u003cdd\u003e\n            \u003cinput\n              type=\"text\"\n              name=\"username\"\n              class=\"form-control col-sm-4\"\n            /\u003e\n          \u003c/dd\u003e\n          \u003cdt\u003ePassword:\u003c/dt\u003e\n          \u003cdd\u003e\n            \u003cinput\n              type=\"password\"\n              name=\"password\"\n              class=\"form-control col-sm-4\"\n            /\u003e\n          \u003c/dd\u003e\n          \u003cbr /\u003e\u003cbr /\u003e\n          \u003cdd\u003e\n            \u003cinput type=\"submit\" class=\"btn btn-primary\" value=\"Login\" /\u003e\n          \u003c/dd\u003e\n          \u003cspan\u003eUse \"admin\" for username and password\u003c/span\u003e\n        \u003c/dl\u003e\n      \u003c/form\u003e\n    \u003c/div\u003e\n    \u003cscript\n      type=\"text/javascript\"\n      src=\"{{url_for('static', filename='main.js') }}\"\n    \u003e\u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\nAnd replace the code in *index.html* with:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003ctitle\u003eFlaskr\u003c/title\u003e\n    \u003clink\n      rel=\"stylesheet\"\n      type=\"text/css\"\n      href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\"\n    /\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv class=\"container\"\u003e\n      \u003cbr /\u003e\u003cbr /\u003e\n      \u003ch1\u003eFlaskr\u003c/h1\u003e\n      \u003cbr /\u003e\u003cbr /\u003e\n\n      {% if not session.logged_in %}\n      \u003ca class=\"btn btn-success\" role=\"button\" href=\"{{ url_for('login') }}\"\n        \u003elog in\u003c/a\n      \u003e\n      {% else %}\n      \u003ca class=\"btn btn-warning\" role=\"button\" href=\"{{ url_for('logout') }}\"\n        \u003elog out\u003c/a\n      \u003e\n      {% endif %}\n\n      \u003cbr /\u003e\u003cbr /\u003e\n\n      {% for message in get_flashed_messages() %}\n      \u003cdiv class=\"flash alert alert-success col-sm-4\" role=\"success\"\u003e\n        {{ message }}\n      \u003c/div\u003e\n      {% endfor %} {% if session.logged_in %}\n      \u003cform\n        action=\"{{ url_for('add_entry') }}\"\n        method=\"post\"\n        class=\"add-entry form-group\"\n      \u003e\n        \u003cdl\u003e\n          \u003cdt\u003eTitle:\u003c/dt\u003e\n          \u003cdd\u003e\n            \u003cinput\n              type=\"text\"\n              size=\"30\"\n              name=\"title\"\n              class=\"form-control col-sm-4\"\n            /\u003e\n          \u003c/dd\u003e\n          \u003cdt\u003eText:\u003c/dt\u003e\n          \u003cdd\u003e\n            \u003ctextarea\n              name=\"text\"\n              rows=\"5\"\n              cols=\"40\"\n              class=\"form-control col-sm-4\"\n            \u003e\u003c/textarea\u003e\n          \u003c/dd\u003e\n          \u003cbr /\u003e\u003cbr /\u003e\n          \u003cdd\u003e\n            \u003cinput type=\"submit\" class=\"btn btn-primary\" value=\"Share\" /\u003e\n          \u003c/dd\u003e\n        \u003c/dl\u003e\n      \u003c/form\u003e\n      {% endif %}\n\n      \u003cbr /\u003e\n\n      \u003cul class=\"entries\"\u003e\n        {% for entry in entries %}\n        \u003cli class=\"entry\"\u003e\n          \u003ch2 id=\"{{ entry.id }}\"\u003e{{ entry.title }}\u003c/h2\u003e\n          {{ entry.text|safe }}\n        \u003c/li\u003e\n        {% else %}\n        \u003cli\u003e\u003cem\u003eNo entries yet. Add some!\u003c/em\u003e\u003c/li\u003e\n        {% endfor %}\n      \u003c/ul\u003e\n    \u003c/div\u003e\n    \u003cscript\n      type=\"text/javascript\"\n      src=\"{{url_for('static', filename='main.js') }}\"\n    \u003e\u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\nRun the app locally:\n\n```sh\n(env)$ FLASK_APP=project/app.py python -m flask run -p 5001\n```\n\nCheck out the changes in the browser!\n\n## SQLAlchemy\n\nLet's upgrade to [Flask-SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/), in order to better manage the database.\n\n### Setup\n\nStart by installing Flask-SQLAlchemy:\n\n```sh\n(env)$ pip install Flask-SQLAlchemy==3.1.1\n```\n\nMake sure to add it to your requirements file as well.\n\nNext, add a *create_db.py* file to the project root. Then, add the following code:\n\n```python\n# create_db.py\n\n\nfrom project.app import app, db\nfrom project.models import Post\n\n\nwith app.app_context():\n    # create the database and the db table\n    db.create_all()\n\n    # commit the changes\n    db.session.commit()\n```\n\nThis file will be used to create our new database. Go ahead and delete the old database file (*flaskr.db*) along with the *project/schema.sql* file.\n\nNext, add a *project/models.py* file, which will be used to generate the new schema:\n\n```python\nfrom project.app import db\n\n\nclass Post(db.Model):\n    id = db.Column(db.Integer, primary_key=True)\n    title = db.Column(db.String, nullable=False)\n    text = db.Column(db.String, nullable=False)\n\n    def __init__(self, title, text):\n        self.title = title\n        self.text = text\n\n    def __repr__(self):\n        return f'\u003ctitle {self.title}\u003e'\n```\n\n### Update *app.py*\n\n```python\nimport sqlite3\nfrom pathlib import Path\n\nfrom flask import Flask, g, render_template, request, session, \\\n                  flash, redirect, url_for, abort, jsonify\nfrom flask_sqlalchemy import SQLAlchemy\n\n\nbasedir = Path(__file__).resolve().parent\n\n# configuration\nDATABASE = \"flaskr.db\"\nUSERNAME = \"admin\"\nPASSWORD = \"admin\"\nSECRET_KEY = \"change_me\"\nSQLALCHEMY_DATABASE_URI = f'sqlite:///{Path(basedir).joinpath(DATABASE)}'\nSQLALCHEMY_TRACK_MODIFICATIONS = False\n\n\n# create and initialize a new Flask app\napp = Flask(__name__)\n# load the config\napp.config.from_object(__name__)\n# init sqlalchemy\ndb = SQLAlchemy(app)\n\nfrom project import models\n\n\n@app.route('/')\ndef index():\n    \"\"\"Searches the database for entries, then displays them.\"\"\"\n    entries = db.session.query(models.Post)\n    return render_template('index.html', entries=entries)\n\n\n@app.route('/add', methods=['POST'])\ndef add_entry():\n    \"\"\"Adds new post to the database.\"\"\"\n    if not session.get('logged_in'):\n        abort(401)\n    new_entry = models.Post(request.form['title'], request.form['text'])\n    db.session.add(new_entry)\n    db.session.commit()\n    flash('New entry was successfully posted')\n    return redirect(url_for('index'))\n\n\n@app.route('/login', methods=['GET', 'POST'])\ndef login():\n    \"\"\"User login/authentication/session management.\"\"\"\n    error = None\n    if request.method == 'POST':\n        if request.form['username'] != app.config['USERNAME']:\n            error = 'Invalid username'\n        elif request.form['password'] != app.config['PASSWORD']:\n            error = 'Invalid password'\n        else:\n            session['logged_in'] = True\n            flash('You were logged in')\n            return redirect(url_for('index'))\n    return render_template('login.html', error=error)\n\n\n@app.route('/logout')\ndef logout():\n    \"\"\"User logout/authentication/session management.\"\"\"\n    session.pop('logged_in', None)\n    flash('You were logged out')\n    return redirect(url_for('index'))\n\n\n@app.route('/delete/\u003cint:post_id\u003e', methods=['GET'])\ndef delete_entry(post_id):\n    \"\"\"Deletes post from database.\"\"\"\n    result = {'status': 0, 'message': 'Error'}\n    try:\n        db.session.query(models.Post).filter_by(id=post_id).delete()\n        db.session.commit()\n        result = {'status': 1, 'message': \"Post Deleted\"}\n        flash('The entry was deleted.')\n    except Exception as e:\n        result = {'status': 0, 'message': repr(e)}\n    return jsonify(result)\n\n\nif __name__ == \"__main__\":\n    app.run()\n```\n\nNotice 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.\n\n### Create the DB\n\nRun the following command to create the initial database:\n\n```sh\n(env)$ python create_db.py\n```\n\n### Tests\n\nFinally, update the `client` fixture in the tests:\n\n```python\n@pytest.fixture\ndef client():\n    BASE_DIR = Path(__file__).resolve().parent.parent\n    app.config[\"TESTING\"] = True\n    app.config[\"DATABASE\"] = BASE_DIR.joinpath(TEST_DB)\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = f\"sqlite:///{BASE_DIR.joinpath(TEST_DB)}\"\n\n    with app.app_context():\n        db.create_all()  # setup\n        yield app.test_client()  # tests run here\n        db.drop_all()  # teardown\n```\n\nUpdate the imports as well:\n\n```python\nfrom project.app import app, db\n```\n\nEnsure the tests pass:\n\n```sh\n(env)$ python -m pytest\n\n=============================== test session starts ===============================\nplatform darwin -- Python 3.10.4, pytest-7.4.2, pluggy-1.0.0\nrootdir: /Users/michael/repos/github/flaskr-tdd\ncollected 6 items\n\ntests/app_test.py ......                                                    [100%]\n\n================================ 6 passed in 0.34s ================================\n```\n\nManually test the app as well by running the server and logging in and out, adding new entries, and deleting old entries.\n\nIf all is well, Update the requirements file:\n\n```\nFlask==3.0.0\nFlask-SQLAlchemy==3.1.1\ngunicorn==21.2.0\npytest==7.4.2\n```\n\nCommit your code, and then push the new version to Heroku!\n\n## Search Page\n\nLet's add a search page. It will be a nice feature that will come in handy after we have a number of posts.\n\n### Update *app.py*\n\n```python\n@app.route('/search/', methods=['GET'])\ndef search():\n    query = request.args.get(\"query\")\n    entries = db.session.query(models.Post)\n    if query:\n        return render_template('search.html', entries=entries, query=query)\n    return render_template('search.html')\n```\n\n\u003e Be sure to write a test for this on your own!\n\n### Add *search.html*\n\nIn the \"templates\" folder create a new file called *search.html*:\n\n```sh\n(env)$ touch search.html\n```\n\nNow add the following code to *search.html*:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003ctitle\u003eFlaskr\u003c/title\u003e\n    \u003clink\n      rel=\"stylesheet\"\n      type=\"text/css\"\n      href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\"\n    /\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv class=\"container\"\u003e\n      \u003cbr /\u003e\u003cbr /\u003e\n      \u003ch1\u003eFlaskr\u003c/h1\u003e\n      \u003cbr /\u003e\u003cbr /\u003e\n\n      \u003ca class=\"btn btn-primary\" role=\"button\" href=\"{{ url_for('index') }}\"\u003e\n        Home\n      \u003c/a\u003e\n\n      {% if not session.logged_in %}\n      \u003ca class=\"btn btn-success\" role=\"button\" href=\"{{ url_for('login') }}\"\n        \u003elog in\u003c/a\n      \u003e\n      {% else %}\n      \u003ca class=\"btn btn-warning\" role=\"button\" href=\"{{ url_for('logout') }}\"\n        \u003elog out\u003c/a\n      \u003e\n      {% endif %}\n\n      \u003cbr /\u003e\u003cbr /\u003e\n\n      {% for message in get_flashed_messages() %}\n      \u003cdiv class=\"flash alert alert-success col-sm-4\" role=\"success\"\u003e\n        {{ message }}\n      \u003c/div\u003e\n      {% endfor %}\n\n      \u003cform action=\"{{ url_for('search') }}\" method=\"get\" class=\"from-group\"\u003e\n        \u003cdl\u003e\n          \u003cdt\u003eSearch:\u003c/dt\u003e\n          \u003cdd\u003e\n            \u003cinput type=\"text\" name=\"query\" class=\"form-control col-sm-4\" /\u003e\n          \u003c/dd\u003e\n          \u003cbr /\u003e\n          \u003cdd\u003e\u003cinput type=\"submit\" class=\"btn btn-info\" value=\"Search\" /\u003e\u003c/dd\u003e\n        \u003c/dl\u003e\n      \u003c/form\u003e\n\n      \u003cul class=\"entries\"\u003e\n        {% for entry in entries %} {% if query.lower() in entry.title.lower() or\n        query.lower() in entry.text.lower() %}\n        \u003cli class=\"entry\"\u003e\n          \u003ch2 id=\"{{ entry.post_id }}\"\u003e{{ entry.title }}\u003c/h2\u003e\n          {{ entry.text|safe }}\n        \u003c/li\u003e\n        {% endif %} {% endfor %}\n      \u003c/ul\u003e\n    \u003c/div\u003e\n    \u003cscript\n      type=\"text/javascript\"\n      src=\"{{url_for('static', filename='main.js') }}\"\n    \u003e\u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Update *index.html*\n\nAdd a search button for better navigation just below `\u003ch1\u003eFlaskr\u003c/h1\u003e`:\n\n```html\n\u003ca class=\"btn btn-info\" role=\"button\" href=\"{{ url_for('search') }}\"\u003eSearch\u003c/a\u003e\n```\n\nTest it out locally. If all is well, commit your code and update the version on Heroku.\n\n## Login Required\n\nCurrently, posts can be deleted by anyone. Let's change that so one has to be logged in before they can delete a post.\n\nAdd the following decorator to *app.py*:\n\n```python\ndef login_required(f):\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if not session.get('logged_in'):\n            flash('Please log in.')\n            return jsonify({'status': 0, 'message': 'Please log in.'}), 401\n        return f(*args, **kwargs)\n    return decorated_function\n```\n\nDon't forget the import:\n\n```python\nfrom functools import wraps\n```\n\n\u003e Be sure to write tests for this on your own!\n\nNext, add the decorator to the `delete_entry` view:\n\n```python\n@app.route('/delete/\u003cint:post_id\u003e', methods=['GET'])\n@login_required\ndef delete_entry(post_id):\n    \"\"\"Deletes post from database.\"\"\"\n    result = {'status': 0, 'message': 'Error'}\n    try:\n        new_id = post_id\n        db.session.query(models.Post).filter_by(id=new_id).delete()\n        db.session.commit()\n        result = {'status': 1, 'message': \"Post Deleted\"}\n        flash('The entry was deleted.')\n    except Exception as e:\n        result = {'status': 0, 'message': repr(e)}\n    return jsonify(result)\n```\n\nUpdate the test:\n\n```python\ndef test_delete_message(client):\n    \"\"\"Ensure the messages are being deleted\"\"\"\n    rv = client.get(\"/delete/1\")\n    data = json.loads(rv.data)\n    assert data[\"status\"] == 0\n    login(client, app.config[\"USERNAME\"], app.config[\"PASSWORD\"])\n    rv = client.get(\"/delete/1\")\n    data = json.loads(rv.data)\n    assert data[\"status\"] == 1\n```\n\nTest it out locally again. If all is well, commit your code and update the version on Heroku.\n\n## Postgres Heroku\n\nSQLite is a great database to use in order to get an app up and running quickly. That said, it's not intended to be used as a production grade database. So, let's move to using Postgres on Heroku.\n\nStart by provisioning a new `mini` plan Postgres database:\n\n```sh\n(env)$ heroku addons:create heroku-postgresql:mini\n```\n\nOnce created, the database URL can be access via the `DATABASE_URL` environment variable:\n\n```sh\n(env)$ heroku config\n```\n\nYou should see something similar to:\n\n```sh\n=== glacial-savannah-72166 Config Vars\n\nDATABASE_URL: postgres://zebzwxlootewbx:da5c19a66cd4765dd39aed40abb06dff10682c3213501695c4b98612de0dfac9@ec2-54-208-11-146.compute-1.amazonaws.com:5432/d77tnmeavvasm0\n```\n\nNext, update the `SQLALCHEMY_DATABASE_URI` variable in *app.py* like so:\n\n```python\nurl = os.getenv('DATABASE_URL', f'sqlite:///{Path(basedir).joinpath(DATABASE)}')\n\nif url.startswith(\"postgres://\"):\n    url = url.replace(\"postgres://\", \"postgresql://\", 1)\n\nSQLALCHEMY_DATABASE_URI = url\n```\n\nSo, `SQLALCHEMY_DATABASE_URI` now uses the value of the `DATABASE_URL` environment variable if it's available. Otherwise, it will use the SQLite URL.\n\nMake sure to import `os`:\n\n```python\nimport os\n```\n\nRun the tests to ensure they still pass:\n\n```sh\n(env)$ python -m pytest\n\n=============================== test session starts ===============================\nplatform darwin -- Python 3.10.4, pytest-7.4.2, pluggy-1.0.0\nrootdir: /Users/michael/repos/github/flaskr-tdd\ncollected 6 items\n\ntests/app_test.py ......                                                    [100%]\n\n================================ 6 passed in 0.32s ================================\n```\n\nTry logging in and out, adding a few new entries, and deleting old entries locally.\n\nBefore updating Heroku, add [Psycopg2](http://initd.org/psycopg/) -- a Postgres database adapter for Python -- to the requirements file:\n\n```\nFlask==3.0.0\nFlask-SQLAlchemy==3.1.1\ngunicorn==21.2.0\npsycopg2-binary==2.9.9\npytest==7.4.2\n```\n\nCommit and push your code up to Heroku.\n\nSnce we're using a new database on Heroku, you'll need to run the following command *once* to create the tables:\n\n```sh\n(env)$ heroku run python create_db.py\n```\n\nTest things out.\n\n## Linting and Code Formatting\n\nFinally, we can lint and auto format our code with [Flake8](http://flake8.pycqa.org/) and [Black](https://black.readthedocs.io/), respectively:\n\n```sh\n(env)$ pip install flake8==6.1.0\n(env)$ pip install black==23.10.0\n```\n\nRun Flake8 and correct any issues:\n\n```sh\n(env)$ python -m flake8 --exclude env --ignore E402,E501 .\n\n./create_db.py:5:1: F401 'project.models.Post' imported but unused\n./tests/app_test.py:2:1: F401 'os' imported but unused\n./project/app.py:2:1: F401 'sqlite3' imported but unused\n./project/app.py:6:1: F401 'flask.g' imported but unused\n./project/app.py:7:19: E126 continuation line over-indented for hanging indent\n```\n\nUpdate the code formatting per Black:\n\n```sh\n$ python -m black --exclude=env .\n\nreformatted /Users/michael/repos/github/flaskr-tdd/project/models.py\nreformatted /Users/michael/repos/github/flaskr-tdd/project/app.py\nAll done! ✨ 🍰 ✨\n2 files reformatted, 4 files left unchanged.\n```\n\nTest everything out once last time!\n\n## Conclusion\n\n1. Want my code? Grab it [here](https://github.com/mjhea0/flaskr-tdd).\n1. Want more Flask fun? Check out [TestDriven.io](https://testdriven.io/). Learn how to build, test, and deploy microservices powered by Docker, Flask, and React!\n1. Want something else added to this tutorial? Add an issue to the repo.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmjhea0%2Fflaskr-tdd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmjhea0%2Fflaskr-tdd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmjhea0%2Fflaskr-tdd/lists"}