[MERGE] forward port of branch 8.0 up to 591e329
[odoo/odoo.git] / openerp / tools / misc.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #    Copyright (C) 2010-2014 OpenERP s.a. (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23
24 """
25 Miscellaneous tools used by OpenERP.
26 """
27
28 from functools import wraps
29 import cProfile
30 from contextlib import contextmanager
31 import subprocess
32 import logging
33 import os
34 import socket
35 import sys
36 import threading
37 import time
38 import werkzeug.utils
39 import zipfile
40 from collections import defaultdict, Mapping
41 from datetime import datetime
42 from itertools import islice, izip, groupby
43 from lxml import etree
44 from which import which
45 from threading import local
46 import traceback
47
48 try:
49     from html2text import html2text
50 except ImportError:
51     html2text = None
52
53 from config import config
54 from cache import *
55 from .parse_version import parse_version 
56
57 import openerp
58 # get_encodings, ustr and exception_to_unicode were originally from tools.misc.
59 # There are moved to loglevels until we refactor tools.
60 from openerp.loglevels import get_encodings, ustr, exception_to_unicode     # noqa
61
62 _logger = logging.getLogger(__name__)
63
64 # List of etree._Element subclasses that we choose to ignore when parsing XML.
65 # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
66 SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase)
67
68 def find_in_path(name):
69     path = os.environ.get('PATH', os.defpath).split(os.pathsep)
70     if config.get('bin_path') and config['bin_path'] != 'None':
71         path.append(config['bin_path'])
72     try:
73         return which(name, path=os.pathsep.join(path))
74     except IOError:
75         return None
76
77 def find_pg_tool(name):
78     path = None
79     if config['pg_path'] and config['pg_path'] != 'None':
80         path = config['pg_path']
81     try:
82         return which(name, path=path)
83     except IOError:
84         return None
85
86 def exec_pg_command(name, *args):
87     prog = find_pg_tool(name)
88     if not prog:
89         raise Exception('Couldn\'t find %s' % name)
90     args2 = (prog,) + args
91
92     with open(os.devnull) as dn:
93         return subprocess.call(args2, stdout=dn, stderr=subprocess.STDOUT)
94
95 def exec_pg_command_pipe(name, *args):
96     prog = find_pg_tool(name)
97     if not prog:
98         raise Exception('Couldn\'t find %s' % name)
99     # on win32, passing close_fds=True is not compatible
100     # with redirecting std[in/err/out]
101     pop = subprocess.Popen((prog,) + args, bufsize= -1,
102           stdin=subprocess.PIPE, stdout=subprocess.PIPE,
103           close_fds=(os.name=="posix"))
104     return pop.stdin, pop.stdout
105
106 def exec_command_pipe(name, *args):
107     prog = find_in_path(name)
108     if not prog:
109         raise Exception('Couldn\'t find %s' % name)
110     # on win32, passing close_fds=True is not compatible
111     # with redirecting std[in/err/out]
112     pop = subprocess.Popen((prog,) + args, bufsize= -1,
113           stdin=subprocess.PIPE, stdout=subprocess.PIPE,
114           close_fds=(os.name=="posix"))
115     return pop.stdin, pop.stdout
116
117 #----------------------------------------------------------
118 # File paths
119 #----------------------------------------------------------
120 #file_path_root = os.getcwd()
121 #file_path_addons = os.path.join(file_path_root, 'addons')
122
123 def file_open(name, mode="r", subdir='addons', pathinfo=False):
124     """Open a file from the OpenERP root, using a subdir folder.
125
126     Example::
127     
128     >>> file_open('hr/report/timesheer.xsl')
129     >>> file_open('addons/hr/report/timesheet.xsl')
130     >>> file_open('../../base/report/rml_template.xsl', subdir='addons/hr/report', pathinfo=True)
131
132     @param name name of the file
133     @param mode file open mode
134     @param subdir subdirectory
135     @param pathinfo if True returns tuple (fileobject, filepath)
136
137     @return fileobject if pathinfo is False else (fileobject, filepath)
138     """
139     import openerp.modules as addons
140     adps = addons.module.ad_paths
141     rtp = os.path.normcase(os.path.abspath(config['root_path']))
142
143     basename = name
144
145     if os.path.isabs(name):
146         # It is an absolute path
147         # Is it below 'addons_path' or 'root_path'?
148         name = os.path.normcase(os.path.normpath(name))
149         for root in adps + [rtp]:
150             root = os.path.normcase(os.path.normpath(root)) + os.sep
151             if name.startswith(root):
152                 base = root.rstrip(os.sep)
153                 name = name[len(base) + 1:]
154                 break
155         else:
156             # It is outside the OpenERP root: skip zipfile lookup.
157             base, name = os.path.split(name)
158         return _fileopen(name, mode=mode, basedir=base, pathinfo=pathinfo, basename=basename)
159
160     if name.replace(os.sep, '/').startswith('addons/'):
161         subdir = 'addons'
162         name2 = name[7:]
163     elif subdir:
164         name = os.path.join(subdir, name)
165         if name.replace(os.sep, '/').startswith('addons/'):
166             subdir = 'addons'
167             name2 = name[7:]
168         else:
169             name2 = name
170
171     # First, try to locate in addons_path
172     if subdir:
173         for adp in adps:
174             try:
175                 return _fileopen(name2, mode=mode, basedir=adp,
176                                  pathinfo=pathinfo, basename=basename)
177             except IOError:
178                 pass
179
180     # Second, try to locate in root_path
181     return _fileopen(name, mode=mode, basedir=rtp, pathinfo=pathinfo, basename=basename)
182
183
184 def _fileopen(path, mode, basedir, pathinfo, basename=None):
185     name = os.path.normpath(os.path.join(basedir, path))
186
187     if basename is None:
188         basename = name
189     # Give higher priority to module directories, which is
190     # a more common case than zipped modules.
191     if os.path.isfile(name):
192         fo = open(name, mode)
193         if pathinfo:
194             return fo, name
195         return fo
196
197     # Support for loading modules in zipped form.
198     # This will not work for zipped modules that are sitting
199     # outside of known addons paths.
200     head = os.path.normpath(path)
201     zipname = False
202     while os.sep in head:
203         head, tail = os.path.split(head)
204         if not tail:
205             break
206         if zipname:
207             zipname = os.path.join(tail, zipname)
208         else:
209             zipname = tail
210         zpath = os.path.join(basedir, head + '.zip')
211         if zipfile.is_zipfile(zpath):
212             from cStringIO import StringIO
213             zfile = zipfile.ZipFile(zpath)
214             try:
215                 fo = StringIO()
216                 fo.write(zfile.read(os.path.join(
217                     os.path.basename(head), zipname).replace(
218                         os.sep, '/')))
219                 fo.seek(0)
220                 if pathinfo:
221                     return fo, name
222                 return fo
223             except Exception:
224                 pass
225     # Not found
226     if name.endswith('.rml'):
227         raise IOError('Report %r doesn\'t exist or deleted' % basename)
228     raise IOError('File not found: %s' % basename)
229
230
231 #----------------------------------------------------------
232 # iterables
233 #----------------------------------------------------------
234 def flatten(list):
235     """Flatten a list of elements into a uniqu list
236     Author: Christophe Simonis (christophe@tinyerp.com)
237
238     Examples::
239     >>> flatten(['a'])
240     ['a']
241     >>> flatten('b')
242     ['b']
243     >>> flatten( [] )
244     []
245     >>> flatten( [[], [[]]] )
246     []
247     >>> flatten( [[['a','b'], 'c'], 'd', ['e', [], 'f']] )
248     ['a', 'b', 'c', 'd', 'e', 'f']
249     >>> t = (1,2,(3,), [4, 5, [6, [7], (8, 9), ([10, 11, (12, 13)]), [14, [], (15,)], []]])
250     >>> flatten(t)
251     [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
252     """
253
254     def isiterable(x):
255         return hasattr(x, "__iter__")
256
257     r = []
258     for e in list:
259         if isiterable(e):
260             map(r.append, flatten(e))
261         else:
262             r.append(e)
263     return r
264
265 def reverse_enumerate(l):
266     """Like enumerate but in the other sens
267     
268     Usage::
269     >>> a = ['a', 'b', 'c']
270     >>> it = reverse_enumerate(a)
271     >>> it.next()
272     (2, 'c')
273     >>> it.next()
274     (1, 'b')
275     >>> it.next()
276     (0, 'a')
277     >>> it.next()
278     Traceback (most recent call last):
279       File "<stdin>", line 1, in <module>
280     StopIteration
281     """
282     return izip(xrange(len(l)-1, -1, -1), reversed(l))
283
284 def topological_sort(elems):
285     """ Return a list of elements sorted so that their dependencies are listed
286     before them in the result.
287
288     :param elems: specifies the elements to sort with their dependencies; it is
289         a dictionary like `{element: dependencies}` where `dependencies` is a
290         collection of elements that must appear before `element`. The elements
291         of `dependencies` are not required to appear in `elems`; they will
292         simply not appear in the result.
293
294     :returns: a list with the keys of `elems` sorted according to their
295         specification.
296     """
297     # the algorithm is inspired by [Tarjan 1976],
298     # http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
299     result = []
300     visited = set()
301
302     def visit(n):
303         if n not in visited:
304             visited.add(n)
305             if n in elems:
306                 # first visit all dependencies of n, then append n to result
307                 map(visit, elems[n])
308                 result.append(n)
309
310     map(visit, elems)
311
312     return result
313
314
315 class UpdateableStr(local):
316     """ Class that stores an updateable string (used in wizards)
317     """
318
319     def __init__(self, string=''):
320         self.string = string
321
322     def __str__(self):
323         return str(self.string)
324
325     def __repr__(self):
326         return str(self.string)
327
328     def __nonzero__(self):
329         return bool(self.string)
330
331
332 class UpdateableDict(local):
333     """Stores an updateable dict to use in wizards
334     """
335
336     def __init__(self, dict=None):
337         if dict is None:
338             dict = {}
339         self.dict = dict
340
341     def __str__(self):
342         return str(self.dict)
343
344     def __repr__(self):
345         return str(self.dict)
346
347     def clear(self):
348         return self.dict.clear()
349
350     def keys(self):
351         return self.dict.keys()
352
353     def __setitem__(self, i, y):
354         self.dict.__setitem__(i, y)
355
356     def __getitem__(self, i):
357         return self.dict.__getitem__(i)
358
359     def copy(self):
360         return self.dict.copy()
361
362     def iteritems(self):
363         return self.dict.iteritems()
364
365     def iterkeys(self):
366         return self.dict.iterkeys()
367
368     def itervalues(self):
369         return self.dict.itervalues()
370
371     def pop(self, k, d=None):
372         return self.dict.pop(k, d)
373
374     def popitem(self):
375         return self.dict.popitem()
376
377     def setdefault(self, k, d=None):
378         return self.dict.setdefault(k, d)
379
380     def update(self, E, **F):
381         return self.dict.update(E, F)
382
383     def values(self):
384         return self.dict.values()
385
386     def get(self, k, d=None):
387         return self.dict.get(k, d)
388
389     def has_key(self, k):
390         return self.dict.has_key(k)
391
392     def items(self):
393         return self.dict.items()
394
395     def __cmp__(self, y):
396         return self.dict.__cmp__(y)
397
398     def __contains__(self, k):
399         return self.dict.__contains__(k)
400
401     def __delitem__(self, y):
402         return self.dict.__delitem__(y)
403
404     def __eq__(self, y):
405         return self.dict.__eq__(y)
406
407     def __ge__(self, y):
408         return self.dict.__ge__(y)
409
410     def __gt__(self, y):
411         return self.dict.__gt__(y)
412
413     def __hash__(self):
414         return self.dict.__hash__()
415
416     def __iter__(self):
417         return self.dict.__iter__()
418
419     def __le__(self, y):
420         return self.dict.__le__(y)
421
422     def __len__(self):
423         return self.dict.__len__()
424
425     def __lt__(self, y):
426         return self.dict.__lt__(y)
427
428     def __ne__(self, y):
429         return self.dict.__ne__(y)
430
431 class currency(float):
432     """ Deprecate
433     
434     .. warning::
435     
436     Don't use ! Use res.currency.round()
437     """
438
439     def __init__(self, value, accuracy=2, rounding=None):
440         if rounding is None:
441             rounding=10**-accuracy
442         self.rounding=rounding
443         self.accuracy=accuracy
444
445     def __new__(cls, value, accuracy=2, rounding=None):
446         return float.__new__(cls, round(value, accuracy))
447
448     #def __str__(self):
449     #   display_value = int(self*(10**(-self.accuracy))/self.rounding)*self.rounding/(10**(-self.accuracy))
450     #   return str(display_value)
451
452 def to_xml(s):
453     return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
454
455 def get_iso_codes(lang):
456     if lang.find('_') != -1:
457         if lang.split('_')[0] == lang.split('_')[1].lower():
458             lang = lang.split('_')[0]
459     return lang
460
461 ALL_LANGUAGES = {
462         'ab_RU': u'Abkhazian / аҧсуа',
463         'am_ET': u'Amharic / አምሃርኛ',
464         'ar_SY': u'Arabic / الْعَرَبيّة',
465         'bg_BG': u'Bulgarian / български език',
466         'bs_BA': u'Bosnian / bosanski jezik',
467         'ca_ES': u'Catalan / Català',
468         'cs_CZ': u'Czech / Čeština',
469         'da_DK': u'Danish / Dansk',
470         'de_DE': u'German / Deutsch',
471         'el_GR': u'Greek / Ελληνικά',
472         'en_CA': u'English (CA)',
473         'en_GB': u'English (UK)',
474         'en_US': u'English (US)',
475         'es_AR': u'Spanish (AR) / Español (AR)',
476         'es_BO': u'Spanish (BO) / Español (BO)',
477         'es_CL': u'Spanish (CL) / Español (CL)',
478         'es_CO': u'Spanish (CO) / Español (CO)',
479         'es_CR': u'Spanish (CR) / Español (CR)',
480         'es_DO': u'Spanish (DO) / Español (DO)',
481         'es_EC': u'Spanish (EC) / Español (EC)',
482         'es_ES': u'Spanish / Español',
483         'es_GT': u'Spanish (GT) / Español (GT)',
484         'es_HN': u'Spanish (HN) / Español (HN)',
485         'es_MX': u'Spanish (MX) / Español (MX)',
486         'es_NI': u'Spanish (NI) / Español (NI)',
487         'es_PA': u'Spanish (PA) / Español (PA)',
488         'es_PE': u'Spanish (PE) / Español (PE)',
489         'es_PR': u'Spanish (PR) / Español (PR)',
490         'es_PY': u'Spanish (PY) / Español (PY)',
491         'es_SV': u'Spanish (SV) / Español (SV)',
492         'es_UY': u'Spanish (UY) / Español (UY)',
493         'es_VE': u'Spanish (VE) / Español (VE)',
494         'et_EE': u'Estonian / Eesti keel',
495         'fa_IR': u'Persian / فارس',
496         'fi_FI': u'Finnish / Suomi',
497         'fr_BE': u'French (BE) / Français (BE)',
498         'fr_CA': u'French (CA) / Français (CA)',
499         'fr_CH': u'French (CH) / Français (CH)',
500         'fr_FR': u'French / Français',
501         'gl_ES': u'Galician / Galego',
502         'gu_IN': u'Gujarati / ગુજરાતી',
503         'he_IL': u'Hebrew / עִבְרִי',
504         'hi_IN': u'Hindi / हिंदी',
505         'hr_HR': u'Croatian / hrvatski jezik',
506         'hu_HU': u'Hungarian / Magyar',
507         'id_ID': u'Indonesian / Bahasa Indonesia',
508         'it_IT': u'Italian / Italiano',
509         'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
510         'ja_JP': u'Japanese / 日本語',
511         'ko_KP': u'Korean (KP) / 한국어 (KP)',
512         'ko_KR': u'Korean (KR) / 한국어 (KR)',
513         'lo_LA': u'Lao / ພາສາລາວ',
514         'lt_LT': u'Lithuanian / Lietuvių kalba',
515         'lv_LV': u'Latvian / latviešu valoda',
516         'mk_MK': u'Macedonian / македонски јазик',
517         'ml_IN': u'Malayalam / മലയാളം',
518         'mn_MN': u'Mongolian / монгол',
519         'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
520         'nl_NL': u'Dutch / Nederlands',
521         'nl_BE': u'Flemish (BE) / Vlaams (BE)',
522         'oc_FR': u'Occitan (FR, post 1500) / Occitan',
523         'pl_PL': u'Polish / Język polski',
524         'pt_BR': u'Portuguese (BR) / Português (BR)',
525         'pt_PT': u'Portuguese / Português',
526         'ro_RO': u'Romanian / română',
527         'ru_RU': u'Russian / русский язык',
528         'si_LK': u'Sinhalese / සිංහල',
529         'sl_SI': u'Slovenian / slovenščina',
530         'sk_SK': u'Slovak / Slovenský jazyk',
531         'sq_AL': u'Albanian / Shqip',
532         'sr_RS': u'Serbian (Cyrillic) / српски',
533         'sr@latin': u'Serbian (Latin) / srpski',
534         'sv_SE': u'Swedish / svenska',
535         'te_IN': u'Telugu / తెలుగు',
536         'tr_TR': u'Turkish / Türkçe',
537         'vi_VN': u'Vietnamese / Tiếng Việt',
538         'uk_UA': u'Ukrainian / українська',
539         'ur_PK': u'Urdu / اردو',
540         'zh_CN': u'Chinese (CN) / 简体中文',
541         'zh_HK': u'Chinese (HK)',
542         'zh_TW': u'Chinese (TW) / 正體字',
543         'th_TH': u'Thai / ภาษาไทย',
544         'tlh_TLH': u'Klingon',
545     }
546
547 def scan_languages():
548     """ Returns all languages supported by OpenERP for translation
549
550     :returns: a list of (lang_code, lang_name) pairs
551     :rtype: [(str, unicode)]
552     """
553     return sorted(ALL_LANGUAGES.iteritems(), key=lambda k: k[1])
554
555 def get_user_companies(cr, user):
556     def _get_company_children(cr, ids):
557         if not ids:
558             return []
559         cr.execute('SELECT id FROM res_company WHERE parent_id IN %s', (tuple(ids),))
560         res = [x[0] for x in cr.fetchall()]
561         res.extend(_get_company_children(cr, res))
562         return res
563     cr.execute('SELECT company_id FROM res_users WHERE id=%s', (user,))
564     user_comp = cr.fetchone()[0]
565     if not user_comp:
566         return []
567     return [user_comp] + _get_company_children(cr, [user_comp])
568
569 def mod10r(number):
570     """
571     Input number : account or invoice number
572     Output return: the same number completed with the recursive mod10
573     key
574     """
575     codec=[0,9,4,6,8,2,7,1,3,5]
576     report = 0
577     result=""
578     for digit in number:
579         result += digit
580         if digit.isdigit():
581             report = codec[ (int(digit) + report) % 10 ]
582     return result + str((10 - report) % 10)
583
584
585 def human_size(sz):
586     """
587     Return the size in a human readable format
588     """
589     if not sz:
590         return False
591     units = ('bytes', 'Kb', 'Mb', 'Gb')
592     if isinstance(sz,basestring):
593         sz=len(sz)
594     s, i = float(sz), 0
595     while s >= 1024 and i < len(units)-1:
596         s /= 1024
597         i += 1
598     return "%0.2f %s" % (s, units[i])
599
600 def logged(f):
601     @wraps(f)
602     def wrapper(*args, **kwargs):
603         from pprint import pformat
604
605         vector = ['Call -> function: %r' % f]
606         for i, arg in enumerate(args):
607             vector.append('  arg %02d: %s' % (i, pformat(arg)))
608         for key, value in kwargs.items():
609             vector.append('  kwarg %10s: %s' % (key, pformat(value)))
610
611         timeb4 = time.time()
612         res = f(*args, **kwargs)
613
614         vector.append('  result: %s' % pformat(res))
615         vector.append('  time delta: %s' % (time.time() - timeb4))
616         _logger.debug('\n'.join(vector))
617         return res
618
619     return wrapper
620
621 class profile(object):
622     def __init__(self, fname=None):
623         self.fname = fname
624
625     def __call__(self, f):
626         @wraps(f)
627         def wrapper(*args, **kwargs):
628             profile = cProfile.Profile()
629             result = profile.runcall(f, *args, **kwargs)
630             profile.dump_stats(self.fname or ("%s.cprof" % (f.func_name,)))
631             return result
632
633         return wrapper
634
635 __icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
636 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
637 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
638 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
639 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT',
640 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE',
641 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM',
642 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK',
643 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK',
644 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC',
645 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL',
646 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD',
647 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY',
648 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND',
649 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW',
650 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES',
651 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT',
652 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED',
653 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT',
654 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK',
655 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE',
656 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100',
657 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT',
658 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase',
659 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner',
660 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
661 'terp-check','terp-go-month','terp-go-year','terp-go-today','terp-document-new','terp-camera_test',
662 'terp-emblem-important','terp-gtk-media-pause','terp-gtk-stop','terp-gnome-cpu-frequency-applet+',
663 'terp-dialog-close','terp-gtk-jump-to-rtl','terp-gtk-jump-to-ltr','terp-accessories-archiver',
664 'terp-stock_align_left_24','terp-stock_effects-object-colorize','terp-go-home','terp-gtk-go-back-rtl',
665 'terp-gtk-go-back-ltr','terp-personal','terp-personal-','terp-personal+','terp-accessories-archiver-minus',
666 'terp-accessories-archiver+','terp-stock_symbol-selection','terp-call-start','terp-dolar',
667 'terp-face-plain','terp-folder-blue','terp-folder-green','terp-folder-orange','terp-folder-yellow',
668 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
669 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
670 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
671 ]
672
673 def icons(*a, **kw):
674     global __icons_list
675     return [(x, x) for x in __icons_list ]
676
677 def detect_ip_addr():
678     """Try a very crude method to figure out a valid external
679        IP or hostname for the current machine. Don't rely on this
680        for binding to an interface, but it could be used as basis
681        for constructing a remote URL to the server.
682     """
683     def _detect_ip_addr():
684         from array import array
685         from struct import pack, unpack
686
687         try:
688             import fcntl
689         except ImportError:
690             fcntl = None
691
692         ip_addr = None
693
694         if not fcntl: # not UNIX:
695             host = socket.gethostname()
696             ip_addr = socket.gethostbyname(host)
697         else: # UNIX:
698             # get all interfaces:
699             nbytes = 128 * 32
700             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
701             names = array('B', '\0' * nbytes)
702             #print 'names: ', names
703             outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
704             namestr = names.tostring()
705
706             # try 64 bit kernel:
707             for i in range(0, outbytes, 40):
708                 name = namestr[i:i+16].split('\0', 1)[0]
709                 if name != 'lo':
710                     ip_addr = socket.inet_ntoa(namestr[i+20:i+24])
711                     break
712
713             # try 32 bit kernel:
714             if ip_addr is None:
715                 ifaces = filter(None, [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)])
716
717                 for ifname in [iface for iface in ifaces if iface != 'lo']:
718                     ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
719                     break
720
721         return ip_addr or 'localhost'
722
723     try:
724         ip_addr = _detect_ip_addr()
725     except Exception:
726         ip_addr = 'localhost'
727     return ip_addr
728
729 # RATIONALE BEHIND TIMESTAMP CALCULATIONS AND TIMEZONE MANAGEMENT:
730 #  The server side never does any timestamp calculation, always
731 #  sends them in a naive (timezone agnostic) format supposed to be
732 #  expressed within the server timezone, and expects the clients to
733 #  provide timestamps in the server timezone as well.
734 #  It stores all timestamps in the database in naive format as well,
735 #  which also expresses the time in the server timezone.
736 #  For this reason the server makes its timezone name available via the
737 #  common/timezone_get() rpc method, which clients need to read
738 #  to know the appropriate time offset to use when reading/writing
739 #  times.
740 def get_win32_timezone():
741     """Attempt to return the "standard name" of the current timezone on a win32 system.
742        @return the standard name of the current win32 timezone, or False if it cannot be found.
743     """
744     res = False
745     if sys.platform == "win32":
746         try:
747             import _winreg
748             hklm = _winreg.ConnectRegistry(None,_winreg.HKEY_LOCAL_MACHINE)
749             current_tz_key = _winreg.OpenKey(hklm, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation", 0,_winreg.KEY_ALL_ACCESS)
750             res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0])  # [0] is value, [1] is type code
751             _winreg.CloseKey(current_tz_key)
752             _winreg.CloseKey(hklm)
753         except Exception:
754             pass
755     return res
756
757 def detect_server_timezone():
758     """Attempt to detect the timezone to use on the server side.
759        Defaults to UTC if no working timezone can be found.
760        @return the timezone identifier as expected by pytz.timezone.
761     """
762     try:
763         import pytz
764     except Exception:
765         _logger.warning("Python pytz module is not available. "
766             "Timezone will be set to UTC by default.")
767         return 'UTC'
768
769     # Option 1: the configuration option (did not exist before, so no backwards compatibility issue)
770     # Option 2: to be backwards compatible with 5.0 or earlier, the value from time.tzname[0], but only if it is known to pytz
771     # Option 3: the environment variable TZ
772     sources = [ (config['timezone'], 'OpenERP configuration'),
773                 (time.tzname[0], 'time.tzname'),
774                 (os.environ.get('TZ',False),'TZ environment variable'), ]
775     # Option 4: OS-specific: /etc/timezone on Unix
776     if os.path.exists("/etc/timezone"):
777         tz_value = False
778         try:
779             f = open("/etc/timezone")
780             tz_value = f.read(128).strip()
781         except Exception:
782             pass
783         finally:
784             f.close()
785         sources.append((tz_value,"/etc/timezone file"))
786     # Option 5: timezone info from registry on Win32
787     if sys.platform == "win32":
788         # Timezone info is stored in windows registry.
789         # However this is not likely to work very well as the standard name
790         # of timezones in windows is rarely something that is known to pytz.
791         # But that's ok, it is always possible to use a config option to set
792         # it explicitly.
793         sources.append((get_win32_timezone(),"Windows Registry"))
794
795     for (value,source) in sources:
796         if value:
797             try:
798                 tz = pytz.timezone(value)
799                 _logger.info("Using timezone %s obtained from %s.", tz.zone, source)
800                 return value
801             except pytz.UnknownTimeZoneError:
802                 _logger.warning("The timezone specified in %s (%s) is invalid, ignoring it.", source, value)
803
804     _logger.warning("No valid timezone could be detected, using default UTC "
805         "timezone. You can specify it explicitly with option 'timezone' in "
806         "the server configuration.")
807     return 'UTC'
808
809 def get_server_timezone():
810     return "UTC"
811
812
813 DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
814 DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
815 DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
816     DEFAULT_SERVER_DATE_FORMAT,
817     DEFAULT_SERVER_TIME_FORMAT)
818
819 # Python's strftime supports only the format directives
820 # that are available on the platform's libc, so in order to
821 # be cross-platform we map to the directives required by
822 # the C standard (1989 version), always available on platforms
823 # with a C standard implementation.
824 DATETIME_FORMATS_MAP = {
825         '%C': '', # century
826         '%D': '%m/%d/%Y', # modified %y->%Y
827         '%e': '%d',
828         '%E': '', # special modifier
829         '%F': '%Y-%m-%d',
830         '%g': '%Y', # modified %y->%Y
831         '%G': '%Y',
832         '%h': '%b',
833         '%k': '%H',
834         '%l': '%I',
835         '%n': '\n',
836         '%O': '', # special modifier
837         '%P': '%p',
838         '%R': '%H:%M',
839         '%r': '%I:%M:%S %p',
840         '%s': '', #num of seconds since epoch
841         '%T': '%H:%M:%S',
842         '%t': ' ', # tab
843         '%u': ' %w',
844         '%V': '%W',
845         '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
846         '%+': '%Y-%m-%d %H:%M:%S',
847
848         # %Z is a special case that causes 2 problems at least:
849         #  - the timezone names we use (in res_user.context_tz) come
850         #    from pytz, but not all these names are recognized by
851         #    strptime(), so we cannot convert in both directions
852         #    when such a timezone is selected and %Z is in the format
853         #  - %Z is replaced by an empty string in strftime() when
854         #    there is not tzinfo in a datetime value (e.g when the user
855         #    did not pick a context_tz). The resulting string does not
856         #    parse back if the format requires %Z.
857         # As a consequence, we strip it completely from format strings.
858         # The user can always have a look at the context_tz in
859         # preferences to check the timezone.
860         '%z': '',
861         '%Z': '',
862 }
863
864 POSIX_TO_LDML = {
865     'a': 'E',
866     'A': 'EEEE',
867     'b': 'MMM',
868     'B': 'MMMM',
869     #'c': '',
870     'd': 'dd',
871     'H': 'HH',
872     'I': 'hh',
873     'j': 'DDD',
874     'm': 'MM',
875     'M': 'mm',
876     'p': 'a',
877     'S': 'ss',
878     'U': 'w',
879     'w': 'e',
880     'W': 'w',
881     'y': 'yy',
882     'Y': 'yyyy',
883     # see comments above, and babel's format_datetime assumes an UTC timezone
884     # for naive datetime objects
885     #'z': 'Z',
886     #'Z': 'z',
887 }
888
889 def posix_to_ldml(fmt, locale):
890     """ Converts a posix/strftime pattern into an LDML date format pattern.
891
892     :param fmt: non-extended C89/C90 strftime pattern
893     :param locale: babel locale used for locale-specific conversions (e.g. %x and %X)
894     :return: unicode
895     """
896     buf = []
897     pc = False
898     quoted = []
899
900     for c in fmt:
901         # LDML date format patterns uses letters, so letters must be quoted
902         if not pc and c.isalpha():
903             quoted.append(c if c != "'" else "''")
904             continue
905         if quoted:
906             buf.append("'")
907             buf.append(''.join(quoted))
908             buf.append("'")
909             quoted = []
910
911         if pc:
912             if c == '%': # escaped percent
913                 buf.append('%')
914             elif c == 'x': # date format, short seems to match
915                 buf.append(locale.date_formats['short'].pattern)
916             elif c == 'X': # time format, seems to include seconds. short does not
917                 buf.append(locale.time_formats['medium'].pattern)
918             else: # look up format char in static mapping
919                 buf.append(POSIX_TO_LDML[c])
920             pc = False
921         elif c == '%':
922             pc = True
923         else:
924             buf.append(c)
925
926     # flush anything remaining in quoted buffer
927     if quoted:
928         buf.append("'")
929         buf.append(''.join(quoted))
930         buf.append("'")
931
932     return ''.join(buf)
933
934 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
935         tz_offset=True, ignore_unparsable_time=True):
936     """
937     Convert a source timestamp string into a destination timestamp string, attempting to apply the
938     correct offset if both the server and local timezone are recognized, or no
939     offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
940
941     WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
942              the client would not be able to format/offset it correctly. DO NOT use it for returning
943              date fields directly, these are supposed to be handled by the client!!
944
945     @param src_tstamp_str: the str value containing the timestamp in the server timezone.
946     @param src_format: the format to use when parsing the server timestamp.
947     @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
948     @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
949     @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
950                                    using src_format or formatted using dst_format.
951
952     @return local/client formatted timestamp, expressed in the local/client timezone if possible
953             and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
954     """
955     if not src_tstamp_str:
956         return False
957
958     res = src_tstamp_str
959     if src_format and dst_format:
960         # find out server timezone
961         server_tz = get_server_timezone()
962         try:
963             # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
964             dt_value = datetime.strptime(src_tstamp_str, src_format)
965             if tz_offset and dst_tz_name:
966                 try:
967                     import pytz
968                     src_tz = pytz.timezone(server_tz)
969                     dst_tz = pytz.timezone(dst_tz_name)
970                     src_dt = src_tz.localize(dt_value, is_dst=True)
971                     dt_value = src_dt.astimezone(dst_tz)
972                 except Exception:
973                     pass
974             res = dt_value.strftime(dst_format)
975         except Exception:
976             # Normal ways to end up here are if strptime or strftime failed
977             if not ignore_unparsable_time:
978                 return False
979     return res
980
981
982 def split_every(n, iterable, piece_maker=tuple):
983     """Splits an iterable into length-n pieces. The last piece will be shorter
984        if ``n`` does not evenly divide the iterable length.
985        @param ``piece_maker``: function to build the pieces
986        from the slices (tuple,list,...)
987     """
988     iterator = iter(iterable)
989     piece = piece_maker(islice(iterator, n))
990     while piece:
991         yield piece
992         piece = piece_maker(islice(iterator, n))
993
994 if __name__ == '__main__':
995     import doctest
996     doctest.testmod()
997
998 class upload_data_thread(threading.Thread):
999     def __init__(self, email, data, type):
1000         self.args = [('email',email),('type',type),('data',data)]
1001         super(upload_data_thread,self).__init__()
1002     def run(self):
1003         try:
1004             import urllib
1005             args = urllib.urlencode(self.args)
1006             fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
1007             fp.read()
1008             fp.close()
1009         except Exception:
1010             pass
1011
1012 def upload_data(email, data, type='SURVEY'):
1013     a = upload_data_thread(email, data, type)
1014     a.start()
1015     return True
1016
1017 def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
1018     """ Read the values of ``field´´ for the given ``ids´´ and group ids by value.
1019
1020        :param string field: name of the field we want to read and group by
1021        :return: mapping of field values to the list of ids that have it
1022        :rtype: dict
1023     """
1024     res = {}
1025     for record in obj.read(cr, uid, ids, [field], context=context):
1026         key = record[field]
1027         res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
1028     return res
1029
1030 def get_and_group_by_company(cr, uid, obj, ids, context=None):
1031     return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context)
1032
1033 # port of python 2.6's attrgetter with support for dotted notation
1034 def resolve_attr(obj, attr):
1035     for name in attr.split("."):
1036         obj = getattr(obj, name)
1037     return obj
1038
1039 def attrgetter(*items):
1040     if len(items) == 1:
1041         attr = items[0]
1042         def g(obj):
1043             return resolve_attr(obj, attr)
1044     else:
1045         def g(obj):
1046             return tuple(resolve_attr(obj, attr) for attr in items)
1047     return g
1048
1049 class unquote(str):
1050     """A subclass of str that implements repr() without enclosing quotation marks
1051        or escaping, keeping the original string untouched. The name come from Lisp's unquote.
1052        One of the uses for this is to preserve or insert bare variable names within dicts during eval()
1053        of a dict's repr(). Use with care.
1054
1055        Some examples (notice that there are never quotes surrounding
1056        the ``active_id`` name:
1057
1058        >>> unquote('active_id')
1059        active_id
1060        >>> d = {'test': unquote('active_id')}
1061        >>> d
1062        {'test': active_id}
1063        >>> print d
1064        {'test': active_id}
1065     """
1066     def __repr__(self):
1067         return self
1068
1069 class UnquoteEvalContext(defaultdict):
1070     """Defaultdict-based evaluation context that returns 
1071        an ``unquote`` string for any missing name used during
1072        the evaluation.
1073        Mostly useful for evaluating OpenERP domains/contexts that
1074        may refer to names that are unknown at the time of eval,
1075        so that when the context/domain is converted back to a string,
1076        the original names are preserved.
1077
1078        **Warning**: using an ``UnquoteEvalContext`` as context for ``eval()`` or
1079        ``safe_eval()`` will shadow the builtins, which may cause other
1080        failures, depending on what is evaluated.
1081
1082        Example (notice that ``section_id`` is preserved in the final
1083        result) :
1084
1085        >>> context_str = "{'default_user_id': uid, 'default_section_id': section_id}"
1086        >>> eval(context_str, UnquoteEvalContext(uid=1))
1087        {'default_user_id': 1, 'default_section_id': section_id}
1088
1089        """
1090     def __init__(self, *args, **kwargs):
1091         super(UnquoteEvalContext, self).__init__(None, *args, **kwargs)
1092
1093     def __missing__(self, key):
1094         return unquote(key)
1095
1096
1097 class mute_logger(object):
1098     """Temporary suppress the logging.
1099     Can be used as context manager or decorator.
1100
1101         @mute_logger('openerp.plic.ploc')
1102         def do_stuff():
1103             blahblah()
1104
1105         with mute_logger('openerp.foo.bar'):
1106             do_suff()
1107
1108     """
1109     def __init__(self, *loggers):
1110         self.loggers = loggers
1111
1112     def filter(self, record):
1113         return 0
1114
1115     def __enter__(self):
1116         for logger in self.loggers:
1117             assert isinstance(logger, basestring),\
1118                 "A logger name must be a string, got %s" % type(logger)
1119             logging.getLogger(logger).addFilter(self)
1120
1121     def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
1122         for logger in self.loggers:
1123             logging.getLogger(logger).removeFilter(self)
1124
1125     def __call__(self, func):
1126         @wraps(func)
1127         def deco(*args, **kwargs):
1128             with self:
1129                 return func(*args, **kwargs)
1130         return deco
1131
1132 _ph = object()
1133 class CountingStream(object):
1134     """ Stream wrapper counting the number of element it has yielded. Similar
1135     role to ``enumerate``, but for use when the iteration process of the stream
1136     isn't fully under caller control (the stream can be iterated from multiple
1137     points including within a library)
1138
1139     ``start`` allows overriding the starting index (the index before the first
1140     item is returned).
1141
1142     On each iteration (call to :meth:`~.next`), increases its :attr:`~.index`
1143     by one.
1144
1145     .. attribute:: index
1146
1147         ``int``, index of the last yielded element in the stream. If the stream
1148         has ended, will give an index 1-past the stream
1149     """
1150     def __init__(self, stream, start=-1):
1151         self.stream = iter(stream)
1152         self.index = start
1153         self.stopped = False
1154     def __iter__(self):
1155         return self
1156     def next(self):
1157         if self.stopped: raise StopIteration()
1158         self.index += 1
1159         val = next(self.stream, _ph)
1160         if val is _ph:
1161             self.stopped = True
1162             raise StopIteration()
1163         return val
1164
1165 def stripped_sys_argv(*strip_args):
1166     """Return sys.argv with some arguments stripped, suitable for reexecution or subprocesses"""
1167     strip_args = sorted(set(strip_args) | set(['-s', '--save', '-u', '--update', '-i', '--init']))
1168     assert all(config.parser.has_option(s) for s in strip_args)
1169     takes_value = dict((s, config.parser.get_option(s).takes_value()) for s in strip_args)
1170
1171     longs, shorts = list(tuple(y) for _, y in groupby(strip_args, lambda x: x.startswith('--')))
1172     longs_eq = tuple(l + '=' for l in longs if takes_value[l])
1173
1174     args = sys.argv[:]
1175
1176     def strip(args, i):
1177         return args[i].startswith(shorts) \
1178             or args[i].startswith(longs_eq) or (args[i] in longs) \
1179             or (i >= 1 and (args[i - 1] in strip_args) and takes_value[args[i - 1]])
1180
1181     return [x for i, x in enumerate(args) if not strip(args, i)]
1182
1183
1184 class ConstantMapping(Mapping):
1185     """
1186     An immutable mapping returning the provided value for every single key.
1187
1188     Useful for default value to methods
1189     """
1190     __slots__ = ['_value']
1191     def __init__(self, val):
1192         self._value = val
1193
1194     def __len__(self):
1195         """
1196         defaultdict updates its length for each individually requested key, is
1197         that really useful?
1198         """
1199         return 0
1200
1201     def __iter__(self):
1202         """
1203         same as len, defaultdict udpates its iterable keyset with each key
1204         requested, is there a point for this?
1205         """
1206         return iter([])
1207
1208     def __getitem__(self, item):
1209         return self._value
1210
1211
1212 def dumpstacks(sig=None, frame=None):
1213     """ Signal handler: dump a stack trace for each existing thread."""
1214     code = []
1215
1216     def extract_stack(stack):
1217         for filename, lineno, name, line in traceback.extract_stack(stack):
1218             yield 'File: "%s", line %d, in %s' % (filename, lineno, name)
1219             if line:
1220                 yield "  %s" % (line.strip(),)
1221
1222     # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696
1223     # modified for python 2.5 compatibility
1224     threads_info = dict([(th.ident, {'name': th.name, 'uid': getattr(th, 'uid', 'n/a')})
1225                         for th in threading.enumerate()])
1226     for threadId, stack in sys._current_frames().items():
1227         thread_info = threads_info.get(threadId)
1228         code.append("\n# Thread: %s (id:%s) (uid:%s)" %
1229                     (thread_info and thread_info['name'] or 'n/a',
1230                      threadId,
1231                      thread_info and thread_info['uid'] or 'n/a'))
1232         for line in extract_stack(stack):
1233             code.append(line)
1234
1235     if openerp.evented:
1236         # code from http://stackoverflow.com/questions/12510648/in-gevent-how-can-i-dump-stack-traces-of-all-running-greenlets
1237         import gc
1238         from greenlet import greenlet
1239         for ob in gc.get_objects():
1240             if not isinstance(ob, greenlet) or not ob:
1241                 continue
1242             code.append("\n# Greenlet: %r" % (ob,))
1243             for line in extract_stack(ob.gr_frame):
1244                 code.append(line)
1245
1246     _logger.info("\n".join(code))
1247
1248 class frozendict(dict):
1249     """ An implementation of an immutable dictionary. """
1250     def __delitem__(self, key):
1251         raise NotImplementedError("'__delitem__' not supported on frozendict")
1252     def __setitem__(self, key, val):
1253         raise NotImplementedError("'__setitem__' not supported on frozendict")
1254     def clear(self):
1255         raise NotImplementedError("'clear' not supported on frozendict")
1256     def pop(self, key, default=None):
1257         raise NotImplementedError("'pop' not supported on frozendict")
1258     def popitem(self):
1259         raise NotImplementedError("'popitem' not supported on frozendict")
1260     def setdefault(self, key, default=None):
1261         raise NotImplementedError("'setdefault' not supported on frozendict")
1262     def update(self, *args, **kwargs):
1263         raise NotImplementedError("'update' not supported on frozendict")
1264
1265 @contextmanager
1266 def ignore(*exc):
1267     try:
1268         yield
1269     except exc:
1270         pass
1271
1272 # Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9
1273 if parse_version(getattr(werkzeug, '__version__', '0.0')) < parse_version('0.9.0'):
1274     def html_escape(text):
1275         return werkzeug.utils.escape(text, quote=True)
1276 else:
1277     def html_escape(text):
1278         return werkzeug.utils.escape(text)
1279
1280 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: