I am using docxtpl to generate a word document, and wondering how a user can download this file once generated using cherrypy, please see my code below.
The only solution I could come up with is to save it to the www folder and create a link to the location, but I am sure this can be simplified.
import os, os.path
import random
import string
import cherrypy
from docxtpl import DocxTemplate
import sys
from auth import AuthController, require, member_of, name_is
import socket
reload(sys)
sys.setdefaultencoding('utf8')
cherrypy.config.update({'server.socket_port': 8000})
cherrypy.server.socket_host = '0.0.0.0'
cherrypy.engine.restart()
class Root(object):
_cp_config = {
'tools.sessions.on': True,
'tools.auth.on': True }
#cherrypy.expose()
def default(self, **kwargs):
print kwargs
if kwargs.get('csa_no'):
# print kwargs.get('csa_no')
tpl=DocxTemplate('csa_tpl.docx')
sd = tpl.new_subdoc()
p = sd.add_paragraph('This 1st insert')
sd2 = tpl.new_subdoc()
p = sd2.add_paragraph('This 2nd insert')
context1 = {
'date': 'jkh',
'company_name' : 'Test Footer',
'cost' : '10,000',
'project_description': kwargs['project_description'],
'site': kwargs['site'],
'sp': kwargs['sp'],
'wo': kwargs['WO'],
'contract_manager': kwargs['contract_manager'],
'csa_no': kwargs['csa_no'],
'variation_reason': kwargs['variation_reason'],
'variation_scope_of_works': kwargs['variation_scope_of_works'],
'Initial_Contract_Value': '$300,000',
'variation_total': '$20,000',
'Ammended_Contract_Value': '$320,000',
'project_manager': kwargs['project_manager'],
'construction_manager': 'Dane Wilson',
'date': '30/04/2016',
}
tpl.render(context1)
file_name_with_path = '/var/www/html/csa_documents/' + kwargs['sp'] + '-'+ kwargs['WO'] + '_' + kwargs['site'] + '_-_' + 'CSA' + kwargs['csa_no'] +'.docx'
file_name = kwargs['sp'] + '-'+ kwargs['WO'] + '_' + kwargs['site'] + '_-_' + 'CSA' + kwargs['csa_no'] +'.docx'
print file_name
print file_name_with_path
tpl.save(file_name_with_path)
return '''
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=9" />
<link href="//ajax.googleapis.com/ajax/libs/jquerymobile/1.4.2/jquery.mobile.min.css" rel="stylesheet">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquerymobile/1.4.2/jquery.mobile.min.js"></script>
<title>Broadspectrum</title>
</head>
<body>
<div data-role="header" data-theme="b">
<h1>TCSS Gateway</h1>
</div>
<h2>Success</h2>
another submission
Download & Review CSA Document
</body>
''' % file_name
The short answer is that basically you need to write some in-memory stream (e.g. BytesIO), pre-set some HTTP headers and return a file_generator.
Your question is almost the same as one asked a month ago and here is my answer to it.
I've drafted a little snippet for you in python3:
from io import BytesIO
import cherrypy
from cherrypy.lib import file_generator
from docxtpl import DocxTemplate
class GenerateDocx:
#cherrypy.expose
def build_docx(self, *args, **kwargs):
iostream = BytesIO()
tpl = DocxTemplate('csa_tpl.docx')
...
# build your tpl here
...
tpl.get_docx().save(iostream)
cherrypy.response.headers['Content-Type'] = (
# set the correct MIME type for docx
'application/vnd.openxmlformats-officedocument'
'.wordprocessingml.document'
)
cherrypy.response.headers['Content-Disposition'] = (
'attachment; filename={fname}.docx'.format(
fname='put your file name here'
)
)
iostream.seek(0)
return file_generator(iostream)
UPD: I've just checked that the response body gets automatically wrapped with file_generator if the return value of a handler has read() method support
Related
Hey i am trying to make a bar graph in my html page. I am using Google Charts, flask and html. Whenever I manually enter data it works perfectly. But when I try to send the data through flask, it doesn't show anything.
Here is my application.py:
import flask
import mysql.connector
app = flask.Flask(__name__)
f = mysql.connector.connect(host = "localhost", user = "root", passwd = "")
mycur = f.cursor()
#app.route("/")
def get_vals():
mycur.execute("use one_stop_healthcare;")
mycur.execute("select NAME, ACTIVE, DISCHARGED from ccinfo;")
dat = mycur.fetchall()
active=[[]]
for i in range(0,5,1):
for j in range(0,2,1):
active[i].append(dat[i][j])
active.append([])
del active[5]
active.insert(0, ['All the wards', 'Active cases', 'Recovered'])
print(active)
return flask.render_template("index.html", active=active)
app.run()
Here is my index.html(In a template folder)
<html>
<head>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
google.charts.load('current', {packages: ['corechart', 'bar']});
google.charts.setOnLoadCallback(drawMultSeries);
var active
function drawMultSeries() {
// Here is where i manually add the data. It works like this.
var active = [['All the wards', 'Active cases', 'Recovered'], ['Ward-1', 357, 153], ['Ward-2', 744, 349], ['Ward-3', 83, 54], ['Ward-4', 1360, 433], ['Ward-5', 149, 100]]
var data = google.visualization.arrayToDataTable(active);
var options = {
title: 'Covid care centers',
chartArea: {width: '50%'},
hAxis: {
title: 'Active cases',
minValue: 0
},
vAxis: {
title: 'Wards'
}
};
var chart = new google.visualization.BarChart(document.getElementById('chart_div'));
chart.draw(data, options);
}
</script>
</head>
<body>
<div id="chart_div"></div>
</body>
You currently don't use the python object 'active' at all.
Replace this line in your html-code:
var active
by
var active = {{active}}
(and remove the manuall declaration)
I'm creating a web application using Flask and the Google Maps Directions API, where I ask the user to enter a starting point, waypoints, and a destination, and pass this data to the Google API to render a map illustrating the user's route. No matter what these data points are, however, the API keeps interpreting all 3 data points as a single location, and renders the map accordingly. The rendered map is
here, where you can also see what this exact wrong address is.
The relevant source files are as follows:
run.py (where I call my template to render the Google Map in the "/finalize_new_trip" route):
from os.path import abspath, dirname, join
from flask import flash, Flask, Markup, redirect, render_template, url_for, request
from flask.ext.sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired
import os
import googlemaps
from datetime import datetime
import json
_cwd = dirname(abspath(__file__))
SECRET_KEY = 'flask-session-insecure-secret-key'
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + join(_cwd, 'TripLogger.db')
SQLALCHEMY_ECHO = True
WTF_CSRF_SECRET_KEY = 'this-should-be-more-random' #TODO: RANDOMIZE THIS KEY
app = Flask(__name__)
app.config.from_object(__name__)
db = SQLAlchemy(app)
class Trip(db.Model):
__tablename__ = 'trips'
id = db.Column(db.Integer, primary_key=True)
trip_name = db.Column(db.String, unique=True)
origin = db.Column(db.String)
destination = db.Column(db.String)
waypoints = db.Column(db.String) #SEE IF ANOTHER DATA TYPE IS BETTER- IDEALLY WANT TO STORE LIST OF STRINGS
def __init__(self, trip_name, origin, destination, waypoints):
self.trip_name = trip_name
self.origin = origin
self.destination = destination
self.waypoints = waypoints
def __repr__(self):
return '<Trip %r(name = %r, origin = %r, destination = %r)>' % (self.id, self.trip_name, self.origin, self.destination)
def __str__(self):
return self.trip_name
#CONSIDER CUSTOM VALIDATORS TO ENSURE THAT TRIP_NAME IS UNIQUE FOR A SPECIFIC USER/ACCOUNT
class TripForm(FlaskForm):
trip_name = StringField('Trip Name', validators=[DataRequired()])
origin = StringField('Origin', validators=[DataRequired()])
destination = StringField('Destination', validators=[DataRequired()])
waypoints = StringField('Waypoint')
#app.route("/", methods = ["GET", "POST"])
def index():
trip_form = TripForm(request.form)
if trip_form.validate_on_submit():
flash("Submitted new trip form, now redirecting to /finalize_new_trip to render map...")
return redirect(url_for("complete_trip", trip_name = trip_form.trip_name.data, origin = trip_form.origin.data, destination = trip_form.destination.data, waypoints = trip_form.waypoints.data))
return render_template("index.html", trip_form = trip_form)
# Route to display trip on map (in future, to modify trip details, e.g. waypoints)
#app.route("/finalize_new_trip", methods=["POST"])
def complete_trip():
return render_template("finish_new_trip.html", trip_name = request.args.get("trip_name"), origin = request.args.get("origin"), destination = request.args.get("destination"), waypoints = request.args.get("waypoints"))
# Store new trip data in database
#app.route("/store_trip", methods=["POST"])
def store_trip():
trip_data = request.form("trip_data")
return json.loads(trip_data)[0]
def query_to_list(query, include_field_names=True):
"""Turns a SQLAlchemy query into a list of data values."""
column_names = []
for i, obj in enumerate(query.all()):
if i == 0:
column_names = [c.name for c in obj.__table__.columns]
if include_field_names:
yield column_names
yield obj_to_list(obj, column_names)
def obj_to_list(sa_obj, field_order):
"""Takes a SQLAlchemy object - returns a list of all its data"""
return [getattr(sa_obj, field_name, None) for field_name in field_order]
if __name__ == "__main__":
app.debug = True
db.create_all()
app.run()
finish_new_trip.html (the template using the Google Maps API to render the map):
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<script async defer
src="https://maps.googleapis.com/maps/api/js?v=3.exp&key=<MY_API_KEY>&callback=getOriginCoordinates" type="text/javascript">
</script>
<title>Finish New Trip</title>
</head>
<body>
<div id="trip_name">
<h1> {{ trip_name }} </h1>
</div>
<div id="map"></div>
<div id="right-panel">
<div>
<form>
<input type="submit" id="finish-new-trip-button" value="Add My Trip!">
</form>
</div>
<div id="directions-panel"></div>
</div>
<script>
//Wrapper function to AJAX POST request that passes data back to server.
/*function passTripData() {
document.getElementById("finish-new-trip-button").onclick = function() {
// Retrieve data from displayed map (after user makes changes) after testing
$.post( "{{ url_for('store_trip') }}", {
trip_data: {
"trip_name": "{{ trip_name }}",
"origin": "{{ origin }}",
"destination": "{{ destination }}",
"waypoints": [
"{{ waypoints }}"
]
}
});
}
}*/
//Use Geocoding API to get coordinates for origin, destination, and waypoints.
function getOriginCoordinates() {
//passTripData(); // function that sets onClick event for "Submit" button
var geocoder = new google.maps.Geocoder();
geocoder.geocode( { 'address': "{{ origin }}"}, function(results, status) {
if (status == 'OK') {
originCoordinates = results[0].geometry.location;
console.log("Origin coords: " + "lat = " + originCoordinates.lat() + ", lng = " + originCoordinates.lng());
getDestCoordinates(originCoordinates);
} else {
alert('Geocode of origin was not successful for the following reason: ' + status);
}
});
}
function getDestCoordinates(originCoords) {
var geocoder = new google.maps.Geocoder();
geocoder.geocode( { 'address': "{{ destination }}"}, function(results, status) {
if (status == 'OK') {
destCoordinates = results[0].geometry.location;
console.log("Dest coords: " + "lat = " + destCoordinates.lat() + ", lng = " + destCoordinates.lng());
getWaypointCoordinates(originCoords, destCoordinates);
} else {
alert('Geocode of destination was not successful for the following reason: ' + status);
}
});
}
function getWaypointCoordinates(originCoords, destCoords) {
var geocoder = new google.maps.Geocoder();
geocoder.geocode( { 'address': "{{ waypoints }}"}, function(results, status) {
if (status == 'OK') {
waypointCoordinates = results[0].geometry.location;
console.log("Waypoint coords: " + "lat = " + waypointCoordinates.lat() + ", lng = " + waypointCoordinates.lng());
initMap(originCoords, destCoords, waypointCoordinates);
} else {
alert('Geocode of waypoints was not successful for the following reason: ' + status);
}
});
}
function initMap(originCoords, destCoords, waypointCoords) {
var directionsService = new google.maps.DirectionsService;
var directionsDisplay = new google.maps.DirectionsRenderer;
//Center map between origin and destination.
//TEST LATLNG OBJECTS FIRST
console.log("Origin: lat=" + originCoords.lat() + ", lng=" + originCoords.lng());
console.log("Destination: lat=" + destCoords.lat() + ", lng=" + destCoords.lng());
var mapLatitudeCenter = (originCoords.lat() + destCoords.lat())/2;
var mapLongitudeCenter = (originCoords.lng() + destCoords.lng())/2;
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 6,
center: {lat: mapLatitudeCenter, lng: mapLongitudeCenter}
});
directionsDisplay.setMap(map);
calculateAndDisplayRoute(directionsService, directionsDisplay, originCoords, destCoords, waypointCoords);
}
function calculateAndDisplayRoute(directionsService, directionsDisplay, originCoords, destCoords, waypointCoords) {
var waypts = [];
waypts.push({
location: waypointCoords,
stopover: true
});
directionsService.route({
origin: originCoords,
destination: destCoords,
waypoints: waypts,
optimizeWaypoints: true,
travelMode: 'DRIVING'
}, function(response, status) {
if (status === 'OK') {
directionsDisplay.setDirections(response);
var route = response.routes[0];
var summaryPanel = document.getElementById('directions-panel');
summaryPanel.innerHTML = '';
// For each route, display summary information.
for (var i = 0; i < route.legs.length; i++) {
var routeSegment = i + 1;
summaryPanel.innerHTML += '<b>Route Segment: ' + routeSegment +
'</b><br>';
summaryPanel.innerHTML += route.legs[i].start_address + ' to ';
summaryPanel.innerHTML += route.legs[i].end_address + '<br>';
summaryPanel.innerHTML += route.legs[i].distance.text + '<br><br>';
}
} else {
window.alert('Directions request failed due to ' + status);
}
});
}
</script>
</body>
</html>
When I test my app locally, the console displays the latitude/longitude coordinates of each data point (origin, destination, and waypoints), and they all correspond to this incorrect address in Italy. I also previously received a "404" error message, but I am not receiving that now.
I attempted to regenerate my API key and use this new key, but this didn't fix the problem. How can I go about resolving this issue?
When I change complete_trip(): to
#app.route("/finalize_new_trip", methods=["GET"])
def complete_trip():
return render_template("finish_new_trip.html", trip_name ='new trip', origin='new york, ny', destination='los angeles, california', waypoints='topeka, kansas')
and request the page, the results are perfect (a trip from NY to LA that goes through Topeka). You've got a lot of commented out code everywhere. I have a feeling that somewhere you're not getting the data you expect (for example, we can't see the form HTML used to post this data).
https://developers.google.com/identity/sign-in/web/backend-auth
Im following the google tutorial to sign in and authenticate with a backend server, what im ultimately trying to do is restrict the access to the application to only the emails with the domain name of a company.
I think the problems lies in the javascript:
function onSignIn(googleUser) {
// Useful data for your client-side scripts:
var profile = googleUser.getBasicProfile();
console.log("ID: " + profile.getId()); // Don't send this directly to your server!
console.log('Full Name: ' + profile.getName());
console.log('Given Name: ' + profile.getGivenName());
console.log('Family Name: ' + profile.getFamilyName());
console.log("Image URL: " + profile.getImageUrl());
console.log("Email: " + profile.getEmail());
// The ID token you need to pass to your backend:
var id_token = googleUser.getAuthResponse().id_token;
console.log("ID Token: " + id_token);
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://yourbackend.example.com/tokensignin');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
console.log('Signed in as: ' + xhr.responseText);
};
xhr.send('idtoken=' + id_token);
};
heres the python part:
def autenticar_usuario(request):
# (Receive token by HTTPS POST)
CLIENT_ID="CLIENT_ID"
APPS_DOMAIN_NAME='example.com'
if request.method == 'POST':
token = request.POST["idtoken"]
try:
idinfo = client.verify_id_token(token, CLIENT_ID)
# If multiple clients access the backend server:
if idinfo['aud'] not in [CLIENT_ID]:
raise crypt.AppIdentityError("Unrecognized client.")
if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
raise crypt.AppIdentityError("Wrong issuer.")
if idinfo['hd'] != APPS_DOMAIN_NAME:
raise crypt.AppIdentityError("Wrong hosted domain.")
except crypt.AppIdentityError:
# Invalid token
userid = idinfo['sub']
return render(request, 'log_in.html')
and the error says:
'CSRF verification failed. Request aborted.'
html:
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title>Log In</title>
</head>
<body>
<meta name="google-signin-client_id" content="">
<div class="g-signin2" data-onsuccess="onSignIn"></div>
<script>
function onSignIn(googleUser) {
// Useful data for your client-side scripts:
var profile = googleUser.getBasicProfile();
console.log("ID: " + profile.getId()); // Don't send this directly to your server!
console.log('Full Name: ' + profile.getName());
console.log('Given Name: ' + profile.getGivenName());
console.log('Family Name: ' + profile.getFamilyName());
console.log("Image URL: " + profile.getImageUrl());
console.log("Email: " + profile.getEmail());
// The ID token you need to pass to your backend:
var id_token = googleUser.getAuthResponse().id_token;
console.log("ID Token: " + id_token);
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://yourbackend.example.com/tokensignin');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
console.log('Signed in as: ' + xhr.responseText);
};
xhr.send('idtoken=' + id_token);
};
</script>
<script src="https://apis.google.com/js/platform.js" async defer></script>
</body>
I am using the following code to implement the backend for a javascript EventSource
from flask import Flask, Response
from time import sleep
import time
class ServerSentEvent(object):
def __init__(self, data):
self.data = data
self.event = None
self.id = None
self.desc_map = {
self.data : "data",
self.event : "event",
self.id : "id",
self.retry: 500
}
def encode(self):
if not self.data:
return ""
lines = ["%s: %s" % (v, k)
for k, v in self.desc_map.items() if k]
return "%s\n\n" % "\n".join(lines)
def stream():
while True:
ev = ServerSentEvent('hi ' + str(int(round(time.time()))))
yield ev
sleep(0.1)
app = Flask(__name__)
#app.route("/events")
def streamSessionEvents():
return Response(stream(), mimetype="text/event-stream")
#app.route("/")
def index():
template = """
<!doctype html>
<html>
<head>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
var source = new EventSource('/events')
source.onmessage = function (event) {
$('#log').append(event.data + '</br>')
}
</script>
</head>
<body>
<div id="log"></div>
</body>
</html>
"""
return(template)
app.run(threaded=True)
However the EventSource keeps reconnecting every 3 seconds (which is the default) because the connection is closed by the server after every event. How can I establish a continuous connection?
CherryPy keeps returning blank pages or with the values I return in the controllers. I rewrote a django and jinja2 version that did work, apparently this one doesn't which is almost identical to the previous mentioned.
I did some pprint's in the tool bit which does fill the request.body with parsed html but doesn't output it when pass is set in the controller. If I return a {'user':True} in the controller that is shown in the form of a simple "User".
with a few examples online and the code of SickBeard I came to the following:
controller:
class RootController(object):
#cherrypy.expose
#cherrypy.tools.render(template="page/home.html")
def index(self):
pass
tool:
class CheetahTool(cherrypy.Tool):
def __init__(self):
cherrypy.Tool.__init__(self, 'on_start_resource',
self._render,
priority=30)
def _render(self, template=None, debug=False):
if cherrypy.response.status > 399:
return
# retrieve the data returned by the handler
data = cherrypy.response.body or {}
template = cherrypy.engine.publish("lookup-template", template).pop()
if template and isinstance(data, dict):
for k,v in data:
template.__setattr__(k, v)
# dump the template using the dictionary
if debug:
try:
cherrypy.response.body = unicode(template).encode('utf-8', 'xmlcharrefreplace')
except Exception as e:
from pprint import pprint
pprint(e.message)
else:
cherrypy.response.body = template.respond()
plugin:
class PageTemplate(Template):
"""
Thank you SickBeard
"""
def __init__(self, base_dir, template, *args, **KWs):
KWs['file'] = os.path.join(base_dir, template)
super(PageTemplate, self).__init__(*args, **KWs)
application = cherrypy.tree.apps['']
config = application.config
self.sbRoot = base_dir
self.sbHttpPort = config['global']['server.socket_port']
self.sbHttpsPort = self.sbHttpPort
self.sbHttpsEnabled = False
if cherrypy.request.headers['Host'][0] == '[':
self.sbHost = re.match("^\[.*\]", cherrypy.request.headers['Host'], re.X|re.M|re.S).group(0)
else:
self.sbHost = re.match("^[^:]+", cherrypy.request.headers['Host'], re.X|re.M|re.S).group(0)
if "X-Forwarded-Host" in cherrypy.request.headers:
self.sbHost = cherrypy.request.headers['X-Forwarded-Host']
if "X-Forwarded-Port" in cherrypy.request.headers:
self.sbHttpPort = cherrypy.request.headers['X-Forwarded-Port']
self.sbHttpsPort = self.sbHttpPort
if "X-Forwarded-Proto" in cherrypy.request.headers:
self.sbHttpsEnabled = True if cherrypy.request.headers['X-Forwarded-Proto'] == 'https' else False
self.sbPID = str(aquapi.PID)
self.menu = [
{ 'title': 'Home', 'key': 'home' },
{ 'title': 'Users', 'key': 'users' },
{ 'title': 'Config', 'key': 'config' },
]
def render(self):
return unicode(self).encode('utf-8', 'xmlcharrefreplace')
class CheetahTemplatePlugin(plugins.SimplePlugin):
def __init__(self, bus, base_dir=None, base_cache_dir=None,
collection_size=50, encoding='utf-8'):
plugins.SimplePlugin.__init__(self, bus)
self.base_dir = base_dir
self.base_cache_dir = base_cache_dir or tempfile.gettempdir()
self.encoding = encoding
self.collection_size = collection_size
def start(self):
self.bus.log('Setting up Cheetah resources')
self.bus.subscribe("lookup-template", self.get_template)
def stop(self):
self.bus.log('Freeing up Cheetah resources')
self.bus.unsubscribe("lookup-template", self.get_template)
self.lookup = None
def get_template(self, name):
"""
Returns Cheetah's template by name.
"""
return PageTemplate(self.base_dir, name)
init:
# Template engine tool
from aquapi.web.tools.template import CheetahTool
cherrypy.tools.render = CheetahTool()
# Tool to load the logged in user or redirect
# the client to the login page
from aquapi.web.tools.user import UserTool
cherrypy.tools.user = UserTool()
from aquapi.web.controllers import RootController
webapp = RootController()
# Let's mount the application so that CherryPy can serve it
app = cherrypy.tree.mount(webapp, '/', os.path.join(self.base_dir, "app.cfg"))
# Template engine plugin
from aquapi.web.plugin.template import CheetahTemplatePlugin
engine.cheetah = CheetahTemplatePlugin(engine,
os.path.join(self.base_dir, 'aquapi/web/templates'),
os.path.join(self.base_dir, 'cache'))
engine.cheetah.subscribe()
In general, to me it's some sort of over-engineering happened in your snippets. CherryPy plugins are usually used for a system task (e.g. put PID-file on engine start, remove it on stop) or for an asynchronous task (e.g. sending email in separate thread). Template rendering happens clearly synchronously to the request handling, so I don't see the point of extracting this logic out of CherryPy tool. There's a class in CherryPy, cherrypy._cptools.HandlerWrapperTool, which demonstrate the suggested approach to wrapping handler return values.
I haven't ever used Cheetah, so my example is Jinja2-based. You will just have to change the templating engine instance (to Cheetah) and correct its calls. The rest is the same.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import types
import cherrypy
import jinja2
path = os.path.abspath(os.path.dirname(__file__))
config = {
'global' : {
'server.socket_host' : '127.0.0.1',
'server.socket_port' : 8080,
'server.thread_pool' : 4
}
}
class TemplateTool(cherrypy.Tool):
_engine = None
'''Jinja environment instance'''
def __init__(self):
viewLoader = jinja2.FileSystemLoader(os.path.join(path, 'view'))
self._engine = jinja2.Environment(loader = viewLoader)
cherrypy.Tool.__init__(self, 'before_handler', self.render)
def __call__(self, *args, **kwargs):
if args and isinstance(args[0], (types.FunctionType, types.MethodType)):
# #template
args[0].exposed = True
return cherrypy.Tool.__call__(self, **kwargs)(args[0])
else:
# #template()
def wrap(f):
f.exposed = True
return cherrypy.Tool.__call__(self, *args, **kwargs)(f)
return wrap
def render(self, name = None):
cherrypy.request.config['template'] = name
handler = cherrypy.serving.request.handler
def wrap(*args, **kwargs):
return self._render(handler, *args, **kwargs)
cherrypy.serving.request.handler = wrap
def _render(self, handler, *args, **kwargs):
template = cherrypy.request.config['template']
if not template:
parts = []
if hasattr(handler.callable, '__self__'):
parts.append(handler.callable.__self__.__class__.__name__.lower())
if hasattr(handler.callable, '__name__'):
parts.append(handler.callable.__name__.lower())
template = u'/'.join(parts)
data = handler(*args, **kwargs) or {}
renderer = self._engine.get_template(u'{0}.html'.format(template))
return renderer.render(**data)
cherrypy.tools.template = TemplateTool()
class App:
#cherrypy.expose
def index(self):
'''No renderer applied, CherryPy outputs dict keys'''
return {'user': 123}
#cherrypy.tools.template
def auto(self):
return {'user': 123}
#cherrypy.tools.template(name = 'app/auto')
def manual(self):
return {'user': 234}
if __name__ == '__main__':
cherrypy.quickstart(App(), '/', config)
Along the python file, create directory view/app and put the following in file named auto.html there.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' content='text/html; charset=utf-8' />
<title>Test</title>
</head>
<body>
<p>User: <em>{{ user }}</em></p>
</body>
</html>
Some notes on the TemplateTool. First, you can use it as a decorator in two ways: not making a call, and making a call with template name argument. You can use the tool as any other CherryPy tool in the configuration (e.g. make all controller methods to render templates). Second, following convention-over-configuration principle, the tool when not provided with template name will use classname/methodname.html. Third, the decorator exposes the method, so you don't need to add #cherrypy.expose on top.