from __future__ import unicode_literals
import json
from inspect import getargspec
from django.conf.urls import patterns, url
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from djangorpc.datastructures import RpcMultiValueDict
from djangorpc.exceptions import RpcExceptionEvent
from djangorpc.settings import JS_API_URL_NAME, ROUTER_URL_NAME
from djangorpc.responses import RpcHttpResponse
[docs]class RpcRouter(object):
"""
Router class for RPC.
"""
def __init__(self, actions={}, url_namespace=None, enable_buffer=True):
"""
Router class for RPC.
:param url_namespace: URL pattern namespace where router insance is included.
Used to get URL which router is connected.
:param actions: Action classes router should add to RPC API
:param enable_buffer: Define client should send requests in a batch
"""
self.url_namespace = url_namespace
self.actions = actions
self.enable_buffer = enable_buffer
def get_urls(self):
return (
url(r'^jsapi/$', self.api, name=JS_API_URL_NAME),
url(r'^router/$', self.dispatch, name=ROUTER_URL_NAME)
)
@property
def urls(self):
if not hasattr(self, '_urls'):
self._urls = patterns('', *self.get_urls())
return self._urls
def dispatch(self, request, *args, **kwargs):
if request.POST.get('rpcAction'):
requests = {
'action': request.POST.get('rpcAction'),
'method': request.POST.get('rpcMethod'),
'data': [dict(request.POST)],
'tid': request.POST.get('rpcTID'),
'upload': request.POST.get('rpcUpload') == 'true'
}
if requests['upload']:
requests['files'] = request.FILES
output = json.dumps(self.call_action(requests, request, *args, **kwargs))
return HttpResponse('<textarea>%s</textarea>' % output)
else:
try:
requests = json.loads(request.body)
except (ValueError, KeyError, IndexError):
# TODO: add loagging and return error event
requests = []
if not isinstance(requests, list):
requests = [requests]
response = HttpResponse('', content_type="application/json")
output = []
for rd in requests:
mr = self.call_action(rd, request, *args, **kwargs)
# FIXME: 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'] or None,
val['expires'],
val['path'],
val['domain'],
val['secure'])
mr['result'] = dict(mr['result'])
output.append(mr)
response.content = json.dumps(output)
return response
[docs] def api(self, request, *args, **kwargs):
"""
This method is view that send js for provider initialization.
Just set this in template after RPC scripts 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,
content_type='application/x-javascript')
def execute_func(self, func, *args, **kwargs):
if hasattr(func, '_pre_execute'):
result = func._pre_execute(func, *args, **kwargs)
if result is not None:
return result
return func(*args, **kwargs)
[docs] def call_action(self, rd, request, *args, **kwargs):
"""
This method checks parameters of RPC request and call method of action.
It checks arguments number, method existing, handle RpcExceptionEvent and send
exception event for RPC.
"""
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) and not isinstance(val, RpcMultiValueDict):
val = RpcMultiValueDict(val)
args.append(val)
if 'files' in rd:
args.append(rd.get('files'))
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': self.execute_func(func, *args, **extra_kwargs)
}
except RpcExceptionEvent, e:
return {
'tid': rd['tid'],
'type': 'exception',
'action': rd['action'],
'method': method,
'message': unicode(e)
}
[docs]class RpcRouterJSONEncoder(json.JSONEncoder):
"""
JSON Encoder for RpcRouter
"""
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('_'):
data = dict(name=method)
f = getattr(o, method)
if hasattr(f, '_form_handler'):
data['formHandler'] = getattr(f, '_form_handler')
output.append(data)
return output
def default(self, obj):
if isinstance(obj, RpcRouter):
if obj.url_namespace:
url_name = '%s:%s' % (obj.url_namespace, ROUTER_URL_NAME)
else:
url_name = ROUTER_URL_NAME
output = {
'url': reverse(url_name, args=self.url_args, kwargs=self.url_kwargs),
'enableBuffer': obj.enable_buffer,
'actions': {}
}
for name, action in obj.actions.items():
output['actions'][name] = self._encode_action(action)
return output
else:
return super(RpcRouterJSONEncoder, self).default(obj)