~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
384
429
    """
385
430
    # Is our root directory itself a branch?
386
431
    if is_root:
387
 
        if view == 'directory':
388
 
            directory = 'files'
389
432
        breadcrumbs = [{
390
433
            'dir_name': path,
391
434
            'path': '',
429
472
    for index, dir_name in enumerate(dir_parts):
430
473
        inner_breadcrumbs.append({
431
474
            'dir_name': dir_name,
432
 
            'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
 
475
            'path': '/'.join(dir_parts[:index + 1]),
433
476
            'suffix': '/' + view,
434
477
        })
435
478
    return inner_breadcrumbs
465
508
        now = time.time()
466
509
        msec = int(now * 1000) % 1000
467
510
        timestr = time.strftime('%Y%m%d%H%M%S',
468
 
                                time.localtime(now)) + ('%03d' % msec)
 
511
                                time.localtime(now)) + ('%03d' % (msec,))
469
512
        filename = f.__name__ + '-' + timestr + '.lsprof'
470
513
        cPickle.dump(stats, open(filename, 'w'), 2)
471
514
        return ret
500
543
#         for re-ordering an existing page by different sort
501
544
 
502
545
t_context = threading.local()
503
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
504
 
          'compare_revid', 'sort')
 
546
_valid = (
 
547
    'start_revid', 'filter_file_id', 'q', 'remember', 'compare_revid', 'sort')
505
548
 
506
549
 
507
550
def set_context(map):
537
580
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
538
581
 
539
582
    @classmethod
540
 
    def _turn_sigterm_into_systemexit(self):
 
583
    def _turn_sigterm_into_systemexit(cls):
541
584
        """
542
585
        Attempts to turn a SIGTERM exception into a SystemExit exception.
543
586
        """
551
594
        signal.signal(signal.SIGTERM, handle_term)
552
595
 
553
596
    @classmethod
554
 
    def is_installed(self):
555
 
        return os.environ.get(self._reloader_environ_key)
 
597
    def is_installed(cls):
 
598
        return os.environ.get(cls._reloader_environ_key)
556
599
 
557
600
    @classmethod
558
 
    def install(self):
 
601
    def install(cls):
559
602
        from paste import reloader
560
603
        reloader.install(int(1))
561
604
 
562
605
    @classmethod
563
 
    def restart_with_reloader(self):
 
606
    def restart_with_reloader(cls):
564
607
        """Based on restart_with_monitor from paste.script.serve."""
565
608
        print 'Starting subprocess with file monitor'
566
 
        while 1:
 
609
        while True:
567
610
            args = [sys.executable] + sys.argv
568
611
            new_environ = os.environ.copy()
569
 
            new_environ[self._reloader_environ_key] = 'true'
 
612
            new_environ[cls._reloader_environ_key] = 'true'
570
613
            proc = None
571
614
            try:
572
615
                try:
573
 
                    self._turn_sigterm_into_systemexit()
 
616
                    cls._turn_sigterm_into_systemexit()
574
617
                    proc = subprocess.Popen(args, env=new_environ)
575
618
                    exit_code = proc.wait()
576
619
                    proc = None
579
622
                    return 1
580
623
            finally:
581
624
                if (proc is not None
582
 
                    and hasattr(os, 'kill')):
 
625
                    and getattr(os, 'kill', None) is not None):
583
626
                    import signal
584
627
                    try:
585
628
                        os.kill(proc.pid, signal.SIGTERM)
591
634
            if exit_code != 3:
592
635
                return exit_code
593
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")