Merge lp:~openerp-dev/openerp-web/7.0-controller-extension-xmo into lp:openerp-web/7.0

Proposed by Antony Lesuisse (OpenERP)
Status: Merged
Merged at revision: 3904
Proposed branch: lp:~openerp-dev/openerp-web/7.0-controller-extension-xmo
Merge into: lp:openerp-web/7.0
Diff against target: 584 lines (+446/-23)
5 files modified
addons/web/controllers/main.py (+15/-9)
addons/web/http.py (+51/-9)
addons/web/tests/__init__.py (+2/-1)
addons/web/tests/test_dispatch.py (+373/-0)
addons/web_diagram/controllers/main.py (+5/-4)
To merge this branch: bzr merge lp:~openerp-dev/openerp-web/7.0-controller-extension-xmo
Reviewer Review Type Date Requested Status
OpenERP Core Team Pending
Review via email: mp+157151@code.launchpad.net
To post a comment you must log in.
3880. By Xavier (Open ERP)

[FIX] Extension of controller in-place with explicit spec of same _cp_path

When extending a controller in-place (e.g. A(Controller), B(A)) and
providing the exact same _cp_path as parent (no-op) execution path
would go into handler for _cp_path overwriting and raise an assertion
error for overwriting of existing controller.

Except this is allowed (if ugly) pattern, so warn & ignore behavior
(it is harmless).

3881. By Xavier (Open ERP)

[IMP] only use name of class being created: module is incorrect for dynamically created classes

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'addons/web/controllers/main.py'
--- addons/web/controllers/main.py 2013-04-05 13:47:47 +0000
+++ addons/web/controllers/main.py 2013-04-18 09:47:37 +0000
@@ -1,11 +1,9 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2
3import ast2import ast
4import base643import base64
5import csv4import csv
6import glob5import glob
7import itertools6import itertools
8import logging
9import operator7import operator
10import datetime8import datetime
11import hashlib9import hashlib
@@ -30,7 +28,6 @@
30 xlwt = None28 xlwt = None
3129
32import openerp30import openerp
33import openerp.modules.registry
34from openerp.tools.translate import _31from openerp.tools.translate import _
3532
36from .. import http33from .. import http
@@ -1439,7 +1436,8 @@
1439 else:1436 else:
1440 return False1437 return False
14411438
1442class Export(View):1439
1440class Export(openerpweb.Controller):
1443 _cp_path = "/web/export"1441 _cp_path = "/web/export"
14441442
1445 @openerpweb.jsonrequest1443 @openerpweb.jsonrequest
@@ -1580,7 +1578,13 @@
1580 (prefix + '/' + k, prefix_string + '/' + v)1578 (prefix + '/' + k, prefix_string + '/' + v)
1581 for k, v in self.fields_info(req, model, export_fields).iteritems())1579 for k, v in self.fields_info(req, model, export_fields).iteritems())
15821580
1583 #noinspection PyPropertyDefinition1581
1582class ExportFormat(object):
1583 """
1584 Superclass for export formats, should probably be an abc and have a way to
1585 generate _cp_path from fmt but it's a pain to deal with conflicts with
1586 ControllerType
1587 """
1584 @property1588 @property
1585 def content_type(self):1589 def content_type(self):
1586 """ Provides the format's content type """1590 """ Provides the format's content type """
@@ -1621,14 +1625,14 @@
1621 else:1625 else:
1622 columns_headers = [val['label'].strip() for val in fields]1626 columns_headers = [val['label'].strip() for val in fields]
16231627
1624
1625 return req.make_response(self.from_data(columns_headers, import_data),1628 return req.make_response(self.from_data(columns_headers, import_data),
1626 headers=[('Content-Disposition',1629 headers=[('Content-Disposition',
1627 content_disposition(self.filename(model), req)),1630 content_disposition(self.filename(model), req)),
1628 ('Content-Type', self.content_type)],1631 ('Content-Type', self.content_type)],
1629 cookies={'fileToken': int(token)})1632 cookies={'fileToken': int(token)})
16301633
1631class CSVExport(Export):1634
1635class CSVExport(ExportFormat, http.Controller):
1632 _cp_path = '/web/export/csv'1636 _cp_path = '/web/export/csv'
1633 fmt = {'tag': 'csv', 'label': 'CSV'}1637 fmt = {'tag': 'csv', 'label': 'CSV'}
16341638
@@ -1663,7 +1667,8 @@
1663 fp.close()1667 fp.close()
1664 return data1668 return data
16651669
1666class ExcelExport(Export):1670
1671class ExcelExport(ExportFormat, http.Controller):
1667 _cp_path = '/web/export/xls'1672 _cp_path = '/web/export/xls'
1668 fmt = {1673 fmt = {
1669 'tag': 'xls',1674 'tag': 'xls',
@@ -1702,7 +1707,8 @@
1702 fp.close()1707 fp.close()
1703 return data1708 return data
17041709
1705class Reports(View):1710
1711class Reports(openerpweb.Controller):
1706 _cp_path = "/web/report"1712 _cp_path = "/web/report"
1707 POLLING_DELAY = 0.251713 POLLING_DELAY = 0.25
1708 TYPES_MAPPING = {1714 TYPES_MAPPING = {
17091715
=== modified file 'addons/web/http.py'
--- addons/web/http.py 2013-03-01 17:16:16 +0000
+++ addons/web/http.py 2013-04-18 09:47:37 +0000
@@ -354,18 +354,64 @@
354#----------------------------------------------------------354#----------------------------------------------------------
355addons_module = {}355addons_module = {}
356addons_manifest = {}356addons_manifest = {}
357controllers_class = []357controllers_class = {}
358controllers_object = {}
359controllers_path = {}358controllers_path = {}
360359
361class ControllerType(type):360class ControllerType(type):
362 def __init__(cls, name, bases, attrs):361 def __init__(cls, name, bases, attrs):
363 super(ControllerType, cls).__init__(name, bases, attrs)362 super(ControllerType, cls).__init__(name, bases, attrs)
364 controllers_class.append(("%s.%s" % (cls.__module__, cls.__name__), cls))363 # Only for root "Controller"
364 if bases == (object,):
365 assert name == 'Controller'
366 return
367
368 path = attrs.get('_cp_path')
369 if Controller in bases:
370 assert path, "Controller subclass %s missing a _cp_path" % name
371 else:
372 parent_paths = set(base._cp_path for base in bases
373 if issubclass(base, Controller))
374 assert len(parent_paths) == 1,\
375 "%s inheriting from multiple controllers is not supported" % (
376 name)
377 [parent_path] = parent_paths
378 [parent] = [
379 controller for controller in controllers_class.itervalues()
380 if controller._cp_path == parent_path]
381
382 # inherit from a Controller subclass
383 if path:
384 # if extending in place with same URL, ignore URL
385 if parent_path == path:
386 _logger.warn(
387 "Controller %s extending %s in-place should not "
388 "explicitly specify URL", name, parent)
389 return
390 _logger.warn("Re-exposing %s at %s.\n"
391 "\tThis usage is unsupported.",
392 parent.__name__,
393 attrs['_cp_path'])
394
395 if path:
396 assert path not in controllers_class,\
397 "Trying to expose %s at the same URL as %s" % (
398 name, controllers_class[path])
399 controllers_class[path] = cls
400
365401
366class Controller(object):402class Controller(object):
367 __metaclass__ = ControllerType403 __metaclass__ = ControllerType
368404
405 def __new__(cls, *args, **kwargs):
406 subclasses = [c for c in cls.__subclasses__()
407 if c._cp_path == cls._cp_path]
408 if subclasses:
409 name = "%s (+%s)" % (
410 cls.__name__,
411 '+'.join(sub.__name__ for sub in subclasses))
412 cls = type(name, tuple(reversed(subclasses)), {})
413 return object.__new__(cls)
414
369#----------------------------------------------------------415#----------------------------------------------------------
370# Session context manager416# Session context manager
371#----------------------------------------------------------417#----------------------------------------------------------
@@ -558,12 +604,8 @@
558 addons_manifest[module] = manifest604 addons_manifest[module] = manifest
559 self.statics['/%s/static' % module] = path_static605 self.statics['/%s/static' % module] = path_static
560606
561 for k, v in controllers_class:607 for c in controllers_class.itervalues():
562 if k not in controllers_object:608 controllers_path[c._cp_path] = c()
563 o = v()
564 controllers_object[k] = o
565 if hasattr(o, '_cp_path'):
566 controllers_path[o._cp_path] = o
567609
568 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)610 app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics)
569 self.dispatch = DisableCacheMiddleware(app)611 self.dispatch = DisableCacheMiddleware(app)
570612
=== modified file 'addons/web/tests/__init__.py'
--- addons/web/tests/__init__.py 2012-11-26 14:05:25 +0000
+++ addons/web/tests/__init__.py 2013-04-18 09:47:37 +0000
@@ -1,9 +1,10 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2from . import test_dataset, test_menu, test_serving_base, test_js2from . import test_dataset, test_menu, test_serving_base, test_js, test_dispatch
33
4fast_suite = []4fast_suite = []
5checks = [5checks = [
6 test_dataset,6 test_dataset,
7 test_menu,7 test_menu,
8 test_serving_base,8 test_serving_base,
9 test_dispatch,
9]10]
1011
=== added file 'addons/web/tests/test_dispatch.py'
--- addons/web/tests/test_dispatch.py 1970-01-01 00:00:00 +0000
+++ addons/web/tests/test_dispatch.py 2013-04-18 09:47:37 +0000
@@ -0,0 +1,373 @@
1# -*- coding: utf-8 -*-
2import contextlib
3import json
4import logging
5import logging.handlers
6import types
7import unittest2
8
9from .. import http
10import werkzeug.test
11
12
13def setUpModule():
14 """
15 Force load_addons once to import all the crap we don't care for as this
16 thing is full of side-effects
17 """
18 http.Root().load_addons()
19
20class DispatchCleanup(unittest2.TestCase):
21 """
22 Cleans up controllers registries in the web client so it's possible to
23 test controllers registration and dispatching in isolation.
24 """
25 def setUp(self):
26 self.classes = http.controllers_class
27 self.paths = http.controllers_path
28
29 http.controllers_class = {}
30 http.controllers_path = {}
31
32 def tearDown(self):
33 http.controllers_path = self.paths
34 http.controllers_class = self.classes
35
36
37def jsonrpc_body(params=None):
38 """
39 Builds and dumps the body of a JSONRPC request with params ``params``
40 """
41 return json.dumps({
42 'jsonrpc': '2.0',
43 'method': 'call',
44 'id': None,
45 'params': params or {},
46 })
47
48
49def jsonrpc_response(result=None):
50 """
51 Builds a JSONRPC response (as a Python dict) with result ``result``
52 """
53 return {
54 u'jsonrpc': u'2.0',
55 u'id': None,
56 u'result': result,
57 }
58
59
60class TestHandler(logging.handlers.BufferingHandler):
61 def __init__(self):
62 logging.handlers.BufferingHandler.__init__(self, 0)
63
64 def shouldFlush(self, record):
65 return False
66
67@contextlib.contextmanager
68def capture_logging(logger, level=logging.DEBUG):
69 logger = logging.getLogger(logger)
70 old_level = logger.level
71 old_handlers = logger.handlers
72 old_propagate = logger.propagate
73
74 test_handler = TestHandler()
75 logger.handlers = [test_handler]
76 logger.setLevel(level)
77 logger.propagate = False
78
79 try:
80 yield test_handler
81 finally:
82 logger.propagate = old_propagate
83 logger.setLevel(old_level)
84 logger.handlers = old_handlers
85
86
87class TestDispatching(DispatchCleanup):
88 def setUp(self):
89 super(TestDispatching, self).setUp()
90 self.app = http.Root()
91 self.client = werkzeug.test.Client(self.app)
92
93 def test_not_exposed(self):
94 class CatController(http.Controller):
95 _cp_path = '/cat'
96
97 def index(self):
98 return 'Blessid iz da feline'
99
100 self.app.load_addons()
101
102 body, status, headers = self.client.get('/cat')
103 self.assertEqual('404 NOT FOUND', status)
104
105 def test_basic_http(self):
106 class CatController(http.Controller):
107 _cp_path = '/cat'
108
109 @http.httprequest
110 def index(self, req):
111 return 'no walk in counsil of wickid,'
112
113 self.app.load_addons()
114
115 body, status, headers = self.client.get('/cat')
116 self.assertEqual('200 OK', status)
117 self.assertEqual('no walk in counsil of wickid,', ''.join(body))
118
119 def test_basic_jsonrpc(self):
120 class CatController(http.Controller):
121 _cp_path = '/cat'
122
123 @http.jsonrequest
124 def index(self, req):
125 return 'no place paws in path of da sinnerz,'
126 self.app.load_addons()
127
128 body, status, headers = self.client.post('/cat', data=jsonrpc_body())
129
130 self.assertEqual('200 OK', status)
131 self.assertEqual(
132 jsonrpc_response('no place paws in path of da sinnerz,'),
133 json.loads(''.join(body)))
134
135
136class TestSubclassing(DispatchCleanup):
137 def setUp(self):
138 super(TestSubclassing, self).setUp()
139 self.app = http.Root()
140 self.client = werkzeug.test.Client(self.app)
141
142 def test_add_method(self):
143 class CatController(http.Controller):
144 _cp_path = '/cat'
145
146 @http.httprequest
147 def index(self, req):
148 return 'no sit and purr with da mockerz.'
149
150 class CeilingController(CatController):
151 @http.httprequest
152 def lol(self, req):
153 return 'But der delightz in lawz of Ceiling Cat,'
154
155 self.app.load_addons()
156
157 body, status, headers = self.client.get('/cat')
158 self.assertEqual('200 OK', status)
159 self.assertEqual('no sit and purr with da mockerz.', ''.join(body))
160 body, status, headers = self.client.get('/cat/lol')
161 self.assertEqual('200 OK', status)
162 self.assertEqual('But der delightz in lawz of Ceiling Cat,',
163 ''.join(body))
164
165 def test_override_method(self):
166 class CatController(http.Controller):
167 _cp_path = '/cat'
168
169 @http.httprequest
170 def index(self, req):
171 return 'an ponderz'
172
173 class CeilingController(CatController):
174 @http.httprequest
175 def index(self, req):
176 return '%s much.' % super(CeilingController, self).index(req)
177
178 self.app.load_addons()
179
180 body, status, headers = self.client.get('/cat')
181 self.assertEqual('200 OK', status)
182 self.assertEqual('an ponderz much.', ''.join(body))
183
184 def test_make_invisible(self):
185 class CatController(http.Controller):
186 _cp_path = '/cat'
187
188 @http.httprequest
189 def index(self, req):
190 return 'Tehy liek treez bai teh waterz,'
191
192 class CeilingController(CatController):
193 def index(self, req):
194 return super(CeilingController, self).index(req)
195
196 self.app.load_addons()
197
198 body, status, headers = self.client.get('/cat')
199 self.assertEqual('404 NOT FOUND', status)
200
201 def test_make_json_invisible(self):
202 class CatController(http.Controller):
203 _cp_path = '/cat'
204
205 @http.jsonrequest
206 def index(self, req):
207 return 'Tehy liek treez bai teh waterz,'
208
209 class CeilingController(CatController):
210 def index(self, req):
211 return super(CeilingController, self).index(req)
212
213 self.app.load_addons()
214
215 body, status, headers = self.client.post('/cat')
216 self.assertEqual('404 NOT FOUND', status)
217
218 def test_extends(self):
219 """
220 When subclassing an existing Controller new classes are "merged" into
221 the base one
222 """
223 class A(http.Controller):
224 _cp_path = '/foo'
225 @http.httprequest
226 def index(self, req):
227 return '1'
228
229 class B(A):
230 @http.httprequest
231 def index(self, req):
232 return "%s 2" % super(B, self).index(req)
233
234 class C(A):
235 @http.httprequest
236 def index(self, req):
237 return "%s 3" % super(C, self).index(req)
238
239 self.app.load_addons()
240
241 body, status, headers = self.client.get('/foo')
242 self.assertEqual('200 OK', status)
243 self.assertEqual('1 2 3', ''.join(body))
244
245 def test_extends_same_path(self):
246 """
247 When subclassing an existing Controller and specifying the same
248 _cp_path as the parent, ???
249 """
250 class A(http.Controller):
251 _cp_path = '/foo'
252 @http.httprequest
253 def index(self, req):
254 return '1'
255
256 class B(A):
257 _cp_path = '/foo'
258 @http.httprequest
259 def index(self, req):
260 return '2'
261
262 self.app.load_addons()
263
264 body, status, headers = self.client.get('/foo')
265 self.assertEqual('200 OK', status)
266 self.assertEqual('2', ''.join(body))
267
268 def test_re_expose(self):
269 """
270 An existing Controller should not be extended with a new cp_path
271 (re-exposing somewhere else)
272 """
273 class CatController(http.Controller):
274 _cp_path = '/cat'
275
276 @http.httprequest
277 def index(self, req):
278 return '[%s]' % self.speak()
279
280 def speak(self):
281 return 'Yu ordered cheezburgerz,'
282
283 with capture_logging('openerp.addons.web.http') as handler:
284 class DogController(CatController):
285 _cp_path = '/dog'
286
287 def speak(self):
288 return 'Woof woof woof woof'
289
290 [record] = handler.buffer
291 self.assertEqual(logging.WARN, record.levelno)
292 self.assertEqual("Re-exposing CatController at /dog.\n"
293 "\tThis usage is unsupported.",
294 record.getMessage())
295
296 def test_fail_redefine(self):
297 """
298 An existing Controller can't be overwritten by a new one on the same
299 path (? or should this generate a warning and still work as if it was
300 an extend?)
301 """
302 class FooController(http.Controller):
303 _cp_path = '/foo'
304
305 with self.assertRaises(AssertionError):
306 class BarController(http.Controller):
307 _cp_path = '/foo'
308
309 def test_fail_no_path(self):
310 """
311 A Controller must have a path (and thus be exposed)
312 """
313 with self.assertRaises(AssertionError):
314 class FooController(http.Controller):
315 pass
316
317 def test_mixin(self):
318 """
319 Can mix "normal" python classes into a controller directly
320 """
321 class Mixin(object):
322 @http.httprequest
323 def index(self, req):
324 return 'ok'
325
326 class FooController(http.Controller, Mixin):
327 _cp_path = '/foo'
328
329 class BarContoller(Mixin, http.Controller):
330 _cp_path = '/bar'
331
332 self.app.load_addons()
333
334 body, status, headers = self.client.get('/foo')
335 self.assertEqual('200 OK', status)
336 self.assertEqual('ok', ''.join(body))
337
338 body, status, headers = self.client.get('/bar')
339 self.assertEqual('200 OK', status)
340 self.assertEqual('ok', ''.join(body))
341
342 def test_mixin_extend(self):
343 """
344 Can mix "normal" python class into a controller by extension
345 """
346 class FooController(http.Controller):
347 _cp_path = '/foo'
348
349 class M1(object):
350 @http.httprequest
351 def m1(self, req):
352 return 'ok 1'
353
354 class M2(object):
355 @http.httprequest
356 def m2(self, req):
357 return 'ok 2'
358
359 class AddM1(FooController, M1):
360 pass
361
362 class AddM2(M2, FooController):
363 pass
364
365 self.app.load_addons()
366
367 body, status, headers = self.client.get('/foo/m1')
368 self.assertEqual('200 OK', status)
369 self.assertEqual('ok 1', ''.join(body))
370
371 body, status, headers = self.client.get('/foo/m2')
372 self.assertEqual('200 OK', status)
373 self.assertEqual('ok 2', ''.join(body))
0374
=== modified file 'addons/web_diagram/controllers/main.py'
--- addons/web_diagram/controllers/main.py 2012-12-06 09:51:23 +0000
+++ addons/web_diagram/controllers/main.py 2013-04-18 09:47:37 +0000
@@ -1,9 +1,10 @@
1import openerp1from openerp.addons.web.http import Controller, jsonrequest
22
3class DiagramView(openerp.addons.web.controllers.main.View):3
4class Diagram(Controller):
4 _cp_path = "/web_diagram/diagram"5 _cp_path = "/web_diagram/diagram"
56
6 @openerp.addons.web.http.jsonrequest7 @jsonrequest
7 def get_diagram_info(self, req, id, model, node, connector,8 def get_diagram_info(self, req, id, model, node, connector,
8 src_node, des_node, label, **kw):9 src_node, des_node, label, **kw):
910