Michael Ziegler
15 years ago
5 changed files with 932 additions and 0 deletions
-
21pyweb/djextdirect/__init__.py
-
190pyweb/djextdirect/client.py
-
352pyweb/djextdirect/formprovider.py
-
334pyweb/djextdirect/provider.py
-
35pyweb/djextdirect/views.py
@ -0,0 +1,21 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# kate: space-indent on; indent-width 4; replace-tabs on; |
||||
|
|
||||
|
""" |
||||
|
* Copyright (C) 2010, Michael "Svedrin" Ziegler <diese-addy@funzt-halt.net> |
||||
|
* |
||||
|
* 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 |
||||
|
|
@ -0,0 +1,190 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# kate: space-indent on; indent-width 4; replace-tabs on; |
||||
|
|
||||
|
""" |
||||
|
* Copyright (C) 2010, Michael "Svedrin" Ziegler <diese-addy@funzt-halt.net> |
||||
|
* |
||||
|
* 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 |
||||
|
<client.AccountsPrx object at 0x93d9e2c> |
||||
|
>>> cli.Mumble |
||||
|
<client.MumblePrx object at 0x93d9a2c> |
||||
|
|
||||
|
These objects provide native Python methods for each method defined in the actions: |
||||
|
|
||||
|
>>> cli.Accounts.login |
||||
|
<bound method AccountsPrx.login of <client.AccountsPrx object at 0x93d9e2c>> |
||||
|
|
||||
|
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 ) |
@ -0,0 +1,352 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# kate: space-indent on; indent-width 4; replace-tabs on; |
||||
|
|
||||
|
""" |
||||
|
* Copyright (C) 2010, Michael "Svedrin" Ziegler <diese-addy@funzt-halt.net> |
||||
|
* |
||||
|
* 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 "<formname>.js". Include this |
||||
|
script via a <script> tag just like the "api.js" for Ext.Direct. |
||||
|
|
||||
|
The form class will then be created as Ext.ux.<FormName> and will |
||||
|
have a registered xtype of "formname". |
||||
|
|
||||
|
When registering a form, the Provider will automatically generate and |
||||
|
export objects and methods for data transfer, so the form will be |
||||
|
ready to use. |
||||
|
|
||||
|
To ensure that validation error messages are displayed properly, be |
||||
|
sure to call Ext.QuickTips.init() somewhere in your code. |
||||
|
|
||||
|
In order to do extra validation, the Provider checks if your form class |
||||
|
has a method called EXT_validate, and if so, calls that method with the |
||||
|
request as parameter before calling is_valid() or save(). If EXT_validate |
||||
|
returns False, the form will not be saved and an error will be returned |
||||
|
instead. EXT_validate should update form.errors before returning False. |
||||
|
""" |
||||
|
|
||||
|
def __init__( self, name="Ext.app.REMOTING_API", autoadd=True ): |
||||
|
Provider.__init__( self, name="Ext.app.REMOTING_API", autoadd=True ) |
||||
|
self.forms = {} |
||||
|
|
||||
|
def get_choices_combo_src( self, request ): |
||||
|
return HttpResponse( EXT_DYNAMICCHOICES_COMBO, mimetype="text/javascript" ) |
||||
|
|
||||
|
def register_form( self, formclass ): |
||||
|
""" Register a Django Form class. """ |
||||
|
if not issubclass( formclass, forms.ModelForm ): |
||||
|
raise TypeError( "Ext.Direct provider can only handle ModelForms, '%s' is something else." % formclass.__name__ ) |
||||
|
|
||||
|
formname = formclass.__name__.lower() |
||||
|
self.forms[formname] = formclass |
||||
|
|
||||
|
getfunc = functools.partial( self.get_form_data, formname ) |
||||
|
getfunc.EXT_len = 1 |
||||
|
getfunc.EXT_argnames = ["pk"] |
||||
|
getfunc.EXT_flags = {} |
||||
|
|
||||
|
updatefunc = functools.partial( self.update_form_data, formname ) |
||||
|
updatefunc.EXT_len = 1 |
||||
|
updatefunc.EXT_argnames = ["pk"] |
||||
|
updatefunc.EXT_flags = { 'formHandler': True } |
||||
|
|
||||
|
choicesfunc = functools.partial( self.get_field_choices, formname ) |
||||
|
choicesfunc.EXT_len = 2 |
||||
|
choicesfunc.EXT_argnames = ["pk", "field"] |
||||
|
choicesfunc.EXT_flags = {} |
||||
|
|
||||
|
self.classes["XD_%s" % formclass.__name__] = { |
||||
|
"get": getfunc, |
||||
|
"update": updatefunc, |
||||
|
"choices": choicesfunc, |
||||
|
} |
||||
|
|
||||
|
return formclass |
||||
|
|
||||
|
def get_form( self, request, formname ): |
||||
|
""" Convert the form given in "formname" to an ExtJS FormPanel. """ |
||||
|
|
||||
|
if formname not in self.forms: |
||||
|
raise Http404(formname) |
||||
|
|
||||
|
items = [] |
||||
|
clsname = self.forms[formname].__name__ |
||||
|
hasfiles = False |
||||
|
|
||||
|
for fldname in self.forms[formname].base_fields: |
||||
|
field = self.forms[formname].base_fields[fldname] |
||||
|
extfld = { |
||||
|
"fieldLabel": field.label is not None and unicode(field.label) or fldname, |
||||
|
"name": fldname, |
||||
|
"xtype": "textfield", |
||||
|
#"allowEmpty": field.required, |
||||
|
} |
||||
|
|
||||
|
if hasattr( field, "choices" ): |
||||
|
if field.choices: |
||||
|
# Static choices dict |
||||
|
extfld.update({ |
||||
|
"name": fldname, |
||||
|
"hiddenName": fldname, |
||||
|
"xtype": "combo", |
||||
|
"store": field.choices, |
||||
|
"typeAhead": True, |
||||
|
"emptyText": 'Select...', |
||||
|
"triggerAction": 'all', |
||||
|
"selectOnFocus": True, |
||||
|
}) |
||||
|
else: |
||||
|
# choices set but empty - load them dynamically when pk is known |
||||
|
extfld.update({ |
||||
|
"name": fldname, |
||||
|
"xtype": "choicescombo", |
||||
|
"displayField": "v", |
||||
|
"valueField": "k", |
||||
|
}) |
||||
|
pass |
||||
|
elif isinstance( field, forms.BooleanField ): |
||||
|
extfld.update({ |
||||
|
"xtype": "checkbox" |
||||
|
}) |
||||
|
elif isinstance( field, forms.IntegerField ): |
||||
|
extfld.update({ |
||||
|
"xtype": "numberfield", |
||||
|
}) |
||||
|
elif isinstance( field, forms.FileField ) or isinstance( field, forms.ImageField ): |
||||
|
hasfiles = True |
||||
|
extfld.update({ |
||||
|
"xtype": "textfield", |
||||
|
"inputType": "file" |
||||
|
}) |
||||
|
elif isinstance( field.widget, forms.Textarea ): |
||||
|
extfld.update({ |
||||
|
"xtype": "textarea", |
||||
|
}) |
||||
|
elif isinstance( field.widget, forms.PasswordInput ): |
||||
|
extfld.update({ |
||||
|
"xtype": "textfield", |
||||
|
"inputType": "password" |
||||
|
}) |
||||
|
|
||||
|
items.append( extfld ) |
||||
|
|
||||
|
if field.help_text: |
||||
|
items.append({ |
||||
|
"xtype": "label", |
||||
|
"text": unicode(field.help_text), |
||||
|
"cls": "form_hint_label", |
||||
|
}) |
||||
|
|
||||
|
clscode = EXT_CLASS_TEMPLATE % { |
||||
|
'clsname': clsname, |
||||
|
'clslowername': formname, |
||||
|
'defaultconf': '{' |
||||
|
'items:' + simplejson.dumps(items, indent=4) + ',' |
||||
|
'fileUpload: ' + simplejson.dumps(hasfiles) + ',' |
||||
|
'defaults: { "anchor": "-20px" },' |
||||
|
'paramsAsHash: true,' |
||||
|
'baseParams: {},' |
||||
|
'autoScroll: true,' |
||||
|
"""buttons: [{ |
||||
|
text: "Submit", |
||||
|
handler: this.submit, |
||||
|
scope: this |
||||
|
}]""" |
||||
|
'}', |
||||
|
'apiconf': ('{' |
||||
|
'load: ' + ("XD_%s.get" % clsname) + "," |
||||
|
'submit:' + ("XD_%s.update" % clsname) + "," |
||||
|
'choices:' + ("XD_%s.choices" % clsname) + "," |
||||
|
"}"), |
||||
|
} |
||||
|
|
||||
|
return HttpResponse( mark_safe( clscode ), mimetype="text/javascript" ) |
||||
|
|
||||
|
def get_field_choices( self, formname, request, pk, field ): |
||||
|
""" Create a bound instance of the form and return choices from the given field. """ |
||||
|
formcls = self.forms[formname] |
||||
|
if pk != -1: |
||||
|
instance = formcls.Meta.model.objects.get( pk=pk ) |
||||
|
else: |
||||
|
instance = None |
||||
|
forminst = formcls( instance=instance ) |
||||
|
return { |
||||
|
'success': True, |
||||
|
'data': [ {'k': c[0], 'v': c[1]} for c in forminst.fields[field].choices ] |
||||
|
} |
||||
|
|
||||
|
def get_form_data( self, formname, request, pk ): |
||||
|
""" Called to get the current values when a form is to be displayed. """ |
||||
|
formcls = self.forms[formname] |
||||
|
if pk != -1: |
||||
|
instance = formcls.Meta.model.objects.get( pk=pk ) |
||||
|
else: |
||||
|
instance = None |
||||
|
forminst = formcls( instance=instance ) |
||||
|
|
||||
|
if hasattr( forminst, "EXT_authorize" ) and \ |
||||
|
forminst.EXT_authorize( request, "get" ) is False: |
||||
|
return { 'success': False, 'errors': {'__all__': 'access denied'} } |
||||
|
|
||||
|
data = {} |
||||
|
for fld in forminst.fields: |
||||
|
if instance: |
||||
|
data[fld] = getattr( instance, fld ) |
||||
|
else: |
||||
|
data[fld] = forminst.base_fields[fld].initial |
||||
|
return { 'data': data, 'success': True } |
||||
|
|
||||
|
def update_form_data( self, formname, request ): |
||||
|
""" Called to update the underlying model when a form has been submitted. """ |
||||
|
pk = int(request.POST['pk']) |
||||
|
formcls = self.forms[formname] |
||||
|
if pk != -1: |
||||
|
instance = formcls.Meta.model.objects.get( pk=pk ) |
||||
|
else: |
||||
|
instance = None |
||||
|
if request.POST['extUpload'] == "true": |
||||
|
forminst = formcls( request.POST, request.FILES, instance=instance ) |
||||
|
else: |
||||
|
forminst = formcls( request.POST, instance=instance ) |
||||
|
|
||||
|
if hasattr( forminst, "EXT_authorize" ) and \ |
||||
|
forminst.EXT_authorize( request, "update" ) is False: |
||||
|
return { 'success': False, 'errors': {'__all__': 'access denied'} } |
||||
|
|
||||
|
# save if either no usable validation method available or validation passes; and form.is_valid |
||||
|
if ( hasattr( forminst, "EXT_validate" ) and callable( forminst.EXT_validate ) |
||||
|
and not forminst.EXT_validate( request ) ): |
||||
|
return { 'success': False, 'errors': {'__all__': 'pre-validation failed'} } |
||||
|
|
||||
|
if forminst.is_valid(): |
||||
|
forminst.save() |
||||
|
return { 'success': True } |
||||
|
else: |
||||
|
errdict = {} |
||||
|
for errfld in forminst.errors: |
||||
|
errdict[errfld] = "\n".join( forminst.errors[errfld] ) |
||||
|
return { 'success': False, 'errors': errdict } |
||||
|
|
||||
|
def get_urls(self): |
||||
|
""" Return the URL patterns. """ |
||||
|
pat = Provider.get_urls(self) |
||||
|
if self.forms: |
||||
|
pat.append( url( r'choicescombo.js$', self.get_choices_combo_src ) ) |
||||
|
pat.append( url( r'(?P<formname>\w+).js$', self.get_form ) ) |
||||
|
return pat |
||||
|
|
||||
|
urls = property(get_urls) |
@ -0,0 +1,334 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# kate: space-indent on; indent-width 4; replace-tabs on; |
||||
|
|
||||
|
""" |
||||
|
* Copyright (C) 2010, Michael "Svedrin" Ziegler <diese-addy@funzt-halt.net> |
||||
|
* |
||||
|
* 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 inspect |
||||
|
import functools |
||||
|
import traceback |
||||
|
from sys import stderr |
||||
|
|
||||
|
from django.http import HttpResponse |
||||
|
from django.conf import settings |
||||
|
from django.conf.urls.defaults import patterns |
||||
|
from django.core.urlresolvers import reverse |
||||
|
from django.utils.datastructures import MultiValueDictKeyError |
||||
|
from django.views.decorators.csrf import csrf_exempt |
||||
|
|
||||
|
def getname( cls_or_name ): |
||||
|
""" If cls_or_name is not a string, return its __name__. """ |
||||
|
if type(cls_or_name) not in ( str, unicode ): |
||||
|
return cls_or_name.__name__ |
||||
|
return cls_or_name |
||||
|
|
||||
|
|
||||
|
class Provider( object ): |
||||
|
""" Provider for Ext.Direct. This class handles building API information and |
||||
|
routing requests to the appropriate functions, and serializing their |
||||
|
response and exceptions - if any. |
||||
|
|
||||
|
Instantiation: |
||||
|
|
||||
|
>>> EXT_JS_PROVIDER = Provider( [name="Ext.app.REMOTING_API", autoadd=True] ) |
||||
|
|
||||
|
If autoadd is True, the api.js will include a line like such:: |
||||
|
|
||||
|
Ext.Direct.addProvider( Ext.app.REMOTING_API ); |
||||
|
|
||||
|
After instantiating the Provider, register functions to it like so: |
||||
|
|
||||
|
>>> @EXT_JS_PROVIDER.register_method("myclass") |
||||
|
... def myview( request, possibly, some, other, arguments ): |
||||
|
... " does something with all those args and returns something " |
||||
|
... return 13.37 |
||||
|
|
||||
|
Note that those views **MUST NOT** return an HttpResponse but simply |
||||
|
the plain result, as the Provider will build a response from whatever |
||||
|
your view returns! |
||||
|
|
||||
|
To be able to access the Provider, include its URLs in an arbitrary |
||||
|
URL pattern, like so: |
||||
|
|
||||
|
>>> from views import EXT_JS_PROVIDER # import our provider instance |
||||
|
>>> urlpatterns = patterns( |
||||
|
... # other patterns go here |
||||
|
... ( r'api/', include(EXT_DIRECT_PROVIDER.urls) ), |
||||
|
... ) |
||||
|
|
||||
|
This way, the Provider will define the URLs "api/api.js" and "api/router". |
||||
|
|
||||
|
If you then access the "api/api.js" URL, you will get a response such as:: |
||||
|
|
||||
|
Ext.app.REMOTING_API = { # Ext.app.REMOTING_API is from Provider.name |
||||
|
"url": "/mumble/api/router", |
||||
|
"type": "remoting", |
||||
|
"actions": {"myclass": [{"name": "myview", "len": 4}]} |
||||
|
} |
||||
|
|
||||
|
You can then use this code in ExtJS to define the Provider there. |
||||
|
""" |
||||
|
|
||||
|
def __init__( self, name="Ext.app.REMOTING_API", autoadd=True ): |
||||
|
self.name = name |
||||
|
self.autoadd = autoadd |
||||
|
self.classes = {} |
||||
|
|
||||
|
def register_method( self, cls_or_name, flags=None ): |
||||
|
""" Return a function that takes a method as an argument and adds that |
||||
|
to cls_or_name. |
||||
|
|
||||
|
The flags parameter is for additional information, e.g. formHandler=True. |
||||
|
|
||||
|
Note: This decorator does not replace the method by a new function, |
||||
|
it returns the original function as-is. |
||||
|
""" |
||||
|
return functools.partial( self._register_method, cls_or_name, flags=flags ) |
||||
|
|
||||
|
def _register_method( self, cls_or_name, method, flags=None ): |
||||
|
""" Actually registers the given function as a method of cls_or_name. """ |
||||
|
clsname = getname(cls_or_name) |
||||
|
if clsname not in self.classes: |
||||
|
self.classes[clsname] = {} |
||||
|
if flags is None: |
||||
|
flags = {} |
||||
|
self.classes[ clsname ][ method.__name__ ] = method |
||||
|
method.EXT_argnames = inspect.getargspec( method ).args[1:] |
||||
|
method.EXT_len = len( method.EXT_argnames ) |
||||
|
method.EXT_flags = flags |
||||
|
return method |
||||
|
|
||||
|
@csrf_exempt |
||||
|
def get_api( self, request ): |
||||
|
""" Introspect the methods and get a JSON description of this API. """ |
||||
|
actdict = {} |
||||
|
for clsname in self.classes: |
||||
|
actdict[clsname] = [] |
||||
|
for methodname in self.classes[clsname]: |
||||
|
methinfo = { |
||||
|
"name": methodname, |
||||
|
"len": self.classes[clsname][methodname].EXT_len |
||||
|
} |
||||
|
methinfo.update( self.classes[clsname][methodname].EXT_flags ) |
||||
|
actdict[clsname].append( methinfo ) |
||||
|
|
||||
|
lines = ["%s = %s;" % ( self.name, simplejson.dumps({ |
||||
|
"url": reverse( self.request ), |
||||
|
"type": "remoting", |
||||
|
"actions": actdict |
||||
|
}))] |
||||
|
|
||||
|
if self.autoadd: |
||||
|
lines.append( "Ext.Direct.addProvider( %s );" % self.name ) |
||||
|
|
||||
|
return HttpResponse( "\n".join( lines ), mimetype="text/javascript" ) |
||||
|
|
||||
|
@csrf_exempt |
||||
|
def request( self, request ): |
||||
|
""" Implements the Router part of the Ext.Direct specification. |
||||
|
|
||||
|
It handles decoding requests, calling the appropriate function (if |
||||
|
found) and encoding the response / exceptions. |
||||
|
""" |
||||
|
# First try to use request.POST, if that doesn't work check for req.raw_post_data. |
||||
|
# The other way round this might make more sense because the case that uses |
||||
|
# raw_post_data is way more common, but accessing request.POST after raw_post_data |
||||
|
# causes issues with Django's test client while accessing raw_post_data after |
||||
|
# request.POST does not. |
||||
|
try: |
||||
|
jsoninfo = { |
||||
|
'action': request.POST['extAction'], |
||||
|
'method': request.POST['extMethod'], |
||||
|
'type': request.POST['extType'], |
||||
|
'upload': request.POST['extUpload'], |
||||
|
'tid': request.POST['extTID'], |
||||
|
} |
||||
|
except (MultiValueDictKeyError, KeyError), err: |
||||
|
try: |
||||
|
rawjson = simplejson.loads( request.raw_post_data ) |
||||
|
except simplejson.JSONDecodeError: |
||||
|
return HttpResponse( simplejson.dumps({ |
||||
|
'type': 'exception', |
||||
|
'message': 'malformed request', |
||||
|
'where': unicode(err), |
||||
|
"tid": None, # dunno |
||||
|
}), mimetype="text/javascript" ) |
||||
|
else: |
||||
|
return self.process_normal_request( request, rawjson ) |
||||
|
else: |
||||
|
return self.process_form_request( request, jsoninfo ) |
||||
|
|
||||
|
def process_normal_request( self, request, rawjson ): |
||||
|
""" Process standard requests (no form submission or file uploads). """ |
||||
|
if not isinstance( rawjson, list ): |
||||
|
rawjson = [rawjson] |
||||
|
|
||||
|
responses = [] |
||||
|
|
||||
|
for reqinfo in rawjson: |
||||
|
cls, methname, data, rtype, tid = (reqinfo['action'], |
||||
|
reqinfo['method'], |
||||
|
reqinfo['data'], |
||||
|
reqinfo['type'], |
||||
|
reqinfo['tid']) |
||||
|
|
||||
|
if cls not in self.classes: |
||||
|
responses.append({ |
||||
|
'type': 'exception', |
||||
|
'message': 'no such action', |
||||
|
'where': cls, |
||||
|
"tid": tid, |
||||
|
}) |
||||
|
continue |
||||
|
|
||||
|
if methname not in self.classes[cls]: |
||||
|
responses.append({ |
||||
|
'type': 'exception', |
||||
|
'message': 'no such method', |
||||
|
'where': methname, |
||||
|
"tid": tid, |
||||
|
}) |
||||
|
continue |
||||
|
|
||||
|
func = self.classes[cls][methname] |
||||
|
|
||||
|
if func.EXT_len and len(data) == 1 and type(data[0]) == dict: |
||||
|
# data[0] seems to contain a dict with params. check if it does, and if so, unpack |
||||
|
args = [] |
||||
|
for argname in func.EXT_argnames: |
||||
|
if argname in data[0]: |
||||
|
args.append( data[0][argname] ) |
||||
|
else: |
||||
|
args = None |
||||
|
break |
||||
|
if args: |
||||
|
data = args |
||||
|
|
||||
|
if data is not None: |
||||
|
datalen = len(data) |
||||
|
else: |
||||
|
datalen = 0 |
||||
|
|
||||
|
if datalen != len(func.EXT_argnames): |
||||
|
responses.append({ |
||||
|
'type': 'exception', |
||||
|
'tid': tid, |
||||
|
'message': 'invalid arguments', |
||||
|
'where': 'Expected %d, got %d' % ( len(func.EXT_argnames), len(data) ) |
||||
|
}) |
||||
|
continue |
||||
|
|
||||
|
try: |
||||
|
if data: |
||||
|
result = func( request, *data ) |
||||
|
else: |
||||
|
result = func( request ) |
||||
|
|
||||
|
except Exception, err: |
||||
|
errinfo = { |
||||
|
'type': 'exception', |
||||
|
"tid": tid, |
||||
|
} |
||||
|
if settings.DEBUG: |
||||
|
traceback.print_exc( file=stderr ) |
||||
|
errinfo['message'] = unicode(err) |
||||
|
errinfo['where'] = traceback.format_exc() |
||||
|
else: |
||||
|
errinfo['message'] = 'The socket packet pocket has an error to report.' |
||||
|
errinfo['where'] = '' |
||||
|
responses.append(errinfo) |
||||
|
|
||||
|
else: |
||||
|
responses.append({ |
||||
|
"type": rtype, |
||||
|
"tid": tid, |
||||
|
"action": cls, |
||||
|
"method": methname, |
||||
|
"result": result |
||||
|
}) |
||||
|
|
||||
|
if len(responses) == 1: |
||||
|
return HttpResponse( simplejson.dumps( responses[0] ), mimetype="text/javascript" ) |
||||
|
else: |
||||
|
return HttpResponse( simplejson.dumps( responses ), mimetype="text/javascript" ) |
||||
|
|
||||
|
def process_form_request( self, request, reqinfo ): |
||||
|
""" Router for POST requests that submit form data and/or file uploads. """ |
||||
|
cls, methname, rtype, tid = (reqinfo['action'], |
||||
|
reqinfo['method'], |
||||
|
reqinfo['type'], |
||||
|
reqinfo['tid']) |
||||
|
|
||||
|
if cls not in self.classes: |
||||
|
response = { |
||||
|
'type': 'exception', |
||||
|
'message': 'no such action', |
||||
|
'where': cls, |
||||
|
"tid": tid, |
||||
|
} |
||||
|
|
||||
|
elif methname not in self.classes[cls]: |
||||
|
response = { |
||||
|
'type': 'exception', |
||||
|
'message': 'no such method', |
||||
|
'where': methname, |
||||
|
"tid": tid, |
||||
|
} |
||||
|
|
||||
|
else: |
||||
|
func = self.classes[cls][methname] |
||||
|
try: |
||||
|
result = func( request ) |
||||
|
|
||||
|
except Exception, err: |
||||
|
errinfo = { |
||||
|
'type': 'exception', |
||||
|
"tid": tid, |
||||
|
} |
||||
|
if settings.DEBUG: |
||||
|
traceback.print_exc( file=stderr ) |
||||
|
errinfo['message'] = unicode(err) |
||||
|
errinfo['where'] = traceback.format_exc() |
||||
|
else: |
||||
|
errinfo['message'] = 'The socket packet pocket has an error to report.' |
||||
|
errinfo['where'] = '' |
||||
|
response = errinfo |
||||
|
|
||||
|
else: |
||||
|
response = { |
||||
|
"type": rtype, |
||||
|
"tid": tid, |
||||
|
"action": cls, |
||||
|
"method": methname, |
||||
|
"result": result |
||||
|
} |
||||
|
|
||||
|
if reqinfo['upload'] == "true": |
||||
|
return HttpResponse( |
||||
|
"<html><body><textarea>%s</textarea></body></html>" % simplejson.dumps(response), |
||||
|
mimetype="text/javascript" |
||||
|
) |
||||
|
else: |
||||
|
return HttpResponse( simplejson.dumps( response ), mimetype="text/javascript" ) |
||||
|
|
||||
|
def get_urls(self): |
||||
|
""" Return the URL patterns. """ |
||||
|
pat = patterns('', |
||||
|
(r'api.js$', self.get_api ), |
||||
|
(r'router/?', self.request ), |
||||
|
) |
||||
|
return pat |
||||
|
|
||||
|
urls = property(get_urls) |
@ -0,0 +1,35 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# kate: space-indent on; indent-width 4; replace-tabs on; |
||||
|
|
||||
|
""" |
||||
|
* Copyright (C) 2010, Michael "Svedrin" Ziegler <diese-addy@funzt-halt.net> |
||||
|
* |
||||
|
* 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. |
||||
|
""" |
||||
|
|
||||
|
def login( request, username, passwd ): |
||||
|
from django.contrib.auth import authenticate, login as djlogin |
||||
|
if request.user.is_authenticated(): |
||||
|
return { 'success': True } |
||||
|
user = authenticate( username=username, password=passwd ) |
||||
|
if user: |
||||
|
if user.is_active: |
||||
|
djlogin( request, user ) |
||||
|
return { 'success': True } |
||||
|
else: |
||||
|
return { 'success': False, 'error': 'account disabled' } |
||||
|
else: |
||||
|
return { 'success': False, 'error': 'invalid credentials' } |
||||
|
|
||||
|
def logout( request ): |
||||
|
from django.contrib.auth import logout as djlogout |
||||
|
djlogout( request ) |
||||
|
return { 'success': True } |
Write
Preview
Loading…
Cancel
Save
Reference in new issue