diff --git a/CHANGELOG b/CHANGELOG index 5911fc8595..c87d99f6b7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add time format validation * Remove ModelSQL.default_sequence Version 2.4.0 - 2012-04-23 diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst index f66c4a8539..82695ec1b3 100644 --- a/doc/ref/models/fields.rst +++ b/doc/ref/models/fields.rst @@ -272,17 +272,27 @@ A date, represented in Python by a ``datetime.date`` instance. DateTime -------- -.. class:: DateTime(string[, \**options]) +.. class:: DateTime(string[, format, \**options]) A date and time, represented in Python by a ``datetime.datetime`` instance. +.. attribute:: DateTime.format + + A string format as used by strftime. This format will be used to display + the time part of the field. The default value is `%H:%M:%S`. + The value can be replaced by a :class:`~trytond.pyson.PYSON` statement. + Time ---- -.. class:: Time(string[, \**options]) +.. class:: Time(string[, format, \**options]) A time, represented in Python by a ``datetime.time`` instance. +.. attribute:: Time.format + + Same as :attr:`DateTime.format` + Binary ------ diff --git a/trytond/ir/translation.xml b/trytond/ir/translation.xml index d9ea0627af..fc852bccf4 100644 --- a/trytond/ir/translation.xml +++ b/trytond/ir/translation.xml @@ -177,6 +177,15 @@ this repository contains the full copyright notices and license terms. --> ir + + time_format_validation_record + en_US + error + The time value of field "%s" on "%s" is not valid. + The time value of field "%s" on "%s" is not valid. + ir + + access_error en_US diff --git a/trytond/model/fields/date.py b/trytond/model/fields/date.py index 5301fcc3ac..7102d38f0f 100644 --- a/trytond/model/fields/date.py +++ b/trytond/model/fields/date.py @@ -17,8 +17,24 @@ class DateTime(Field): ''' _type = 'datetime' + def __init__(self, string='', format='%H:%M:%S', help='', required=False, + readonly=False, domain=None, states=None, select=False, + on_change=None, on_change_with=None, depends=None, + order_field=None, context=None, loading='eager'): + ''' + :param format: The validation format as used by strftime. + ''' + super(DateTime, self).__init__(string=string, help=help, + required=required, readonly=readonly, domain=domain, states=states, + select=select, on_change=on_change, on_change_with=on_change_with, + depends=depends, order_field=order_field, context=context, + loading=loading) + self.format = format -class Time(Field): + __init__.__doc__ += Field.__init__.__doc__ + + +class Time(DateTime): ''' Define a time field (``time``). ''' diff --git a/trytond/model/model.py b/trytond/model/model.py index 9ee98eb040..5d2df7a37d 100644 --- a/trytond/model/model.py +++ b/trytond/model/model.py @@ -554,10 +554,12 @@ class Model(WarningErrorMixin, URLMixin): and hasattr(self._columns[field], 'field'): res[field]['relation_field'] = copy.copy( self._columns[field].field) + if res[field]['type'] in ('datetime', 'time'): + res[field]['format'] = copy.copy(self._columns[field].format) # convert attributes into pyson for attr in ('states', 'domain', 'context', 'digits', - 'add_remove'): + 'add_remove', 'format'): if attr in res[field]: res[field][attr] = encoder.encode(res[field][attr]) diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index 6a09b4a765..8c8e1d19c3 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -19,7 +19,7 @@ from trytond.model import fields from trytond.model.browse import BrowseRecordList, BrowseRecord, \ BrowseRecordNull from trytond.model.browse import EvalEnvironment -from trytond.tools import safe_eval, reduce_domain +from trytond.tools import safe_eval, reduce_domain, datetime_strftime from trytond.pyson import PYSONEncoder, PYSONDecoder, PYSON from trytond.const import OPERATORS, RECORD_CACHE_SIZE from trytond.transaction import Transaction @@ -1132,6 +1132,35 @@ class ModelStorage(Model): digits_test(record[field_name], field.digits, field_name) + def format_test(value, format, field_name): + if not value: + return + if not isinstance(value, datetime.time): + value = value.time() + if value != datetime.datetime.strptime( + value.strftime(format), format).time(): + self.raise_user_error('time_format_validation_record', + error_args=self._get_error_args(field_name)) + + # validate time format + if (field._type in ('datetime', 'time') + and field_name not in ('create_date', 'write_date')): + if is_pyson(field.format): + pyson_format = PYSONDecoder().encode(field.format) + for record in records: + env = EvalEnvironment(record, self) + env.update(Transaction().context) + env['current_date'] = datetime.datetime.today() + env['time'] = time + env['context'] = Transaction().context + env['active_id'] = record.id + format = PYSONDecoder(env).decode(pyson_format) + format_test(record[field_name], format, field_name) + else: + for record in records: + format_test(record[field_name], field.format, + field_name) + def _clean_defaults(self, defaults): pool = Pool() vals = {} diff --git a/trytond/test/test.py b/trytond/test/test.py index bb83747de3..b7961247ee 100644 --- a/trytond/test/test.py +++ b/trytond/test/test.py @@ -356,6 +356,15 @@ class DateTimeRequired(ModelSQL): DateTimeRequired() +class DateTimeFormat(ModelSQL): + 'DateTime Format' + _name = 'test.datetime_format' + _description = __doc__ + datetime = fields.DateTime(string='DateTime', format='%H:%M') + +DateTimeFormat() + + class Time(ModelSQL): 'Time' _name = 'test.time' @@ -386,6 +395,15 @@ class TimeRequired(ModelSQL): TimeRequired() +class TimeFormat(ModelSQL): + 'Time Format' + _name = 'test.time_format' + _description = __doc__ + time = fields.Time(string='Time', format='%H:%M') + +TimeFormat() + + class One2One(ModelSQL): 'One2One' _name = 'test.one2one' diff --git a/trytond/tests/test_fields.py b/trytond/tests/test_fields.py index 8412fff41d..9fb25511c1 100644 --- a/trytond/tests/test_fields.py +++ b/trytond/tests/test_fields.py @@ -65,10 +65,12 @@ class FieldsTestCase(unittest.TestCase): self.datetime = POOL.get('test.datetime') self.datetime_default = POOL.get('test.datetime_default') self.datetime_required = POOL.get('test.datetime_required') + self.datetime_format = POOL.get('test.datetime_format') self.time = POOL.get('test.time') self.time_default = POOL.get('test.time_default') self.time_required = POOL.get('test.time_required') + self.time_format = POOL.get('test.time_format') self.one2one = POOL.get('test.one2one') self.one2one_target = POOL.get('test.one2one.target') @@ -2059,6 +2061,14 @@ class FieldsTestCase(unittest.TestCase): datetime9 = self.datetime.read(datetime9_id, ['datetime']) self.assert_(datetime9['datetime'] == today) + # Test format + self.assert_(self.datetime_format.create({ + 'datetime': datetime.datetime(2009, 1, 1, 12, 30), + })) + self.failUnlessRaises(Exception, self.datetime_format.create, { + 'datetime': datetime.datetime(2009, 1, 1, 12, 30, 25), + }) + transaction.cursor.rollback() def test0100time(self): @@ -2312,6 +2322,14 @@ class FieldsTestCase(unittest.TestCase): time9 = self.time.read(time9_id, ['time']) self.assert_(time9['time'] == evening) + # Test format + self.assert_(self.time_format.create({ + 'time': datetime.time(12, 30), + })) + self.failUnlessRaises(Exception, self.time_format.create, { + 'time': datetime.time(12, 30, 25), + }) + transaction.cursor.rollback() def test0110one2one(self):