[CalendarServer-changes] [8092] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Sep 13 12:14:09 PDT 2011


Revision: 8092
          http://trac.macosforge.org/projects/calendarserver/changeset/8092
Author:   glyph at apple.com
Date:     2011-09-13 12:14:09 -0700 (Tue, 13 Sep 2011)
Log Message:
-----------
Use twisted.web.template rather than string concatenation for remaining uses of HTML.

Modified Paths:
--------------
    CalendarServer/trunk/setup.py
    CalendarServer/trunk/twext/web2/dav/static.py
    CalendarServer/trunk/twext/web2/dav/test/test_static.py
    CalendarServer/trunk/twext/web2/error.py
    CalendarServer/trunk/twext/web2/http.py
    CalendarServer/trunk/twext/web2/static.py
    CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/extensions.py
    CalendarServer/trunk/twistedcaldav/test/test_extensions.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/directory/calendar-user-proxy-principal-resource.html
    CalendarServer/trunk/twistedcaldav/directory/directory-principal-resource.html
    CalendarServer/trunk/twistedcaldav/directory-listing.html

Removed Paths:
-------------
    CalendarServer/trunk/twext/web2/dirlist.py

Property Changed:
----------------
    CalendarServer/trunk/


Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593

Modified: CalendarServer/trunk/setup.py
===================================================================
--- CalendarServer/trunk/setup.py	2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/setup.py	2011-09-13 19:14:09 UTC (rev 8092)
@@ -108,6 +108,7 @@
         packages         = find_modules(),
         package_data     = {
                              "twistedcaldav": [
+                               "*.html",
                                "zoneinfo/*.ics",
                                "zoneinfo/*/*.ics",
                                "zoneinfo/*/*/*.ics",
@@ -116,7 +117,7 @@
                              "calendarserver.webadmin": [
                                  "*.html"
                              ],
-                             "twistedcaldav": [
+                             "twistedcaldav.directory": [
                                  "*.html"
                              ],
                              "txdav.common.datastore": [

Modified: CalendarServer/trunk/twext/web2/dav/static.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/static.py	2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/dav/static.py	2011-09-13 19:14:09 UTC (rev 8092)
@@ -1,3 +1,4 @@
+# -*- test-case-name: twext.web2.dav.test.test_static -*-
 ##
 # Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
 #

Modified: CalendarServer/trunk/twext/web2/dav/test/test_static.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/test/test_static.py	2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/dav/test/test_static.py	2011-09-13 19:14:09 UTC (rev 8092)
@@ -49,7 +49,7 @@
             data = []
             d = readStream(response.stream, lambda s: data.append(str(s)))
             d.addCallback(lambda _: self.failIf(
-                'href="dir2/"' not in "".join(data),
+                'dir2/' not in "".join(data),
                 "'dir2' expected in listing: %r" % (data,)
             ))
             return d

Deleted: CalendarServer/trunk/twext/web2/dirlist.py
===================================================================
--- CalendarServer/trunk/twext/web2/dirlist.py	2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/dirlist.py	2011-09-13 19:14:09 UTC (rev 8092)
@@ -1,140 +0,0 @@
-##
-# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
-# Copyright (c) 2010 Apple Computer, Inc. All rights reserved.
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-#
-##
-
-"""Directory listing."""
-
-# system imports
-import os
-import urllib
-import stat
-import time
-
-# twisted imports
-from twext.web2 import resource, http, http_headers
-
-def formatFileSize(size):
-    if size < 1024:
-        return '%i' % size
-    elif size < (1024**2):
-        return '%iK' % (size / 1024)
-    elif size < (1024**3):
-        return '%iM' % (size / (1024**2))
-    else:
-        return '%iG' % (size / (1024**3))
-
-class DirectoryLister(resource.Resource):
-    def __init__(self, pathname, dirs=None,
-                 contentTypes={},
-                 contentEncodings={},
-                 defaultType='text/html'):
-        self.contentTypes = contentTypes
-        self.contentEncodings = contentEncodings
-        self.defaultType = defaultType
-        # dirs allows usage of the File to specify what gets listed
-        self.dirs = dirs
-        self.path = pathname
-        resource.Resource.__init__(self)
-
-    def data_listing(self, request, data):
-        if self.dirs is None:
-            directory = os.listdir(self.path)
-            directory.sort()
-        else:
-            directory = self.dirs
-
-        files = []
-
-        for path in directory:
-            url = urllib.quote(path, '/')
-            fullpath = os.path.join(self.path, path)
-            try:
-                st = os.stat(fullpath)
-            except OSError:
-                continue
-            if stat.S_ISDIR(st.st_mode):
-                url = url + '/'
-                files.append({
-                    'link': url,
-                    'linktext': path + "/",
-                    'size': '',
-                    'type': '-',
-                    'lastmod': time.strftime("%Y-%b-%d %H:%M", time.localtime(st.st_mtime))
-                    })
-            else:
-                from twext.web2.static import getTypeAndEncoding
-                mimetype, encoding = getTypeAndEncoding(
-                    path,
-                    self.contentTypes, self.contentEncodings, self.defaultType)
-                
-                filesize = st.st_size
-                files.append({
-                    'link': url,
-                    'linktext': path,
-                    'size': formatFileSize(filesize),
-                    'type': mimetype,
-                    'lastmod': time.strftime("%Y-%b-%d %H:%M", time.localtime(st.st_mtime))
-                    })
-
-        return files
-
-    def __repr__(self):  
-        return '<DirectoryLister of %r>' % self.path
-        
-    __str__ = __repr__
-
-
-    def render(self, request):
-        title = "Directory listing for %s" % urllib.unquote(request.path)
-    
-        s= """<html><head><title>%s</title><style>
-          th, .even td, .odd td { padding-right: 0.5em; font-family: monospace}
-          .even-dir { background-color: #efe0ef }
-          .even { background-color: #eee }
-          .odd-dir {background-color: #f0d0ef }
-          .odd { background-color: #dedede }
-          .icon { text-align: center }
-          .listing {
-              margin-left: auto;
-              margin-right: auto;
-              width: 50%%;
-              padding: 0.1em;
-              }
-
-          body { border: 0; padding: 0; margin: 0; background-color: #efefef;}
-          h1 {padding: 0.1em; background-color: #777; color: white; border-bottom: thin white dashed;}
-</style></head><body><div class="directory-listing"><h1>%s</h1>""" % (title,title)
-        s+="<table>"
-        s+="<tr><th>Filename</th><th>Size</th><th>Last Modified</th><th>File Type</th></tr>"
-        even = False
-        for row in self.data_listing(request, None):
-            s+='<tr class="%s">' % (even and 'even' or 'odd',)
-            s+='<td><a href="%(link)s">%(linktext)s</a></td><td align="right">%(size)s</td><td>%(lastmod)s</td><td>%(type)s</td></tr>' % row
-            even = not even
-                
-        s+="</table></div></body></html>"
-        response = http.Response(200, {}, s)
-        response.headers.setHeader("content-type", http_headers.MimeType('text', 'html'))
-        return response
-
-__all__ = ['DirectoryLister']

Modified: CalendarServer/trunk/twext/web2/error.py
===================================================================
--- CalendarServer/trunk/twext/web2/error.py	2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/error.py	2011-09-13 19:14:09 UTC (rev 8092)
@@ -1,3 +1,4 @@
+# -*- test-case-name: twext.web2.test.test_log -*-
 ##
 # Copyright (c) 2001-2004 Twisted Matrix Laboratories.
 # Copyright (c) 2010 Apple Computer, Inc. All rights reserved.
@@ -27,12 +28,24 @@
 """
 
 from twext.web2 import stream, http_headers
-from twext.web2.responsecode import *
+from twext.web2.responsecode import (
+    MOVED_PERMANENTLY, FOUND, SEE_OTHER, USE_PROXY, TEMPORARY_REDIRECT,
+    BAD_REQUEST, UNAUTHORIZED, PAYMENT_REQUIRED, FORBIDDEN, NOT_FOUND,
+    NOT_ALLOWED, NOT_ACCEPTABLE, PROXY_AUTH_REQUIRED, REQUEST_TIMEOUT, CONFLICT,
+    GONE, LENGTH_REQUIRED, PRECONDITION_FAILED, REQUEST_ENTITY_TOO_LARGE,
+    REQUEST_URI_TOO_LONG, UNSUPPORTED_MEDIA_TYPE,
+    REQUESTED_RANGE_NOT_SATISFIABLE, EXPECTATION_FAILED, INTERNAL_SERVER_ERROR,
+    NOT_IMPLEMENTED, BAD_GATEWAY, SERVICE_UNAVAILABLE, GATEWAY_TIMEOUT,
+    HTTP_VERSION_NOT_SUPPORTED, INSUFFICIENT_STORAGE_SPACE, NOT_EXTENDED,
+    RESPONSES,
+)
 
+from twisted.web.template import Element, flattenString, XMLString, renderer
+
 # 300 - Should include entity with choices
 # 301 -
 # 304 - Must include Date, ETag, Content-Location, Expires, Cache-Control, Vary.
-# 
+
 # 401 - Must include WWW-Authenticate.
 # 405 - Must include Allow.
 # 406 - Should include entity describing allowable characteristics
@@ -44,80 +57,190 @@
 ERROR_MESSAGES = {
     # 300
     # no MULTIPLE_CHOICES
-    MOVED_PERMANENTLY: 'The document has permanently moved <a href="%(location)s">here</a>.',
-    FOUND: 'The document has temporarily moved <a href="%(location)s">here</a>.',
-    SEE_OTHER: 'The results are available <a href="%(location)s">here</a>.',
+    MOVED_PERMANENTLY:
+    'The document has permanently moved <a>here<t:attr name="href">'
+    '<t:slot name="location" /></t:attr></a>.',
+    FOUND:
+    'The document has temporarily moved <a>here<t:attr name="href">'
+    '<t:slot name="location" /></t:attr></a>.',
+    SEE_OTHER:
+    'The results are available <a>here<t:attr name="href">'
+    '<t:slot name="location" /></t:attr></a>.',
     # no NOT_MODIFIED
-    USE_PROXY: "Access to this resource must be through the proxy %(location)s.",
+    USE_PROXY:
+    'Access to this resource must be through the proxy '
+    '<t:slot name="location" />.',
     # 306 unused
-    TEMPORARY_REDIRECT: 'The document has temporarily moved <a href="%(location)s">here</a>.',
+    TEMPORARY_REDIRECT:
+    'The document has temporarily moved <a><t:attr name="href">'
+    '<t:slot name="location" /></t:attr>here</a>.',
 
     # 400
-    BAD_REQUEST: "Your browser sent an invalid request.",
-    UNAUTHORIZED: "You are not authorized to view the resource at %(uri)s. Perhaps you entered a wrong password, or perhaps your browser doesn't support authentication.",
-    PAYMENT_REQUIRED: "Payment Required (useful result code, this...).",
-    FORBIDDEN: "You don't have permission to access %(uri)s.",
-    NOT_FOUND: "The resource %(uri)s cannot be found.",
-    NOT_ALLOWED: "The requested method %(method)s is not supported by %(uri)s.",
-    NOT_ACCEPTABLE: "No representation of %(uri)s that is acceptable to your client could be found.",
-    PROXY_AUTH_REQUIRED: "You are not authorized to view the resource at %(uri)s. Perhaps you entered a wrong password, or perhaps your browser doesn't support authentication.",
-    REQUEST_TIMEOUT: "Server timed out waiting for your client to finish sending the HTTP request.",
-    CONFLICT: "Conflict (?)",
-    GONE: "The resource %(uri)s has been permanently removed.",
-    LENGTH_REQUIRED: "The resource %(uri)s requires a Content-Length header.",
-    PRECONDITION_FAILED: "A precondition evaluated to false.",
-    REQUEST_ENTITY_TOO_LARGE: "The provided request entity data is too longer than the maximum for the method %(method)s at %(uri)s.",
-    REQUEST_URI_TOO_LONG: "The request URL is longer than the maximum on this server.",
-    UNSUPPORTED_MEDIA_TYPE: "The provided request data has a format not understood by the resource at %(uri)s.",
-    REQUESTED_RANGE_NOT_SATISFIABLE: "None of the ranges given in the Range request header are satisfiable by the resource %(uri)s.",
-    EXPECTATION_FAILED: "The server does support one of the expectations given in the Expect header.",
+    BAD_REQUEST:
+    'Your browser sent an invalid request.',
+    UNAUTHORIZED:
+    'You are not authorized to view the resource at <t:slot name="uri" />. '
+    "Perhaps you entered a wrong password, or perhaps your browser doesn't "
+    'support authentication.',
+    PAYMENT_REQUIRED:
+    'Payment Required (useful result code, this...).',
+    FORBIDDEN:
+    'You don\'t have permission to access <t:slot name="uri" />.',
+    NOT_FOUND:
+    'The resource <t:slot name="uri" /> cannot be found.',
+    NOT_ALLOWED:
+    'The requested method <t:slot name="method" /> is not supported by '
+    '<t:slot name="uri" />.',
+    NOT_ACCEPTABLE:
+    'No representation of <t:slot name="uri" /> that is acceptable to your '
+    'client could be found.',
+    PROXY_AUTH_REQUIRED:
+    'You are not authorized to view the resource at <t:slot name="uri" />. '
+    'Perhaps you entered a wrong password, or perhaps your browser doesn\'t '
+    'support authentication.',
+    REQUEST_TIMEOUT:
+    'Server timed out waiting for your client to finish sending the request.',
+    CONFLICT:
+    'Conflict (?)',
+    GONE:
+    'The resource <t:slot name="uri" /> has been permanently removed.',
+    LENGTH_REQUIRED:
+    'The resource <t:slot name="uri" /> requires a Content-Length header.',
+    PRECONDITION_FAILED:
+    'A precondition evaluated to false.',
+    REQUEST_ENTITY_TOO_LARGE:
+    'The provided request entity data is too longer than the maximum for '
+    'the method <t:slot name="method" /> at <t:slot name="uri" />.',
+    REQUEST_URI_TOO_LONG:
+    'The request URL is longer than the maximum on this server.',
+    UNSUPPORTED_MEDIA_TYPE:
+    'The provided request data has a format not understood by the resource '
+    'at <t:slot name="uri" />.',
+    REQUESTED_RANGE_NOT_SATISFIABLE:
+    'None of the ranges given in the Range request header are satisfiable by '
+    'the resource <t:slot name="uri" />.',
+    EXPECTATION_FAILED:
+    'The server does support one of the expectations given in the Expect '
+    'header.',
 
     # 500
-    INTERNAL_SERVER_ERROR: "An internal error occurred trying to process your request. Sorry.",
-    NOT_IMPLEMENTED: "Some functionality requested is not implemented on this server.",
-    BAD_GATEWAY: "An upstream server returned an invalid response.",
-    SERVICE_UNAVAILABLE: "This server cannot service your request becaues it is overloaded.",
-    GATEWAY_TIMEOUT: "An upstream server is not responding.",
-    HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported.",
-    INSUFFICIENT_STORAGE_SPACE: "There is insufficient storage space available to perform that request.",
-    NOT_EXTENDED: "This server does not support the a mandatory extension requested."
+    INTERNAL_SERVER_ERROR:
+    'An internal error occurred trying to process your request. Sorry.',
+    NOT_IMPLEMENTED:
+    'Some functionality requested is not implemented on this server.',
+    BAD_GATEWAY:
+    'An upstream server returned an invalid response.',
+    SERVICE_UNAVAILABLE:
+    'This server cannot service your request becaues it is overloaded.',
+    GATEWAY_TIMEOUT:
+    'An upstream server is not responding.',
+    HTTP_VERSION_NOT_SUPPORTED:
+    'HTTP Version not supported.',
+    INSUFFICIENT_STORAGE_SPACE:
+    'There is insufficient storage space available to perform that request.',
+    NOT_EXTENDED:
+    'This server does not support the a mandatory extension requested.'
 }
 
-# Is there a good place to keep this function?
-def _escape(original):
-    if original is None:
-        return None
-    return original.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
 
+
+class DefaultErrorElement(Element):
+    """
+    An L{ErrorElement} is an L{Element} that renders some HTML for the default
+    rendering of an error page.
+    """
+
+    loader = XMLString("""
+    <html xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+          t:render="error">
+          <head>
+              <title><t:slot name="code"/> <t:slot name="title"/></title>
+          </head>
+        <body>
+            <h1><t:slot name="title" /></h1>
+            <t:slot name="message" />
+        </body>
+    </html>
+    """)
+
+    def __init__(self, request, response):
+        super(DefaultErrorElement, self).__init__()
+        self.request = request
+        self.response = response
+
+
+    @renderer
+    def error(self, request, tag):
+        """
+        Top-level renderer for page.
+        """
+        return tag.fillSlots(
+            code=str(self.response.code),
+            title=RESPONSES.get(self.response.code),
+            message=self.loadMessage(self.response.code).fillSlots(
+                uri=self.request.uri,
+                location=self.response.headers.getHeader('location'),
+                method=self.request.method,
+            )
+        )
+
+
+    def loadMessage(self, code):
+        tag = XMLString(('<t:transparent xmlns:t="http://twistedmatrix.com/'
+                   'ns/twisted.web.template/0.1">') +
+                  ERROR_MESSAGES.get(code, "") +
+                    '</t:transparent>').load()[0]
+        return tag
+
+
+
 def defaultErrorHandler(request, response):
+    """
+    Handle errors which do not have any stream (i.e. output) associated with
+    them, so that users will see a nice message in their browser.
+
+    This is used as a response filter in L{twext.web2.server.Request}.
+    """
     if response.stream is not None:
         # Already got an error message
         return response
+
     if response.code < 300:
         # We only do error messages
         return response
-    
+
     message = ERROR_MESSAGES.get(response.code, None)
     if message is None:
         # No message specified for that code
         return response
-    
+
     message = message % {
-        'uri':_escape(request.uri),
-        'location':_escape(response.headers.getHeader('location')),
-        'method':_escape(request.method)
-        }
+        'uri': request.uri,
+        'location': response.headers.getHeader('location'),
+        'method': request.method,
+    }
 
-    title = RESPONSES.get(response.code, "")
-    body = ("<html><head><title>%d %s</title></head>"
-            "<body><h1>%s</h1>%s</body></html>") % (
-        response.code, title, title, message)
-    
-    response.headers.setHeader("content-type", http_headers.MimeType('text', 'html', {'charset':'utf-8'}))
+    data = []
+    error = []
+
+    (flattenString(request, DefaultErrorElement(request, response))
+     .addCallbacks(data.append, error.append))
+
+    # No deferreds from our renderers above, so this has always already fired.
+    if data:
+        subtype = 'html'
+        body = data[0]
+    else:
+        subtype = 'error'
+        body = 'Error in default error handler:\n' + error[0].getTraceback()
+
+    ctype = http_headers.MimeType('text', subtype, {'charset':'utf-8'})
+    response.headers.setHeader("content-type", ctype)
     response.stream = stream.MemoryStream(body)
-    
     return response
+
 defaultErrorHandler.handleErrors = True
 
 
 __all__ = ['defaultErrorHandler',]
+

Modified: CalendarServer/trunk/twext/web2/http.py
===================================================================
--- CalendarServer/trunk/twext/web2/http.py	2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/http.py	2011-09-13 19:14:09 UTC (rev 8092)
@@ -33,10 +33,11 @@
 #        import traceback; log.msg(''.join(traceback.format_stack()))
 
 import time
-import cgi
 
 from twisted.internet import interfaces, error
 from twisted.python import components
+from twisted.web.template import Element, XMLString, renderer, flattenString
+
 from zope.interface import implements
 
 from twext.python.log import Logger
@@ -138,11 +139,39 @@
         return "<%s.%s code=%d, streamlen=%s>" % (self.__module__, self.__class__.__name__, self.code, streamlen)
 
 
+class StatusResponseElement(Element):
+    """
+    Render the HTML for a L{StatusResponse}
+    """
+
+    loader = XMLString("""<html
+        xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+        t:render="response"><head><title><t:slot name="title"
+                /></title></head><body><h1><t:slot name="title"
+                /></h1><p><t:slot name="description"
+            /></p></body></html>""")
+
+    def __init__(self, title, description):
+        super(StatusResponseElement, self).__init__()
+        self.title = title
+        self.description = description
+
+
+    @renderer
+    def response(self, request, tag):
+        """
+        Top-level renderer.
+        """
+        return tag.fillSlots(title=self.title, description=self.description)
+
+
+
 class StatusResponse (Response):
     """
-    A L{Response} object which simply contains a status code and a description of
-    what happened.
+    A L{Response} object which simply contains a status code and a description
+    of what happened.
     """
+
     def __init__(self, code, description, title=None):
         """
         @param code: a response code in L{responsecode.RESPONSES}.
@@ -151,36 +180,27 @@
             to C{responsecode.RESPONSES[code]}.
         """
         if title is None:
-            title = cgi.escape(responsecode.RESPONSES[code])
+            title = responsecode.RESPONSES[code]
 
-        output = "".join((
-            "<html>",
-            "<head>",
-            "<title>%s</title>" % (title,),
-            "</head>",
-            "<body>",
-            "<h1>%s</h1>" % (title,),
-            "<p>%s</p>" % (cgi.escape(description),),
-            "</body>",
-            "</html>",
-        ))
+        element = StatusResponseElement(title, description)
+        out = []
+        flattenString(None, element).addCallback(out.append)
 
-        if type(output) == unicode:
-            output = output.encode("utf-8")
-            mime_params = {"charset": "utf-8"}
-        else:
-            mime_params = {}
+        mime_params = {"charset": "utf-8"}
+        super(StatusResponse, self).__init__(code=code, stream=out[0])
 
-        super(StatusResponse, self).__init__(code=code, stream=output)
+        self.headers.setHeader(
+            "content-type", http_headers.MimeType("text", "html", mime_params)
+        )
 
-        self.headers.setHeader("content-type", http_headers.MimeType("text", "html", mime_params))
-
         self.description = description
 
+
     def __repr__(self):
         return "<%s %s %s>" % (self.__class__.__name__, self.code, self.description)
 
 
+
 class RedirectResponse (StatusResponse):
     """
     A L{Response} object that contains a redirect to another network location.

Modified: CalendarServer/trunk/twext/web2/static.py
===================================================================
--- CalendarServer/trunk/twext/web2/static.py	2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twext/web2/static.py	2011-09-13 19:14:09 UTC (rev 8092)
@@ -34,7 +34,7 @@
 
 # Sibling Imports
 from twext.web2 import http_headers, resource
-from twext.web2 import http, iweb, stream, responsecode, server, dirlist
+from twext.web2 import http, iweb, stream, responsecode, server
 from twext.web2.http import HTTPError
 
 # Twisted Imports
@@ -189,10 +189,9 @@
     be files underneath that directory. This provides access to an entire
     filesystem tree with a single Resource.
 
-    If you map the URL 'http://server/FILE' to a resource created as
-    File('/tmp'), then http://server/FILE/ will return an HTML-formatted
-    listing of the /tmp/ directory, and http://server/FILE/foo/bar.html will
-    return the contents of /tmp/foo/bar.html .
+    If you map the URL C{http://server/FILE} to a resource created as
+    File('/tmp'), C{http://server/FILE/foo/bar.html} will return the contents of
+    C{/tmp/foo/bar.html} .
     """
     implements(iweb.IResource)
 
@@ -418,14 +417,12 @@
                     # Render from the index file
                     standin = self.createSimilarFile(ifp.path)
                 else:
-                    # Render from a DirectoryLister
-                    standin = dirlist.DirectoryLister(
-                        self.fp.path,
-                        self.listChildren(),
-                        self.contentTypes,
-                        self.contentEncodings,
-                        self.defaultType
-                    )
+                    # Directory listing is in twistedcaldav.extensions
+                    standin = Data(
+                        "\n".join(["Directory: " + str(req.path), "---"] +
+                                  [x.basename() + ("/" if x.isdir() else "")
+                                   for x in self.fp.children()]),
+                        "text/plain")
                 return standin.render(req)
 
         try:

Copied: CalendarServer/trunk/twistedcaldav/directory/calendar-user-proxy-principal-resource.html (from rev 8091, CalendarServer/branches/users/glyph/other-html/twistedcaldav/directory/calendar-user-proxy-principal-resource.html)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendar-user-proxy-principal-resource.html	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/calendar-user-proxy-principal-resource.html	2011-09-13 19:14:09 UTC (rev 8092)
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<t:transparent xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+     t:render="principal">
+     <div class="directory-listing">
+    <h1>Proxy Principal Details</h1>
+<pre><blockquote>Directory Information
+---------------------
+Directory GUID: <t:slot name="directoryGUID"></t:slot>
+Realm: <t:slot name="realm"></t:slot>
+
+Parent Principal Information
+---------------------
+GUID: <t:slot name="guid"></t:slot>
+Record type: <t:slot name="recordType"></t:slot>
+Short names: <t:slot name="shortNames"></t:slot>
+Full name: <t:slot name="fullName"></t:slot>
+Principal UID: <t:slot name="principalUID"></t:slot>
+Principal URL: <t:slot name="principalURL"></t:slot>
+
+Proxy Principal Information
+---------------------
+Principal UID: <t:slot name="proxyPrincipalUID"></t:slot>
+Principal URL: <t:slot name="proxyPrincipalURL"></t:slot>
+
+Alternate URIs:
+<t:slot name="alternateURIs"></t:slot>
+Group members:
+<t:slot name="groupMembers"></t:slot>
+Group memberships:
+<t:slot name="groupMemberships"></t:slot></blockquote></pre></div>
+    </t:transparent>

Modified: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2011-09-13 19:14:09 UTC (rev 8092)
@@ -1,3 +1,4 @@
+# -*- test-case-name: twistedcaldav.directory.test.test_proxyprincipalmembers -*-
 ##
 # Copyright (c) 2006-2010 Apple Inc. All rights reserved.
 #
@@ -39,6 +40,12 @@
 
 from twext.python.log import Logger, LoggingMixIn
 
+from twisted.web.template import XMLFile, Element, renderer
+from twisted.python.modules import getModule
+from twistedcaldav.extensions import DirectoryElement
+from twistedcaldav.directory.principal import formatLink
+from twistedcaldav.directory.principal import formatLinks
+from twistedcaldav.directory.principal import formatPrincipals
 
 from twistedcaldav.config import config, fullServerPath
 from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin,\
@@ -49,6 +56,7 @@
 from twistedcaldav.memcacher import Memcacher
 from twistedcaldav.resource import CalDAVComplianceMixIn
 
+thisModule = getModule(__name__)
 log = Logger()
 
 class PermissionsMixIn (ReadOnlyWritePropertiesResourceMixIn):
@@ -79,14 +87,79 @@
 
         return davxml.ACL(*aces)
 
-    def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
+
+    def accessControlList(self, request, inheritance=True, expanding=False,
+                          inherited_aces=None):
         # Permissions here are fixed, and are not subject to inheritance rules, etc.
         return succeed(self.defaultAccessControlList())
 
-class CalendarUserProxyPrincipalResource (CalDAVComplianceMixIn, PermissionsMixIn, DAVResourceWithChildrenMixin, DAVPrincipalResource):
+
+
+class ProxyPrincipalDetailElement(Element):
     """
+    A L{ProxyPrincipalDetailElement} is an L{Element} that can render the
+    details of a L{CalendarUserProxyPrincipalResource}.
+    """
+
+    loader = XMLFile(thisModule.filePath.sibling(
+        "calendar-user-proxy-principal-resource.html").open()
+    )
+
+    def __init__(self, resource):
+        super(ProxyPrincipalDetailElement, self).__init__()
+        self.resource = resource
+
+
+    @renderer
+    def principal(self, request, tag):
+        """
+        Top-level renderer in the template.
+        """
+        record = self.resource.parent.record
+        resource = self.resource
+        parent = self.resource.parent
+        return tag.fillSlots(
+            directoryGUID=record.service.guid,
+            realm=record.service.realmName,
+            guid=record.guid,
+            recordType=record.recordType,
+            shortNames=record.shortNames,
+            fullName=record.fullName,
+            principalUID=parent.principalUID(),
+            principalURL=formatLink(parent.principalURL()),
+            proxyPrincipalUID=resource.principalUID(),
+            proxyPrincipalURL=formatLink(resource.principalURL()),
+            alternateURIs=formatLinks(resource.alternateURIs()),
+            groupMembers=resource.groupMembers().addCallback(formatPrincipals),
+            groupMemberships=resource.groupMemberships().addCallback(
+                formatPrincipals
+            ),
+        )
+
+
+
+class ProxyPrincipalElement(DirectoryElement):
+    """
+    L{ProxyPrincipalElement} is a renderer for a
+    L{CalendarUserProxyPrincipalResource}.
+    """
+
+    @renderer
+    def resourceDetail(self, request, tag):
+        """
+        Render the proxy principal's details.
+        """
+        return ProxyPrincipalDetailElement(self.resource)
+
+
+
+class CalendarUserProxyPrincipalResource (
+        CalDAVComplianceMixIn, PermissionsMixIn, DAVResourceWithChildrenMixin,
+        DAVPrincipalResource):
+    """
     Calendar user proxy principal resource.
     """
+
     def __init__(self, parent, proxyType):
         """
         @param parent: the parent of this resource.
@@ -102,28 +175,27 @@
         super(CalendarUserProxyPrincipalResource, self).__init__()
         DAVResourceWithChildrenMixin.__init__(self)
 
-        self.parent      = parent
-        self.proxyType   = proxyType
-        self.pcollection = self.parent.parent.parent # FIXME: if this is supposed to be public, it needs a better name
-        self._url        = url
+        self.parent          = parent
+        self.proxyType       = proxyType
+        self._url            = url
 
-        # Not terribly useful at present because we don't have a way
-        # to map a GUID back to the correct principal.
-        #self.guid = uuidFromName(self.parent.principalUID(), proxyType)
+        # FIXME: if this is supposed to be public, it needs a better name:
+        self.pcollection     = self.parent.parent.parent
 
-        # Principal UID is parent's GUID plus the proxy type; this we
-        # can easily map back to a principal.
-        self.uid = "%s#%s" % (self.parent.principalUID(), proxyType)
-
+        # Principal UID is parent's GUID plus the proxy type; this we can easily
+        # map back to a principal.
+        self.uid             = "%s#%s" % (self.parent.principalUID(), proxyType)
         self._alternate_urls = tuple(
             joinURL(url, proxyType) + slash
             for url in parent.alternateURIs()
             if url.startswith("/")
         )
 
+
     def __str__(self):
         return "%s [%s]" % (self.parent, self.proxyType)
 
+
     def _index(self):
         """
         Return the SQL database for this group principal.
@@ -160,24 +232,29 @@
             self._dead_properties = NonePropertyStore(self)
         return self._dead_properties
 
+
     def writeProperty(self, property, request):
         assert isinstance(property, davxml.WebDAVElement)
 
         if property.qname() == (dav_namespace, "group-member-set"):
             return self.setGroupMemberSet(property, request)
 
-        return super(CalendarUserProxyPrincipalResource, self).writeProperty(property, request)
+        return super(CalendarUserProxyPrincipalResource, self).writeProperty(
+            property, request)
 
+
     @inlineCallbacks
     def setGroupMemberSet(self, new_members, request):
-        # FIXME: as defined right now it is not possible to specify a calendar-user-proxy group as
-        # a member of any other group since the directory service does not know how to lookup
-        # these special resource UIDs.
+        # FIXME: as defined right now it is not possible to specify a
+        # calendar-user-proxy group as a member of any other group since the
+        # directory service does not know how to lookup these special resource
+        # UIDs.
         #
-        # Really, c-u-p principals should be treated the same way as any other principal, so
-        # they should be allowed as members of groups.
+        # Really, c-u-p principals should be treated the same way as any other
+        # principal, so they should be allowed as members of groups.
         #
-        # This implementation now raises an exception for any principal it cannot find.
+        # This implementation now raises an exception for any principal it
+        # cannot find.
 
         # Break out the list into a set of URIs.
         members = [str(h) for h in new_members.children]
@@ -191,29 +268,31 @@
             if principal is None or principal.principalURL() != uri:
                 raise HTTPError(StatusResponse(
                     responsecode.BAD_REQUEST,
-                    "Attempt to use a non-existent principal %s as a group member of %s." % (uri, self.principalURL(),)
+                    "Attempt to use a non-existent principal %s "
+                    "as a group member of %s." % (uri, self.principalURL(),)
                 ))
             principals.append(principal)
             newUIDs.add(principal.principalUID())
 
         # Get the old set of UIDs
         oldUIDs = (yield self._index().getMembers(self.uid))
-        
+
         # Change membership
         yield self.setGroupMemberSetPrincipals(principals)
-        
+
         # Invalidate the primary principal's cache, and any principal's whose
         # membership status changed
         yield self.parent.cacheNotifier.changed()
-        
+
         changedUIDs = newUIDs.symmetric_difference(oldUIDs)
         for uid in changedUIDs:
             principal = self.pcollection.principalForUID(uid)
             if principal:
                 yield principal.cacheNotifier.changed()
-            
+
         returnValue(True)
 
+
     def setGroupMemberSetPrincipals(self, principals):
         # Map the principals to UIDs.
         return self._index().setGroupMembers(
@@ -225,55 +304,13 @@
     # HTTP
     ##
 
-    def renderDirectoryBody(self, request):
-        # FIXME: Too much code duplication here from principal.py
-        from twistedcaldav.directory.principal import format_list, format_principals, format_link
+    def htmlElement(self):
+        """
+        Customize HTML display of proxy groups.
+        """
+        return ProxyPrincipalElement(self)
 
-        closure = {}
 
-        d = super(CalendarUserProxyPrincipalResource, self).renderDirectoryBody(request)
-        d.addCallback(lambda output: closure.setdefault("output", output))
-
-        d.addCallback(lambda _: self.groupMembers())
-        d.addCallback(lambda members: closure.setdefault("members", members))
-
-        d.addCallback(lambda _: self.groupMemberships())
-        d.addCallback(lambda memberships: closure.setdefault("memberships", memberships))
-        
-        d.addCallback(
-            lambda _: "".join((
-                """<div class="directory-listing">"""
-                """<h1>Principal Details</h1>"""
-                """<pre><blockquote>"""
-                """Directory Information\n"""
-                """---------------------\n"""
-                """Directory GUID: %s\n"""         % (self.parent.record.service.guid,),
-                """Realm: %s\n"""                  % (self.parent.record.service.realmName,),
-                """\n"""
-                """Parent Principal Information\n"""
-                """---------------------\n"""
-                """GUID: %s\n"""                   % (self.parent.record.guid,),
-                """Record type: %s\n"""            % (self.parent.record.recordType,),
-                """Short names: %s\n"""            % (",".join(self.parent.record.shortNames,)),
-                """Full name: %s\n"""              % (self.parent.record.fullName,),
-                """Principal UID: %s\n"""          % (self.parent.principalUID(),),
-                """Principal URL: %s\n"""          % (format_link(self.parent.principalURL()),),
-                """\n"""
-                """Proxy Principal Information\n"""
-                """---------------------\n"""
-               #"""GUID: %s\n"""                   % (self.guid,),
-                """Principal UID: %s\n"""          % (self.principalUID(),),
-                """Principal URL: %s\n"""          % (format_link(self.principalURL()),),
-                """\nAlternate URIs:\n"""          , format_list(format_link(u) for u in self.alternateURIs()),
-                """\nGroup members:\n"""           , format_principals(closure["members"]),
-                """\nGroup memberships:\n"""       , format_principals(closure["memberships"]),
-                """</pre></blockquote></div>""",
-                closure["output"]
-            ))
-        )
-
-        return d
-
     ##
     # DAV
     ##
@@ -281,6 +318,7 @@
     def displayName(self):
         return self.proxyType
 
+
     ##
     # ACL
     ##

Copied: CalendarServer/trunk/twistedcaldav/directory/directory-principal-resource.html (from rev 8091, CalendarServer/branches/users/glyph/other-html/twistedcaldav/directory/directory-principal-resource.html)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory-principal-resource.html	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/directory-principal-resource.html	2011-09-13 19:14:09 UTC (rev 8092)
@@ -0,0 +1,41 @@
+<div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+    t:render="principal"  class="directory-listing">
+    <h1>Principal Details</h1>
+    <pre><blockquote>Directory Information
+---------------------
+Directory GUID: <t:slot name="directoryGUID"/>
+Realm: <t:slot name="realm"/>
+<t:transparent t:render="serversEnabled">Hosted-At: <t:slot name="hostedAt"/>
+Partition: <t:slot name="partition"/></t:transparent>
+Principal Information
+---------------------
+GUID: <t:slot name="principalGUID"/>
+Record type: <t:slot name="recordType"/>
+Short names: <t:slot name="shortNames"/>
+Security Identities: <t:slot name="securityIDs"/>
+Full name: <t:slot name="fullName"/>
+First name: <t:slot name="firstName"/>
+Last name: <t:slot name="lastName"/>
+Email addresses:
+<t:slot name="emailAddresses" />Principal UID: <t:slot name="principalUID"/>
+Principal URL: <t:slot name="principalURL"/>
+
+Alternate URIs:
+<t:slot name="alternateURIs"/>
+Group members:
+<t:slot name="groupMembers"/>
+Group memberships:
+<t:slot name="groupMemberships"/>
+Read-write Proxy For:
+<t:slot name="readWriteProxyFor"/>
+Read-only Proxy For:
+<t:slot name="readOnlyProxyFor"/><t:transparent
+t:render="extra"><t:transparent t:render="enabledForCalendaring">
+Calendar Homes:
+<t:slot name="calendarHomes" />
+Calendar user addresses:
+<t:slot name="calendarUserAddresses" /></t:transparent><t:transparent
+t:render="enabledForAddressBooks">
+Address Book homes:
+<t:slot name="addressBookHomes"
+/></t:transparent></t:transparent></blockquote></pre></div>

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2011-09-13 19:14:09 UTC (rev 8092)
@@ -26,12 +26,8 @@
     "DirectoryPrincipalUIDProvisioningResource",
     "DirectoryPrincipalResource",
     "DirectoryCalendarPrincipalResource",
-    "format_list",
-    "format_principals",
-    "format_link",
 ]
 
-from cgi import escape
 from urllib import unquote
 from urlparse import urlparse
 
@@ -39,6 +35,8 @@
 from twisted.python.failure import Failure
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.defer import succeed
+from twisted.web.template import XMLFile, Element, renderer, tags
+
 from twext.web2.auth.digest import DigestedCredentials
 from twext.web2 import responsecode
 from twext.web2.http import HTTPError
@@ -50,29 +48,37 @@
 
 try:
     from twistedcaldav.authkerb import NegotiateCredentials
+    NegotiateCredentials # sigh, pyflakes
 except ImportError:
     NegotiateCredentials = None
+from twisted.python.modules import getModule
 from twistedcaldav.config import config
 from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
-from twistedcaldav.directory import calendaruserproxy
-from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyPrincipalResource
+
+from twistedcaldav.extensions import DirectoryElement
+
 from twistedcaldav.directory.common import uidsResourceName
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
 from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVPrincipalResource,\
     DAVResourceWithChildrenMixin
-from twistedcaldav.resource import CalendarPrincipalCollectionResource, CalendarPrincipalResource
+from twistedcaldav.resource import (
+    CalendarPrincipalCollectionResource, CalendarPrincipalResource
+)
 from twistedcaldav.directory.idirectory import IDirectoryService
 from twistedcaldav import caldavxml, customxml
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
 
+thisModule = getModule(__name__)
 log = Logger()
 
+
 class PermissionsMixIn (ReadOnlyResourceMixIn):
     def defaultAccessControlList(self):
         return authReadACL
 
-    def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
+    def accessControlList(self, request, inheritance=True, expanding=False,
+                          inherited_aces=None):
 
         return succeed(self.defaultAccessControlList())
 
@@ -487,8 +493,172 @@
     def principalCollections(self):
         return self.parent.principalCollections()
 
-class DirectoryPrincipalResource (PropfindCacheMixin, PermissionsMixIn, DAVPrincipalResource):
+
+
+class DirectoryPrincipalDetailElement(Element):
     """
+    Element that can render the details of a
+    L{CalendarUserDirectoryPrincipalResource}.
+    """
+
+    loader = XMLFile(thisModule.filePath.sibling(
+        "directory-principal-resource.html").open()
+    )
+
+    def __init__(self, resource):
+        super(DirectoryPrincipalDetailElement, self).__init__()
+        self.resource = resource
+
+
+    @renderer
+    def serversEnabled(self, request, tag):
+        """
+        Renderer for when servers are enabled.
+        """
+        if not config.Servers.Enabled:
+            return ""
+        record = self.resource.record
+        return tag.fillSlots(
+            hostedAt=str(record.serverURI()),
+            partition=str(record.effectivePartitionID()),
+        )
+
+
+    @renderer
+    def principal(self, request, tag):
+        """
+        Top-level renderer in the template.
+        """
+        record = self.resource.record
+        return tag.fillSlots(
+            directoryGUID=str(record.service.guid),
+            realm=str(record.service.realmName),
+            principalGUID=str(record.guid),
+            recordType=str(record.recordType),
+            shortNames=",".join(record.shortNames),
+            securityIDs=",".join(record.authIDs),
+            fullName=str(record.fullName),
+            firstName=str(record.firstName),
+            lastName=str(record.lastName),
+            emailAddresses=formatList(record.emailAddresses),
+            principalUID=str(self.resource.principalUID()),
+            principalURL=formatLink(self.resource.principalURL()),
+            alternateURIs=formatLinks(self.resource.alternateURIs()),
+            groupMembers=self.resource.groupMembers().addCallback(
+                formatPrincipals
+            ),
+            groupMemberships=self.resource.groupMemberships().addCallback(
+                formatPrincipals
+            ),
+            readWriteProxyFor=self.resource.proxyFor(True).addCallback(
+                formatPrincipals
+            ),
+            readOnlyProxyFor=self.resource.proxyFor(False).addCallback(
+                formatPrincipals
+            ),
+        )
+
+
+    @renderer
+    def extra(self, request, tag):
+        """
+        No-op; implemented in subclass.
+        """
+        return ''
+
+
+    @renderer
+    def enabledForCalendaring(self, request, tag):
+        """
+        No-op; implemented in subclass.
+        """
+        return ''
+
+
+    @renderer
+    def enabledForAddressBooks(self, request, tag):
+        """
+        No-op; implemented in subclass.
+        """
+        return ''
+
+
+
+class DirectoryPrincipalElement(DirectoryElement):
+    """
+    L{DirectoryPrincipalElement} is a renderer for directory details.
+    """
+
+    @renderer
+    def resourceDetail(self, request, tag):
+        """
+        Render the directory principal's details.
+        """
+        return DirectoryPrincipalDetailElement(self.resource)
+
+
+class DirectoryCalendarPrincipalDetailElement(DirectoryPrincipalDetailElement):
+
+    @renderer
+    def extra(self, request, tag):
+        """
+        Renderer for extra directory body items for calendar/addressbook
+        principals.
+        """
+        return tag
+
+
+    @renderer
+    def enabledForCalendaring(self, request, tag):
+        """
+        Renderer which returns its tag when the wrapped record is enabled for
+        calendaring.
+        """
+        resource = self.resource
+        record = resource.record
+        if record.enabledForCalendaring:
+            return tag.fillSlots(
+                calendarUserAddresses=formatLinks(
+                    resource.calendarUserAddresses()
+                ),
+                calendarHomes=formatLinks(resource.calendarHomeURLs())
+            )
+        return ''
+
+
+    @renderer
+    def enabledForAddressBooks(self, request, tag):
+        """
+        Renderer which returnst its tag when the wrapped record is enabled for
+        addressbooks.
+        """
+        resource = self.resource
+        record = resource.record
+        if record.enabledForAddressBooks:
+            return tag.fillSlots(
+                addressBookHomes=formatLinks(resource.addressBookHomeURLs())
+            )
+        return ''
+
+
+
+class DirectoryCalendarPrincipalElement(DirectoryPrincipalElement):
+    """
+    L{DirectoryPrincipalElement} is a renderer for directory details, with
+    calendaring additions.
+    """
+
+    @renderer
+    def resourceDetail(self, request, tag):
+        """
+        Render the directory calendar principal's details.
+        """
+        return DirectoryCalendarPrincipalDetailElement(self.resource)
+
+
+class DirectoryPrincipalResource (
+        PropfindCacheMixin, PermissionsMixIn, DAVPrincipalResource):
+    """
     Directory principal resource.
     """
 
@@ -579,54 +749,12 @@
     # HTTP
     ##
 
-    @inlineCallbacks
-    def renderDirectoryBody(self, request):
+    def htmlElement(self):
+        """
+        Customize HTML rendering for directory principals.
+        """
+        return DirectoryPrincipalElement(self)
 
-        extras = self.extraDirectoryBodyItems(request)
-        output = (yield super(DirectoryPrincipalResource, self).renderDirectoryBody(request))
-
-        members = (yield self.groupMembers())
-        
-        memberships = (yield self.groupMemberships())
-
-        proxyFor = (yield self.proxyFor(True))
-        readOnlyProxyFor = (yield self.proxyFor(False))
-        
-        returnValue("".join((
-            """<div class="directory-listing">"""
-            """<h1>Principal Details</h1>"""
-            """<pre><blockquote>"""
-            """Directory Information\n"""
-            """---------------------\n"""
-            """Directory GUID: %s\n"""         % (self.record.service.guid,),
-            """Realm: %s\n"""                  % (self.record.service.realmName,),
-            """Hosted-At: %s\n"""              % (self.record.serverURI(),) if config.Servers.Enabled else "", 
-            """Partition: %s\n"""              % (self.record.effectivePartitionID(),) if config.Servers.Enabled else "", 
-            """\n"""
-            """Principal Information\n"""
-            """---------------------\n"""
-            """GUID: %s\n"""                   % (self.record.guid,),
-            """Record type: %s\n"""            % (self.record.recordType,),
-            """Short names: %s\n"""            % (",".join(self.record.shortNames),),
-            """Security Identities: %s\n"""    % (",".join(self.record.authIDs),),
-            """Full name: %s\n"""              % (self.record.fullName,),
-            """First name: %s\n"""             % (self.record.firstName,),
-            """Last name: %s\n"""              % (self.record.lastName,),
-            """Email addresses:\n"""           , format_list(self.record.emailAddresses),
-            """Principal UID: %s\n"""          % (self.principalUID(),),
-            """Principal URL: %s\n"""          % (format_link(self.principalURL()),),
-            """\nAlternate URIs:\n"""          , format_list(format_link(u) for u in self.alternateURIs()),
-            """\nGroup members:\n"""           , format_principals(members),
-            """\nGroup memberships:\n"""       , format_principals(memberships),
-            """\nRead-write Proxy For:\n"""    , format_principals(proxyFor),
-            """\nRead-only Proxy For:\n"""     , format_principals(readOnlyProxyFor),
-            """%s</pre></blockquote></div>"""  % extras,
-            output
-        )))
-
-    def extraDirectoryBodyItems(self, request):
-        return ""
-
     ##
     # DAV
     ##
@@ -652,8 +780,10 @@
         """
 
         # The db is located in the principal collection root
-        return calendaruserproxy.ProxyDBService
+        from twistedcaldav.directory.calendaruserproxy import ProxyDBService
+        return ProxyDBService
 
+
     def alternateURIs(self):
         # FIXME: Add API to IDirectoryRecord for getting a record URI?
         return self._alternate_urls
@@ -839,7 +969,8 @@
         return ()
 
 
-class DirectoryCalendarPrincipalResource (DirectoryPrincipalResource, CalendarPrincipalResource):
+class DirectoryCalendarPrincipalResource(DirectoryPrincipalResource,
+                                         CalendarPrincipalResource):
     """
     Directory calendar principal resource.
     """
@@ -861,18 +992,6 @@
             result = (yield CalendarPrincipalResource.readProperty(self, property, request))
         returnValue(result)
 
-    def extraDirectoryBodyItems(self, request):
-        extra = ""
-        if self.record.enabledForCalendaring:
-            extra += "".join((
-                """\nCalendar homes:\n"""          , format_list(format_link(u) for u in self.calendarHomeURLs()),
-                """\nCalendar user addresses:\n""" , format_list(format_link(a) for a in self.calendarUserAddresses()),
-            ))
-        if self.record.enabledForAddressBooks:
-            extra += "".join((
-                """\nAddress Book homes:\n"""       , format_list(format_link(u) for u in self.addressBookHomeURLs()),
-            ))
-        return extra
 
     ##
     # CalDAV
@@ -897,6 +1016,14 @@
 
         return addresses
 
+
+    def htmlElement(self):
+        """
+        Customize HTML generation for calendar principals.
+        """
+        return DirectoryCalendarPrincipalElement(self)
+
+
     def canonicalCalendarUserAddress(self):
         """
         Return a CUA for this principal, preferring in this order:
@@ -1052,8 +1179,12 @@
         if name == "":
             return self
 
-        if config.EnableProxyPrincipals and name in ("calendar-proxy-read", "calendar-proxy-write"):
+        if config.EnableProxyPrincipals and name in ("calendar-proxy-read",
+                                                     "calendar-proxy-write"):
             # name is required to be str
+            from twistedcaldav.directory.calendaruserproxy import (
+                CalendarUserProxyPrincipalResource
+            )
             return CalendarUserProxyPrincipalResource(self, str(name))
         else:
             return None
@@ -1077,21 +1208,11 @@
     ),
 )
 
-def format_list(items, *args):
-    def genlist():
-        try:
-            item = None
-            for item in items:
-                yield " -> %s\n" % (item,)
-            if item is None:
-                yield " '()\n"
-        except Exception, e:
-            log.err("Exception while rendering: %s" % (e,))
-            Failure().printTraceback()
-            yield "  ** %s **: %s\n" % (e.__class__.__name__, e)
-    return "".join(genlist())
 
-def format_principals(principals):
+def formatPrincipals(principals):
+    """
+    Format a list of principals into some twisted.web.template DOM objects.
+    """
     def recordKey(principal):
         try:
             record = principal.record
@@ -1100,7 +1221,6 @@
                 record = principal.parent.record
             except:
                 return None
-
         return (record.recordType, record.shortNames[0])
 
     def describe(principal):
@@ -1109,11 +1229,49 @@
         else:
             return ""
 
-    return format_list(
-        """<a href="%s">%s%s</a>"""
-        % (principal.principalURL(), escape(str(principal)), describe(principal))
+    return formatList(
+        tags.a(href=principal.principalURL())(
+            str(principal), describe(principal)
+        )
         for principal in sorted(principals, key=recordKey)
     )
 
-def format_link(url):
-    return """<a href="%s">%s</a>""" % (url, url)
+
+def formatList(iterable):
+    """
+    Format a list of stuff as an interable.
+    """
+    thereAreAny = False
+    try:
+        item = None
+        for item in iterable:
+            thereAreAny = True
+            yield " -> "
+            if item is None:
+                yield "None"
+            else:
+                yield item
+            yield "\n"
+    except Exception, e:
+        log.err("Exception while rendering: %s" % (e,))
+        Failure().printTraceback()
+        yield "  ** %s **: %s\n" % (e.__class__.__name__, e)
+    if not thereAreAny:
+        yield " '()\n"
+
+
+
+def formatLink(url):
+    """
+    Convert a URL string into some twisted.web.template DOM objects for
+    rendering as a link to itself.
+    """
+    return tags.a(href=url)(url)
+
+
+def formatLinks(urls):
+    """
+    Format a list of URL strings as a list of twisted.web.template DOM links.
+    """
+    return formatList(formatLink(link) for link in urls)
+

Copied: CalendarServer/trunk/twistedcaldav/directory-listing.html (from rev 8091, CalendarServer/branches/users/glyph/other-html/twistedcaldav/directory-listing.html)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory-listing.html	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory-listing.html	2011-09-13 19:14:09 UTC (rev 8092)
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+    t:render="main">
+    <head>
+        <title>Collection listing for <t:slot name="name" /></title>
+    </head>
+    <style>
+        th, .even td, .odd td { padding-right: 0.5em; font-family: monospace}
+        .even-dir { background-color: #efe0ef }
+        .even { background-color: #eee }
+        .odd-dir {background-color: #f0d0ef }
+        .odd { background-color: #dedede }
+        .icon { text-align: center }
+        .listing {
+            margin-left: auto;
+            margin-right: auto;
+            width: 50%;
+            padding: 0.1em;
+        }
+        body {
+            border: 0; padding: 0; margin: 0;
+            background-color: #efefef;
+        }
+        h1 {
+            padding: 0.1em;
+            background-color: #777;
+            color: white;
+            border-bottom: thin white dashed;
+        }
+    </style>
+    <body>
+        <t:transparent t:render="resourceDetail" />
+        <div class="directory-listing">
+            <h1>Collection Listing</h1>
+            <table>
+                <tr>
+                    <th>Name</th><th>Size</th><th>Last Modified</th>
+                    <th>MIME Type</th>
+                </tr>
+                <tr t:render="children">
+                    <t:attr name="class"><t:slot name="even"/></t:attr>
+                    <td><a><t:slot name="name"/><t:attr
+                            name="href"><t:slot name="url"/></t:attr></a></td>
+                    <td align="right"><t:slot name="size"/></td>
+                    <td><t:slot name="lastModified"/></td>
+                    <td><t:slot name="type"/></td>
+                </tr>
+            </table>
+        </div>
+        <div class="directory-listing">
+            <h1>Properties</h1>
+            <table>
+                <tr><th>Name</th><th>Value</th></tr>
+                <tr t:render="properties">
+                    <t:attr name="class"><t:slot name="even"/></t:attr>
+                    <td valign="top"><t:slot name="name"/></td>
+                    <td><pre><t:slot name="value"/></pre></td>
+                </tr>
+            </table>
+        </div>
+    </body>
+</html>

Modified: CalendarServer/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py	2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twistedcaldav/extensions.py	2011-09-13 19:14:09 UTC (rev 8092)
@@ -30,13 +30,16 @@
 ]
 
 import urllib
-import cgi
 import time
+from itertools import cycle
 
-from twisted.internet.defer import succeed, DeferredList, maybeDeferred
+from twisted.internet.defer import succeed, maybeDeferred
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.cred.error import LoginFailed, UnauthorizedLogin
 
+from twisted.web.template import Element, XMLFile, renderer, tags, flattenString
+from twisted.python.modules import getModule
+
 from twext.web2 import responsecode, server
 from twext.web2.auth.wrapper import UnauthorizedResponse
 from twext.web2.http import HTTPError, Response, RedirectResponse
@@ -51,7 +54,10 @@
 from twext.web2.dav.idav import IDAVPrincipalResource
 from twext.web2.dav.static import DAVFile as SuperDAVFile
 from twext.web2.dav.resource import DAVResource as SuperDAVResource
-from twext.web2.dav.resource import DAVPrincipalResource as SuperDAVPrincipalResource
+from twext.web2.dav.resource import (
+    DAVPrincipalResource as SuperDAVPrincipalResource
+)
+from twisted.internet.defer import gatherResults
 from twext.web2.dav.method import prop_common
 from twext.web2.dav.method.report import max_number_of_matches
 
@@ -59,11 +65,14 @@
 
 from twistedcaldav import customxml
 from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.util import Alternator
+
 from twistedcaldav.directory.sudo import SudoDirectoryService
 from twistedcaldav.directory.directory import DirectoryService
 from twistedcaldav.method.report import http_REPORT
 
+
+thisModule = getModule(__name__)
+
 log = Logger()
 
 
@@ -429,197 +438,156 @@
 
         if resultsWereLimited is not None:
             if resultsWereLimited[0] == "server":
-                log.err("Too many matching resources in principal-property-search report")
+                log.err("Too many matching resources in "
+                        "principal-property-search report")
             responses.append(davxml.StatusResponse(
                 davxml.HRef.fromString(request.uri),
-                davxml.Status.fromResponseCode(responsecode.INSUFFICIENT_STORAGE_SPACE),
+                davxml.Status.fromResponseCode(
+                    responsecode.INSUFFICIENT_STORAGE_SPACE
+                ),
                 davxml.Error(davxml.NumberOfMatchesWithinLimits()),
-                davxml.ResponseDescription("Results limited by %s at %d" % resultsWereLimited),
+                davxml.ResponseDescription("Results limited by %s at %d"
+                                           % resultsWereLimited),
             ))
         returnValue(MultiStatusResponse(responses))
 
 
-class DirectoryRenderingMixIn(object):
 
-    def directoryStyleSheet(self):
-        return (
-            "th, .even td, .odd td { padding-right: 0.5em; font-family: monospace}"
-            ".even-dir { background-color: #efe0ef }"
-            ".even { background-color: #eee }"
-            ".odd-dir {background-color: #f0d0ef }"
-            ".odd { background-color: #dedede }"
-            ".icon { text-align: center }"
-            ".listing {"
-              "margin-left: auto;"
-              "margin-right: auto;"
-              "width: 50%;"
-              "padding: 0.1em;"
-            "}"
-            "body { border: 0; padding: 0; margin: 0; background-color: #efefef;}"
-            "h1 {padding: 0.1em; background-color: #777; color: white; border-bottom: thin white dashed;}"
-        )
+class DirectoryElement(Element):
+    """
+    A L{DirectoryElement} is an L{Element} for rendering the contents of a
+    L{DirectoryRenderingMixIn} resource as HTML.
+    """
 
-    def renderDirectory(self, request):
+    loader = XMLFile(
+        thisModule.filePath.sibling("directory-listing.html").open()
+    )
+
+    def __init__(self, resource):
         """
-        Render a directory listing.
+        @param resource: the L{DirectoryRenderingMixIn} resource being
+            listed.
         """
-        output = [
-            """<html>"""
-            """<head>"""
-            """<title>Collection listing for %(path)s</title>"""
-            """<style>%(style)s</style>"""
-            """</head>"""
-            """<body>"""
-            % {
-                "path": "%s" % cgi.escape(urllib.unquote(request.path)),
-                "style": self.directoryStyleSheet(),
-            }
-        ]
+        super(DirectoryElement, self).__init__()
+        self.resource = resource
 
-        def gotBody(body, output=output):
-            output.append(body)
-            output.append("</body></html>")
 
-            output = "".join(output)
+    @renderer
+    def resourceDetail(self, request, tag):
+        """
+        Renderer which returns a distinct element for this resource's data.
+        Subclasses should override.
+        """
+        return ''
 
-            if isinstance(output, unicode):
-                output = output.encode("utf-8")
 
-            mime_params = {"charset": "utf-8"}
+    @renderer
+    def children(self, request, tag):
+        """
+        Renderer which yields all child object tags as table rows.
+        """
+        whenChildren = (
+            maybeDeferred(self.resource.listChildren)
+            .addCallback(sorted)
+            .addCallback(
+                lambda names: gatherResults(
+                    [maybeDeferred(self.resource.getChild, x) for x in names]
+                )
+                .addCallback(lambda children: zip(children, names))
+            )
+        )
+        @whenChildren.addCallback
+        def gotChildren(children):
+            for even, [child, name] in zip(cycle(["odd", "even"]), children):
+                [url, name, size, lastModified, contentType] = map(
+                    str, self.resource.getChildDirectoryEntry(
+                        child, name, request)
+                )
+                yield tag.clone().fillSlots(
+                    url=url, name=name, size=str(size),
+                    lastModified=lastModified, even=even, type=contentType,
+                )
+        return whenChildren
 
-            response = Response(200, {}, output)
-            response.headers.setHeader("content-type", MimeType("text", "html", mime_params))
-            return response
 
-        d = self.renderDirectoryBody(request)
-        d.addCallback(gotBody)
-        return d
-
-    @inlineCallbacks
-    def renderDirectoryBody(self, request):
+    @renderer
+    def main(self, request, tag):
         """
-        Generate a directory listing table in HTML.
+        Main renderer; fills slots for title, etc.
         """
-        output = [
-            """<div class="directory-listing">"""
-            """<h1>Collection Listing</h1>"""
-            """<table>"""
-            """<tr><th>Name</th> <th>Size</th> <th>Last Modified</th> <th>MIME Type</th></tr>"""
-        ]
+        return tag.fillSlots(name=request.path)
 
-        even = Alternator()
-        for name in sorted((yield self.listChildren())):
-            child = (yield maybeDeferred(self.getChild, name))
 
-            url, name, size, lastModified, contentType = self.getChildDirectoryEntry(child, name, request)
-
-            # FIXME: gray out resources that are not readable
-            output.append(
-                """<tr class="%(even)s">"""
-                """<td><a href="%(url)s">%(name)s</a></td>"""
-                """<td align="right">%(size)s</td>"""
-                """<td>%(lastModified)s</td>"""
-                """<td>%(type)s</td>"""
-                """</tr>"""
-                % {
-                    "even": even.state() and "even" or "odd",
-                    "url": url,
-                    "name": cgi.escape(name),
-                    "size": size,
-                    "lastModified": lastModified,
-                    "type": contentType,
-                }
-            )
-
-        output.append(
-            """</table></div>"""
-            """<div class="directory-listing">"""
-            """<h1>Properties</h1>"""
-            """<table>"""
-            """<tr><th>Name</th> <th>Value</th></tr>"""
-        )
-
+    @renderer
+    def properties(self, request, tag):
+        """
+        Renderer which yields all properties as table row tags.
+        """
+        whenPropertiesListed = self.resource.listProperties(request)
+        @whenPropertiesListed.addCallback
         def gotProperties(qnames):
-            ds = []
-
-            noneValue         = object()
             accessDeniedValue = object()
 
-            def gotProperty(property):
-                if property is None:
-                    name = "{%s}%s" % qname
-                    value = noneValue
-                else:
-                    name = property.sname()
-                    value = property.toxml()
-
-                return (name, value)
-
-            def gotError(f, qname):
+            def gotError(f, name):
                 f.trap(HTTPError)
-
-                name = "{%s}%s" % qname
                 code = f.value.response.code
-
                 if code == responsecode.NOT_FOUND:
-                    log.err("Property {%s}%s was returned by listProperties() but does not exist for resource %s."
-                            % (qname[0], qname[1], self))
+                    log.err("Property %s was returned by listProperties() "
+                            "but does not exist for resource %s."
+                            % (name, self.resource))
                     return (name, None)
-
                 if code == responsecode.UNAUTHORIZED:
                     return (name, accessDeniedValue)
-
                 return f
 
-            for qname in sorted(qnames):
-                d = self.readProperty(qname, request)
-                d.addCallback(gotProperty)
-                d.addErrback(gotError, qname)
-                ds.append(d)
+            whenAllProperties = gatherResults([
+                maybeDeferred(self.resource.readProperty, qn, request)
+                .addCallback(lambda p, iqn=qn: (p.sname(), p.toxml())
+                             if p is not None else ("{%s}%s" % iqn, None) )
+                .addErrback(gotError, "{%s}%s" % qn)
+                for qn in sorted(qnames)
+            ])
 
-            even = Alternator()
-
+            @whenAllProperties.addCallback
             def gotValues(items):
-                for result, (name, value) in items:
-                    if not result:
-                        continue
-
+                for even, [name, value] in zip(cycle(["odd", "even"]), items):
                     if value is None:
-                        # An AssertionError might be appropriate, but
-                        # we may as well continue rendering.
-                        log.err("Unexpected None value for property: %s" % (name,))
-                        continue
-                    elif value is noneValue:
-                        value = "<i>(no value)</i>"
+                        value = tags.i("(no value)")
                     elif value is accessDeniedValue:
-                        value = "<i>(access forbidden)</i>"
-                    else:
-                        value = cgi.escape(value)
-
-                    output.append(
-                        str("""<tr class="%(even)s">"""
-                            """<td valign="top">%(name)s</td>"""
-                            """<td><pre>%(value)s</pre></td>"""
-                            """</tr>"""
-                            % {
-                                "even": even.state() and "even" or "odd",
-                                "name": name,
-                                "value": value,
-                            }
-                        )
+                        value = tags.i("(access forbidden)")
+                    yield tag.clone().fillSlots(
+                        even=even, name=name, value=value,
                     )
+            return whenAllProperties
+        return whenPropertiesListed
 
-                output.append("</div>")
-                return "".join(output)
 
-            d = DeferredList(ds)
-            d.addCallback(gotValues)
-            return d
 
-        qnames = (yield self.listProperties(request))
-        result = (yield gotProperties(qnames))
-        returnValue(result)
+class DirectoryRenderingMixIn(object):
 
+    def renderDirectory(self, request):
+        """
+        Render a directory listing.
+        """
+        def gotBody(output):
+            mime_params = {"charset": "utf-8"}
+            response = Response(200, {}, output)
+            response.headers.setHeader(
+                "content-type",
+                MimeType("text", "html", mime_params)
+            )
+            return response
+        return flattenString(request, self.htmlElement()).addCallback(gotBody)
+
+
+    def htmlElement(self):
+        """
+        Create a L{DirectoryElement} or appropriate subclass for rendering this
+        resource.
+        """
+        return DirectoryElement(self)
+
+
     def getChildDirectoryEntry(self, child, name, request):
         def orNone(value, default="?", f=None):
             if value is None:
@@ -628,7 +596,7 @@
                 return f(value)
             else:
                 return value
-            
+
         url = urllib.quote(name, '/')
         if isinstance(child, DAVResource) and child.isCollection():
             url += "/"
@@ -666,9 +634,8 @@
                     rtypes.append(rtype.name)
                 if rtypes:
                     contentType = "(%s)" % (", ".join(rtypes),)
-                
 
-        return ((
+        return (
             url,
             name,
             orNone(size),
@@ -676,11 +643,12 @@
                 lastModified,
                 default="",
                 f=lambda t: time.strftime("%Y-%b-%d %H:%M", time.localtime(t))
-             ),
-             contentType,
-         ))
+            ),
+            contentType,
+        )
 
 
+
 def updateCacheTokenOnCallback(f):
     def wrapper(self, *args, **kwargs):
         if hasattr(self, "cacheNotifier"):

Modified: CalendarServer/trunk/twistedcaldav/test/test_extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_extensions.py	2011-09-13 19:12:15 UTC (rev 8091)
+++ CalendarServer/trunk/twistedcaldav/test/test_extensions.py	2011-09-13 19:14:09 UTC (rev 8092)
@@ -105,7 +105,8 @@
     """
 
     @inlineCallbacks
-    def doDirectoryTest(self, addedNames, modify=lambda x: None, expectedNames=None):
+    def doDirectoryTest(self, addedNames, modify=lambda x: None,
+                        expectedNames=None):
         """
         Do a test of a L{DAVFile} pointed at a directory, verifying that files
         existing with the given names will be faithfully 'played back' via HTML
@@ -119,9 +120,8 @@
             fp.child(sampleName).touch()
         df = DAVFile(fp)
         modify(df)
-        responseXML = browserHTML2ETree(
-            (yield df.render(SimpleFakeRequest('/'))).stream.read()
-        )
+        responseText = (yield df.render(SimpleFakeRequest('/'))).stream.read()
+        responseXML = browserHTML2ETree(responseText)
         names = set([element.text.encode("utf-8")
                      for element in responseXML.findall(".//a")])
         self.assertEquals(set(expectedNames), names)
@@ -185,7 +185,16 @@
                                    [nonASCIIFilename.encode("utf-8")])
 
 
+    def test_quotedCharacters(self):
+        """
+        Filenames might contain < or > characters, which need to be quoted in
+        HTML.
+        """
+        return self.doDirectoryTest([u'<a>.txt', u'<script>.html',
+                                     u'<style>.xml'])
 
+
+
 class ChildTraversalTests(TestCase):
     def test_makeChildDeferred(self):
         """
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110913/9ca95ee3/attachment-0001.html>


More information about the calendarserver-changes mailing list