Merge lp:~jnaous/rpc4django/urlgroups into lp:rpc4django
- urlgroups
- Merge into main
Proposed by
Jad Naous
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 |
Related bugs: | |
Related blueprints: |
Different URLs expose different methods
(Undefined)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
davidfischer | Pending | ||
Review via email: mp+41388@code.launchpad.net |
Commit message
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.
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 |