1 from django.db.models import Q
2 from django.http import HttpResponse
3 from django.utils import simplejson as json
4 from django.utils.encoding import smart_str
5 from django.views.debug import ExceptionReporter
6 from inspect import isclass, ismethod, isfunction, getmembers, getargspec
7 from traceback import format_tb
8 from abc import ABCMeta, abstractproperty
9 from collections import Callable, Sized, Mapping
13 # __all__ = ('ext_action', 'ext_method', 'is_ext_action', 'is_ext_method', 'ExtAction', 'ExtMethod')
16 class ExtRequest(object):
18 Represents a single Ext.Direct request along with the :class:`django.http.HttpRequest` it originates from.
22 Passes undefined attribute accesses through to the underlying :class:`django.http.HttpRequest`.
27 def parse(cls, request, object_hook=None):
29 Parses Ext.Direct request(s) from the originating HTTP request.
31 :arg request: the originating HTTP request
32 :type request: :class:`django.http.HttpRequest`
33 :returns: list of :class:`ExtRequest` instances
39 if request.META['CONTENT_TYPE'].startswith('application/x-www-form-urlencoded') or request.META['CONTENT_TYPE'].startswith('multipart/form-data'):
40 requests.append(cls(request,
41 type = request.POST.get('extType'),
42 tid = request.POST.get('extTID'),
43 action = request.POST.get('extAction'),
44 method = request.POST.get('extMethod'),
45 data = request.POST.get('extData', None),
46 upload = True if request.POST.get('extUpload', False) in (True, 'true', 'True') else False,
50 decoded_requests = json.loads(request.raw_post_data, object_hook=object_hook)
51 if type(decoded_requests) is dict:
52 decoded_requests = [decoded_requests]
53 for inner_request in decoded_requests:
54 requests.append(cls(request,
55 type = inner_request.get('type'),
56 tid = inner_request.get('tid'),
57 action = inner_request.get('action'),
58 method = inner_request.get('method'),
59 data = inner_request.get('data', None),
64 def __init__(self, request, type, tid, action, method, data, upload=False, form_request=False):
66 :arg request: the originating HTTP request
67 :type request: :class:`django.http.HttpRequest`
68 :arg type: Ext.Direct request type
70 :arg tid: Ext.Direct transaction identifier
72 :arg action: Ext.Direct action name
74 :arg method: Ext.Direct method name
76 :arg data: Ext.Direct method arguments
78 :arg upload: request includes uploaded file(s)
80 :arg form_request: request made by form submission
81 :type form_request: bool
87 self.request = request
90 self.data = data if data is not None else []
92 self.form_request = form_request
94 def __getattr__(self, key):
96 return getattr(self.request, key)
101 class ExtMethod(Callable, Sized):
103 Wraps a (previously :meth:`decorated <ExtMethod.decorate>`) function as an Ext.Direct method.
108 def decorate(cls, function=None, name=None, form_handler=False):
110 Applies metadata to function identifying it as wrappable, or returns a decorator for doing the same::
113 def method(self, request):
116 @ExtMethod.decorate(name='custom_name', form_handler=True)
117 def form_handler_with_custom_name(self, request):
120 Intended for use on methods of classes already decorated by :meth:`ExtAction.decorate`.
122 :arg name: custom Ext.Direct method name
124 :arg form_handler: function handles form submissions
125 :type form_handler: bool
126 :returns: function with metadata applied
130 def setter(function):
131 setattr(function, '_ext_method', True)
132 setattr(function, '_ext_method_form_handler', form_handler)
134 setattr(function, '_ext_method_name', name)
136 if function is not None:
137 return setter(function)
141 def validate(cls, function):
143 Validates that function has been :meth:`decorated <ExtMethod.decorate>` and is therefore wrappable.
147 return getattr(function, '_ext_method', False)
149 def __init__(self, function):
151 :arg function: function to wrap
152 :type function: callable
154 If the function accepts variable positional arguments, the Ext.Direct method argument count will be increased by one for acceptance of a list.
156 Similarly, if the function accepts variable keyword arguments, the argument count will be increased by one for acceptance of a dictionary.
160 Wrapped functions **must** accept at least one positional argument (in addition to self if function is a method): the :class:`ExtRequest` that caused the invocation.
164 Wrapped functions identified as handling form submissions **must** return a tuple containing:
166 * a boolean indicating success or failure
167 * a dictionary of fields mapped to errors, if any, or None
170 self.function = function
171 self.form_handler = getattr(function, '_ext_method_form_handler', False)
172 self.name = getattr(function, '_ext_method_name', function.__name__)
174 argspec = getargspec(self.function)
175 len_ = len(argspec.args)
177 if len_ >= 2 and ismethod(self.function):
179 elif len_ >= 1 and not ismethod(self.function):
182 raise TypeError('%s cannot be wrapped as an Ext.Direct method as it does not take an ExtRequest as its first positional argument')
184 if argspec.varargs is not None:
186 self.accepts_varargs = True
188 self.accepts_varargs = False
190 if argspec.keywords is not None:
192 self.accepts_keywords = True
194 self.accepts_keywords = False
203 'formHandler': self.form_handler
209 def __call__(self, request):
211 Invoke the wrapped function using the provided :class:`ExtRequest` and return the raw result.
213 :arg request: the :class:`ExtRequest`
215 :raises TypeError: the request did not provide the required number of arguments
216 :raises Exception: the (form handling) function did not return a valid result for a form submission request
223 if args_len != self.len:
224 raise TypeError('%s takes exactly %i arguments (%i given)' % (self.name, self.len, args_len))
227 if self.accepts_keywords:
228 keywords = dict([(smart_str(k, 'ascii'), v) for k,v in args.pop().items()])
231 if self.accepts_varargs:
234 result = self.function(request, *(args + varargs), **keywords)
236 if self.form_handler:
239 'success': result[0],
243 new_result['pk'] = result[2]
245 if new_result['success']:
246 del new_result['errors']
250 raise Exception # pick a better one
255 ext_method = ExtMethod.decorate
257 Convenience alias for :meth:`ExtMethod.decorate`.
262 is_ext_method = ExtMethod.validate
264 Convenience alias for :meth:`ExtMethod.validate`.
269 class ExtAction(Callable, Mapping):
271 Wraps a (previously :meth:`decorated <ExtAction.decorate>`) object as an Ext.Direct action.
275 method_class = ExtMethod
277 The :class:`ExtMethod` subclass used when wrapping the wrapped object's members.
282 def decorate(cls, obj=None, name=None):
284 Applies metadata to obj identifying it as wrappable, or returns a decorator for doing the same::
287 class MyAction(object):
290 @ExtAction.decorate(name='GoodAction')
291 class BadAction(object):
294 Intended for use on classes with member functions (methods) already decorated with :meth:`ExtMethod.decorate`.
296 :arg name: custom Ext.Direct action name
298 :returns: obj with metadata applied
303 setattr(obj, '_ext_action', True)
305 setattr(obj, '_ext_action_name', name)
312 def validate(cls, obj):
314 Validates that obj has been :meth:`decorated <ExtAction.decorate>` and is therefore wrappable.
318 return getattr(obj, '_ext_action', False)
320 def __init__(self, obj):
322 self.name = getattr(obj, '_ext_action_name', obj.__name__ if isclass(obj) else obj.__class__.__name__)
327 if not self._methods:
328 self._methods = dict((method.name, method) for method in (self.method_class(member) for name, member in getmembers(self.obj, self.method_class.validate)))
332 return len(self.methods)
335 return iter(self.methods)
337 def __getitem__(self, name):
338 return self.methods[name]
343 Returns a tuple containing:
346 * a list of :class:`method specs <ExtMethod.spec>`
348 Used internally by :class:`providers <ExtProvider>` to construct an Ext.Direct provider spec.
352 return self.name, list(method.spec for method in self.itervalues())
354 def __call__(self, request):
355 return self[request.method](request)
358 ext_action = ExtAction.decorate
360 Convenience alias for :meth:`ExtAction.decorate`.
365 is_ext_action = ExtAction.validate
367 Convenience alias for :meth:`ExtAction.validate`.
372 class ExtResponse(object):
374 Abstract base class for responses to :class:`requests <ExtRequest>`.
377 __metaclass__ = ABCMeta
381 raise NotImplementedError
384 class ExtResult(ExtResponse):
386 Represents a successful response to a :class:`request <ExtRequest>`.
390 def __init__(self, request, result):
392 :arg request: the originating Ext.Direct request
393 :type request: :class:`ExtRequest`
394 :arg result: the raw result
397 self.request = request
403 'type': self.request.type,
404 'tid': self.request.tid,
405 'action': self.request.action,
406 'method': self.request.method,
407 'result': self.result
411 class ExtException(ExtResponse):
413 Represents an exception raised by an unsuccessful response to a :class:`request <ExtRequest>`.
417 If :data:`django.conf.settings.DEBUG` is True, information about which exception was raised and where it was raised (including a traceback in both plain text and HTML) will be provided to the client.
421 def __init__(self, request, exc_info):
425 self.request = request
426 self.exc_info = exc_info
430 from django.conf import settings
432 reporter = ExceptionReporter(self.request.request, *self.exc_info)
435 'tid': self.request.tid,
436 'message': '%s: %s' % (self.exc_info[0], self.exc_info[1]),
437 'where': format_tb(self.exc_info[2])[0],
438 'identifier': '%s.%s' % (self.exc_info[0].__module__, self.exc_info[0].__name__),
439 'html': reporter.get_traceback_html()
444 'tid': self.request.tid
448 class ExtProvider(Callable, Mapping):
450 Abstract base class for Ext.Direct provider implementations.
454 __metaclass__ = ABCMeta
456 result_class = ExtResult
458 The :class:`ExtResponse` subclass used to represent the results of a successful Ext.Direct method invocation.
462 exception_class = ExtException
464 The :class:`ExtResponse` subclass used to represent the exception raised during an unsuccessful Ext.Direct method invocation.
471 The Ext.Direct provider namespace.
474 raise NotImplementedError
479 The Ext.Direct provider url.
482 raise NotImplementedError
487 The Ext.Direct provider type.
490 raise NotImplementedError
495 The dictionary of action names and :class:`ExtAction` instances handled by the provider.
498 raise NotImplementedError
501 return len(self.actions)
504 return iter(self.actions)
506 def __getitem__(self, name):
507 return self.actions[name]
512 'namespace': self.namespace,
515 'actions': dict(action.spec for action in self.itervalues())
518 def __call__(self, request):
520 Returns a :class:`response <ExtResponse>` to the :class:`request <ExtRequest>`.
524 return self.result_class(request=request, result=self[request.action](request))
526 return self.exception_class(request=request, exc_info=sys.exc_info())
529 class ExtRouter(ExtProvider):
531 A :class:`provider <ExtProvider>` base class with an implementation capable of handling the complete round-trip from a :class:`django.http.HttpRequest` to a :class:`django.http.HttpResponse`.
535 __metaclass__ = ABCMeta
537 request_class = ExtRequest
539 The :class:`ExtRequest` subclass used parse and to represent the individual Ext.Direct requests within a :class:`django.http.HttpRequest`.
544 def json_object_hook(cls, obj):
545 if obj.get('q_object', False):
546 return Q._new_instance(obj['children'], obj['connector'], obj['negated'])
550 def json_default(cls, obj):
551 from django.forms.models import ModelChoiceIterator
552 from django.db.models.query import ValuesListQuerySet
553 from django.utils.functional import Promise
555 if isinstance(obj, ExtResponse):
557 elif isinstance(obj, datetime.datetime):
558 obj = obj.replace(microsecond=0)
559 return obj.isoformat(' ')
560 elif isinstance(obj, ModelChoiceIterator) or isinstance(obj, ValuesListQuerySet):
562 elif isinstance(obj, Promise):
564 elif isinstance(obj, Q):
567 'connector': obj.connector,
568 'negated': obj.negated,
569 'children': obj.children
572 raise TypeError, 'Object of type %s with value of %s is not JSON serializable' % (type(obj), repr(obj))
574 def render_to_response(self, request):
576 Returns a :class:`django.http.HttpResponse` containing the :class:`response(s) <ExtResponse>` to the :class:`request(s) <ExtRequest>` in the provided :class:`django.http.HttpRequest`.
580 from django.conf import settings
582 requests = self.request_class.parse(request, object_hook=self.json_object_hook)
584 html_response = False
586 for request in requests:
587 if request.form_request and request.upload:
589 responses.append(self(request))
591 response = responses[0] if len(responses) == 1 else responses
592 json_response = json.dumps(responses, default=self.json_default)
595 return HttpResponse('<html><body><textarea>%s</textarea></body></html>' % json_response)
596 return HttpResponse(json_response, content_type='application/json; charset=%s' % settings.DEFAULT_CHARSET)
599 class SimpleExtRouter(ExtRouter):
601 A simple concrete :class:`router <ExtRouter>` implementation.
605 def __init__(self, namespace, url, actions=None, type='remoting'):
607 :arg namespace: the Ext.Direct provider namespace
609 :arg url: the Ext.Direct provider url
611 :arg actions: the dictionary of action names and :class:`ExtAction` instances handled by the provider
613 :arg type: the Ext.Direct provider type
619 self._namespace = namespace
621 self._actions = actions if actions is not None else {}
625 return self._namespace