Pyjamas + Django = Pure Win
In my last post, I focused on detailing a new way of developing web applications (using GWT/Pyjamas) and showing examples of compiled web front-ends. In this post I am going to bring everything together (so both front- and back-ends).
In the past I made a post about Django, which is my absolute favorite web application (back-end) framework. Coincidentally, Pyjamas (this is for creating the front-end, remember?!) ALSO uses Python. So in case you haven't had an aneurysm from the pure win yet, let me boil it down for you: you can write your entire web application in a single language, and an awesome one at that! For those of you who have created native GUI programs and web apps before, you can easily see the advantages that this type of approach has (hint: web development is... painful).
So enough of my fawning, let's roll!
My example is a modified version of the original GWT sample application used in my previous post. I will be adding persistence to the app. So when you add stocks to your list, they will show up again when you revisit the page, etc. Also, this will not be a global list that is viewable to all users, but is saved on a per-user basis. Also, it will NOT require an account (i.e. no login to tie the stocks lists to), it will be anonymous, based on cookies only. Not terribly complicated but it should make for a good example.
So here is the source as we left it, and here are the rest of the associated files. This is where we'll be starting from.
First off, here is the end product: http://stockwatcher2.derekschaefer.net/
It's the same as before, except it has persistence based on a Django session cookie set to expire in one year.
Try adding a few "stocks" and then refreshing, or even closing the browser and coming back. The same stocks should reappear. The only reason they won't is if you have configured your browser to be especially judicious when it comes to cookies, otherwise it should work fine (tested in Chrome, Firefox, Opera). The only exception is IE 8, as far as I know, which has rather severe problems with complex Javascript and I just can't be asked to make compatibility changes at the moment.
Let's first create the Django model for the database we're going to use to keep track of. It doesn't need to be complex, but we need to track everything based the session.
from django.db import models
from django.contrib.sessions.models import Session
class Stocks(models.Model):
session = models.ForeignKey(Session)
symbol = models.CharField(max_length=10)
def __unicode__(self):
return '%s | %s' % (self.session.pk, self.symbol)
To tie the stocks to a given session, it's as simple as importing the Session model and creating a foreign key field for it (you must have "django.contrib.sessions" listed under "INSTALLED_APPS" in your settings file, like so:
...
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
# Uncomment the next line to enable the admin:
'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'stockwatcher2.stockwatcher',
)
...
Now let's look at the views. The first thing we need to consider is how Pyjamas and Django are going to communicate. As it turns out, the most convenient way to go about this to use JSON-RPC (a.k.a. JSON remote procedure calls). Pyjamas already has this functionality build it, so we only need to utilize it, but Django has a somewhat harder time. To cut right to it, I've found that the best option is libcommonDjango (which also contains lots of other useful things for Django). It will easily allow us to setup an RPC environment using standard Django views. First, you will have to download libcommonDjango and run "python setup.py install" which will place it in your "site-packages" directory so it will be on your $PYTHONPATH. And that's it! Now let's look at how to use it:
from django_pimentech.network import *
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.contrib.sessions.models import Session
from stockwatcher.models import Stocks
import datetime
service = JSONRPCService()
def index(request):
request.session.set_expiry(datetime.datetime.now() + datetime.timedelta(365))
request.session.save()
return render_to_response('StockWatcher.html')
def getStocks(request):
try:
session = Session.objects.get(pk=request.session._session_key)
stocks = Stocks.objects.filter(session=session)
except Exception as e:
return []
return [(stock.symbol) for stock in stocks]
service.add_method('getStocks', getStocks)
def addStock(request, symbol):
session = Session.objects.get(pk=request.session._session_key)
# Don't allow duplicates
if len(Stocks.objects.filter(session=session, symbol=symbol)) > 0:
return getStocks(request)
stock = Stocks()
stock.session = session
stock.symbol = symbol
stock.save()
return getStocks(request)
service.add_method('addStock', addStock)
def deleteStock(request, symbol):
try:
session = Session.objects.get(pk=request.session._session_key)
Stocks.objects.get(session=session, symbol=symbol).delete()
except Stocks.DoesNotExist:
pass
return getStocks(request)
service.add_method('deleteStock', deleteStock)
As you can see, these are just normal Django view functions. The only difference is we import "from django_pimentech.network import *" and then setup the service variable to be a "JSONRPCService()" object. Finally then adding each view to the service by calling "service.add_method", which can also be done with a decorator but I've found this way to be more compatible.
The other things of note are "request.session.set_expiry(datetime.datetime.now() + datetime.timedelta(365))" and retrieving the session primary key via "request.session._session_key". With the former I am setting the session (and correspondingly the session cookie that will be stored) expiration date to one year (totally arbitrary) from the current time.
Also, you will need to add the following to your URLs file like so:
urlpatterns = patterns('',
...
(r'^services/$', 'stockwatcher.views.service'),
...
)
Additionally, you will probably run into a CSRF verification error, in which case you need to disable CSRF verification in your settings file by removing "django.middleware.csrf.CsrfViewMiddleware" from "MIDDLEWARE_CLASSES".
Now the JSON-RPC environment is ready to go, which leaves the Pyjamas front-end.
In terms of organization, it's easiest if you place all the Pyjamas on your Django "MEDIA_PATH", as you can see here: http://downloads.derekschaefer.net/index.php?dir=pyjamas/stockwatcher2/
Once that is done, we just need to modify "StockWatcher.py".
import pyjd
from pyjamas.ui.RootPanel import RootPanel
from pyjamas.ui.Label import Label
from pyjamas.ui.Button import Button
from pyjamas.ui.FlexTable import FlexTable
from pyjamas.ui.HorizontalPanel import HorizontalPanel
from pyjamas.ui.TextBox import TextBox
from pyjamas.ui.VerticalPanel import VerticalPanel
from pyjamas.ui.KeyboardListener import KeyboardHandler, KEY_ENTER
from pyjamas.Timer import Timer
from pyjamas import Window
from pyjamas.JSONService import JSONProxy
from StockPrice import StockPrice
import re
import random
import datetime
class StockWatcher:
def onModuleLoad(self):
'''
This is the main entry point method.
'''
# Setup JSON RPC
self.remote = DataService()
# Initialize member variables
self.mainPanel = VerticalPanel()
self.stocksFlexTable = FlexTable()
self.addPanel = HorizontalPanel()
self.newSymbolTextBox = TextBox()
self.lastUpdatedLabel = Label()
self.addStockButton = Button('Add', self.addStock)
self.stocks = []
self.stocksTableColumns = ['Symbol', 'Price', 'Change', 'Remove']
# Add styles to elements in the stock list table
self.stocksFlexTable.getRowFormatter().addStyleName(0, 'watchListHeader')
self.stocksFlexTable.addStyleName('watchList')
self.stocksFlexTable.getCellFormatter().addStyleName(0, 1, 'watchListNumericColumn')
self.stocksFlexTable.getCellFormatter().addStyleName(0, 2, 'watchListNumericColumn')
self.stocksFlexTable.getCellFormatter().addStyleName(0, 3, 'watchListRemoveColumn')
# Create table for stock data
for i in range(len(self.stocksTableColumns)):
self.stocksFlexTable.setText(0, i, self.stocksTableColumns[i])
# Assemble Add Stock panel
self.addPanel.add(self.newSymbolTextBox)
self.addPanel.add(self.addStockButton)
self.addPanel.addStyleName('addPanel')
# Assemble Main panel
self.mainPanel.add(self.stocksFlexTable)
self.mainPanel.add(self.addPanel)
self.mainPanel.add(self.lastUpdatedLabel)
# Associate the Main panel with the HTML host page
RootPanel('stocksList').add(self.mainPanel)
# Move cursor focus to the input box
self.newSymbolTextBox.setFocus(True)
# Setup timer to refresh list automatically
refresh = self.refreshWatchlist
class MyTimer(Timer):
def run(self):
refresh()
refreshTimer = MyTimer()
refreshTimer.scheduleRepeating(5000)
# Listen for keyboard events in the input box
self_addStock = self.addStock
class StockTextBox_KeyboardHandler():
def onKeyPress(self, sender, keycode, modifiers):
if keycode == KEY_ENTER:
self_addStock()
def onKeyDown(self, sender, keycode, modifiers): return
def onKeyUp(self, sender, keycode, modifiers): return
self.newSymbolTextBox.addKeyboardListener(StockTextBox_KeyboardHandler())
# Load the stocks
self.remote.getStocks(self)
def addStock(self, sender, symbol=None):
'''
Add stock to FlexTable. Executed when the user clicks the addStockButton
or presses enter in the newSymbolTextBox
'''
if symbol is None:
# Get the symbol
symbol = self.newSymbolTextBox.getText().upper().trim()
self.newSymbolTextBox.setText('')
# Don't add the stock if it's already in the table
if symbol in self.stocks:
return
# Tell the server that we're adding this stock
self.remote.addStock(symbol, self)
self.newSymbolTextBox.setFocus(True)
# Stocks code must be between 1 and 10 chars that are numbers/letters/dots
p = re.compile('^[0-9A-Z\\.]{1,10}$')
if p.match(symbol) == None:
Window.alert('"%s" is not a valid symbol.' % symbol)
self.newSymbolTextBox.selectAll()
return
# Add the stock to the table
row = self.stocksFlexTable.getRowCount()
self.stocks.append(symbol)
self.stocksFlexTable.setText(row, 0, symbol)
self.stocksFlexTable.setWidget(row, 2, Label())
self.stocksFlexTable.getCellFormatter().addStyleName(row, 1, 'watchListNumericColumn')
self.stocksFlexTable.getCellFormatter().addStyleName(row, 2, 'watchListNumericColumn')
self.stocksFlexTable.getCellFormatter().addStyleName(row, 3, 'watchListRemoveColumn')
# Add a button to remove this stock from the table
def _removeStockButton_Click(event):
if symbol not in self.stocks:
return
removedIndex = self.stocks.index(symbol)
self.remote.deleteStock(symbol, self)
self.stocks.remove(symbol)
self.stocksFlexTable.removeRow(removedIndex + 1)
removeStockButton = Button('x', _removeStockButton_Click)
removeStockButton.addStyleDependentName('remove')
self.stocksFlexTable.setWidget(row, 3, removeStockButton)
# Get the stock price
self.refreshWatchlist()
def refreshWatchlist(self):
'''
Update the price change for each stock
'''
MAX_PRICE = 100.0
MAX_PRICE_CHANGE = 0.02
prices = []
for i in range(len(self.stocks)):
price = random.random() * MAX_PRICE
change = price * MAX_PRICE_CHANGE * (random.random() * 2.0 - 1.0)
prices.append(StockPrice(self.stocks[i], price, change))
self.updateTable(prices)
def updateTable(self, prices):
'''
Update the price and change fields of all the rows in the stock table
prices -- List of StockPrice objects for all rows
'''
# Type checking
assert isinstance(prices, list)
for price in prices:
assert isinstance(price, StockPrice)
# Nothing to do...
if len(prices) == 0:
return
# Update each individual row
for i in range(len(prices)):
self.updateRow(prices[i])
# Display timestamp showing last refresh
self.lastUpdatedLabel.setText("Last update: %s" % datetime.datetime.now().strftime("%m/%d/%Y %I:%M:%S %p"))
def updateRow(self, price):
'''
Update a single row in the stock table
price -- StockPrice object for a single row
'''
# Type checking
assert isinstance(price, StockPrice)
# Make sure the stock is still in the stock table
if price.symbol not in self.stocks:
return
# Find the index of
row = self.stocks.index(price.symbol) + 1
# Populate the price and change fields with new data
self.stocksFlexTable.setText(row, 1, '%.2f' % price.price)
changeWidget = self.stocksFlexTable.getWidget(row, 2)
changeWidget.setText('%.2f (%.2f%%)' % (price.change, price.getChangePercent()))
# Change the color of the text in the Change field based on its value
changeStyleName = 'noChange'
if price.getChangePercent() < -0.1:
changeStyleName = 'negativeChange'
else:
changeStyleName = 'positiveChange'
changeWidget.setStyleName(changeStyleName)
def onRemoteResponse(self, response, request_info):
'''
Called when a response is received from a RPC.
'''
if request_info.method in DataService.methods:
# Compare self.stocks and the stocks in response
stocks_set = set(self.stocks)
response_set = set(response)
# Add the differences
for symbol in list(response_set.difference(stocks_set)):
self.addStock(None, symbol)
else:
Window.alert('Unrecognized JSONRPC method.')
class DataService(JSONProxy):
methods = ['getStocks', 'addStock', 'deleteStock']
def __init__(self):
JSONProxy.__init__(self, 'services/', DataService.methods)
if __name__ == '__main__':
pyjd.setup('./StockWatcher.html')
app = StockWatcher()
app.onModuleLoad()
pyjd.run()
Essentially it's all the same as last time, except now we import "JSONProxy" from "pyjamas.JSONService" then subclass it, change the code in the "addStock" method, and add the "onRemoteResponse" method. Now the stocks (if any) will be loaded when the site is viewed, and stocks will be saved/deleted on the server when the user does. Not bad, right?
...and that how one writes an entire web application in Python!
Again, you can view all of the code here and use the actual application here.
Thanks for reading.
UPDATE (6/5/2011): A commenter has notified me that in the most recent version of Pyjamas, you have to change RootPanel('stocksList').add(self.mainPanel) to RootPanel().add(self.mainPanel) in order to get this demo to work. I'll look into this soon and post another update.

February 26th, 2011 - 03:57
excellent article. Thank you very much
April 27th, 2011 - 14:55
Kind of disappointed that pyjamas doesn’t let you add python lists together. Also kinda disappointed that there is IE breakage
Node.js here we go!
April 27th, 2011 - 14:56
But overall, nice article and very informative. Thanks Derek!
April 28th, 2011 - 14:07
Jay,
Please use the latest git or wait a week or so for the 0.8 release … 0.7 is incredibly old and missing many improvements.
I’m not able to test ATM but I’m pretty sure we support list concatenation, and we have several users who depend on proper IE functionality.
May 3rd, 2011 - 08:36
Which parts of the IE functionality is broken? I use a Linux machine for developing and testing pyjamas and Django, so I have not really tested my apps under IE much for obvious reasons…
May 6th, 2011 - 13:53
The tutorial is pure win too! Thanks! I ran it on the git version of pyjamas and it works fine, though there is one tricky change I had to make:
From
RootPanel(‘stocklist’).add(self.mainPanel)
To
RootPanel().add(self.mainPanel) #removed stocklist
I haven’t really looked into this, but it is probably a glitch in the newer version of pyjamas.
Again — great stuff!
May 6th, 2011 - 14:19
Thanks, Pyjamas is quite awesome!
Hmmm, interesting. I’ll make a note of that in the post, and try to find the cause.
June 12th, 2011 - 11:21
How do you serve Pyajamas output pages as Django views?
October 29th, 2011 - 09:44
Ok, that is great except the stock watcher doesn’t work. The prices are not the correct ones and they are changing on Saturday.
November 10th, 2011 - 08:22
That’s correct! Pulling real data would involve back-end work which was beyond the scope of this tutorial.