~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Robert Collins
  • Date: 2012-02-02 07:42:24 UTC
  • Revision ID: robertc@robertcollins.net-20120202074224-ujea2ocm1u1ws1en
    - Make tz calculations consistent and use UTC in the UI everywhere we show
      a precise timestamp. (Robert Collins, #594591)

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
20
#
21
21
 
22
 
try:
23
 
    from xml.etree import ElementTree as ET
24
 
except ImportError:
25
 
    from elementtree import ElementTree as ET
26
 
 
27
 
from simpletal.simpleTALUtils import HTMLStructureCleaner
28
 
 
29
22
import base64
30
 
import cgi
31
23
import datetime
32
24
import logging
33
25
import re
38
30
import os
39
31
import subprocess
40
32
 
 
33
try:
 
34
    from xml.etree import ElementTree as ET
 
35
except ImportError:
 
36
    from elementtree import ElementTree as ET
 
37
 
 
38
from bzrlib import urlutils
 
39
 
 
40
from simpletal.simpleTALUtils import HTMLStructureCleaner
 
41
 
41
42
log = logging.getLogger("loggerhead.controllers")
42
43
 
43
44
 
51
52
# Display of times.
52
53
 
53
54
# date_day -- just the day
54
 
# date_time -- full date with time
 
55
# date_time -- full date with time (UTC)
55
56
#
56
 
# displaydate -- for use in sentences
57
57
# approximatedate -- for use in tables
58
58
#
59
 
# displaydate and approximatedate return an elementtree <span> Element
60
 
# with the full date in a tooltip.
 
59
# approximatedate return an elementtree <span> Element
 
60
# with the full date (UTC) in a tooltip.
61
61
 
62
62
 
63
63
def date_day(value):
66
66
 
67
67
def date_time(value):
68
68
    if value is not None:
69
 
        return value.strftime('%Y-%m-%d %T')
 
69
        # Note: this assumes that the value is UTC in some fashion.
 
70
        return value.strftime('%Y-%m-%d %H:%M:%S UTC')
70
71
    else:
71
72
        return 'N/A'
72
73
 
73
74
 
74
 
def _displaydate(date):
75
 
    delta = abs(datetime.datetime.now() - date)
76
 
    if delta > datetime.timedelta(1, 0, 0):
77
 
        # far in the past or future, display the date
78
 
        return 'on ' + date_day(date)
79
 
    return _approximatedate(date)
80
 
 
81
 
 
82
75
def _approximatedate(date):
83
76
    delta = datetime.datetime.now() - date
84
77
    if abs(delta) > datetime.timedelta(1, 0, 0):
125
118
    return _wrap_with_date_time_title(date, _approximatedate(date))
126
119
 
127
120
 
128
 
def displaydate(date):
129
 
    return _wrap_with_date_time_title(date, _displaydate(date))
130
 
 
131
 
 
132
 
class Container (object):
 
121
class Container(object):
133
122
    """
134
123
    Convert a dict into an object with attributes.
135
124
    """
136
125
 
137
126
    def __init__(self, _dict=None, **kw):
 
127
        self._properties = {}
138
128
        if _dict is not None:
139
129
            for key, value in _dict.iteritems():
140
130
                setattr(self, key, value)
151
141
        out += '}'
152
142
        return out
153
143
 
 
144
    def __getattr__(self, attr):
 
145
        """Used for handling things that aren't already available."""
 
146
        if attr.startswith('_') or attr not in self._properties:
 
147
            raise AttributeError('No attribute: %s' % (attr,))
 
148
        val = self._properties[attr](self, attr)
 
149
        setattr(self, attr, val)
 
150
        return val
 
151
 
 
152
    def _set_property(self, attr, prop_func):
 
153
        """Set a function that will be called when an attribute is desired.
 
154
 
 
155
        We will cache the return value, so the function call should be
 
156
        idempotent. We will pass 'self' and the 'attr' name when triggered.
 
157
        """
 
158
        if attr.startswith('_'):
 
159
            raise ValueError("Cannot create properties that start with _")
 
160
        self._properties[attr] = prop_func
 
161
 
154
162
 
155
163
def trunc(text, limit=10):
156
164
    if len(text) <= limit:
193
201
# only do this if unicode turns out to be a problem
194
202
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
195
203
 
 
204
# Can't be a dict; &amp; needs to be done first.
 
205
html_entity_subs = [
 
206
    ("&", "&amp;"),
 
207
    ('"', "&quot;"),
 
208
    ("'", "&#39;"), # &apos; is defined in XML, but not HTML.
 
209
    (">", "&gt;"),
 
210
    ("<", "&lt;"),
 
211
    ]
 
212
 
 
213
 
 
214
def html_escape(s):
 
215
    """Transform dangerous (X)HTML characters into entities.
 
216
 
 
217
    Like cgi.escape, except also escaping \" and '. This makes it safe to use
 
218
    in both attribute and element content.
 
219
 
 
220
    If you want to safely fill a format string with escaped values, use
 
221
    html_format instead
 
222
    """
 
223
    for char, repl in html_entity_subs:
 
224
        s = s.replace(char, repl)
 
225
    return s
 
226
 
 
227
 
 
228
def html_format(template, *args):
 
229
    """Safely format an HTML template string, escaping the arguments.
 
230
 
 
231
    The template string must not be user-controlled; it will not be escaped.
 
232
    """
 
233
    return template % tuple(html_escape(arg) for arg in args)
 
234
 
 
235
 
196
236
# FIXME: get rid of this method; use fixed_width() and avoid XML().
197
237
 
198
 
 
199
238
def html_clean(s):
200
239
    """
201
240
    clean up a string for html display.  expand any tabs, encode any html
202
241
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
203
242
    in displaying monospace text.
204
243
    """
205
 
    s = cgi.escape(s.expandtabs())
 
244
    s = html_escape(s.expandtabs())
206
245
    s = s.replace(' ', '&nbsp;')
207
246
    return s
208
247
 
217
256
 
218
257
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
219
258
    """
220
 
 
221
 
 
222
259
    if s is None:
223
260
        return '&nbsp;'
224
261
    elif isinstance(s, int):
250
287
        except UnicodeDecodeError:
251
288
            s = s.decode('iso-8859-15')
252
289
 
253
 
    s = cgi.escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
 
290
    s = html_escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
254
291
 
255
292
    return HSC.clean(s).replace('\n', '<br/>')
256
293
 
316
353
 
317
354
    out = str(base)
318
355
    if (base < 100) and (dot != 0):
319
 
        out += '.%d' % (dot)
 
356
        out += '.%d' % (dot,)
320
357
    if divisor == KILO:
321
358
        out += 'K'
322
359
    elif divisor == MEG:
326
363
    return out
327
364
 
328
365
 
 
366
def local_path_from_url(url):
 
367
    """Convert Bazaar URL to local path, ignoring readonly+ prefix"""
 
368
    readonly_prefix = 'readonly+'
 
369
    if url.startswith(readonly_prefix):
 
370
        url = url[len(readonly_prefix):]
 
371
    return urlutils.local_path_from_url(url)
 
372
 
 
373
 
329
374
def fill_in_navigation(navigation):
330
375
    """
331
376
    given a navigation block (used by the template for the page header), fill
427
472
    for index, dir_name in enumerate(dir_parts):
428
473
        inner_breadcrumbs.append({
429
474
            'dir_name': dir_name,
430
 
            'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
 
475
            'path': '/'.join(dir_parts[:index + 1]),
431
476
            'suffix': '/' + view,
432
477
        })
433
478
    return inner_breadcrumbs
463
508
        now = time.time()
464
509
        msec = int(now * 1000) % 1000
465
510
        timestr = time.strftime('%Y%m%d%H%M%S',
466
 
                                time.localtime(now)) + ('%03d' % msec)
 
511
                                time.localtime(now)) + ('%03d' % (msec,))
467
512
        filename = f.__name__ + '-' + timestr + '.lsprof'
468
513
        cPickle.dump(stats, open(filename, 'w'), 2)
469
514
        return ret
498
543
#         for re-ordering an existing page by different sort
499
544
 
500
545
t_context = threading.local()
501
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
502
 
          'compare_revid', 'sort')
 
546
_valid = (
 
547
    'start_revid', 'filter_file_id', 'q', 'remember', 'compare_revid', 'sort')
503
548
 
504
549
 
505
550
def set_context(map):
535
580
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
536
581
 
537
582
    @classmethod
538
 
    def _turn_sigterm_into_systemexit(self):
 
583
    def _turn_sigterm_into_systemexit(cls):
539
584
        """
540
585
        Attempts to turn a SIGTERM exception into a SystemExit exception.
541
586
        """
549
594
        signal.signal(signal.SIGTERM, handle_term)
550
595
 
551
596
    @classmethod
552
 
    def is_installed(self):
553
 
        return os.environ.get(self._reloader_environ_key)
 
597
    def is_installed(cls):
 
598
        return os.environ.get(cls._reloader_environ_key)
554
599
 
555
600
    @classmethod
556
 
    def install(self):
 
601
    def install(cls):
557
602
        from paste import reloader
558
603
        reloader.install(int(1))
559
604
 
560
605
    @classmethod
561
 
    def restart_with_reloader(self):
 
606
    def restart_with_reloader(cls):
562
607
        """Based on restart_with_monitor from paste.script.serve."""
563
608
        print 'Starting subprocess with file monitor'
564
 
        while 1:
 
609
        while True:
565
610
            args = [sys.executable] + sys.argv
566
611
            new_environ = os.environ.copy()
567
 
            new_environ[self._reloader_environ_key] = 'true'
 
612
            new_environ[cls._reloader_environ_key] = 'true'
568
613
            proc = None
569
614
            try:
570
615
                try:
571
 
                    self._turn_sigterm_into_systemexit()
 
616
                    cls._turn_sigterm_into_systemexit()
572
617
                    proc = subprocess.Popen(args, env=new_environ)
573
618
                    exit_code = proc.wait()
574
619
                    proc = None
577
622
                    return 1
578
623
            finally:
579
624
                if (proc is not None
580
 
                    and hasattr(os, 'kill')):
 
625
                    and getattr(os, 'kill', None) is not None):
581
626
                    import signal
582
627
                    try:
583
628
                        os.kill(proc.pid, signal.SIGTERM)
589
634
            if exit_code != 3:
590
635
                return exit_code
591
636
            print '-'*20, 'Restarting', '-'*20
 
637
 
 
638
 
 
639
def convert_file_errors(application):
 
640
    """WSGI wrapper to convert some file errors to Paste exceptions"""
 
641
    def new_application(environ, start_response):
 
642
        try:
 
643
            return application(environ, start_response)
 
644
        except (IOError, OSError), e:
 
645
            import errno
 
646
            from paste import httpexceptions
 
647
            if e.errno == errno.ENOENT:
 
648
                raise httpexceptions.HTTPNotFound()
 
649
            elif e.errno == errno.EACCES:
 
650
                raise httpexceptions.HTTPForbidden()
 
651
            else:
 
652
                raise
 
653
    return new_application
 
654
 
 
655
 
 
656
def convert_to_json_ready(obj):
 
657
    if isinstance(obj, Container):
 
658
        d = obj.__dict__.copy()
 
659
        del d['_properties']
 
660
        return d
 
661
    elif isinstance(obj, datetime.datetime):
 
662
        return tuple(obj.utctimetuple())
 
663
    raise TypeError(repr(obj) + " is not JSON serializable")