Skip to content
This repository
tree: 199e88ce13
Fetching contributors…

Cannot retrieve contributors at this time

file 312 lines (267 sloc) 10.75 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
import datetime, time
import json as json
from Cookie import SimpleCookie
from inspect import getargspec
from urllib import urlencode, quote

from django.core.serializers.json import DateTimeAwareJSONEncoder
from django.core.urlresolvers import reverse
from django.db import models
from django.http import HttpResponse
from django.utils.datastructures import MultiValueDict
from django.utils.encoding import smart_str, force_unicode
from django.utils.functional import Promise


class LazyEncoder(DateTimeAwareJSONEncoder):
    """
Smarter encoder that can encode a few data types tipically found
in django code such as datetimes, promises and querysets
"""
    def default(self, obj):
        if isinstance(obj, Promise) :
            return force_unicode(obj)
        elif isinstance(obj, datetime.datetime):
            return time.strftime("%d-%m-%Y %H-%M-%S")
        elif isinstance(obj, models.query.ValuesQuerySet):
            self.default(list(obj))
        elif isinstance(obj, models.query.QuerySet):
            self.default(list(obj))
        try:
            return super(LazyEncoder, self).default(obj)
        except:
            raise


class RpcMultiValueDict(MultiValueDict):
    """
Just allow pass not list values and get only dict as argument
"""

    def __init__(self, key_to_list_mapping={}):
        for key, value in key_to_list_mapping.items():
            if not isinstance(value, (list, tuple)):
                key_to_list_mapping[key] = [value]

        super(MultiValueDict, self).__init__(key_to_list_mapping)

    def urlencode(self, safe=None):
        output = []
        if safe:
            encode = lambda k, v: '%s=%s' % ((quote(k, safe), quote(v, safe)))
        else:
            encode = lambda k, v: urlencode({k: v})
        for k, list_ in self.lists():
            k = smart_str(k)
            output.extend([encode(k, smart_str(v))
                           for v in list_])
        return '&'.join(output)

class RpcExceptionEvent(Exception):
    """
This exception is sent to server as Ext.Direct.ExceptionEvent.
So we can handle it in client and show pretty message for user.
"""
    pass

class RpcResponse(dict):
    pass

class Error(RpcResponse):
    """
Simple responses. Just for pretty code and some kind of "protocol"
"""
    def __init__(self, text, **kwargs):
        super(Error, self).__init__(error=text, **kwargs)

class Msg(RpcResponse):
    """
Simple responses. Just for pretty code and some kind of "protocol"
"""
    def __init__(self, text, **kwargs):
        super(Msg, self).__init__(msg=text, **kwargs)

class RpcHttpResponse(RpcResponse):
    """
This is wrapper for method's reponse, which allow save some modification of
HTTP response. For example set COOKIES. This should be flexible and useful
for in future.
"""

    def __init__(self, *args, **kwargs):
        super(RpcHttpResponse, self).__init__(*args, **kwargs)
        self.cookies = SimpleCookie()

    def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
                   domain=None, secure=False):
        self.cookies[key] = value
        if max_age is not None:
            self.cookies[key]['max-age'] = max_age
        if expires is not None:
            self.cookies[key]['expires'] = expires
        if path is not None:
            self.cookies[key]['path'] = path
        if domain is not None:
            self.cookies[key]['domain'] = domain
        if secure:
            self.cookies[key]['secure'] = True

# For jQuery.Rpc
class RpcRouter(object):
    """
Router for jQuery.Rpc calls.
"""
    def __init__(self, url, actions={}, enable_buffer=True, max_retries=0):
        self.url = url
        self.actions = actions
        self.enable_buffer = enable_buffer
        self.max_retries = max_retries

    def __call__(self, request, *args, **kwargs):
        """
This method is view that receive requests from Ext.Direct.
"""
        user = request.user
        POST = request.POST

        if POST.get('extAction'):
            #Forms with upload not supported yet
            requests = {
                'action': POST.get('rpcAction'),
                'method': POST.get('rpcMethod'),
                'data': [POST],
                'upload': POST.get('rpcUpload') == 'true',
                'tid': POST.get('rpcTID')
            }

            if requests['upload']:
                requests['data'].append(request.FILES)
                output = json.dumps(self.call_action(requests, user))
                return HttpResponse('<textarea>%s</textarea>' \
                                    % output)
        else:
            try:
                requests = json.loads(request.POST.keys()[0])
            except (ValueError, KeyError, IndexError):
                requests = []

        if not isinstance(requests, list):
                requests = [requests]

        response = HttpResponse('', mimetype="application/json")

        output = []

        for rd in requests:
            mr = self.call_action(rd, request, *args, **kwargs)

            #This looks like a little ugly
            if 'result' in mr and isinstance(mr['result'], RpcHttpResponse):
                for key, val in mr['result'].cookies.items():
                    response.set_cookie(key, val.value, val['max-age'], val['expires'], val['path'],
                                        val['domain'], val['secure'])
                mr['result'] = dict(mr['result'])

            output.append(mr)
        response.content = json.dumps(output, cls=LazyEncoder)

        return response

    def action_extra_kwargs(self, action, request, *args, **kwargs):
        """
Check maybe this action get some extra arguments from request
"""
        if hasattr(action, '_extra_kwargs'):
            return action._extra_kwargs(request, *args, **kwargs)
        return {}

    def method_extra_kwargs(self, method, request, *args, **kwargs):
        """
Check maybe this method get some extra arguments from request
"""
        if hasattr(method, '_extra_kwargs'):
            return method._extra_kwargs(request, *args, **kwargs)
        return {}

    def extra_kwargs(self, request, *args, **kwargs):
        """
For all method in ALL actions we add request.user to arguments.
You can add something else, request for example.
For adding extra arguments for one action use action_extra_kwargs.
"""
        return {
            'user': request.user
        }

    def api(self, request, *args, **kwargs):
        """
This method is view that send js for provider initialization.
Just set this in template after ExtJs including:
<script src="{% url api_url_name %}"></script>
"""
        obj = json.dumps(self, cls=RpcRouterJSONEncoder, url_args=args, url_kwargs=kwargs)
        return HttpResponse('jQuery.Rpc.addProvider(%s)' % obj, mimetype="text/javascript")

    def call_action(self, rd, request, *args, **kwargs):
        """
This method checks parameters of Ext.Direct request and call method of action.
It checks arguments number, method existing, handle RpcExceptionEvent and send
exception event for Ext.Direct.
"""
        method = rd['method']

        if not rd['action'] in self.actions:
            return {
                'tid': rd['tid'],
                'type': 'exception',
                'action': rd['action'],
                'method': method,
                'message': 'Undefined action'
            }

        action = self.actions[rd['action']]

        if not hasattr(action, method):
            return {
                'tid': rd['tid'],
                'type': 'exception',
                'action': rd['action'],
                'method': method,
                'message': 'Undefined method'
            }

        func = getattr(action, method)

        args = []
        for val in (rd.get('data') or []):
            if isinstance(val, dict):
                val = RpcMultiValueDict(val)
            args.append(val)

        extra_kwargs = self.extra_kwargs(request, *args, **kwargs)
        extra_kwargs.update(self.action_extra_kwargs(action, request, *args, **kwargs))
        extra_kwargs.update(self.method_extra_kwargs(func, request, *args, **kwargs))

        func_args, varargs, varkw, func_defaults = getargspec(func)
        func_args.remove('self') #TODO: or cls for classmethod
        for name in extra_kwargs.keys():
            if name in func_args:
                func_args.remove(name)

        required_args_count = len(func_args) - len(func_defaults or [])
        if (required_args_count - len(args)) > 0 or (not varargs and len(args) > len(func_args)):
            return {
                'tid': rd['tid'],
                'type': 'exception',
                'action': rd['action'],
                'method': method,
                'message': 'Incorrect arguments number'
            }

        try:
            return {
                'tid': rd['tid'],
                'type': 'rpc',
                'action': rd['action'],
                'method': method,
                'result': func(*args, **extra_kwargs)
            }
        except RpcExceptionEvent, e:
            return {
                'tid': rd['tid'],
                'type': 'exception',
                'action': rd['action'],
                'method': method,
                'message': unicode(e)
            }

class RpcRouterJSONEncoder(json.JSONEncoder):
    def __init__(self, url_args, url_kwargs, *args, **kwargs):
        self.url_args = url_args
        self.url_kwargs = url_kwargs
        super(RpcRouterJSONEncoder, self).__init__(*args, **kwargs)

    def _encode_action(self, o):
        output = []
        for method in dir(o):
            if not method.startswith('_'):
                #f = getattr(o, method)
                data = dict(name=method)
                output.append(data)
        return output

    def default(self, o):
        if isinstance(o, RpcRouter):
            output = {
                'url': reverse(o.url, args=self.url_args, kwargs=self.url_kwargs),
                'enableBuffer': o.enable_buffer,
                'actions': {},
                'maxRetries': o.max_retries
            }
            for name, action in o.actions.items():
                output['actions'][name] = self._encode_action(action)
            return output
        else:
            return super(RpcRouterJSONEncoder, self).default(o)

def add_request_to_kwargs(func):
    def extra_kwargs_func(request, *args, **kwargs):
        return dict(request=request)

    func._extra_kwargs = extra_kwargs_func
    return func
Something went wrong with that request. Please try again.