]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/commitdiff
Merge branch '2438.magic-folder-stable.9' of https://github.com/tahoe-lafs/tahoe... 2438.magic-folder-stable.10
authorDaira Hopwood <daira@jacaranda.org>
Sat, 30 Jan 2016 23:31:11 +0000 (23:31 +0000)
committerDaira Hopwood <daira@jacaranda.org>
Sat, 30 Jan 2016 23:31:11 +0000 (23:31 +0000)
Signed-off-by: Daira Hopwood <daira@jacaranda.org>
16 files changed:
src/allmydata/frontends/magic_folder.py
src/allmydata/interfaces.py
src/allmydata/magicfolderdb.py
src/allmydata/storage_client.py
src/allmydata/test/common_util.py
src/allmydata/test/test_util.py
src/allmydata/test/test_web.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/static/css/new-tahoe.css
src/allmydata/web/status.py
src/allmydata/web/welcome.xhtml
src/allmydata/webish.py

index 323e926f974a64b9348021306a194364dc38d774..bd226156d23b81c576b988c32d1babe63c3d3512 100644 (file)
@@ -619,10 +619,10 @@ class Downloader(QueueMixin, WriteFileMixin):
                     if 'version' not in result[1]:
                         self._log("invalid remote metadata detected")
                         continue
-                    else:
-                        if result[1]['version'] > max_version:
-                            node, metadata = result
-                            max_version = result[1]['version']
+
+                    if result[1]['version'] > max_version:
+                        node, metadata = result
+                        max_version = result[1]['version']
             return node, metadata
         collective_dirmap_d.addCallback(highest_version)
         return collective_dirmap_d
@@ -637,11 +637,12 @@ class Downloader(QueueMixin, WriteFileMixin):
 
                 file_node, metadata = listing_map[encoded_relpath_u]
                 local_version = self._get_local_latest(relpath_u)
+
                 if 'version' not in metadata:
                     self._log("invalid remote metadata detected for %r" % (relpath_u,))
                     continue
-                else:
-                    remote_version = metadata['version']
+
+                remote_version = metadata['version']
                 self._log("%r has local version %r, remote version %r" % (relpath_u, local_version, remote_version))
 
                 if local_version is None or local_version < remote_version:
index e495c759a0e7735833257cce6f1b898679936d1b..31fc614443cd8d750090b41c11f954a51b1a0924 100644 (file)
@@ -417,7 +417,6 @@ class IStorageBroker(Interface):
         public attributes::
 
           service_name: the type of service provided, like 'storage'
-          announcement_time: when we first heard about this service
           last_connect_time: when we last established a connection
           last_loss_time: when we last lost a connection
 
index fdda3f0624f6a306a225ff9ad66c32dc1c72f8f5..1729e3f860ec8a6c83bf39994f73119a485a76b8 100644 (file)
@@ -20,8 +20,8 @@ CREATE TABLE local_files
  mtime               REAL,                      -- ST_MTIME
  ctime               REAL,                      -- ST_CTIME
  version             INTEGER,
- last_uploaded_uri   VARCHAR(256),                -- URI:CHK:...
- last_downloaded_uri VARCHAR(256),                -- URI:CHK:...
+ last_uploaded_uri   VARCHAR(256),              -- URI:CHK:...
+ last_downloaded_uri VARCHAR(256),              -- URI:CHK:...
  last_downloaded_timestamp REAL
 );
 """
index 09148932d9271d5ee0fa4c2b4c309a930e69ac90..8b6cb4e2c9bd9171c916321fe77e616e2bae2928 100644 (file)
@@ -178,7 +178,6 @@ class NativeStorageServer:
     the their version information. I remember information about when we were
     last connected too, even if we aren't currently connected.
 
-    @ivar announcement_time: when we first heard about this service
     @ivar last_connect_time: when we last established a connection
     @ivar last_loss_time: when we last lost a connection
 
@@ -227,7 +226,6 @@ class NativeStorageServer:
             self._long_description = tubid_s
             self._short_description = tubid_s[:6]
 
-        self.announcement_time = time.time()
         self.last_connect_time = None
         self.last_loss_time = None
         self.remote_host = None
@@ -278,8 +276,11 @@ class NativeStorageServer:
         return self.last_connect_time
     def get_last_loss_time(self):
         return self.last_loss_time
-    def get_announcement_time(self):
-        return self.announcement_time
+    def get_last_received_data_time(self):
+        if self.rref is None:
+            return None
+        else:
+            return self.rref.getDataLastReceivedAt()
 
     def get_available_space(self):
         version = self.get_version()
index e51ab8b001c0da1a8874f1293ff55827b89ac70f..47dbe59a3fb685ad02cb99c54f7425e71cff9b61 100644 (file)
@@ -173,6 +173,24 @@ class TestMixin(SignalMixin):
         if required_to_quiesce and active:
             self.fail("Reactor was still active when it was required to be quiescent.")
 
+
+class TimezoneMixin(object):
+
+    def setTimezone(self, timezone):
+        unset = object()
+        originalTimezone = os.environ.get('TZ', unset)
+        def restoreTimezone():
+            if originalTimezone is unset:
+                del os.environ['TZ']
+                time.tzset()
+            else:
+                os.environ['TZ'] = originalTimezone
+                time.tzset()
+        os.environ['TZ'] = timezone
+        time.tzset()
+        self.addCleanup(restoreTimezone)
+
+
 try:
     import win32file
     import win32con
index ad97e6c082dcb698c5e0e0329a7e98c1f7d9e927..abf0a4479c68e1cad6fa0f7023285169f87b0896 100644 (file)
@@ -1145,6 +1145,55 @@ 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 = "This test is known to fail on systems with 32-bit time_t."
+
+    def test_format_delta(self):
+        time_1 = 1389812723
+        time_5s_delta = 1389812728
+        time_28m7s_delta = 1389814410
+        time_1h_delta = 1389816323
+        time_1d21h46m49s_delta = 1389977532
+
+        self.failUnlessEqual(
+            time_format.format_delta(time_1, time_1), '0s')
+
+        self.failUnlessEqual(
+            time_format.format_delta(time_1, time_5s_delta), '5s')
+        self.failUnlessEqual(
+            time_format.format_delta(time_1, time_28m7s_delta), '28m 7s')
+        self.failUnlessEqual(
+            time_format.format_delta(time_1, time_1h_delta), '1h 0m 0s')
+        self.failUnlessEqual(
+            time_format.format_delta(time_1, time_1d21h46m49s_delta), '1d 21h 46m 49s')
+
+        self.failUnlessEqual(
+            time_format.format_delta(time_1d21h46m49s_delta, time_1), '-')
+
+        # time_1 with a decimal fraction will make the delta 1s less
+        time_1decimal = 1389812723.383963
+
+        self.failUnlessEqual(
+            time_format.format_delta(time_1decimal, time_5s_delta), '4s')
+        self.failUnlessEqual(
+            time_format.format_delta(time_1decimal, time_28m7s_delta), '28m 6s')
+        self.failUnlessEqual(
+            time_format.format_delta(time_1decimal, time_1h_delta), '59m 59s')
+        self.failUnlessEqual(
+            time_format.format_delta(time_1decimal, time_1d21h46m49s_delta), '1d 21h 46m 48s')
+
 class CacheDir(unittest.TestCase):
     def test_basic(self):
         basedir = "test_util/CacheDir/test_basic"
index 6368f5b9651fc1048998ad9a4e97c18cbc94a51e..12e76e340c9b98c12cfe5a8541f067086819688f 100644 (file)
@@ -172,21 +172,28 @@ class FakeHistory:
         return []
 
 class FakeDisplayableServer(StubServer):
-    def __init__(self, serverid, nickname):
+    def __init__(self, serverid, nickname, connected,
+                 last_connect_time, last_loss_time, last_rx_time):
         StubServer.__init__(self, serverid)
         self.announcement = {"my-version": "allmydata-tahoe-fake",
                              "service-name": "storage",
                              "nickname": nickname}
+        self.connected = connected
+        self.last_loss_time = last_loss_time
+        self.last_rx_time = last_rx_time
+        self.last_connect_time = last_connect_time
     def is_connected(self):
-        return True
+        return self.connected
     def get_permutation_seed(self):
         return ""
     def get_remote_host(self):
         return ""
     def get_last_loss_time(self):
-        return None
-    def get_announcement_time(self):
-        return None
+        return self.last_loss_time
+    def get_last_received_data_time(self):
+        return self.last_rx_time
+    def get_last_connect_time(self):
+        return self.last_connect_time
     def get_announcement(self):
         return self.announcement
     def get_nickname(self):
@@ -242,7 +249,13 @@ class FakeClient(Client):
         self.storage_broker = StorageFarmBroker(None, True, 0, None)
         # fake knowledge of another server
         self.storage_broker.test_add_server("other_nodeid",
-                                            FakeDisplayableServer("other_nodeid", u"other_nickname \u263B"))
+            FakeDisplayableServer(
+                serverid="other_nodeid", nickname=u"other_nickname \u263B", connected = True,
+                last_connect_time = 10, last_loss_time = 20, last_rx_time = 30))
+        self.storage_broker.test_add_server("disconnected_nodeid",
+            FakeDisplayableServer(
+                serverid="other_nodeid", nickname=u"disconnected_nickname \u263B", connected = False,
+                last_connect_time = 15, last_loss_time = 25, last_rx_time = 35))
         self.introducer_client = None
         self.history = FakeHistory()
         self.uploader = FakeUploader()
@@ -268,14 +281,16 @@ class FakeClient(Client):
 
     MUTABLE_SIZELIMIT = FakeMutableFileNode.MUTABLE_SIZELIMIT
 
-class WebMixin(object):
+class WebMixin(testutil.TimezoneMixin):
     def setUp(self):
+        self.setTimezone('UTC-13:00')
         self.s = FakeClient()
         self.s.startService()
         self.staticdir = self.mktemp()
         self.clock = Clock()
+        self.fakeTime = 86460 # 1d 0h 1m 0s
         self.ws = webish.WebishServer(self.s, "0", staticdir=self.staticdir,
-                                      clock=self.clock)
+                                      clock=self.clock, now_fn=lambda:self.fakeTime)
         self.ws.setServiceParent(self.s)
         self.webish_port = self.ws.getPortnum()
         self.webish_url = self.ws.getURL()
@@ -601,7 +616,6 @@ class WebMixin(object):
             self.fail("%s was supposed to Error(302), not get '%s'" %
                       (which, res))
 
-
 class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixin, unittest.TestCase):
     def test_create(self):
         pass
@@ -619,6 +633,11 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
             res_u = res.decode('utf-8')
             self.failUnlessIn(u'<td>fake_nickname \u263A</td>', res_u)
             self.failUnlessIn(u'<div class="nickname">other_nickname \u263B</div>', res_u)
+            self.failUnlessIn(u'Connected to <span>1</span>\n              of <span>2</span> known storage servers', res_u)
+            self.failUnlessIn(u'<div class="status-indicator"><img src="img/connected-yes.png" alt="Connected" /></div>\n                <a class="timestamp" title="1970-01-01 13:00:10">1d\u00A00h\u00A00m\u00A050s</a>', res_u)
+            self.failUnlessIn(u'<div class="status-indicator"><img src="img/connected-no.png" alt="Disconnected" /></div>\n                <a class="timestamp" title="1970-01-01 13:00:25">1d\u00A00h\u00A00m\u00A035s</a>', res_u)
+            self.failUnlessIn(u'<td class="service-last-received-data"><a class="timestamp" title="1970-01-01 13:00:30">1d\u00A00h\u00A00m\u00A030s</a></td>', res_u)
+            self.failUnlessIn(u'<td class="service-last-received-data"><a class="timestamp" title="1970-01-01 13:00:35">1d\u00A00h\u00A00m\u00A025s</a></td>', res_u)
             self.failUnlessIn(u'\u00A9 <a href="https://tahoe-lafs.org/">Tahoe-LAFS Software Foundation', res_u)
             self.failUnlessIn('<td><h3>Available</h3></td>', res)
             self.failUnlessIn('123.5kB', res)
index 0f8f2f387dd89735e25ae208e8e6064851fd6878..a6fe3ec72c45ba21e4220bf1eea5d7680e48a29b 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().
@@ -68,3 +66,27 @@ def parse_date(s):
     # day
     return int(iso_utc_time_to_seconds(s + "T00:00:00"))
 
+def format_delta(time_1, time_2):
+    if time_1 is None:
+        return "N/A"
+    if time_1 > time_2:
+        return '-'
+    delta = int(time_2 - time_1)
+    seconds = delta % 60
+    delta  -= seconds
+    minutes = (delta / 60) % 60
+    delta  -= minutes * 60
+    hours   = delta / (60*60) % 24
+    delta  -= hours * 24
+    days    = delta / (24*60*60)
+    if not days:
+        if not hours:
+            if not minutes:
+                return "%ss" % (seconds)
+            else:
+                return "%sm %ss" % (minutes, seconds)
+        else:
+            return "%sh %sm %ss" % (hours, minutes, seconds)
+    else:
+        return "%sd %sh %sm %ss" % (days, hours, minutes, seconds)
+
index 52fed6a04be4c0002a52acc7e546380b5abfe5d4..5cfe84d9a4a09237669dc913280c08a78cc994f0 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, format_delta
 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,18 @@ 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_delta(time_1, time_2):
+    return spaces_to_nbsp(format_delta(time_1, time_2))
+
+def render_time(t):
+    return spaces_to_nbsp(format_time(time.localtime(t)))
+
+def render_time_attr(t):
+    return 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..d8d789cf37c08e14b0de8758754ea84f69bccd95 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_delta, render_time, render_time_attr
 
 
 class URIHandler(RenderMixin, rend.Page):
@@ -138,12 +138,13 @@ class Root(rend.Page):
         "no": "Disconnected",
         }
 
-    def __init__(self, client, clock=None):
+    def __init__(self, client, clock=None, now_fn=None):
         rend.Page.__init__(self, client)
         self.client = client
         # If set, clock is a twisted.internet.task.Clock that the tests
         # use to test ophandle expiration.
         self.child_operations = operations.OphandleTable(clock)
+        self.now_fn = now_fn
         try:
             s = client.getServiceNamed("storage")
         except KeyError:
@@ -171,7 +172,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):
@@ -282,7 +283,7 @@ class Root(rend.Page):
         ctx.fillSlots("peerid", server.get_longname())
         ctx.fillSlots("nickname", server.get_nickname())
         rhost = server.get_remote_host()
-        if rhost:
+        if server.is_connected():
             if nodeid == self.client.nodeid:
                 rhost_s = "(loopback)"
             elif isinstance(rhost, address.IPv4Address):
@@ -290,31 +291,37 @@ class Root(rend.Page):
             else:
                 rhost_s = str(rhost)
             addr = rhost_s
-            connected = "yes"
-            since = server.get_last_connect_time()
+            service_connection_status = "yes"
+            last_connect_time = server.get_last_connect_time()
+            service_connection_status_rel_time = render_time_delta(last_connect_time, self.now_fn())
+            service_connection_status_abs_time = render_time_attr(last_connect_time)
         else:
             addr = "N/A"
-            connected = "no"
-            since = server.get_last_loss_time()
-        announced = server.get_announcement_time()
+            service_connection_status = "no"
+            last_loss_time = server.get_last_loss_time()
+            service_connection_status_rel_time = render_time_delta(last_loss_time, self.now_fn())
+            service_connection_status_abs_time = render_time_attr(last_loss_time)
+
+        last_received_data_time = server.get_last_received_data_time()
+        last_received_data_rel_time = render_time_delta(last_received_data_time, self.now_fn())
+        last_received_data_abs_time = render_time_attr(last_received_data_time)
+
         announcement = server.get_announcement()
         version = announcement["my-version"]
-        service_name = announcement["service-name"]
         available_space = server.get_available_space()
         if available_space is None:
             available_space = "N/A"
         else:
             available_space = abbreviate_size(available_space)
         ctx.fillSlots("address", addr)
-        ctx.fillSlots("connected", connected)
-        ctx.fillSlots("connected_alt", self._connectedalts[connected])
+        ctx.fillSlots("service_connection_status", service_connection_status)
+        ctx.fillSlots("service_connection_status_alt", self._connectedalts[service_connection_status])
         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("service_connection_status_abs_time", service_connection_status_abs_time)
+        ctx.fillSlots("service_connection_status_rel_time", service_connection_status_rel_time)
+        ctx.fillSlots("last_received_data_abs_time", last_received_data_abs_time)
+        ctx.fillSlots("last_received_data_rel_time", last_received_data_rel_time)
         ctx.fillSlots("version", version)
-        ctx.fillSlots("service_name", service_name)
         ctx.fillSlots("available_space", available_space)
 
         return ctx.tag
index 175c3e33718c42d0eb4d04905079fd5b1ed75aa8..8ab7f47d64b4b6f3aa0811bcee8a38d6eac0d2b7 100644 (file)
@@ -77,3 +77,12 @@ body {
     float: left;
     margin: 5px;
 }
+
+.nickname-and-peerid .timestamp {
+    float: right;
+}
+
+a.timestamp {
+    color: inherit;
+    text-decoration:none;
+}
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())
index 3e23fc736fa3446b09ae9560caa80d79fe44a5e7..ca2387672fdf499e4dbaadf74aa9834a0dcad965 100644 (file)
@@ -1,4 +1,5 @@
-<!DOCTYPE html>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0
+      Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html lang="en" xmlns:n="http://nevow.com/ns/nevow/0.1">
   <head>
     <meta charset="utf-8"/>
               <tr n:pattern="header">
                 <td><h3>Nickname</h3></td>
                 <td><h3>Address</h3></td>
-                <td><h3>Service</h3></td>
-                <td><h3>Since</h3></td>
-                <td><h3>Announced</h3></td>
+                <td><h3>Last&nbsp;RX</h3></td>
                 <td><h3>Version</h3></td>
                 <td><h3>Available</h3></td>
               </tr>
             </thead>
             <tr n:pattern="item" n:render="service_row">
               <td class="nickname-and-peerid">
-              <div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="connected" />.png</n:attr><n:attr name="alt"><n:slot name="connected_alt" /></n:attr></img></div>
+                <div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="service_connection_status" />.png</n:attr><n:attr name="alt"><n:slot name="service_connection_status_alt" /></n:attr></img></div>
+                <a class="timestamp"><n:attr name="title"><n:slot name="service_connection_status_abs_time"/></n:attr><n:slot name="service_connection_status_rel_time"/></a>
                 <div class="nickname"><n:slot name="nickname"/></div>
                 <div class="nodeid"><n:slot name="peerid"/></div>
               </td>
               <td class="address"><n:slot name="address"/></td>
-            <td class="service-service-name"><n:slot name="service_name"/></td>
-              <td class="service-since timestamp"><n:slot name="since"/></td>
-              <td class="service-announced timestamp"><n:slot name="announced"/></td>
+              <td class="service-last-received-data"><a class="timestamp"><n:attr name="title"><n:slot name="last_received_data_abs_time"/></n:attr><n:slot name="last_received_data_rel_time"/></a></td>
               <td class="service-version"><n:slot name="version"/></td>
               <td class="service-available-space"><n:slot name="available_space"/></td>
             </tr>
-            <tr n:pattern="empty"><td>You are not presently connected to any peers</td></tr>
+            <tr n:pattern="empty"><td colspan="5">You are not presently connected to any peers</td></tr>
           </table>
         </div><!--/span-->
       </div><!--/row-->
index e2029feecd595ba21bf3516c880189065286569e..b26b34da30d3e1f32dcc3923ea92d86e7a3f6849 100644 (file)
@@ -129,14 +129,14 @@ class WebishServer(service.MultiService):
     name = "webish"
 
     def __init__(self, client, webport, nodeurl_path=None, staticdir=None,
-                 clock=None):
+                 clock=None, now_fn=time.time):
         service.MultiService.__init__(self)
         # the 'data' argument to all render() methods default to the Client
         # the 'clock' argument to root.Root is, if set, a
         # twisted.internet.task.Clock that is provided by the unit tests
         # so that they can test features that involve the passage of
         # time in a deterministic manner.
-        self.root = root.Root(client, clock)
+        self.root = root.Root(client, clock, now_fn)
         self.buildServer(webport, nodeurl_path, staticdir)
         if self.root.child_operations:
             self.site.remember(self.root.child_operations, IOpHandleTable)