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): |
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() |