Merge pull request #710 from sajith/3305.root-from-nevow-to-twisted-web

Move root.Root from nevow to twisted.web.template

Fixes: ticket:3305
This commit is contained in:
Sajith Sasidharan 2020-05-05 15:56:21 -04:00 committed by GitHub
commit 5e4f2d88f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 461 additions and 339 deletions

0
newsfragments/3305.minor Normal file
View File

View File

@ -3,6 +3,8 @@ from __future__ import print_function
import os, re, sys, time, json
from functools import partial
from bs4 import BeautifulSoup
from twisted.internet import reactor
from twisted.trial import unittest
from twisted.internet import defer
@ -38,6 +40,9 @@ from .common import (
SameProcessStreamEndpointAssigner,
)
from .common_web import do_http, Error
from .web.common import (
assert_soup_has_tag_with_attributes
)
# TODO: move this to common or common_util
from allmydata.test.test_runner import RunBinTahoeMixin
@ -1771,8 +1776,11 @@ class SystemTest(SystemTestMixin, RunBinTahoeMixin, unittest.TestCase):
# get the welcome page from the node that uses the helper too
d.addCallback(lambda res: do_http("get", self.helper_webish_url))
def _got_welcome_helper(page):
html = page.replace('\n', ' ')
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/>', html), page)
soup = BeautifulSoup(page, 'html5lib')
assert_soup_has_tag_with_attributes(
self, soup, u"img",
{ u"alt": u"Connected", u"src": u"img/connected-yes.png" }
)
self.failUnlessIn("Not running helper", page)
d.addCallback(_got_welcome_helper)

View File

@ -1,13 +1,21 @@
from mock import Mock
from twisted.trial import unittest
from twisted.web.test.requesthelper import DummyRequest
import time
from ...storage_client import NativeStorageServer
from ...web.root import Root
from twisted.trial import unittest
from twisted.web.template import Tag
from twisted.web.test.requesthelper import DummyRequest
from twisted.application import service
from ...storage_client import (
NativeStorageServer,
StorageFarmBroker,
)
from ...web.root import RootElement
from ...util.connection_status import ConnectionStatus
from allmydata.web.root import URIHandler
from allmydata.web.common import WebError
from allmydata.client import _Client
from hypothesis import given
from hypothesis.strategies import text
@ -17,21 +25,6 @@ from ..common import (
EMPTY_CLIENT_CONFIG,
)
class FakeRoot(Root):
def __init__(self):
pass
def now_fn(self):
return 0
class FakeContext(object):
def __init__(self):
self.slots = {}
self.tag = self
def fillSlots(self, slotname, contents):
self.slots[slotname] = contents
class RenderSlashUri(unittest.TestCase):
"""
Ensure that URIs starting with /uri?uri= only accept valid
@ -90,13 +83,28 @@ class RenderServiceRow(unittest.TestCase):
ann = {"anonymous-storage-FURL": "pb://w2hqnbaa25yw4qgcvghl5psa3srpfgw3@tcp:127.0.0.1:51309/vucto2z4fxment3vfxbqecblbf6zyp6x",
"permutation-seed-base32": "w2hqnbaa25yw4qgcvghl5psa3srpfgw3",
}
s = NativeStorageServer("server_id", ann, None, {}, EMPTY_CLIENT_CONFIG)
cs = ConnectionStatus(False, "summary", {}, 0, 0)
s.get_connection_status = lambda: cs
srv = NativeStorageServer("server_id", ann, None, {}, EMPTY_CLIENT_CONFIG)
srv.get_connection_status = lambda: ConnectionStatus(False, "summary", {}, 0, 0)
r = FakeRoot()
ctx = FakeContext()
res = r.render_service_row(ctx, s)
self.assertIdentical(res, ctx)
self.assertEqual(ctx.slots["version"], "")
self.assertEqual(ctx.slots["nickname"], "")
class FakeClient(_Client):
def __init__(self):
service.MultiService.__init__(self)
self.storage_broker = StorageFarmBroker(
permute_peers=True,
tub_maker=None,
node_config=EMPTY_CLIENT_CONFIG,
)
self.storage_broker.test_add_server("test-srv", srv)
root = RootElement(FakeClient(), time.time)
req = DummyRequest(b"")
tag = Tag(b"")
# Pick all items from services table.
items = root.services_table(req, tag).item(req, tag)
# Coerce `items` to list and pick the first item from it.
item = list(items)[0]
self.assertEqual(item.slotData.get("version"), "")
self.assertEqual(item.slotData.get("nickname"), "")

View File

@ -53,6 +53,8 @@ from .common import (
assert_soup_has_favicon,
assert_soup_has_text,
assert_soup_has_tag_with_attributes,
assert_soup_has_tag_with_content,
assert_soup_has_tag_with_attributes_and_content,
)
from allmydata.interfaces import IMutableFileNode, SDMF_VERSION, MDMF_VERSION
@ -832,10 +834,16 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
return self.GET("/")
d.addCallback(_set_introducer_not_connected_unguessable)
def _check_introducer_not_connected_unguessable(res):
html = res.replace('\n', ' ')
self.failIfIn('pb://someIntroducer/secret', html)
self.failUnless(re.search('<img (alt="Disconnected" |src="img/connected-no.png" ){2}/></div>[ ]*<div>No introducers connected</div>', html), res)
soup = BeautifulSoup(res, 'html5lib')
self.failIfIn('pb://someIntroducer/secret', res)
assert_soup_has_tag_with_attributes(
self, soup, u"img",
{u"alt": u"Disconnected", u"src": u"img/connected-no.png"}
)
assert_soup_has_tag_with_content(
self, soup, u"div",
u"No introducers connected"
)
d.addCallback(_check_introducer_not_connected_unguessable)
# introducer connected, unguessable furl
@ -845,10 +853,21 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
return self.GET("/")
d.addCallback(_set_introducer_connected_unguessable)
def _check_introducer_connected_unguessable(res):
html = res.replace('\n', ' ')
self.failUnlessIn('<div class="connection-status" title="(no other hints)">summary</div>', html)
self.failIfIn('pb://someIntroducer/secret', html)
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>[ ]*<div>1 introducer connected</div>', html), res)
soup = BeautifulSoup(res, 'html5lib')
assert_soup_has_tag_with_attributes_and_content(
self, soup, u"div",
u"summary",
{ u"class": u"connection-status", u"title": u"(no other hints)" }
)
self.failIfIn('pb://someIntroducer/secret', res)
assert_soup_has_tag_with_attributes(
self, soup, u"img",
{ u"alt": u"Connected", u"src": u"img/connected-yes.png" }
)
assert_soup_has_tag_with_content(
self, soup, u"div",
u"1 introducer connected"
)
d.addCallback(_check_introducer_connected_unguessable)
# introducer connected, guessable furl
@ -858,9 +877,20 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
return self.GET("/")
d.addCallback(_set_introducer_connected_guessable)
def _check_introducer_connected_guessable(res):
html = res.replace('\n', ' ')
self.failUnlessIn('<div class="connection-status" title="(no other hints)">summary</div>', html)
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>[ ]*<div>1 introducer connected</div>', html), res)
soup = BeautifulSoup(res, 'html5lib')
assert_soup_has_tag_with_attributes_and_content(
self, soup, u"div",
u"summary",
{ u"class": u"connection-status", u"title": u"(no other hints)" }
)
assert_soup_has_tag_with_attributes(
self, soup, u"img",
{ u"src": u"img/connected-yes.png", u"alt": u"Connected" }
)
assert_soup_has_tag_with_content(
self, soup, u"div",
u"1 introducer connected"
)
d.addCallback(_check_introducer_connected_guessable)
return d
@ -873,8 +903,11 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
return self.GET("/")
d.addCallback(_set_no_helper)
def _check_no_helper(res):
html = res.replace('\n', ' ')
self.failUnless(re.search('<img (src="img/connected-not-configured.png" |alt="Not Configured" ){2}/>', html), res)
soup = BeautifulSoup(res, 'html5lib')
assert_soup_has_tag_with_attributes(
self, soup, u"img",
{ u"src": u"img/connected-not-configured.png", u"alt": u"Not Configured" }
)
d.addCallback(_check_no_helper)
# enable helper, not connected
@ -884,10 +917,17 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
return self.GET("/")
d.addCallback(_set_helper_not_connected)
def _check_helper_not_connected(res):
html = res.replace('\n', ' ')
self.failUnlessIn('<div class="furl">pb://someHelper/[censored]</div>', html)
self.failIfIn('pb://someHelper/secret', html)
self.failUnless(re.search('<img (src="img/connected-no.png" |alt="Disconnected" ){2}/>', html), res)
soup = BeautifulSoup(res, 'html5lib')
assert_soup_has_tag_with_attributes_and_content(
self, soup, u"div",
u"pb://someHelper/[censored]",
{ u"class": u"furl" }
)
self.failIfIn('pb://someHelper/secret', res)
assert_soup_has_tag_with_attributes(
self, soup, u"img",
{ u"src": u"img/connected-no.png", u"alt": u"Disconnected" }
)
d.addCallback(_check_helper_not_connected)
# enable helper, connected
@ -897,10 +937,17 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
return self.GET("/")
d.addCallback(_set_helper_connected)
def _check_helper_connected(res):
html = res.replace('\n', ' ')
self.failUnlessIn('<div class="furl">pb://someHelper/[censored]</div>', html)
self.failIfIn('pb://someHelper/secret', html)
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/>', html), res)
soup = BeautifulSoup(res, 'html5lib')
assert_soup_has_tag_with_attributes_and_content(
self, soup, u"div",
u"pb://someHelper/[censored]",
{ u"class": u"furl" }
)
self.failIfIn('pb://someHelper/secret', res)
assert_soup_has_tag_with_attributes(
self, soup, u"img",
{ u"src": u"img/connected-yes.png", "alt": u"Connected" }
)
d.addCallback(_check_helper_connected)
return d

View File

@ -3,32 +3,40 @@ import time
import json
import urllib
from hyperlink import DecodedURL, URL
from pkg_resources import resource_filename
from twisted.web import (
http,
resource,
static,
)
from twisted.web.util import redirectTo
from hyperlink import DecodedURL, URL
from nevow import rend, tags as T
from nevow.inevow import IRequest
from nevow.static import File as nevow_File # TODO: merge with static.File?
from nevow.util import resource_filename
from twisted.python.filepath import FilePath
from twisted.web.template import (
Element,
XMLFile,
renderer,
renderElement,
tags,
)
import allmydata # to display import path
from allmydata.version_checks import get_package_versions_string
from allmydata.util import log
from allmydata.interfaces import IFileNode
from allmydata.web import filenode, directory, unlinked, status
from allmydata.web import (
filenode,
directory,
unlinked,
status,
)
from allmydata.web import storage
from allmydata.web.common import (
abbreviate_size,
getxmlfile,
WebError,
get_arg,
MultiFormatPage,
MultiFormatResource,
SlotsSequenceElement,
get_format,
get_mutable_type,
render_time_delta,
@ -193,21 +201,22 @@ class IncidentReporter(MultiFormatResource):
SPACE = u"\u00A0"*2
class Root(MultiFormatPage):
class Root(MultiFormatResource):
addSlash = True
docFactory = getxmlfile("welcome.xhtml")
_connectedalts = {
"not-configured": "Not Configured",
"yes": "Connected",
"no": "Disconnected",
}
def __init__(self, client, clock=None, now_fn=None):
rend.Page.__init__(self, client)
self.client = client
self.now_fn = now_fn
"""
Render root page ("/") of the URI.
:client allmydata.client._Client: a stats provider.
:clock: unused here.
:now_fn: a function that returns current time.
"""
super(Root, self).__init__()
self._client = client
self._now_fn = now_fn
self.putChild("uri", URIHandler(client))
self.putChild("cap", URIHandler(client))
@ -223,56 +232,35 @@ class Root(MultiFormatPage):
self.putChild("statistics", status.Statistics(client.stats_provider))
static_dir = resource_filename("allmydata.web", "static")
for filen in os.listdir(static_dir):
self.putChild(filen, nevow_File(os.path.join(static_dir, filen)))
self.putChild(filen, static.File(os.path.join(static_dir, filen)))
self.putChild("report_incident", IncidentReporter())
# until we get rid of nevow.Page in favour of twisted.web.resource
# we can't use getChild() -- but we CAN use childFactory or
# override locatechild
def childFactory(self, ctx, name):
request = IRequest(ctx)
return self.getChild(name, request)
def getChild(self, path, request):
if not path:
# Render "/" path.
return self
if path == "helper_status":
# the Helper isn't attached until after the Tub starts, so this child
# needs to created on each request
return status.HelperStatus(self.client.helper)
return status.HelperStatus(self._client.helper)
if path == "storage":
# Storage isn't initialized until after the web hierarchy is
# constructed so this child needs to be created later than
# `__init__`.
try:
storage_server = self.client.getServiceNamed("storage")
storage_server = self._client.getServiceNamed("storage")
except KeyError:
storage_server = None
return storage.StorageStatus(storage_server, self.client.nickname)
# FIXME: This code is duplicated in root.py and introweb.py.
def data_rendered_at(self, ctx, data):
return render_time(time.time())
def data_version(self, ctx, data):
return get_package_versions_string()
def data_import_path(self, ctx, data):
return str(allmydata)
def render_my_nodeid(self, ctx, data):
tubid_s = "TubID: "+self.client.get_long_tubid()
return T.td(title=tubid_s)[self.client.get_long_nodeid()]
def data_my_nickname(self, ctx, data):
return self.client.nickname
return storage.StorageStatus(storage_server, self._client.nickname)
def render_HTML(self, req):
return renderElement(req, RootElement(self._client, self._now_fn))
def render_JSON(self, req):
req.setHeader("content-type", "application/json; charset=utf-8")
intro_summaries = [s.summary for s in self.client.introducer_connection_statuses()]
sb = self.client.get_storage_broker()
intro_summaries = [s.summary for s in self._client.introducer_connection_statuses()]
sb = self._client.get_storage_broker()
servers = self._describe_known_servers(sb)
result = {
"introducers": {
@ -307,11 +295,54 @@ class Root(MultiFormatPage):
return description
class RootElement(Element):
def render_services(self, ctx, data):
ul = T.ul()
loader = XMLFile(FilePath(__file__).sibling("welcome.xhtml"))
def __init__(self, client, now_fn):
super(RootElement, self).__init__()
self._client = client
self._now_fn = now_fn
_connectedalts = {
"not-configured": "Not Configured",
"yes": "Connected",
"no": "Disconnected",
}
@renderer
def my_nodeid(self, req, tag):
tubid_s = "TubID: "+self._client.get_long_tubid()
return tags.td(self._client.get_long_nodeid(), title=tubid_s)
@renderer
def my_nickname(self, req, tag):
return tag(self._client.nickname)
def _connected_introducers(self):
return len([1 for cs in self._client.introducer_connection_statuses()
if cs.connected])
@renderer
def connected_introducers(self, req, tag):
return tag(str(self._connected_introducers()))
@renderer
def connected_to_at_least_one_introducer(self, req, tag):
if self._connected_introducers():
return "yes"
return "no"
@renderer
def connected_to_at_least_one_introducer_alt(self, req, tag):
state = self.connected_to_at_least_one_introducer(req, tag)
return self._connectedalts.get(state)
@renderer
def services(self, req, tag):
ul = tags.ul()
try:
ss = self.client.getServiceNamed("storage")
ss = self._client.getServiceNamed("storage")
stats = ss.get_stats()
if stats["storage_server.accepting_immutable_shares"]:
msg = "accepting new shares"
@ -320,113 +351,60 @@ class Root(MultiFormatPage):
available = stats.get("storage_server.disk_avail")
if available is not None:
msg += ", %s available" % abbreviate_size(available)
ul[T.li[T.a(href="storage")["Storage Server"], ": ", msg]]
ul(tags.li(tags.a("Storage Server", href="storage"), ": ", msg))
except KeyError:
ul[T.li["Not running storage server"]]
ul(tags.li("Not running storage server"))
if self.client.helper:
stats = self.client.helper.get_stats()
if self._client.helper:
stats = self._client.helper.get_stats()
active_uploads = stats["chk_upload_helper.active_uploads"]
ul[T.li["Helper: %d active uploads" % (active_uploads,)]]
ul(tags.li("Helper: %d active uploads" % (active_uploads,)))
else:
ul[T.li["Not running helper"]]
ul(tags.li("Not running helper"))
return ctx.tag[ul]
return tag(ul)
def data_introducer_description(self, ctx, data):
connected_count = self.data_connected_introducers( ctx, data )
@renderer
def introducer_description(self, req, tag):
connected_count = self._connected_introducers()
if connected_count == 0:
return "No introducers connected"
return tag("No introducers connected")
elif connected_count == 1:
return "1 introducer connected"
return tag("1 introducer connected")
else:
return "%s introducers connected" % (connected_count,)
return tag("%s introducers connected" % (connected_count,))
def data_total_introducers(self, ctx, data):
return len(self.client.introducer_connection_statuses())
def data_connected_introducers(self, ctx, data):
return len([1 for cs in self.client.introducer_connection_statuses()
if cs.connected])
def data_connected_to_at_least_one_introducer(self, ctx, data):
if self.data_connected_introducers(ctx, data):
return "yes"
return "no"
def data_connected_to_at_least_one_introducer_alt(self, ctx, data):
return self._connectedalts[self.data_connected_to_at_least_one_introducer(ctx, data)]
@renderer
def total_introducers(self, req, tag):
return tag(str(len(self._get_introducers())))
# In case we configure multiple introducers
def data_introducers(self, ctx, data):
return self.client.introducer_connection_statuses()
@renderer
def introducers(self, req, tag):
ix = self._get_introducers()
if not ix:
return tag("No introducers")
return tag
def _render_connection_status(self, ctx, cs):
connected = "yes" if cs.connected else "no"
ctx.fillSlots("service_connection_status", connected)
ctx.fillSlots("service_connection_status_alt",
self._connectedalts[connected])
def _get_introducers(self):
return self._client.introducer_connection_statuses()
since = cs.last_connection_time
ctx.fillSlots("service_connection_status_rel_time",
render_time_delta(since, self.now_fn())
if since is not None
else "N/A")
ctx.fillSlots("service_connection_status_abs_time",
render_time_attr(since)
if since is not None
else "N/A")
last_received_data_time = cs.last_received_time
ctx.fillSlots("last_received_data_abs_time",
render_time_attr(last_received_data_time)
if last_received_data_time is not None
else "N/A")
ctx.fillSlots("last_received_data_rel_time",
render_time_delta(last_received_data_time, self.now_fn())
if last_received_data_time is not None
else "N/A")
others = cs.non_connected_statuses
if cs.connected:
ctx.fillSlots("summary", cs.summary)
if others:
details = "\n".join(["* %s: %s\n" % (which, others[which])
for which in sorted(others)])
ctx.fillSlots("details", "Other hints:\n" + details)
else:
ctx.fillSlots("details", "(no other hints)")
else:
details = T.ul()
for which in sorted(others):
details[T.li["%s: %s" % (which, others[which])]]
ctx.fillSlots("summary", [cs.summary, details])
ctx.fillSlots("details", "")
def render_introducers_row(self, ctx, cs):
self._render_connection_status(ctx, cs)
return ctx.tag
def data_helper_furl_prefix(self, ctx, data):
@renderer
def helper_furl_prefix(self, req, tag):
try:
uploader = self.client.getServiceNamed("uploader")
uploader = self._client.getServiceNamed("uploader")
except KeyError:
return None
return tag("None")
furl, connected = uploader.get_helper_info()
if not furl:
return None
return tag("None")
# trim off the secret swissnum
(prefix, _, swissnum) = furl.rpartition("/")
return "%s/[censored]" % (prefix,)
return tag("%s/[censored]" % (prefix,))
def data_helper_description(self, ctx, data):
if self.data_connected_to_helper(ctx, data) == "no":
return "Helper not connected"
return "Helper"
def data_connected_to_helper(self, ctx, data):
def _connected_to_helper(self):
try:
uploader = self.client.getServiceNamed("uploader")
uploader = self._client.getServiceNamed("uploader")
except KeyError:
return "no" # we don't even have an Uploader
furl, connected = uploader.get_helper_info()
@ -437,123 +415,147 @@ class Root(MultiFormatPage):
return "yes"
return "no"
def data_connected_to_helper_alt(self, ctx, data):
return self._connectedalts[self.data_connected_to_helper(ctx, data)]
@renderer
def helper_description(self, req, tag):
if self._connected_to_helper() == "no":
return tag("Helper not connected")
return tag("Helper")
def data_known_storage_servers(self, ctx, data):
sb = self.client.get_storage_broker()
return len(sb.get_all_serverids())
@renderer
def connected_to_helper(self, req, tag):
return tag(self._connected_to_helper())
def data_connected_storage_servers(self, ctx, data):
sb = self.client.get_storage_broker()
return len(sb.get_connected_servers())
@renderer
def connected_to_helper_alt(self, req, tag):
return tag(self._connectedalts.get(self._connected_to_helper()))
def data_services(self, ctx, data):
sb = self.client.get_storage_broker()
@renderer
def known_storage_servers(self, req, tag):
sb = self._client.get_storage_broker()
return tag(str(len(sb.get_all_serverids())))
@renderer
def connected_storage_servers(self, req, tag):
sb = self._client.get_storage_broker()
return tag(str(len(sb.get_connected_servers())))
@renderer
def services_table(self, req, tag):
rows = [ self._describe_server_and_connection(server)
for server in self._services() ]
return SlotsSequenceElement(tag, rows)
@renderer
def introducers_table(self, req, tag):
rows = [ self._describe_connection_status(cs)
for cs in self._get_introducers() ]
return SlotsSequenceElement(tag, rows)
def _services(self):
sb = self._client.get_storage_broker()
return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
def render_service_row(self, ctx, server):
cs = server.get_connection_status()
self._render_connection_status(ctx, cs)
@staticmethod
def _describe_server(server):
"""Return a dict containing server stats."""
peerid = server.get_longname()
nickname = server.get_nickname()
version = server.get_announcement().get("my-version", "")
ctx.fillSlots("peerid", server.get_longname())
ctx.fillSlots("nickname", server.get_nickname())
announcement = server.get_announcement()
version = announcement.get("my-version", "")
available_space = server.get_available_space()
if available_space is None:
available_space = "N/A"
space = server.get_available_space()
if space is not None:
available_space = abbreviate_size(space)
else:
available_space = abbreviate_size(available_space)
ctx.fillSlots("version", version)
ctx.fillSlots("available_space", available_space)
available_space = "N/A"
return ctx.tag
return {
"peerid": peerid,
"nickname": nickname,
"version": version,
"available_space": available_space,
}
def render_download_form(self, ctx, data):
# this is a form where users can download files by URI
form = T.form(action="uri", method="get",
enctype="multipart/form-data")[
T.fieldset[
T.legend(class_="freeform-form-label")["Download a file"],
T.div["Tahoe-URI to download:"+SPACE,
T.input(type="text", name="uri")],
T.div["Filename to download as:"+SPACE,
T.input(type="text", name="filename")],
T.input(type="submit", value="Download!"),
]]
return T.div[form]
def _describe_server_and_connection(self, server):
"""Return a dict containing both server and connection stats."""
srvstat = self._describe_server(server)
cs = server.get_connection_status()
constat = self._describe_connection_status(cs)
return dict(list(srvstat.items()) + list(constat.items()))
def render_view_form(self, ctx, data):
# this is a form where users can download files by URI, or jump to a
# named directory
form = T.form(action="uri", method="get",
enctype="multipart/form-data")[
T.fieldset[
T.legend(class_="freeform-form-label")["View a file or directory"],
"Tahoe-URI to view:"+SPACE,
T.input(type="text", name="uri"), SPACE*2,
T.input(type="submit", value="View!"),
]]
return T.div[form]
def _describe_connection_status(self, cs):
"""Return a dict containing some connection stats."""
others = cs.non_connected_statuses
def render_upload_form(self, ctx, data):
# This is a form where users can upload unlinked files.
# Users can choose immutable, SDMF, or MDMF from a radio button.
if cs.connected:
summary = cs.summary
if others:
hints = "\n".join(["* %s: %s\n" % (which, others[which])
for which in sorted(others)])
details = "Other hints:\n" + hints
else:
details = "(no other hints)"
else:
details = tags.ul()
for which in sorted(others):
details(tags.li("%s: %s" % (which, others[which])))
summary = [cs.summary, details]
upload_chk = T.input(type='radio', name='format',
value='chk', id='upload-chk',
checked='checked')
upload_sdmf = T.input(type='radio', name='format',
value='sdmf', id='upload-sdmf')
upload_mdmf = T.input(type='radio', name='format',
value='mdmf', id='upload-mdmf')
connected = "yes" if cs.connected else "no"
connected_alt = self._connectedalts[connected]
form = T.form(action="uri", method="post",
enctype="multipart/form-data")[
T.fieldset[
T.legend(class_="freeform-form-label")["Upload a file"],
T.div["Choose a file:"+SPACE,
T.input(type="file", name="file", class_="freeform-input-file")],
T.input(type="hidden", name="t", value="upload"),
T.div[upload_chk, T.label(for_="upload-chk") [" Immutable"], SPACE,
upload_sdmf, T.label(for_="upload-sdmf")[" SDMF"], SPACE,
upload_mdmf, T.label(for_="upload-mdmf")[" MDMF (experimental)"], SPACE*2,
T.input(type="submit", value="Upload!")],
]]
return T.div[form]
since = cs.last_connection_time
def render_mkdir_form(self, ctx, data):
# This is a form where users can create new directories.
# Users can choose SDMF or MDMF from a radio button.
if since is not None:
service_connection_status_rel_time = render_time_delta(since, self._now_fn())
service_connection_status_abs_time = render_time_attr(since)
else:
service_connection_status_rel_time = "N/A"
service_connection_status_abs_time = "N/A"
mkdir_sdmf = T.input(type='radio', name='format',
value='sdmf', id='mkdir-sdmf',
checked='checked')
mkdir_mdmf = T.input(type='radio', name='format',
value='mdmf', id='mkdir-mdmf')
last_received_data_time = cs.last_received_time
form = T.form(action="uri", method="post",
enctype="multipart/form-data")[
T.fieldset[
T.legend(class_="freeform-form-label")["Create a directory"],
mkdir_sdmf, T.label(for_='mkdir-sdmf')[" SDMF"], SPACE,
mkdir_mdmf, T.label(for_='mkdir-mdmf')[" MDMF (experimental)"], SPACE*2,
T.input(type="hidden", name="t", value="mkdir"),
T.input(type="hidden", name="redirect_to_result", value="true"),
T.input(type="submit", value="Create a directory"),
]]
return T.div[form]
if last_received_data_time is not None:
last_received_data_abs_time = render_time_attr(last_received_data_time)
last_received_data_rel_time = render_time_delta(last_received_data_time, self._now_fn())
else:
last_received_data_abs_time = "N/A"
last_received_data_rel_time = "N/A"
def render_incident_button(self, ctx, data):
return {
"summary": summary,
"details": details,
"service_connection_status": connected,
"service_connection_status_alt": connected_alt,
"service_connection_status_abs_time": service_connection_status_abs_time,
"service_connection_status_rel_time": service_connection_status_rel_time,
"last_received_data_abs_time": last_received_data_abs_time,
"last_received_data_rel_time": last_received_data_rel_time,
}
@renderer
def incident_button(self, req, tag):
# this button triggers a foolscap-logging "incident"
form = T.form(action="report_incident", method="post",
enctype="multipart/form-data")[
T.fieldset[
T.input(type="hidden", name="t", value="report-incident"),
"What went wrong?"+SPACE,
T.input(type="text", name="details"), SPACE,
T.input(type="submit", value=u"Save \u00BB"),
]]
return T.div[form]
form = tags.form(
tags.fieldset(
tags.input(type="hidden", name="t", value="report-incident"),
"What went wrong?"+SPACE,
tags.input(type="text", name="details"), SPACE,
tags.input(type="submit", value=u"Save \u00BB"),
),
action="report_incident",
method="post",
enctype="multipart/form-data"
)
return tags.div(form)
@renderer
def rendered_at(self, req, tag):
return tag(render_time(time.time()))
@renderer
def version(self, req, tag):
return tag(get_package_versions_string())
@renderer
def import_path(self, req, tag):
return tag(str(allmydata))

View File

@ -1,6 +1,4 @@
<!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">
<html xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
<head>
<meta charset="utf-8"/>
<title>Tahoe-LAFS - Welcome</title>
@ -25,11 +23,11 @@
<table class="node-info pull-right">
<tr>
<th>Nickname:</th>
<td n:render="data" n:data="my_nickname" />
<td t:render="my_nickname" />
</tr>
<tr>
<th>Node ID:</th>
<td n:render="my_nodeid" />
<td t:render="my_nodeid" />
</tr>
</table>
</div>
@ -124,7 +122,7 @@
<div class="nav-header">
<ul class="nav nav-list">
<li class="nav-header">Save incident report</li>
<li><div n:render="incident_button" /></li>
<li><div t:render="incident_button" /></li>
</ul>
</div>
</div><!--/.well -->
@ -138,22 +136,32 @@
<div class="span6">
<div>
<h3>
<div class="status-indicator"><img><n:attr name="src">img/connected-<n:invisible n:render="string" n:data="connected_to_at_least_one_introducer" />.png</n:attr><n:attr name="alt"><n:invisible n:render="string" n:data="connected_to_at_least_one_introducer_alt" /></n:attr></img></div>
<div n:render="string" n:data="introducer_description" />
<div class="status-indicator">
<img>
<t:attr name="src">img/connected-<t:transparent t:render="connected_to_at_least_one_introducer" />.png</t:attr>
<t:attr name="alt"><t:transparent t:render="connected_to_at_least_one_introducer_alt" /></t:attr>
</img>
</div>
<div t:render="introducer_description" />
</h3>
</div>
<div>
<h3>
<div class="status-indicator"><img><n:attr name="src">img/connected-<n:invisible n:render="string" n:data="connected_to_helper" />.png</n:attr><n:attr name="alt"><n:invisible n:render="string" n:data="connected_to_helper_alt" /></n:attr></img></div>
<div n:render="string" n:data="helper_description" />
<div class="status-indicator">
<img>
<t:attr name="src">img/connected-<t:transparent t:render="connected_to_helper" />.png</t:attr>
<t:attr name="alt"><t:transparent t:render="connected_to_helper_alt" /></t:attr>
</img>
</div>
<div t:render="helper_description" />
</h3>
<div class="furl" n:render="string" n:data="helper_furl_prefix" />
<div class="furl" t:render="helper_furl_prefix" />
</div>
</div><!--/span-->
<div class="span6">
<div class="span4 services">
<h3>Services</h3>
<div n:render="services" />
<div t:render="services" />
</div><!--/span-->
</div><!--/span-->
</div><!--/row-->
@ -161,61 +169,110 @@
<div class="row-fluid">
<h2>
Connected to <span n:render="string" n:data="connected_storage_servers" />
of <span n:render="string" n:data="known_storage_servers" /> known storage servers
Connected to <span t:render="connected_storage_servers" />
of <span t:render="known_storage_servers" /> known storage servers
</h2>
</div><!--/row-->
<table class="table table-striped table-bordered peer-status" n:render="sequence" n:data="services">
<!-- table with storage service connection status -->
<table class="table table-striped table-bordered peer-status" t:render="services_table">
<thead>
<tr n:pattern="header">
<tr t:render="header">
<td><h3>Nickname</h3></td>
<td><h3>Connection</h3></td>
<td><h3>Last&nbsp;RX</h3></td>
<td><h3>Last RX</h3></td>
<td><h3>Version</h3></td>
<td><h3>Available</h3></td>
</tr>
</thead>
<tr n:pattern="item" n:render="service_row">
<tr t:render="item">
<!-- Nickname -->
<td class="nickname-and-peerid">
<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>
<div class="nickname"><n:slot name="nickname"/></div>
<div class="nodeid"><n:slot name="peerid"/></div>
<div class="status-indicator">
<img>
<t:attr name="src">img/connected-<t:slot name="service_connection_status" />.png</t:attr>
<t:attr name="alt"><t:slot name="service_connection_status_alt" /></t:attr>
</img>
</div>
<div class="nickname"><t:slot name="nickname"/></div>
<div class="nodeid"><t:slot name="peerid"/></div>
</td>
<!-- Connection -->
<td class="connection-status">
<n:attr name="title"><n:slot name="details"/></n:attr>
<n:slot name="summary"/>
<t:attr name="title"><t:slot name="details"/></t:attr>
<t:slot name="summary"/>
<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"/>
<t:attr name="title"><t:slot name="service_connection_status_abs_time"/></t:attr>
<t:slot name="service_connection_status_rel_time"/>
</a>
</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>
<!-- Last RX -->
<td class="service-last-received-data">
<a class="timestamp">
<t:attr name="title"><t:slot name="last_received_data_abs_time"/></t:attr>
<t:slot name="last_received_data_rel_time"/>
</a>
</td>
<!-- Version -->
<td class="service-version">
<t:slot name="version"/>
</td>
<!-- Available -->
<td class="service-available-space">
<t:slot name="available_space"/>
</td>
</tr>
<tr t:render="empty">
<td colspan="5">You are not presently connected to any servers.</td>
</tr>
<tr n:pattern="empty"><td colspan="5">You are not presently connected to any servers.</td></tr>
</table>
<div class="row-fluid">
<h2>Connected to <span n:render="string" n:data="connected_introducers" /> of <span n:render="string" n:data="total_introducers" /> introducers</h2>
<h2>Connected to <span t:render="connected_introducers" /> of <span t:render="total_introducers" /> introducers</h2>
</div>
<table class="table table-striped table-bordered peer-status" n:render="sequence" n:data="introducers">
<!-- table with introducers status -->
<table class="table table-striped table-bordered peer-status" t:render="introducers_table">
<thead>
<tr n:pattern="header">
<tr t:pattern="header">
<td><h3>Connection</h3></td>
<td><h3>Last&nbsp;RX</h3></td>
<td><h3>Last RX</h3></td>
</tr>
</thead>
<tr n:pattern="item" n:render="introducers_row">
<tr t:render="item">
<!-- Connection -->
<td class="nickname-and-peerid">
<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="connection-status"><n:attr name="title"><n:slot name="details"/></n:attr><n:slot name="summary"/></div>
<div class="status-indicator">
<img>
<t:attr name="src">img/connected-<t:slot name="service_connection_status" />.png</t:attr>
<t:attr name="alt"><t:slot name="service_connection_status_alt" /></t:attr>
</img>
</div>
<a class="timestamp">
<t:attr name="title"><t:slot name="service_connection_status_abs_time"/></t:attr>
<t:slot name="service_connection_status_rel_time"/>
</a>
<div class="connection-status">
<t:attr name="title">
<t:slot name="details"/>
</t:attr>
<t:slot name="summary"/>
</div>
</td>
<!-- Last RX -->
<td class="service-last-received-data">
<a class="timestamp">
<t:attr name="title"><t:slot name="last_received_data_abs_time"/></t:attr>
<t:slot name="last_received_data_rel_time"/>
</a>
</td>
</tr>
<tr t:render="empty">
<td colspan="2">
No introducers are configured.
</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>
</tr>
<tr n:pattern="empty"><td colspan="2">No introducers are configured.</td></tr>
</table>
</div><!--/span-->
</div><!--/row-->
@ -223,10 +280,10 @@
<hr/>
<footer>
<p>&#169; <a href="https://tahoe-lafs.org/">Tahoe-LAFS Software Foundation 2013-2016</a></p>
<p class="minutia">Page rendered at <span n:render="data" n:data="rendered_at" /></p>
<p class="minutia" n:render="string" n:data="version"></p>
<p class="minutia">Tahoe-LAFS code imported from: <span n:render="data" n:data="import_path" /></p>
<p>&#169; <a href="https://tahoe-lafs.org/">Tahoe-LAFS Software Foundation 2013-2020</a></p>
<p class="minutia">Page rendered at <span t:render="rendered_at" /></p>
<p class="minutia" t:render="version"></p>
<p class="minutia">Tahoe-LAFS code imported from: <span t:render="import_path" /></p>
</footer>
</div><!--/.fluid-container-->