utils.py
branchstable
changeset 8124 acc23c284432
parent 7998 9ef285eb20f4
child 8682 20bd1cdf86ae
equal deleted inserted replaced
8118:7b2c7f3d3703 8124:acc23c284432
    15 #
    15 #
    16 # You should have received a copy of the GNU Lesser General Public License along
    16 # You should have received a copy of the GNU Lesser General Public License along
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """Some utilities for CubicWeb server/clients."""
    18 """Some utilities for CubicWeb server/clients."""
    19 
    19 
       
    20 from __future__ import division, with_statement
       
    21 
    20 __docformat__ = "restructuredtext en"
    22 __docformat__ = "restructuredtext en"
    21 
    23 
    22 import os
       
    23 import sys
    24 import sys
    24 import decimal
    25 import decimal
    25 import datetime
    26 import datetime
    26 import random
    27 import random
       
    28 import re
       
    29 
       
    30 from operator import itemgetter
    27 from inspect import getargspec
    31 from inspect import getargspec
    28 from itertools import repeat
    32 from itertools import repeat
    29 from uuid import uuid4
    33 from uuid import uuid4
    30 from warnings import warn
    34 from warnings import warn
       
    35 from threading import Lock
       
    36 
       
    37 from logging import getLogger
    31 
    38 
    32 from logilab.mtconverter import xml_escape
    39 from logilab.mtconverter import xml_escape
    33 from logilab.common.deprecation import deprecated
    40 from logilab.common.deprecation import deprecated
    34 
    41 
    35 _MARKER = object()
    42 _MARKER = object()
   225     # content-types (xml/html) and all possible browsers is very
   232     # content-types (xml/html) and all possible browsers is very
   226     # tricky, see http://www.hixie.ch/advocacy/xhtml for an in-depth discussion
   233     # tricky, see http://www.hixie.ch/advocacy/xhtml for an in-depth discussion
   227     xhtml_safe_script_opening = u'<script type="text/javascript"><!--//--><![CDATA[//><!--\n'
   234     xhtml_safe_script_opening = u'<script type="text/javascript"><!--//--><![CDATA[//><!--\n'
   228     xhtml_safe_script_closing = u'\n//--><!]]></script>'
   235     xhtml_safe_script_closing = u'\n//--><!]]></script>'
   229 
   236 
   230     def __init__(self, datadir_url=None):
   237     def __init__(self, req):
   231         super(HTMLHead, self).__init__()
   238         super(HTMLHead, self).__init__()
   232         self.jsvars = []
   239         self.jsvars = []
   233         self.jsfiles = []
   240         self.jsfiles = []
   234         self.cssfiles = []
   241         self.cssfiles = []
   235         self.ie_cssfiles = []
   242         self.ie_cssfiles = []
   236         self.post_inlined_scripts = []
   243         self.post_inlined_scripts = []
   237         self.pagedata_unload = False
   244         self.pagedata_unload = False
   238         self.datadir_url = datadir_url
   245         self._cw = req
   239 
   246         self.datadir_url = req.datadir_url
   240 
   247 
   241     def add_raw(self, rawheader):
   248     def add_raw(self, rawheader):
   242         self.write(rawheader)
   249         self.write(rawheader)
   243 
   250 
   244     def define_var(self, var, value, override=True):
   251     def define_var(self, var, value, override=True):
   346                     vardecl = (u'if (typeof %s == "undefined") {%s}' %
   353                     vardecl = (u'if (typeof %s == "undefined") {%s}' %
   347                                (var, vardecl))
   354                                (var, vardecl))
   348                 w(vardecl + u'\n')
   355                 w(vardecl + u'\n')
   349             w(self.xhtml_safe_script_closing)
   356             w(self.xhtml_safe_script_closing)
   350         # 2/ css files
   357         # 2/ css files
   351         for cssfile, media in (self.group_urls(self.cssfiles) if self.datadir_url else self.cssfiles):
   358         ie_cssfiles = ((x, (y, z)) for x, y, z in self.ie_cssfiles)
       
   359         if self.datadir_url and self._cw.vreg.config['concat-resources']:
       
   360             cssfiles = self.group_urls(self.cssfiles)
       
   361             ie_cssfiles = self.group_urls(ie_cssfiles)
       
   362             jsfiles = (x for x, _ in self.group_urls((x, None) for x in self.jsfiles))
       
   363         else:
       
   364             cssfiles = self.cssfiles
       
   365             jsfiles = self.jsfiles
       
   366         for cssfile, media in cssfiles:
   352             w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
   367             w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
   353               (media, xml_escape(cssfile)))
   368               (media, xml_escape(cssfile)))
   354         # 3/ ie css if necessary
   369         # 3/ ie css if necessary
   355         if self.ie_cssfiles:
   370         if self.ie_cssfiles: # use self.ie_cssfiles because `ie_cssfiles` is a genexp
   356             ie_cssfiles = ((x, (y, z)) for x, y, z in self.ie_cssfiles)
   371             for cssfile, (media, iespec) in ie_cssfiles:
   357             for cssfile, (media, iespec) in (self.group_urls(ie_cssfiles) if self.datadir_url else ie_cssfiles):
       
   358                 w(u'<!--%s>\n' % iespec)
   372                 w(u'<!--%s>\n' % iespec)
   359                 w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
   373                 w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
   360                   (media, xml_escape(cssfile)))
   374                   (media, xml_escape(cssfile)))
   361             w(u'<![endif]--> \n')
   375             w(u'<![endif]--> \n')
   362         # 4/ js files
   376         # 4/ js files
   363         jsfiles = ((x, None) for x in self.jsfiles)
   377         for jsfile in jsfiles:
   364         for jsfile, media in self.group_urls(jsfiles) if self.datadir_url else jsfiles:
       
   365             if skiphead:
   378             if skiphead:
   366                 # Don't insert <script> tags directly as they would be
   379                 # Don't insert <script> tags directly as they would be
   367                 # interpreted directly by some browsers (e.g. IE).
   380                 # interpreted directly by some browsers (e.g. IE).
   368                 # Use <pre class="script"> tags instead and let
   381                 # Use <pre class="script"> tags instead and let
   369                 # `loadAjaxHtmlHead` handle the script insertion / execution.
   382                 # `loadAjaxHtmlHead` handle the script insertion / execution.
   471 
   484 
   472     class CubicWebJsonEncoder(json.JSONEncoder):
   485     class CubicWebJsonEncoder(json.JSONEncoder):
   473         """define a json encoder to be able to encode yams std types"""
   486         """define a json encoder to be able to encode yams std types"""
   474 
   487 
   475         def default(self, obj):
   488         def default(self, obj):
   476             if hasattr(obj, 'eid'):
   489             if hasattr(obj, '__json_encode__'):
   477                 d = obj.cw_attr_cache.copy()
   490                 return obj.__json_encode__()
   478                 d['eid'] = obj.eid
       
   479                 return d
       
   480             if isinstance(obj, datetime.datetime):
   491             if isinstance(obj, datetime.datetime):
   481                 return ustrftime(obj, '%Y/%m/%d %H:%M:%S')
   492                 return ustrftime(obj, '%Y/%m/%d %H:%M:%S')
   482             elif isinstance(obj, datetime.date):
   493             elif isinstance(obj, datetime.date):
   483                 return ustrftime(obj, '%Y/%m/%d')
   494                 return ustrftime(obj, '%Y/%m/%d')
   484             elif isinstance(obj, datetime.time):
   495             elif isinstance(obj, datetime.time):
   492             except TypeError:
   503             except TypeError:
   493                 # we never ever want to fail because of an unknown type,
   504                 # we never ever want to fail because of an unknown type,
   494                 # just return None in those cases.
   505                 # just return None in those cases.
   495                 return None
   506                 return None
   496 
   507 
   497     def json_dumps(value):
   508     def json_dumps(value, **kwargs):
   498         return json.dumps(value, cls=CubicWebJsonEncoder)
   509         return json.dumps(value, cls=CubicWebJsonEncoder, **kwargs)
   499 
   510 
   500 
   511 
   501     class JSString(str):
   512     class JSString(str):
   502         """use this string sub class in values given to :func:`js_dumps` to
   513         """use this string sub class in values given to :func:`js_dumps` to
   503         insert raw javascript chain in some JSON string
   514         insert raw javascript chain in some JSON string
   529             return _list2js(something, predictable)
   540             return _list2js(something, predictable)
   530         if isinstance(something, JSString):
   541         if isinstance(something, JSString):
   531             return something
   542             return something
   532         return json_dumps(something)
   543         return json_dumps(something)
   533 
   544 
       
   545 PERCENT_IN_URLQUOTE_RE = re.compile(r'%(?=[0-9a-fA-F]{2})')
       
   546 def js_href(javascript_code):
       
   547     """Generate a "javascript: ..." string for an href attribute.
       
   548 
       
   549     Some % which may be interpreted in a href context will be escaped.
       
   550 
       
   551     In an href attribute, url-quotes-looking fragments are interpreted before
       
   552     being given to the javascript engine. Valid url quotes are in the form
       
   553     ``%xx`` with xx being a byte in hexadecimal form. This means that ``%toto``
       
   554     will be unaltered but ``%babar`` will be mangled because ``ba`` is the
       
   555     hexadecimal representation of 186.
       
   556 
       
   557     >>> js_href('alert("babar");')
       
   558     'javascript: alert("babar");'
       
   559     >>> js_href('alert("%babar");')
       
   560     'javascript: alert("%25babar");'
       
   561     >>> js_href('alert("%toto %babar");')
       
   562     'javascript: alert("%toto %25babar");'
       
   563     >>> js_href('alert("%1337%");')
       
   564     'javascript: alert("%251337%");'
       
   565     """
       
   566     return 'javascript: ' + PERCENT_IN_URLQUOTE_RE.sub(r'%25', javascript_code)
       
   567 
   534 
   568 
   535 @deprecated('[3.7] merge_dicts is deprecated')
   569 @deprecated('[3.7] merge_dicts is deprecated')
   536 def merge_dicts(dict1, dict2):
   570 def merge_dicts(dict1, dict2):
   537     """update a copy of `dict1` with `dict2`"""
   571     """update a copy of `dict1` with `dict2`"""
   538     dict1 = dict(dict1)
   572     dict1 = dict(dict1)
   539     dict1.update(dict2)
   573     dict1.update(dict2)
   540     return dict1
   574     return dict1
   541 
   575 
   542 from logilab.common import date
   576 
   543 _THIS_MOD_NS = globals()
   577 logger = getLogger('cubicweb.utils')
   544 for funcname in ('date_range', 'todate', 'todatetime', 'datetime2ticks',
   578 
   545                  'days_in_month', 'days_in_year', 'previous_month',
   579 class QueryCache(object):
   546                  'next_month', 'first_day', 'last_day',
   580     """ a minimalist dict-like object to be used by the querier
   547                  'strptime'):
   581     and native source (replaces lgc.cache for this very usage)
   548     msg = '[3.6] %s has been moved to logilab.common.date' % funcname
   582 
   549     _THIS_MOD_NS[funcname] = deprecated(msg)(getattr(date, funcname))
   583     To be efficient it must be properly used. The usage patterns are
       
   584     quite specific to its current clients.
       
   585 
       
   586     The ceiling value should be sufficiently high, else it will be
       
   587     ruthlessly inefficient (there will be warnings when this happens).
       
   588     A good (high enough) value can only be set on a per-application
       
   589     value. A default, reasonnably high value is provided but tuning
       
   590     e.g `rql-cache-size` can certainly help.
       
   591 
       
   592     There are two kinds of elements to put in this cache:
       
   593     * frequently used elements
       
   594     * occasional elements
       
   595 
       
   596     The former should finish in the _permanent structure after some
       
   597     warmup.
       
   598 
       
   599     Occasional elements can be buggy requests (server-side) or
       
   600     end-user (web-ui provided) requests. These have to be cleaned up
       
   601     when they fill the cache, without evicting the usefull, frequently
       
   602     used entries.
       
   603     """
       
   604     # quite arbitrary, but we want to never
       
   605     # immortalize some use-a-little query
       
   606     _maxlevel = 15
       
   607 
       
   608     def __init__(self, ceiling=3000):
       
   609         self._max = ceiling
       
   610         # keys belonging forever to this cache
       
   611         self._permanent = set()
       
   612         # mapping of key (that can get wiped) to getitem count
       
   613         self._transient = {}
       
   614         self._data = {}
       
   615         self._lock = Lock()
       
   616 
       
   617     def __len__(self):
       
   618         with self._lock:
       
   619             return len(self._data)
       
   620 
       
   621     def __getitem__(self, k):
       
   622         with self._lock:
       
   623             if k in self._permanent:
       
   624                 return self._data[k]
       
   625             v = self._transient.get(k, _MARKER)
       
   626             if v is _MARKER:
       
   627                 self._transient[k] = 1
       
   628                 return self._data[k]
       
   629             if v > self._maxlevel:
       
   630                 self._permanent.add(k)
       
   631                 self._transient.pop(k, None)
       
   632             else:
       
   633                 self._transient[k] += 1
       
   634             return self._data[k]
       
   635 
       
   636     def __setitem__(self, k, v):
       
   637         with self._lock:
       
   638             if len(self._data) >= self._max:
       
   639                 self._try_to_make_room()
       
   640             self._data[k] = v
       
   641 
       
   642     def pop(self, key, default=_MARKER):
       
   643         with self._lock:
       
   644             try:
       
   645                 if default is _MARKER:
       
   646                     return self._data.pop(key)
       
   647                 return self._data.pop(key, default)
       
   648             finally:
       
   649                 if key in self._permanent:
       
   650                     self._permanent.remove(key)
       
   651                 else:
       
   652                     self._transient.pop(key, None)
       
   653 
       
   654     def clear(self):
       
   655         with self._lock:
       
   656             self._clear()
       
   657 
       
   658     def _clear(self):
       
   659         self._permanent = set()
       
   660         self._transient = {}
       
   661         self._data = {}
       
   662 
       
   663     def _try_to_make_room(self):
       
   664         current_size = len(self._data)
       
   665         items = sorted(self._transient.items(), key=itemgetter(1))
       
   666         level = 0
       
   667         for k, v in items:
       
   668             self._data.pop(k, None)
       
   669             self._transient.pop(k, None)
       
   670             if v > level:
       
   671                 datalen = len(self._data)
       
   672                 if datalen == 0:
       
   673                     return
       
   674                 if (current_size - datalen) / datalen > .1:
       
   675                     break
       
   676                 level = v
       
   677         else:
       
   678             # we removed cruft but everything is permanent
       
   679             if len(self._data) >= self._max:
       
   680                 logger.warning('Cache %s is full.' % id(self))
       
   681                 self._clear()
       
   682 
       
   683     def _usage_report(self):
       
   684         with self._lock:
       
   685             return {'itemcount': len(self._data),
       
   686                     'transientcount': len(self._transient),
       
   687                     'permanentcount': len(self._permanent)}
       
   688 
       
   689     def popitem(self):
       
   690         raise NotImplementedError()
       
   691 
       
   692     def setdefault(self, key, default=None):
       
   693         raise NotImplementedError()
       
   694 
       
   695     def update(self, other):
       
   696         raise NotImplementedError()