Add TimeDelta field

And remove float_time widget.

issue4026
review5911002
This commit is contained in:
Cédric Krier
2015-02-09 22:58:56 +01:00
parent 0f468bde64
commit 0991a552de
19 changed files with 363 additions and 39 deletions
+2
View File
@@ -1,3 +1,5 @@
* Remove float_time widget
* Add TimeDelta field
* search_global yields record instead of id
* Add ModelTestCase
* Add test for missing default model access
+14
View File
@@ -364,6 +364,20 @@ A time, represented in Python by a ``datetime.time`` instance.
Same as :attr:`DateTime.format`
TimeDelta
---------
.. class::TimeDelta(string[, converter[, \**options]])
An interval, represented in Python by a ``datetime.timedelta`` instance.
.. attribute:: TimeDelta.converter
The name of the context key containing the time converter.
A time converter is a dictionary with the keys: ``s`` (second), ``m``
(minute), ``h`` (hour), ``d`` (day), ``w`` (week), ``M`` (month), ``Y``
(year) and the value in second.
Binary
------
+4 -2
View File
@@ -9,9 +9,9 @@ from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from psycopg2.extensions import register_type, register_adapter
from psycopg2.extensions import UNICODE, AsIs
try:
from psycopg2.extensions import PYDATE, PYDATETIME, PYTIME
from psycopg2.extensions import PYDATE, PYDATETIME, PYTIME, PYINTERVAL
except ImportError:
PYDATE, PYDATETIME, PYTIME = None, None, None
PYDATE, PYDATETIME, PYTIME, PYINTERVAL = None, None, None, None
from psycopg2 import IntegrityError as DatabaseIntegrityError
from psycopg2 import OperationalError as DatabaseOperationalError
import time
@@ -379,5 +379,7 @@ if PYDATETIME:
register_type(PYDATETIME)
if PYTIME:
register_type(PYTIME)
if PYINTERVAL:
register_type(PYINTERVAL)
register_adapter(float, lambda value: AsIs(repr(value)))
register_adapter(Decimal, lambda value: AsIs(str(value)))
+3
View File
@@ -419,3 +419,6 @@ sqlite.register_adapter(datetime.datetime, adapt_datetime)
sqlite.register_adapter(datetime.time, lambda val: val.isoformat())
sqlite.register_converter('TIME', lambda val: datetime.time(*map(int,
val.split(':'))))
sqlite.register_adapter(datetime.timedelta, lambda val: val.total_seconds())
sqlite.register_converter('INTERVAL', lambda val: datetime.timedelta(
seconds=float(val)))
+32 -9
View File
@@ -2,7 +2,7 @@
# this repository contains the full copyright notices and license terms.
import datetime
import time
from sql import Literal
from sql import Literal, Null
from sql.aggregate import Count, Max
from ..model import ModelView, ModelSQL, fields
@@ -45,9 +45,10 @@ class Trigger(ModelSQL, ModelView):
limit_number = fields.Integer('Limit Number', required=True,
help='Limit the number of call to "Action Function" by records.\n'
'0 for no limit.')
minimum_delay = fields.Float('Minimum Delay', help='Set a minimum delay '
'in minutes between call to "Action Function" for the same record.\n'
'0 for no delay.')
minimum_time_delay = fields.TimeDelta('Minimum Delay',
help='Set a minimum time delay between call to "Action Function" '
'for the same record.\n'
'empty for no delay.')
action_model = fields.Many2One('ir.model', 'Action Model', required=True)
action_function = fields.Char('Action Function', required=True)
_get_triggers_cache = Cache('ir_trigger.get_triggers')
@@ -66,6 +67,29 @@ class Trigger(ModelSQL, ModelView):
})
cls._order.insert(0, ('name', 'ASC'))
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
cursor = Transaction().cursor
table = TableHandler(cursor, cls, module_name)
sql_table = cls.__table__()
super(Trigger, cls).__register__(module_name)
# Migration from 3.4:
# change minimum_delay into timedelta minimu_time_delay
if table.column_exist('minimum_delay'):
cursor.execute(*sql_table.select(
sql_table.id, sql_table.minimum_delay,
where=sql_table.minimum_delay != Null))
for id_, delay in cursor.fetchall():
delay = datetime.timedelta(hours=delay)
cursor.execute(*sql_table.update(
[sql_table.minimu_time_delay],
[delay],
where=sql_table.id == id_))
table.drop_column('minimum_delay')
@classmethod
def validate(cls, triggers):
super(Trigger, cls).validate(triggers)
@@ -182,8 +206,8 @@ class Trigger(ModelSQL, ModelView):
new_ids.append(record_id)
ids = new_ids
# Filter on minimum_delay
if trigger.minimum_delay:
# Filter on minimum_time_delay
if trigger.minimum_time_delay:
new_ids = []
for sub_ids in grouped_slice(ids):
sub_ids = list(sub_ids)
@@ -211,15 +235,14 @@ class Trigger(ModelSQL, ModelView):
delay[record_id] = datetime.datetime(year, month, day,
hours, minutes, seconds, microseconds)
if (datetime.datetime.now() - delay[record_id]
>= datetime.timedelta(
minutes=trigger.minimum_delay)):
>= trigger.minimum_time_delay):
new_ids.append(record_id)
ids = new_ids
records = Model.browse(ids)
if records:
getattr(ActionModel, trigger.action_function)(records, trigger)
if trigger.limit_number or trigger.minimum_delay:
if trigger.limit_number or trigger.minimum_time_delay:
to_create = []
for record in records:
to_create.append({
+1 -2
View File
@@ -49,7 +49,7 @@ attlist.field &=
| "selection"
| "char"
| "password"
| "float_time"
| "timedelta"
| "boolean"
| "reference"
| "binary"
@@ -98,7 +98,6 @@ attlist.field &=
| "top_to_bottom"
}?
attlist.field &= attribute spell { text }?
attlist.field &= attribute float_time { text }?
attlist.field &=
[a:defaultValue = "0"] attribute filename_visible { "0" | "1" }?
attlist.field &= [a:defaultValue = "0"] attribute pre_validate { "0" | "1" }?
+1 -6
View File
@@ -148,7 +148,7 @@
<value>selection</value>
<value>char</value>
<value>password</value>
<value>float_time</value>
<value>timedelta</value>
<value>boolean</value>
<value>reference</value>
<value>binary</value>
@@ -327,11 +327,6 @@
<attribute name="spell"/>
</optional>
</define>
<define name="attlist.field" combine="interleave">
<optional>
<attribute name="float_time"/>
</optional>
</define>
<define name="attlist.field" combine="interleave">
<optional>
<attribute name="filename_visible" a:defaultValue="0">
+1 -2
View File
@@ -23,8 +23,7 @@ attlist.field &= attribute domain { text }?
attlist.field &= attribute fill { "0" | "1" }?
attlist.field &= attribute empty { "0" | "1" }?
attlist.field &= attribute color { text }?
attlist.field &= attribute widget { "float_time" }?
attlist.field &= attribute float_time { text }?
attlist.field &= attribute timedelta { text }?
attlist.field &=
[ a:defaultValue = "linear" ]
attribute interpolation { "constant-left" | "constant-right" | "constant-center" | "linear" }?
+1 -8
View File
@@ -115,14 +115,7 @@
</define>
<define name="attlist.field" combine="interleave">
<optional>
<attribute name="widget">
<value>float_time</value>
</attribute>
</optional>
</define>
<define name="attlist.field" combine="interleave">
<optional>
<attribute name="float_time"/>
<attribute name="timedelta"/>
</optional>
</define>
<define name="attlist.field" combine="interleave">
+1 -2
View File
@@ -27,7 +27,7 @@ attlist.field &=
| "selection"
| "float"
| "numeric"
| "float_time"
| "timedelta"
| "integer"
| "datetime"
| "time"
@@ -58,7 +58,6 @@ attlist.field &=
| "bottom_to_top"
| "top_to_bottom"
}?
attlist.field &= attribute float_time { text }?
attlist.field &= [a:defaultValue = "0"] attribute pre_validate { "0" | "1" }?
attlist.field &= [a:defaultValue = "1"] attribute completion { "0" | "1" }?
attlist.field &= attribute string { text }?
+1 -6
View File
@@ -97,7 +97,7 @@
<value>selection</value>
<value>float</value>
<value>numeric</value>
<value>float_time</value>
<value>timedelta</value>
<value>integer</value>
<value>datetime</value>
<value>time</value>
@@ -168,11 +168,6 @@
</attribute>
</optional>
</define>
<define name="attlist.field" combine="interleave">
<optional>
<attribute name="float_time"/>
</optional>
</define>
<define name="attlist.field" combine="interleave">
<optional>
<attribute name="pre_validate" a:defaultValue="0">
+2 -2
View File
@@ -22,8 +22,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="condition" colspan="3"/>
<label name="limit_number"/>
<field name="limit_number"/>
<label name="minimum_delay"/>
<field name="minimum_delay" widget="float_time"/>
<label name="minimum_time_delay"/>
<field name="minimum_time_delay"/>
<label name="action_model"/>
<field name="action_model"/>
<label name="action_function"/>
+33
View File
@@ -134,3 +134,36 @@ class Time(DateTime):
def sql_type(self):
return SQLType('TIME', 'TIME')
class TimeDelta(Field):
'''
Define a timedelta field (``timedelta``).
'''
_type = 'timedelta'
def __init__(self, string='', converter=None, help='', required=False,
readonly=False, domain=None, states=None, select=False,
on_change=None, on_change_with=None, depends=None,
context=None, loading='eager'):
'''
:param converter: The name of the context key containing
the time converter.
'''
super(TimeDelta, 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, context=context, loading=loading)
self.converter = converter
@staticmethod
def sql_format(value):
if isinstance(value, (Query, Expression)):
return value
if value is None:
return None
assert(isinstance(value, datetime.timedelta))
return value
def sql_type(self):
return SQLType('INTERVAL', 'INTERVAL')
+1
View File
@@ -212,6 +212,7 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
'filename',
'selection_change_with',
'domain',
'converter',
):
if getattr(cls._fields[field], arg, None) is not None:
value = getattr(cls._fields[field], arg)
+7
View File
@@ -55,6 +55,8 @@ JSONDecoder.register('date',
JSONDecoder.register('time',
lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
dct['microsecond']))
JSONDecoder.register('timedelta',
lambda dct: datetime.timedelta(seconds=dct['seconds']))
JSONDecoder.register('buffer', lambda dct:
buffer(base64.decodestring(dct['base64'])))
JSONDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
@@ -105,6 +107,11 @@ JSONEncoder.register(datetime.time,
'second': o.second,
'microsecond': o.microsecond,
})
JSONEncoder.register(datetime.timedelta,
lambda o: {
'__class__': 'timedelta',
'seconds': o.total_seconds(),
})
JSONEncoder.register(buffer,
lambda o: {
'__class__': 'buffer',
+10
View File
@@ -54,11 +54,19 @@ def dump_time(self, value, write):
}
self.dump_struct(value, write)
def dump_timedelta(self, value, write):
value = {'__class__': 'timedelta',
'seconds': value.total_seconds(),
}
self.dump_struct(value, write)
xmlrpclib.Marshaller.dispatch[Decimal] = dump_decimal
xmlrpclib.Marshaller.dispatch[type(None)] = \
lambda self, value, write: write("<value><nil/></value>")
xmlrpclib.Marshaller.dispatch[datetime.date] = dump_date
xmlrpclib.Marshaller.dispatch[datetime.time] = dump_time
xmlrpclib.Marshaller.dispatch[datetime.timedelta] = dump_timedelta
xmlrpclib.Marshaller.dispatch[buffer] = dump_buffer
@@ -94,6 +102,8 @@ XMLRPCDecoder.register('date',
XMLRPCDecoder.register('time',
lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
dct['microsecond']))
XMLRPCDecoder.register('timedelta',
lambda dct: datetime.timedelta(seconds=dct['seconds']))
XMLRPCDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
+3
View File
@@ -54,6 +54,9 @@ def register():
TimeDefault,
TimeRequired,
TimeFormat,
TimeDelta,
TimeDeltaDefault,
TimeDeltaRequired,
One2One,
One2OneTarget,
One2OneRelation,
+26
View File
@@ -16,6 +16,7 @@ __all__ = [
'Date', 'DateDefault', 'DateRequired',
'DateTime', 'DateTimeDefault', 'DateTimeRequired', 'DateTimeFormat',
'Time', 'TimeDefault', 'TimeRequired', 'TimeFormat',
'TimeDelta', 'TimeDeltaDefault', 'TimeDeltaRequired',
'One2One', 'One2OneTarget', 'One2OneRelation', 'One2OneRequired',
'One2OneRequiredRelation', 'One2OneDomain', 'One2OneDomainRelation',
'One2Many', 'One2ManyTarget',
@@ -315,6 +316,31 @@ class TimeFormat(ModelSQL):
time = fields.Time(string='Time', format='%H:%M')
class TimeDelta(ModelSQL):
'TimeDelta'
__name__ = 'test.timedelta'
timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta',
required=False)
class TimeDeltaDefault(ModelSQL):
'TimeDelta Default'
__name__ = 'test.timedelta_default'
timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta',
required=False)
@staticmethod
def default_timedelta():
return datetime.timedelta(seconds=3600)
class TimeDeltaRequired(ModelSQL):
'TimeDelta Required'
__name__ = 'test.timedelta_required'
timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta',
required=True)
class One2One(ModelSQL):
'One2One'
__name__ = 'test.one2one'
+220
View File
@@ -67,6 +67,10 @@ class FieldsTestCase(unittest.TestCase):
self.time_required = POOL.get('test.time_required')
self.time_format = POOL.get('test.time_format')
self.timedelta = POOL.get('test.timedelta')
self.timedelta_default = POOL.get('test.timedelta_default')
self.timedelta_required = POOL.get('test.timedelta_required')
self.one2one = POOL.get('test.one2one')
self.one2one_target = POOL.get('test.one2one.target')
self.one2one_required = POOL.get('test.one2one_required')
@@ -3258,6 +3262,222 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
def test0200timedelta(self):
'Test timedelta'
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
minute = datetime.timedelta(minutes=1)
hour = datetime.timedelta(hours=1)
day = datetime.timedelta(days=1)
default_timedelta = datetime.timedelta(seconds=3600)
timedelta1, = self.timedelta.create([{
'timedelta': hour,
}])
self.assert_(timedelta1)
self.assertEqual(timedelta1.timedelta, hour)
timedelta = self.timedelta.search([
('timedelta', '=', hour),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', '=', day),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', '=', None),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', '!=', day),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', '!=', None),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', 'in', [hour]),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', 'in', [day]),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', 'in', [minute]),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', 'in', [None]),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', 'in', []),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', 'not in', [hour]),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', 'not in', [day]),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', 'not in', [None]),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', 'not in', []),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', '<', day),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', '<', minute),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', '<', hour),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', '<=', hour),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', '<=', minute),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', '<=', day),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', '>', day),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', '>', minute),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', '>', hour),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', '>=', day),
])
self.assertEqual(timedelta, [])
timedelta = self.timedelta.search([
('timedelta', '>=', minute),
])
self.assertEqual(timedelta, [timedelta1])
timedelta = self.timedelta.search([
('timedelta', '>=', hour),
])
self.assertEqual(timedelta, [timedelta1])
timedelta2, = self.timedelta.create([{
'timedelta': minute,
}])
self.assert_(timedelta2)
self.assertEqual(timedelta2.timedelta, minute)
timedelta = self.timedelta.search([
('timedelta', '=', minute),
])
self.assertEqual(timedelta, [timedelta2])
timedelta = self.timedelta.search([
('timedelta', 'in', [minute, hour]),
])
self.assertEqual(timedelta, [timedelta1, timedelta2])
timedelta = self.timedelta.search([
('timedelta', 'not in', [minute, hour]),
])
self.assertEqual(timedelta, [])
timedelta3, = self.timedelta.create([{}])
self.assert_(timedelta3)
self.assertEqual(timedelta3.timedelta, None)
timedelta4, = self.timedelta_default.create([{}])
self.assert_(timedelta4)
self.assertEqual(timedelta4.timedelta, default_timedelta)
self.timedelta.write([timedelta1], {
'timedelta': minute,
})
self.assertEqual(timedelta1.timedelta, minute)
self.timedelta.write([timedelta2], {
'timedelta': day,
})
self.assertEqual(timedelta2.timedelta, day)
self.assertRaises(Exception, self.timedelta.create, [{
'timedelta': 'test',
}])
self.assertRaises(Exception, self.timedelta.write, [timedelta1], {
'timedelta': 'test',
})
self.assertRaises(Exception, self.timedelta.create, [{
'timedelta': 1,
}])
self.assertRaises(Exception, self.timedelta.write, [timedelta1], {
'timedelta': 1,
})
self.assertRaises(UserError, self.timedelta_required.create, [{}])
transaction.cursor.rollback()
timedelta6, = self.timedelta_required.create([{
'timedelta': day,
}])
self.assert_(timedelta6)
timedelta7, = self.timedelta.create([{
'timedelta': None,
}])
self.assert_(timedelta7)
transaction.cursor.rollback()
def suite():
return unittest.TestLoader().loadTestsFromTestCase(FieldsTestCase)