diff --git a/CHANGELOG b/CHANGELOG
index d21545a51d..e89b883454 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Add ir.trigger to trigger ModelStorage change
* Add default value to Boolean fields at database level
Version 1.6.0 - 2010-05-08
diff --git a/doc/ref/models/models.rst b/doc/ref/models/models.rst
index ec3c1fa192..9f5fa0b9e3 100644
--- a/doc/ref/models/models.rst
+++ b/doc/ref/models/models.rst
@@ -203,6 +203,11 @@ Instance methods:
Create a record. ``values`` is a dictionary with fields names as key and
created values as value.
+.. method:: ModelStorage.trigger_create(cursor, user, id[, context])
+
+ Trigger create actions. It will call actions defined in ``ir.trigger`` if
+ ``on_create`` is set and ``condition`` is true.
+
.. method:: ModelStorage.read(cursor, user, ids[, fields_names[, context]])
Return values for the ids. If ``fields_names`` is set, there will be only
@@ -217,10 +222,26 @@ Instance methods:
``values`` is a dictionary with fields names as key and writen values as
value.
+.. method:: ModelStorage.trigger_write_get_eligibles(cursor, user, ids[, context])
+
+ Return eligible record ids for write actions by triggers. This dictionary
+ is to pass to :method:`~ModelStorage.trigger_write`.
+
+.. method:: ModelStorage.trigger_write(cursor, user, eligibles[, context])
+
+ Trigger write actions. It will call actions defined in ``ir.trigger`` if
+ ``on_write`` is set and ``condition`` was false before
+ :method:`~ModelStorage.write` and true after.
+
.. method:: ModelStorage.delete(cursor, user, ids[, context])
Delete records. ``ids`` can be a list of ids or an id.
+.. method:: ModelStorage.trigger_delete(cursor, user, ids[, context])
+
+ Trigger delete actions. It will call actions defined in ``ir.trigger`` if
+ ``on_delete`` is set and ``condition`` is true.
+
.. method:: ModelStorage.copy(cursor, user, ids[, default[, context]])
Duplicate the records. ``ids`` can be a list of ids or an id. ``default``
diff --git a/trytond/ir/__init__.py b/trytond/ir/__init__.py
index 9abd3e7c11..ee6e216edd 100644
--- a/trytond/ir/__init__.py
+++ b/trytond/ir/__init__.py
@@ -15,3 +15,4 @@ import module
from default import *
from cache import *
from date import *
+from trigger import *
diff --git a/trytond/ir/__tryton__.py b/trytond/ir/__tryton__.py
index 62c627950b..c491d51efc 100644
--- a/trytond/ir/__tryton__.py
+++ b/trytond/ir/__tryton__.py
@@ -40,6 +40,7 @@ Fournit les concepts et l'administration des modèles, actions, séquences, loca
'property.xml',
'module/module.xml',
'default.xml',
+ 'trigger.xml',
],
'translation': [
'fr_FR.csv',
diff --git a/trytond/ir/trigger.py b/trytond/ir/trigger.py
new file mode 100644
index 0000000000..30b4107462
--- /dev/null
+++ b/trytond/ir/trigger.py
@@ -0,0 +1,287 @@
+#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 datetime
+import time
+from trytond.model import ModelView, ModelSQL, fields
+from trytond.pyson import Eval, Or
+from trytond.tools import Cache, safe_eval
+from trytond.backend import TableHandler
+from trytond.tools import reduce_ids
+
+
+class Trigger(ModelSQL, ModelView):
+ "Trigger"
+ _name = 'ir.trigger'
+ _description = __doc__
+ name = fields.Char('Name', required=True, translate=True)
+ active = fields.Boolean('Active', select=2)
+ model = fields.Many2One('ir.model', 'Model', required=True, select=1)
+ on_time = fields.Boolean('On Time', select=1, states={
+ 'invisible': Or(Eval('on_create', False), Eval('on_write', False),
+ Eval('on_delete', False)),
+ }, depends=['on_create', 'on_write', 'on_delete'],
+ on_change=['on_time'])
+ on_create = fields.Boolean('On Create', select=1, states={
+ 'invisible': Eval('on_time', False),
+ }, depends=['on_time'],
+ on_change=['on_create'])
+ on_write = fields.Boolean('On Write', select=1, states={
+ 'invisible': Eval('on_time', False),
+ }, depends=['on_time'],
+ on_change=['on_write'])
+ on_delete = fields.Boolean('On Delete', select=1, states={
+ 'invisible': Eval('on_time', False),
+ }, depends=['on_time'],
+ on_change=['on_delete'])
+ condition = fields.Char('Condition', required=True,
+ help='A Python statement evaluated with record represented by '
+ '"self"\nIt triggers the action if true.')
+ limit_number = fields.Integer('Limit Number', 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 minimu delay '
+ 'in minutes between call to "Action Function" for the same record.\n'
+ '0 for no delay.')
+ action_model = fields.Many2One('ir.model', 'Action Model', required=True)
+ action_function = fields.Char('Action Function', required=True)
+
+ def __init__(self):
+ super(Trigger, self).__init__()
+ self._sql_constraints += [
+ ('on_exclusive',
+ 'CHECK(NOT(on_time AND (on_create OR on_write OR on_delete)))',
+ '"On Time" and others are mutually exclusive!'),
+ ]
+ self._constraints += [
+ ('check_condition', 'invalid_condition'),
+ ]
+ self._error_messages.update({
+ 'invalid_condition': 'Condition must be a python expression!',
+ })
+ self._order.insert(0, ('name', 'ASC'))
+
+ def check_condition(self, cursor, user, ids):
+ '''
+ Check condition
+ '''
+ for trigger in self.browse(cursor, user, ids):
+ try:
+ compile(trigger.condition, '', 'eval')
+ except (SyntaxError, TypeError):
+ return False
+ return True
+
+ def default_active(self, cursor, user, context=None):
+ return True
+
+ def on_change_on_time(self, cursor, user, values, context=None):
+ if values.get('on_time'):
+ return {
+ 'on_create': False,
+ 'on_write': False,
+ 'on_delete': False,
+ }
+ return {}
+
+ def on_change_on_create(self, cursor, user, values, context=None):
+ if values.get('on_create'):
+ return {
+ 'on_time': False,
+ }
+ return {}
+
+ def on_change_on_write(self, cursor, user, values, context=None):
+ if values.get('on_write'):
+ return {
+ 'on_time': False,
+ }
+ return {}
+
+ def on_change_on_delete(self, cursor, user, values, context=None):
+ if values.get('on_delete'):
+ return {
+ 'on_time': False,
+ }
+ return {}
+
+ @Cache('ir_trigger.get_triggers')
+ def get_triggers(self, cursor, user, model_name, mode):
+ """
+ Return trigger ids for a model and a mode
+
+ :param cursor: the database cursor
+ :param user: the user id
+ :param model_name: the name of the model
+ :param mode: the mode that can be 'create', 'write', 'delete' or 'time'
+ :return: a list of ir.trigger ids
+ """
+ assert mode in ['create', 'write', 'delete', 'time'], \
+ 'Invalid trigger mode'
+
+ if user == 0:
+ return [] # XXX is it want we want?
+
+ trigger_ids = self.search(cursor, user, [
+ ('model.model', '=', model_name),
+ ('on_%s' % mode, '=', True),
+ ])
+ return trigger_ids
+
+ def eval(self, cursor, user, trigger, record, context=None):
+ """
+ Evaluate the condition of trigger
+
+ :param cursor: the database cursor
+ :param user: the user id
+ :param trigger: a BrowseRecord of ir.trigger
+ :param record: a BrowseRecord of the tested model
+ :param context: the context
+ :return: a boolean
+ """
+ if context is None:
+ context = {}
+ model_obj = self.pool.get(trigger.model.model)
+ env = {}
+ env.update(context)
+ env['current_date'] = datetime.datetime.today()
+ env['time'] = time
+ env['context'] = context
+ env['self'] = record
+ return bool(safe_eval(trigger.condition, env))
+
+ def trigger_action(self, cursor, user, ids, trigger_id, context=None):
+ """
+ Trigger the action define on trigger_id for the ids
+
+ :param cursor: the database cursor
+ :param user: the user id
+ :param ids: the list of record ids triggered
+ :param trigger_id: the trigger id
+ :param context: the context
+ """
+ trigger_log_obj = self.pool.get('ir.trigger.log')
+ trigger = self.browse(cursor, user, trigger_id, context=context)
+ model_obj = self.pool.get(trigger.action_model.model)
+
+ # Filter on limit_number
+ if trigger.limit_number:
+ new_ids = []
+ for i in range(0, len(ids), cursor.IN_MAX):
+ sub_ids = ids[i:i + cursor.IN_MAX]
+ red_sql, red_ids = reduce_ids('"record_id"', sub_ids)
+ cursor.execute('SELECT "record_id", COUNT(1) FROM "%s" '
+ 'WHERE %s AND "trigger" = %%s '
+ 'GROUP BY "record_id"'
+ % (trigger_log_obj._table, red_sql),
+ red_ids + [trigger.id])
+ number = dict(cursor.fetchall())
+ for record_id in sub_ids:
+ if record_id not in number:
+ new_ids.append(record_id)
+ continue
+ if number[record_id] < trigger.limit_number:
+ new_ids.append(record_id)
+ ids = new_ids
+
+ # Filter on minimum_delay
+ if trigger.minimum_delay:
+ new_ids = []
+ for i in range(0, len(ids), cursor.IN_MAX):
+ sub_ids = ids[i:i + cursor.IN_MAX]
+ red_sql, red_ids = reduce_ids('"record_id"', sub_ids)
+ cursor.execute('SELECT "record_id", MAX("create_date") '
+ 'FROM "%s" '
+ 'WHERE %s AND "trigger" = %%s '
+ 'GROUP BY "record_id"'
+ % (trigger_log_obj._table, red_sql),
+ red_ids + [trigger.id])
+ delay = dict(cursor.fetchall())
+ for record_id in sub_ids:
+ if record_id not in delay:
+ new_ids.append(record_id)
+ continue
+ # SQLite return string for MAX
+ if isinstance(delay[record_id], basestring):
+ datepart, timepart = delay[record_id].split(" ")
+ year, month, day = map(int, datepart.split("-"))
+ timepart_full = timepart.split(".")
+ hours, minutes, seconds = map(int, timepart_full[0].split(":"))
+ if len(timepart_full) == 2:
+ microseconds = int(timepart_full[1])
+ 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):
+ new_ids.append(record_id)
+ ids = new_ids
+
+ if ids:
+ getattr(model_obj, trigger.action_function)(cursor, user, ids,
+ trigger_id, context=context)
+ if trigger.limit_number or trigger.minimum_delay:
+ for record_id in ids:
+ trigger_log_obj.create(cursor, user, {
+ 'trigger': trigger.id,
+ 'record_id': record_id,
+ }, context=context)
+
+ def trigger_time(self, cursor, user, context=None):
+ '''
+ Trigger time actions
+
+ :param cursor: the database cursor
+ :param user: the user id
+ :param context: the context
+ '''
+ trigger_ids = self.search(cursor, user, [
+ ('on_time', '=', True),
+ ], context=context)
+ for trigger in self.browse(cursor, user, trigger_ids, context=context):
+ model_obj = self.pool.get(trigger.model.model)
+ triggered_ids = []
+ # TODO add a domain
+ record_ids = model_obj.search(cursor, user, [], context=context)
+ for record in model_obj.browse(cursor, user, record_ids,
+ context=context):
+ if self.eval(cursor, user, trigger, record, context=context):
+ triggered_ids.append(record.id)
+ if triggered_ids:
+ self.trigger_action(cursor, user, triggered_ids, trigger.id,
+ context=context)
+
+ def create(self, cursor, user, values, context=None):
+ res = super(Trigger, self).create(cursor, user, values, context=context)
+ # Restart the cache on the get_triggers method of ir.trigger
+ self.get_triggers(cursor.dbname)
+ return res
+
+ def write(self, cursor, user, ids, values, context=None):
+ res = super(Trigger, self).write(cursor, user, ids, values,
+ context=context)
+ # Restart the cache on the get_triggers method of ir.trigger
+ self.get_triggers(cursor.dbname)
+ return res
+
+ def delete(self, cursor, user, ids, context=None):
+ res = super(Trigger, self).delete(cursor, user, ids, context=context)
+ # Restart the cache on the get_triggers method of ir.trigger
+ self.get_triggers(cursor.dbname)
+ return res
+
+Trigger()
+
+
+class TriggerLog(ModelSQL):
+ 'Trigger Log'
+ _name = 'ir.trigger.log'
+ _description = __doc__
+ trigger = fields.Many2One('ir.trigger', 'Trigger', required=True)
+ record_id = fields.Integer('Record ID', required=True)
+
+ def init(self, cursor, module_name):
+ super(TriggerLog, self).init(cursor, module_name)
+
+ table = TableHandler(cursor, self, module_name)
+ table.index_action(['trigger', 'record_id'], 'add')
+
+TriggerLog()
diff --git a/trytond/ir/trigger.xml b/trytond/ir/trigger.xml
new file mode 100644
index 0000000000..c7dacf1968
--- /dev/null
+++ b/trytond/ir/trigger.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+ ir.trigger
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ]]>
+
+
+
+ ir.trigger
+ tree
+
+
+
+
+
+
+
+
+
+
+ ]]>
+
+
+
+ Triggers
+ ir.trigger
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py
index 33df209aac..a104bf5715 100644
--- a/trytond/model/modelsql.py
+++ b/trytond/model/modelsql.py
@@ -417,6 +417,8 @@ class ModelSQL(ModelStorage):
self._update_tree(cursor, user, id_new, k, field.left,
field.right)
+ self.trigger_create(cursor, user, id_new, context=context)
+
return id_new
def read(self, cursor, user, ids, fields_names=None, context=None):
@@ -716,6 +718,11 @@ class ModelSQL(ModelStorage):
return res
def write(self, cursor, user, ids, values, context=None):
+
+ # Call before cursor cache cleaning
+ trigger_eligibles = self.trigger_write_get_eligibles(cursor, user,
+ isinstance(ids, (int, long)) and [ids] or ids, context=context)
+
super(ModelSQL, self).write(cursor, user, ids, values, context=context)
if context is None:
@@ -966,6 +973,9 @@ class ModelSQL(ModelStorage):
field.left, field.right)
else:
self._rebuild_tree(cursor, 0, k, False, 0)
+
+ self.trigger_write(cursor, user, trigger_eligibles, context=context)
+
return True
def delete(self, cursor, user, ids, context=None):
@@ -1067,6 +1077,8 @@ class ModelSQL(ModelStorage):
self.raise_user_error(cursor, 'access_error',
self._description, context=context)
+ self.trigger_delete(cursor, user, ids, context=context)
+
for i in range(0, len(ids), cursor.IN_MAX):
sub_ids = ids[i:i + cursor.IN_MAX]
red_sql, red_ids = reduce_ids('id', sub_ids)
diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py
index 2fddf09cff..c4cb3ffe69 100644
--- a/trytond/model/modelstorage.py
+++ b/trytond/model/modelstorage.py
@@ -88,6 +88,26 @@ class ModelStorage(Model):
self.__clean_xxx2many_cache(cursor, user, context=context)
return False
+ def trigger_create(self, cursor, user, id, context=None):
+ '''
+ Trigger create actions
+
+ :param cursor: the database cursor
+ :param user: the user id
+ :param id: the created id
+ :param context: the context
+ '''
+ trigger_obj = self.pool.get('ir.trigger')
+ trigger_ids = trigger_obj.get_triggers(cursor, user, self._name, 'create')
+ if not trigger_ids:
+ return
+ record = self.browse(cursor, user, id, context=context)
+ triggers = trigger_obj.browse(cursor, user, trigger_ids, context=context)
+ for trigger in triggers:
+ if trigger_obj.eval(cursor, user, trigger, record, context=context):
+ trigger_obj.trigger_action(cursor, user, [id], trigger.id,
+ context=context)
+
def read(self, cursor, user, ids, fields_names=None, context=None):
'''
Read records.
@@ -144,6 +164,58 @@ class ModelStorage(Model):
self.__clean_xxx2many_cache(cursor, user, context=context)
return False
+ def trigger_write_get_eligibles(self, cursor, user, ids, context=None):
+ '''
+ Return eligible ids for write actions by triggers
+
+ :param cursor: the database cursor
+ :param user: the user id
+ :param ids: a list of ids
+ :param context: the context
+ :return: a dictionary of the lists of eligible ids by triggers
+ '''
+ trigger_obj = self.pool.get('ir.trigger')
+ trigger_ids = trigger_obj.get_triggers(cursor, user, self._name, 'write')
+ if not trigger_ids:
+ return {}
+ records = self.browse(cursor, user, ids, context=context)
+ triggers = trigger_obj.browse(cursor, user, trigger_ids, context=context)
+ eligibles = {}
+ for trigger in triggers:
+ eligibles[trigger.id] = []
+ for record in records:
+ if not trigger_obj.eval(cursor, user, trigger, record,
+ context=context):
+ eligibles[trigger.id].append(record.id)
+ return eligibles
+
+ def trigger_write(self, cursor, user, eligibles, context=None):
+ '''
+ Trigger write actions
+
+ :param cursor: the database cursor
+ :param user: the user id
+ :param eligibles: a dictionary of the lists of eligible ids by triggers
+ :param context: the context
+ '''
+ trigger_obj = self.pool.get('ir.trigger')
+ trigger_ids = eligibles.keys()
+ if not trigger_ids:
+ return
+ records = self.browse(cursor, user, chain(*eligibles.values()),
+ context=context)
+ id2record = dict((x.id, x) for x in records)
+ triggers = trigger_obj.browse(cursor, user, trigger_ids, context=context)
+ for trigger in triggers:
+ triggered_ids = []
+ for record_id in eligibles[trigger.id]:
+ record = id2record[record_id]
+ if trigger_obj.eval(cursor, user, trigger, record, context=context):
+ triggered_ids.append(record.id)
+ if triggered_ids:
+ trigger_obj.trigger_action(cursor, user, triggered_ids,
+ trigger.id, context=context)
+
def delete(self, cursor, user, ids, context=None):
'''
Delete records.
@@ -176,6 +248,30 @@ class ModelStorage(Model):
self.__clean_xxx2many_cache(cursor, user, context=context)
return False
+ def trigger_delete(self, cursor, user, ids, context=None):
+ '''
+ Trigger delete actions
+
+ :param cursor: the database cursor
+ :param user: the user id
+ :param ids: the deleted ids
+ :param context: the context
+ '''
+ trigger_obj = self.pool.get('ir.trigger')
+ trigger_ids = trigger_obj.get_triggers(cursor, user, self._name, 'delete')
+ if not trigger_ids:
+ return
+ records = self.browse(cursor, user, ids, context=context)
+ triggers = trigger_obj.browse(cursor, user, trigger_ids, context=context)
+ for trigger in triggers:
+ triggered_ids = []
+ for record in records:
+ if trigger_obj.eval(cursor, user, trigger, record, context=context):
+ triggered_ids.append(record.id)
+ if triggered_ids:
+ trigger_obj.trigger_action(cursor, user, triggered_ids,
+ trigger.id, context=context)
+
def copy(self, cursor, user, ids, default=None, context=None):
'''
Duplicate the record(s) in ids.
diff --git a/trytond/res/ir.xml b/trytond/res/ir.xml
index d43f52d8bc..10429a79ff 100644
--- a/trytond/res/ir.xml
+++ b/trytond/res/ir.xml
@@ -348,6 +348,36 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ user_cron_trigger
+ Cron Trigger
+
+
+
+
+ Run On Time Triggers
+
+
+
+ 5
+ minutes
+ -1
+
+ ir.trigger
+ trigger_time
+
+
diff --git a/trytond/test/__init__.py b/trytond/test/__init__.py
index 5fe3bc17f4..0af27ec012 100644
--- a/trytond/test/__init__.py
+++ b/trytond/test/__init__.py
@@ -6,3 +6,4 @@ from model import *
from mptt import *
from import_data import *
from export_data import *
+from trigger import *
diff --git a/trytond/test/trigger.py b/trytond/test/trigger.py
new file mode 100644
index 0000000000..af41bf891e
--- /dev/null
+++ b/trytond/test/trigger.py
@@ -0,0 +1,20 @@
+#This file is part of Tryton. The COPYRIGHT file at the top level of
+#this repository contains the full copyright notices and license terms.
+from trytond.model import ModelSQL, fields
+
+TRIGGER_LOGS = []
+
+class Triggered(ModelSQL):
+ 'Triggered'
+ _name = 'test.triggered'
+ _description = __doc__
+
+ name = fields.Char('Name')
+
+ def trigger(self, cursor, user, ids, trigger_id, context=None):
+ '''
+ Trigger function for test
+ '''
+ TRIGGER_LOGS.append((ids, trigger_id))
+
+Triggered()
diff --git a/trytond/tests/test_trigger.py b/trytond/tests/test_trigger.py
new file mode 100644
index 0000000000..29b437d464
--- /dev/null
+++ b/trytond/tests/test_trigger.py
@@ -0,0 +1,451 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#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 unittest
+import sys
+import time
+try:
+ from itertools import combinations
+except ImportError:
+ def combinations(iterable, r):
+ pool = tuple(iterable)
+ n = len(pool)
+ if r > n:
+ return
+ indices = range(r)
+ yield tuple(pool[i] for i in indices)
+ while True:
+ for i in reversed(range(r)):
+ if indices[i] != i + n - r:
+ break
+ else:
+ return
+ indices[i] += 1
+ for j in range(i+1, r):
+ indices[j] = indices[j-1] + 1
+ yield tuple(pool[i] for i in indices)
+from trytond.tests.test_tryton import POOL, DB, USER, CONTEXT, install_module
+from trytond.test.trigger import TRIGGER_LOGS
+
+
+class TriggerTestCase(unittest.TestCase):
+ '''
+ Test Trigger
+ '''
+
+ def setUp(self):
+ install_module('test')
+ self.triggered = POOL.get('test.triggered')
+ self.trigger = POOL.get('ir.trigger')
+ self.trigger_log = POOL.get('ir.trigger.log')
+ self.model = POOL.get('ir.model')
+
+ def test0010constraints(self):
+ '''
+ Test constraints
+ '''
+ cursor = DB.cursor()
+
+ model_id = self.model.search(cursor, USER, [
+ ('model', '=', 'test.triggered'),
+ ], context=CONTEXT)[0]
+
+ values = {
+ 'name': 'Test',
+ 'model': model_id,
+ 'on_time': True,
+ 'condition': 'True',
+ 'action_model': model_id,
+ 'action_function': 'test',
+ }
+ self.assert_(self.trigger.create(cursor, USER, values, context=CONTEXT))
+
+ # on_exclusive
+ for i in range(1, 4):
+ for combination in combinations(['create', 'write', 'delete'], i):
+ combination_values = values.copy()
+ for mode in combination:
+ combination_values['on_%s' % mode] = True
+ self.failUnlessRaises(Exception, self.trigger.create, cursor, USER,
+ combination_values, context=CONTEXT)
+
+ # check_condition
+ condition_values = values.copy()
+ condition_values['condition'] = '='
+ self.failUnlessRaises(Exception, self.trigger.create, cursor, USER,
+ condition_values, context=CONTEXT)
+
+ # Restart the cache on the get_triggers method of ir.trigger
+ self.trigger.get_triggers(cursor.dbname)
+ cursor.rollback()
+ cursor.close()
+
+ def test0020on_create(self):
+ '''
+ Test on_create
+ '''
+ cursor = DB.cursor()
+
+ model_id = self.model.search(cursor, USER, [
+ ('model', '=', 'test.triggered'),
+ ], context=CONTEXT)[0]
+
+ trigger_id = self.trigger.create(cursor, USER, {
+ 'name': 'Test',
+ 'model': model_id,
+ 'on_create': True,
+ 'condition': 'True',
+ 'action_model': model_id,
+ 'action_function': 'trigger',
+ }, context=CONTEXT)
+
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Test',
+ }, context=CONTEXT)
+
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+
+ # Trigger with condition
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'condition': 'self.name == "Bar"',
+ }, context=CONTEXT)
+
+ # Matching condition
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Bar',
+ }, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+
+ # Non matching condition
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Foo',
+ }, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [])
+
+ # With limit number
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'condition': 'True',
+ 'limit_number': 1,
+ }, context=CONTEXT)
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Test',
+ }, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+
+ # With minimum delay
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'limit_number': 0,
+ 'minimum_delay': 1,
+ }, context=CONTEXT)
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Test',
+ }, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+
+ # Restart the cache on the get_triggers method of ir.trigger
+ self.trigger.get_triggers(cursor.dbname)
+ cursor.rollback()
+ cursor.close()
+
+ def test0030on_write(self):
+ '''
+ Test on_write
+ '''
+ cursor = DB.cursor()
+
+ model_id = self.model.search(cursor, USER, [
+ ('model', '=', 'test.triggered'),
+ ], context=CONTEXT)[0]
+
+ trigger_id = self.trigger.create(cursor, USER, {
+ 'name': 'Test',
+ 'model': model_id,
+ 'on_write': True,
+ 'condition': 'True',
+ 'action_model': model_id,
+ 'action_function': 'trigger',
+ }, context=CONTEXT)
+
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Test',
+ }, context=CONTEXT)
+
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': 'Foo',
+ }, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [])
+
+ # Trigger with condition
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'condition': 'self.name == "Bar"',
+ }, context=CONTEXT)
+
+ # Matching condition
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': 'Bar',
+ }, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+
+ # No change in condition
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': 'Bar',
+ }, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [])
+
+ # Different change in condition
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': 'Foo',
+ }, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [])
+
+ # With limit number
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'condition': 'self.name == "Bar"',
+ 'limit_number': 1,
+ }, context=CONTEXT)
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Foo',
+ }, context=CONTEXT)
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': 'Bar',
+ }, context=CONTEXT)
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': 'Foo',
+ }, context=CONTEXT)
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': 'Bar',
+ }, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+
+ # With minimum delay
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'limit_number': 0,
+ 'minimum_delay': sys.maxint,
+ }, context=CONTEXT)
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Foo',
+ }, context=CONTEXT)
+ for name in ('Bar', 'Foo', 'Bar'):
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': name,
+ }, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'minimum_delay': 0.02,
+ }, context=CONTEXT)
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Foo',
+ }, context=CONTEXT)
+ for name in ('Bar', 'Foo'):
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': name,
+ }, context=CONTEXT)
+ time.sleep(1.2)
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': 'Bar',
+ }, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id),
+ ([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+ TRIGGER_LOGS.pop()
+
+ # Restart the cache on the get_triggers method of ir.trigger
+ self.trigger.get_triggers(cursor.dbname)
+ cursor.rollback()
+ cursor.close()
+
+ def test0040on_delete(self):
+ '''
+ Test on_delete
+ '''
+ cursor = DB.cursor()
+
+ model_id = self.model.search(cursor, USER, [
+ ('model', '=', 'test.triggered'),
+ ], context=CONTEXT)[0]
+
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Test',
+ }, context=CONTEXT)
+
+ trigger_id = self.trigger.create(cursor, USER, {
+ 'name': 'Test',
+ 'model': model_id,
+ 'on_delete': True,
+ 'condition': 'True',
+ 'action_model': model_id,
+ 'action_function': 'trigger',
+ }, context=CONTEXT)
+
+ self.triggered.delete(cursor, USER, triggered_id, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], triggered_id)])
+ TRIGGER_LOGS.pop()
+
+ # Trigger with condition
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'condition': 'self.name == "Bar"',
+ }, context=CONTEXT)
+
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Bar',
+ }, context=CONTEXT)
+
+ # Matching condition
+ self.triggered.delete(cursor, USER, triggered_id, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], triggered_id)])
+ TRIGGER_LOGS.pop()
+
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Foo',
+ }, context=CONTEXT)
+
+ # Non matching condition
+ self.triggered.delete(cursor, USER, triggered_id, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [])
+
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Test',
+ }, context=CONTEXT)
+
+ # With limit number
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'condition': 'True',
+ 'limit_number': 1,
+ }, context=CONTEXT)
+ self.triggered.delete(cursor, USER, triggered_id, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], triggered_id)])
+ TRIGGER_LOGS.pop()
+ # Delete trigger logs because SQLite reuse the same triggered_id
+ self.trigger_log.delete(cursor, USER,
+ self.trigger_log.search(cursor, USER, [
+ ('trigger', '=', trigger_id),
+ ], context=CONTEXT))
+
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Test',
+ }, context=CONTEXT)
+
+ # With minimum delay
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'limit_number': 0,
+ 'minimum_delay': 1,
+ }, context=CONTEXT)
+ self.triggered.delete(cursor, USER, triggered_id, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], triggered_id)])
+ TRIGGER_LOGS.pop()
+
+ # Restart the cache on the get_triggers method of ir.trigger
+ self.trigger.get_triggers(cursor.dbname)
+ cursor.rollback()
+ cursor.close()
+
+ def test0050on_time(self):
+ '''
+ Test on_time
+ '''
+ cursor = DB.cursor()
+
+ model_id = self.model.search(cursor, USER, [
+ ('model', '=', 'test.triggered'),
+ ], context=CONTEXT)[0]
+
+ trigger_id = self.trigger.create(cursor, USER, {
+ 'name': 'Test',
+ 'model': model_id,
+ 'on_time': True,
+ 'condition': 'True',
+ 'action_model': model_id,
+ 'action_function': 'trigger',
+ }, context=CONTEXT)
+
+ triggered_id = self.triggered.create(cursor, USER, {
+ 'name': 'Test',
+ }, context=CONTEXT)
+ self.trigger.trigger_time(cursor, USER, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+
+ # Trigger with condition
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'condition': 'self.name == "Bar"',
+ }, context=CONTEXT)
+
+ # Matching condition
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': 'Bar',
+ }, context=CONTEXT)
+ self.trigger.trigger_time(cursor, USER, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+
+ # Non matching condition
+ self.triggered.write(cursor, USER, triggered_id, {
+ 'name': 'Foo',
+ }, context=CONTEXT)
+ self.trigger.trigger_time(cursor, USER, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [])
+
+ # With limit number
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'condition': 'True',
+ 'limit_number': 1,
+ }, context=CONTEXT)
+ self.trigger.trigger_time(cursor, USER, context=CONTEXT)
+ self.trigger.trigger_time(cursor, USER, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+
+ # Delete trigger logs of limit number test
+ self.trigger_log.delete(cursor, USER,
+ self.trigger_log.search(cursor, USER, [
+ ('trigger', '=', trigger_id),
+ ], context=CONTEXT))
+
+ # With minimum delay
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'limit_number': 0,
+ 'minimum_delay': sys.maxint,
+ }, context=CONTEXT)
+ self.trigger.trigger_time(cursor, USER, context=CONTEXT)
+ self.trigger.trigger_time(cursor, USER, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+
+ # Delete trigger logs of previous minimum delay test
+ self.trigger_log.delete(cursor, USER,
+ self.trigger_log.search(cursor, USER, [
+ ('trigger', '=', trigger_id),
+ ], context=CONTEXT))
+
+ self.trigger.write(cursor, USER, trigger_id, {
+ 'minimum_delay': 0.02,
+ }, context=CONTEXT)
+ self.trigger.trigger_time(cursor, USER, context=CONTEXT)
+ time.sleep(1.2)
+ self.trigger.trigger_time(cursor, USER, context=CONTEXT)
+ self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id),
+ ([triggered_id], trigger_id)])
+ TRIGGER_LOGS.pop()
+ TRIGGER_LOGS.pop()
+
+ # Restart the cache on the get_triggers method of ir.trigger
+ self.trigger.get_triggers(cursor.dbname)
+ cursor.rollback()
+ cursor.close()
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(TriggerTestCase)
+
+if __name__ == '__main__':
+ suite = suite()
+ unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/trytond/tests/test_tryton.py b/trytond/tests/test_tryton.py
index c913305ff8..dd5121754e 100644
--- a/trytond/tests/test_tryton.py
+++ b/trytond/tests/test_tryton.py
@@ -172,6 +172,8 @@ def all_suite():
suite_.addTests(test_importdata.suite())
import trytond.tests.test_exportdata as test_exportdata
suite_.addTests(test_exportdata.suite())
+ import trytond.tests.test_trigger as test_trigger
+ suite_.addTests(test_trigger.suite())
return suite_
def modules_suite():