From 2173098d8d4d3f350f9b6642292f42a730d2864a Mon Sep 17 00:00:00 2001 From: Michael Ziegler Date: Sun, 29 Aug 2010 14:51:02 +0200 Subject: [PATCH] import the new and shiny djextdirect module --- pyweb/djextdirect/__init__.py | 21 ++ pyweb/djextdirect/client.py | 190 ++++++++++++++++ pyweb/djextdirect/formprovider.py | 352 ++++++++++++++++++++++++++++++ pyweb/djextdirect/provider.py | 334 ++++++++++++++++++++++++++++ pyweb/djextdirect/views.py | 35 +++ 5 files changed, 932 insertions(+) create mode 100644 pyweb/djextdirect/__init__.py create mode 100644 pyweb/djextdirect/client.py create mode 100644 pyweb/djextdirect/formprovider.py create mode 100644 pyweb/djextdirect/provider.py create mode 100644 pyweb/djextdirect/views.py diff --git a/pyweb/djextdirect/__init__.py b/pyweb/djextdirect/__init__.py new file mode 100644 index 0000000..f161d62 --- /dev/null +++ b/pyweb/djextdirect/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# kate: space-indent on; indent-width 4; replace-tabs on; + +""" + * Copyright (C) 2010, Michael "Svedrin" Ziegler + * + * djExtDirect is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +VERSION = ( 0, 3 ) + +VERSIONSTR = "v%d.%d" % VERSION + diff --git a/pyweb/djextdirect/client.py b/pyweb/djextdirect/client.py new file mode 100644 index 0000000..f83bc79 --- /dev/null +++ b/pyweb/djextdirect/client.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +# kate: space-indent on; indent-width 4; replace-tabs on; + +""" + * Copyright (C) 2010, Michael "Svedrin" Ziegler + * + * djExtDirect is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import simplejson +import httplib +from threading import Lock +from urlparse import urljoin, urlparse + +def lexjs(javascript): + """ Parse the given javascript and return a dict of variables defined in there. """ + ST_NAME, ST_ASSIGN = range(2) + state = ST_NAME + foundvars = {} + buf = "" + name = "" + + for char in javascript: + if state == ST_NAME: + if char == ' ': + continue + elif char == '=': + state = ST_ASSIGN + name = buf + buf = "" + elif char == ';': + state = ST_NAME + buf = "" + else: + buf += char + + elif state == ST_ASSIGN: + if char == ';': + state = ST_NAME + foundvars[name] = simplejson.loads(buf) + name = "" + buf = "" + else: + buf += char + + return foundvars + +class RequestError(Exception): + """ Raised if the request returned a status code other than 200. """ + pass + +class ReturnedError(Exception): + """ Raised if the "type" field in the response is "exception". """ + pass + +class Client(object): + """ Ext.Direct client side implementation. + + This class handles parsing an API specification, building proxy objects from it, + and making calls to the router specified in the API. + + Instantiation: + + >>> cli = Client( "http://localhost:8000/mumble/api/api.js", "Ext.app.REMOTING_API" ) + + The apiname parameter defaults to ``Ext.app.REMOTING_API`` and is used to select + the proper API variable from the API source. + + The client will then create proxy objects for each action defined in the URL, + which are accessible as properties of the Client instance. Suppose your API defines + the ``Accounts`` and ``Mumble`` actions, then the client will provide those as such: + + >>> cli.Accounts + + >>> cli.Mumble + + + These objects provide native Python methods for each method defined in the actions: + + >>> cli.Accounts.login + > + + So, in order to make a call over Ext.Direct, you would simply call the proxy method: + + >>> cli.Accounts.login( "svedrin", "passwort" ) + {'success': True} + """ + + def __init__( self, apiurl, apiname="Ext.app.REMOTING_API", cookie=None ): + self.apiurl = apiurl + self.apiname = apiname + self.cookie = cookie + + purl = urlparse( self.apiurl ) + conn = httplib.HTTPConnection( purl.netloc ) + conn.putrequest( "GET", purl.path ) + conn.endheaders() + resp = conn.getresponse() + conn.close() + foundvars = lexjs( resp.read() ) + + self.api = foundvars[apiname] + self.routerurl = urljoin( self.apiurl, self.api["url"] ) + + self._tid = 1 + self._tidlock = Lock() + + for action in self.api['actions']: + setattr( self, action, self.get_object(action) ) + + @property + def tid( self ): + """ Thread-safely get a new TID. """ + self._tidlock.acquire() + self._tid += 1 + newtid = self._tid + self._tidlock.release() + return newtid + + def call( self, action, method, *args ): + """ Make a call to Ext.Direct. """ + reqtid = self.tid + data=simplejson.dumps({ + 'tid': reqtid, + 'action': action, + 'method': method, + 'data': args, + 'type': 'rpc' + }) + + purl = urlparse( self.routerurl ) + conn = httplib.HTTPConnection( purl.netloc ) + conn.putrequest( "POST", purl.path ) + conn.putheader( "Content-Type", "application/json" ) + conn.putheader( "Content-Length", len(data) ) + if self.cookie: + conn.putheader( "Cookie", self.cookie ) + conn.endheaders() + conn.send( data ) + resp = conn.getresponse() + conn.close() + + if resp.status != 200: + raise RequestError( resp.status, resp.reason ) + + respdata = simplejson.loads( resp.read() ) + if respdata['type'] == 'exception': + raise ReturnedError( respdata['message'], respdata['where'] ) + if respdata['tid'] != reqtid: + raise RequestError( 'TID mismatch' ) + + cookie = resp.getheader( "set-cookie" ) + if cookie: + self.cookie = cookie.split(';')[0] + + return respdata['result'] + + def get_object( self, action ): + """ Return a proxy object that has methods defined in the API. """ + + def makemethod( methspec ): + def func( self, *args ): + if len(args) != methspec['len']: + raise TypeError( '%s() takes exactly %d arguments (%d given)' % ( + methspec['name'], methspec['len'], len(args) + ) ) + return self._cli.call( action, methspec['name'], *args ) + + func.__name__ = methspec['name'] + return func + + def init( self, cli ): + self._cli = cli + + attrs = { + '__init__': init + } + + for methspec in self.api['actions'][action]: + attrs[methspec['name']] = makemethod( methspec ) + + return type( action+"Prx", (object,), attrs )( self ) diff --git a/pyweb/djextdirect/formprovider.py b/pyweb/djextdirect/formprovider.py new file mode 100644 index 0000000..5122835 --- /dev/null +++ b/pyweb/djextdirect/formprovider.py @@ -0,0 +1,352 @@ +# -*- coding: utf-8 -*- +# kate: space-indent on; indent-width 4; replace-tabs on; + +""" + * Copyright (C) 2010, Michael "Svedrin" Ziegler + * + * djExtDirect is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import simplejson +import functools + +from django import forms +from django.http import HttpResponse, Http404 +from django.conf.urls.defaults import url +from django.utils.safestring import mark_safe + +from provider import Provider + +# Template used for the auto-generated form classes +EXT_CLASS_TEMPLATE = """ +Ext.namespace('Ext.ux'); + +Ext.ux.%(clsname)s = function( config ){ + Ext.apply( this, config ); + + var defaultconf = %(defaultconf)s; + + Ext.applyIf( this, defaultconf ); + this.initialConfig = defaultconf; + + this.api = %(apiconf)s; + + Ext.ux.%(clsname)s.superclass.constructor.call( this ); + + this.form.api = this.api; + this.form.paramsAsHash = true; + + if( typeof config.pk != "undefined" ){ + this.load(); + } +} + +Ext.extend( Ext.ux.%(clsname)s, Ext.form.FormPanel, { + load: function(){ + this.getForm().load({ params: Ext.applyIf( {pk: this.pk}, this.baseParams ) }); + }, + submit: function(){ + this.getForm().submit({ + params: Ext.applyIf( {pk: this.pk}, this.baseParams ), + failure: function( form, action ){ + if( action.failureType == Ext.form.Action.SERVER_INVALID && + typeof action.result.errors['__all__'] != 'undefined' ){ + Ext.Msg.alert( "Error", action.result.errors['__all__'] ); + } + } + }); + }, +} ); + +Ext.reg( '%(clslowername)s', Ext.ux.%(clsname)s ); +""" +# About the this.form.* lines, see +# http://www.sencha.com/forum/showthread.php?96001-solved-Ext.Direct-load-data-in-extended-Form-fails-%28scope-issue%29 + +EXT_DYNAMICCHOICES_COMBO = """ +Ext.namespace('Ext.ux'); + +Ext.ux.ChoicesCombo = function( config ){ + Ext.apply( this, config ); + + Ext.applyIf( this, { + displayField: this.name, + valueField: this.name, + hiddenName: this.name, + autoSelect: false, + typeAhead: true, + emptyText: 'Select...', + triggerAction: 'all', + selectOnFocus: true, + }); + + this.triggerAction = 'all'; + this.store = new Ext.data.DirectStore({ + baseParams: {'pk': this.ownerCt.pk, 'field': this.name}, + directFn: this.ownerCt.api.choices, + paramOrder: ['pk', 'field'], + reader: new Ext.data.JsonReader({ + successProperty: 'success', + idProperty: this.valueField, + root: 'data', + fields: [this.valueField, this.displayField] + }), + autoLoad: true + }); + + Ext.ux.ChoicesCombo.superclass.constructor.call( this ); + }; + +Ext.extend( Ext.ux.ChoicesCombo, Ext.form.ComboBox, { + + }); + +Ext.reg( 'choicescombo', Ext.ux.ChoicesCombo ); +""" + + +class FormProvider(Provider): + """ This class extends the provider class to handle Django forms. + + To export a form, register it using the ``register_form`` decorator. + + After registration, you will be able to retrieve an ExtJS form class + definition for this form under the URL ".js". Include this + script via a