Close modal

Blog Post

Filters with Jinja2 (and bottle)

Development
Thu 29 September 2016
0 Comments


Rationale

Jinja2 provides a very flexible HTML formatting engine for python developers. Sometimes you might store a date natively as an epoch (unix timestamp) but want to show it to the user properly formatted and human readable. It would be tedious and unncessary to add new or altered fields just for the templating engine, so let's use the existing property but with a filter.

Data

Here's an example JSON feed for some errors, in this instance we'll simply hard-code it into our service but please visualise as follows, it contains 1. unix timestamp, 2. http code, 3 url of incident.

[{"date": 1475121857, "code": 404, "url": "/media/content/movie34.mp4"},
{"date": 1475122834, "code": 403, "url": "/admin/passwords.txt"}]

JINJA2 HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <meta charset="utf-8">
    <title>Log Viewer</title>
</head>
<body>
<div class="container">
    <h3>Log Viewer </h3>
<table>
    <thead>
    <td>Event Date</td><td>Error Code</td><td>Error Reason</td><td>Request Path</td>
    </thead>
    <tbody>
    {% for event in events %}
    <tr>
        <td>{{event.date| epoch_as_date}}</td>
        <td>{{event.code}}</td>
        <td>{{event.code| format_http_reason}}</td>
        <td>{{event.url}}</td>
    </tr>
    {% endfor %}
    </tbody>
</table>
</div>
</body>
</html>

Python Service

Our filters

Using the below (expressed as lambda) filter functions, we can transform the epoch or for another example selected HTTP status codes into a reason code.

lambda value: strftime("%Y-%m-%d %H:%M", gmtime(int(value)))
lambda value: http_codes[int(value)]

Here is where we setup the view for jinja and configure filters.

view = functools.partial(jinja2_view, ..., template_settings={'filters': filters})

Although we used lambda objects, function objects do of course work equally well for anything non-trivial.

Full Code

Here is the full code for a service that does as follows, it requires log.html to be placed under views for jinja_view to use.

from bottle import run, default_app, request, get, jinja2_view
import os
from time import gmtime, strftime
import functools
import inspect


http_codes = {200: 'OK', 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout'}

script_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
view_dir = os.path.join(script_dir, 'views')
filters = {'epoch_as_date': lambda value: strftime("%Y-%m-%d %H:%M", gmtime(int(value))),
           'format_http_reason': lambda value: http_codes[int(value)]}
view = functools.partial(jinja2_view, template_lookup=[view_dir], template_settings={'filters': filters})


@get("/")
@view('log.html')
def admin_user_timetable():
    events = list()
    events.append({'date': 1475121857, 'code': 404, 'url': '/media/content/movie34.mp4'})
    events.append({'date': 1475122834, 'code': 403, 'url': '/admin/passwords.txt'})

    return {'events': events}

# Create app instance
app = application = default_app()
app.catchall = False
# Run bottle internal test server when invoked directly ie: non-uxsgi mode
if __name__ == '__main__':
    run(app=app, host='0.0.0.0', port=8080)

Output

So let's take a look at what we have here.

Log Viewer

Event Date Error Code Error Reason Request Path
2016-09-29 04:04 404 Not Found /media/content/movie34.mp4
2016-09-29 04:20 403 Forbidden /admin/passwords.txt

As you can see, despite the original data containing unix timestamps and http reason codes only, we've transformed it into something more user readable without requiring transformations on our data, which could potentially be required to be done in several places before being passed in.