What will we cover?
While CGI programs are an effective way to provide interactive web sites they have several serious problems. Perhaps the most serious is that every request requires the server to start a new process. This is comparatively slow and resource intensive so will not scale to large numbers of concurrent requests. The other problem is that the code and the HTML and the data are all intermingled in a single file. This makes maintenance complex and error prone. A minor tweak to the HTML can result in syntax errors in the Python code.
To deal with these issues various different approaches were tried but the one that has endured, and which we will consider next, is the web framework. There are many Web frameworks available for many different languages. Some of the best known are:
We are going to use the web framework called Flask which provides all the key elements which mark a framework but is relatively easy to pick up and, crucially for this tutorial, can be used in a very basic form without all the bells and whistles to distract attention from the core ideas.
The key concepts behind all of the web frameworks are to minimise the load on the server by utilising concurrent programming techniques (which we look at in the next topic) while separating out the logic and presentation code, thus making the site easier to maintain.
HTML5 and CSS3, the latest standards from the W3C web standards body, are themselves striving to achieve a similar separation. HTML should be used purely to indicate the structure of the document while CSS is used for all presentation issues (fonts, colours, positioning, visibility etc.) By combining good HTML/CSS habits with a web framework, web programming is at last becoming a well ordered, maintainable activity.
This is achieved using two key techniques:
We will see both of these techniques used within Flask, but almost all web frameworks have the same concepts although the syntax details and calling conventions will vary.
The first thing to do is install Flask. It is not part of the standard Python library so you can either use the pip tool that comes with Python or you can use a dedicated installer. The latter approach is recommended for Linux users, especially if you have more than one Python version installed.
If using pip simply open an OS console and type:
$ pip install flask
You can check if it has worked by starting python and trying to import flask. If you don't get any errors then you have succeeded. (You may have to run the pip command as administrator, especially if your Python install was done for all users, which is quite common.)
Flask provides a very simple web server module that we can use for test and development before moving our code to more robust web hosting platform, either on a company network or the internet. Let's look at how to get it running and serving a simple web page.
Unlike with CGI we will not need to create an HTML file for this, instead we return the HTML code from a Flask function. Let's look at some code:
import flask helloapp = flask.Flask(__name__) @helloapp.route("/") def index(): return """ <html><head><title>Hello</title></head> <body> <h1>Hello from Flask!</h1> </body></html>""" if __name__ == "__main__": helloapp.run()
There are several things here that we need to discuss. The first line after the import flask assigns an instance of a Flask application object to the helloapp variable. (helloapp is the name of our application but the choice of variable name is completely arbitrary.) We pass the special variable __name__ (which you have seen many times before) to the application which uses it to identify the home location of the web site.
The next thing is the function that actually processes the http GET request. It is called index by convention in deference to the traditional index.htm file. What is more interesting is the @helloapp.route("/") decorator that precedes it. This tells Flask that any requests to the root of the website (/) should be routed to the following method, namely index in this case. We can, if we wish, route multiple end points to the same method by just stacking the decorators on top of each other. You will recall that end-point mapping was one of the key technologies used by web frameworks? This is how Flask does it.
The Flask code is now ready to run. Start in the usual manner by invoking
$ python hello.py
from the folder where the app lives. you should see a message telling you that the server is running and waiting for requests
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
You can now go to your browser and enter the address shown in the message
http://127.0.0.1:5000/
You should be rewarded by seeing the "Welcome from Flask" message appear. Congratulations, you've just built your first Flask web app.
The Flask code you've seen so far is OK so far as it goes and it deals with the issues of scalability and tidies up some of the mess of CGI programming, but it doesn't do anything to separate the code from the HTML. We are still writing HTML strings within the program code. Let's address that now by introducing Flask templates.
A Flask template is just a static HTML file with some special makers to receive data from the app. It's a lot like the format strings we use when printing in Python. In this case we want a template that can receive the welcome message as a string and insert it into a template. The template looks like this:
<!doctype html> <head><title>Hello from Flask</title></head> <body> <h1>{{message}}</h1> </body> </html>
Notice the {{message}} part? The double braces are the markers and the message is the name of the variable that the template engine will insert into the HTML for us.
Save that as a file called hello.htm in a folder called templates inside your project folder. The name of the templates folder is significant this time because it's where Flask expects to find the template files. Now we just need a couple of tweaks to our Python code to link the Python code to the templating engine and all should be well. Change the previous code to look like:
from flask import Flask, render_template helloapp = Flask(__name__) @helloapp.route("/") def index(): return render_template("hello.htm", message="Hello again from Flask") if __name__ == "__main__": helloapp.run()
The first change is to the import statement where we change the style so that we directly import the names that we need.
Now you can see that there is no HTML code in our Python file, instead we just pass the message as a plain string to the render_template function along with the name of the template and Flask does the rest. We just need to ensure that the named arguments in the call to render_template match the names in the template markers.
Now to complete the picture we need to find out how to read incoming data from the http requests so that we can replicate our "hello user" CGI application.
We now want to modify the application to work with the HTML form we used previously. We start by converting the HTML into a template which we serve when the user specifies an end-point of "hello". Because we are not inserting any data we don't need to add any markers to the HTML but we do need to change the form method attribute to "POST" and its action attribute to reflect the Flask port number (5000) and the required URL end-point (sayhello). Finally, save it in the templates folder as helloform.htm so that Flask can find it. With the changes made it looks like:
<!doctype html> <html> <head> <title>Hello user page</title> </head> <body> <h1>Hello, welcome to our website!</h1>8000 <form name="hello" method="POST" action="http://localhost:5000/sayhello"> <p>Please tell us your name:</p> <input type="text" id="username" name="username" size="30" required autofocus/> <input type="submit" value="Submit" /> </form> </body>
We only need to add two new methods to our application. The first will simply render the helloform.htm template, the second will respond to the form submission. Note that the second function will utilise the original hello.htm template, we just write a new message string. It should now look like this:
from flask import Flask, render_template, request helloapp = Flask(__name__) @helloapp.route("/") def index(): return render_template("hello.htm", message="Hello again from Flask") @helloapp.route("/hello") def hello(): return render_template("helloform.htm") @helloapp.route("/sayhello", methods=['POST']) def sayhello(): name=request.form['username'] return render_template("hello.htm", message="Hello %s" % name) if __name__ == "__main__": helloapp.run()
Notice we had to add request to our list of imported items because we use it to access the http request data.
The sayhello handler has its decorator modified to specify that it can process POST type http requests (the default is GET). Inside the function we simply extract the username data from the form and insert it into the string we return to the hello.htm template.
The hello handler is even simpler, it simply sends the helloform.htm template to the user.
If you now run the code the server should start up and, by typing in the address (localhost is an alias for 127.0.0.1 - and a bit easier to remember!):
http://localhost:5000/hello
You should see the same form as we had in the CGI example. If you fill in the form and hit Submit you should get a familiar greeting.
So what have we achieved? We have removed the need to start up a separate process, thereby making the application much more scalable. We have also completely removed any HTML from our program code, making it easier to maintain. And this is just the tip of the iceberg because Flask has a lot more to offer that we haven't seen yet. For example, it can automatically connect to a database on start-up and close it down on termination. It can check that users are logged in before performing any changes to data. And there are several coding idioms that we have not used which assist in the maintenance and operation of a production scale web site. In particular, the templating system has several other tricks up its sleeve and we will look at a few of those as we move on to build a web front end to our address book database.
Back in the database topic we built an address book application running from the command line. We will reuse that database to build a web version based on Flask and adding a few extra elements of web programming along the way. It will still be a very minimal web application but it should give you enough of a grounding to start building your own apps and to investigate the many tutorials etc. with confidence.
Flask projects usually have a very specific structure. They take the form of a folder hierarchy with the project name at the top followed by a static folder (for static files like images or style sheets), a templates folder (for templates!) and a Python package, often named after the project.
A Python package is just a folder with a file called __init__.py which controls what gets exported by the package. Conventionally this package will also contain a setup.py file which is used to install the package if it is distributed from the Python Package Index (or PyPI). This structure makes the application easy to distribute using the standard Python tools like pip.
For the address book we will not be distributing the app so we will stick with a simpler arrangement based on the three standard folders plus an extra one for the database file:
addressbook static templates db
We can copy the database file from the previous topic into the db folder.
We will have a single web page comprising a simple form for entering data with three buttons: Create, Find and List all. The Create button will use the data in the form to add a new entry to the database. The Find button will display a list of all addresses that match the data in the form and the List All button will simply display the full list of addresses.
We will be using a new feature of the templating engine, namely the ability to duplicate rows of HTML based on an input data collection. This will be combined with a new HTML element the <table> tag and its cousins which we use to display the address list. The template therefore looks like this:
<!doctype html> <html> <head> <title>Flask Address Book</title> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> h1 {text-align: center;} label { width: 20em; text-align: left; margin-top: 2px; margin-right: 2em; float: left;} input { margin-left: 1em; float: right; width: 12em;} input.button {width: 6em; text-align: center; float: left;} br {clear: all;} div.buttons {float: left; width:100%; padding: 1em;} </style> </head> <body> <header> <h1>Address Book Website</h1> </header> <div class="content"> <form name="hello" method="POST" action="http://localhost:5000/display"> <fieldset><legend>Address</legend> <label>First name: <input type="text" id="First" name="First" required autofocus/> </label> <label>Second Name: <input type="text" id="Last" name="Last" required /> </label> <br /> <label>House Number: <input type="text" id="House" name="House" /> </label> <label>Street: <input type="text" id="Street" name="Street" required /> </label> <br /> <label>District: <input type="text" id="District" name="District" /> </label> <label>Town: <input type="text" id="Town" name="Town" required /> </label> <br /> <label>Postcode: <input type="text" id="Postcode" name="Postcode" required /> </label> <label>Phone: <input type="text" id="Phone" name="Phone" /> </label> </fieldset> <div class="buttons"> <input class="button" type="submit" value="List All" /> <input class="button" type="submit" name="Filter" value="Filter" /> <input class="button" type="submit" name="Add" value="Add" /> <input class="button" type="reset" value="Clear" /> </div> </form> <br /> <div class="data"> <table id="addresses"> <tr> <th>First</th> <th>Second</th> <th>House#</th> <th>Street</th> <th>District</th> <th>Town</th> <th>Postcode</th> <th>Phone</th> </tr> {% for row in book %} <tr> <td>{{row.First}}</td> <td>{{row.Last}}</td> <td>{{row.House}}</td> <td>{{row.Street}}</td> <td>{{row.District}}</td> <td>{{row.Town}}</td> <td>{{row.PostCode}}</td> <td>{{row.Phone}}</td> </tr> {% endfor %} </table> </div> </div> </body> </html>
There's quite a few new elements in there, including a <style> section which primarily controls the layout of the form. The details are not relevant for our purposes but you can find out what the CSS elements do in any CSS tutorial. There are some significant new HTML features (highlighted in bold), namely:
There is very little new in the code needed for this project. The Flask elements are just a slight extension of the previous example to determine which of the three submit buttons has been pushed. Once we have done that we simply call regular Python functions, which manipulate the database using the sqlite3 module as discussed in the database topic. These functions return a list of dictionary items, one per database row. This list is passed to the template rendering engine which does all the work of inserting the HTML into the generated page. The code looks like this:
from flask import Flask, render_template, request, g import sqlite3, os addBook = Flask(__name__) addBook.config.update(dict( DATABASE=os.path.join(addBook.root_path,'static','address.db'), )) @addBook.route("/") def index(): data = findAddresses() return render_template("address.htm", book=data) @addBook.route("/display", methods=['POST']) def handleForm(): if 'Filter' in request.form: query= buildQueryString(request.form) return render_template('address.htm', book=findAddresses(query)) elif 'Add' in request.form: addAddress(request.form) # add a new entry return render_template("address.htm", book=findAddresses()) # Note: flask.g is the "global context" object # that lives for the life of the application. def get_db(): if not hasattr(g, 'sqlite_db'): try: file = addBook.config['DATABASE'] db = sqlite3.connect(file) db.row_factory = sqlite3.Row # return dicts instead of tuples except: print("failed to initialise sqlite") g.sqlite_db = db return g.sqlite_db @addBook.teardown_appcontext def close_db(error): if hasattr(g, 'sqlite_db'): db = g.sqlite_db db.commit() db.close def buildQueryString(aForm): base = "WHERE" test = " %s LIKE '%s' " fltr = '' for name in ('First','Last', 'House','Street', 'District','Town', 'PostCode','Phone'): field = aForm.get(name, '') if field: if not fltr: fltr = base + test % (name, field) else: fltr = fltr + ' AND ' + test % (name, field) return fltr def findAddresses(filter=None): base = """ SELECT First,Last,House,Street,District,Town,PostCode,Phone FROM address %s ORDER BY First;""" db = get_db() if not filter: filter = "" # empty -> get all query = base % filter cursor = db.execute(query) data = cursor.fetchall() return data def addAddress(aForm): db = get_db() cursor = db.cursor() first = aForm.get('First','') last = aForm.get('Last','') house = aForm.get('House','') street = aForm.get('Street','') district= aForm.get('District','') town = aForm.get('Town','') code = aForm.get('PostCode','') phone = aForm.get('Phone','') query = '''INSERT INTO Address (First,Last,House,Street,District,Town,PostCode,Phone) Values ("%s","%s","%s","%s","%s","%s","%s","%s;");''' %\ (first, last, house, street, district, town, code, phone) cursor.execute(query) if __name__ == "__main__": addBook.run()
There are a few points of interest here:
Running the application is exactly the same as before. Change to the application folder and run the file address.py then use your browser to visit localhost:5000. You should see all the same names and addresses you created in the database topic presented on your web browser. You should be able to enter values in the form and filter the results to match (you can use % as a wild card) and add new entries. There is no field or form validation so you will need to take care to enter sensible values or tidy up any mistakes by hand using the sqlite3 prompt.
That's all I will cover on this topic. It should have given you a feel for what it takes to build a web application. There is a lot more to learn, including all of the browser programming techniques. Even on the server side there are security issues and cookies to consider as well as the whole area of deploying to a live internet site. Thankfully there are web tutorials and books aplenty to address all those areas. Web hosting companies also provide copious material on how to maximise your site.
Things to remember