Filters with Jinja2 (and bottle)
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.