diff --git a/CHANGELOG b/CHANGELOG index 7dfd90bec0..28aae47f2c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +* Drop support of Python 2.5 +* Remove support of Many2Many field in record XML +* Change Pool into a Singleton * Remove support of zipped modules Version 2.0.0 - 2011-04-26 diff --git a/bin/trytond b/bin/trytond index ffd5d54750..a55071c781 100755 --- a/bin/trytond +++ b/bin/trytond @@ -37,6 +37,8 @@ def parse_commandline(): help="file where the server pid will be stored") parser.add_option("--logfile", dest="logfile", help="file where the server log will be stored") + parser.add_option("--disable-cron", dest="cron", + action="store_false", help="disable cron") (opt, _) = parser.parse_args() @@ -51,6 +53,7 @@ def parse_commandline(): 'debug_mode', 'pidfile', 'logfile', + 'cron', ): if getattr(opt, arg) is not None: options[arg] = getattr(opt, arg) diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst index 4d741c22ba..52e8d036ee 100644 --- a/doc/ref/models/fields.rst +++ b/doc/ref/models/fields.rst @@ -508,7 +508,7 @@ A many-to-many relation field. Instance methods: -.. method:: Many2Many.get_target(pool) +.. method:: Many2Many.get_target() Return the target :class:`~trytond.model.Model`. @@ -530,7 +530,7 @@ A one-to-one relation field. Instance methods: -.. method:: One2One.get_target(pool) +.. method:: One2One.get_target() Return the target :class:`~trytond.model.Model`. diff --git a/doc/topics/install.rst b/doc/topics/install.rst index e44100ed6a..ed496ac66a 100644 --- a/doc/topics/install.rst +++ b/doc/topics/install.rst @@ -7,7 +7,7 @@ How to install Tryton Prerequisites ============= - * Python 2.5 or later (http://www.python.org/) + * Python 2.6 or later (http://www.python.org/) * lxml 2.0 or later (http://codespeak.net/lxml/) * relatorio 0.2.0 or later (http://relatorio.openhex.org/) * python-dateutil (http://labix.org/python-dateutil) diff --git a/doc/topics/reports/index.rst b/doc/topics/reports/index.rst index f45b8a62f5..96a70b11ad 100644 --- a/doc/topics/reports/index.rst +++ b/doc/topics/reports/index.rst @@ -61,7 +61,7 @@ When defining an `ir.action.report` the following attributes are available: * ``name``: The name of the report. * ``report_name``: The name of the report model, for example - my_module.my_report. This is the name you would use with `pool.get` + my_module.my_report. This is the name you would use with `Pool().get` * ``module_name``: If this report is of an existing model this is its name. For example my_module.my_model. Custom reports that aren't of a specific @@ -228,7 +228,7 @@ context. Now the invoice report will be able to access the employee object. class InvoiceReport(Report): _name = 'account.invoice' def parse(self, report, objects, datas, localcontext): - employee_obj = self.pool.get('company.employee') + employee_obj = Pool().get('company.employee') employee = False if Transaction().context.get('employee'): employee = employee_obj.browse(Transaction().context['employee']) diff --git a/etc/trytond.conf b/etc/trytond.conf index 90e74c0c97..894d201565 100644 --- a/etc/trytond.conf +++ b/etc/trytond.conf @@ -90,3 +90,6 @@ # Prevent database listing #prevent_dblist = False + +# Enable cron +# cron = True diff --git a/setup.py b/setup.py index 5bb69db1e5..e21a377c6b 100644 --- a/setup.py +++ b/setup.py @@ -8,10 +8,6 @@ import sys execfile(os.path.join('trytond', 'version.py')) -SIMPLEJSON = [] -if sys.version_info < (2, 6): - SIMPLEJSON = ['simplejson'] - def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() @@ -52,7 +48,6 @@ setup(name=PACKAGE, 'Natural Language :: Russian', 'Natural Language :: Spanish', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Software Development :: Libraries :: Application Frameworks', @@ -63,7 +58,7 @@ setup(name=PACKAGE, 'relatorio >= 0.2.0', 'Genshi', 'python-dateutil', - ] + SIMPLEJSON, + ], extras_require={ 'PostgreSQL': ['psycopg2 >= 2.0'], 'MySQL': ['MySQL-python'], diff --git a/trytond/backend/database.py b/trytond/backend/database.py index 168d1f9624..b61c88baa2 100644 --- a/trytond/backend/database.py +++ b/trytond/backend/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 trytond.const import CONTEXT_CACHE_SIZE, MODEL_CACHE_SIZE DatabaseIntegrityError = None DatabaseOperationalError = None @@ -103,7 +104,8 @@ class CursorInterface(object): IN_MAX = 1000 def __init__(self): - self.cache = {} + from trytond.cache import LRUDict + self.cache = LRUDict(CONTEXT_CACHE_SIZE) def get_cache(self, context=None): ''' @@ -112,6 +114,7 @@ class CursorInterface(object): :param context: the context :return: the cache dictionary ''' + from trytond.cache import LRUDict if context is None: context = {} cache_ctx = context.copy() @@ -119,7 +122,8 @@ class CursorInterface(object): '_delete_records'): if i in cache_ctx: del cache_ctx[i] - return self.cache.setdefault(repr(cache_ctx), {}) + return self.cache.setdefault(repr(cache_ctx), + LRUDict(MODEL_CACHE_SIZE)) def execute(self, sql, params=None): ''' diff --git a/trytond/backend/mysql/database.py b/trytond/backend/mysql/database.py index de7fc52785..c01a3030cc 100644 --- a/trytond/backend/mysql/database.py +++ b/trytond/backend/mysql/database.py @@ -1,6 +1,5 @@ #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 __future__ import with_statement from trytond.backend.database import DatabaseInterface, CursorInterface from trytond.config import CONFIG import MySQLdb diff --git a/trytond/backend/mysql/init.sql b/trytond/backend/mysql/init.sql index bb500939fd..a74b656807 100644 --- a/trytond/backend/mysql/init.sql +++ b/trytond/backend/mysql/init.sql @@ -84,10 +84,10 @@ CREATE TABLE res_group ( ) ENGINE=InnoDB; CREATE TABLE `res_user-res_group` ( - uid BIGINT NOT NULL, - gid BIGINT NOT NULL, - CONSTRAINT `res_user-res_group_uid_fkey` FOREIGN KEY (uid) REFERENCES res_user (id) ON DELETE CASCADE, - CONSTRAINT `res_user-res_group_gid_fkey` FOREIGN KEY (gid) REFERENCES res_group (id) ON DELETE CASCADE + `user` BIGINT NOT NULL, + `group` BIGINT NOT NULL, + CONSTRAINT `res_user-res_group_user_fkey` FOREIGN KEY (`user`) REFERENCES res_user (id) ON DELETE CASCADE, + CONSTRAINT `res_user-res_group_group_fkey` FOREIGN KEY (`group`) REFERENCES res_group (id) ON DELETE CASCADE ) ENGINE=InnoDB; CREATE TABLE wkf ( diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py index c432fa9673..be7f01d7ec 100644 --- a/trytond/backend/postgresql/database.py +++ b/trytond/backend/postgresql/database.py @@ -1,6 +1,5 @@ #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 __future__ import with_statement from trytond.backend.database import DatabaseInterface, CursorInterface from trytond.config import CONFIG from trytond.session import Session diff --git a/trytond/backend/postgresql/init.sql b/trytond/backend/postgresql/init.sql index be4da11b13..3e3c4229c5 100644 --- a/trytond/backend/postgresql/init.sql +++ b/trytond/backend/postgresql/init.sql @@ -102,10 +102,10 @@ CREATE SEQUENCE "res_user-res_group_id_seq"; CREATE TABLE "res_user-res_group" ( id INTEGER DEFAULT NEXTVAL('res_user-res_group_id_seq') NOT NULL, - uid INTEGER NOT NULL, - gid INTEGER NOT NULL, - FOREIGN KEY (uid) REFERENCES res_user (id) ON DELETE CASCADE, - FOREIGN KEY (gid) REFERENCES res_group (id) ON DELETE CASCADE, + "user" INTEGER NOT NULL, + "group" INTEGER NOT NULL, + FOREIGN KEY ("user") REFERENCES res_user (id) ON DELETE CASCADE, + FOREIGN KEY ("group") REFERENCES res_group (id) ON DELETE CASCADE, PRIMARY KEY(id) ); diff --git a/trytond/backend/sqlite/database.py b/trytond/backend/sqlite/database.py index 0c4fa0ae60..e4d04a2b65 100644 --- a/trytond/backend/sqlite/database.py +++ b/trytond/backend/sqlite/database.py @@ -1,6 +1,5 @@ #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 __future__ import with_statement from trytond.backend.database import DatabaseInterface, CursorInterface from trytond.config import CONFIG from trytond.session import Session @@ -9,6 +8,7 @@ import re from decimal import Decimal import datetime import time +import sys _FIX_ROWCOUNT = False try: @@ -144,10 +144,9 @@ class Database(DatabaseInterface): return path = os.path.join(CONFIG['data_path'], database_name + '.sqlite') - conn = sqlite.connect(path) - cursor = conn.cursor() - cursor.close() - conn.close() + with sqlite.connect(path) as conn: + cursor = conn.cursor() + cursor.close() def drop(self, cursor, database_name): if database_name == ':memory:': @@ -358,7 +357,10 @@ class Cursor(CursorInterface): return select sqlite.register_converter('NUMERIC', lambda val: Decimal(val)) -sqlite.register_adapter(Decimal, lambda val: buffer(str(val))) +if sys.version_info[0] == 2: + sqlite.register_adapter(Decimal, lambda val: buffer(str(val))) +else: + sqlite.register_adapter(Decimal, lambda val: bytes(str(val))) sqlite.register_adapter(Session, int) def adapt_datetime(val): return val.replace(tzinfo=None).isoformat(" ") diff --git a/trytond/backend/sqlite/init.sql b/trytond/backend/sqlite/init.sql index 52bbf66e15..9d1879c227 100644 --- a/trytond/backend/sqlite/init.sql +++ b/trytond/backend/sqlite/init.sql @@ -73,8 +73,8 @@ CREATE TABLE res_group ( CREATE TABLE "res_user-res_group" ( id INTEGER PRIMARY KEY AUTOINCREMENT, - uid INTEGER, - gid INTEGER + "user" INTEGER, + "group" INTEGER ); CREATE TABLE wkf ( diff --git a/trytond/backend/sqlite/table.py b/trytond/backend/sqlite/table.py index 6d942b7962..2a18b038b6 100644 --- a/trytond/backend/sqlite/table.py +++ b/trytond/backend/sqlite/table.py @@ -127,7 +127,7 @@ class TableHandler(TableHandlerInterface): match = re.match(r'(\w+)(\((.*?)\))?', type_) if match: typname = match.group(1).upper() - size = match.group(3) and int(match.group(3)) or None + size = match.group(3) and int(match.group(3)) or 0 else: typname = type_.upper() size = -1 diff --git a/trytond/cache.py b/trytond/cache.py index dfda7e465c..005030e3af 100644 --- a/trytond/cache.py +++ b/trytond/cache.py @@ -7,6 +7,7 @@ from threading import Lock from trytond.transaction import Transaction from trytond.config import CONFIG from trytond.backend import Database +from trytond.tools import OrderedDict class Cache(object): @@ -167,3 +168,32 @@ class Cache(object): cursor.commit() cursor.close() Cache._resets_lock.release() + + +class LRUDict(OrderedDict): + """ + Dictionary with a size limit. + If size limit is reached, it will remove the first added items. + """ + def __init__(self, size_limit, *args, **kwargs): + assert size_limit > 0 + self.size_limit = size_limit + super(LRUDict, self).__init__(*args, **kwargs) + self._check_size_limit() + + def __setitem__(self, key, value): + super(LRUDict, self).__setitem__(key, value) + self._check_size_limit() + + def update(self, *args, **kwargs): + super(LRUDict, self).update(*args, **kwargs) + self._check_size_limit() + + def setdefault(self, key, default=None): + default = super(LRUDict, self).setdefault(key, default=default) + self._check_size_limit() + return default + + def _check_size_limit(self): + while len(self) > self.size_limit: + self.popitem(last=False) diff --git a/trytond/config.py b/trytond/config.py index 6d59bccdcb..39e772c0ad 100644 --- a/trytond/config.py +++ b/trytond/config.py @@ -51,6 +51,7 @@ class ConfigManager(object): 'prevent_dblist': False, 'init': {}, 'update': {}, + 'cron': True, } self.configfile = None @@ -82,12 +83,9 @@ class ConfigManager(object): if not self.configfile: return - fp = open(self.configfile) parser = ConfigParser.ConfigParser() - try: + with open(self.configfile) as fp: parser.readfp(fp) - finally: - fp.close() for (name, value) in parser.items('options'): if value == 'True' or value == 'true': value = True diff --git a/trytond/const.py b/trytond/const.py index 2a569f5f6b..b0a2006442 100644 --- a/trytond/const.py +++ b/trytond/const.py @@ -18,4 +18,7 @@ OPERATORS = ( '>', ) +CONTEXT_CACHE_SIZE = 10 +MODEL_CACHE_SIZE = 200 +RECORD_CACHE_SIZE = 2000 BROWSE_FIELD_TRESHOLD = 100 diff --git a/trytond/convert.py b/trytond/convert.py index 03ba03d2a6..3b2eea2fd6 100644 --- a/trytond/convert.py +++ b/trytond/convert.py @@ -1,6 +1,5 @@ #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 __future__ import with_statement import time from xml import sax from decimal import Decimal @@ -110,16 +109,7 @@ class MenuitemTagHandler: values['icon'] = 'tryton-new' if values.get('groups'): - g_names = values['groups'].split(',') - groups_value = [] - for group in g_names: - if group.startswith('-'): - group_id = self.mh.get_id(group[1:]) - groups_value.append(('remove', group_id)) - else: - group_id = self.mh.get_id(group) - groups_value.append(('add', group_id)) - values['groups'] = groups_value + raise Exception("Please use separate records for groups") if not values.get('name'): if not action_name: @@ -205,17 +195,8 @@ class RecordTagHandler: search_model = self.model._columns[field_name].model_name f_obj = self.mh.pool.get(search_model) with Transaction().set_context(active_test=False): - answer = f_obj.browse(f_obj.search(safe_eval(search_attr))) - - if not answer: return - - if self.model._columns[field_name]._type == 'many2many': - self.values[field_name] = [ - ('set', [x['id'] for x in answer]), - ] - - elif self.model._columns[field_name]._type == 'many2one': - self.values[field_name] = answer[0]['id'] + self.values[field_name], = \ + f_obj.search(safe_eval(search_attr)) elif ref_attr: self.values[field_name] = self.mh.get_id(ref_attr) @@ -527,7 +508,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler): ref_id = ref_id[0] return ref_mode + ',' + str(ref_id) elif field_type in ['one2many', 'many2many']: - raise Unhandled_field() + raise Unhandled_field("Unhandled field %s" % key) else: return browse_record[key] @@ -695,13 +676,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler): to_update = {} for key in values: - try: - db_field = self._clean_value(key, db_val, object_ref) - except Unhandled_field: - logging.getLogger("convert").info( - 'Field %s on %s : integrity not tested.'%(key, model)) - to_update[key] = values[key] - continue + db_field = self._clean_value(key, db_val, object_ref) # if the fs value is the same has in the db, whe ignore it val = values[key] @@ -813,11 +788,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler): if not db_val: db_val = object_ref.browse(db_id) for key in to_update: - try: - values[key] = self._clean_value( - key, db_val, object_ref) - except Unhandled_field: - continue + values[key] = self._clean_value( key, db_val, object_ref) if module != self.module: temp_values = old_values.copy() @@ -904,11 +875,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler): # in the model_data table: db_val = object_ref.browse(db_id) for key in values: - try: - values[key] = self._clean_value(key, db_val, - object_ref) - except Unhandled_field: - continue + values[key] = self._clean_value(key, db_val, object_ref) for table in inherit_db_ids.keys(): self.modeldata_obj.create({ diff --git a/trytond/error.py b/trytond/error.py index 1ca8e42b9c..46381d2dd8 100644 --- a/trytond/error.py +++ b/trytond/error.py @@ -1,6 +1,7 @@ #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.transaction import Transaction +from trytond.pool import Pool class WarningErrorMixin(object): @@ -24,7 +25,7 @@ class WarningErrorMixin(object): (or tuple if error_description is not empty) instead of raising an exception. ''' - translation_obj = self.pool.get('ir.translation') + translation_obj = Pool().get('ir.translation') error = self._error_messages.get(error, error) @@ -92,7 +93,7 @@ class WarningErrorMixin(object): :param warning_description_args: the arguments that will be used for "%"-based substitution ''' - warning_obj = self.pool.get('res.user.warning') + warning_obj = Pool().get('res.user.warning') if warning_obj.check(warning_name): if warning_description: warning, warning_description = self.raise_user_error(warning, diff --git a/trytond/ir/action.py b/trytond/ir/action.py index 6996c13b04..cbd5cbd750 100644 --- a/trytond/ir/action.py +++ b/trytond/ir/action.py @@ -1,6 +1,5 @@ #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 __future__ import with_statement import base64 import os from trytond.model import ModelView, ModelSQL, ModelStorage, fields @@ -9,6 +8,7 @@ from trytond.backend import TableHandler from trytond.pyson import PYSONEncoder, CONTEXT, PYSON from trytond.transaction import Transaction from trytond.cache import Cache +from trytond.pool import Pool class Action(ModelSQL, ModelView): @@ -20,7 +20,7 @@ class Action(ModelSQL, ModelView): usage = fields.Char('Usage') keywords = fields.One2Many('ir.action.keyword', 'action', 'Keywords') - groups = fields.Many2Many('ir.action-res.group', 'action_id', 'gid', + groups = fields.Many2Many('ir.action-res.group', 'action', 'group', 'Groups') icon = fields.Many2One('ir.ui.icon', 'Icon') active = fields.Boolean('Active', select=2) @@ -38,6 +38,7 @@ class Action(ModelSQL, ModelView): return True def get_action_id(self, action_id): + pool = Pool() with Transaction().set_context(active_test=False): if self.search([ ('id', '=', action_id), @@ -49,7 +50,7 @@ class Action(ModelSQL, ModelView): 'ir.action.wizard', 'ir.action.url', ): - action_obj = self.pool.get(action_type) + action_obj = pool.get(action_type) action_id2 = action_obj.search([ ('id', '=', action_id), ]) @@ -88,7 +89,8 @@ class ActionKeyword(ModelSQL, ModelView): }) def check_wizard_model(self, ids): - action_wizard_obj = self.pool.get('ir.action.wizard') + pool = Pool() + action_wizard_obj = pool.get('ir.action.wizard') for action_keyword in self.browse(ids): if action_keyword.action.type == 'ir.action.wizard': action_wizard_id = action_wizard_obj.search([ @@ -103,13 +105,15 @@ class ActionKeyword(ModelSQL, ModelView): def _convert_vals(self, vals): vals = vals.copy() - action_obj = self.pool.get('ir.action') + pool = Pool() + action_obj = pool.get('ir.action') if 'action' in vals: vals['action'] = action_obj.get_action_id(vals['action']) return vals def models_get(self): - model_obj = self.pool.get('ir.model') + pool = Pool() + model_obj = pool.get('ir.model') model_ids = model_obj.search([]) res = [] for model in model_obj.browse(model_ids): @@ -117,36 +121,39 @@ class ActionKeyword(ModelSQL, ModelView): return res def delete(self, ids): + pool = Pool() if isinstance(ids, (int, long)): ids = [ids] for keyword in self.browse(ids): # Restart the cache view try: - self.pool.get(keyword.model.split(',')[0] + pool.get(keyword.model.split(',')[0] ).fields_view_get.reset() except Exception: pass return super(ActionKeyword, self).delete(ids) def create(self, vals): + pool = Pool() vals = self._convert_vals(vals) if 'model' in vals: # Restart the cache view try: - self.pool.get(vals['model'].split(',')[0] + pool.get(vals['model'].split(',')[0] ).fields_view_get.reset() except Exception: pass return super(ActionKeyword, self).create(vals) def write(self, ids, vals): + pool = Pool() vals = self._convert_vals(vals) if isinstance(ids, (int, long)): ids = [ids] for keyword in self.browse(ids): # Restart the cache view try: - self.pool.get(keyword.model.split(',')[0] + pool.get(keyword.model.split(',')[0] ).fields_view_get.reset() except Exception: pass @@ -154,13 +161,14 @@ class ActionKeyword(ModelSQL, ModelView): for keyword in self.browse(ids): # Restart the cache view try: - self.pool.get(keyword.model.split(',')[0] + pool.get(keyword.model.split(',')[0] ).fields_view_get.reset() except Exception: pass return res def get_keyword(self, keyword, value): + pool = Pool() res = [] model, model_id = value @@ -176,7 +184,7 @@ class ActionKeyword(ModelSQL, ModelView): ])) for action_keyword in self.browse(action_keyword_ids): try: - action_obj = self.pool.get(action_keyword.action.type) + action_obj = pool.get(action_keyword.action.type) except Exception: continue action_id = action_obj.search([ @@ -369,7 +377,8 @@ class ActionReport(ModelSQL, ModelView): return super(ActionReport, self).write(ids, vals) def delete(self, ids): - action_obj = self.pool.get('ir.action') + pool = Pool() + action_obj = pool.get('ir.action') if isinstance(ids, (int, long)): ids = [ids] @@ -563,7 +572,8 @@ class ActionActWindow(ModelSQL, ModelView): return new_id def delete(self, ids): - action_obj = self.pool.get('ir.action') + pool = Pool() + action_obj = pool.get('ir.action') if isinstance(ids, (int, long)): ids = [ids] @@ -639,7 +649,8 @@ class ActionWizard(ModelSQL, ModelView): return new_id def delete(self, ids): - action_obj = self.pool.get('ir.action') + pool = Pool() + action_obj = pool.get('ir.action') if isinstance(ids, (int, long)): ids = [ids] @@ -773,7 +784,8 @@ class ActionURL(ModelSQL, ModelView): return new_id def delete(self, ids): - action_obj = self.pool.get('ir.action') + pool = Pool() + action_obj = pool.get('ir.action') if isinstance(ids, (int, long)): ids = [ids] diff --git a/trytond/ir/action.xml b/trytond/ir/action.xml index 088ca1c6e4..0fd88dbc8c 100644 --- a/trytond/ir/action.xml +++ b/trytond/ir/action.xml @@ -33,7 +33,6 @@ this repository contains the full copyright notices and license terms. --> - @@ -119,7 +118,6 @@ this repository contains the full copyright notices and license terms. --> - @@ -218,7 +216,6 @@ this repository contains the full copyright notices and license terms. --> - @@ -294,7 +291,6 @@ this repository contains the full copyright notices and license terms. --> - @@ -423,7 +419,6 @@ this repository contains the full copyright notices and license terms. --> - diff --git a/trytond/ir/attachment.py b/trytond/ir/attachment.py index 46d5df1aa1..4136554184 100644 --- a/trytond/ir/attachment.py +++ b/trytond/ir/attachment.py @@ -1,6 +1,5 @@ #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 __future__ import with_statement import os try: import hashlib @@ -13,6 +12,7 @@ from trytond.config import CONFIG from trytond.backend import TableHandler from trytond.transaction import Transaction from trytond.pyson import Not, Equal, Eval +from trytond.pool import Pool def firstline(description): try: @@ -84,7 +84,8 @@ class Attachment(ModelSQL, ModelView): return 0 def models_get(self): - model_obj = self.pool.get('ir.model') + pool = Pool() + model_obj = pool.get('ir.model') model_ids = model_obj.search([]) res = [] for model in model_obj.browse(model_ids): @@ -189,7 +190,8 @@ class Attachment(ModelSQL, ModelView): def check_access(self, ids, mode='read'): - model_access_obj = self.pool.get('ir.model.access') + pool = Pool() + model_access_obj = pool.get('ir.model.access') if Transaction().user == 0: return if not ids: @@ -224,7 +226,8 @@ class Attachment(ModelSQL, ModelView): return res def view_header_get(self, value, view_type='form'): - ir_model_obj = self.pool.get('ir.model') + pool = Pool() + ir_model_obj = pool.get('ir.model') value = super(Attachment, self).view_header_get(value, view_type=view_type) resource = Transaction().context.get('resource') @@ -232,7 +235,7 @@ class Attachment(ModelSQL, ModelView): model_name, record_id = resource.split(',', 1) ir_model_id, = ir_model_obj.search([('model', '=', model_name)]) ir_model = ir_model_obj.browse(ir_model_id) - model_obj = self.pool.get(model_name) + model_obj = pool.get(model_name) record = model_obj.browse(int(record_id)) value = '%s - %s - %s' % (ir_model.name, record.rec_name, value) return value diff --git a/trytond/ir/cron.py b/trytond/ir/cron.py index dfa81d2c45..99ed8d329b 100644 --- a/trytond/ir/cron.py +++ b/trytond/ir/cron.py @@ -1,18 +1,19 @@ #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 __future__ import with_statement import contextlib import datetime from dateutil.relativedelta import relativedelta import traceback import sys +import logging from trytond.backend import Database from trytond.model import ModelView, ModelSQL, fields from trytond.tools import safe_eval from trytond.transaction import Transaction +from trytond.pool import Pool +from trytond.backend import TableHandler _INTERVALTYPES = { - 'work_days': lambda interval: relativedelta(days=interval), 'days': lambda interval: relativedelta(days=interval), 'hours': lambda interval: relativedelta(hours=interval), 'weeks': lambda interval: relativedelta(weeks=interval), @@ -26,7 +27,8 @@ class Cron(ModelSQL, ModelView): _description = __doc__ name = fields.Char('Name', required=True, translate=True) user = fields.Many2One('res.user', 'Execution User', required=True, - help="The user used to execute this action") + domain=[('active', '=', False)], + help="The user used to execute this action") request_user = fields.Many2One( 'res.user', 'Request User', required=True, help="The user who will receive requests in case of failure") @@ -35,24 +37,20 @@ class Cron(ModelSQL, ModelView): interval_type = fields.Selection( [ ('minutes', 'Minutes'), ('hours', 'Hours'), - ('work_days', 'Work Days'), ('days', 'Days'), ('weeks', 'Weeks'), ('months', 'Months'), ], 'Interval Unit') - numbercall = fields.Integer('Number of calls', select=1, - help='Number of times the function is called,\n' \ - 'a negative number indicates that the function ' \ - 'will always be called') - doall = fields.Boolean('Repeat missed') - nextcall = fields.DateTime('Next call date', required=True, + number_calls = fields.Integer('Number of Calls', select=1, + help=('Number of times the function is called, a negative ' + 'number indicates that the function will always be ' + 'called')) + repeat_missed = fields.Boolean('Repeat Missed') + next_call = fields.DateTime('Next Call', required=True, select=1) model = fields.Char('Model') function = fields.Char('Function') args = fields.Text('Arguments') - priority = fields.Integer('Priority', - help='0=Very Urgent\n10=Not urgent') - running = fields.Boolean('Running', readonly=True, select=1) def __init__(self): super(Cron, self).__init__() @@ -62,135 +60,132 @@ class Cron(ModelSQL, ModelView): "properly: \"%s\"\n Traceback: \n\n%s\n" }) - def default_nextcall(self): + def init(self, module_name): + cursor = Transaction().cursor + + # Migration from 2.0: rename numbercall, doall and nextcall + table = TableHandler(cursor, self, module_name) + table.column_rename('numbercall', 'number_calls') + table.column_rename('doall', 'repeat_missed') + table.column_rename('nextcall', 'next_call') + + super(Cron, self).init(module_name) + + # Migration from 2.0: work_days removed + cursor.execute('UPDATE "%s" ' + 'SET interval_type = %%s ' + 'WHERE interval_type = %%s' % self._table, + ('days', 'work_days')) + + def default_next_call(self): return datetime.datetime.now() - def default_priority(self): - return 5 - - def default_user(self): - return int(Transaction().user) - def default_interval_number(self): return 1 def default_interval_type(self): return 'months' - def default_numbercall(self): - return 1 + def default_number_calls(self): + return -1 def default_active(self): return True - def default_doall(self): + def default_repeat_missed(self): return True - def default_running(self): - return False - def check_xml_record(self, ids, values): return True + def get_delta(self, cron): + ''' + Return the relativedelta for the next call + ''' + return _INTERVALTYPES[cron.interval_type](cron.interval_number) + + def _get_request_values(self, cron): + tb_s = reduce(lambda x, y: x + y, + traceback.format_exception(*sys.exc_info())) + tb_s = tb_s.decode('utf-8', 'ignore') + name = self.raise_user_error('request_title', + raise_exception=False) + body = self.raise_user_error('request_body', (cron.name, tb_s), + raise_exception=False) + values = { + 'name': name, + 'priority': '2', + 'act_from': cron.user.id, + 'act_to': cron.request_user.id, + 'body': body, + 'date_sent': datetime.datetime.now(), + 'references': [ + ('create', { + 'reference': '%s,%s' % (self._name, cron.id), + }), + ], + 'state': 'waiting', + 'trigger_date': datetime.datetime.now(), + } + return values + def _callback(self, cron): + pool = Pool() try: - args = (cron['args'] or []) and safe_eval(cron['args']) - obj = self.pool.get(cron['model']) - if not obj and hasattr(obj, cron['function']): - return False - fct = getattr(obj, cron['function']) - with Transaction().set_user(cron['user']): - fct(*args) + args = (cron.args or []) and safe_eval(cron.args) + model_obj = pool.get(cron.model) + with Transaction().set_user(cron.user.id): + getattr(model_obj, cron.function)(*args) except Exception, error: Transaction().cursor.rollback() - tb_s = '' - for line in traceback.format_exception(*sys.exc_info()): - try: - line = line.encode('utf-8', 'ignore') - except Exception: - continue - tb_s += line - try: - tb_s += str(error).decode('utf-8', 'ignore') - except Exception: - pass + request_obj = pool.get('res.request') + req_user = cron.request_user + language = (req_user.language.code if req_user.language + else 'en_US') + with contextlib.nested(Transaction().set_user(cron.user.id), + Transaction().set_context(language=language)): + values = self._get_request_values(cron) + rid = request_obj.create(values) + Transaction().cursor.commit() - request_obj = self.pool.get('res.request') - try: - user_obj = self.pool.get('res.user') - req_user = user_obj.browse(cron['request_user']) - language = (req_user.language.code if req_user.language - else 'en_US') - with contextlib.nested(Transaction().set_user(cron['user']), - Transaction().set_context(language=language)): - rid = request_obj.create({ - 'name': self.raise_user_error('request_title', - raise_exception=False), - 'priority': '2', - 'act_from': cron['user'], - 'act_to': cron['request_user'], - 'body': self.raise_user_error('request_body', - (cron['name'], tb_s), raise_exception=False), - 'date_sent': datetime.datetime.now(), - 'references': [ - ('create', { - 'reference': "ir.cron,%s" % cron['id'], - }), - ], - 'state': 'waiting', - 'trigger_date': datetime.datetime.now(), - }) - Transaction().cursor.commit() - except Exception: - Transaction().cursor.rollback() - - def pool_jobs(self, db_name): + def run(self, db_name): now = datetime.datetime.now() with Transaction().start(db_name, 0) as transaction: - try: - transaction.cursor.lock(self._table) - transaction.cursor.execute('SELECT * FROM ir_cron ' - 'WHERE numbercall <> 0 ' - 'AND active ' - 'AND nextcall <= %s ' - 'AND NOT running ' - 'ORDER BY priority', (datetime.datetime.now(),)) - crons = transaction.cursor.dictfetchall() + transaction.cursor.lock(self._table) + cron_ids = self.search([ + ('number_calls', '!=', 0), + ('next_call', '<=', datetime.datetime.now()), + ]) + crons = self.browse(cron_ids) - for cron in crons: - transaction.cursor.execute('UPDATE ir_cron ' - 'SET running = %s ' - 'WHERE id = %s', (True, cron['id'])) - nextcall = cron['nextcall'] - numbercall = cron['numbercall'] - done = False - - while nextcall < now and numbercall: - if numbercall > 0: - numbercall -= 1 - if not done or cron['doall']: + for cron in crons: + try: + next_call = cron.next_call + number_calls = cron.number_calls + first = True + while next_call < now and number_calls != 0: + if first or cron.repeat_missed: self._callback(cron) - if numbercall: - nextcall += _INTERVALTYPES[cron['interval_type']]( - cron['interval_number']) - done = True + next_call += self.get_delta(cron) + if number_calls > 0: + number_calls -= 1 + first = False - addsql = '' - addsql_param = [] - if not numbercall: - addsql = ', active = %s' - addsql_param = [False] - transaction.cursor.execute("UPDATE ir_cron " - "SET nextcall = %s, " - "running = %s, " - "numbercall = %s" + addsql + " " - "WHERE id = %s", - [nextcall, False, numbercall] + addsql_param + - [cron['id']]) + values = { + 'next_call': next_call, + 'number_calls': number_calls, + } + if not number_calls: + values['active'] = False + self.write(cron.id, values) transaction.cursor.commit() - except Exception: - transaction.cursor.rollback() - raise + except Exception: + transaction.cursor.rollback() + tb_s = reduce(lambda x, y: x + y, + traceback.format_exception(*sys.exc_info())) + tb_s = tb_s.decode('utf-8', 'ignore') + logger = logging.getLogger('cron') + logger.error('Exception:\n%s' % tb_s) Cron() diff --git a/trytond/ir/cron.xml b/trytond/ir/cron.xml index ac7842a8e9..cef16614b4 100644 --- a/trytond/ir/cron.xml +++ b/trytond/ir/cron.xml @@ -12,16 +12,14 @@ this repository contains the full copyright notices and license terms. --> - + - - + - ]]> @@ -31,37 +29,33 @@ this repository contains the full copyright notices and license terms. --> form +