Add option to remove leading and trailing white spaces from char

issue7914
review340511008
This commit is contained in:
Cédric Krier
2022-09-19 21:25:55 +02:00
parent 827bb0b69d
commit c3a8be00ca
12 changed files with 186 additions and 21 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add strip option to Char fields
* Strip only one wildcard
* Manage web extension origin as null
* Remove same types restriction on PYSON If
+8 -1
View File
@@ -264,7 +264,7 @@ BigInteger
Char
----
.. class:: Char(string[, size[, translate[, \**options]]])
.. class:: Char(string[, size[, translate[, strip[, \**options]]]])
A single line :py:class:`string <str>` field.
@@ -291,6 +291,13 @@ Char
The value readed and stored will depend on the ``language`` defined in the
context.
.. attribute:: Char.strip
If ``True``, leading and trailing whitespace are removed.
If ``leading``, leading whitespace are removed.
If ``trailing``, trailing whitespace are removed.
The default value is ``True``.
.. attribute:: Char.autocomplete
A set of field names.
+1 -1
View File
@@ -9,7 +9,7 @@ class Configuration(ModelSingleton, ModelSQL):
'Configuration'
__name__ = 'ir.configuration'
language = fields.Char('language')
hostname = fields.Char("Hostname")
hostname = fields.Char("Hostname", strip=False)
_get_language_cache = Cache('ir_configuration.get_language')
@staticmethod
+10 -8
View File
@@ -52,24 +52,26 @@ class Lang(DeactivableMixin, ModelSQL, ModelView):
], 'Direction', required=True)
# date
date = fields.Char('Date', required=True)
date = fields.Char("Date", required=True, strip=False)
am = fields.Char("AM")
pm = fields.Char("PM")
am = fields.Char("AM", strip=False)
pm = fields.Char("PM", strip=False)
# number
grouping = fields.Char('Grouping', required=True)
decimal_point = fields.Char('Decimal Separator', required=True)
thousands_sep = fields.Char('Thousands Separator')
decimal_point = fields.Char(
"Decimal Separator", required=True, strip=False)
thousands_sep = fields.Char("Thousands Separator", strip=False)
# monetary formatting
mon_grouping = fields.Char('Grouping', required=True)
mon_decimal_point = fields.Char('Decimal Separator', required=True)
mon_decimal_point = fields.Char(
"Decimal Separator", required=True, strip=False)
mon_thousands_sep = fields.Char('Thousands Separator')
p_sign_posn = fields.Integer('Positive Sign Position', required=True)
n_sign_posn = fields.Integer('Negative Sign Position', required=True)
positive_sign = fields.Char('Positive Sign')
negative_sign = fields.Char('Negative Sign')
positive_sign = fields.Char("Positive Sign", strip=False)
negative_sign = fields.Char("Negative Sign", strip=False)
p_cs_precedes = fields.Boolean('Positive Currency Symbol Precedes')
n_cs_precedes = fields.Boolean('Negative Currency Symbol Precedes')
p_sep_by_space = fields.Boolean('Positive Separate by Space')
+2 -2
View File
@@ -60,8 +60,8 @@ class Sequence(DeactivableMixin, ModelSQL, ModelView):
'readonly': Eval('id', -1) >= 0,
},
depends=['id'])
prefix = fields.Char('Prefix')
suffix = fields.Char('Suffix')
prefix = fields.Char('Prefix', strip='leading')
suffix = fields.Char('Suffix', strip='trailing')
type = fields.Selection([
('incremental', 'Incremental'),
('decimal timestamp', 'Decimal Timestamp'),
+1 -1
View File
@@ -13,7 +13,7 @@ class Session(ModelSQL):
__name__ = 'ir.session'
_rec_name = 'key'
key = fields.Char('Key', required=True, select=True)
key = fields.Char('Key', required=True, select=True, strip=False)
@classmethod
def __setup__(cls):
+45 -1
View File
@@ -1,8 +1,11 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import string
import warnings
from sql import Expression, Query
from sql.conditionals import Coalesce, NullIf
from sql.functions import Trim
from sql.operators import Not
from trytond.rpc import RPC
@@ -25,7 +28,7 @@ class Char(FieldTranslate):
def __init__(self, string='', size=None, help='', required=False,
readonly=False, domain=None, states=None, translate=False,
select=False, on_change=None, on_change_with=None, depends=None,
context=None, loading=None, autocomplete=None):
context=None, loading=None, autocomplete=None, strip=True):
'''
:param translate: A boolean. If ``True`` the field is translatable.
:param size: A integer. If set defines the maximum size of the values.
@@ -41,6 +44,7 @@ class Char(FieldTranslate):
warnings.warn('autocomplete argument is deprecated, use the '
'depends decorator', DeprecationWarning, stacklevel=2)
self.autocomplete.update(autocomplete)
self.strip = strip
self.translate = translate
self.__size = None
self.size = size
@@ -55,6 +59,15 @@ class Char(FieldTranslate):
size = property(_get_size, _set_size)
@property
def strip(self):
return self.__strip
@strip.setter
def strip(self, value):
assert value in {False, True, 'leading', 'trailing'}
self.__strip = value
@property
def _sql_type(self):
if isinstance(self.size, int):
@@ -62,6 +75,36 @@ class Char(FieldTranslate):
else:
return 'VARCHAR'
def __set__(self, inst, value):
if isinstance(value, str) and self.strip:
if self.strip == 'leading':
value = value.lstrip()
elif self.strip == 'trailing':
value = value.rstrip()
else:
value = value.strip()
super().__set__(inst, value)
def sql_format(self, value):
if value is not None and self.strip:
if isinstance(value, (Query, Expression)):
if self.strip == 'leading':
position = 'LEADING'
elif self.strip == 'trailing':
position = 'TRAILING'
else:
position = 'BOTH'
value = Trim(
value, position=position, characters=string.whitespace)
else:
if self.strip == 'leading':
value = value.lstrip()
elif self.strip == 'trailing':
value = value.rstrip()
else:
value = value.strip()
return super().sql_format(value)
def set_rpc(self, model):
super(Char, self).set_rpc(model)
if self.autocomplete:
@@ -188,6 +231,7 @@ class Char(FieldTranslate):
def definition(self, model, language):
definition = super().definition(model, language)
definition['autocomplete'] = list(self.autocomplete)
definition['strip'] = self.strip
if self.size is not None:
definition['size'] = self.size
return definition
+3
View File
@@ -18,6 +18,9 @@ class Text(Char):
forbidden_chars = ''
search_full_text = True
def __init__(self, *args, strip=False, **kwarg):
super().__init__(*args, strip=strip, **kwarg)
class FullText(Field):
'''
+4 -2
View File
@@ -698,7 +698,8 @@ class ModelSQL(ModelStorage):
if (getattr(field, 'translate', False)
and not hasattr(field, 'set')):
translation_values.setdefault(
'%s,%s' % (cls.__name__, fname), {})[new_id] = value
'%s,%s' % (cls.__name__, fname), {})[new_id] = (
field.sql_format(value))
if hasattr(field, 'set'):
args = fields_to_set.setdefault(fname, [])
actions = iter(args)
@@ -1113,7 +1114,8 @@ class ModelSQL(ModelStorage):
and not hasattr(field, 'set')):
Translation.set_ids(
'%s,%s' % (cls.__name__, fname), 'model',
transaction.language, ids, [value] * len(ids))
transaction.language, ids,
[field.sql_format(value)] * len(ids))
if hasattr(field, 'set'):
fields_to_set.setdefault(fname, []).extend((ids, value))
+5 -5
View File
@@ -112,7 +112,7 @@ class User(avatar_mixin(100, 'login'), DeactivableMixin, ModelSQL, ModelView):
__name__ = "res.user"
name = fields.Char('Name', select=True)
login = fields.Char('Login', required=True)
password_hash = fields.Char('Password Hash')
password_hash = fields.Char('Password Hash', strip=False)
password = fields.Function(fields.Char(
"Password",
states={
@@ -120,7 +120,7 @@ class User(avatar_mixin(100, 'login'), DeactivableMixin, ModelSQL, ModelView):
}),
getter='get_password', setter='set_password')
password_reset = fields.Char(
"Reset Password",
"Reset Password", strip=False,
states={
'invisible': not _has_password,
})
@@ -812,7 +812,7 @@ class LoginAttempt(ModelSQL):
"""
__name__ = 'res.user.login.attempt'
login = fields.Char('Login', size=512)
device_cookie = fields.Char("Device Cookie")
device_cookie = fields.Char("Device Cookie", strip=False)
ip_address = fields.Char("IP Address")
ip_network = fields.Char("IP Network")
@@ -896,7 +896,7 @@ class UserDevice(ModelSQL):
__name__ = 'res.user.device'
login = fields.Char("Login", required=True)
cookie = fields.Char("Cookie", readonly=True, required=True)
cookie = fields.Char("Cookie", readonly=True, required=True, strip=False)
@classmethod
def __setup__(cls):
@@ -1082,7 +1082,7 @@ class UserApplication(Workflow, ModelSQL, ModelView):
__name__ = 'res.user.application'
_rec_name = 'key'
key = fields.Char("Key", required=True, select=True)
key = fields.Char("Key", required=True, select=True, strip=False)
user = fields.Many2One('res.user', "User", select=True)
application = fields.Selection([], "Application")
state = fields.Selection([
+6
View File
@@ -10,6 +10,9 @@ class Char(ModelSQL):
'Char'
__name__ = 'test.char'
char = fields.Char("Char")
char_lstripped = fields.Char("Char", strip='leading')
char_rstripped = fields.Char("Char", strip='trailing')
char_unstripped = fields.Char("Char", strip=False)
class CharDefault(ModelSQL):
@@ -47,6 +50,9 @@ class CharTranslate(ModelSQL):
'Char Translate'
__name__ = 'test.char_translate'
char = fields.Char("Char", translate=True)
char_lstripped = fields.Char("Char", strip='leading', translate=True)
char_rstripped = fields.Char("Char", strip='trailing', translate=True)
char_unstripped = fields.Char("Char", strip=False, translate=True)
class CharUnaccentedOn(ModelSQL):
+100
View File
@@ -323,6 +323,64 @@ class CommonTestCaseMixin:
self.assertEqual(char.char, "é")
@with_transaction()
def test_create_strip(self):
"Test create with stripping"
Char = self.Char()
char, = Char.create([{
'char': " Foo ",
'char_lstripped': " Foo ",
'char_rstripped': " Foo ",
'char_unstripped': " Foo ",
}])
read_record = Char(char.id)
self.assertEqual(read_record.char, "Foo")
self.assertEqual(read_record.char_lstripped, "Foo ")
self.assertEqual(read_record.char_rstripped, " Foo")
self.assertEqual(read_record.char_unstripped, " Foo ")
@with_transaction()
def test_write_strip(self):
"Test write with stripping"
Char = self.Char()
char, = Char.create([{
'char': "Foo",
'char_lstripped': "Foo",
'char_rstripped': "Foo",
'char_unstripped': "Foo",
}])
Char.write([char], {
'char': " Bar ",
'char_lstripped': " Bar ",
'char_rstripped': " Bar ",
'char_unstripped': " Bar ",
})
read_record = Char(char.id)
self.assertEqual(read_record.char, "Bar")
self.assertEqual(read_record.char_lstripped, "Bar ")
self.assertEqual(read_record.char_rstripped, " Bar")
self.assertEqual(read_record.char_unstripped, " Bar ")
@with_transaction()
def test_set_strip(self):
"Test set with stripping"
Char = self.Char()
char = Char()
char.char = " Foo "
char.char_lstripped = " Foo "
char.char_rstripped = " Foo "
char.char_unstripped = " Foo "
self.assertEqual(char.char, "Foo")
self.assertEqual(char.char_lstripped, "Foo ")
self.assertEqual(char.char_rstripped, " Foo")
self.assertEqual(char.char_unstripped, " Foo ")
class FieldCharTestCase(unittest.TestCase, CommonTestCaseMixin):
"Test Field Char"
@@ -507,6 +565,48 @@ class FieldCharTestCase(unittest.TestCase, CommonTestCaseMixin):
'char': 'foobar',
})
@with_transaction()
def test_create_strip_with_sql_value(self):
"Test create with stripping with SQL value"
Char = self.Char()
char, = Char.create([{
'char': Literal(" Foo "),
'char_lstripped': Literal(" Foo "),
'char_rstripped': Literal(" Foo "),
'char_unstripped': Literal(" Foo "),
}])
read_record = Char(char.id)
self.assertEqual(read_record.char, "Foo")
self.assertEqual(read_record.char_lstripped, "Foo ")
self.assertEqual(read_record.char_rstripped, " Foo")
self.assertEqual(read_record.char_unstripped, " Foo ")
@with_transaction()
def test_write_strip_with_sql_value(self):
"Test write with stripping with SQL value"
Char = self.Char()
char, = Char.create([{
'char': "Foo",
'char_lstripped': "Foo",
'char_rstripped': "Foo",
'char_unstripped': "Foo",
}])
Char.write([char], {
'char': Literal(" Bar "),
'char_lstripped': Literal(" Bar "),
'char_rstripped': Literal(" Bar "),
'char_unstripped': Literal(" Bar "),
})
read_record = Char(char.id)
self.assertEqual(read_record.char, "Bar")
self.assertEqual(read_record.char_lstripped, "Bar ")
self.assertEqual(read_record.char_rstripped, " Bar")
self.assertEqual(read_record.char_unstripped, " Bar ")
class FieldCharTranslatedTestCase(unittest.TestCase, CommonTestCaseMixin):
"Test Field Char Translated"