From 335b0b6851ac465c87f5d3751917fe0f616b9e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 18 Mar 2014 19:54:32 +0100 Subject: [PATCH] Add history revisions issue3678 review3551002 --- CHANGELOG | 1 + doc/ref/models/models.rst | 5 ++++ trytond/ir/model.py | 7 ++++++ trytond/model/modelsql.py | 44 +++++++++++++++++++++++++++++++++-- trytond/tests/test_history.py | 39 +++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index aae718270d..a87c3912a0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add history revisions * Add the multi selection widget * Add index to one2many's on_change * Remove auto-refresh on Action Window diff --git a/doc/ref/models/models.rst b/doc/ref/models/models.rst index 45ac96ffa6..1e6931df9c 100644 --- a/doc/ref/models/models.rst +++ b/doc/ref/models/models.rst @@ -401,6 +401,11 @@ Class methods: Could be overrided to use a custom SQL query instead of a table of the database. It should return a SQL FromItem. +.. classmethod:: ModelSQL.history_revisions(ids) + + Return a sorted list of all revisions for ids. The list is composed of + the date, id and username of the revision. + .. classmethod:: ModelStorage.search(domain[, offset[, limit[, order[, count[, query]]]]]) Return a list of records that match the :ref:`domain ` or diff --git a/trytond/ir/model.py b/trytond/ir/model.py index d3aadb1d8f..cb633031bd 100644 --- a/trytond/ir/model.py +++ b/trytond/ir/model.py @@ -68,6 +68,7 @@ class Model(ModelSQL, ModelView): cls._order.insert(0, ('model', 'ASC')) cls.__rpc__.update({ 'list_models': RPC(), + 'list_history': RPC(), 'global_search': RPC(), }) @@ -129,6 +130,12 @@ class Model(ModelSQL, ModelView): ]) return [m.model for m in models] + @classmethod + def list_history(cls): + 'Return a list of all models with history' + return [name for name, model in Pool().iterobject() + if getattr(model, '_history', False)] + @classmethod def create(cls, vlist): pool = Pool() diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py index b3cf9cf2da..48e48f16e1 100644 --- a/trytond/model/modelsql.py +++ b/trytond/model/modelsql.py @@ -1,8 +1,9 @@ #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 re +import datetime from functools import reduce -from itertools import islice, izip +from itertools import islice, izip, chain from sql import Table, Column, Literal, Desc, Asc, Expression, Flavor from sql.functions import Now, Extract @@ -10,7 +11,7 @@ from sql.conditionals import Coalesce from sql.operators import Or, And, Operator from sql.aggregate import Count, Max -from trytond.model import ModelStorage +from trytond.model import ModelStorage, ModelView from trytond.model import fields from trytond import backend from trytond.tools import reduce_ids @@ -19,6 +20,7 @@ from trytond.transaction import Transaction from trytond.pool import Pool from trytond.cache import LRUDict from trytond.exceptions import ConcurrencyException +from trytond.rpc import RPC _RE_UNIQUE = re.compile('UNIQUE\s*\((.*)\)', re.I) _RE_CHECK = re.compile('CHECK\s*\((.*)\)', re.I) @@ -38,6 +40,10 @@ class ModelSQL(ModelStorage): cls._sql_constraints = [] cls._order = [('id', 'ASC')] cls._sql_error_messages = {} + if issubclass(cls, ModelView): + cls.__rpc__.update({ + 'history_revisions': RPC(), + }) if not cls._table: cls._table = cls.__name__.replace('.', '_') @@ -228,6 +234,40 @@ class ModelSQL(ModelStorage): if name in exception[0]: cls.raise_user_error(error) + @classmethod + def history_revisions(cls, ids): + pool = Pool() + ModelAccess = pool.get('ir.model.access') + User = pool.get('res.user') + cursor = Transaction().cursor + + ModelAccess.check(cls.__name__, 'read') + + table = cls.__table_history__() + user = User.__table__() + revisions = [] + in_max = cursor.IN_MAX + for i in range(0, len(ids), in_max): + sub_ids = ids[i:i + in_max] + where = reduce_ids(table.id, sub_ids) + cursor.execute(*table.join(user, 'LEFT', + Coalesce(table.write_uid, table.create_uid) == user.id) + .select( + Coalesce(table.write_date, table.create_date), + table.id, + user.name, + where=where)) + revisions.append(cursor.fetchall()) + revisions = list(chain(*revisions)) + revisions.sort(reverse=True) + # SQLite uses char for COALESCE + if revisions and isinstance(revisions[0][0], basestring): + strptime = datetime.datetime.strptime + format_ = '%Y-%m-%d %H:%M:%S.%f' + revisions = [(strptime(timestamp, format_), id_, name) + for timestamp, id_, name in revisions] + return revisions + @classmethod def __insert_history(cls, ids, deleted=False): transaction = Transaction() diff --git a/trytond/tests/test_history.py b/trytond/tests/test_history.py index 5ff6c7d4ba..6d82ab433f 100644 --- a/trytond/tests/test_history.py +++ b/trytond/tests/test_history.py @@ -104,6 +104,45 @@ class HistoryTestCase(unittest.TestCase): history = History(history_id) self.assertEqual(history.value, value) + def test0030history_revisions(self): + 'Test history revisions' + History = POOL.get('test.history') + + with Transaction().start(DB_NAME, USER, + context=CONTEXT) as transaction: + history = History(value=1) + history.save() + history_id = history.id + first = history.create_date + + transaction.cursor.commit() + + with Transaction().start(DB_NAME, USER, + context=CONTEXT) as transaction: + history = History(history_id) + history.value = 2 + history.save() + second = history.write_date + + transaction.cursor.commit() + + with Transaction().start(DB_NAME, USER, + context=CONTEXT) as transaction: + history = History(history_id) + history.value = 3 + history.save() + third = history.write_date + + transaction.cursor.commit() + + with Transaction().start(DB_NAME, USER, context=CONTEXT): + revisions = History.history_revisions([history_id]) + self.assertEqual(revisions, [ + (third, history_id, u'Administrator'), + (second, history_id, u'Administrator'), + (first, history_id, u'Administrator'), + ]) + def suite(): return unittest.TestLoader().loadTestsFromTestCase(HistoryTestCase)