X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=openerp%2Ftools%2Fmisc.py;h=6739f45cc5a83cda3ef86a7acd387cd4b1a9c219;hb=418739cf3ea8c3e825a55be3c75fac1d2d70970d;hp=e1a6a717e874af286478a51f936c4c8a627548d2;hpb=fb753b29eede7df92c5bfbd62c780783398baa25;p=odoo%2Fodoo.git diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index e1a6a71..6739f45 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -20,10 +20,13 @@ # ############################################################################## +#.apidoc title: Utilities: tools.misc + """ Miscelleanous tools used by OpenERP. """ +from functools import wraps import inspect import subprocess import logging @@ -36,28 +39,28 @@ import threading import time import warnings import zipfile +from collections import defaultdict from datetime import datetime from email.MIMEText import MIMEText from email.MIMEBase import MIMEBase from email.MIMEMultipart import MIMEMultipart from email.Header import Header from email.Utils import formatdate, COMMASPACE +from email import Utils from email import Encoders from itertools import islice, izip from lxml import etree from which import which -if sys.version_info[:2] < (2, 4): - from threadinglocal import local -else: - from threading import local +from threading import local try: from html2text import html2text except ImportError: html2text = None import openerp.loglevels as loglevels +import openerp.pooler as pooler from config import config -from lru import LRU +from cache import * # get_encodings, ustr and exception_to_unicode were originally from tools.misc. # There are moved to loglevels until we refactor tools. @@ -69,76 +72,6 @@ _logger = logging.getLogger('tools') # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones. SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase) -# initialize a database with base/base.sql -def init_db(cr): - import openerp.addons as addons - f = addons.get_module_resource('base', 'base.sql') - base_sql_file = file_open(f) - try: - cr.execute(base_sql_file.read()) - cr.commit() - finally: - base_sql_file.close() - - for i in addons.get_modules(): - mod_path = addons.get_module_path(i) - if not mod_path: - continue - - info = addons.load_information_from_description_file(i) - - if not info: - continue - categs = info.get('category', 'Uncategorized').split('/') - p_id = None - while categs: - if p_id is not None: - cr.execute('SELECT id \ - FROM ir_module_category \ - WHERE name=%s AND parent_id=%s', (categs[0], p_id)) - else: - cr.execute('SELECT id \ - FROM ir_module_category \ - WHERE name=%s AND parent_id IS NULL', (categs[0],)) - c_id = cr.fetchone() - if not c_id: - cr.execute('INSERT INTO ir_module_category \ - (name, parent_id) \ - VALUES (%s, %s) RETURNING id', (categs[0], p_id)) - c_id = cr.fetchone()[0] - else: - c_id = c_id[0] - p_id = c_id - categs = categs[1:] - - active = info.get('active', False) - installable = info.get('installable', True) - if installable: - if active: - state = 'to install' - else: - state = 'uninstalled' - else: - state = 'uninstallable' - cr.execute('INSERT INTO ir_module_module \ - (author, website, name, shortdesc, description, \ - category_id, state, certificate, web, license) \ - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', ( - info.get('author', ''), - info.get('website', ''), i, info.get('name', False), - info.get('description', ''), p_id, state, info.get('certificate') or None, - info.get('web') or False, - info.get('license') or 'AGPL-3')) - id = cr.fetchone()[0] - cr.execute('INSERT INTO ir_model_data \ - (name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', ( - 'module_meta_information', 'ir.module.module', i, id, True)) - dependencies = info.get('depends', []) - for d in dependencies: - cr.execute('INSERT INTO ir_module_module_dependency \ - (module_id,name) VALUES (%s, %s)', (id, d)) - cr.commit() - def find_in_path(name): try: return which(name) @@ -193,19 +126,21 @@ def exec_command_pipe(name, *args): def file_open(name, mode="r", subdir='addons', pathinfo=False): """Open a file from the OpenERP root, using a subdir folder. + Example:: + >>> file_open('hr/report/timesheer.xsl') >>> file_open('addons/hr/report/timesheet.xsl') >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True) - @param name: name of the file - @param mode: file open mode - @param subdir: subdirectory - @param pathinfo: if True returns tupple (fileobject, filepath) + @param name name of the file + @param mode file open mode + @param subdir subdirectory + @param pathinfo if True returns tupple (fileobject, filepath) - @return: fileobject if pathinfo is False else (fileobject, filepath) + @return fileobject if pathinfo is False else (fileobject, filepath) """ - import openerp.addons as addons - adps = addons.ad_paths + import openerp.modules as addons + adps = addons.module.ad_paths rtp = os.path.normcase(os.path.abspath(config['root_path'])) if name.replace(os.path.sep, '/').startswith('addons/'): @@ -286,7 +221,7 @@ def flatten(list): """Flatten a list of elements into a uniqu list Author: Christophe Simonis (christophe@tinyerp.com) - Examples: + Examples:: >>> flatten(['a']) ['a'] >>> flatten('b') @@ -315,6 +250,8 @@ def flatten(list): def reverse_enumerate(l): """Like enumerate but in the other sens + + Usage:: >>> a = ['a', 'b', 'c'] >>> it = reverse_enumerate(a) >>> it.next() @@ -343,27 +280,17 @@ email_re = re.compile(r""" """, re.VERBOSE) res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE) command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE) -reference_re = re.compile("<.*-openobject-(\\d+)@(.*)>", re.UNICODE) - -priorities = { - '1': '1 (Highest)', - '2': '2 (High)', - '3': '3 (Normal)', - '4': '4 (Low)', - '5': '5 (Lowest)', - } +reference_re = re.compile("<.*-open(?:object|erp)-(\\d+).*@(.*)>", re.UNICODE) def html2plaintext(html, body_id=None, encoding='utf-8'): + """ From an HTML text, convert the HTML to plain text. + If @param body_id is provided then this is the tag where the + body (not necessarily ) starts. + """ ## (c) Fry-IT, www.fry-it.com, 2007 ## ## download here: http://www.peterbe.com/plog/html2plaintext - - """ from an HTML text, convert the HTML to plain text. - If @body_id is provided then this is the tag where the - body (not necessarily ) starts. - """ - html = ustr(html) from lxml.etree import tostring @@ -419,149 +346,51 @@ def html2plaintext(html, body_id=None, encoding='utf-8'): return html -def generate_tracking_message_id(openobject_id): - """Returns a string that can be used in the Message-ID RFC822 header field so we - can track the replies related to a given object thanks to the "In-Reply-To" or - "References" fields that Mail User Agents will set. - """ - return "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname()) - -def _email_send(smtp_from, smtp_to_list, message, openobject_id=None, ssl=False, debug=False): - """Low-level method to send directly a Message through the configured smtp server. - :param smtp_from: RFC-822 envelope FROM (not displayed to recipient) - :param smtp_to_list: RFC-822 envelope RCPT_TOs (not displayed to recipient) - :param message: an email.message.Message to send - :param debug: True if messages should be output to stderr before being sent, - and smtplib.SMTP put into debug mode. - :return: True if the mail was delivered successfully to the smtp, - else False (+ exception logged) +def generate_tracking_message_id(res_id): + """Returns a string that can be used in the Message-ID RFC822 header field + + Used to track the replies related to a given object thanks to the "In-Reply-To" + or "References" fields that Mail User Agents will set. """ - class WriteToLogger(object): - def __init__(self): - self.logger = loglevels.Logger() + return "<%s-openerp-%s@%s>" % (time.time(), res_id, socket.gethostname()) - def write(self, s): - self.logger.notifyChannel('email_send', loglevels.LOG_DEBUG, s) +def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, + attachments=None, message_id=None, references=None, openobject_id=False, debug=False, subtype='plain', headers=None, + smtp_server=None, smtp_port=None, ssl=False, smtp_user=None, smtp_password=None, cr=None, uid=None): + """Low-level function for sending an email (deprecated). + + :deprecate: since OpenERP 6.1, please use ir.mail_server.send_email() instead. + :param email_from: A string used to fill the `From` header, if falsy, + config['email_from'] is used instead. Also used for + the `Reply-To` header if `reply_to` is not provided + :param email_to: a sequence of addresses to send the mail to. + """ - if openobject_id: - message['Message-Id'] = generate_tracking_message_id(openobject_id) + # If not cr, get cr from current thread database + if not cr: + db_name = getattr(threading.currentThread(), 'dbname', None) + if db_name: + cr = pooler.get_db_only(db_name).cursor() + else: + raise Exception("No database cursor found, please pass one explicitly") + # Send Email try: - smtp_server = config['smtp_server'] - - if smtp_server.startswith('maildir:/'): - from mailbox import Maildir - maildir_path = smtp_server[8:] - mdir = Maildir(maildir_path,factory=None, create = True) - mdir.add(message.as_string(True)) - return True - - oldstderr = smtplib.stderr - if not ssl: ssl = config.get('smtp_ssl', False) - s = smtplib.SMTP() - try: - # in case of debug, the messages are printed to stderr. - if debug: - smtplib.stderr = WriteToLogger() - - s.set_debuglevel(int(bool(debug))) # 0 or 1 - s.connect(smtp_server, config['smtp_port']) - if ssl: - s.ehlo() - s.starttls() - s.ehlo() - - if config['smtp_user'] or config['smtp_password']: - s.login(config['smtp_user'], config['smtp_password']) - - s.sendmail(smtp_from, smtp_to_list, message.as_string()) - finally: - try: - s.quit() - if debug: - smtplib.stderr = oldstderr - except Exception: - # ignored, just a consequence of the previous exception - pass - + mail_server_pool = pooler.get_pool(cr.dbname).get('ir.mail_server') + res = False + # Pack Message into MIME Object + email_msg = mail_server_pool.build_email(email_from, email_to, subject, body, email_cc, email_bcc, reply_to, + attachments, message_id, references, openobject_id, subtype, headers=headers) + + res = mail_server_pool.send_email(cr, uid or 1, email_msg, mail_server_id=None, + smtp_server=smtp_server, smtp_port=smtp_port, smtp_user=smtp_user, smtp_password=smtp_password, + smtp_encryption=('ssl' if ssl else None), debug=debug) except Exception: - _logger.error('could not deliver email', exc_info=True) + _log.exception("tools.email_send failed to deliver email") return False - - return True - - -def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, - attach=None, openobject_id=False, ssl=False, debug=False, subtype='plain', x_headers=None, priority='3'): - - """Send an email. - - Arguments: - - `email_from`: A string used to fill the `From` header, if falsy, - config['email_from'] is used instead. Also used for - the `Reply-To` header if `reply_to` is not provided - - `email_to`: a sequence of addresses to send the mail to. - """ - if x_headers is None: - x_headers = {} - - - if not (email_from or config['email_from']): - raise ValueError("Sending an email requires either providing a sender " - "address or having configured one") - - if not email_from: email_from = config.get('email_from', False) - email_from = ustr(email_from).encode('utf-8') - - if not email_cc: email_cc = [] - if not email_bcc: email_bcc = [] - if not body: body = u'' - - email_body = ustr(body).encode('utf-8') - email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8') - - msg = MIMEMultipart() - - msg['Subject'] = Header(ustr(subject), 'utf-8') - msg['From'] = email_from - del msg['Reply-To'] - if reply_to: - msg['Reply-To'] = reply_to - else: - msg['Reply-To'] = msg['From'] - msg['To'] = COMMASPACE.join(email_to) - if email_cc: - msg['Cc'] = COMMASPACE.join(email_cc) - if email_bcc: - msg['Bcc'] = COMMASPACE.join(email_bcc) - msg['Date'] = formatdate(localtime=True) - - msg['X-Priority'] = priorities.get(priority, '3 (Normal)') - - # Add dynamic X Header - for key, value in x_headers.iteritems(): - msg['%s' % key] = str(value) - - if html2text and subtype == 'html': - text = html2text(email_body.decode('utf-8')).encode('utf-8') - alternative_part = MIMEMultipart(_subtype="alternative") - alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain')) - alternative_part.attach(email_text) - msg.attach(alternative_part) - else: - msg.attach(email_text) - - if attach: - for (fname,fcontent) in attach: - part = MIMEBase('application', "octet-stream") - part.set_payload( fcontent ) - Encoders.encode_base64(part) - part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,)) - msg.attach(part) - - return _email_send(email_from, flatten([email_to, email_cc, email_bcc]), msg, openobject_id=openobject_id, ssl=ssl, debug=debug) + finally: + cr.close() + return res #---------------------------------------------------------- # SMS @@ -576,10 +405,9 @@ def sms_send(user, password, api_id, text, to): # FIXME: Use the logger if there is an error return True -#--------------------------------------------------------- -# Class that stores an updateable string (used in wizards) -#--------------------------------------------------------- class UpdateableStr(local): + """ Class that stores an updateable string (used in wizards) + """ def __init__(self, string=''): self.string = string @@ -595,7 +423,8 @@ class UpdateableStr(local): class UpdateableDict(local): - '''Stores an updateable dict to use in wizards''' + """Stores an updateable dict to use in wizards + """ def __init__(self, dict=None): if dict is None: @@ -692,9 +521,13 @@ class UpdateableDict(local): def __ne__(self, y): return self.dict.__ne__(y) - -# Don't use ! Use res.currency.round() class currency(float): + """ Deprecate + + .. warning:: + + Don't use ! Use res.currency.round() + """ def __init__(self, value, accuracy=2, rounding=None): if rounding is None: @@ -709,164 +542,9 @@ class currency(float): # display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy)) # return str(display_value) - -def is_hashable(h): - try: - hash(h) - return True - except TypeError: - return False - -class cache(object): - """ - Use it as a decorator of the function you plan to cache - Timeout: 0 = no timeout, otherwise in seconds - """ - - __caches = [] - - def __init__(self, timeout=None, skiparg=2, multi=None, size=8192): - assert skiparg >= 2 # at least self and cr - if timeout is None: - self.timeout = config['cache_timeout'] - else: - self.timeout = timeout - self.skiparg = skiparg - self.multi = multi - self.lasttime = time.time() - self.cache = LRU(size) # TODO take size from config - self.fun = None - cache.__caches.append(self) - - - def _generate_keys(self, dbname, kwargs2): - """ - Generate keys depending of the arguments and the self.mutli value - """ - - def to_tuple(d): - pairs = d.items() - pairs.sort(key=lambda (k,v): k) - for i, (k, v) in enumerate(pairs): - if isinstance(v, dict): - pairs[i] = (k, to_tuple(v)) - if isinstance(v, (list, set)): - pairs[i] = (k, tuple(v)) - elif not is_hashable(v): - pairs[i] = (k, repr(v)) - return tuple(pairs) - - if not self.multi: - key = (('dbname', dbname),) + to_tuple(kwargs2) - yield key, None - else: - multis = kwargs2[self.multi][:] - for id in multis: - kwargs2[self.multi] = (id,) - key = (('dbname', dbname),) + to_tuple(kwargs2) - yield key, id - - def _unify_args(self, *args, **kwargs): - # Update named arguments with positional argument values (without self and cr) - kwargs2 = self.fun_default_values.copy() - kwargs2.update(kwargs) - kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:]))) - return kwargs2 - - def clear(self, dbname, *args, **kwargs): - """clear the cache for database dbname - if *args and **kwargs are both empty, clear all the keys related to this database - """ - if not args and not kwargs: - keys_to_del = [key for key in self.cache.keys() if key[0][1] == dbname] - else: - kwargs2 = self._unify_args(*args, **kwargs) - keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache.keys()] - - for key in keys_to_del: - self.cache.pop(key) - - @classmethod - def clean_caches_for_db(cls, dbname): - for c in cls.__caches: - c.clear(dbname) - - def __call__(self, fn): - if self.fun is not None: - raise Exception("Can not use a cache instance on more than one function") - self.fun = fn - - argspec = inspect.getargspec(fn) - self.fun_arg_names = argspec[0][self.skiparg:] - self.fun_default_values = {} - if argspec[3]: - self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3])) - - def cached_result(self2, cr, *args, **kwargs): - if time.time()-int(self.timeout) > self.lasttime: - self.lasttime = time.time() - t = time.time()-int(self.timeout) - old_keys = [key for key in self.cache.keys() if self.cache[key][1] < t] - for key in old_keys: - self.cache.pop(key) - - kwargs2 = self._unify_args(*args, **kwargs) - - result = {} - notincache = {} - for key, id in self._generate_keys(cr.dbname, kwargs2): - if key in self.cache: - result[id] = self.cache[key][0] - else: - notincache[id] = key - - if notincache: - if self.multi: - kwargs2[self.multi] = notincache.keys() - - result2 = fn(self2, cr, *args[:self.skiparg-2], **kwargs2) - if not self.multi: - key = notincache[None] - self.cache[key] = (result2, time.time()) - result[None] = result2 - else: - for id in result2: - key = notincache[id] - self.cache[key] = (result2[id], time.time()) - result.update(result2) - - if not self.multi: - return result[None] - return result - - cached_result.clear_cache = self.clear - return cached_result - def to_xml(s): return s.replace('&','&').replace('<','<').replace('>','>') -# to be compatible with python 2.4 -import __builtin__ -if not hasattr(__builtin__, 'all'): - def all(iterable): - for element in iterable: - if not element: - return False - return True - - __builtin__.all = all - del all - -if not hasattr(__builtin__, 'any'): - def any(iterable): - for element in iterable: - if element: - return True - return False - - __builtin__.any = any - del any - def get_iso_codes(lang): if lang.find('_') != -1: if lang.split('_')[0] == lang.split('_')[1].lower(): @@ -1013,8 +691,6 @@ def human_size(sz): return "%0.2f %s" % (s, units[i]) def logged(f): - from func import wraps - @wraps(f) def wrapper(*args, **kwargs): from pprint import pformat @@ -1040,8 +716,6 @@ class profile(object): self.fname = fname def __call__(self, f): - from func import wraps - @wraps(f) def wrapper(*args, **kwargs): class profile_wrapper(object): @@ -1057,40 +731,6 @@ class profile(object): return wrapper -def debug(what): - """ - This method allow you to debug your code without print - Example: - >>> def func_foo(bar) - ... baz = bar - ... debug(baz) - ... qnx = (baz, bar) - ... debug(qnx) - ... - >>> func_foo(42) - - This will output on the logger: - - [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42 - [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42) - - To view the DEBUG lines in the logger you must start the server with the option - --log-level=debug - - """ - warnings.warn("The tools.debug() method is deprecated, please use logging.", - DeprecationWarning, stacklevel=2) - from inspect import stack - from pprint import pformat - st = stack()[1] - param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip() - while param.count(')') > param.count('('): param = param[:param.rfind(')')] - what = pformat(what) - if param != what: - what = "%s = %s" % (param, what) - logging.getLogger(st[3]).debug(what) - - __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD', 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER', 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE', @@ -1213,7 +853,7 @@ def detect_ip_addr(): # times. def get_win32_timezone(): """Attempt to return the "standard name" of the current timezone on a win32 system. - @return: the standard name of the current win32 timezone, or False if it cannot be found. + @return the standard name of the current win32 timezone, or False if it cannot be found. """ res = False if (sys.platform == "win32"): @@ -1231,7 +871,7 @@ def get_win32_timezone(): def detect_server_timezone(): """Attempt to detect the timezone to use on the server side. Defaults to UTC if no working timezone can be found. - @return: the timezone identifier as expected by pytz.timezone. + @return the timezone identifier as expected by pytz.timezone. """ try: import pytz @@ -1282,10 +922,7 @@ def detect_server_timezone(): return 'UTC' def get_server_timezone(): - # timezone detection is safe in multithread, so lazy init is ok here - if (not config['timezone']): - config['timezone'] = detect_server_timezone() - return config['timezone'] + return "UTC" DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d" @@ -1357,7 +994,7 @@ def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_nam @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed using src_format or formatted using dst_format. - @return: local/client formatted timestamp, expressed in the local/client timezone if possible + @return local/client formatted timestamp, expressed in the local/client timezone if possible and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined. """ if not src_tstamp_str: @@ -1422,6 +1059,21 @@ def upload_data(email, data, type='SURVEY'): a.start() return True +def get_and_group_by_field(cr, uid, obj, ids, field, context=None): + """ Read the values of ``field´´ for the given ``ids´´ and group ids by value. + + :param string field: name of the field we want to read and group by + :return: mapping of field values to the list of ids that have it + :rtype: dict + """ + res = {} + for record in obj.read(cr, uid, ids, [field], context=context): + key = record[field] + res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id']) + return res + +def get_and_group_by_company(cr, uid, obj, ids, context=None): + return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context) # port of python 2.6's attrgetter with support for dotted notation def resolve_attr(obj, attr): @@ -1444,18 +1096,46 @@ class unquote(str): or escaping, keeping the original string untouched. The name come from Lisp's unquote. One of the uses for this is to preserve or insert bare variable names within dicts during eval() of a dict's repr(). Use with care. - Some examples: + + Some examples (notice that there are never quotes surrounding + the ``active_id`` name: + >>> unquote('active_id') active_id - >>> repr(unquote('active_id')) - active_id >>> d = {'test': unquote('active_id')} >>> d {'test': active_id} - >>> repr(d) - "{'test': active_id}" + >>> print d + {'test': active_id} """ def __repr__(self): return self +class UnquoteEvalContext(defaultdict): + """Defaultdict-based evaluation context that returns + an ``unquote`` string for any missing name used during + the evaluation. + Mostly useful for evaluating OpenERP domains/contexts that + may refer to names that are unknown at the time of eval, + so that when the context/domain is converted back to a string, + the original names are preserved. + + **Warning**: using an ``UnquoteEvalContext`` as context for ``eval()`` or + ``safe_eval()`` will shadow the builtins, which may cause other + failures, depending on what is evaluated. + + Example (notice that ``section_id`` is preserved in the final + result) : + + >>> context_str = "{'default_user_id': uid, 'default_section_id': section_id}" + >>> eval(context_str, UnquoteEvalContext(uid=1)) + {'default_user_id': 1, 'default_section_id': section_id} + + """ + def __init__(self, *args, **kwargs): + super(UnquoteEvalContext, self).__init__(None, *args, **kwargs) + + def __missing__(self, key): + return unquote(key) + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: