This page includes a hit counter that uses a Google App Engine to store, retrieve, and increment a count. Polling is used continuously after page load to keep the count current - if the page is accessed by another browser, the page count will update this page as well. Polling is set to each 10 seconds.
With the help of the Prototype library, the javascript to do the Ajax request is shown in the box. The hitcount URL has two actions inc and get. The increment uses a POST action, since it updates state on the server, where as the get is a query, and uses the GET action.
An initial inc is followed by periodical get's, to keep the count current. The tag is a string identifing the counter, but could be the URL of this page, properly escaped for CGI.
<body onload="dohitcounter('hitcounter');">
<script type="text/javascript">
function dohitcounter(tag) {
new Ajax.Updater({success:'hitcount',failure:'errmsg'},
'/hitcount/inc', { method: 'post', parameters: { 'tag':tag }});
$('errmsg').update("OK");
new Ajax.PeriodicalUpdater({success:'hitcount',failure:'errmsg'},
'/hitcount/get', { method: 'get', parameters: { 'tag':tag }, frequency: 10 });
$('errmsg').update("OK");
}
</script>
The following URL's are suppored:
Shown in the box are the three class that I think you should look at first: the data model, the increment code and the query code.
A certain amount of clean up and protection occurs in gettag(), limit the length of tags, and restricting them to the character set of a URL. Also, cancreate() budgets the number of counters that can be created per server start, to avoid a resource exhaustion attack (and running up a bill for storage).
class Counter(db.Model):
count = db.IntegerProperty(required=True)
label = db.StringProperty(required=True)
class IncPage(webapp.RequestHandler):
def post(self):
self.response.headers['Content-Type'] = 'text/plain'
tag = gettag(self.request)
# query database for url u, increment by 1, or otherwise set to 1
c = db.GqlQuery("SELECT * FROM Counter WHERE label IN :1", [tag]).get()
if c == None:
if cancreate():
c = Counter(count=0,label=tag)
else:
self.response.out.write(str(0))
return
c.count += 1
c.put()
self.response.out.write(str(c.count))
def get(self):
# only for debugging, get's can be cached and count might not increment
self.post()
class GetPage(webapp.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/plain'
tag = gettag(self.request)
# query database for url u, return result, or otherwise return 0
c = db.GqlQuery("SELECT * FROM Counter WHERE label IN :1", [tag]).get()
if c == None:
self.response.out.write(str(0))
else:
self.response.out.write(str(c.count))
Here is the entire code as a text file.