📜 ⬆️ ⬇️

"Flaskr" - introduction to Flask, development through testing (TDD) and jQuery

Flask is a great Python-based micro web framework. Flaskr is a mini-blog, which is described in the official Flask manual . I have cracked through this guide more times than I can admit. However, I would like to take this tutorial for the next step by adding test driven development and a little jQuery to it.


If you are new to Flask and / or web development in general, it is important to understand these basic fundamental concepts:


  1. The difference between the Get and Post request, and what functions in the application process them.
  2. What is request (Request).
  3. How HTML pages are displayed and returned to the visitor in the browser.

** Note **: this guide is presented by the project https://realpython.com. Please support this open source project by purchasing our http://www.realpython.com/courses Python training courses and web development with Django and flask!

Content


  1. Development through testing?
  2. Downloading Python
  3. Project Installation
  4. First test
  5. Flaskr installation
  6. Second test
  7. Database installation
  8. Templates and Views (Views)
  9. Add colors
  10. Test
  11. jQuery
  12. Deployment
  13. Another test!
  14. Bootstrap
  15. SQLAlchemy
  16. Conclusion

Requirements


The manual uses the following software:


  1. Python v3.5.1
  2. Flask v0.10.1
  3. Flask-SQLAlchemy v2.1
  4. gunicorn v19.4.5

Development through testing?


tdd


Development through testing (tdd) is a type of development that involves writing automatic tests before writing the function itself. In other words, it is a combination of testing and writing code. This process not only helps to ensure the correctness of the code, but also allows you to develop the design and architecture of the project under constant control.


TDD typically follows the Red-Green-Refactoring pattern as shown in the picture above:


  1. Write a test
  2. Run test (and fail)
  3. Write code to pass the test
  4. Code refactoring and retesting again and again (if necessary)

Downloading Python


Before you begin, make sure you have the latest version of Python 3.5 installed, which you can download from http://www.python.org/download/


Note : this guide uses Python v 3.5.1.

Together with Python you also need to put:



Project Installation


  1. Create a new folder to save the project:


    $ mkdir flaskr-tdd $ cd flaskr-tdd 

  2. Create and activate a virtual environment:


     $ pyvenv-3.5 env $ source env/bin/activate 

    Note :
    When you are inside a virtual environment, an inscription (env) is displayed in the terminal up to the $ sign. To exit the virtual environment, use the deactivate command, and if necessary activate it go to the desired directory and run the command source env/bin/activate .

  3. Installing Flask with pip:


     $ pip3 install Flask 


First test


Let's start with a simple program "hello, world".


  1. Create a test file:


     $ touch app-test.py 

    Open this file in your favorite text editor. And add the following lines to the app-test.py file:


     from app import app import unittest class BasicTestCase(unittest.TestCase): def test_index(self): tester = app.test_client(self) response = tester.get('/', content_type='html/text') self.assertEqual(response.status_code, 200) self.assertEqual(response.data, b'Hello, World!') if __name__ == '__main__': unittest.main() 


In fact, we check whether the answer will come to us with the code "200" and whether "hello, world" is displayed.


  1. Run the test:


     $ python app-test.py 

    If all is well, the test will fail.


  2. Now add the following lines to the app.py file to successfully pass the test.


     $ touch app.py 

    Code:


     from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello, World!" if __name__ == "__main__": app.run() 

  3. Run our app:


     $ python app.py 

    Contact http: // localhost: 5000 / . You will see the string "Hello, World!" on your screen.


    Let's go back to the terminal and stop the development server using Ctrl + C.


  4. Run our test again:


     $ python app-test.py . ---------------------------------------------------------------------- Ran 1 test in 0.016s OK 

    Great, what you need.



Flaskr installation


  1. Add a structure


    Add a couple of folders, "static" and "templates", to the root of our project. You should get this structure:


     ├── app-test.py ├── app.py ├── static └── templates 

  2. SQL schema


    Create a new file called "schema.sql" and add the following code to it:


     drop table if exists entries; create table entries ( id integer primary key autoincrement, title text not null, text text not null ); 

    This will create a table with three fields - "id", "title" and "text". SQLite will be used for our DBMS, since SQLite is built into the standard Python library and does not require configuration.



Second test


Let's create a base file to run our application. However, we first need to write a test for it.


  1. Just change our app-test.py from the first test:


     from app import app import unittest class BasicTestCase(unittest.TestCase): def test_index(self): tester = app.test_client(self) response = tester.get('/', content_type='html/text') self.assertEqual(response.status_code, 404) if __name__ == '__main__': unittest.main() 

    So, we expect to get the code 404 (error). Run the test. The test failed. Why was the test failed? It's simple. We were expecting 404, but in fact we get back the code 200 from this route.


  2. Change app.py :


     # imports import sqlite3 from flask import Flask, request, session, g, redirect, url_for, \ abort, render_template, flash, jsonify # configuration DATABASE = 'flaskr.db' DEBUG = True SECRET_KEY = 'my_precious' USERNAME = 'admin' PASSWORD = 'admin' # create and initialize app app = Flask(__name__) app.config.from_object(__name__) if __name__ == '__main__': app.run() 

    Here we import the necessary modules, create a configuration section for global variables, initialize and then start the application.


  3. So, run it:


     $ python app.py 

    We start the server. You should see error message 404, when referring to the route "/", since there is no route and its presentation does not exist. Let's go back to the terminal. Stop the development server. Now run the unit test. It must pass without error.



Database installation


Our goal is to create a connection to the database, create a database based on the schema, if it does not already exist, then close the connection every time after running the test.


  1. How can we check the existence of a database file? Update our app-test.py :


     import unittest import os from app import app class BasicTestCase(unittest.TestCase): def test_index(self): tester = app.test_client(self) response = tester.get('/', content_type='html/text') self.assertEqual(response.status_code, 404) def test_database(self): tester = os.path.exists("flaskr.db") self.assertTrue(tester) if __name__ == '__main__': unittest.main() 

    Run it to make sure the test fails, indicating that the database does not exist.


  2. Now add the following code to app.py :


     # 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 # close database connection @app.teardown_appcontext def close_db(error): if hasattr(g, 'sqlite_db'): g.sqlite_db.close() 

    And add the init_db () function to the end of app.py , to be sure that we will start the server every time with a new database:


     if __name__ == '__main__': init_db() app.run() 

    Now you can create a database using the Python Shell and import and call init_db() :


     >>> from app import init_db >>> init_db() 

    Close the shell and run the test again. Test passed? Now we are convinced that the database has been created.



Templates and Views


Next, we need to customize the templates and the corresponding views that define the routes. Think about it from the user's point of view. We need to enable users to be able to log in and log out. After logging in, the user should be able to create a post. Finally, we should be able to show these posts.


First of all, we will write several tests for this.


Unit testing


Take a look at the final code. I have added comments for explanations.


 import unittest import os import tempfile import app class BasicTestCase(unittest.TestCase): def test_index(self): """ . ,    """ tester = app.app.test_client(self) response = tester.get('/', content_type='html/text') self.assertEqual(response.status_code, 200) def test_database(self): """ , ,    """ tester = os.path.exists("flaskr.db") self.assertEqual(tester, True) class FlaskrTestCase(unittest.TestCase): def setUp(self): """    """ self.db_fd, app.app.config['DATABASE'] = tempfile.mkstemp() app.app.config['TESTING'] = True self.app = app.app.test_client() app.init_db() def tearDown(self): """     """ os.close(self.db_fd) os.unlink(app.app.config['DATABASE']) def login(self, username, password): """  """ return self.app.post('/login', data=dict( username=username, password=password ), follow_redirects=True) def logout(self): """    """ return self.app.get('/logout', follow_redirects=True) #    (assert) def test_empty_db(self): """,    """ rv = self.app.get('/') assert b'No entries here so far' in rv.data def test_login_logout(self): """    """ rv = self.login( app.app.config['USERNAME'], app.app.config['PASSWORD'] ) assert b'You were logged in' in rv.data rv = self.logout() assert b'You were logged out' in rv.data rv = self.login( app.app.config['USERNAME'] + 'x', app.app.config['PASSWORD'] ) assert b'Invalid username' in rv.data rv = self.login( app.app.config['USERNAME'], app.app.config['PASSWORD'] + 'x' ) assert b'Invalid password' in rv.data def test_messages(self): """,       """ self.login( app.app.config['USERNAME'], app.app.config['PASSWORD'] ) rv = self.app.post('/add', data=dict( title='<Hello>', text='<strong>HTML</strong> allowed here' ), follow_redirects=True) assert b'No entries here so far' not in rv.data assert b'&lt;Hello&gt;' in rv.data assert b'<strong>HTML</strong> allowed here' in rv.data if __name__ == '__main__': unittest.main() 

If you run the tests now, everything will crash except test_database () `:


 python app-test.py .FFFF ====================================================================== FAIL: test_index (__main__.BasicTestCase) initial test. ensure flask was set up correctly ---------------------------------------------------------------------- Traceback (most recent call last): File "app-test.py", line 13, in test_index self.assertEqual(response.status_code, 200) AssertionError: 404 != 200 ====================================================================== FAIL: test_empty_db (__main__.FlaskrTestCase) Ensure database is blank ---------------------------------------------------------------------- Traceback (most recent call last): File "app-test.py", line 51, in test_empty_db assert b'No entries here so far' in rv.data AssertionError ====================================================================== FAIL: test_login_logout (__main__.FlaskrTestCase) Test login and logout using helper functions ---------------------------------------------------------------------- Traceback (most recent call last): File "app-test.py", line 59, in test_login_logout assert b'You were logged in' in rv.data AssertionError ====================================================================== FAIL: test_messages (__main__.FlaskrTestCase) Ensure that user can post messages ---------------------------------------------------------------------- Traceback (most recent call last): File "app-test.py", line 84, in test_messages assert b'&lt;Hello&gt;' in rv.data AssertionError ---------------------------------------------------------------------- Ran 5 tests in 0.088s FAILED (failures=4) 

Let's make the tests pass ...


Showing records


  1. First, add a view to display the entries in app.py :


     @app.route('/') def show_entries(): """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) 

  2. Next, go to the "templates" folder and add an index.html file with this content:


     <!DOCTYPE html> <html> <head> <title>Flaskr</title> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <div class="page"> <h1>Flaskr-TDD</h1> <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">{{ message }}</div> {% endfor %} {% block body %}{% endblock %} {% if session.logged_in %} <form action="{{ url_for('add_entry') }}" method="post" class="add-entry"> <dl> <dt>Title:</dt> <dd><input type="text" size="30" name="title"></dd> <dt>Text:</dt> <dd><textarea name="text" rows="5" cols="40"></textarea></dd> <dd><input type="submit" value="Share"></dd> </dl> </form> {% endif %} <ul class="entries"> {% for entry in entries %} <li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}</li> {% else %} <li><em>No entries yet. Add some!</em></li> {% endfor %} </ul> </div> </body> </html> 

  3. Run the test. Should see the following:


     Ran 5 tests in 0.131s FAILED (failures=2, errors=2) 


Authorization of users


  1. Add to the app.py file:


     @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' else: session['logged_in'] = True flash('You were logged in') return redirect(url_for('index')) return render_template('login.html', error=error) @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')) 

    In the above login() function, there is a decorator, which indicates that the route can accept either a Get or Post request. Simply put, the authorization request starts from the user when accessing the url /login . The difference between these types of requests is simple: Get is used to access the site, and POST is used to send information to the server. Thus, when a user simply accesses /login , he uses a Get-request, but when he tries to log in, he uses a Post-request.


  2. Add the file "login.html" to the template folder:


     <!DOCTYPE html> <html> <head> <title>Flaskr-TDD | Login</title> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <div class="page"> <h1>Flaskr</h1> <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">{{ message }}</div> {% endfor %} {% block body %}{% endblock %} <h2>Login</h2> {% if error %} <p class="error"><strong>Error:</strong> {{ error }}</p> {% endif %} <form action="{{ url_for('login') }}" method="post"> <dl> <dt>Username:</dt> <dd><input type="text" name="username"></dd> <dt>Password:</dt> <dd><input type="password" name="password"></dd> <dd><input type="submit" value="Login"></dd> </dl> </form> </div> </body> </html> 

  3. Run the test again.


    You should still see some errors! Consider one of the errors - werkzeug.routing.BuildError: Could not build url for endpoint 'index'. Did you mean 'login' instead? werkzeug.routing.BuildError: Could not build url for endpoint 'index'. Did you mean 'login' instead?


    In essence, we are trying to access the index() function, which does not exist. Rename the show_entries() function to the index() function in the app.py file and run the test with a new one:


     Ran 5 tests in 0.070s FAILED (failures=1, errors=2) 

  4. Next, add the add entry function to the view:


     @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')) 

  5. Retest:


    Now you should see this:


     ====================================================================== FAIL: test_empty_db (__main__.FlaskrTestCase) Ensure database is blank ---------------------------------------------------------------------- Traceback (most recent call last): File "app-test.py", line 49, in test_empty_db assert b'No entries here so far' in rv.data AssertionError ---------------------------------------------------------------------- Ran 5 tests in 0.072s FAILED (failures=1) 

    This error states that when referring to the route / message "No entries here so far" is returned. Check out the template index.html . The text actually reads: "No entries yet. Add some!". So update the test and run the test again:


     Ran 5 tests in 0.156s OK 

    Sumptuously.



Add colors


Save the following styles to a new file named style.css in the "static" folder:


 body { font-family: sans-serif; background: #eee; } a, h1, h2 { color: #377BA8; } h1, h2 { font-family: 'Georgia', serif; margin: 0; } h1 { border-bottom: 2px solid #eee; } h2 { font-size: 1.2em; } .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; padding: 0.8em; background: white; } .entries { list-style: none; margin: 0; padding: 0; } .entries li { margin: 0.8em 1.2em; } .entries li h2 { margin-left: -1em; } .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } .add-entry dl { font-weight: bold; } .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; margin-bottom: 1em; background: #fafafa; } .flash { background: #CEE5F5; padding: 0.5em; border: 1px solid #AACBE2; } .error { background: #F0D6D6; padding: 0.5em; } 

Test


Run the application, log in (username / password = "admin"), create a post, and exit the blog. Then run the tests to make sure everything is still working.


jQuery


Now add some jQuery to make the site a bit more interactive.


  1. Open index.html and change the first <li > something like this:


     <li class="entry"><h2 id={{ entry.id }}>{{ entry.title }}</h2>{{ entry.text|safe }}</li> 

    Now we can use jQuery for each <li> . First, we need to add the following script to the document immediately before the closing Body tag:


     <script src="//code.jquery.com/jquery-1.10.2.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script type="text/javascript" src="{{url_for('static', filename='main.js') }}"></script> 

  2. Create a main.js file in the "static" directory and write the following code into it:


     $(function() { console.log( "ready!" ); // sanity check $('.entry').on('click', function() { var entry = this; var post_id = $(this).find('h2').attr('id'); $.ajax({ type:'GET', url: '/delete' + '/' + post_id, context: entry, success:function(result) { if(result.status === 1) { $(this).remove(); console.log(result); } } }); }); }); 

  3. Add a new function to app.py to be able to delete messages from the database:


     @app.route('/delete/<post_id>', methods=['GET']) def delete_entry(post_id): '''Delete 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"} except Exception as e: result = {'status': 0, 'message': repr(e)} return jsonify(result) 

  4. Finally, write a new test:


     def test_delete_message(self): """Ensure the messages are being deleted""" rv = self.app.get('/delete/1') data = json.loads((rv.data).decode('utf-8')) self.assertEqual(data['status'], 1) 

    Make sure you add import import json


    Check it manually by starting the server and adding two new entries. Click on one of them. The entry must be removed from the dom as well as from the database. Double check it out.


    Then run the test. The test result should be as follows:


     $ python app-test.py ...... ---------------------------------------------------------------------- Ran 6 tests in 0.132s OK 


Deployment


The application is operational, let's not stop at this and deploy the application on Heroku .


  1. To do this, first register, and then install the Heroku Toolbelt .


  2. Next, install a web server called gunicorn :


     $ pip install gunicorn 

  3. Create a Procfile in the root of your project:


     $ touch Procfile 

    Add the following code:


     web: gunicorn app:app 

  4. Create a requirements.txt file to specify the external dependencies that must be installed for the application in order for it to work:


     $ pip freeze > requirements.txt 

  5. Create a .gitignore file:


     $ touch .gitignore 

    And add files and folders that should not be included in the version control system:


     env *.pyc *.DS_Store __pycache__ 

  6. Add a local repository:


     $ git init $ git add -A $ git commit -m "initial" 

  7. Deploy Heroku:


     $ heroku create $ git push heroku master $ heroku open 


Test (Again!)


Run the test in the cloud. The heroku open command will open the application in your browser.


Bootstrap


Let's update the styles from Bootstrap 3.


  1. Remove the style.css and link to it in index.html and login.html . Then add this style in both files.


     <link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"> 

    Now we have full access to all Bootstrap helper classes.


  2. Replace the code in the login.html file with:


     <!DOCTYPE html> <html> <head> <title>Flaskr-TDD | Login</title> <link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"> </head> <body> <div class="container"> <h1>Flaskr</h1> {% for message in get_flashed_messages() %} <div class="flash">{{ message }}</div> {% endfor %} <h3>Login</h3> {% if error %}<p class="error"><strong>Error:</strong> {{ error }}{% endif %}</p> <form action="{{ url_for('login') }}" method="post"> <dl> <dt>Username:</dt> <dd><input type="text" name="username"></dd> <dt>Password:</dt> <dd><input type="password" name="password"></dd> <br><br> <dd><input type="submit" class="btn btn-default" value="Login"></dd> <span>Use "admin" for username and password</span> </dl> </form> </div> <script src="//code.jquery.com/jquery-1.10.2.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script type="text/javascript" src="{{url_for('static', filename='main.js') }}"></script> </body> </html> 

  3. And change the code in index.html :


     <!DOCTYPE html> <html> <head> <title>Flaskr</title> <link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"> </head> <body> <div class="container"> <h1>Flaskr-TDD</h1> {% if not session.logged_in %} <a href="{{ url_for('login') }}">log in</a> {% else %} <a href="{{ url_for('logout') }}">log out</a> {% endif %} {% for message in get_flashed_messages() %} <div class="flash">{{ message }}</div> {% endfor %} {% if session.logged_in %} <form action="{{ url_for('add_entry') }}" method="post" class="add-entry"> <dl> <dt>Title:</dt> <dd><input type="text" size="30" name="title"></dd> <dt>Text:</dt> <dd><textarea name="text" rows="5" cols="40"></textarea></dd> <br><br> <dd><input type="submit" class="btn btn-default" value="Share"></dd> </dl> </form> {% endif %} <br> <ul class="entries"> {% for entry in entries %} <li class="entry"><h2 id={{ entry.id }}>{{ entry.title }}</h2>{{ entry.text|safe }}</li> {% else %} <li><em>No entries yet. Add some!</em></li> {% endfor %} </ul> </div> <script src="//code.jquery.com/jquery-1.10.2.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script type="text/javascript" src="{{url_for('static', filename='main.js') }}"></script> </body> </html> 

    Check your changes!



SQLAlchemy


Let's try Flask-SQLAlchemy to better manage our database.


Install SQLAlchemy


  1. Run the Flask-SQLAlchemy installation:


     $ pip install Flask-SQLAlchemy 

  2. Create a create_db.py file and add the following code to it:


     # create_db.py from app import db from models import Flaskr # create the database and the db table db.create_all() # commit the changes db.session.commit() 

    This file will be used to create our new database. Go further, delete the old one ( flaskr.db ) and schema.sql


  3. Next, add the following content to the new models.py file, which generates a new schema:


     from app import db class Flaskr(db.Model): __tablename__ = "flaskr" post_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 '<title {}>'.format(self.body) 


Update app.py


 # imports from flask import Flask, request, session, g, redirect, url_for, \ abort, render_template, flash, jsonify from flask.ext.sqlalchemy import SQLAlchemy import os # grabs the folder where the script runs basedir = os.path.abspath(os.path.dirname(__file__)) # configuration DATABASE = 'flaskr.db' DEBUG = True SECRET_KEY = 'my_precious' USERNAME = 'admin' PASSWORD = 'admin' # defines the full path for the database DATABASE_PATH = os.path.join(basedir, DATABASE) # the database uri SQLALCHEMY_DATABASE_URI = 'sqlite:///' + DATABASE_PATH # create app app = Flask(__name__) app.config.from_object(__name__) db = SQLAlchemy(app) import models @app.route('/') def index(): """Searches the database for entries, then displays them.""" entries = db.session.query(models.Flaskr) return render_template('index.html', entries=entries) @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.Flaskr(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' else: session['logged_in'] = True flash('You were logged in') return redirect(url_for('index')) return render_template('login.html', error=error) @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('/delete/<int:post_id>', methods=['GET']) def delete_entry(post_id): """Deletes post from database""" result = {'status': 0, 'message': 'Error'} try: new_id = post_id db.session.query(models.Flaskr).filter_by(post_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)} return jsonify(result) if __name__ == '__main__': app.run() 

Notice the changes in the config at the top, as well as the means through which we now have access to and manage the database in each view function — via SQLAlchemy instead of embedded SQL.


Create a database


Run the command to create and initialize the database:


 $ python create_db.py 

Update index.html


Update this line:


 <li class="entry"><h2 id={{ entry.post_id }}>{{ entry.title }}</h2>{{ entry.text|safe }}</li> 

Pay attention to post_id . Check the database to make sure the appropriate field is available.


Tests


Finally, we update our tests:


 import unittest import os from flask import json from app import app, db TEST_DB = 'test.db' class BasicTestCase(unittest.TestCase): def test_index(self): """initial test. ensure flask was set up correctly""" tester = app.test_client(self) response = tester.get('/', content_type='html/text') self.assertEqual(response.status_code, 200) def test_database(self): """initial test. ensure that the database exists""" tester = os.path.exists("flaskr.db") self.assertTrue(tester) class FlaskrTestCase(unittest.TestCase): def setUp(self): """Set up a blank temp database before each test""" basedir = os.path.abspath(os.path.dirname(__file__)) app.config['TESTING'] = True app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + \ os.path.join(basedir, TEST_DB) self.app = app.test_client() db.create_all() def tearDown(self): """Destroy blank temp database after each test""" db.drop_all() def login(self, username, password): """Login helper function""" return self.app.post('/login', data=dict( username=username, password=password ), follow_redirects=True) def logout(self): """Logout helper function""" return self.app.get('/logout', follow_redirects=True) # assert functions def test_empty_db(self): """Ensure database is blank""" rv = self.app.get('/') self.assertIn(b'No entries yet. Add some!', rv.data) def test_login_logout(self): """Test login and logout using helper functions""" rv = self.login(app.config['USERNAME'], app.config['PASSWORD']) self.assertIn(b'You were logged in', rv.data) rv = self.logout() self.assertIn(b'You were logged out', rv.data) rv = self.login(app.config['USERNAME'] + 'x', app.config['PASSWORD']) self.assertIn(b'Invalid username', rv.data) rv = self.login(app.config['USERNAME'], app.config['PASSWORD'] + 'x') self.assertIn(b'Invalid password', rv.data) def test_messages(self): """Ensure that user can post messages""" self.login(app.config['USERNAME'], app.config['PASSWORD']) rv = self.app.post('/add', data=dict( title='<Hello>', text='<strong>HTML</strong> allowed here' ), follow_redirects=True) self.assertNotIn(b'No entries here so far', rv.data) self.assertIn(b'&lt;Hello&gt;', rv.data) self.assertIn(b'<strong>HTML</strong> allowed here', rv.data) def test_delete_message(self): """Ensure the messages are being deleted""" rv = self.app.get('/delete/1') data = json.loads(rv.data) self.assertEqual(data['status'], 1) if __name__ == '__main__': unittest.main() 

We basically just updated the setUp() and tearDown() methods.


Run tests, and then check manually by starting the server and logging in and out, adding new records and deleting old records.


, ( pip freeze > requirements.txt ), heroku!


Conclusion


  1. ? .
  2. Heroku . Hooray!
  3. Flask? Real Python .
  4. - ? . Hooray!

* : Flask

')

Source: https://habr.com/ru/post/312110/


All Articles