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

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
Reviewer Review Type Date Requested Status
davidfischer 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>

Implement isolation across URLs

15. By Jad Naous <jnaous@jadsm>

removed test file

14. By Jad Naous <jnaous@jadsm>

test

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'example/urls.py'
--- example/urls.py 2010-03-29 05:04:00 +0000
+++ example/urls.py 2010-11-20 07:51:10 +0000
@@ -1,4 +1,5 @@
1from django.conf.urls.defaults import *1from django.conf.urls.defaults import *
2from rpc4django.utils import rpc_url
23
3# Uncomment the next two lines to enable the admin:4# Uncomment the next two lines to enable the admin:
4# from django.contrib import admin5# from django.contrib import admin
@@ -14,6 +15,6 @@
1415
15 # Uncomment the next line to enable the admin:16 # Uncomment the next line to enable the admin:
16 # (r'^admin/(.*)', admin.site.root),17 # (r'^admin/(.*)', admin.site.root),
17 ('^$', 'rpc4django.views.serve_rpc_request'),18 rpc_url('^$'),
18 ('^RPC2$', 'rpc4django.views.serve_rpc_request'),19 rpc_url('^RPC2$'),
19)20)
2021
=== modified file 'rpc4django/rpcdispatcher.py'
--- rpc4django/rpcdispatcher.py 2010-10-27 04:36:20 +0000
+++ rpc4django/rpcdispatcher.py 2010-11-20 07:51:10 +0000
@@ -8,7 +8,6 @@
8import inspect8import inspect
9import platform9import platform
10import pydoc10import pydoc
11import types
12import xmlrpclib11import xmlrpclib
13from xmlrpclib import Fault12from xmlrpclib import Fault
14from django.contrib.auth import authenticate, login, logout13from django.contrib.auth import authenticate, login, logout
@@ -43,25 +42,16 @@
43 42
44 @rpcmethod()43 @rpcmethod()
45 @rpcmethod(name='myns.myFuncName', signature=['int','int'])44 @rpcmethod(name='myns.myFuncName', signature=['int','int'])
46 @rpcmethod(permission='add_group')45 @rpcmethod(permission='add_group', url_name="my_url_name")
47 46
48 '''47 '''
49 48
50 def set_rpcmethod_info(method):49 def set_rpcmethod_info(method):
51 method.is_rpcmethod = True50 method.is_rpcmethod = True
52 method.signature = []51 method.external_name = kwargs.get("name", getattr(method, '__name__'))
53 method.permission = None52 method.signature = kwargs.get('signature', [])
54 method.external_name = getattr(method, '__name__')53 method.permission = kwargs.get('permission', None)
5554 method.url_name = kwargs.get("url_name", "root")
56 if 'name' in kwargs:
57 method.external_name = kwargs['name']
58
59 if 'signature' in kwargs:
60 method.signature = kwargs['signature']
61
62 if 'permission' in kwargs:
63 method.permission = kwargs['permission']
64
65 return method55 return method
66 return set_rpcmethod_info56 return set_rpcmethod_info
6757
@@ -210,9 +200,9 @@
210 200
211 '''201 '''
212 202
213 def __init__(self, url='', apps=[], restrict_introspection=False, restrict_ootb_auth=True):203 def __init__(self, url_name='root', restrict_introspection=False, restrict_ootb_auth=True):
214 version = platform.python_version_tuple()204 version = platform.python_version_tuple()
215 self.url = url205 self.url_name = url_name
216 self.rpcmethods = [] # a list of RPCMethod objects206 self.rpcmethods = [] # a list of RPCMethod objects
217 self.jsonrpcdispatcher = JSONRPCDispatcher()207 self.jsonrpcdispatcher = JSONRPCDispatcher()
218 self.xmlrpcdispatcher = XMLRPCDispatcher()208 self.xmlrpcdispatcher = XMLRPCDispatcher()
@@ -226,8 +216,6 @@
226 if not restrict_ootb_auth:216 if not restrict_ootb_auth:
227 self.register_method(self.system_login)217 self.register_method(self.system_login)
228 self.register_method(self.system_logout)218 self.register_method(self.system_logout)
229
230 self.register_rpcmethods(apps)
231 219
232 @rpcmethod(name='system.describe', signature=['struct'])220 @rpcmethod(name='system.describe', signature=['struct'])
233 def system_describe(self):221 def system_describe(self):
@@ -237,7 +225,6 @@
237 225
238 description = {}226 description = {}
239 description['serviceType'] = 'RPC4Django JSONRPC+XMLRPC'227 description['serviceType'] = 'RPC4Django JSONRPC+XMLRPC'
240 description['serviceURL'] = self.url,
241 description['methods'] = [{'name': method.name, 228 description['methods'] = [{'name': method.name,
242 'summary': method.help, 229 'summary': method.help,
243 'params': method.get_params(),230 'params': method.get_params(),
@@ -314,32 +301,6 @@
314 301
315 return False302 return False
316 303
317 def register_rpcmethods(self, apps):
318 '''
319 Scans the installed apps for methods with the rpcmethod decorator
320 Adds these methods to the list of methods callable via RPC
321 '''
322
323 for appname in apps:
324 # check each app for any rpcmethods
325 app = __import__(appname, globals(), locals(), ['*'])
326 for obj in dir(app):
327 method = getattr(app, obj)
328 if callable(method) and \
329 hasattr(method, 'is_rpcmethod') and \
330 method.is_rpcmethod == True:
331 # if this method is callable and it has the rpcmethod
332 # decorator, add it to the dispatcher
333 self.register_method(method, method.external_name)
334 elif isinstance(method, types.ModuleType):
335 # if this is not a method and instead a sub-module,
336 # scan the module for methods with @rpcmethod
337 try:
338 self.register_rpcmethods(["%s.%s" % (appname, obj)])
339 except ImportError:
340 pass
341
342
343 def jsondispatch(self, raw_post_data, **kwargs):304 def jsondispatch(self, raw_post_data, **kwargs):
344 '''305 '''
345 Sends the post data to :meth:`rpc4django.jsonrpcdispatcher.JSONRPCDispatcher.dispatch`306 Sends the post data to :meth:`rpc4django.jsonrpcdispatcher.JSONRPCDispatcher.dispatch`
346307
=== modified file 'rpc4django/tests/test_rpcdispatcher.py'
--- rpc4django/tests/test_rpcdispatcher.py 2010-03-29 05:04:00 +0000
+++ rpc4django/tests/test_rpcdispatcher.py 2010-11-20 07:51:10 +0000
@@ -139,24 +139,6 @@
139 self.assertEqual(jsondict['id'], 1)139 self.assertEqual(jsondict['id'], 1)
140 self.assertEqual(jsondict['result'], 3)140 self.assertEqual(jsondict['result'], 3)
141 141
142 def test_register_methods(self):
143 self.d.register_rpcmethods(['rpc4django.tests.testmod'])
144
145 jsontxt = '{"params":[3,1],"method":"subtract","id":1}'
146 resp = self.d.jsondispatch(jsontxt)
147 jsondict = json.loads(resp)
148 self.assertTrue(jsondict['error'] is None)
149 self.assertEqual(jsondict['id'], 1)
150 self.assertEqual(jsondict['result'], 2)
151
152 jsontxt = '{"params":[3,2],"method":"power","id":99}'
153 resp = self.d.jsondispatch(jsontxt)
154 jsondict = json.loads(resp)
155 self.assertTrue(jsondict['error'] is None)
156 self.assertEqual(jsondict['id'], 99)
157 self.assertEqual(jsondict['result'], 9)
158
159
160 def test_kwargs(self):142 def test_kwargs(self):
161 self.d.register_method(self.kwargstest)143 self.d.register_method(self.kwargstest)
162 144
163145
=== modified file 'rpc4django/tests/test_rpcviews.py'
--- rpc4django/tests/test_rpcviews.py 2010-03-29 05:04:00 +0000
+++ rpc4django/tests/test_rpcviews.py 2010-11-20 07:51:10 +0000
@@ -6,30 +6,57 @@
66
7import unittest7import unittest
8import xmlrpclib8import xmlrpclib
9from django.test.client import Client9from django.core.urlresolvers import reverse
10from rpc4django.jsonrpcdispatcher import json, JSONRPC_SERVICE_ERROR10from rpc4django.jsonrpcdispatcher import json, JSONRPC_SERVICE_ERROR
1111from rpc4django import views
12RPCPATH = '/RPC2'12from rpc4django.tests.utils import SettingsTestCase
1313
14class TestRPCViews(unittest.TestCase):14
1515class TestRPCViews(SettingsTestCase):
16
17 urls = "rpc4django.tests.test_urls"
18
16 def setUp(self):19 def setUp(self):
17 self.client = Client()20 self.settings_manager.set(
1821 INSTALLED_APPS=(
22 "rpc4django",
23 "rpc4django.tests.testmod",
24 ),
25 MIDDLEWARE_CLASSES=(
26 'django.middleware.common.CommonMiddleware',
27 'django.middleware.transaction.TransactionMiddleware',
28 'django.middleware.csrf.CsrfViewMiddleware',
29 'django.contrib.sessions.middleware.SessionMiddleware',
30 'django.contrib.auth.middleware.AuthenticationMiddleware',
31 ),
32 DEBUG_PROPAGATE_EXCEPTIONS=False,
33 )
34
35 views._register_rpcmethods(
36 [
37 "rpc4django",
38 "rpc4django.tests.testmod",
39 ],
40 restrict_introspection=False,
41 dispatchers=views.dispatchers)
42
43 self.rpc_path = reverse("serve_rpc_request")
44 self.ns_rpc_path = reverse("my_url_name")
45
19 def test_methodsummary(self):46 def test_methodsummary(self):
20 response = self.client.get(RPCPATH)47 response = self.client.get(self.rpc_path)
21 self.assertEqual(response.status_code, 200)48 self.assertEqual(response.status_code, 200)
22 self.assertEqual(response.template.name, 'rpc4django/rpcmethod_summary.html')49 self.assertEqual(response.template.name, 'rpc4django/rpcmethod_summary.html')
23 50
24 def test_xmlrequests(self):51 def test_xmlrequests(self):
25 data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>'52 data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>'
26 response = self.client.post(RPCPATH, data, 'text/xml')53 response = self.client.post(self.rpc_path, data, 'text/xml')
27 self.assertEqual(response.status_code, 200)54 self.assertEqual(response.status_code, 200)
28 xmlrpclib.loads(response.content) # this will throw an exception with bad data55 xmlrpclib.loads(response.content) # this will throw an exception with bad data
29 56
30 def test_jsonrequests(self):57 def test_jsonrequests(self):
31 data = '{"params":[],"method":"system.listMethods","id":123}'58 data = '{"params":[],"method":"system.listMethods","id":123}'
32 response = self.client.post(RPCPATH, data, 'application/json')59 response = self.client.post(self.rpc_path, data, 'application/json')
33 self.assertEqual(response.status_code, 200)60 self.assertEqual(response.status_code, 200)
34 jsondict = json.loads(response.content)61 jsondict = json.loads(response.content)
35 self.assertTrue(jsondict['error'] is None)62 self.assertTrue(jsondict['error'] is None)
@@ -37,7 +64,7 @@
37 self.assertTrue(isinstance(jsondict['result'], list))64 self.assertTrue(isinstance(jsondict['result'], list))
38 65
39 data = '{"params":[],"method":"system.describe","id":456}'66 data = '{"params":[],"method":"system.describe","id":456}'
40 response = self.client.post(RPCPATH, data, 'text/javascript')67 response = self.client.post(self.rpc_path, data, 'text/javascript')
41 self.assertEqual(response.status_code, 200)68 self.assertEqual(response.status_code, 200)
42 jsondict = json.loads(response.content)69 jsondict = json.loads(response.content)
43 self.assertTrue(jsondict['error'] is None)70 self.assertTrue(jsondict['error'] is None)
@@ -46,7 +73,7 @@
46 73
47 def test_typedetection(self):74 def test_typedetection(self):
48 data = '{"params":[],"method":"system.listMethods","id":123}'75 data = '{"params":[],"method":"system.listMethods","id":123}'
49 response = self.client.post(RPCPATH, data, 'text/plain')76 response = self.client.post(self.rpc_path, data, 'text/plain')
50 self.assertEqual(response.status_code, 200)77 self.assertEqual(response.status_code, 200)
51 jsondict = json.loads(response.content)78 jsondict = json.loads(response.content)
52 self.assertTrue(jsondict['error'] is None)79 self.assertTrue(jsondict['error'] is None)
@@ -54,13 +81,13 @@
54 self.assertTrue(isinstance(jsondict['result'], list))81 self.assertTrue(isinstance(jsondict['result'], list))
55 82
56 data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>'83 data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>'
57 response = self.client.post(RPCPATH, data, 'text/plain')84 response = self.client.post(self.rpc_path, data, 'text/plain')
58 self.assertEqual(response.status_code, 200)85 self.assertEqual(response.status_code, 200)
59 xmlrpclib.loads(response.content) # this will throw an exception with bad data86 xmlrpclib.loads(response.content) # this will throw an exception with bad data
60 87
61 # jsonrpc request with xmlrpc data (should be error)88 # jsonrpc request with xmlrpc data (should be error)
62 data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>'89 data = '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>'
63 response = self.client.post(RPCPATH, data, 'application/json')90 response = self.client.post(self.rpc_path, data, 'application/json')
64 self.assertEqual(response.status_code, 200)91 self.assertEqual(response.status_code, 200)
65 jsondict = json.loads(response.content)92 jsondict = json.loads(response.content)
66 self.assertTrue(jsondict['result'] is None)93 self.assertTrue(jsondict['result'] is None)
@@ -69,7 +96,7 @@
69 96
70 data = '{"params":[],"method":"system.listMethods","id":123}'97 data = '{"params":[],"method":"system.listMethods","id":123}'
71 try:98 try:
72 response = self.client.post(RPCPATH, data, 'text/xml')99 response = self.client.post(self.rpc_path, data, 'text/xml')
73 except:100 except:
74 # for some reason, this throws an expat error101 # for some reason, this throws an expat error
75 # but only in python 2.4102 # but only in python 2.4
@@ -83,7 +110,7 @@
83 110
84 def test_badrequests(self):111 def test_badrequests(self):
85 data = '{"params":[],"method":"system.methodHelp","id":456}'112 data = '{"params":[],"method":"system.methodHelp","id":456}'
86 response = self.client.post(RPCPATH, data, 'application/json')113 response = self.client.post(self.rpc_path, data, 'application/json')
87 self.assertEqual(response.status_code, 200)114 self.assertEqual(response.status_code, 200)
88 jsondict = json.loads(response.content)115 jsondict = json.loads(response.content)
89 self.assertTrue(jsondict['error'] is not None)116 self.assertTrue(jsondict['error'] is not None)
@@ -92,7 +119,7 @@
92 self.assertEqual(jsondict['error']['code'], JSONRPC_SERVICE_ERROR)119 self.assertEqual(jsondict['error']['code'], JSONRPC_SERVICE_ERROR)
93 120
94 data = '<?xml version="1.0"?><methodCall><methodName>method.N0t.Exists</methodName><params></params></methodCall>'121 data = '<?xml version="1.0"?><methodCall><methodName>method.N0t.Exists</methodName><params></params></methodCall>'
95 response = self.client.post(RPCPATH, data, 'text/xml')122 response = self.client.post(self.rpc_path, data, 'text/xml')
96 self.assertEqual(response.status_code, 200)123 self.assertEqual(response.status_code, 200)
97 try:124 try:
98 xmlrpclib.loads(response.content)125 xmlrpclib.loads(response.content)
@@ -108,9 +135,42 @@
108 # options requests can only be tested by django 1.1+135 # options requests can only be tested by django 1.1+
109 self.fail('This version of django "%s" does not support http access control' %str(t))136 self.fail('This version of django "%s" does not support http access control' %str(t))
110 137
111 response = self.client.options(RPCPATH, '', 'text/plain')138 response = self.client.options(self.rpc_path, '', 'text/plain')
112 self.assertEqual(response['Access-Control-Allow-Methods'], 'POST, GET, OPTIONS')139 self.assertEqual(response['Access-Control-Allow-Methods'], 'POST, GET, OPTIONS')
113 self.assertEqual(response['Access-Control-Max-Age'], '0')140 self.assertEqual(response['Access-Control-Max-Age'], '0')
114 141
142 def test_good_url_name(self):
143 """
144 Make sure we call functions based on the url they are arriving on.
145 """
146 data = xmlrpclib.dumps((5, 4), "subtract")
147 response = self.client.post(self.rpc_path, data, 'text/xml')
148 self.assertEqual(response.status_code, 200)
149 result, methname = xmlrpclib.loads(response.content)
150 self.assertEqual(result, (1,))
151 self.assertEqual(methname, None)
152
153 data = xmlrpclib.dumps((5, 4), "product")
154 response = self.client.post(self.ns_rpc_path, data, 'text/xml')
155 self.assertEqual(response.status_code, 200)
156 result, methname = xmlrpclib.loads(response.content)
157 self.assertEqual(result, (20,))
158 self.assertEqual(methname, None)
159
160 def test_bad_url_name(self):
161 """
162 Make sure we cannot call functions using the wrong url_name.
163 """
164 data = xmlrpclib.dumps((5, 4), "subtract")
165 response = self.client.post(self.ns_rpc_path, data, 'text/xml')
166
167 self.assertEqual(response.status_code, 200)
168 try:
169 result, methname = xmlrpclib.loads(response.content)
170 self.fail("Expected xmlrpclib Fault")
171 except xmlrpclib.Fault as fault:
172 self.assertEqual(fault.faultCode, 1)
173 self.assertTrue(fault.faultString.endswith('method "subtract" is not supported'))
174
115if __name__ == '__main__':175if __name__ == '__main__':
116 unittest.main()176 unittest.main()
117\ No newline at end of file177\ No newline at end of file
118178
=== added file 'rpc4django/tests/test_urls.py'
--- rpc4django/tests/test_urls.py 1970-01-01 00:00:00 +0000
+++ rpc4django/tests/test_urls.py 2010-11-20 07:51:10 +0000
@@ -0,0 +1,12 @@
1'''
2Created on Jun 9, 2010
3
4@author: jnaous
5'''
6from django.conf.urls.defaults import *
7from rpc4django.utils import rpc_url
8
9urlpatterns = patterns("",
10 rpc_url(r"^RPC2/$", name="serve_rpc_request", use_name_for_dispatch=False),
11 rpc_url(r"^my_url/RPC2/$", name="my_url_name"),
12)
013
=== modified file 'rpc4django/tests/testmod/__init__.py'
--- rpc4django/tests/testmod/__init__.py 2010-03-29 05:04:00 +0000
+++ rpc4django/tests/testmod/__init__.py 2010-11-20 07:51:10 +0000
@@ -5,6 +5,6 @@
5def subtract(a, b):5def subtract(a, b):
6 return a - b6 return a - b
77
8@rpcmethod(signature=['int', 'int', 'int'])8@rpcmethod(signature=['int', 'int', 'int'], url_name="my_url_name")
9def product(a, b):9def product(a, b):
10 return a * b10 return a * b
1111
=== added file 'rpc4django/tests/testmod/models.py'
--- rpc4django/tests/testmod/models.py 1970-01-01 00:00:00 +0000
+++ rpc4django/tests/testmod/models.py 2010-11-20 07:51:10 +0000
@@ -0,0 +1,5 @@
1'''
2Created on Jun 9, 2010
3
4@author: jnaous
5'''
06
=== modified file 'rpc4django/tests/testmod/testsubmod/__init__.py'
--- rpc4django/tests/testmod/testsubmod/__init__.py 2010-03-29 05:04:00 +0000
+++ rpc4django/tests/testmod/testsubmod/__init__.py 2010-11-20 07:51:10 +0000
@@ -3,3 +3,7 @@
3@rpcmethod(signature=['int', 'int', 'int'])3@rpcmethod(signature=['int', 'int', 'int'])
4def power(a, b):4def power(a, b):
5 return a ** b5 return a ** b
6
7@rpcmethod(signature=['int', 'int'], url_name="my_url_name")
8def power2(a):
9 return 2 ** a
610
=== added file 'rpc4django/tests/utils.py'
--- rpc4django/tests/utils.py 1970-01-01 00:00:00 +0000
+++ rpc4django/tests/utils.py 2010-11-20 07:51:10 +0000
@@ -0,0 +1,64 @@
1'''
2Created on Nov 19, 2010
3
4http://djangosnippets.org/snippets/1011/
5
6@author: jnaous
7'''
8from django.conf import settings
9from django.core.management import call_command
10from django.db.models import loading
11from django.test import TestCase
12
13NO_SETTING = ('!', None)
14
15class TestSettingsManager(object):
16 """
17 A class which can modify some Django settings temporarily for a
18 test and then revert them to their original values later.
19
20 Automatically handles resyncing the DB if INSTALLED_APPS is
21 modified.
22
23 """
24 def __init__(self):
25 self._original_settings = {}
26
27 def set(self, **kwargs):
28 for k,v in kwargs.iteritems():
29 self._original_settings.setdefault(k, getattr(settings, k,
30 NO_SETTING))
31 setattr(settings, k, v)
32 if 'INSTALLED_APPS' in kwargs:
33 self.syncdb()
34
35 def syncdb(self):
36 loading.cache.loaded = False
37 call_command('syncdb', verbosity=0)
38
39 def revert(self):
40 for k,v in self._original_settings.iteritems():
41 if v == NO_SETTING:
42 delattr(settings, k)
43 else:
44 setattr(settings, k, v)
45 if 'INSTALLED_APPS' in self._original_settings:
46 self.syncdb()
47 self._original_settings = {}
48
49
50class SettingsTestCase(TestCase):
51 """
52 A subclass of the Django TestCase with a settings_manager
53 attribute which is an instance of TestSettingsManager.
54
55 Comes with a tearDown() method that calls
56 self.settings_manager.revert().
57
58 """
59 def __init__(self, *args, **kwargs):
60 super(SettingsTestCase, self).__init__(*args, **kwargs)
61 self.settings_manager = TestSettingsManager()
62
63 def tearDown(self):
64 self.settings_manager.revert()
065
=== added file 'rpc4django/utils.py'
--- rpc4django/utils.py 1970-01-01 00:00:00 +0000
+++ rpc4django/utils.py 2010-11-20 07:51:10 +0000
@@ -0,0 +1,20 @@
1'''
2Created on Jun 9, 2010
3
4@author: jnaous
5'''
6from django.conf.urls.defaults import url
7
8def rpc_url(regex, kwargs=None, name=None, prefix='',
9 use_name_for_dispatch=True):
10 """
11 Wrapper around C{django.conf.urls.defaults.url} to add the C{name} parameter
12 to C{kwargs} as "url_name", and to automatically assign the C{view}.
13 C{use_name_for_dispatch} controls whether or not to add C{name} to C{kwargs}
14 """
15 if name != None and use_name_for_dispatch:
16 if kwargs == None:
17 kwargs = {}
18 kwargs["url_name"] = name
19 view = "rpc4django.views.serve_rpc_request"
20 return url(regex, view, kwargs, name, prefix)
021
=== modified file 'rpc4django/views.py'
--- rpc4django/views.py 2010-10-27 04:36:20 +0000
+++ rpc4django/views.py 2010-11-20 07:51:10 +0000
@@ -1,23 +1,23 @@
1'''1'''
2The main entry point for RPC4Django. Usually, the user simply puts2The main entry point for RPC4Django. Usually, the user simply puts
3:meth:`serve_rpc_request <rpc4django.views.serve_rpc_request>` into ``urls.py``3:meth:`rpc_url <rpc4django.utils.rpc_url>` into ``urls.py``
44
5::5::
66
7 urlpatterns = patterns('', 7 urlpatterns = patterns('',
8 # rpc4django will need to be in your Python path 8 # rpc4django will need to be in your Python path
9 (r'^RPC2$', 'rpc4django.views.serve_rpc_request'), 9 rpc_url(r'^RPC2$', name="my_url_name"),
10 )10 )
11 11
12'''12'''
1313
14import logging14import logging
15import types
15from xml.dom.minidom import parseString16from xml.dom.minidom import parseString
16from xml.parsers.expat import ExpatError17from xml.parsers.expat import ExpatError
17from django.http import HttpResponse, Http404, HttpResponseForbidden18from django.http import HttpResponse, Http404, HttpResponseForbidden
18from django.shortcuts import render_to_response19from django.shortcuts import render_to_response
19from django.conf import settings20from django.conf import settings
20from django.core.urlresolvers import reverse, NoReverseMatch
21from rpcdispatcher import RPCDispatcher21from rpcdispatcher import RPCDispatcher
22from __init__ import version22from __init__ import version
2323
@@ -45,7 +45,25 @@
45# these will be scanned for @rpcmethod decorators45# these will be scanned for @rpcmethod decorators
46APPS = getattr(settings, 'INSTALLED_APPS', [])46APPS = getattr(settings, 'INSTALLED_APPS', [])
4747
48def check_request_permission(request, request_format='xml'):48logger = logging.getLogger("rpc4django.views")
49
50class NonExistingDispatcher(Exception):
51 """Raised when the dispatcher for a particular name is not found."""
52 def __init__(self, path, url_name):
53 super(NonExistingDispatcher, self).__init__(
54 "URL name '%s' is not used in any rpcmethod,\
55 however, the URL '%s' uses it." % (url_name, path))
56 self.path = path
57 self.url_name = url_name
58
59def get_dispatcher(path, url_name):
60 try:
61 dispatcher = dispatchers[url_name]
62 except KeyError:
63 raise NonExistingDispatcher(path, url_name)
64 return dispatcher
65
66def check_request_permission(request, request_format='xml', url_name="root"):
49 '''67 '''
50 Checks whether this user has permission to call a particular method68 Checks whether this user has permission to call a particular method
51 This method does not check method call validity. That is done later69 This method does not check method call validity. That is done later
@@ -54,11 +72,13 @@
54 72
55 - ``request`` - a django HttpRequest object73 - ``request`` - a django HttpRequest object
56 - ``request_format`` - the request type: 'json' or 'xml' 74 - ``request_format`` - the request type: 'json' or 'xml'
75 - ``url_name`` - the name of the url at which this request was received
57 76
58 Returns ``False`` if permission is denied and ``True`` otherwise77 Returns ``False`` if permission is denied and ``True`` otherwise
59 '''78 '''
60 79
61 user = getattr(request, 'user', None)80 user = getattr(request, 'user', None)
81 dispatcher = get_dispatcher(request.path, url_name)
62 methods = dispatcher.list_methods()82 methods = dispatcher.list_methods()
63 method_name = dispatcher.get_method_name(request.raw_post_data, \83 method_name = dispatcher.get_method_name(request.raw_post_data, \
64 request_format)84 request_format)
@@ -69,20 +89,20 @@
69 # this is the method the user is calling89 # this is the method the user is calling
70 # time to check the permissions90 # time to check the permissions
71 if method.permission is not None:91 if method.permission is not None:
72 logging.debug('Method "%s" is protected by permission "%s"' \92 logger.debug('Method "%s" is protected by permission "%s"' \
73 %(method.name, method.permission))93 %(method.name, method.permission))
74 if user is None:94 if user is None:
75 # user is only none if not using AuthenticationMiddleware95 # user is only none if not using AuthenticationMiddleware
76 logging.warn('AuthenticationMiddleware is not enabled')96 logger.warn('AuthenticationMiddleware is not enabled')
77 response = False97 response = False
78 elif not user.has_perm(method.permission):98 elif not user.has_perm(method.permission):
79 # check the permission against the permission database99 # check the permission against the permission database
80 logging.info('User "%s" is NOT authorized' %(str(user)))100 logger.info('User "%s" is NOT authorized' %(str(user)))
81 response = False101 response = False
82 else:102 else:
83 logging.debug('User "%s" is authorized' %(str(user)))103 logger.debug('User "%s" is authorized' %(str(user)))
84 else:104 else:
85 logging.debug('Method "%s" is unprotected' %(method.name))105 logger.debug('Method "%s" is unprotected' %(method.name))
86 106
87 break107 break
88 108
@@ -111,8 +131,8 @@
111 return False131 return False
112 132
113 if LOG_REQUESTS_RESPONSES:133 if LOG_REQUESTS_RESPONSES:
114 logging.info('Unrecognized content-type "%s"' %conttype)134 logger.info('Unrecognized content-type "%s"' %conttype)
115 logging.info('Analyzing rpc request data to get content type')135 logger.info('Analyzing rpc request data to get content type')
116 136
117 # analyze post data to see whether it is xml or json137 # analyze post data to see whether it is xml or json
118 # this is slower than if the content-type was set properly138 # this is slower than if the content-type was set properly
@@ -124,7 +144,7 @@
124 144
125 return False145 return False
126146
127def serve_rpc_request(request):147def serve_rpc_request(request, url_name="root", **kwargs):
128 '''148 '''
129 Handles rpc calls based on the content type of the request or149 Handles rpc calls based on the content type of the request or
130 returns the method documentation page if the request150 returns the method documentation page if the request
@@ -134,38 +154,42 @@
134 154
135 ``request``155 ``request``
136 the Django HttpRequest object156 the Django HttpRequest object
137 157 ``url_name``
158 the name of the url at which the request arrived
159
138 '''160 '''
139161
162 dispatcher = get_dispatcher(request.path, url_name)
163
140 if request.method == "POST" and len(request.POST) > 0:164 if request.method == "POST" and len(request.POST) > 0:
141 # Handle POST request with RPC payload165 # Handle POST request with RPC payload
142 166
143 if LOG_REQUESTS_RESPONSES:167 if LOG_REQUESTS_RESPONSES:
144 logging.debug('Incoming request: %s' %str(request.raw_post_data))168 logger.debug('Incoming request: %s' % str(request.raw_post_data))
145 169
146 if is_xmlrpc_request(request):170 if is_xmlrpc_request(request):
147 if RESTRICT_XML:171 if RESTRICT_XML:
148 raise Http404172 raise Http404
149 173
150 if not check_request_permission(request, 'xml'):174 if not check_request_permission(request, 'xml', url_name=url_name):
151 return HttpResponseForbidden()175 return HttpResponseForbidden()
152 176
153 resp = dispatcher.xmldispatch(request.raw_post_data, \177 resp = dispatcher.xmldispatch(request.raw_post_data,
154 request=request)178 request=request, **kwargs)
155 response_type = 'text/xml'179 response_type = 'text/xml'
156 else:180 else:
157 if RESTRICT_JSON:181 if RESTRICT_JSON:
158 raise Http404182 raise Http404
159 183
160 if not check_request_permission(request, 'json'):184 if not check_request_permission(request, 'json', url_name=url_name):
161 return HttpResponseForbidden()185 return HttpResponseForbidden()
162 186
163 resp = dispatcher.jsondispatch(request.raw_post_data, \187 resp = dispatcher.jsondispatch(request.raw_post_data,
164 request=request)188 request=request, **kwargs)
165 response_type = 'application/json'189 response_type = 'application/json'
166 190
167 if LOG_REQUESTS_RESPONSES:191 if LOG_REQUESTS_RESPONSES:
168 logging.debug('Outgoing %s response: %s' %(response_type, resp))192 logger.debug('Outgoing %s response: %s' %(response_type, resp))
169 193
170 return HttpResponse(resp, response_type)194 return HttpResponse(resp, response_type)
171 elif request.method == 'OPTIONS':195 elif request.method == 'OPTIONS':
@@ -185,7 +209,7 @@
185 request.META.get('HTTP_ACCESS_CONTROL_REQUEST_HEADERS', '')209 request.META.get('HTTP_ACCESS_CONTROL_REQUEST_HEADERS', '')
186 210
187 if LOG_REQUESTS_RESPONSES:211 if LOG_REQUESTS_RESPONSES:
188 logging.debug('Outgoing HTTP access response to: %s' %(origin))212 logger.debug('Outgoing HTTP access response to: %s' %(origin))
189 213
190 return response214 return response
191 else:215 else:
@@ -199,7 +223,7 @@
199 methods = dispatcher.list_methods()223 methods = dispatcher.list_methods()
200 template_data = {224 template_data = {
201 'methods': methods,225 'methods': methods,
202 'url': URL,226 'url': request.path,
203 227
204 # rpc4django version228 # rpc4django version
205 'version': version(),229 'version': version(),
@@ -225,13 +249,41 @@
225if csrf_exempt is not None:249if csrf_exempt is not None:
226 serve_rpc_request = csrf_exempt(serve_rpc_request)250 serve_rpc_request = csrf_exempt(serve_rpc_request)
227251
228# reverse the method for use with system.describe and ajax252def _register_rpcmethods(apps, restrict_introspection=False, restrict_ootb_auth=True, dispatchers={}):
229try:253 '''
230 URL = reverse(serve_rpc_request)254 Scans the installed apps for methods with the rpcmethod decorator
231except NoReverseMatch:255 Adds these methods to the list of methods callable via RPC
232 URL = ''256 '''
233 257
234# instantiate the rpcdispatcher -- this examines the INSTALLED_APPS258 for appname in apps:
235# for any @rpcmethod decorators and adds them to the callable methods259 # check each app for any rpcmethods
236dispatcher = RPCDispatcher(URL, APPS, RESTRICT_INTROSPECTION, RESTRICT_OOTB_AUTH) 260 app = __import__(appname, globals(), locals(), ['*'])
261 for obj in dir(app):
262 method = getattr(app, obj)
263 if callable(method) and \
264 getattr(method, 'is_rpcmethod', False) == True:
265 # if this method is callable and it has the rpcmethod
266 # decorator, add it to the dispatcher
267 if method.url_name not in dispatchers:
268 logger.debug("Registered URL name '%s'" % method.url_name)
269 dispatchers[method.url_name] = RPCDispatcher(
270 method.url_name, restrict_introspection,
271 restrict_ootb_auth)
272 logger.debug(
273 "Registered method '%s' to URL name '%s'"
274 % (method.external_name, method.url_name))
275 dispatchers[method.url_name].register_method(
276 method, method.external_name)
277 elif isinstance(method, types.ModuleType):
278 # if this is not a method and instead a sub-module,
279 # scan the module for methods with @rpcmethod
280 try:
281 _register_rpcmethods(
282 ["%s.%s" % (appname, obj)],
283 restrict_introspection, restrict_ootb_auth, dispatchers)
284 except ImportError:
285 pass
286 return dispatchers
287
288dispatchers = _register_rpcmethods(APPS, RESTRICT_INTROSPECTION, RESTRICT_OOTB_AUTH)
237289

Subscribers

People subscribed via source and target branches

to all changes: