Merge lp:~jnaous/rpc4django/urlgroups into lp:rpc4django

Proposed by Jad Naous on 2010-11-20
Status: Needs review
Proposed branch: lp:~jnaous/rpc4django/urlgroups
Merge into: lp:rpc4django
Diff against target: 714 lines (+279/-118)
11 files modified
example/urls.py (+3/-2)
rpc4django/rpcdispatcher.py (+7/-46)
rpc4django/tests/test_rpcdispatcher.py (+0/-18)
rpc4django/tests/test_rpcviews.py (+79/-19)
rpc4django/tests/test_urls.py (+12/-0)
rpc4django/tests/testmod/__init__.py (+1/-1)
rpc4django/tests/testmod/models.py (+5/-0)
rpc4django/tests/testmod/testsubmod/__init__.py (+4/-0)
rpc4django/tests/utils.py (+64/-0)
rpc4django/utils.py (+20/-0)
rpc4django/views.py (+84/-32)
To merge this branch: bzr merge lp:~jnaous/rpc4django/urlgroups
Reviewer Review Type Date Requested Status
davidfischer 2010-11-20 Pending
Review via email: mp+41388@code.launchpad.net

Description of the Change

I've implemented the blueprint for enforcing specific methods to be available at specific URLs. I've also written tests for it. I've also allowed arbitrary keyword args to be passed to RPC methods because this can be useful for passing parameters from the URL at which an RPC is called.

To post a comment you must log in.

Unmerged revisions

16. By Jad Naous <jnaous@jadsm> on 2010-11-20

Implement isolation across URLs

15. By Jad Naous <jnaous@jadsm> on 2010-11-20

removed test file

14. By Jad Naous <jnaous@jadsm> on 2010-11-20

test

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'example/urls.py'
2--- example/urls.py 2010-03-29 05:04:00 +0000
3+++ example/urls.py 2010-11-20 07:51:10 +0000
4@@ -1,4 +1,5 @@
5 from django.conf.urls.defaults import *
6+from rpc4django.utils import rpc_url
7
8 # Uncomment the next two lines to enable the admin:
9 # from django.contrib import admin
10@@ -14,6 +15,6 @@
11
12 # Uncomment the next line to enable the admin:
13 # (r'^admin/(.*)', admin.site.root),
14- ('^$', 'rpc4django.views.serve_rpc_request'),
15- ('^RPC2$', 'rpc4django.views.serve_rpc_request'),
16+ rpc_url('^$'),
17+ rpc_url('^RPC2$'),
18 )
19
20=== modified file 'rpc4django/rpcdispatcher.py'
21--- rpc4django/rpcdispatcher.py 2010-10-27 04:36:20 +0000
22+++ rpc4django/rpcdispatcher.py 2010-11-20 07:51:10 +0000
23@@ -8,7 +8,6 @@
24 import inspect
25 import platform
26 import pydoc
27-import types
28 import xmlrpclib
29 from xmlrpclib import Fault
30 from django.contrib.auth import authenticate, login, logout
31@@ -43,25 +42,16 @@
32
33 @rpcmethod()
34 @rpcmethod(name='myns.myFuncName', signature=['int','int'])
35- @rpcmethod(permission='add_group')
36+ @rpcmethod(permission='add_group', url_name="my_url_name")
37
38 '''
39
40 def set_rpcmethod_info(method):
41 method.is_rpcmethod = True
42- method.signature = []
43- method.permission = None
44- method.external_name = getattr(method, '__name__')
45-
46- if 'name' in kwargs:
47- method.external_name = kwargs['name']
48-
49- if 'signature' in kwargs:
50- method.signature = kwargs['signature']
51-
52- if 'permission' in kwargs:
53- method.permission = kwargs['permission']
54-
55+ method.external_name = kwargs.get("name", getattr(method, '__name__'))
56+ method.signature = kwargs.get('signature', [])
57+ method.permission = kwargs.get('permission', None)
58+ method.url_name = kwargs.get("url_name", "root")
59 return method
60 return set_rpcmethod_info
61
62@@ -210,9 +200,9 @@
63
64 '''
65
66- def __init__(self, url='', apps=[], restrict_introspection=False, restrict_ootb_auth=True):
67+ def __init__(self, url_name='root', restrict_introspection=False, restrict_ootb_auth=True):
68 version = platform.python_version_tuple()
69- self.url = url
70+ self.url_name = url_name
71 self.rpcmethods = [] # a list of RPCMethod objects
72 self.jsonrpcdispatcher = JSONRPCDispatcher()
73 self.xmlrpcdispatcher = XMLRPCDispatcher()
74@@ -226,8 +216,6 @@
75 if not restrict_ootb_auth:
76 self.register_method(self.system_login)
77 self.register_method(self.system_logout)
78-
79- self.register_rpcmethods(apps)
80
81 @rpcmethod(name='system.describe', signature=['struct'])
82 def system_describe(self):
83@@ -237,7 +225,6 @@
84
85 description = {}
86 description['serviceType'] = 'RPC4Django JSONRPC+XMLRPC'
87- description['serviceURL'] = self.url,
88 description['methods'] = [{'name': method.name,
89 'summary': method.help,
90 'params': method.get_params(),
91@@ -314,32 +301,6 @@
92
93 return False
94
95- def register_rpcmethods(self, apps):
96- '''
97- Scans the installed apps for methods with the rpcmethod decorator
98- Adds these methods to the list of methods callable via RPC
99- '''
100-
101- for appname in apps:
102- # check each app for any rpcmethods
103- app = __import__(appname, globals(), locals(), ['*'])
104- for obj in dir(app):
105- method = getattr(app, obj)
106- if callable(method) and \
107- hasattr(method, 'is_rpcmethod') and \
108- method.is_rpcmethod == True:
109- # if this method is callable and it has the rpcmethod
110- # decorator, add it to the dispatcher
111- self.register_method(method, method.external_name)
112- elif isinstance(method, types.ModuleType):
113- # if this is not a method and instead a sub-module,
114- # scan the module for methods with @rpcmethod
115- try:
116- self.register_rpcmethods(["%s.%s" % (appname, obj)])
117- except ImportError:
118- pass
119-
120-
121 def jsondispatch(self, raw_post_data, **kwargs):
122 '''
123 Sends the post data to :meth:`rpc4django.jsonrpcdispatcher.JSONRPCDispatcher.dispatch`
124
125=== modified file 'rpc4django/tests/test_rpcdispatcher.py'
126--- rpc4django/tests/test_rpcdispatcher.py 2010-03-29 05:04:00 +0000
127+++ rpc4django/tests/test_rpcdispatcher.py 2010-11-20 07:51:10 +0000
128@@ -139,24 +139,6 @@
129 self.assertEqual(jsondict['id'], 1)
130 self.assertEqual(jsondict['result'], 3)
131
132- def test_register_methods(self):
133- self.d.register_rpcmethods(['rpc4django.tests.testmod'])
134-
135- jsontxt = '{"params":[3,1],"method":"subtract","id":1}'
136- resp = self.d.jsondispatch(jsontxt)
137- jsondict = json.loads(resp)
138- self.assertTrue(jsondict['error'] is None)
139- self.assertEqual(jsondict['id'], 1)
140- self.assertEqual(jsondict['result'], 2)
141-
142- jsontxt = '{"params":[3,2],"method":"power","id":99}'
143- resp = self.d.jsondispatch(jsontxt)
144- jsondict = json.loads(resp)
145- self.assertTrue(jsondict['error'] is None)
146- self.assertEqual(jsondict['id'], 99)
147- self.assertEqual(jsondict['result'], 9)
148-
149-
150 def test_kwargs(self):
151 self.d.register_method(self.kwargstest)
152
153
154=== modified file 'rpc4django/tests/test_rpcviews.py'
155--- rpc4django/tests/test_rpcviews.py 2010-03-29 05:04:00 +0000
156+++ rpc4django/tests/test_rpcviews.py 2010-11-20 07:51:10 +0000
157@@ -6,30 +6,57 @@
158
159 import unittest
160 import xmlrpclib
161-from django.test.client import Client
162+from django.core.urlresolvers import reverse
163 from rpc4django.jsonrpcdispatcher import json, JSONRPC_SERVICE_ERROR
164-
165-RPCPATH = '/RPC2'
166-
167-class TestRPCViews(unittest.TestCase):
168-
169+from rpc4django import views
170+from rpc4django.tests.utils import SettingsTestCase
171+
172+
173+class TestRPCViews(SettingsTestCase):
174+
175+ urls = "rpc4django.tests.test_urls"
176+
177 def setUp(self):
178- self.client = Client()
179-
180+ self.settings_manager.set(
181+ INSTALLED_APPS=(
182+ "rpc4django",
183+ "rpc4django.tests.testmod",
184+ ),
185+ MIDDLEWARE_CLASSES=(
186+ 'django.middleware.common.CommonMiddleware',
187+ 'django.middleware.transaction.TransactionMiddleware',
188+ 'django.middleware.csrf.CsrfViewMiddleware',
189+ 'django.contrib.sessions.middleware.SessionMiddleware',
190+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
191+ ),
192+ DEBUG_PROPAGATE_EXCEPTIONS=False,
193+ )
194+
195+ views._register_rpcmethods(
196+ [
197+ "rpc4django",
198+ "rpc4django.tests.testmod",
199+ ],
200+ restrict_introspection=False,
201+ dispatchers=views.dispatchers)
202+
203+ self.rpc_path = reverse("serve_rpc_request")
204+ self.ns_rpc_path = reverse("my_url_name")
205+
206 def test_methodsummary(self):
207- response = self.client.get(RPCPATH)
208+ response = self.client.get(self.rpc_path)
209 self.assertEqual(response.status_code, 200)
210 self.assertEqual(response.template.name, 'rpc4django/rpcmethod_summary.html')
211
212 def test_xmlrequests(self):
213 data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>'
214- response = self.client.post(RPCPATH, data, 'text/xml')
215+ response = self.client.post(self.rpc_path, data, 'text/xml')
216 self.assertEqual(response.status_code, 200)
217 xmlrpclib.loads(response.content) # this will throw an exception with bad data
218
219 def test_jsonrequests(self):
220 data = '{"params":[],"method":"system.listMethods","id":123}'
221- response = self.client.post(RPCPATH, data, 'application/json')
222+ response = self.client.post(self.rpc_path, data, 'application/json')
223 self.assertEqual(response.status_code, 200)
224 jsondict = json.loads(response.content)
225 self.assertTrue(jsondict['error'] is None)
226@@ -37,7 +64,7 @@
227 self.assertTrue(isinstance(jsondict['result'], list))
228
229 data = '{"params":[],"method":"system.describe","id":456}'
230- response = self.client.post(RPCPATH, data, 'text/javascript')
231+ response = self.client.post(self.rpc_path, data, 'text/javascript')
232 self.assertEqual(response.status_code, 200)
233 jsondict = json.loads(response.content)
234 self.assertTrue(jsondict['error'] is None)
235@@ -46,7 +73,7 @@
236
237 def test_typedetection(self):
238 data = '{"params":[],"method":"system.listMethods","id":123}'
239- response = self.client.post(RPCPATH, data, 'text/plain')
240+ response = self.client.post(self.rpc_path, data, 'text/plain')
241 self.assertEqual(response.status_code, 200)
242 jsondict = json.loads(response.content)
243 self.assertTrue(jsondict['error'] is None)
244@@ -54,13 +81,13 @@
245 self.assertTrue(isinstance(jsondict['result'], list))
246
247 data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>'
248- response = self.client.post(RPCPATH, data, 'text/plain')
249+ response = self.client.post(self.rpc_path, data, 'text/plain')
250 self.assertEqual(response.status_code, 200)
251 xmlrpclib.loads(response.content) # this will throw an exception with bad data
252
253 # jsonrpc request with xmlrpc data (should be error)
254 data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>'
255- response = self.client.post(RPCPATH, data, 'application/json')
256+ response = self.client.post(self.rpc_path, data, 'application/json')
257 self.assertEqual(response.status_code, 200)
258 jsondict = json.loads(response.content)
259 self.assertTrue(jsondict['result'] is None)
260@@ -69,7 +96,7 @@
261
262 data = '{"params":[],"method":"system.listMethods","id":123}'
263 try:
264- response = self.client.post(RPCPATH, data, 'text/xml')
265+ response = self.client.post(self.rpc_path, data, 'text/xml')
266 except:
267 # for some reason, this throws an expat error
268 # but only in python 2.4
269@@ -83,7 +110,7 @@
270
271 def test_badrequests(self):
272 data = '{"params":[],"method":"system.methodHelp","id":456}'
273- response = self.client.post(RPCPATH, data, 'application/json')
274+ response = self.client.post(self.rpc_path, data, 'application/json')
275 self.assertEqual(response.status_code, 200)
276 jsondict = json.loads(response.content)
277 self.assertTrue(jsondict['error'] is not None)
278@@ -92,7 +119,7 @@
279 self.assertEqual(jsondict['error']['code'], JSONRPC_SERVICE_ERROR)
280
281 data = '<?xml version="1.0"?><methodCall><methodName>method.N0t.Exists</methodName><params></params></methodCall>'
282- response = self.client.post(RPCPATH, data, 'text/xml')
283+ response = self.client.post(self.rpc_path, data, 'text/xml')
284 self.assertEqual(response.status_code, 200)
285 try:
286 xmlrpclib.loads(response.content)
287@@ -108,9 +135,42 @@
288 # options requests can only be tested by django 1.1+
289 self.fail('This version of django "%s" does not support http access control' %str(t))
290
291- response = self.client.options(RPCPATH, '', 'text/plain')
292+ response = self.client.options(self.rpc_path, '', 'text/plain')
293 self.assertEqual(response['Access-Control-Allow-Methods'], 'POST, GET, OPTIONS')
294 self.assertEqual(response['Access-Control-Max-Age'], '0')
295
296+ def test_good_url_name(self):
297+ """
298+ Make sure we call functions based on the url they are arriving on.
299+ """
300+ data = xmlrpclib.dumps((5, 4), "subtract")
301+ response = self.client.post(self.rpc_path, data, 'text/xml')
302+ self.assertEqual(response.status_code, 200)
303+ result, methname = xmlrpclib.loads(response.content)
304+ self.assertEqual(result, (1,))
305+ self.assertEqual(methname, None)
306+
307+ data = xmlrpclib.dumps((5, 4), "product")
308+ response = self.client.post(self.ns_rpc_path, data, 'text/xml')
309+ self.assertEqual(response.status_code, 200)
310+ result, methname = xmlrpclib.loads(response.content)
311+ self.assertEqual(result, (20,))
312+ self.assertEqual(methname, None)
313+
314+ def test_bad_url_name(self):
315+ """
316+ Make sure we cannot call functions using the wrong url_name.
317+ """
318+ data = xmlrpclib.dumps((5, 4), "subtract")
319+ response = self.client.post(self.ns_rpc_path, data, 'text/xml')
320+
321+ self.assertEqual(response.status_code, 200)
322+ try:
323+ result, methname = xmlrpclib.loads(response.content)
324+ self.fail("Expected xmlrpclib Fault")
325+ except xmlrpclib.Fault as fault:
326+ self.assertEqual(fault.faultCode, 1)
327+ self.assertTrue(fault.faultString.endswith('method "subtract" is not supported'))
328+
329 if __name__ == '__main__':
330 unittest.main()
331\ No newline at end of file
332
333=== added file 'rpc4django/tests/test_urls.py'
334--- rpc4django/tests/test_urls.py 1970-01-01 00:00:00 +0000
335+++ rpc4django/tests/test_urls.py 2010-11-20 07:51:10 +0000
336@@ -0,0 +1,12 @@
337+'''
338+Created on Jun 9, 2010
339+
340+@author: jnaous
341+'''
342+from django.conf.urls.defaults import *
343+from rpc4django.utils import rpc_url
344+
345+urlpatterns = patterns("",
346+ rpc_url(r"^RPC2/$", name="serve_rpc_request", use_name_for_dispatch=False),
347+ rpc_url(r"^my_url/RPC2/$", name="my_url_name"),
348+)
349
350=== modified file 'rpc4django/tests/testmod/__init__.py'
351--- rpc4django/tests/testmod/__init__.py 2010-03-29 05:04:00 +0000
352+++ rpc4django/tests/testmod/__init__.py 2010-11-20 07:51:10 +0000
353@@ -5,6 +5,6 @@
354 def subtract(a, b):
355 return a - b
356
357-@rpcmethod(signature=['int', 'int', 'int'])
358+@rpcmethod(signature=['int', 'int', 'int'], url_name="my_url_name")
359 def product(a, b):
360 return a * b
361
362=== added file 'rpc4django/tests/testmod/models.py'
363--- rpc4django/tests/testmod/models.py 1970-01-01 00:00:00 +0000
364+++ rpc4django/tests/testmod/models.py 2010-11-20 07:51:10 +0000
365@@ -0,0 +1,5 @@
366+'''
367+Created on Jun 9, 2010
368+
369+@author: jnaous
370+'''
371
372=== modified file 'rpc4django/tests/testmod/testsubmod/__init__.py'
373--- rpc4django/tests/testmod/testsubmod/__init__.py 2010-03-29 05:04:00 +0000
374+++ rpc4django/tests/testmod/testsubmod/__init__.py 2010-11-20 07:51:10 +0000
375@@ -3,3 +3,7 @@
376 @rpcmethod(signature=['int', 'int', 'int'])
377 def power(a, b):
378 return a ** b
379+
380+@rpcmethod(signature=['int', 'int'], url_name="my_url_name")
381+def power2(a):
382+ return 2 ** a
383
384=== added file 'rpc4django/tests/utils.py'
385--- rpc4django/tests/utils.py 1970-01-01 00:00:00 +0000
386+++ rpc4django/tests/utils.py 2010-11-20 07:51:10 +0000
387@@ -0,0 +1,64 @@
388+'''
389+Created on Nov 19, 2010
390+
391+http://djangosnippets.org/snippets/1011/
392+
393+@author: jnaous
394+'''
395+from django.conf import settings
396+from django.core.management import call_command
397+from django.db.models import loading
398+from django.test import TestCase
399+
400+NO_SETTING = ('!', None)
401+
402+class TestSettingsManager(object):
403+ """
404+ A class which can modify some Django settings temporarily for a
405+ test and then revert them to their original values later.
406+
407+ Automatically handles resyncing the DB if INSTALLED_APPS is
408+ modified.
409+
410+ """
411+ def __init__(self):
412+ self._original_settings = {}
413+
414+ def set(self, **kwargs):
415+ for k,v in kwargs.iteritems():
416+ self._original_settings.setdefault(k, getattr(settings, k,
417+ NO_SETTING))
418+ setattr(settings, k, v)
419+ if 'INSTALLED_APPS' in kwargs:
420+ self.syncdb()
421+
422+ def syncdb(self):
423+ loading.cache.loaded = False
424+ call_command('syncdb', verbosity=0)
425+
426+ def revert(self):
427+ for k,v in self._original_settings.iteritems():
428+ if v == NO_SETTING:
429+ delattr(settings, k)
430+ else:
431+ setattr(settings, k, v)
432+ if 'INSTALLED_APPS' in self._original_settings:
433+ self.syncdb()
434+ self._original_settings = {}
435+
436+
437+class SettingsTestCase(TestCase):
438+ """
439+ A subclass of the Django TestCase with a settings_manager
440+ attribute which is an instance of TestSettingsManager.
441+
442+ Comes with a tearDown() method that calls
443+ self.settings_manager.revert().
444+
445+ """
446+ def __init__(self, *args, **kwargs):
447+ super(SettingsTestCase, self).__init__(*args, **kwargs)
448+ self.settings_manager = TestSettingsManager()
449+
450+ def tearDown(self):
451+ self.settings_manager.revert()
452
453=== added file 'rpc4django/utils.py'
454--- rpc4django/utils.py 1970-01-01 00:00:00 +0000
455+++ rpc4django/utils.py 2010-11-20 07:51:10 +0000
456@@ -0,0 +1,20 @@
457+'''
458+Created on Jun 9, 2010
459+
460+@author: jnaous
461+'''
462+from django.conf.urls.defaults import url
463+
464+def rpc_url(regex, kwargs=None, name=None, prefix='',
465+ use_name_for_dispatch=True):
466+ """
467+ Wrapper around C{django.conf.urls.defaults.url} to add the C{name} parameter
468+ to C{kwargs} as "url_name", and to automatically assign the C{view}.
469+ C{use_name_for_dispatch} controls whether or not to add C{name} to C{kwargs}
470+ """
471+ if name != None and use_name_for_dispatch:
472+ if kwargs == None:
473+ kwargs = {}
474+ kwargs["url_name"] = name
475+ view = "rpc4django.views.serve_rpc_request"
476+ return url(regex, view, kwargs, name, prefix)
477
478=== modified file 'rpc4django/views.py'
479--- rpc4django/views.py 2010-10-27 04:36:20 +0000
480+++ rpc4django/views.py 2010-11-20 07:51:10 +0000
481@@ -1,23 +1,23 @@
482 '''
483 The main entry point for RPC4Django. Usually, the user simply puts
484-:meth:`serve_rpc_request <rpc4django.views.serve_rpc_request>` into ``urls.py``
485+:meth:`rpc_url <rpc4django.utils.rpc_url>` into ``urls.py``
486
487 ::
488
489 urlpatterns = patterns('',
490 # rpc4django will need to be in your Python path
491- (r'^RPC2$', 'rpc4django.views.serve_rpc_request'),
492+ rpc_url(r'^RPC2$', name="my_url_name"),
493 )
494
495 '''
496
497 import logging
498+import types
499 from xml.dom.minidom import parseString
500 from xml.parsers.expat import ExpatError
501 from django.http import HttpResponse, Http404, HttpResponseForbidden
502 from django.shortcuts import render_to_response
503 from django.conf import settings
504-from django.core.urlresolvers import reverse, NoReverseMatch
505 from rpcdispatcher import RPCDispatcher
506 from __init__ import version
507
508@@ -45,7 +45,25 @@
509 # these will be scanned for @rpcmethod decorators
510 APPS = getattr(settings, 'INSTALLED_APPS', [])
511
512-def check_request_permission(request, request_format='xml'):
513+logger = logging.getLogger("rpc4django.views")
514+
515+class NonExistingDispatcher(Exception):
516+ """Raised when the dispatcher for a particular name is not found."""
517+ def __init__(self, path, url_name):
518+ super(NonExistingDispatcher, self).__init__(
519+ "URL name '%s' is not used in any rpcmethod,\
520+ however, the URL '%s' uses it." % (url_name, path))
521+ self.path = path
522+ self.url_name = url_name
523+
524+def get_dispatcher(path, url_name):
525+ try:
526+ dispatcher = dispatchers[url_name]
527+ except KeyError:
528+ raise NonExistingDispatcher(path, url_name)
529+ return dispatcher
530+
531+def check_request_permission(request, request_format='xml', url_name="root"):
532 '''
533 Checks whether this user has permission to call a particular method
534 This method does not check method call validity. That is done later
535@@ -54,11 +72,13 @@
536
537 - ``request`` - a django HttpRequest object
538 - ``request_format`` - the request type: 'json' or 'xml'
539+ - ``url_name`` - the name of the url at which this request was received
540
541 Returns ``False`` if permission is denied and ``True`` otherwise
542 '''
543
544 user = getattr(request, 'user', None)
545+ dispatcher = get_dispatcher(request.path, url_name)
546 methods = dispatcher.list_methods()
547 method_name = dispatcher.get_method_name(request.raw_post_data, \
548 request_format)
549@@ -69,20 +89,20 @@
550 # this is the method the user is calling
551 # time to check the permissions
552 if method.permission is not None:
553- logging.debug('Method "%s" is protected by permission "%s"' \
554+ logger.debug('Method "%s" is protected by permission "%s"' \
555 %(method.name, method.permission))
556 if user is None:
557 # user is only none if not using AuthenticationMiddleware
558- logging.warn('AuthenticationMiddleware is not enabled')
559+ logger.warn('AuthenticationMiddleware is not enabled')
560 response = False
561 elif not user.has_perm(method.permission):
562 # check the permission against the permission database
563- logging.info('User "%s" is NOT authorized' %(str(user)))
564+ logger.info('User "%s" is NOT authorized' %(str(user)))
565 response = False
566 else:
567- logging.debug('User "%s" is authorized' %(str(user)))
568+ logger.debug('User "%s" is authorized' %(str(user)))
569 else:
570- logging.debug('Method "%s" is unprotected' %(method.name))
571+ logger.debug('Method "%s" is unprotected' %(method.name))
572
573 break
574
575@@ -111,8 +131,8 @@
576 return False
577
578 if LOG_REQUESTS_RESPONSES:
579- logging.info('Unrecognized content-type "%s"' %conttype)
580- logging.info('Analyzing rpc request data to get content type')
581+ logger.info('Unrecognized content-type "%s"' %conttype)
582+ logger.info('Analyzing rpc request data to get content type')
583
584 # analyze post data to see whether it is xml or json
585 # this is slower than if the content-type was set properly
586@@ -124,7 +144,7 @@
587
588 return False
589
590-def serve_rpc_request(request):
591+def serve_rpc_request(request, url_name="root", **kwargs):
592 '''
593 Handles rpc calls based on the content type of the request or
594 returns the method documentation page if the request
595@@ -134,38 +154,42 @@
596
597 ``request``
598 the Django HttpRequest object
599-
600+ ``url_name``
601+ the name of the url at which the request arrived
602+
603 '''
604-
605+
606+ dispatcher = get_dispatcher(request.path, url_name)
607+
608 if request.method == "POST" and len(request.POST) > 0:
609 # Handle POST request with RPC payload
610
611 if LOG_REQUESTS_RESPONSES:
612- logging.debug('Incoming request: %s' %str(request.raw_post_data))
613+ logger.debug('Incoming request: %s' % str(request.raw_post_data))
614
615 if is_xmlrpc_request(request):
616 if RESTRICT_XML:
617 raise Http404
618
619- if not check_request_permission(request, 'xml'):
620+ if not check_request_permission(request, 'xml', url_name=url_name):
621 return HttpResponseForbidden()
622
623- resp = dispatcher.xmldispatch(request.raw_post_data, \
624- request=request)
625+ resp = dispatcher.xmldispatch(request.raw_post_data,
626+ request=request, **kwargs)
627 response_type = 'text/xml'
628 else:
629 if RESTRICT_JSON:
630 raise Http404
631
632- if not check_request_permission(request, 'json'):
633+ if not check_request_permission(request, 'json', url_name=url_name):
634 return HttpResponseForbidden()
635
636- resp = dispatcher.jsondispatch(request.raw_post_data, \
637- request=request)
638+ resp = dispatcher.jsondispatch(request.raw_post_data,
639+ request=request, **kwargs)
640 response_type = 'application/json'
641
642 if LOG_REQUESTS_RESPONSES:
643- logging.debug('Outgoing %s response: %s' %(response_type, resp))
644+ logger.debug('Outgoing %s response: %s' %(response_type, resp))
645
646 return HttpResponse(resp, response_type)
647 elif request.method == 'OPTIONS':
648@@ -185,7 +209,7 @@
649 request.META.get('HTTP_ACCESS_CONTROL_REQUEST_HEADERS', '')
650
651 if LOG_REQUESTS_RESPONSES:
652- logging.debug('Outgoing HTTP access response to: %s' %(origin))
653+ logger.debug('Outgoing HTTP access response to: %s' %(origin))
654
655 return response
656 else:
657@@ -199,7 +223,7 @@
658 methods = dispatcher.list_methods()
659 template_data = {
660 'methods': methods,
661- 'url': URL,
662+ 'url': request.path,
663
664 # rpc4django version
665 'version': version(),
666@@ -225,13 +249,41 @@
667 if csrf_exempt is not None:
668 serve_rpc_request = csrf_exempt(serve_rpc_request)
669
670-# reverse the method for use with system.describe and ajax
671-try:
672- URL = reverse(serve_rpc_request)
673-except NoReverseMatch:
674- URL = ''
675+def _register_rpcmethods(apps, restrict_introspection=False, restrict_ootb_auth=True, dispatchers={}):
676+ '''
677+ Scans the installed apps for methods with the rpcmethod decorator
678+ Adds these methods to the list of methods callable via RPC
679+ '''
680
681-# instantiate the rpcdispatcher -- this examines the INSTALLED_APPS
682-# for any @rpcmethod decorators and adds them to the callable methods
683-dispatcher = RPCDispatcher(URL, APPS, RESTRICT_INTROSPECTION, RESTRICT_OOTB_AUTH)
684+ for appname in apps:
685+ # check each app for any rpcmethods
686+ app = __import__(appname, globals(), locals(), ['*'])
687+ for obj in dir(app):
688+ method = getattr(app, obj)
689+ if callable(method) and \
690+ getattr(method, 'is_rpcmethod', False) == True:
691+ # if this method is callable and it has the rpcmethod
692+ # decorator, add it to the dispatcher
693+ if method.url_name not in dispatchers:
694+ logger.debug("Registered URL name '%s'" % method.url_name)
695+ dispatchers[method.url_name] = RPCDispatcher(
696+ method.url_name, restrict_introspection,
697+ restrict_ootb_auth)
698+ logger.debug(
699+ "Registered method '%s' to URL name '%s'"
700+ % (method.external_name, method.url_name))
701+ dispatchers[method.url_name].register_method(
702+ method, method.external_name)
703+ elif isinstance(method, types.ModuleType):
704+ # if this is not a method and instead a sub-module,
705+ # scan the module for methods with @rpcmethod
706+ try:
707+ _register_rpcmethods(
708+ ["%s.%s" % (appname, obj)],
709+ restrict_introspection, restrict_ootb_auth, dispatchers)
710+ except ImportError:
711+ pass
712+ return dispatchers
713+
714+dispatchers = _register_rpcmethods(APPS, RESTRICT_INTROSPECTION, RESTRICT_OOTB_AUTH)
715

Subscribers

People subscribed via source and target branches

to all changes: