[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:
Martin Trigaux
2019-07-11 14:53:01 +00:00
parent dbf9aac272
commit bf88f3e3d1
14 changed files with 170 additions and 46 deletions
+26 -13
View File
@@ -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):
+3 -4
View File
@@ -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>
+1
View File
@@ -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>
+21
View File
@@ -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 ""
+4
View File
@@ -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
}]);
});
+19
View File
@@ -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")
+1
View File
@@ -245,6 +245,7 @@
<field name="parent"/>
<field name="root_categ"/>
<field name="dummy"/>
<field name="color"/>
</group>
</sheet>
</form>
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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']