diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index ae04dcf5853..62ecd605223 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -77,7 +77,9 @@ class ir_http(osv.AbstractModel): # what if error in security.check() # -> res_users.check() # -> res_users.check_credentials() - except Exception: + except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException): + # All other exceptions mean undetermined status (e.g. connection pool full), + # let them bubble up request.session.logout() getattr(self, "_auth_method_%s" % auth_method)() return auth_method diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index d119c0344b4..634152f6e01 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -1117,6 +1117,10 @@ class ir_model_data(osv.osv): # Don't remove the LOG_ACCESS_COLUMNS unless _log_access # has been turned off on the model. field = self.pool[model].browse(cr, uid, [res_id], context=context)[0] + if not field.exists(): + _logger.info('Deleting orphan external_ids %s', external_ids) + self.unlink(cr, uid, external_ids) + continue if field.name in openerp.osv.orm.LOG_ACCESS_COLUMNS and self.pool[field.model]._log_access: continue if field.name == 'id': diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 590ce90188f..e9e0386b4e2 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -32,7 +32,7 @@ class QWebException(Exception): class QWebTemplateNotFound(QWebException): pass -def convert_to_qweb_exception(etype=None, **kw): +def raise_qweb_exception(etype=None, **kw): if etype is None: etype = QWebException orig_type, original, tb = sys.exc_info() @@ -43,7 +43,7 @@ def convert_to_qweb_exception(etype=None, **kw): e.qweb[k] = v # Will use `raise foo from bar` in python 3 and rename cause to __cause__ e.qweb['cause'] = original - return e + raise class QWebContext(dict): def __init__(self, cr, uid, data, loader=None, templates=None, context=None): @@ -166,7 +166,7 @@ class QWeb(orm.AbstractModel): try: xml_doc = qwebcontext.loader(name) except ValueError: - raise convert_to_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template) + raise_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template) self.load_document(xml_doc, qwebcontext=qwebcontext) if name in qwebcontext.templates: @@ -179,7 +179,7 @@ class QWeb(orm.AbstractModel): return qwebcontext.safe_eval(expr) except Exception: template = qwebcontext.get('__template__') - raise convert_to_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template) + raise_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template) def eval_object(self, expr, qwebcontext): return self.eval(expr, qwebcontext) @@ -207,7 +207,7 @@ class QWeb(orm.AbstractModel): return str(expr % qwebcontext) except Exception: template = qwebcontext.get('__template__') - raise convert_to_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template) + raise_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template) def eval_bool(self, expr, qwebcontext): return int(bool(self.eval(expr, qwebcontext))) @@ -292,7 +292,7 @@ class QWeb(orm.AbstractModel): raise except Exception: template = qwebcontext.get('__template__') - raise convert_to_qweb_exception(message="Could not render element %r" % element.nodeName, node=element, template=template) + raise_qweb_exception(message="Could not render element %r" % element.nodeName, node=element, template=template) name = str(element.nodeName) inner = "".join(g_inner) trim = template_attributes.get("trim", 0) @@ -611,6 +611,9 @@ class DateTimeConverter(osv.AbstractModel): strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format)) pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale) + if options and options.get('hide_seconds'): + pattern = pattern.replace(":ss", "").replace(":s", "") + return babel.dates.format_datetime(value, format=pattern, locale=locale) class TextConverter(osv.AbstractModel): diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index d4655bf8b9c..360c5e37b31 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -728,9 +728,26 @@ class view(osv.osv): def clear_cache(self): self.read_template.clear_cache(self) + def _contains_branded(self, node): + return node.tag == 't'\ + or 't-raw' in node.attrib\ + or any(self.is_node_branded(child) for child in node.iterdescendants()) + + def _pop_view_branding(self, element): + distributed_branding = dict( + (attribute, element.attrib.pop(attribute)) + for attribute in MOVABLE_BRANDING + if element.get(attribute)) + return distributed_branding + def distribute_branding(self, e, branding=None, parent_xpath='', index_map=misc.ConstantMapping(1)): if e.get('t-ignore') or e.tag == 'head': + # remove any view branding possibly injected by inheritance + attrs = set(MOVABLE_BRANDING) + for descendant in e.iterdescendants(tag=etree.Element): + if not attrs.intersection(descendant.attrib): continue + self._pop_view_branding(descendant) # TODO: find a better name and check if we have a string to boolean helper return @@ -742,15 +759,15 @@ class view(osv.osv): e.set('data-oe-xpath', node_path) if not e.get('data-oe-model'): return - # if a branded element contains branded elements distribute own - # branding to children unless it's t-raw, then just remove branding - # on current element - if e.tag == 't' or 't-raw' in e.attrib or \ - any(self.is_node_branded(child) for child in e.iterdescendants()): - distributed_branding = dict( - (attribute, e.attrib.pop(attribute)) - for attribute in MOVABLE_BRANDING - if e.get(attribute)) + if set(('t-esc', 't-escf', 't-raw', 't-rawf')).intersection(e.attrib): + # nodes which fully generate their content and have no reason to + # be branded because they can not sensibly be edited + self._pop_view_branding(e) + elif self._contains_branded(e): + # if a branded element contains branded elements distribute own + # branding to children unless it's t-raw, then just remove branding + # on current element + distributed_branding = self._pop_view_branding(e) if 't-raw' not in e.attrib: # TODO: collections.Counter if remove p2.6 compat @@ -760,11 +777,12 @@ class view(osv.osv): if child.get('data-oe-xpath'): # injected by view inheritance, skip otherwise # generated xpath is incorrect - continue - indexes[child.tag] += 1 - self.distribute_branding(child, distributed_branding, - parent_xpath=node_path, - index_map=indexes) + self.distribute_branding(child) + else: + indexes[child.tag] += 1 + self.distribute_branding( + child, distributed_branding, + parent_xpath=node_path, index_map=indexes) def is_node_branded(self, node): """ Finds out whether a node is branded or qweb-active (bears a diff --git a/openerp/addons/base/res/res_company.py b/openerp/addons/base/res/res_company.py index ae83aa4f2d1..64ab5676ca2 100644 --- a/openerp/addons/base/res/res_company.py +++ b/openerp/addons/base/res/res_company.py @@ -208,20 +208,19 @@ class res_company(osv.osv): res['value'] = {'currency_id': currency_id} return res - def _search(self, cr, uid, args, offset=0, limit=None, order=None, - context=None, count=False, access_rights_uid=None): + def name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100): if context is None: context = {} - if context.get('user_preference'): + if context.pop('user_preference', None): # We browse as superuser. Otherwise, the user would be able to # select only the currently visible companies (according to rules, # which are probably to allow to see the child companies) even if # she belongs to some other companies. user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context) cmp_ids = list(set([user.company_id.id] + [cmp.id for cmp in user.company_ids])) - return cmp_ids - return super(res_company, self)._search(cr, uid, args, offset=offset, limit=limit, order=order, - context=context, count=count, access_rights_uid=access_rights_uid) + uid = SUPERUSER_ID + args = (args or []) + [('id', 'in', cmp_ids)] + return super(res_company, self).name_search(cr, uid, name=name, args=args, operator=operator, context=context, limit=limit) def _company_default_get(self, cr, uid, object=False, field=False, context=None): """ diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index e9573088680..46f0218e21f 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -350,6 +350,7 @@ class res_partner(osv.osv, format_address): def copy(self, cr, uid, id, default=None, context=None): if default is None: default = {} + default['user_ids'] = False name = self.read(cr, uid, [id], ['name'], context)[0]['name'] default.update({'name': _('%s (copy)') % name}) return super(res_partner, self).copy(cr, uid, id, default, context) diff --git a/openerp/addons/base/res/res_users_view.xml b/openerp/addons/base/res/res_users_view.xml index ba603c7a0f0..8a3f372af7a 100644 --- a/openerp/addons/base/res/res_users_view.xml +++ b/openerp/addons/base/res/res_users_view.xml @@ -282,7 +282,7 @@ - diff --git a/openerp/addons/base/tests/test_fields.py b/openerp/addons/base/tests/test_fields.py index 47ffcd2f5b1..ce34bceb014 100644 --- a/openerp/addons/base/tests/test_fields.py +++ b/openerp/addons/base/tests/test_fields.py @@ -179,3 +179,51 @@ class TestPropertyField(common.TransactionCase): self.partner.write(cr, alice, [partner_id], {'property_country': country_be}) self.assertEqual(self.partner.browse(cr, alice, partner_id).property_country.id, country_be, "Alice does not see the value he has set on the property field") self.assertEqual(self.partner.browse(cr, bob, partner_id).property_country.id, country_fr, "Changes made by Alice have overwritten Bob's value") + + +class TestHtmlField(common.TransactionCase): + + def setUp(self): + super(TestHtmlField, self).setUp() + self.partner = self.registry('res.partner') + + def test_00_sanitize(self): + cr, uid, context = self.cr, self.uid, {} + old_columns = self.partner._columns + self.partner._columns = dict(old_columns) + self.partner._columns.update({ + 'comment': fields.html('Secure Html', sanitize=False), + }) + some_ugly_html = """

Oops this should maybe be sanitized +% if object.some_field and not object.oriented: + + % if object.other_field: + + ${object.mako_thing} + + % endif + +%if object.dummy_field: +

Youpie

+%endif""" + + pid = self.partner.create(cr, uid, { + 'name': 'Raoul Poilvache', + 'comment': some_ugly_html, + }, context=context) + partner = self.partner.browse(cr, uid, pid, context=context) + self.assertEqual(partner.comment, some_ugly_html, 'Error in HTML field: content was sanitized but field has sanitize=False') + + self.partner._columns.update({ + 'comment': fields.html('Unsecure Html', sanitize=True), + }) + self.partner.write(cr, uid, [pid], { + 'comment': some_ugly_html, + }, context=context) + partner = self.partner.browse(cr, uid, pid, context=context) + # sanitize should have closed tags left open in the original html + self.assertIn('
+
', partner.comment, 'Error in HTML field: content does not seem to have been sanitized despise sanitize=True') + self.assertIn('', partner.comment, 'Error in HTML field: content does not seem to have been sanitized despise sanitize=True') + + self.partner._columns = old_columns diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 6780996ed6e..06030a84792 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -1,6 +1,8 @@ # -*- encoding: utf-8 -*- from functools import partial +import unittest2 + from lxml import etree as ET from lxml.builder import E @@ -8,6 +10,24 @@ from openerp.tests import common Field = E.field +class ViewCase(common.TransactionCase): + def setUp(self): + super(ViewCase, self).setUp() + self.addTypeEqualityFunc(ET._Element, self.assertTreesEqual) + + def assertTreesEqual(self, n1, n2, msg=None): + self.assertEqual(n1.tag, n2.tag) + self.assertEqual((n1.text or '').strip(), (n2.text or '').strip(), msg) + self.assertEqual((n1.tail or '').strip(), (n2.tail or '').strip(), msg) + + # Because lxml uses ordereddicts in which order is important to + # equality (!?!?!?!) + self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg) + + for c1, c2 in zip(n1, n2): + self.assertTreesEqual(c1, c2, msg) + + class TestNodeLocator(common.BaseCase): """ The node locator returns None when it can not find a node, and the first @@ -98,7 +118,7 @@ class TestNodeLocator(common.BaseCase): E.foo(attr='1', version='3')) self.assertIsNone(node) -class TestViewInheritance(common.TransactionCase): +class TestViewInheritance(ViewCase): def arch_for(self, name, view_type='form', parent=None): """ Generates a trivial view of the specified ``view_type``. @@ -206,7 +226,7 @@ class TestViewInheritance(common.TransactionCase): self.View.default_view( self.cr, self.uid, model=self.model, view_type='graph')) -class TestApplyInheritanceSpecs(common.TransactionCase): +class TestApplyInheritanceSpecs(ViewCase): """ Applies a sequence of inheritance specification nodes to a base architecture. IO state parameters (cr, uid, model, context) are used for error reporting @@ -230,8 +250,8 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form(Field(name="replacement"), string="Title"))) + self.base_arch, + E.form(Field(name="replacement"), string="Title")) def test_delete(self): spec = Field(name="target", position="replace") @@ -241,8 +261,8 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form(string="Title"))) + self.base_arch, + E.form(string="Title")) def test_insert_after(self): spec = Field( @@ -254,12 +274,12 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form( + self.base_arch, + E.form( Field(name="target"), Field(name="inserted"), string="Title" - ))) + )) def test_insert_before(self): spec = Field( @@ -271,11 +291,11 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form( + self.base_arch, + E.form( Field(name="inserted"), Field(name="target"), - string="Title"))) + string="Title")) def test_insert_inside(self): default = Field(Field(name="inserted"), name="target") @@ -289,13 +309,13 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form( + self.base_arch, + E.form( Field( Field(name="inserted"), Field(name="inserted 2"), name="target"), - string="Title"))) + string="Title")) def test_unpack_data(self): spec = E.data( @@ -310,15 +330,15 @@ class TestApplyInheritanceSpecs(common.TransactionCase): spec, None) self.assertEqual( - ET.tostring(self.base_arch), - ET.tostring(E.form( + self.base_arch, + E.form( Field( Field(name="inserted 0"), Field(name="inserted 1"), Field(name="inserted 2"), Field(name="inserted 3"), name="target"), - string="Title"))) + string="Title")) def test_invalid_position(self): spec = Field( @@ -350,18 +370,18 @@ class TestApplyInheritanceSpecs(common.TransactionCase): self.base_arch, spec, None) -class TestApplyInheritedArchs(common.TransactionCase): +class TestApplyInheritedArchs(ViewCase): """ Applies a sequence of modificator archs to a base view """ -class TestViewCombined(common.TransactionCase): +class TestViewCombined(ViewCase): """ Test fallback operations of View.read_combined: * defaults mapping * ? """ -class TestNoModel(common.TransactionCase): +class TestNoModel(ViewCase): def test_create_view_nomodel(self): View = self.registry('ir.ui.view') view_id = View.create(self.cr, self.uid, { @@ -411,13 +431,11 @@ class TestNoModel(common.TransactionCase): 'value': translated_text, }) sarch = View.translate_qweb(self.cr, self.uid, None, self.arch, 'fr_FR') - - self.text_para.text = translated_text - self.assertEqual( - ET.tostring(sarch, encoding='utf-8'), - ET.tostring(self.arch, encoding='utf-8')) -class TestTemplating(common.TransactionCase): + self.text_para.text = translated_text + self.assertEqual(sarch, self.arch) + +class TestTemplating(ViewCase): def setUp(self): import openerp.modules super(TestTemplating, self).setUp() @@ -473,7 +491,126 @@ class TestTemplating(common.TransactionCase): second.get('data-oe-id'), "second should come from the extension view") -class test_views(common.TransactionCase): + def test_branding_distribute_inner(self): + """ Checks that the branding is correctly distributed within a view + extension + """ + Views = self.registry('ir.ui.view') + id = Views.create(self.cr, self.uid, { + 'name': "Base view", + 'type': 'qweb', + 'arch': """ + + """ + }) + id2 = Views.create(self.cr, self.uid, { + 'name': "Extension", + 'type': 'qweb', + 'inherit_id': id, + 'arch': """ + + bar + + """ + }) + + arch_string = Views.read_combined( + self.cr, self.uid, id, fields=['arch'], + context={'inherit_branding': True})['arch'] + + arch = ET.fromstring(arch_string) + Views.distribute_branding(arch) + + self.assertEqual( + arch, + E.root( + E.item( + E.content("bar", { + 't-att-href': "foo", + 'data-oe-model': 'ir.ui.view', + 'data-oe-id': str(id2), + 'data-oe-field': 'arch', + 'data-oe-xpath': '/xpath/item/content[1]', + }), { + 'order': '2', + 'data-oe-source-id': str(id) + }), + E.item({ + 'order': '1', + 'data-oe-model': 'ir.ui.view', + 'data-oe-id': str(id), + 'data-oe-field': 'arch', + 'data-oe-xpath': '/root[1]/item[1]' + }) + ) + ) + + def test_esc_no_branding(self): + Views = self.registry('ir.ui.view') + id = Views.create(self.cr, self.uid, { + 'name': "Base View", + 'type': 'qweb', + 'arch': """ + + """, + }) + + arch_string = Views.read_combined( + self.cr, self.uid, id, fields=['arch'], + context={'inherit_branding': True})['arch'] + arch = ET.fromstring(arch_string) + Views.distribute_branding(arch) + + self.assertEqual(arch, E.root(E.item(E.span({'t-esc': "foo"})))) + + def test_ignore_unbrand(self): + Views = self.registry('ir.ui.view') + id = Views.create(self.cr, self.uid, { + 'name': "Base view", + 'type': 'qweb', + 'arch': """ + + + + """ + }) + id2 = Views.create(self.cr, self.uid, { + 'name': "Extension", + 'type': 'qweb', + 'inherit_id': id, + 'arch': """ + + bar + + """ + }) + + arch_string = Views.read_combined( + self.cr, self.uid, id, fields=['arch'], + context={'inherit_branding': True})['arch'] + + arch = ET.fromstring(arch_string) + Views.distribute_branding(arch) + + self.assertEqual( + arch, + E.root( + E.item( + {'t-ignore': 'true', 'order': '1'}, + E.t({'t-esc': 'foo'}), + E.item( + {'order': '2', 'data-oe-source-id': str(id)}, + E.content( + {'t-att-href': 'foo'}, + "bar") + ) + ) + ), + "t-ignore should apply to injected sub-view branding, not just to" + " the main view's" + ) + +class test_views(ViewCase): def test_nonexistent_attribute_removal(self): Views = self.registry('ir.ui.view') @@ -589,16 +726,17 @@ class test_views(common.TransactionCase): }) self.assertEqual(view['type'], 'form') self.assertEqual( - ET.tostring(ET.fromstring( + ET.fromstring( view['arch'], parser=ET.XMLParser(remove_blank_text=True) - )), - '

' - '

Replacement data

' - '
' - '
' - '
') + ), + E.form( + E.p("Replacement data"), + E.footer( + E.button(name="action_next", type="object", string="New button"), + thing="bob" + ), + string="Replacement title", version="7.0")) def test_view_inheritance_divergent_models(self): Views = self.registry('ir.ui.view') @@ -656,14 +794,13 @@ class test_views(common.TransactionCase): }) self.assertEqual(view['type'], 'form') self.assertEqual( - ET.tostring(ET.fromstring( + ET.fromstring( view['arch'], parser=ET.XMLParser(remove_blank_text=True) - )), - '
' - '

Replacement data

' - '' - '
') - + ), + E.form( + E.p("Replacement data"), + E.footer( + E.button(name="action_next", type="object", string="New button")), + string="Replacement title", version="7.0" + )) diff --git a/openerp/http.py b/openerp/http.py index 1750478a836..128d5a91246 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -209,20 +209,19 @@ class WebRequest(object): # Backward for 7.0 if self.endpoint.first_arg_is_req: args = (request,) + args + # Correct exception handling and concurency retry @service_model.check def checked_call(___dbname, *a, **kw): - return self.endpoint(*a, **kw) - - # FIXME: code and rollback management could be cleaned - try: - if self.db: - return checked_call(self.db, *args, **kwargs) - return self.endpoint(*args, **kwargs) - except Exception: + # The decorator can call us more than once if there is an database error. In this + # case, the request cursor is unusable. Rollback transaction to create a new one. if self._cr: self._cr.rollback() - raise + return self.endpoint(*a, **kw) + + if self.db: + return checked_call(self.db, *args, **kwargs) + return self.endpoint(*args, **kwargs) @property def debug(self): @@ -733,7 +732,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): self.setdefault("uid", None) self.setdefault("login", None) self.setdefault("password", None) - self.setdefault("context", {'tz': "UTC", "uid": None}) + self.setdefault("context", {}) def get_context(self): """ diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 478f982cd69..d5d0ee0fa30 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -236,15 +236,24 @@ class char(_column): class text(_column): _type = 'text' + class html(text): _type = 'html' _symbol_c = '%s' - def _symbol_f(x): - if x is None or x == False: + + def _symbol_set_html(self, value): + if value is None or value is False: return None - return html_sanitize(x) - - _symbol_set = (_symbol_c, _symbol_f) + if not self._sanitize: + return value + return html_sanitize(value) + + def __init__(self, string='unknown', sanitize=True, **args): + super(html, self).__init__(string=string, **args) + self._sanitize = sanitize + # symbol_set redefinition because of sanitize specific behavior + self._symbol_f = self._symbol_set_html + self._symbol_set = (self._symbol_c, self._symbol_f) import __builtin__