wui: use standard time format (#1077)
authorLeif Ryge <leif@synthesize.us>
Mon, 4 Jan 2016 16:00:59 +0000 (16:00 +0000)
committerDaira Hopwood <daira@jacaranda.org>
Fri, 15 Jan 2016 19:46:39 +0000 (19:46 +0000)
src/allmydata/test/test_util.py
src/allmydata/util/time_format.py
src/allmydata/web/common.py
src/allmydata/web/directory.py
src/allmydata/web/introweb.py
src/allmydata/web/root.py
src/allmydata/web/status.py

index db64bf194aa21c1d330ec78e2b06f51844063fd2..6335d6f6c55cbf84ffd30d1e277fa0aca74e6a62 100644 (file)
@@ -1006,6 +1006,21 @@ class TimeFormat(unittest.TestCase):
     def test_parse_date(self):
         self.failUnlessEqual(time_format.parse_date("2010-02-21"), 1266710400)
 
+    def test_format_time(self):
+        self.failUnlessEqual(time_format.format_time(time.gmtime(0)), '1970-01-01 00:00:00')
+        self.failUnlessEqual(time_format.format_time(time.gmtime(60)), '1970-01-01 00:01:00')
+        self.failUnlessEqual(time_format.format_time(time.gmtime(60*60)), '1970-01-01 01:00:00')
+        seconds_per_day = 60*60*24
+        leap_years_1970_to_2014_inclusive = ((2012 - 1968) // 4)
+        self.failUnlessEqual(time_format.format_time(time.gmtime(seconds_per_day*((2015 - 1970)*365+leap_years_1970_to_2014_inclusive))), '2015-01-01 00:00:00')
+
+    def test_format_time_y2038(self):
+        seconds_per_day = 60*60*24
+        leap_years_1970_to_2047_inclusive = ((2044 - 1968) // 4)
+        self.failUnlessEqual(time_format.format_time(time.gmtime(seconds_per_day*((2048 - 1970)*365+leap_years_1970_to_2047_inclusive))), '2048-01-01 00:00:00')
+
+    test_format_time_y2038.todo = "one day we'll move beyond 32-bit time"
+
 class CacheDir(unittest.TestCase):
     def test_basic(self):
         basedir = "test_util/CacheDir/test_basic"
index 0f8f2f387dd89735e25ae208e8e6064851fd6878..c481adc37fe734ed6f1896732ee57d71e3b2e049 100644 (file)
@@ -3,6 +3,9 @@
 
 import calendar, datetime, re, time
 
+def format_time(t):
+    return time.strftime("%Y-%m-%d %H:%M:%S", t)
+
 def iso_utc_date(now=None, t=time.time):
     if now is None:
         now = t()
@@ -13,11 +16,6 @@ def iso_utc(now=None, sep='_', t=time.time):
         now = t()
     return datetime.datetime.utcfromtimestamp(now).isoformat(sep)
 
-def iso_local(now=None, sep='_', t=time.time):
-    if now is None:
-        now = t()
-    return datetime.datetime.fromtimestamp(now).isoformat(sep)
-
 def iso_utc_time_to_seconds(isotime, _conversion_re=re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})[T_ ](?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(?P<subsecond>\.\d+)?")):
     """
     The inverse of iso_utc().
index 52fed6a04be4c0002a52acc7e546380b5abfe5d4..b8e6d5b4490395cd4fb35e0fc7756691dc86e2e7 100644 (file)
@@ -1,5 +1,7 @@
 
+import time
 import simplejson
+
 from twisted.web import http, server
 from twisted.python import log
 from zope.interface import Interface
@@ -13,11 +15,10 @@ from allmydata.interfaces import ExistingChildError, NoSuchChildError, \
      MustBeReadonlyError, MustNotBeUnknownRWError, SDMF_VERSION, MDMF_VERSION
 from allmydata.mutable.common import UnrecoverableFileError
 from allmydata.util import abbreviate
+from allmydata.util.time_format import format_time
 from allmydata.util.encodingutil import to_str, quote_output
 
 
-TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-
 def get_filenode_metadata(filenode):
     metadata = {'mutable': filenode.is_mutable()}
     if metadata['mutable']:
@@ -209,6 +210,12 @@ def text_plain(text, ctx):
     req.setHeader("content-length", b"%d" % len(text))
     return text
 
+def spaces_to_nbsp(text):
+    return unicode(text).replace(u' ', u'\u00A0')
+
+def render_time(t):
+    return spaces_to_nbsp(format_time(time.localtime(t)))
+
 class WebError(Exception):
     def __init__(self, text, code=http.BAD_REQUEST):
         self.text = text
index d82bf643600751957a97e390bc5509970dd8f9b8..29b57620ba2048aa8d3ad55f7802b1878cc0492c 100644 (file)
@@ -12,7 +12,7 @@ from nevow.inevow import IRequest
 
 from foolscap.api import fireEventually
 
-from allmydata.util import base32, time_format
+from allmydata.util import base32
 from allmydata.util.encodingutil import to_str
 from allmydata.uri import from_string_dirnode
 from allmydata.interfaces import IDirectoryNode, IFileNode, IFilesystemNode, \
@@ -26,7 +26,7 @@ from allmydata.web.common import text_plain, WebError, \
      boolean_of_arg, get_arg, get_root, parse_replace_arg, \
      should_create_intermediate_directories, \
      getxmlfile, RenderMixin, humanize_failure, convert_children_json, \
-     get_format, get_mutable_type, get_filenode_metadata
+     get_format, get_mutable_type, get_filenode_metadata, render_time
 from allmydata.web.filenode import ReplaceMeMixin, \
      FileNodeHandler, PlaceHolderNodeHandler
 from allmydata.web.check_results import CheckResultsRenderer, \
@@ -702,21 +702,21 @@ class DirectoryAsHTML(rend.Page):
         times = []
         linkcrtime = metadata.get('tahoe', {}).get("linkcrtime")
         if linkcrtime is not None:
-            times.append("lcr: " + time_format.iso_local(linkcrtime))
+            times.append("lcr: " + render_time(linkcrtime))
         else:
             # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
             if "ctime" in metadata:
-                ctime = time_format.iso_local(metadata["ctime"])
+                ctime = render_time(metadata["ctime"])
                 times.append("c: " + ctime)
         linkmotime = metadata.get('tahoe', {}).get("linkmotime")
         if linkmotime is not None:
             if times:
                 times.append(T.br())
-            times.append("lmo: " + time_format.iso_local(linkmotime))
+            times.append("lmo: " + render_time(linkmotime))
         else:
             # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
             if "mtime" in metadata:
-                mtime = time_format.iso_local(metadata["mtime"])
+                mtime = render_time(metadata["mtime"])
                 if times:
                     times.append(T.br())
                 times.append("m: " + mtime)
index 6287af63131351a15bcbf3ee1fd118c9b18aafd8..2cfe6a55fb58064b0a8d7668f14a94886db6650e 100644 (file)
@@ -7,7 +7,7 @@ import allmydata
 import simplejson
 from allmydata import get_package_versions_string
 from allmydata.util import idlib
-from allmydata.web.common import getxmlfile, get_arg, TIME_FORMAT
+from allmydata.web.common import getxmlfile, get_arg, render_time
 
 
 class IntroducerRoot(rend.Page):
@@ -53,7 +53,7 @@ class IntroducerRoot(rend.Page):
 
     # FIXME: This code is duplicated in root.py and introweb.py.
     def data_rendered_at(self, ctx, data):
-        return time.strftime(TIME_FORMAT, time.localtime())
+        return render_time(time.time())
     def data_version(self, ctx, data):
         return get_package_versions_string()
     def data_import_path(self, ctx, data):
@@ -92,7 +92,7 @@ class IntroducerRoot(rend.Page):
         ctx.fillSlots("connection-hints",
                       "connection hints: " + " ".join(ad.connection_hints))
         ctx.fillSlots("connected", "?")
-        when_s = time.strftime("%H:%M:%S %d-%b-%Y", time.localtime(ad.when))
+        when_s = render_time(ad.when)
         ctx.fillSlots("announced", when_s)
         ctx.fillSlots("version", ad.version)
         ctx.fillSlots("service_name", ad.service_name)
@@ -105,7 +105,7 @@ class IntroducerRoot(rend.Page):
         ctx.fillSlots("nickname", s.nickname)
         ctx.fillSlots("tubid", s.tubid)
         ctx.fillSlots("connected", s.remote_address)
-        since_s = time.strftime("%H:%M:%S %d-%b-%Y", time.localtime(s.when))
+        since_s = render_time(s.when)
         ctx.fillSlots("since", since_s)
         ctx.fillSlots("version", s.version)
         ctx.fillSlots("service_name", s.service_name)
index 8a9969c610f43a18097a8e904a3a19e35afa99d2..c46a6dd5f8bc5fb9c49f8199cc74d1a742bb47a8 100644 (file)
@@ -14,7 +14,7 @@ from allmydata.interfaces import IFileNode
 from allmydata.web import filenode, directory, unlinked, status, operations
 from allmydata.web import storage
 from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \
-     get_arg, RenderMixin, get_format, get_mutable_type, TIME_FORMAT
+     get_arg, RenderMixin, get_format, get_mutable_type, render_time
 
 
 class URIHandler(RenderMixin, rend.Page):
@@ -171,7 +171,7 @@ class Root(rend.Page):
 
     # FIXME: This code is duplicated in root.py and introweb.py.
     def data_rendered_at(self, ctx, data):
-        return time.strftime(TIME_FORMAT, time.localtime())
+        return render_time(time.time())
     def data_version(self, ctx, data):
         return get_package_versions_string()
     def data_import_path(self, ctx, data):
@@ -309,10 +309,8 @@ class Root(rend.Page):
         ctx.fillSlots("connected", connected)
         ctx.fillSlots("connected_alt", self._connectedalts[connected])
         ctx.fillSlots("connected-bool", bool(rhost))
-        ctx.fillSlots("since", time.strftime(TIME_FORMAT,
-                                             time.localtime(since)))
-        ctx.fillSlots("announced", time.strftime(TIME_FORMAT,
-                                                 time.localtime(announced)))
+        ctx.fillSlots("since", render_time(since))
+        ctx.fillSlots("announced", render_time(announced))
         ctx.fillSlots("version", version)
         ctx.fillSlots("service_name", service_name)
         ctx.fillSlots("available_space", available_space)
index b363f857c50e04327b1345d24406bd7af685d8ca..7ab3efe27f6cc04b2830b45f6c7f335078c2578f 100644 (file)
@@ -5,7 +5,7 @@ from twisted.internet import defer
 from nevow import rend, inevow, tags as T
 from allmydata.util import base32, idlib
 from allmydata.web.common import getxmlfile, get_arg, \
-     abbreviate_time, abbreviate_rate, abbreviate_size, plural, compute_rate
+     abbreviate_time, abbreviate_rate, abbreviate_size, plural, compute_rate, render_time
 from allmydata.interfaces import IUploadStatus, IDownloadStatus, \
      IPublishStatus, IRetrieveStatus, IServermapUpdaterStatus
 
@@ -162,9 +162,7 @@ class UploadStatusPage(UploadResultsRendererMixin, rend.Page):
         return d
 
     def render_started(self, ctx, data):
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        started_s = time.strftime(TIME_FORMAT,
-                                  time.localtime(data.get_started()))
+        started_s = render_time(data.get_started())
         return started_s
 
     def render_si(self, ctx, data):
@@ -614,9 +612,7 @@ class DownloadStatusPage(DownloadResultsRendererMixin, rend.Page):
         return d
 
     def render_started(self, ctx, data):
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        started_s = time.strftime(TIME_FORMAT,
-                                  time.localtime(data.get_started()))
+        started_s = render_time(data.get_started())
         return started_s + " (%s)" % data.get_started()
 
     def render_si(self, ctx, data):
@@ -647,9 +643,7 @@ class DownloadStatusTimelinePage(rend.Page):
     docFactory = getxmlfile("download-status-timeline.xhtml")
 
     def render_started(self, ctx, data):
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        started_s = time.strftime(TIME_FORMAT,
-                                  time.localtime(data.get_started()))
+        started_s = render_time(data.get_started())
         return started_s + " (%s)" % data.get_started()
 
     def render_si(self, ctx, data):
@@ -684,9 +678,7 @@ class RetrieveStatusPage(rend.Page, RateAndTimeMixin):
         self.retrieve_status = data
 
     def render_started(self, ctx, data):
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        started_s = time.strftime(TIME_FORMAT,
-                                  time.localtime(data.get_started()))
+        started_s = render_time(data.get_started())
         return started_s
 
     def render_si(self, ctx, data):
@@ -772,9 +764,7 @@ class PublishStatusPage(rend.Page, RateAndTimeMixin):
         self.publish_status = data
 
     def render_started(self, ctx, data):
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        started_s = time.strftime(TIME_FORMAT,
-                                  time.localtime(data.get_started()))
+        started_s = render_time(data.get_started())
         return started_s
 
     def render_si(self, ctx, data):
@@ -883,18 +873,14 @@ class MapupdateStatusPage(rend.Page, RateAndTimeMixin):
         self.update_status = data
 
     def render_started(self, ctx, data):
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        started_s = time.strftime(TIME_FORMAT,
-                                  time.localtime(data.get_started()))
+        started_s = render_time(data.get_started())
         return started_s
 
     def render_finished(self, ctx, data):
         when = data.get_finished()
         if not when:
             return "not yet"
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        started_s = time.strftime(TIME_FORMAT,
-                                  time.localtime(data.get_finished()))
+        started_s = render_time(data.get_finished())
         return started_s
 
     def render_si(self, ctx, data):
@@ -1110,9 +1096,7 @@ class Status(rend.Page):
     def render_row(self, ctx, data):
         s = data
 
-        TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
-        started_s = time.strftime(TIME_FORMAT,
-                                  time.localtime(s.get_started()))
+        started_s = render_time(s.get_started())
         ctx.fillSlots("started", started_s)
 
         si_s = base32.b2a_or_none(s.get_storage_index())