[IMP] base: replace 'sql_constraint' translations by 'model' translations
Store the SQL constraint message directly in the field `message` of the corresponding `ir.model.constraint` record, and manage translations from there. Those records are given an XML id in order to be tracked in PO files. The translation type 'sql_constraint` is removed.
This commit is contained in:
@@ -970,6 +970,7 @@ class IrModelConstraint(models.Model):
|
||||
name = fields.Char(string='Constraint', required=True, index=True,
|
||||
help="PostgreSQL constraint or foreign key name.")
|
||||
definition = fields.Char(help="PostgreSQL constraint definition")
|
||||
message = fields.Char(help="Error message returned when the constraint is violated.", translate=True)
|
||||
model = fields.Many2one('ir.model', required=True, ondelete="cascade", index=True)
|
||||
module = fields.Many2one('ir.module.module', required=True, index=True)
|
||||
type = fields.Char(string='Constraint Type', required=True, size=1, index=True,
|
||||
@@ -1033,9 +1034,10 @@ class IrModelConstraint(models.Model):
|
||||
default['name'] = self.name + '_copy'
|
||||
return super(IrModelConstraint, self).copy(default)
|
||||
|
||||
def _reflect_constraint(self, model, conname, type, definition, module):
|
||||
""" Reflect the given constraint, to make it possible to delete it later
|
||||
when the module is uninstalled. ``type`` is either 'f' or 'u'
|
||||
def _reflect_constraint(self, model, conname, type, definition, module, message=None):
|
||||
""" Reflect the given constraint, and return its corresponding record.
|
||||
The reflection makes it possible to remove a constraint when its
|
||||
corresponding module is uninstalled. ``type`` is either 'f' or 'u'
|
||||
depending on the constraint being a foreign key or not.
|
||||
"""
|
||||
if not module:
|
||||
@@ -1044,23 +1046,28 @@ class IrModelConstraint(models.Model):
|
||||
return
|
||||
assert type in ('f', 'u')
|
||||
cr = self._cr
|
||||
query = """ SELECT type, definition
|
||||
query = """ SELECT c.id, type, definition, message
|
||||
FROM ir_model_constraint c, ir_module_module m
|
||||
WHERE c.module=m.id AND c.name=%s AND m.name=%s """
|
||||
cr.execute(query, (conname, module))
|
||||
cons = cr.dictfetchone()
|
||||
if not cons:
|
||||
query = """ INSERT INTO ir_model_constraint
|
||||
(name, date_init, date_update, module, model, type, definition)
|
||||
(name, date_init, date_update, module, model, type, definition, message)
|
||||
VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC',
|
||||
(SELECT id FROM ir_module_module WHERE name=%s),
|
||||
(SELECT id FROM ir_model WHERE model=%s), %s, %s) """
|
||||
cr.execute(query, (conname, module, model._name, type, definition))
|
||||
elif cons['type'] != type or (definition and cons['definition'] != definition):
|
||||
(SELECT id FROM ir_model WHERE model=%s), %s, %s, %s)
|
||||
RETURNING id"""
|
||||
cr.execute(query, (conname, module, model._name, type, definition, message))
|
||||
return self.browse(cr.fetchone()[0])
|
||||
|
||||
cons_id = cons.pop('id')
|
||||
if cons != dict(type=type, definition=definition, message=message):
|
||||
query = """ UPDATE ir_model_constraint
|
||||
SET date_update=now() AT TIME ZONE 'UTC', type=%s, definition=%s
|
||||
WHERE name=%s AND module=(SELECT id FROM ir_module_module WHERE name=%s) """
|
||||
cr.execute(query, (type, definition, conname, module))
|
||||
SET date_update=now() AT TIME ZONE 'UTC', type=%s, definition=%s, message=%s
|
||||
WHERE id=%s"""
|
||||
cr.execute(query, (type, definition, message, cons_id))
|
||||
return self.browse(cons_id)
|
||||
|
||||
def _reflect_model(self, model):
|
||||
""" Reflect the _sql_constraints of the given model. """
|
||||
@@ -1075,10 +1082,16 @@ class IrModelConstraint(models.Model):
|
||||
for constraint in getattr(cls, '_local_sql_constraints', ())
|
||||
}
|
||||
|
||||
for (key, definition, _) in model._sql_constraints:
|
||||
data_list = []
|
||||
for (key, definition, message) in model._sql_constraints:
|
||||
conname = '%s_%s' % (model._table, key)
|
||||
module = constraint_module.get(key)
|
||||
self._reflect_constraint(model, conname, 'u', cons_text(definition), module)
|
||||
record = self._reflect_constraint(model, conname, 'u', cons_text(definition), module, message)
|
||||
if record:
|
||||
xml_id = '%s.constraint_%s' % (module, conname)
|
||||
data_list.append(dict(xml_id=xml_id, record=record))
|
||||
|
||||
self.env['ir.model.data']._update_xmlids(data_list)
|
||||
|
||||
|
||||
class IrModelRelation(models.Model):
|
||||
|
||||
@@ -18,7 +18,6 @@ TRANSLATION_TYPE = [
|
||||
('selection', 'Selection'),
|
||||
('code', 'Code'),
|
||||
('constraint', 'Constraint'),
|
||||
('sql_constraint', 'SQL Constraint')
|
||||
]
|
||||
|
||||
|
||||
@@ -127,9 +126,9 @@ class IrTranslationImport(object):
|
||||
cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments)
|
||||
SELECT name, lang, res_id, src, type, value, module, state, comments
|
||||
FROM %s
|
||||
WHERE type IN ('selection', 'constraint', 'sql_constraint')
|
||||
WHERE type IN ('selection', 'constraint')
|
||||
AND noupdate IS NOT TRUE
|
||||
ON CONFLICT (type, lang, name, md5(src)) WHERE type IN ('selection', 'constraint', 'sql_constraint')
|
||||
ON CONFLICT (type, lang, name, md5(src)) WHERE type IN ('selection', 'constraint')
|
||||
DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments)
|
||||
WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != '';
|
||||
""" % (self._model_table, self._table))
|
||||
@@ -259,7 +258,7 @@ class IrTranslation(models.Model):
|
||||
if not tools.index_exists(self._cr, 'ir_translation_model_unique'):
|
||||
self._cr.execute("CREATE UNIQUE INDEX ir_translation_model_unique ON ir_translation (type, lang, name, res_id) WHERE type = 'model'")
|
||||
if not tools.index_exists(self._cr, 'ir_translation_selection_unique'):
|
||||
self._cr.execute("CREATE UNIQUE INDEX ir_translation_selection_unique ON ir_translation (type, lang, name, md5(src)) WHERE type IN ('selection', 'constraint', 'sql_constraint')")
|
||||
self._cr.execute("CREATE UNIQUE INDEX ir_translation_selection_unique ON ir_translation (type, lang, name, md5(src)) WHERE type IN ('selection', 'constraint')")
|
||||
return res
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -468,6 +468,9 @@
|
||||
<field name="date_update" />
|
||||
<field name="date_init" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="message"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<odoo>
|
||||
<template id="assets_tests" name="Test New Api Assets Tests" inherit_id="web.assets_tests">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/test_new_api/static/tests/tours/constraint.js"></script>
|
||||
<script type="text/javascript" src="/test_new_api/static/tests/tours/x2many.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * test_new_api
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 13.0alpha1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-07-08 06:43+0000\n"
|
||||
"PO-Revision-Date: 2019-07-08 06:43+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: test_new_api
|
||||
#: model:ir.model.constraint,message:test_new_api.constraint_test_new_api_category_positive_color
|
||||
msgid "The color code must be positive !"
|
||||
msgstr "La couleur doit être une valeur positive !"
|
||||
@@ -0,0 +1,21 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * test_new_api
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 13.0alpha1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-07-08 06:43+0000\n"
|
||||
"PO-Revision-Date: 2019-07-08 06:43+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: test_new_api
|
||||
#: model:ir.model.constraint,message:test_new_api.constraint_test_new_api_category_positive_color
|
||||
msgid "The color code must be positive !"
|
||||
msgstr ""
|
||||
@@ -24,6 +24,10 @@ class Category(models.Model):
|
||||
discussions = fields.Many2many('test_new_api.discussion', 'test_new_api_discussion_category',
|
||||
'category', 'discussion')
|
||||
|
||||
_sql_constraints = [
|
||||
('positive_color', 'CHECK(color >= 0)', 'The color code must be positive !')
|
||||
]
|
||||
|
||||
@api.depends('name', 'parent.display_name') # this definition is recursive
|
||||
def _compute_display_name(self):
|
||||
for cat in self:
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
odoo.define('web.test.constraint', function (require) {
|
||||
'use strict';
|
||||
|
||||
var tour = require("web_tour.tour");
|
||||
var inc;
|
||||
|
||||
tour.register('sql_constaint', {
|
||||
url: '/web?debug=1#action=test_new_api.action_categories',
|
||||
test: true,
|
||||
}, [
|
||||
{
|
||||
content: "wait web client",
|
||||
trigger: '.breadcrumb:contains(Categories)',
|
||||
}, { // create test category
|
||||
content: "create new category",
|
||||
trigger: 'button.o_list_button_add',
|
||||
}, {
|
||||
content: "insert content",
|
||||
trigger: 'input.o_required_modifier',
|
||||
run: 'text Test Category',
|
||||
}, { // try to insert a value that will raise the SQL constraint
|
||||
content: "insert invalid value",
|
||||
trigger: 'input[name="color"]',
|
||||
run: 'text -1',
|
||||
}, { // save
|
||||
content: "save category",
|
||||
trigger: 'button.o_form_button_save',
|
||||
}, { // check popup content
|
||||
content: "check notification box",
|
||||
trigger: '.o_notification_content:contains(The color code must be positive !)',
|
||||
run: function () {}, // it's a check
|
||||
}]);
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import odoo.tests
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@odoo.tests.common.tagged('post_install', '-at_install')
|
||||
@@ -14,3 +15,21 @@ class TestUi(odoo.tests.HttpCase):
|
||||
# too small or there are too many items preceding it in the tests menu
|
||||
self.start_tour("/web#action=test_new_api.action_discussions",
|
||||
'widget_x2many', step_delay=100, login="admin", timeout=120)
|
||||
|
||||
|
||||
class TestUiTranslation(odoo.tests.HttpCase):
|
||||
|
||||
@mute_logger('odoo.sql_db', 'odoo.http')
|
||||
def test_01_sql_constraints(self):
|
||||
# Raise an SQL constraint and test the message
|
||||
self.env['ir.translation']._load_module_terms(['test_new_api'], ['fr_FR'])
|
||||
constraint = self.env.ref('test_new_api.constraint_test_new_api_category_positive_color')
|
||||
message = constraint.with_context(lang='fr_FR').message
|
||||
self.assertEqual(message, "La couleur doit être une valeur positive !")
|
||||
|
||||
# TODO: make the test work with French translations. As the transaction
|
||||
# is rollbacked at insert and a new cusor is opened, can not test that
|
||||
# the message is translated (_load_module_terms is also) rollbacked.
|
||||
# Test individually the external id and loading of translation.
|
||||
self.start_tour("/web#action=test_new_api.action_categories",
|
||||
'sql_constaint', login="admin")
|
||||
|
||||
@@ -245,6 +245,7 @@
|
||||
<field name="parent"/>
|
||||
<field name="root_categ"/>
|
||||
<field name="dummy"/>
|
||||
<field name="color"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
|
||||
+3
-3
@@ -584,9 +584,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
|
||||
@classmethod
|
||||
def _init_constraints_onchanges(cls):
|
||||
# store sql constraint error messages
|
||||
for (key, _, msg) in cls._sql_constraints:
|
||||
cls.pool._sql_error[cls._table + '_' + key] = msg
|
||||
# store list of sql constraint qualified names
|
||||
for (key, _, _) in cls._sql_constraints:
|
||||
cls.pool._sql_constraints.add(cls._table + '_' + key)
|
||||
|
||||
# reset properties memoized on cls
|
||||
cls._constraint_methods = BaseModel._constraint_methods
|
||||
|
||||
@@ -105,7 +105,7 @@ class Registry(Mapping):
|
||||
|
||||
def init(self, db_name):
|
||||
self.models = {} # model name/model instance mapping
|
||||
self._sql_error = {}
|
||||
self._sql_constraints = set()
|
||||
self._init = True
|
||||
self._assertion_report = assertion_report.assertion_report()
|
||||
self._fields_by_model = None
|
||||
|
||||
+10
-15
@@ -10,7 +10,7 @@ import time
|
||||
import odoo
|
||||
from odoo.exceptions import UserError, ValidationError, QWebException
|
||||
from odoo.models import check_method_name
|
||||
from odoo.tools.translate import translate
|
||||
from odoo.tools.translate import translate, translate_sql_constraint
|
||||
from odoo.tools.translate import _
|
||||
|
||||
from . import security
|
||||
@@ -73,18 +73,13 @@ def check(f):
|
||||
|
||||
# We open a *new* cursor here, one reason is that failed SQL
|
||||
# queries (as in IntegrityError) will invalidate the current one.
|
||||
cr = False
|
||||
|
||||
try:
|
||||
cr = odoo.sql_db.db_connect(dbname).cursor()
|
||||
res = translate(cr, name=False, source_type=ttype,
|
||||
lang=lang, source=src)
|
||||
if res:
|
||||
return res
|
||||
with odoo.sql_db.db_connect(dbname).cursor() as cr:
|
||||
if ttype == 'sql_constraint':
|
||||
res = translate_sql_constraint(cr, key=key, lang=lang)
|
||||
else:
|
||||
return src
|
||||
finally:
|
||||
if cr: cr.close()
|
||||
res = translate(cr, name=False, source_type=ttype,
|
||||
lang=lang, source=src)
|
||||
return res or src
|
||||
|
||||
def _(src):
|
||||
return tr(src, 'code')
|
||||
@@ -114,9 +109,9 @@ def check(f):
|
||||
time.sleep(wait_time)
|
||||
except IntegrityError as inst:
|
||||
registry = odoo.registry(dbname)
|
||||
for key in registry._sql_error.keys():
|
||||
if key in inst.pgerror:
|
||||
raise ValidationError(tr(registry._sql_error[key], 'sql_constraint') or inst.pgerror)
|
||||
key = inst.diag.constraint_name
|
||||
if key in registry._sql_constraints:
|
||||
raise ValidationError(tr(key, 'sql_constraint') or inst.pgerror)
|
||||
if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION):
|
||||
msg = _('The operation cannot be completed, probably due to the following:\n- deletion: you may be trying to delete a record while other records still reference it\n- creation/update: a mandatory field is not correctly set')
|
||||
_logger.debug("IntegrityError", exc_info=True)
|
||||
|
||||
+24
-10
@@ -352,6 +352,21 @@ def translate(cr, name, source_type, lang, source=None):
|
||||
res = res_trans and res_trans[0] or False
|
||||
return res
|
||||
|
||||
def translate_sql_constraint(cr, key, lang):
|
||||
cr.execute("""
|
||||
SELECT COALESCE(t.value, c.message) as message
|
||||
FROM ir_model_constraint c
|
||||
LEFT JOIN
|
||||
(SELECT res_id, value FROM ir_translation
|
||||
WHERE type='model'
|
||||
AND name='ir.model.constraint,message'
|
||||
AND lang=%s
|
||||
AND value!='') AS t
|
||||
ON c.id=t.res_id
|
||||
WHERE name=%s and type='u'
|
||||
""", (lang, key))
|
||||
return cr.fetchone()[0]
|
||||
|
||||
class GettextAlias(object):
|
||||
|
||||
def _get_db(self):
|
||||
@@ -445,7 +460,7 @@ class GettextAlias(object):
|
||||
if cr:
|
||||
# Try to use ir.translation to benefit from global cache if possible
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
res = env['ir.translation']._get_source(None, ('code','sql_constraint'), lang, source)
|
||||
res = env['ir.translation']._get_source(None, ('code',), lang, source)
|
||||
else:
|
||||
_logger.debug('no context cursor detected, skipping translation for "%r"', source)
|
||||
else:
|
||||
@@ -594,6 +609,9 @@ class PoFileReader:
|
||||
match = re.match(r'(sql_constraint|constraint):([\w.]+)', occurrence)
|
||||
if match:
|
||||
type, model = match.groups()
|
||||
if type == "sql_constraint":
|
||||
_logger.info("Skipped deprecated occurrence %s", occurrence)
|
||||
continue
|
||||
yield {
|
||||
'type': type,
|
||||
'name': model,
|
||||
@@ -921,17 +939,15 @@ def trans_generate(lang, modules, cr):
|
||||
if not callable(msg):
|
||||
push_translation(encode(module), term_type, encode(model), 0, msg)
|
||||
|
||||
def push_local_constraints(module, model, cons_type='sql_constraints'):
|
||||
def push_local_constraints(module, model):
|
||||
""" Climb up the class hierarchy and ignore inherited constraints from other modules. """
|
||||
term_type = 'sql_constraint' if cons_type == 'sql_constraints' else 'constraint'
|
||||
msg_pos = 2 if cons_type == 'sql_constraints' else 1
|
||||
for cls in model.__class__.__mro__:
|
||||
if getattr(cls, '_module', None) != module:
|
||||
continue
|
||||
constraints = getattr(cls, '_local_' + cons_type, [])
|
||||
constraints = getattr(cls, '_local_constraints', [])
|
||||
for constraint in constraints:
|
||||
push_constraint_msg(module, term_type, model._name, constraint[msg_pos])
|
||||
|
||||
push_constraint_msg(module, 'constraint', model._name, constraint[1])
|
||||
|
||||
cr.execute(query_models, query_param)
|
||||
|
||||
for (_, model, module) in cr.fetchall():
|
||||
@@ -940,9 +956,7 @@ def trans_generate(lang, modules, cr):
|
||||
continue
|
||||
Model = env[model]
|
||||
if Model._constraints:
|
||||
push_local_constraints(module, Model, 'constraints')
|
||||
if Model._sql_constraints:
|
||||
push_local_constraints(module, Model, 'sql_constraints')
|
||||
push_local_constraints(module, Model)
|
||||
|
||||
installed_modules = [
|
||||
m['name']
|
||||
|
||||
Reference in New Issue
Block a user