From 151342698776539c452e0d8275411be81dead4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sat, 29 May 2010 15:58:49 +0200 Subject: [PATCH] Add ir.trigger to trigger ModelStorage change --- CHANGELOG | 1 + doc/ref/models/models.rst | 21 ++ trytond/ir/__init__.py | 1 + trytond/ir/__tryton__.py | 1 + trytond/ir/trigger.py | 287 ++++++++++++++++++++++ trytond/ir/trigger.xml | 78 ++++++ trytond/model/modelsql.py | 12 + trytond/model/modelstorage.py | 96 ++++++++ trytond/res/ir.xml | 49 ++++ trytond/test/__init__.py | 1 + trytond/test/trigger.py | 20 ++ trytond/tests/test_trigger.py | 451 ++++++++++++++++++++++++++++++++++ trytond/tests/test_tryton.py | 2 + 13 files changed, 1020 insertions(+) create mode 100644 trytond/ir/trigger.py create mode 100644 trytond/ir/trigger.xml create mode 100644 trytond/test/trigger.py create mode 100644 trytond/tests/test_trigger.py 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. --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -494,5 +524,24 @@ 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():