diff --git a/.drone.yml b/.drone.yml index c45281213..8b3b37ff8 100644 --- a/.drone.yml +++ b/.drone.yml @@ -19,6 +19,8 @@ pipeline: services: postgresql: image: postgres + environment: + - POSTGRES_HOST_AUTH_METHOD=trust when: matrix: DATABASE: postgresql diff --git a/.hgtags b/.hgtags index f3916c4df..992b1c6fd 100644 --- a/.hgtags +++ b/.hgtags @@ -20,3 +20,22 @@ e7a6d0e8002237f624ecc8a45a6a472f7baadd08 4.6.0 321b0104a732419c11aa4926785502e0615b89c9 4.8.0 e8cadc044d59edff8a2ca73932c723d507517ec8 5.0.0 ef34e4e92d45ab2b2b2956bbb6aa8b3ca259eb13 5.2.0 +aee8cd9fecdfbf082030abd07cb2f98ed4bb839a 5.2.1 +06b6af6a1a7182b83884cc2a3000a12966d1e14c 5.2.2 +d06499aa4be143227c8f1aed0188106f2ee51c0c 5.2.3 +e49a560dd265f6d018abf3974b227c6a841a8d12 5.2.4 +99b0686c8702d2b41898c54bcdbfc36af8253a2b 5.2.5 +4363488df01bb84b337267279b6a5e0e3b61d0ab 5.2.6 +a533a499c493e206bf5fb8546038ba3e62af130b 5.2.7 +af82c05398fea9ffc5f3713a7cd9e2418168fbba 5.2.8 +5fb0c06625b777dbc55a615cc8d11ed1ae63c2af 5.2.9 +ab630bf13ef2514b0c2b24f4343619a6064a5f41 5.2.10 +2577bf7becc73304de840426946460250ad3e4ef 5.2.11 +d9d5240b1e8966f80f74f15933cb3fa641cba5c1 5.2.12 +8fc2d6f98d7b2b9a0f674e23a7ee486970a30902 5.2.13 +732633a7fe34a4a20c6408c0f33181505a05aeeb 5.2.14 +d647abbefe43bc434319efacbff63a0954e1a3d9 5.2.15 +8e77e3156c32026b61b2ebe535f4521402b69305 5.2.16 +ce808db9a1f59e4621c1b7eef8f297924b59ada5 5.2.17 +3bfd0307262590bcae72c4e6778544761ae74149 5.2.18 +aff310d8f73f6db33d60e6d2e865cb4dc1983522 5.2.19 diff --git a/CHANGELOG b/CHANGELOG index d10472738..9369859b4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,61 @@ +Version 5.2.19 - 2020-07-01 +* Bug fixes (see mercurial logs for details) + +Version 5.2.18 - 2020-06-03 +* Bug fixes (see mercurial logs for details) + +Version 5.2.17 - 2020-05-18 +* Bug fixes (see mercurial logs for details) + +Version 5.2.16 - 2020-05-15 +* Bug fixes (see mercurial logs for details) + +Version 5.2.15 - 2020-05-01 +* Bug fixes (see mercurial logs for details) + +Version 5.2.14 - 2020-04-04 +* Bug fixes (see mercurial logs for details) + +Version 5.2.13 - 2020-03-09 +* Bug fixes (see mercurial logs for details) +* Enable check_access context when checking wizard access (issue9108) + +Version 5.2.12 - 2020-02-02 +* Bug fixes (see mercurial logs for details) + +Version 5.2.11 - 2020-01-09 +* Bug fixes (see mercurial logs for details) + +Version 5.2.10 - 2019-12-16 +* Bug fixes (see mercurial logs for details) + +Version 5.2.9 - 2019-12-02 +* Bug fixes (see mercurial logs for details) + +Version 5.2.8 - 2019-11-08 +* Bug fixes (see mercurial logs for details) + +Version 5.2.7 - 2019-10-06 +* Bug fixes (see mercurial logs for details) + +Version 5.2.6 - 2019-09-15 +* Bug fixes (see mercurial logs for details) + +Version 5.2.5 - 2019-08-17 +* Bug fixes (see mercurial logs for details) + +Version 5.2.4 - 2019-08-01 +* Bug fixes (see mercurial logs for details) + +Version 5.2.3 - 2019-07-17 +* Bug fixes (see mercurial logs for details) + +Version 5.2.2 - 2019-07-01 +* Bug fixes (see mercurial logs for details) + +Version 5.2.1 - 2019-06-10 +* Bug fixes (see mercurial logs for details) + Version 5.2.0 - 2019-05-06 * Bug fixes (see mercurial logs for details) * Add sort and translate options to Reference field diff --git a/COPYRIGHT b/COPYRIGHT index 9f4714320..439a6eca2 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,9 +1,9 @@ Copyright (C) 2004-2008 Tiny SPRL. -Copyright (C) 2007-2019 Cédric Krier. +Copyright (C) 2007-2020 Cédric Krier. Copyright (C) 2007-2013 Bertrand Chenal. -Copyright (C) 2008-2019 B2CK SPRL. +Copyright (C) 2008-2020 B2CK SPRL. Copyright (C) 2011 Openlabs Technologies & Consulting (P) Ltd. -Copyright (C) 2011-2019 Nicolas Évrard. +Copyright (C) 2011-2020 Nicolas Évrard. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/doc/topics/configuration.rst b/doc/topics/configuration.rst index 15bb23faf..31d4bca5a 100644 --- a/doc/topics/configuration.rst +++ b/doc/topics/configuration.rst @@ -360,7 +360,7 @@ The time in seconds until the reset password expires. Default: `86400` (24h) passlib -------- +~~~~~~~ The path to the `INI file to load as CryptContext `_. diff --git a/doc/topics/views/index.rst b/doc/topics/views/index.rst index c3a5d1b7f..6477a070e 100644 --- a/doc/topics/views/index.rst +++ b/doc/topics/views/index.rst @@ -345,16 +345,6 @@ Display a button. The function may return an `ir.action` id or one of those client side action keywords: - * ``string``: The string that will be displayed inside the button. - - * ``confirm``: A string that will be shown in order to request - confirmation when clicking the button. - - * ``help``: see in common-attributes-help_. - -The button should be registered on ``ir.model.button`` where the default value -of the ``string``, ``confirm`` and ``help`` attributes can be can be defined. - .. _topics-views-client-actions: * ``new``: to create a new record @@ -384,6 +374,13 @@ of the ``string``, ``confirm`` and ``help`` attributes can be can be defined. toolbar. The valid values are the keywords starting with `form_` from :ref:`Actions ` without the `form_` part. + +.. warning:: + The button should be registered on ``ir.model.button`` where the default + value of the ``string``, ``confirm`` and ``help`` attributes can be can be + defined. + + notebook ^^^^^^^^ diff --git a/setup.py b/setup.py index 3cb851043..cc2d60b56 100755 --- a/setup.py +++ b/setup.py @@ -121,7 +121,6 @@ def run(self): 'Natural Language :: Turkish', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', diff --git a/trytond/__init__.py b/trytond/__init__.py index 394cf7807..f7e049ec8 100644 --- a/trytond/__init__.py +++ b/trytond/__init__.py @@ -5,7 +5,7 @@ import warnings from email import charset -__version__ = "5.2.0" +__version__ = "5.2.20" os.environ['TZ'] = 'UTC' if hasattr(time, 'tzset'): diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py index 69dc5f084..ef553112c 100644 --- a/trytond/backend/postgresql/database.py +++ b/trytond/backend/postgresql/database.py @@ -1,5 +1,6 @@ # 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 collections import defaultdict import time import logging import os @@ -108,7 +109,7 @@ class JSONContains(BinaryOperator): class Database(DatabaseInterface): _lock = RLock() - _databases = {} + _databases = defaultdict(dict) _connpool = None _list_cache = {} _list_cache_timestamp = {} @@ -130,13 +131,14 @@ class Database(DatabaseInterface): def __new__(cls, name='template1'): with cls._lock: now = datetime.now() - for database in list(cls._databases.values()): + databases = cls._databases[os.getpid()] + for database in list(databases.values()): if ((now - database._last_use).total_seconds() > _timeout and database.name != name and not database._connpool._used): database.close() - if name in cls._databases: - inst = cls._databases[name] + if name in databases: + inst = databases[name] else: if name == 'template1': minconn = 0 @@ -147,7 +149,7 @@ def __new__(cls, name='template1'): inst._connpool = ThreadedConnectionPool( minconn, _maxconn, **cls._connection_params(name), cursor_factory=LoggingCursor) - cls._databases[name] = inst + databases[name] = inst inst._last_use = datetime.now() return inst @@ -200,7 +202,7 @@ def close(self): with self._lock: logger.info('disconnect from "%s"', self.name) self._connpool.closeall() - self._databases.pop(self.name) + self._databases[os.getpid()].pop(self.name) @classmethod def create(cls, connection, database_name, template='template0'): diff --git a/trytond/bus.py b/trytond/bus.py index 9a1461cee..d46ba2888 100644 --- a/trytond/bus.py +++ b/trytond/bus.py @@ -213,7 +213,7 @@ def publish(cls, channel, message): cursor.execute('NOTIFY "%s", %%s' % cls._channel, (payload,)) -if config.get('bus', 'queue'): +if config.get('bus', 'class'): Bus = resolve(config.get('bus', 'class')) else: Bus = LongPollingBus diff --git a/trytond/cache.py b/trytond/cache.py index df428cb99..583e49c0f 100644 --- a/trytond/cache.py +++ b/trytond/cache.py @@ -16,7 +16,7 @@ from trytond import backend from trytond.config import config from trytond.transaction import Transaction -from trytond.tools import resolve +from trytond.tools import resolve, grouped_slice __all__ = ['BaseCache', 'Cache', 'LRUDict'] _clear_timeout = config.getint('cache', 'clean_timeout', default=5 * 60) @@ -165,8 +165,9 @@ def _clear(self, dbname, timestamp=None): @classmethod def sync(cls, transaction): - dbname = transaction.database.name - if not _clear_timeout and transaction.database.has_channel(): + database = transaction.database + dbname = database.name + if not _clear_timeout and database.has_channel(): with cls._listener_lock: if dbname not in cls._listener: cls._listener[dbname] = listener = threading.Thread( @@ -175,12 +176,17 @@ def sync(cls, transaction): return if (datetime.now() - cls._clean_last).total_seconds() < _clear_timeout: return - with transaction.connection.cursor() as cursor: - table = Table(cls._table) - cursor.execute(*table.select(_cast(table.timestamp), table.name)) - timestamps = {} - for timestamp, name in cursor.fetchall(): - timestamps[name] = timestamp + connection = database.get_connection(readonly=True, autocommit=True) + try: + with connection.cursor() as cursor: + table = Table(cls._table) + cursor.execute(*table.select( + _cast(table.timestamp), table.name)) + timestamps = {} + for timestamp, name in cursor.fetchall(): + timestamps[name] = timestamp + finally: + database.put_connection(connection) for name, timestamp in timestamps.items(): try: inst = cls._instances[name] @@ -197,39 +203,51 @@ def commit(cls, transaction): reset = cls._reset.setdefault(transaction, set()) if not reset: return - dbname = transaction.database.name - with transaction.connection.cursor() as cursor: - if not _clear_timeout and transaction.database.has_channel(): - cursor.execute( - 'NOTIFY "%s", %%s' % cls._channel, - (json.dumps(list(reset), separators=(',', ':')),)) - else: - for name in reset: - cursor.execute(*table.select(table.name, - where=table.name == name, - limit=1)) - if cursor.fetchone(): - # It would be better to insert only - cursor.execute(*table.update([table.timestamp], - [CurrentTimestamp()], + database = transaction.database + dbname = database.name + if not _clear_timeout and transaction.database.has_channel(): + with transaction.connection.cursor() as cursor: + # The count computed as + # 8000 (max notify size) / 64 (max name data len) + for sub_reset in grouped_slice(reset, 125): + cursor.execute( + 'NOTIFY "%s", %%s' % cls._channel, + (json.dumps(list(sub_reset), separators=(',', ':')),)) + else: + connection = database.get_connection( + readonly=False, autocommit=True) + try: + with connection.cursor() as cursor: + for name in reset: + cursor.execute(*table.select(table.name, table.id, + table.timestamp, + where=table.name == name, + limit=1)) + if cursor.fetchone(): + # It would be better to insert only + cursor.execute(*table.update([table.timestamp], + [CurrentTimestamp()], + where=table.name == name)) + else: + cursor.execute(*table.insert( + [table.timestamp, table.name], + [[CurrentTimestamp(), name]])) + + cursor.execute(*table.select( + Max(table.timestamp), + where=table.name == name)) + timestamp, = cursor.fetchone() + + cursor.execute(*table.select( + _cast(Max(table.timestamp)), where=table.name == name)) - else: - cursor.execute(*table.insert( - [table.timestamp, table.name], - [[CurrentTimestamp(), name]])) - - cursor.execute(*table.select( - Max(table.timestamp), - where=table.name == name)) - timestamp, = cursor.fetchone() - - cursor.execute(*table.select( - _cast(Max(table.timestamp)), - where=table.name == name)) - timestamp, = cursor.fetchone() - - inst = cls._instances[name] - inst._clear(dbname, timestamp) + timestamp, = cursor.fetchone() + + inst = cls._instances[name] + inst._clear(dbname, timestamp) + connection.commit() + finally: + database.put_connection(connection) reset.clear() @classmethod diff --git a/trytond/convert.py b/trytond/convert.py index b6df78c64..eb135eaa9 100644 --- a/trytond/convert.py +++ b/trytond/convert.py @@ -764,7 +764,7 @@ def write_records(self, module, model, fs_values = old_values.copy() fs_values.update(new_values) - if values != old_values: + if values != fs_values: self.grouped_model_data.extend(([self.ModelData(mdata_id)], { 'fs_id': fs_id, 'model': model, diff --git a/trytond/exceptions.py b/trytond/exceptions.py index d66fa217c..5e9dfa72c 100644 --- a/trytond/exceptions.py +++ b/trytond/exceptions.py @@ -14,7 +14,7 @@ def __init__(self, message, description=''): self.description = description self.code = 1 - def __unicode__(self): + def __str__(self): return '%s - %s' % (self.message, self.description) @@ -29,7 +29,7 @@ def __init__(self, name, message, description=''): self.description = description self.code = 2 - def __unicode__(self): + def __str__(self): return '%s - %s' % (self.message, self.description) @@ -55,7 +55,7 @@ def __init__(self, message): self.message = message self.code = 4 - def __unicode__(self): + def __str__(self): return self.message @@ -68,5 +68,5 @@ class MissingDependenciesException(TrytonException): def __init__(self, missings): self.missings = missings - def __unicode__(self): + def __str__(self): return 'Missing dependencies: %s' % ' '.join(self.missings) diff --git a/trytond/ir/action.py b/trytond/ir/action.py index 9c2e5d272..a0c86cc46 100644 --- a/trytond/ir/action.py +++ b/trytond/ir/action.py @@ -636,14 +636,14 @@ def get_report_content_html(cls, reports, name): @classmethod def set_report_content_html(cls, reports, name, value): - value = value.encode('utf-8') + if value is not None: + value = value.encode('utf-8') cls.set_report_content(reports, name[:-5], value) @fields.depends('name', 'template_extension') def on_change_with_report_content_name(self, name=None): - if not self.name: - return - return ''.join([self.name, os.extsep, self.template_extension]) + return ''.join( + filter(None, [self.name, os.extsep, self.template_extension])) @classmethod def get_pyson(cls, reports, name): @@ -668,6 +668,7 @@ def copy(cls, reports, default=None): for report in reports: if report.report: default['report_content'] = None + default['report'] = None default['report_name'] = report.report_name new_reports.extend(super(ActionReport, cls).copy([report], default=default)) diff --git a/trytond/ir/model.py b/trytond/ir/model.py index 89d5fe729..167bac1bc 100644 --- a/trytond/ir/model.py +++ b/trytond/ir/model.py @@ -1239,6 +1239,17 @@ def load_values(cls, values): @classmethod @ModelView.button def sync(cls, records): + def settable(Model, fieldname): + try: + field = Model._fields[fieldname] + except KeyError: + return False + + if isinstance(field, fields.Function) and not field.setter: + return False + + return True + with Transaction().set_user(0): pool = Pool() to_write = [] @@ -1254,7 +1265,7 @@ def sync(cls, records): # if they come from version < 3.2 if values != fs_values: values = {f: v for f, v in fs_values.items() - if f in Model._fields} + if settable(Model, f)} record = Model(data.db_id) models_to_write[Model].extend(([record], values)) to_write.extend([[data], { diff --git a/trytond/ir/note.py b/trytond/ir/note.py index 8f988f195..16157b2eb 100644 --- a/trytond/ir/note.py +++ b/trytond/ir/note.py @@ -100,11 +100,13 @@ def set_unread(cls, notes, name, value): @classmethod def write(cls, notes, values, *args): # Avoid changing write meta data if only unread is set - if args or values.keys() != ['unread']: + if args or set(values.keys()) != {'unread'}: super(Note, cls).write(notes, values, *args) else: # Check access write and clean cache - ModelStorage.write(notes, values) + # Use __func__ to directly access ModelStorage's write method and + # pass it the right class + ModelStorage.write.__func__(cls, notes, values) cls.set_unread(notes, 'unread', values['unread']) diff --git a/trytond/ir/queue.py b/trytond/ir/queue.py index 2f42a1a96..1c63a5801 100644 --- a/trytond/ir/queue.py +++ b/trytond/ir/queue.py @@ -103,7 +103,7 @@ def pull(cls, database, connection, name=None): candidates.expected_at.nulls_first], limit=1)) next_timeout = With('seconds', query=candidates.select( - Min(Extract('second', + Min(Extract('EPOCH', candidates.scheduled_at - CurrentTimestamp()) ), where=candidates.scheduled_at >= CurrentTimestamp())) diff --git a/trytond/ir/translation.py b/trytond/ir/translation.py index dcc738a6f..021ae3c9c 100644 --- a/trytond/ir/translation.py +++ b/trytond/ir/translation.py @@ -11,7 +11,6 @@ from sql import Column, Null, Literal from sql.functions import Substring, Position from sql.conditionals import Case -from sql.operators import Or, And from sql.aggregate import Max from genshi.filters.i18n import extract as genshi_extract @@ -23,7 +22,7 @@ from ..model import ModelView, ModelSQL, fields from ..wizard import Wizard, StateView, StateTransition, StateAction, \ Button -from ..tools import file_open, reduce_ids, grouped_slice, cursor_dict +from ..tools import file_open, grouped_slice, cursor_dict from ..pyson import PYSONEncoder, Eval from ..transaction import Transaction from ..pool import Pool @@ -343,27 +342,20 @@ def get_ids(cls, name, ttype, lang, ids): translations.update( cls.get_ids(name, ttype, parent_lang, to_fetch)) - transaction = Transaction() - cursor = transaction.connection.cursor() - table = cls.__table__() - fuzzy_sql = table.fuzzy == False if Transaction().context.get('fuzzy_translation', False): - fuzzy_sql = None - in_max = transaction.database.IN_MAX // 7 - for sub_to_fetch in grouped_slice(to_fetch, in_max): - red_sql = reduce_ids(table.res_id, sub_to_fetch) - where = And(((table.lang == lang), - (table.type == ttype), - (table.name == name), - (table.value != ''), - (table.value != Null), - red_sql, - )) - if fuzzy_sql: - where &= fuzzy_sql - cursor.execute(*table.select(table.res_id, table.value, - where=where)) - translations.update(cursor) + fuzzy_clause = [] + else: + fuzzy_clause = [('fuzzy', '=', False)] + for sub_to_fetch in grouped_slice(to_fetch): + for translation in cls.search([ + ('lang', '=', lang), + ('type', '=', ttype), + ('name', '=', name), + ('value', '!=', ''), + ('value', '!=', None), + ('res_id', 'in', list(sub_to_fetch)), + ] + fuzzy_clause): + translations[translation.res_id] = translation.value # Don't store fuzzy translation in cache if not Transaction().context.get('fuzzy_translation', False): for res_id in to_fetch: @@ -523,10 +515,8 @@ def get_sources(cls, args): res = {} parent_args = [] parent_langs = [] - clause = [] + clauses = [] transaction = Transaction() - cursor = transaction.connection.cursor() - table = cls.__table__() if len(args) > transaction.database.IN_MAX: for sub_args in grouped_slice(args): res.update(cls.get_sources(list(sub_args))) @@ -549,17 +539,18 @@ def get_sources(cls, args): parent_args.append((name, ttype, parent_lang, source)) parent_langs.append(lang) res[(name, ttype, lang, source)] = None - where = And(((table.lang == lang), - (table.type == ttype), - (table.name == name), - (table.value != ''), - (table.value != Null), - (table.fuzzy == False), - (table.res_id == -1), - )) + clause = [ + ('lang', '=', lang), + ('type', '=', ttype), + ('name', '=', name), + ('value', '!=', ''), + ('value', '!=', None), + ('fuzzy', '=', False), + ('res_id', '=', -1), + ] if source is not None: - where &= table.src == source - clause.append(where) + clause.append(('src', '=', source)) + clauses.append(clause) # Get parent transactions if parent_args: @@ -569,17 +560,14 @@ def get_sources(cls, args): res[(name, ttype, lang, source)] = parent_src[ (name, ttype, parent_lang, source)] - if clause: - in_max = transaction.database.IN_MAX // 7 - for sub_clause in grouped_slice(clause, in_max): - cursor.execute(*table.select( - table.lang, table.type, table.name, table.src, - table.value, - where=Or(list(sub_clause)))) - for lang, ttype, name, source, value in cursor.fetchall(): - if (name, ttype, lang, source) not in args: - source = None - res[(name, ttype, lang, source)] = value + in_max = transaction.database.IN_MAX // 7 + for sub_clause in grouped_slice(clauses, in_max): + for translation in cls.search(['OR'] + list(sub_clause)): + key = (translation.name, translation.type, + translation.lang, translation.src) + if key not in args: + key = key[:-1] + (None,) + res[key] = translation.value for key in to_cache: cls._translation_cache.set(key, res[key]) return res diff --git a/trytond/ir/ui/form.rnc b/trytond/ir/ui/form.rnc index eda2a299f..362ad501a 100644 --- a/trytond/ir/ui/form.rnc +++ b/trytond/ir/ui/form.rnc @@ -113,7 +113,7 @@ attlist.image &= [ a:defaultValue = "0" ] attribute yexpand { "0" | "1" }? attlist.image &= [ a:defaultValue = "0" ] attribute yfill { "0" | "1" }? attlist.image &= - [ a:defaultValue = "0" ] attribute xexpand { "0" | "1" }? + [ a:defaultValue = "1" ] attribute xexpand { "0" | "1" }? attlist.image &= [ a:defaultValue = "48" ] attribute size {text }? attlist.image &= [ a:defaultValue = "0" ] attribute xfill { "0" | "1" }? @@ -132,7 +132,7 @@ attlist.separator &= attlist.separator &= [ a:defaultValue = "0" ] attribute yfill { "0" | "1" }? attlist.separator &= - [ a:defaultValue = "0" ] attribute xexpand { "0" | "1" }? + [ a:defaultValue = "1" ] attribute xexpand { "0" | "1" }? attlist.separator &= [ a:defaultValue = "0" ] attribute xfill { "0" | "1" }? attlist.separator &= attribute help { text }? diff --git a/trytond/ir/ui/form.rng b/trytond/ir/ui/form.rng index bd83b0f2d..9a770af35 100644 --- a/trytond/ir/ui/form.rng +++ b/trytond/ir/ui/form.rng @@ -520,7 +520,7 @@ - + xexpand 0 @@ -639,7 +639,7 @@ - + xexpand 0 diff --git a/trytond/ir/ui/view.py b/trytond/ir/ui/view.py index 01056559a..081a4fbf7 100644 --- a/trytond/ir/ui/view.py +++ b/trytond/ir/ui/view.py @@ -211,6 +211,7 @@ def write(cls, views, values, *args): class ShowViewStart(ModelView): 'Show view' __name__ = 'ir.ui.view.show.start' + __no_slots__ = True class ShowView(Wizard): diff --git a/trytond/model/model.py b/trytond/model/model.py index 508098b66..7b865b135 100644 --- a/trytond/model/model.py +++ b/trytond/model/model.py @@ -181,7 +181,7 @@ def fields_get(cls, fields_names=None): if fields_names and fname not in fields_names: continue definition[fname] = field.definition(cls, language) - if not accesses.get(field, {}).get('write', True): + if not accesses.get(fname, {}).get('write', True): definition[fname]['readonly'] = True states = decoder.decode(definition[fname]['states']) states.pop('readonly', None) @@ -265,9 +265,6 @@ def __int__(self): def __str__(self): return '%s,%s' % (self.__name__, self.id) - def __unicode__(self): - return '%s,%s' % (self.__name__, self.id) - def __repr__(self): if self.id is None or self.id < 0: return "Pool().get('%s')(**%s)" % (self.__name__, diff --git a/trytond/model/modelsingleton.py b/trytond/model/modelsingleton.py index 752ff9820..404da4b51 100644 --- a/trytond/model/modelsingleton.py +++ b/trytond/model/modelsingleton.py @@ -9,6 +9,12 @@ class ModelSingleton(ModelStorage): Define a singleton model in Tryton. """ + @classmethod + def __setup__(cls): + super().__setup__() + # Cache disable because it is used as a read by the client + cls.__rpc__['default_get'].cache = None + @classmethod def get_singleton(cls): ''' diff --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py index 89e156353..e0c9f4312 100644 --- a/trytond/model/modelsql.py +++ b/trytond/model/modelsql.py @@ -538,11 +538,13 @@ def __check_timestamp(cls, ids): '%s,%s' % (cls.__name__, id_)) except KeyError: continue - sql_type = fields.Numeric('timestamp').sql_type().base + if timestamp is None: + continue + sql_type = fields.Char('timestamp').sql_type().base where.append((table.id == id_) & (Extract('EPOCH', Coalesce(table.write_date, table.create_date) - ).cast(sql_type) > timestamp)) + ).cast(sql_type) != timestamp)) if where: cursor.execute(*table.select(table.id, where=where, limit=1)) if cursor.fetchone(): @@ -704,6 +706,7 @@ def read(cls, ids, fields_names): extra_fields.add(field.datetime_field) if field.context: extra_fields.update(fields.get_eval_fields(field.context)) + extra_fields.discard('id') all_fields = ( set(fields_names) | set(fields_related.keys()) | extra_fields) @@ -1587,6 +1590,8 @@ def validate(cls, records): clause &= Literal(False) clause &= operator(column, value) where |= clause + if isinstance(sql, Exclude) and sql.where: + where &= sql.where cursor.execute( *table.select(table.id, where=where, limit=1)) if cursor.fetchone(): diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index 1d5da76b9..d7fc46adf 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -857,11 +857,26 @@ def process_lines(data, prefix, fields_def, position=0, klass=cls): else: res = bool(int(value)) elif field_type == 'integer': - res = int(value) if value else None + if isinstance(value, int): + res = value + elif value: + res = int(value) + else: + res = None elif field_type == 'float': - res = float(value) if value else None + if isinstance(value, float): + res = value + elif value: + res = float(value) + else: + res = None elif field_type == 'numeric': - res = Decimal(value) if value else None + if isinstance(value, Decimal): + res = value + elif value: + res = Decimal(value) + else: + res = None elif field_type == 'date': if isinstance(value, datetime.date): res = value @@ -1071,7 +1086,8 @@ def validate_domain(field): in_max = Transaction().database.IN_MAX count = in_max // 10 new_domains = {} - for sub_domains in grouped_slice(list(domains.keys()), count): + for sub_domains in grouped_slice( + list(domains.keys()), count): grouped_domain = ['OR'] grouped_records = [] for d in sub_domains: @@ -1082,7 +1098,11 @@ def validate_domain(field): break grouped_domain.append( [('id', 'in', [r.id for r in relations]), d]) - new_domains[freeze(grouped_domain)] = grouped_records + else: + new_domains[freeze(grouped_domain)] = \ + grouped_records + continue + break else: domains = new_domains else: @@ -1425,26 +1445,30 @@ def overrided(item): ifields = islice(ifields, 0, threshold) ffields.update(ifields) + require_context_field = False # add datetime_field for field in list(ffields.values()): if hasattr(field, 'datetime_field') and field.datetime_field: datetime_field = self._fields[field.datetime_field] ffields[field.datetime_field] = datetime_field + require_context_field = True # add depends of field with context for field in list(ffields.values()): if field.context: eval_fields = fields.get_eval_fields(field.context) for context_field_name in eval_fields: - if context_field_name in field.depends: + if context_field_name not in field.depends: continue context_field = self._fields.get(context_field_name) + require_context_field = True if context_field not in ffields: ffields[context_field_name] = context_field def filter_(id_): - return (name not in self._cache.get(id_, {}) - and name not in self._local_cache.get(id_, {})) + return (id_ == self.id # Ensure the value is read + or (name not in self._cache.get(id_, {}) + and name not in self._local_cache.get(id_, {}))) def unique(ids): s = set() @@ -1512,7 +1536,8 @@ def instantiate(field, value, data): self._transaction.set_user(self._user), \ self._transaction.reset_context(), \ self._transaction.set_context(self._context): - if self.id in self._cache and name in self._cache[self.id]: + if (self.id in self._cache and name in self._cache[self.id] + and not require_context_field): # Use values from cache ids = islice(chain(islice(self._ids, index, None), islice(self._ids, 0, max(index - 1, 0))), diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py index 4c521c79e..6b51f302d 100644 --- a/trytond/model/modelview.py +++ b/trytond/model/modelview.py @@ -765,7 +765,9 @@ def _changed_values(self): if field._type in ('many2one', 'one2one', 'reference'): if value: if isinstance(value, ModelStorage): - changed['%s.rec_name' % fname] = value.rec_name + changed['%s.' % fname] = { + 'rec_name': value.rec_name, + } if value.id is None: # Don't consider temporary instance as a change continue @@ -775,16 +777,17 @@ def _changed_values(self): value = value.id elif field._type == 'one2many': targets = value - init_targets = list(init_values.get(fname, [])) + init_targets = list(init_values.get(fname, targets)) value = collections.defaultdict(list) value['remove'] = [t.id for t in init_targets if t.id] for i, target in enumerate(targets): if target.id in value['remove']: value['remove'].remove(target.id) - target_changed = target._changed_values - if target_changed: - target_changed['id'] = target.id - value['update'].append(target_changed) + if isinstance(target, ModelView): + target_changed = target._changed_values + if target_changed: + target_changed['id'] = target.id + value['update'].append(target_changed) else: if isinstance(target, ModelView): # Ensure initial values are returned because target diff --git a/trytond/modules/__init__.py b/trytond/modules/__init__.py index e39a0eeda..11185137d 100644 --- a/trytond/modules/__init__.py +++ b/trytond/modules/__init__.py @@ -14,6 +14,7 @@ from sql.functions import CurrentTimestamp import trytond.tools as tools +from trytond.cache import Cache from trytond.config import config from trytond.exceptions import MissingDependenciesException from trytond.transaction import Transaction @@ -266,6 +267,8 @@ def load_module_graph(graph, pool, update=None, lang=None): ])) module2state[module] = 'activated' + # Avoid clearing cache to prevent dead lock on ir.cache table + Cache.rollback(transaction) transaction.commit() if not update: diff --git a/trytond/report/report.py b/trytond/report/report.py index 11504a695..d42e2a45a 100644 --- a/trytond/report/report.py +++ b/trytond/report/report.py @@ -2,6 +2,7 @@ # this repository contains the full copyright notices and license terms. import datetime import os +import inspect import logging import subprocess import tempfile @@ -229,8 +230,6 @@ def __int__(self): def __str__(self): return '%s,%s' % (Model.__name__, self.id) - def __unicode__(self): - return '%s,%s' % (Model.__name__, self.id) return [TranslateModel(id) for id in ids] @classmethod @@ -308,7 +307,8 @@ def convert(cls, report, data, timeout=5 * 60): path = os.path.join( dtemp, report.report_name + os.extsep + input_format) oext = FORMAT2EXT.get(output_format, output_format) - with open(path, 'wb+') as fp: + mode = 'w+' if isinstance(data, str) else 'wb+' + with open(path, mode) as fp: fp.write(data) try: cmd = ['soffice', @@ -364,7 +364,7 @@ def get_email(report, record, languages): pool = Pool() ActionReport = pool.get('ir.action.report') report_id = None - if isinstance(report, Report): + if inspect.isclass(report) and issubclass(report, Report): Report_ = report else: if isinstance(report, ActionReport): diff --git a/trytond/res/user.py b/trytond/res/user.py index c481464d6..70d355407 100644 --- a/trytond/res/user.py +++ b/trytond/res/user.py @@ -901,6 +901,8 @@ def __setup__(cls): 'depends': ['state'], }, }) + # Do not cache default_key as it depends on time + cls.__rpc__['default_get'].cache = None @classmethod def default_key(cls): @@ -945,6 +947,11 @@ def check(cls, key, application): def create(cls, vlist): pool = Pool() User = pool.get('res.user') + vlist = [v.copy() for v in vlist] + for values in vlist: + # Ensure we get a different key for each record + # default methods are called only once + values.setdefault('key', cls.default_key()) applications = super(UserApplication, cls).create(vlist) User._get_preferences_cache.clear() return applications diff --git a/trytond/sendmail.py b/trytond/sendmail.py index c290a074a..1557a9cac 100644 --- a/trytond/sendmail.py +++ b/trytond/sendmail.py @@ -3,6 +3,7 @@ import logging import smtplib from email.message import Message +from email.utils import formatdate from urllib.parse import parse_qs, unquote_plus from .config import config, parse_uri @@ -31,6 +32,8 @@ def sendmail(from_addr, to_addrs, msg, server=None): quit = True else: quit = False + if 'Date' not in msg: + msg['Date'] = formatdate() try: senderrs = server.sendmail(from_addr, to_addrs, msg.as_string()) except smtplib.SMTPException: diff --git a/trytond/tests/field_context.py b/trytond/tests/field_context.py index e575b7dc7..61d2ec98a 100644 --- a/trytond/tests/field_context.py +++ b/trytond/tests/field_context.py @@ -17,7 +17,9 @@ class FieldContextParent(ModelSQL): child = fields.Many2One('test.field_context.child', 'Child', context={ 'name': Eval('name'), - }) + 'rec_name': Eval('rec_name'), + }, + depends=['name']) class FieldContextChild(ModelSQL): diff --git a/trytond/tests/modelsql.py b/trytond/tests/modelsql.py index 34c855bf2..0ab9346f4 100644 --- a/trytond/tests/modelsql.py +++ b/trytond/tests/modelsql.py @@ -1,5 +1,6 @@ # 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 sql import Literal from sql.operators import Equal from trytond.model import ModelSQL, fields, Check, Unique, Exclude @@ -111,13 +112,19 @@ class ModelExclude(ModelSQL): "ModelSQL with exclude constraint" __name__ = 'test.modelsql.exclude' value = fields.Integer("Value") + condition = fields.Boolean("Condition") + + @classmethod + def default_condition(cls): + return True @classmethod def __setup__(cls): super(ModelExclude, cls).__setup__() t = cls.__table__() cls._sql_constraints = [ - ('exclude', Exclude(t, (t.value, Equal), where=t.value > 0), + ('exclude', Exclude(t, (t.value, Equal), + where=t.condition == Literal(True)), "Value must be unique."), ] diff --git a/trytond/tests/test_cache.py b/trytond/tests/test_cache.py index de7cb532e..539112586 100644 --- a/trytond/tests/test_cache.py +++ b/trytond/tests/test_cache.py @@ -93,16 +93,28 @@ def test_memory_cache_transactions(self): cache.set('foo', 'baz') self.assertEqual(cache.get('foo'), 'baz') - Transaction().set_current_transaction(transaction1) - self.addCleanup(transaction1.stop) - self.assertEqual(cache.get('foo'), 'bar') + with Transaction().set_current_transaction(transaction1): + self.assertEqual(cache.get('foo'), 'bar') transaction2.commit() for n in range(10): - if cache.get('foo') is None: + if cache.get('foo') == 'baz': break self.wait_cache_sync() - self.assertEqual(cache.get('foo'), None) + self.assertEqual(cache.get('foo'), 'baz') + + def test_memory_cache_nested_transactions(self): + "Test MemoryCache with nested transactions" + # Create entry in the cache table to trigger 2 updates + with Transaction().start(DB_NAME, USER): + cache.clear() + # Ensure sync is performed on start + time.sleep(cache_mod._clear_timeout) + + with Transaction().start(DB_NAME, USER) as transaction1: + cache.clear() + with transaction1.new_transaction(): + cache.clear() def test_memory_cache_sync(self): "Test MemoryCache synchronisation" @@ -124,6 +136,7 @@ def test_memory_cache_old_transaction(self): self.addCleanup(transaction2.stop) cache.clear() transaction2.commit() + self.wait_cache_sync() # Set value from old transaction Transaction().set_current_transaction(transaction1) diff --git a/trytond/tests/test_field_context.py b/trytond/tests/test_field_context.py index c60c377af..6ea9dc23c 100644 --- a/trytond/tests/test_field_context.py +++ b/trytond/tests/test_field_context.py @@ -24,10 +24,12 @@ def test_context(self): parent = Parent(name='foo', child=child) parent.save() self.assertEqual(parent.child._context['name'], 'foo') + self.assertEqual(parent.child._context['rec_name'], '') parent.name = 'bar' parent.save() self.assertEqual(parent.child._context['name'], 'bar') + self.assertEqual(parent.child._context['rec_name'], '') def suite(): diff --git a/trytond/tests/test_importdata.py b/trytond/tests/test_importdata.py index bff8307f8..f05f25355 100644 --- a/trytond/tests/test_importdata.py +++ b/trytond/tests/test_importdata.py @@ -55,6 +55,9 @@ def test_integer(self): self.assertEqual(Integer.import_data(['integer'], [['1']]), 1) + self.assertEqual(Integer.import_data(['integer'], + [[0]]), 1) + self.assertEqual(Integer.import_data(['integer'], [[1]]), 1) @@ -79,6 +82,9 @@ def test_integer(self): self.assertEqual(Integer.import_data(['integer'], [['0']]), 1) + self.assertEqual(Integer.import_data(['integer'], + [[None]]), 1) + @with_transaction() def test_integer_required(self): 'Test required integer' @@ -111,6 +117,13 @@ def test_integer_required(self): self.assertEqual(IntegerRequired.import_data(['integer'], [['0']]), 1) + self.assertEqual(IntegerRequired.import_data(['integer'], + [[0]]), 1) + + with self.assertRaises(RequiredValidationError): + IntegerRequired.import_data(['integer'], [[None]]) + transaction.rollback() + @with_transaction() def test_float(self): 'Test float' @@ -120,6 +133,9 @@ def test_float(self): self.assertEqual(Float.import_data(['float'], [['1.1']]), 1) + self.assertEqual(Float.import_data(['float'], + [[0.0]]), 1) + self.assertEqual(Float.import_data(['float'], [[1.1]]), 1) @@ -144,6 +160,9 @@ def test_float(self): self.assertEqual(Float.import_data(['float'], [['0.0']]), 1) + self.assertEqual(Float.import_data(['float'], + [[None]]), 1) + @with_transaction() def test_float_required(self): 'Test required float' @@ -176,6 +195,13 @@ def test_float_required(self): self.assertEqual(FloatRequired.import_data(['float'], [['0.0']]), 1) + self.assertEqual(FloatRequired.import_data(['float'], + [[0.0]]), 1) + + with self.assertRaises(RequiredValidationError): + FloatRequired.import_data(['float'], [[None]]) + transaction.rollback() + @with_transaction() def test_numeric(self): 'Test numeric' @@ -185,6 +211,9 @@ def test_numeric(self): self.assertEqual(Numeric.import_data(['numeric'], [['1.1']]), 1) + self.assertEqual(Numeric.import_data(['numeric'], + [[Decimal('0.0')]]), 1) + self.assertEqual(Numeric.import_data(['numeric'], [[Decimal('1.1')]]), 1) @@ -209,6 +238,9 @@ def test_numeric(self): self.assertEqual(Numeric.import_data(['numeric'], [['0.0']]), 1) + self.assertEqual(Numeric.import_data(['numeric'], + [[None]]), 1) + @with_transaction() def test_numeric_required(self): 'Test required numeric' @@ -241,6 +273,13 @@ def test_numeric_required(self): self.assertEqual(NumericRequired.import_data(['numeric'], [['0.0']]), 1) + self.assertEqual(NumericRequired.import_data(['numeric'], + [[Decimal('0.0')]]), 1) + + with self.assertRaises(RequiredValidationError): + NumericRequired.import_data(['numeric'], [[None]]) + transaction.rollback() + @with_transaction() def test_char(self): 'Test char' diff --git a/trytond/tests/test_modelsql.py b/trytond/tests/test_modelsql.py index 3a4c8a3ea..5cad32db9 100644 --- a/trytond/tests/test_modelsql.py +++ b/trytond/tests/test_modelsql.py @@ -282,6 +282,7 @@ def test_check_timestamp(self): # timestamp precision of sqlite is the second time.sleep(1) + transaction.timestamp[str(record)] = timestamp ModelsqlTimestamp.write([record], {}) transaction.commit() @@ -293,6 +294,10 @@ def test_check_timestamp(self): self.assertRaises(ConcurrencyException, ModelsqlTimestamp.delete, [record]) + transaction.timestamp[str(record)] = None + ModelsqlTimestamp.write([record], {}) + transaction.commit() + transaction.timestamp.pop(str(record), None) ModelsqlTimestamp.write([record], {}) transaction.commit() @@ -462,7 +467,20 @@ def test_constraint_exclude_exclusion(self): pool = Pool() Model = pool.get('test.modelsql.exclude') - records = Model.create([{'value': -1}, {'value': -1}]) + records = Model.create([{'value': 1, 'condition': False}] * 2) + + self.assertEqual(len(records), 2) + + @with_transaction() + def test_constraint_exclude_exclusion_mixed(self): + "Test exclude constraint exclusion mixed" + pool = Pool() + Model = pool.get('test.modelsql.exclude') + + records = Model.create([ + {'value': 1, 'condition': False}, + {'value': 1, 'condition': True}, + ]) self.assertEqual(len(records), 2) diff --git a/trytond/tests/test_pyson.py b/trytond/tests/test_pyson.py index 9b04febb6..c3c4e128a 100644 --- a/trytond/tests/test_pyson.py +++ b/trytond/tests/test_pyson.py @@ -4,6 +4,8 @@ import unittest import datetime +import sys + from decimal import Decimal from trytond import pyson @@ -109,8 +111,9 @@ def test_And(self): 's': [True, False], }) - self.assertRaises(AssertionError, pyson.And, True) - self.assertRaises(AssertionError, pyson.And) + if not sys.flags.optimize: + self.assertRaises(AssertionError, pyson.And, True) + self.assertRaises(AssertionError, pyson.And) self.assertEqual(pyson.And(True, False).types(), set([bool])) @@ -151,8 +154,9 @@ def test_Or(self): 's': [True, False], }) - self.assertRaises(AssertionError, pyson.Or, True) - self.assertRaises(AssertionError, pyson.Or) + if not sys.flags.optimize: + self.assertRaises(AssertionError, pyson.Or, True) + self.assertRaises(AssertionError, pyson.Or) self.assertEqual(pyson.Or(True, False).types(), set([bool])) @@ -194,7 +198,8 @@ def test_Equal(self): 's2': 'test', }) - self.assertRaises(AssertionError, pyson.Equal, 'test', True) + if not sys.flags.optimize: + self.assertRaises(AssertionError, pyson.Equal, 'test', True) self.assertEqual(pyson.Equal('test', 'test').types(), set([bool])) @@ -216,8 +221,9 @@ def test_Greater(self): 'e': False, }) - self.assertRaises(AssertionError, pyson.Greater, 'test', 0) - self.assertRaises(AssertionError, pyson.Greater, 1, 'test') + if not sys.flags.optimize: + self.assertRaises(AssertionError, pyson.Greater, 'test', 0) + self.assertRaises(AssertionError, pyson.Greater, 1, 'test') self.assertEqual(pyson.Greater(1, 0).types(), set([bool])) @@ -256,8 +262,9 @@ def test_Less(self): 'e': False, }) - self.assertRaises(AssertionError, pyson.Less, 'test', 1) - self.assertRaises(AssertionError, pyson.Less, 0, 'test') + if not sys.flags.optimize: + self.assertRaises(AssertionError, pyson.Less, 'test', 1) + self.assertRaises(AssertionError, pyson.Less, 0, 'test') self.assertEqual(pyson.Less(0, 1).types(), set([bool])) @@ -296,7 +303,8 @@ def test_If(self): 'e': 'bar', }) - self.assertRaises(AssertionError, pyson.If, True, 'foo', False) + if not sys.flags.optimize: + self.assertRaises(AssertionError, pyson.If, True, 'foo', False) self.assertEqual(pyson.If(True, 'foo', 'bar').types(), set([str])) @@ -320,8 +328,10 @@ def test_Get(self): 'd': 'default', }) - self.assertRaises(AssertionError, pyson.Get, 'test', 'foo', 'default') - self.assertRaises(AssertionError, pyson.Get, {}, 1, 'default') + if not sys.flags.optimize: + self.assertRaises( + AssertionError, pyson.Get, 'test', 'foo', 'default') + self.assertRaises(AssertionError, pyson.Get, {}, 1, 'default') self.assertEqual(pyson.Get({}, 'foo', 'default').types(), set([str])) @@ -350,8 +360,9 @@ def test_In(self): 'v': {'foo': 'bar'}, }) - self.assertRaises(AssertionError, pyson.In, object(), {}) - self.assertRaises(AssertionError, pyson.In, 'test', 'foo') + if not sys.flags.optimize: + self.assertRaises(AssertionError, pyson.In, object(), {}) + self.assertRaises(AssertionError, pyson.In, 'test', 'foo') self.assertEqual(pyson.In('foo', {}).types(), set([bool])) @@ -400,18 +411,19 @@ def test_Date(self): 'dd': -7 }) - self.assertRaises(AssertionError, pyson.Date, 'test', 1, 12, -1, 12, - -7) - self.assertRaises(AssertionError, pyson.Date, 2010, 'test', 12, -1, 12, - -7) - self.assertRaises(AssertionError, pyson.Date, 2010, 1, 'test', -1, 12, - -7) - self.assertRaises(AssertionError, pyson.Date, 2010, 1, 12, 'test', 12, - -7) - self.assertRaises(AssertionError, pyson.Date, 2010, 1, 12, -1, 'test', - -7) - self.assertRaises(AssertionError, pyson.Date, 2010, 1, 12, -1, 12, - 'test') + if not sys.flags.optimize: + self.assertRaises( + AssertionError, pyson.Date, 'test', 1, 12, -1, 12, -7) + self.assertRaises( + AssertionError, pyson.Date, 2010, 'test', 12, -1, 12, -7) + self.assertRaises( + AssertionError, pyson.Date, 2010, 1, 'test', -1, 12, -7) + self.assertRaises( + AssertionError, pyson.Date, 2010, 1, 12, 'test', 12, -7) + self.assertRaises( + AssertionError, pyson.Date, 2010, 1, 12, -1, 'test', -7) + self.assertRaises( + AssertionError, pyson.Date, 2010, 1, 12, -1, 12, 'test') self.assertEqual(pyson.Date(2010, 1, 12, -1, 12, -7).types(), set([datetime.date])) @@ -464,34 +476,49 @@ def test_DateTime(self): 'dms': 1, }) - self.assertRaises(AssertionError, pyson.DateTime, 'test', 1, 12, 10, - 30, 20, 0, -1, 12, -7, 2, 15, 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 'test', 12, 10, - 30, 20, 0, -1, 12, -7, 2, 15, 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 'test', 10, - 30, 20, 0, -1, 12, -7, 2, 15, 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 'test', - 30, 20, 0, -1, 12, -7, 2, 15, 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, - 'test', 20, 0, -1, 12, -7, 2, 15, 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30, - 'test', 0, -1, 12, -7, 2, 15, 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30, - 20, 'test', -1, 12, -7, 2, 15, 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30, - 20, 0, 'test', 12, -7, 2, 15, 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30, - 20, 0, -1, 'test', -7, 2, 15, 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30, - 20, 0, -1, 12, 'test', 2, 15, 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30, - 20, 0, -1, 12, -7, 'test', 15, 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30, - 20, 0, -1, 12, -7, 2, 'test', 30, 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30, - 20, 0, -1, 12, -7, 2, 15, 'test', 1) - self.assertRaises(AssertionError, pyson.DateTime, 2010, 1, 12, 10, 30, - 20, 0, -1, 12, -7, 2, 15, 30, 'test') + if not sys.flags.optimize: + self.assertRaises( + AssertionError, pyson.DateTime, + 'test', 1, 12, 10, 30, 20, 0, -1, 12, -7, 2, 15, 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 'test', 12, 10, 30, 20, 0, -1, 12, -7, 2, 15, 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 'test', 10, 30, 20, 0, -1, 12, -7, 2, 15, 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 12, 'test', 30, 20, 0, -1, 12, -7, 2, 15, 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 12, 10, 'test', 20, 0, -1, 12, -7, 2, 15, 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 12, 10, 30, 'test', 0, -1, 12, -7, 2, 15, 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 12, 10, 30, 20, 'test', -1, 12, -7, 2, 15, 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 12, 10, 30, 20, 0, 'test', 12, -7, 2, 15, 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 12, 10, 30, 20, 0, -1, 'test', -7, 2, 15, 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 12, 10, 30, 20, 0, -1, 12, 'test', 2, 15, 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 12, 10, 30, 20, 0, -1, 12, -7, 'test', 15, 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 12, 10, 30, 20, 0, -1, 12, -7, 2, 'test', 30, 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 12, 10, 30, 20, 0, -1, 12, -7, 2, 15, 'test', 1) + self.assertRaises( + AssertionError, pyson.DateTime, + 2010, 1, 12, 10, 30, 20, 0, -1, 12, -7, 2, 15, 30, 'test') self.assertEqual(pyson.DateTime(2010, 1, 12, 10, 30, 20, 0, -1, 12, -7, 2, 15, 30, 1).types(), set([datetime.datetime])) @@ -552,7 +579,8 @@ def test_Len(self): 'v': [1, 2, 3], }) - self.assertRaises(AssertionError, pyson.Len, object()) + if not sys.flags.optimize: + self.assertRaises(AssertionError, pyson.Len, object()) self.assertEqual(pyson.Len([1, 2, 3]).types(), set([int])) @@ -581,6 +609,7 @@ def test_TimeDelta_types(self): self.assertEqual( pyson.TimeDelta(seconds=10).types(), {datetime.timedelta}) + @unittest.skipIf(sys.flags.optimize, "assert removed by optimization") def test_TimeDelta_invalid_type(self): "Test pyson.TimeDelta invalid type" with self.assertRaises(AssertionError): diff --git a/trytond/tests/test_sendmail.py b/trytond/tests/test_sendmail.py index 14bd364b1..9e887a72e 100644 --- a/trytond/tests/test_sendmail.py +++ b/trytond/tests/test_sendmail.py @@ -3,7 +3,7 @@ import smtplib import unittest from email.message import Message -from unittest.mock import Mock, patch, call +from unittest.mock import Mock, MagicMock, patch, call from trytond.sendmail import ( sendmail_transactional, sendmail, SMTPDataManager, get_smtp_server) @@ -21,7 +21,7 @@ def setUpClass(cls): @with_transaction() def test_sendmail_transactional(self): 'Test sendmail_transactional' - message = Mock() + message = MagicMock() datamanager = Mock() sendmail_transactional( 'tryton@example.com', 'foo@example.com', message, @@ -32,7 +32,7 @@ def test_sendmail_transactional(self): def test_sendmail(self): 'Test sendmail' - message = Mock() + message = MagicMock() server = Mock() sendmail( 'tryton@example.com', 'foo@example.com', message, server=server) @@ -89,8 +89,8 @@ def test_SMTPDataManager(self, get_smtp_server): # multiple join must return the same self.assertEqual(transaction.join(SMTPDataManager()), datamanager) - msg1 = Mock(Message) - msg2 = Mock(Message) + msg1 = MagicMock(Message) + msg2 = MagicMock(Message) datamanager.put('foo@example.com', 'bar@example.com', msg1) datamanager.put('bar@example.com', 'foo@example.com', msg2) @@ -105,7 +105,8 @@ def test_SMTPDataManager(self, get_smtp_server): server.reset_mock() - datamanager.put('foo@example.com', 'bar@example.com', Mock(Message)) + datamanager.put( + 'foo@example.com', 'bar@example.com', MagicMock(Message)) transaction.rollback() server.sendmail.assert_not_called() diff --git a/trytond/tests/test_tools.py b/trytond/tests/test_tools.py index 7f1734af8..600b9a477 100644 --- a/trytond/tests/test_tools.py +++ b/trytond/tests/test_tools.py @@ -4,6 +4,8 @@ import unittest import doctest +import sys + import sql import sql.operators @@ -51,6 +53,7 @@ def test_reduce_ids_complex_small_continue(self): (((self.table.id >= 1) & (self.table.id <= 12)) | (self.table.id.in_([15, 18, 19, 21])))) + @unittest.skipIf(sys.flags.optimize, "assert removed by optimization") def test_reduce_ids_float(self): 'Test reduce_ids with integer as float' self.assertEqual(reduce_ids(self.table.id, diff --git a/trytond/tests/test_tryton.py b/trytond/tests/test_tryton.py index a7cb12fe6..00579edb3 100644 --- a/trytond/tests/test_tryton.py +++ b/trytond/tests/test_tryton.py @@ -163,6 +163,7 @@ def _pg_restore(cache_file): return not subprocess.call(cmd, env=env) except OSError: cache_name, _ = os.path.splitext(os.path.basename(cache_file)) + cache_name = backend.get('TableHandler').convert_name(cache_name) with Transaction().start( None, 0, close=True, autocommit=True) as transaction: transaction.database.drop(transaction.connection, DB_NAME) @@ -180,6 +181,7 @@ def _pg_dump(cache_file): return not subprocess.call(cmd, env=env) except OSError: cache_name, _ = os.path.splitext(os.path.basename(cache_file)) + cache_name = backend.get('TableHandler').convert_name(cache_name) # Ensure any connection is left open backend.get('Database')(DB_NAME).close() with Transaction().start( diff --git a/trytond/tools/misc.py b/trytond/tools/misc.py index e46e55c3a..ee2763c9d 100644 --- a/trytond/tools/misc.py +++ b/trytond/tools/misc.py @@ -225,6 +225,7 @@ def grouped_slice(records, count=None): from trytond.transaction import Transaction if count is None: count = Transaction().database.IN_MAX + count = max(1, count) for i in range(0, len(records), count): yield islice(records, i, i + count) diff --git a/trytond/tryton.rnc b/trytond/tryton.rnc index fe8769800..636ddf6c8 100644 --- a/trytond/tryton.rnc +++ b/trytond/tryton.rnc @@ -8,6 +8,7 @@ attlist.data &= attlist.data &= [ a:defaultValue = "0" ] attribute grouped { "0" | "1" }? attlist.data &= attribute depends { text } +attlist.data &= attribute skiptest { "0" | "1" }? record = element record { attlist.record, field* } attlist.record &= attribute model { text } attlist.record &= attribute id { text } diff --git a/trytond/tryton.rng b/trytond/tryton.rng index ea4c9c590..62a579e06 100644 --- a/trytond/tryton.rng +++ b/trytond/tryton.rng @@ -53,6 +53,17 @@ + + + + skiptest + + 0 + 1 + + + + record diff --git a/trytond/wizard/wizard.py b/trytond/wizard/wizard.py index 8275938e5..72a9d9c5a 100644 --- a/trytond/wizard/wizard.py +++ b/trytond/wizard/wizard.py @@ -12,6 +12,7 @@ from trytond.transaction import Transaction from trytond.url import URLMixin from trytond.protocols.jsonrpc import JSONDecoder, JSONEncoder +from trytond.model import ModelSQL from trytond.model.fields import states_validate from trytond.pyson import PYSONEncoder from trytond.rpc import RPC @@ -224,18 +225,22 @@ def check_access(cls): if Transaction().user == 0: return - model = context.get('active_model') - if model: - ModelAccess.check(model, 'read') - groups = set(User.get_groups()) - wizard_groups = ActionWizard.get_groups(cls.__name__, - action_id=context.get('action_id')) - if wizard_groups: - if not groups & wizard_groups: - raise UserError('Calling wizard %s is not allowed!' - % cls.__name__) - elif model: - ModelAccess.check(model, 'write') + with Transaction().set_context(_check_access=True): + model = context.get('active_model') + if model and model != 'ir.ui.menu': + ModelAccess.check(model, 'read') + groups = set(User.get_groups()) + wizard_groups = ActionWizard.get_groups(cls.__name__, + action_id=context.get('action_id')) + if wizard_groups: + if not groups & wizard_groups: + raise UserError('Calling wizard %s is not allowed!' + % cls.__name__) + elif model and model != 'ir.ui.menu': + Model = pool.get(model) + if (not callable(getattr(Model, 'table_query', None)) + or Model.write.__func__ != ModelSQL.write.__func__): + ModelAccess.check(model, 'write') @classmethod def create(cls): diff --git a/trytond/worker.py b/trytond/worker.py index bbf2bca63..569aa7b91 100644 --- a/trytond/worker.py +++ b/trytond/worker.py @@ -1,6 +1,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 datetime as dt import logging +import random import select import signal import time @@ -114,5 +116,16 @@ def run_task(pool, task_id): continue raise logger.info('task "%d" done', task_id) + except DatabaseOperationalError: + try: + with Transaction().start(pool.database_name, 0) as transaction: + task = Queue(task_id) + scheduled_at = dt.datetime.now() + scheduled_at += dt.timedelta( + seconds=random.randint(0, 2 * retry)) + Queue.push(task.name, task.data, scheduled_at=scheduled_at) + except Exception: + logger.critical( + 'rescheduling task "%d" failed', task_id, exc_info=True) except Exception: logger.critical('task "%d" failed', task_id, exc_info=True)