X-Git-Url: http://git.ithinksw.org/philo.git/blobdiff_plain/834e27b62ba09014f91feabe89da08adb472ce9e..f314a8ddceb543e2bb4711d50bdd2060452689b1:/contrib/gilbert/extdirect/core.py diff --git a/contrib/gilbert/extdirect/core.py b/contrib/gilbert/extdirect/core.py new file mode 100644 index 0000000..42c24c0 --- /dev/null +++ b/contrib/gilbert/extdirect/core.py @@ -0,0 +1,637 @@ +import sys +import datetime +from inspect import isclass, ismethod, isfunction, getmembers, getargspec +from traceback import format_tb +from abc import ABCMeta, abstractproperty +from collections import Callable, Sized, Mapping +from django.utils import simplejson as json +from django.views.debug import ExceptionReporter +from django.http import HttpResponse +from django.db.models import Q + + +# __all__ = ('ext_action', 'ext_method', 'is_ext_action', 'is_ext_method', 'ExtAction', 'ExtMethod') + + +class ExtRequest(object): + """ + Represents a single Ext.Direct request along with the :class:`django.http.HttpRequest` it originates from. + + .. note:: + + Passes undefined attribute accesses through to the underlying :class:`django.http.HttpRequest`. + + """ + + @classmethod + def parse(cls, request, object_hook=None): + """ + Parses Ext.Direct request(s) from the originating HTTP request. + + :arg request: the originating HTTP request + :type request: :class:`django.http.HttpRequest` + :returns: list of :class:`ExtRequest` instances + + """ + + requests = [] + + if request.META['CONTENT_TYPE'].startswith('application/x-www-form-urlencoded') or request.META['CONTENT_TYPE'].startswith('multipart/form-data'): + requests.append(cls(request, + type = request.POST.get('extType'), + tid = request.POST.get('extTID'), + action = request.POST.get('extAction'), + method = request.POST.get('extMethod'), + data = request.POST.get('extData', None), + upload = True if request.POST.get('extUpload', False) in (True, 'true', 'True') else False, + form_request = True, + )) + else: + decoded_requests = json.loads(request.raw_post_data, object_hook=object_hook) + if type(decoded_requests) is dict: + decoded_requests = [decoded_requests] + for inner_request in decoded_requests: + requests.append(cls(request, + type = inner_request.get('type'), + tid = inner_request.get('tid'), + action = inner_request.get('action'), + method = inner_request.get('method'), + data = inner_request.get('data', None), + )) + + return requests + + def __init__(self, request, type, tid, action, method, data, upload=False, form_request=False): + """ + :arg request: the originating HTTP request + :type request: :class:`django.http.HttpRequest` + :arg type: Ext.Direct request type + :type type: str + :arg tid: Ext.Direct transaction identifier + :type tid: str + :arg action: Ext.Direct action name + :type action: str + :arg method: Ext.Direct method name + :type method: str + :arg data: Ext.Direct method arguments + :type data: list + :arg upload: request includes uploaded file(s) + :type upload: bool + :arg form_request: request made by form submission + :type form_request: bool + + """ + + self.type = type + self.tid = tid + self.request = request + self.action = action + self.method = method + self.data = data if data is not None else [] + self.upload = upload + self.form_request = form_request + + def __getattr__(self, key): + try: + return getattr(self.request, key) + except: + raise AttributeError + + +class ExtMethod(Callable, Sized): + """ + Wraps a (previously :meth:`decorated `) function as an Ext.Direct method. + + """ + + @classmethod + def decorate(cls, function=None, name=None, form_handler=False): + """ + Applies metadata to function identifying it as wrappable, or returns a decorator for doing the same:: + + @ExtMethod.decorate + def method(self, request): + pass + + @ExtMethod.decorate(name='custom_name', form_handler=True) + def form_handler_with_custom_name(self, request): + pass + + Intended for use on methods of classes already decorated by :meth:`ExtAction.decorate`. + + :arg name: custom Ext.Direct method name + :type name: str + :arg form_handler: function handles form submissions + :type form_handler: bool + :returns: function with metadata applied + + """ + + def setter(function): + setattr(function, '_ext_method', True) + setattr(function, '_ext_method_form_handler', form_handler) + if name is not None: + setattr(function, '_ext_method_name', name) + return function + if function is not None: + return setter(function) + return setter + + @classmethod + def validate(cls, function): + """ + Validates that function has been :meth:`decorated ` and is therefore wrappable. + + """ + + return getattr(function, '_ext_method', False) + + def __init__(self, function): + """ + :arg function: function to wrap + :type function: callable + + If the function accepts variable positional arguments, the Ext.Direct method argument count will be increased by one for acceptance of a list. + + Similarly, if the function accepts variable keyword arguments, the argument count will be increased by one for acceptance of a dictionary. + + .. warning:: + + 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. + + .. warning:: + + Wrapped functions identified as handling form submissions **must** return a tuple containing: + + * a boolean indicating success or failure + * a dictionary of fields mapped to errors, if any, or None + + """ + self.function = function + self.form_handler = getattr(function, '_ext_method_form_handler', False) + self.name = getattr(function, '_ext_method_name', function.__name__) + + argspec = getargspec(self.function) + len_ = len(argspec.args) + + if len_ >= 2 and ismethod(self.function): + len_ -= 2 + elif len_ >= 1 and not ismethod(self.function): + len_ -= 1 + else: + raise TypeError('%s cannot be wrapped as an Ext.Direct method as it does not take an ExtRequest as its first positional argument') + + if argspec.varargs is not None: + len_ += 1 + self.accepts_varargs = True + else: + self.accepts_varargs = False + + if argspec.keywords is not None: + len_ += 1 + self.accepts_keywords = True + else: + self.accepts_keywords = False + + self.len = len_ + + @property + def spec(self): + return { + 'name': self.name, + 'len': self.len, + 'formHandler': self.form_handler + } + + def __len__(self): + return self.len + + def __call__(self, request): + """ + Invoke the wrapped function using the provided :class:`ExtRequest` and return the raw result. + + :arg request: the :class:`ExtRequest` + + :raises TypeError: the request did not provide the required number of arguments + :raises Exception: the (form handling) function did not return a valid result for a form submission request + + """ + + args = request.data + args_len = len(args) + + if args_len != self.len: + raise TypeError('%s takes exactly %i arguments (%i given)' % (self.name, self.len, args_len)) + + keywords = {} + if self.accepts_keywords: + keywords = args.pop() + + varargs = [] + if self.accepts_varargs: + varargs = args.pop() + + result = self.function(request, *(args + varargs), **keywords) + + if self.form_handler: + try: + new_result = { + 'success': result[0], + 'errors': result[1], + } + if len(result) > 2: + new_result['pk'] = result[2] + + if new_result['success']: + del new_result['errors'] + + result = new_result + except: + raise Exception # pick a better one + + return result + + +ext_method = ExtMethod.decorate +""" +Convenience alias for :meth:`ExtMethod.decorate`. + +""" + + +is_ext_method = ExtMethod.validate +""" +Convenience alias for :meth:`ExtMethod.validate`. + +""" + + +class ExtAction(Callable, Mapping): + """ + Wraps a (previously :meth:`decorated `) object as an Ext.Direct action. + + """ + + method_class = ExtMethod + """ + The :class:`ExtMethod` subclass used when wrapping the wrapped object's members. + + """ + + @classmethod + def decorate(cls, obj=None, name=None): + """ + Applies metadata to obj identifying it as wrappable, or returns a decorator for doing the same:: + + @ExtAction.decorate + class MyAction(object): + pass + + @ExtAction.decorate(name='GoodAction') + class BadAction(object): + pass + + Intended for use on classes with member functions (methods) already decorated with :meth:`ExtMethod.decorate`. + + :arg name: custom Ext.Direct action name + :type name: str + :returns: obj with metadata applied + + """ + + def setter(obj): + setattr(obj, '_ext_action', True) + if name is not None: + setattr(obj, '_ext_action_name', name) + return obj + if obj is not None: + return setter(obj) + return setter + + @classmethod + def validate(cls, obj): + """ + Validates that obj has been :meth:`decorated ` and is therefore wrappable. + + """ + + return getattr(obj, '_ext_action', False) + + def __init__(self, obj): + self.obj = obj + self.name = getattr(obj, '_ext_action_name', obj.__name__ if isclass(obj) else obj.__class__.__name__) + self._methods = None + + @property + def methods(self): + if not self._methods: + self._methods = dict((method.name, method) for method in (self.method_class(member) for name, member in getmembers(self.obj, self.method_class.validate))) + return self._methods + + def __len__(self): + return len(self.methods) + + def __iter__(self): + return iter(self.methods) + + def __getitem__(self, name): + return self.methods[name] + + @property + def spec(self): + """ + Returns a tuple containing: + + * the action name + * a list of :class:`method specs ` + + Used internally by :class:`providers ` to construct an Ext.Direct provider spec. + + """ + + return self.name, list(method.spec for method in self.itervalues()) + + def __call__(self, request): + return self[request.method](request) + + +ext_action = ExtAction.decorate +""" +Convenience alias for :meth:`ExtAction.decorate`. + +""" + + +is_ext_action = ExtAction.validate +""" +Convenience alias for :meth:`ExtAction.validate`. + +""" + + +class ExtResponse(object): + """ + Abstract base class for responses to :class:`requests `. + + """ + __metaclass__ = ABCMeta + + @abstractproperty + def as_ext(self): + raise NotImplementedError + + +class ExtResult(ExtResponse): + """ + Represents a successful response to a :class:`request `. + + """ + + def __init__(self, request, result): + """ + :arg request: the originating Ext.Direct request + :type request: :class:`ExtRequest` + :arg result: the raw result + + """ + self.request = request + self.result = result + + @property + def as_ext(self): + return { + 'type': self.request.type, + 'tid': self.request.tid, + 'action': self.request.action, + 'method': self.request.method, + 'result': self.result + } + + +class ExtException(ExtResponse): + """ + Represents an exception raised by an unsuccessful response to a :class:`request `. + + .. warning:: + + 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. + + """ + + def __init__(self, request, exc_info): + """ + + """ + self.request = request + self.exc_info = exc_info + + @property + def as_ext(self): + from django.conf import settings + if settings.DEBUG: + reporter = ExceptionReporter(self.request.request, *self.exc_info) + return { + 'type': 'exception', + 'tid': self.request.tid, + 'message': '%s: %s' % (self.exc_info[0], self.exc_info[1]), + 'where': format_tb(self.exc_info[2])[0], + 'identifier': '%s.%s' % (self.exc_info[0].__module__, self.exc_info[0].__name__), + 'html': reporter.get_traceback_html() + } + else: + return { + 'type': 'exception', + 'tid': self.request.tid + } + + +class ExtProvider(Callable, Mapping): + """ + Abstract base class for Ext.Direct provider implementations. + + """ + + __metaclass__ = ABCMeta + + result_class = ExtResult + """ + The :class:`ExtResponse` subclass used to represent the results of a successful Ext.Direct method invocation. + + """ + + exception_class = ExtException + """ + The :class:`ExtResponse` subclass used to represent the exception raised during an unsuccessful Ext.Direct method invocation. + + """ + + @abstractproperty + def namespace(self): + """ + The Ext.Direct provider namespace. + + """ + raise NotImplementedError + + @abstractproperty + def url(self): + """ + The Ext.Direct provider url. + + """ + raise NotImplementedError + + @abstractproperty + def type(self): + """ + The Ext.Direct provider type. + + """ + raise NotImplementedError + + @abstractproperty + def actions(self): + """ + The dictionary of action names and :class:`ExtAction` instances handled by the provider. + + """ + raise NotImplementedError + + def __len__(self): + return len(self.actions) + + def __iter__(self): + return iter(self.actions) + + def __getitem__(self, name): + return self.actions[name] + + @property + def spec(self): + return { + 'namespace': self.namespace, + 'url': self.url, + 'type': self.type, + 'actions': dict(action.spec for action in self.itervalues()) + } + + def __call__(self, request): + """ + Returns a :class:`response ` to the :class:`request `. + + """ + try: + return self.result_class(request=request, result=self[request.action](request)) + except Exception: + return self.exception_class(request=request, exc_info=sys.exc_info()) + + +class ExtRouter(ExtProvider): + """ + A :class:`provider ` base class with an implementation capable of handling the complete round-trip from a :class:`django.http.HttpRequest` to a :class:`django.http.HttpResponse`. + + """ + + __metaclass__ = ABCMeta + + request_class = ExtRequest + """ + The :class:`ExtRequest` subclass used parse and to represent the individual Ext.Direct requests within a :class:`django.http.HttpRequest`. + + """ + + @classmethod + def json_object_hook(cls, obj): + if obj.get('q_object', False): + return Q._new_instance(obj['children'], obj['connector'], obj['negated']) + return obj + + @classmethod + def json_default(cls, obj): + from django.forms.models import ModelChoiceIterator + from django.db.models.query import ValuesListQuerySet + from django.utils.functional import Promise + + if isinstance(obj, ExtResponse): + return obj.as_ext + elif isinstance(obj, datetime.datetime): + obj = obj.replace(microsecond=0) + return obj.isoformat(' ') + elif isinstance(obj, ModelChoiceIterator) or isinstance(obj, ValuesListQuerySet): + return list(obj) + elif isinstance(obj, Promise): + return unicode(obj) + elif isinstance(obj, Q): + return { + 'q_object': True, + 'connector': obj.connector, + 'negated': obj.negated, + 'children': obj.children + } + else: + raise TypeError, 'Object of type %s with value of %s is not JSON serializable' % (type(obj), repr(obj)) + + def render_to_response(self, request): + """ + Returns a :class:`django.http.HttpResponse` containing the :class:`response(s) ` to the :class:`request(s) ` in the provided :class:`django.http.HttpRequest`. + + """ + + from django.conf import settings + + requests = self.request_class.parse(request, object_hook=self.json_object_hook) + responses = [] + html_response = False + + for request in requests: + if request.form_request and request.upload: + html_response = True + responses.append(self(request)) + + response = responses[0] if len(responses) == 1 else responses + json_response = json.dumps(responses, default=self.json_default) + + if html_response: + return HttpResponse('' % json_response) + return HttpResponse(json_response, content_type='application/json; charset=%s' % settings.DEFAULT_CHARSET) + + +class SimpleExtRouter(ExtRouter): + """ + A simple concrete :class:`router ` implementation. + + """ + + def __init__(self, namespace, url, actions=None, type='remoting'): + """ + :arg namespace: the Ext.Direct provider namespace + :type namespace: str + :arg url: the Ext.Direct provider url + :type url: str + :arg actions: the dictionary of action names and :class:`ExtAction` instances handled by the provider + :type actions: dict + :arg type: the Ext.Direct provider type + :type type: str + + """ + + self._type = type + self._namespace = namespace + self._url = url + self._actions = actions if actions is not None else {} + + @property + def namespace(self): + return self._namespace + + @property + def url(self): + return self._url + + @property + def type(self): + return self._type + + @property + def actions(self): + return self._actions \ No newline at end of file