[CalendarServer-changes] [4230] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Mon May 11 15:12:49 PDT 2009


Revision: 4230
          http://trac.macosforge.org/projects/calendarserver/changeset/4230
Author:   darla at apple.com
Date:     2009-05-11 15:12:48 -0700 (Mon, 11 May 2009)
Log Message:
-----------
Web-based admin utility for resource management

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/conf/caldavd-apple.plist
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/conf/caldavd.plist
    CalendarServer/trunk/twistedcaldav/config.py

Added Paths:
-----------
    CalendarServer/trunk/calendarserver/webadmin/
    CalendarServer/trunk/calendarserver/webadmin/__init__.py
    CalendarServer/trunk/calendarserver/webadmin/helper.py
    CalendarServer/trunk/calendarserver/webadmin/resource.py

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2009-05-11 21:38:13 UTC (rev 4229)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2009-05-11 22:12:48 UTC (rev 4230)
@@ -90,6 +90,7 @@
     NegotiateCredentialFactory = None
 
 from calendarserver.provision.root import RootResource
+from calendarserver.webadmin.resource import WebAdminResource
 from calendarserver.webcal.resource import WebCalendarResource
 
 log = Logger()
@@ -341,6 +342,7 @@
     imipResourceClass            = IMIPInboxResource
     timezoneServiceResourceClass = TimezoneServiceFile
     webCalendarResourceClass     = WebCalendarResource
+    webAdminResourceClass        = WebAdminResource
 
     def makeService(self, options):
 
@@ -648,6 +650,17 @@
             root.putChild("webcal", webCalendar)
 
         #
+        # WebAdmin
+        #
+        if config.EnableWebAdmin:
+            self.log_info("Setting up WebAdmin resource")
+            webAdmin = self.webAdminResourceClass(
+                config.WebCalendarRoot,
+                principalCollections=(principalCollection,),
+            )
+            root.putChild("admin", webAdmin)
+
+        #
         # Configure ancillary data
         #
         self.log_info("Setting up Timezone Cache")

Added: CalendarServer/trunk/calendarserver/webadmin/__init__.py
===================================================================
--- CalendarServer/trunk/calendarserver/webadmin/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/webadmin/__init__.py	2009-05-11 22:12:48 UTC (rev 4230)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Calendar Server Web Admin UI.
+"""

Added: CalendarServer/trunk/calendarserver/webadmin/helper.py
===================================================================
--- CalendarServer/trunk/calendarserver/webadmin/helper.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/webadmin/helper.py	2009-05-11 22:12:48 UTC (rev 4230)
@@ -0,0 +1,224 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Calendar Server Web Admin helper.
+"""
+
+__all__ = [
+    "ResourceWrapper",
+]
+
+
+import os
+
+from calendarserver.provision.root import RootResource
+
+from twistedcaldav import memcachepool
+from twistedcaldav.log import setLogLevelForNamespace
+from twistedcaldav.static import CalendarHomeProvisioningFile
+
+from twisted.internet.address import IPv4Address
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.reflect import namedClass
+from twisted.web2.dav import davxml
+
+
+ at inlineCallbacks
+def search(directory, searchStr):
+    fields = []
+    for fieldName in ("fullName", "firstName", "lastName", "emailAddresses"):
+        fields.append((fieldName, searchStr, True, "contains"))
+    
+    records = list((yield directory.recordsMatchingFields(fields)))
+    returnValue(records)
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+class ResourceWrapper(object):
+
+    def __init__(self, resource):
+        self.resource = resource
+
+    def readProperty(self, prop):
+        return self.resource.readProperty(prop, FakeRequest())
+
+    def writeProperty(self, prop):
+        return self.resource.writeProperty(prop, FakeRequest())
+
+    def lookupResource(self, specifier):
+        # For now, support GUID lookup
+        return self.getChild("principals/__uids__/%s" % (specifier,))
+
+    def getChild(self, path):
+        resource = self.resource
+        segments = path.strip("/").split("/")
+        for segment in segments:
+            resource = resource.getChild(segment)
+            if resource is None:
+                return None
+        return ResourceWrapper(resource)
+
+    @inlineCallbacks
+    def removeDelegate(self, delegate, permission):
+        subPrincipalName = "calendar-proxy-%s" % (permission,)
+        subPrincipal = self.getChild(subPrincipalName)
+        if subPrincipal is None:
+            abort("No proxy subprincipal found for %s" % (self.resource,))
+
+        namespace, name = davxml.dav_namespace, "group-member-set"
+        prop = (yield subPrincipal.readProperty((namespace, name)))
+        newChildren = []
+        for child in prop.children:
+            if str(child) != delegate.url():
+                newChildren.append(child)
+
+        if len(prop.children) == len(newChildren):
+            # Nothing to do -- the delegate wasn't there
+            returnValue(False)
+
+        newProp = davxml.GroupMemberSet(*newChildren)
+        result = (yield subPrincipal.writeProperty(newProp))
+        returnValue(result)
+
+    @inlineCallbacks
+    def addDelegate(self, delegate, permission):
+
+        opposite = "read" if permission == "write" else "write"
+        result = (yield self.removeDelegate(delegate, opposite))
+
+        subPrincipalName = "calendar-proxy-%s" % (permission,)
+        subPrincipal = self.getChild(subPrincipalName)
+        if subPrincipal is None:
+            abort("No proxy subprincipal found for %s" % (self.resource,))
+
+        namespace, name = davxml.dav_namespace, "group-member-set"
+        prop = (yield subPrincipal.readProperty((namespace, name)))
+        for child in prop.children:
+            if str(child) == delegate.url():
+                # delegate is already in the group
+                break
+        else:
+            # delegate is not already in the group
+            newChildren = list(prop.children)
+            newChildren.append(davxml.HRef(delegate.url()))
+            newProp = davxml.GroupMemberSet(*newChildren)
+            result = (yield subPrincipal.writeProperty(newProp))
+            returnValue(result)
+
+    @inlineCallbacks
+    def getDelegates(self, permission):
+
+        subPrincipalName = "calendar-proxy-%s" % (permission,)
+        subPrincipal = self.getChild(subPrincipalName)
+        if subPrincipal is None:
+            abort("No proxy subprincipal found for %s" % (self.resource,))
+
+        namespace, name = davxml.dav_namespace, "group-member-set"
+        prop = (yield subPrincipal.readProperty((namespace, name)))
+        result = []
+        for child in prop.children:
+            result.append(str(child))
+        returnValue(result)
+
+    def setAutoSchedule(self, autoSchedule):
+        return self.resource.setAutoSchedule(autoSchedule)
+
+    def getAutoSchedule(self):
+        return self.resource.getAutoSchedule()
+
+    def url(self):
+        return self.resource.url()
+
+class FakeRequest(object):
+    pass
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+def setup(config):
+
+    setLogLevelForNamespace(None, "warn")
+
+    directory = getDirectory(config)
+    if config.Memcached["ClientEnabled"]:
+        memcachepool.installPool(
+            IPv4Address(
+                'TCP',
+                config.Memcached["BindAddress"],
+                config.Memcached["Port"]
+            ),
+            config.Memcached["MaxClients"]
+        )
+    if config.Notifications["Enabled"]:
+        installNotificationClient(
+            config.Notifications["InternalNotificationHost"],
+            config.Notifications["InternalNotificationPort"],
+        )
+    principalCollection = directory.getPrincipalCollection()
+    root = RootResource(
+        config.DocumentRoot,
+        principalCollections=(principalCollection,),
+    )
+    root.putChild("principals", principalCollection)
+    calendarCollection = CalendarHomeProvisioningFile(
+        os.path.join(config.DocumentRoot, "calendars"),
+        directory, "/calendars/",
+    )
+    root.putChild("calendars", calendarCollection)
+
+    return (directory, root)
+
+def getDirectory(config):
+    BaseDirectoryService = namedClass(config.DirectoryService["type"])
+
+    class MyDirectoryService (BaseDirectoryService):
+        def getPrincipalCollection(self):
+            if not hasattr(self, "_principalCollection"):
+                from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
+                self._principalCollection = DirectoryPrincipalProvisioningResource("/principals/", self)
+
+            return self._principalCollection
+
+        def setPrincipalCollection(self, coll):
+            # See principal.py line 237:  self.directory.principalCollection = self
+            pass
+
+        principalCollection = property(getPrincipalCollection, setPrincipalCollection)
+
+        def calendarHomeForRecord(self, record):
+            principal = self.principalCollection.principalForRecord(record)
+            if principal:
+                try:
+                    return principal._calendarHome()
+                except AttributeError:
+                    pass
+            return None
+
+        def calendarHomeForShortName(self, recordType, shortName):
+            principal = self.principalCollection.principalForShortName(recordType, shortName)
+            if principal:
+                try:
+                    return principal._calendarHome()
+                except AttributeError:
+                    pass
+            return None
+
+        def principalForCalendarUserAddress(self, cua):
+            return self.principalCollection.principalForCalendarUserAddress(cua)
+
+
+    return MyDirectoryService(**config.DirectoryService["params"])
+

Added: CalendarServer/trunk/calendarserver/webadmin/resource.py
===================================================================
--- CalendarServer/trunk/calendarserver/webadmin/resource.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/webadmin/resource.py	2009-05-11 22:12:48 UTC (rev 4230)
@@ -0,0 +1,423 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Calendar Server Web Admin UI.
+"""
+
+__all__ = [
+    "WebAdminResource",
+]
+
+import cgi
+import operator
+import os
+import urllib
+import urlparse
+
+from calendarserver.webadmin.helper import setup, search, ResourceWrapper
+
+from twistedcaldav.config import config
+from twistedcaldav.extensions import DAVFile, ReadOnlyResourceMixIn
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.web2.http import Response
+from twisted.web2.http_headers import MimeType
+from twisted.web2.stream import MemoryStream
+from twisted.web2.dav import davxml
+from twisted.web2.dav.resource import TwistedACLInheritable
+
+
+class WebAdminResource (ReadOnlyResourceMixIn, DAVFile):
+
+    # Only allow administrators to access
+    def defaultAccessControlList(self):
+        return davxml.ACL(*config.AdminACEs)
+    
+    def etag(self):
+        # Can't be calculated here
+        return None
+
+    def contentLength(self):
+        # Can't be calculated here
+        return None
+
+    def lastModified(self):
+        return None
+
+    def exists(self):
+        return True
+
+    def displayName(self):
+        return "Web Admin"
+
+    def contentType(self):
+        return MimeType.fromString("text/html; charset=utf-8");
+
+    def contentEncoding(self):
+        return None
+
+    def createSimilarFile(self, path):
+        return DAVFile(path, principalCollections=self.principalCollections())
+
+    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; padding-left:10px; padding-right:10px; background-color: #777; color: white; border-bottom: thin white dashed;}"
+        )
+
+    def header(self, title):
+
+        if title is None or title == "":
+            title = "Calendar Server Web Administration"
+        else:
+            title = "Calendar Server Web Administration: " + title
+        
+        return ( "<html>\n"
+                 "<head>\n"
+                 "<h1>%(title)s</h1>\n" 
+                 "<title>%(title)s</title>\n"
+                 "<style>\n  %(style)s\n</style>\n"
+                 "</head>\n"
+                 "<body>\n"
+                 "<div style=\"padding-left:10px; padding-right:10px\">\n" % { "title": title, "style": self.directoryStyleSheet() })
+
+    def footer(self) :
+        return ( "\n</div>\n"
+                 "</body>\n"
+                 "</html>" )
+              
+    @inlineCallbacks
+    def htmlContent(self, resourceWrapper, directory, request):
+
+        def queryValue(arg):
+            query = cgi.parse_qs(urlparse.urlparse(request.uri).query, True)
+            return query.get(arg, [""])[0]
+
+        def queryValues(arg):
+            query = cgi.parse_qs(urlparse.urlparse(request.uri).query, True)
+            matches = []
+            for key in query.keys():
+                if key.startswith(arg):
+                    matches.append(key[len(arg):])
+            return matches
+
+        # Read request parameters.
+        resourceGuid = queryValue("resourceGuid")
+        resourceSearch = queryValue("resourceSearch")
+        davPropertyName = queryValue("davPropertyName")
+        autoSchedule = queryValue("autoSchedule")
+        delegateSearch = queryValue("delegateSearch")
+        makeReadDelegates = queryValues("mkReadDelegate|")
+        makeWriteDelegates = queryValues("mkWriteDelegate|")
+        removeDelegates = queryValues("rmDelegate|")
+
+        # Begin the content
+        content = ("%(header)s\n"
+                   "<h2>Resource Management</h2>\n"
+                   "%(search)s\n" % { "header": self.header(None),
+                                     "search": (yield self.searchContent(directory, resourceSearch)) })
+
+        # Add details if a resource has been selected.
+        if resourceGuid is not None and resourceGuid != "":
+        
+            resource = resourceWrapper.lookupResource(resourceGuid)
+    
+            # Update the auto-schedule value if specified.
+            if autoSchedule is not None and (autoSchedule == "true" or autoSchedule == "false"):
+                result = (yield resource.setAutoSchedule(autoSchedule == "true"))
+
+            # Update the delegates if specified.
+            for delegateGuid in removeDelegates:
+                delegate = resourceWrapper.lookupResource(delegateGuid)
+                result = (yield resource.removeDelegate(delegate, "read"))
+                result = (yield resource.removeDelegate(delegate, "write"))
+
+            for delegateGuid in makeReadDelegates:
+                delegate = resourceWrapper.lookupResource(delegateGuid)
+                result = (yield resource.addDelegate(delegate, "read"))
+
+            for delegateGuid in makeWriteDelegates:
+                delegate = resourceWrapper.lookupResource(delegateGuid)
+                result = (yield resource.addDelegate(delegate, "write"))
+                
+            # Add the detailed content
+            content += (yield self.detailContent(resourceWrapper, directory, resource, resourceGuid, davPropertyName, delegateSearch))
+
+        # Add the footer
+        content += self.footer()
+
+        returnValue(content)
+        
+    @inlineCallbacks
+    def searchContent(self, directory, resourceSearch):
+        
+        formHtml = ("""
+<form id=\"frm_resource\" name=\"resourceForm\" action=\"/admin/\">
+  Search for resource to manage:
+  <input type=\"text\" id=\"txt_resourceSearch\" name=\"resourceSearch\" value=\"%s\" size=\"40\" />
+  <input type=\"submit\" value=\"Search\" />
+</form>
+""" % resourceSearch)
+
+        # Perform the search if a parameter was specified.
+        resultHtml = ""
+        if resourceSearch is not None and resourceSearch != "":
+
+            records = (yield search(directory, resourceSearch))
+            if records:
+                records.sort(key=operator.attrgetter('fullName'))
+                resultHtml = """
+<table cellspacing=\"0\" cellpadding=\"3\" border=\"1\" style=\"margin-top:2px\">
+  <tr class=\"odd\">
+    <th>GUID</th>
+    <th>Full Name</th>
+    <th>Type</th>
+    <th>Short Names</th>
+    <th>Auth IDs</th>
+    <th>Email Addresses</th>
+  </tr>""" % { "resourceSearch": resourceSearch }
+
+                for _i in range(0, len(records)):
+                    resultHtml += """
+  <tr class=\"%(rowClass)s\">
+    <td><a href=\"/admin/?resourceGuid=%(guid)s\">%(guid)s</a></td>
+    <td>%(name)s</td>
+    <td>%(type)s</td>
+    <td>%(shortNames)s</td>
+    <td>%(authIds)s</td>
+    <td>%(emails)s</td>
+  </tr>""" % { "rowClass": "even" if _i%2 == 0 else "odd",
+               "guid": urllib.quote(records[_i].guid),
+               "name": records[_i].fullName,
+               "type": { "users"     : "User",
+                         "groups"    : "Group",
+                         "locations" : "Place",
+                         "resources" : "Resource",
+                       }.get(records[_i].recordType),
+               "shortNames": str(", ".join(records[_i].shortNames),),
+               "authIds": str(", ".join(records[_i].authIDs),),
+               "emails": str(", ".join(records[_i].emailAddresses),)
+             }
+                resultHtml += "\n</table>"
+            else:
+                resultHtml += "<div style=\"margin-top:4px\">No matches found for resource <b>%(resourceSearch)s</b>.</div>\n" % { "resourceSearch": resourceSearch }
+
+        result = "%s%s" % (formHtml, resultHtml)
+        returnValue(result)
+
+    @inlineCallbacks
+    def detailContent(self, resourceWrapper, directory, resource, resourceGuid, davPropertyName, delegateSearch):
+
+        ###
+        # Resource title
+        ###
+        headerHtml = """
+<div style=\"margin-top:15px; background-color: #777; border-bottom:1px #ffffff dotted\"></div>
+<div style=\"background-color: #777; padding-top:2px; border-bottom:1px #ffffff dotted\"></div>
+<h3>Resource Details: %(resourceTitle)s</h3>""" % { "resourceTitle": resource.resource }
+
+        ###
+        # DAV properties
+        ###
+        propertyHtml = """
+<div style=\"margin-top:15px; border-bottom:1px #444444 dotted\"></div>
+<form id=\"frm_davProperty\" name=\"davPropertyForm\" action=\"/admin/\" style=\"margin-top:15px; margin-bottom:0; padding-bottom:0\">
+  Show a DAV property value:
+  <input type=\"hidden\" id=\"hdn_resourceGuid\" name=\"resourceGuid\" value=\"%(resourceGuid)s\" />
+  <input type=\"text\" id=\"txt_davPropertyName\" name=\"davPropertyName\" value=\"%(davPropertyName)s\" size=\"40\" />
+  <input type=\"submit\" value=\"Get Value\" />
+</form>
+""" % { "resourceGuid": urllib.quote(resourceGuid),
+        "davPropertyName": davPropertyName if davPropertyName is not None and davPropertyName != "" else "DAV:#" }
+        
+        if davPropertyName is not None and davPropertyName != "":
+            try:
+                namespace, name = davPropertyName.split("#")
+            except Exception, e:
+                propertyHtml += "<div>Unable to parse property to read: <b>%s</b></div>" % davPropertyName
+
+            result = (yield resource.readProperty((namespace, name)))
+            propertyHtml += "<div style=\"margin-top:7px\">Value of property <b>%(name)s</b>:</div><pre style=\"margin-top:5px; padding-top:0\">%(value)s</pre>" % { 
+                "name": davPropertyName, 
+                "value": cgi.escape(result.toxml())
+            }
+
+        ###
+        # Auto-schedule
+        ###
+        autoSchedule = (yield resource.getAutoSchedule())
+        autoScheduleHtml = """
+<div style=\"margin-top:15px; border-bottom:1px #444444 dotted\"></div>
+<form id=\"frm_autoSchedule\" name=\"autoScheduleForm\" action=\"/admin/\" style=\"margin-top:15px\">
+  <input type=\"hidden\" id=\"hdn_resourceGuid\" name=\"resourceGuid\" value=\"%(resourceGuid)s\" />
+  <div style=\"margin-top:7px\">
+    Auto-Schedule
+    <select id=\"sel_autoSchedule\" name=\"autoSchedule\">
+      <option value=\"true\"%(trueSelected)s>Yes</option>
+      <option value=\"false\"%(falseSelected)s>No</option>
+    </select>
+    <input type=\"submit\" value=\"Change\" />
+  </div>
+</form>\n""" % { "resourceGuid": urllib.quote(resourceGuid),
+               "trueSelected": " selected=\"selected\"" if autoSchedule else "",
+               "falseSelected": "" if autoSchedule else " selected=\"selected\"" }
+
+        ###
+        # Current delegates
+        ###
+        currentDelegatesHtml = "\n<div style=\"margin-top:15px; border-bottom:1px #444444 dotted\"></div>"
+
+        (readDelegates, writeDelegates) = ((yield resource.getDelegates("read")), (yield resource.getDelegates("write")))
+        if len(readDelegates) > 0 or len(writeDelegates) > 0:
+            currentDelegatesHtml += """
+<form id=\"frm_delegates\" name=\"delegatesForm\" action=\"/admin/\" style=\"margin-top:15px\">
+  <input type=\"hidden\" id=\"hdn_resourceGuid\" name=\"resourceGuid\" value=\"%(resourceGuid)s\" />
+  <table cellspacing=\"0\" cellpadding=\"3\" border=\"1\">
+    <tr class=\"odd\">
+      <th colspan=\"2\">Read-Only Delegates</th>
+      <th colspan=\"2\">Read-Write Delegates</th>
+    </tr>
+""" % { "resourceTitle": resource.resource,
+        "resourceGuid": urllib.quote(resourceGuid) }
+
+            for _i in range(0, max(len(readDelegates), len(writeDelegates))):
+                currentDelegatesHtml += "    <tr class=\"%(rowClass)s\">" % { "rowClass": "even" if _i%2 == 0 else "odd" }
+                if (_i < len(readDelegates)) :
+                    currentDelegatesHtml += """
+      <td>%(delegate)s</td>
+      <td>
+        <input type=\"submit\" name=\"mkWriteDelegate|%(delegatePath)s\" value=\"Make Read-Write\" />
+        <input type=\"submit\" name=\"rmDelegate|%(delegatePath)s\" value=\"Remove Delegate\" />
+      </td>""" % { "delegatePath": readDelegates[_i][21:-1], # GUID only, not full path
+                   "delegate" : resourceWrapper.getChild(readDelegates[_i]).resource }
+                else :
+                    currentDelegatesHtml += "\n      <td colspan=\"2\"></td>"
+                if (_i < len(writeDelegates)) :
+                    currentDelegatesHtml += """
+      <td>%(delegate)s</td>
+      <td>
+        <input type=\"submit\" name=\"mkReadDelegate|%(delegatePath)s\" value=\"Make Read-Only\" />
+        <input type=\"submit\" name=\"rmDelegate|%(delegatePath)s\" value=\"Remove Delegate\" />
+      </td>""" % { "delegatePath": writeDelegates[_i][21:-1], # GUID only, not full path
+                   "delegate" : resourceWrapper.getChild(writeDelegates[_i]).resource }
+                else :
+                    currentDelegatesHtml += "\n      <td colspan=\"2\"></td>"
+                currentDelegatesHtml += "\n    </tr>\n"
+
+            currentDelegatesHtml += "  </table>\n</form>\n"
+        else:
+            currentDelegatesHtml += "<div style=\"margin-top:15px\">This resource has no delegates.</div>\n"
+
+        ###
+        # Search for new delegates
+        ###
+        delegateSearchHtml = """
+<div style=\"margin-top:15px; border-bottom:1px #444444 dotted\"></div>
+<form id=\"frm_delegateSearch\" name=\"delegateSearchForm\" action=\"/admin/\" style=\"margin-top:15px; margin-bottom:0; padding-bottom:0\">
+  Search to add new delegates:
+  <input type=\"hidden\" id=\"hdn_resourceGuid\" name=\"resourceGuid\" value=\"%(resourceGuid)s\" />
+  <input type=\"text\" id=\"txt_delegateSearch\" name=\"delegateSearch\" value=\"%(delegateSearch)s\" size=\"40\" />
+  <input type=\"submit\" value=\"Search\" />
+</form>
+""" % { "resourceGuid": urllib.quote(resourceGuid),
+        "delegateSearch": delegateSearch }
+
+        # Perform the search if a parameter was specified.
+        if delegateSearch is not None and delegateSearch != "":
+            records = (yield search(directory, delegateSearch))
+            if records:
+                records.sort(key=operator.attrgetter('fullName'))
+
+                delegateSearchHtml += """
+<form id=\"frm_delegateAdd\" name=\"delegateAddForm\" action=\"/admin/\" style=\"margin-top:2px; padding-top:0\">
+  <input type=\"hidden\" id=\"hdn_resourceGuid\" name=\"resourceGuid\" value=\"%(resourceGuid)s\" />
+  <table cellspacing=\"0\" cellpadding=\"3\" border=\"1\">
+    <tr class=\"odd\">
+      <th>Full Name</th>
+      <th>Type</th>
+      <th>Short Names</th>
+      <th>Email Addresses</th>
+      <th>Add Delegate</th>
+    </tr>""" % { "resourceGuid": urllib.quote(resourceGuid) }
+
+                for _i in range(0, len(records)):
+                    delegateSearchHtml += """
+    <tr class=\"%(rowClass)s\">
+      <td>%(name)s</td>
+      <td>%(type)s</td>
+      <td>%(shortNames)s</td>
+      <td>%(emails)s</td>
+      <td>
+        <input type=\"submit\" name=\"mkReadDelegate|%(delegateGuid)s\" value=\"Make Read-Only Delegate\" />
+        <input type=\"submit\" name=\"mkWriteDelegate|%(delegateGuid)s\" value=\"Make Read-Write Delegate\" />
+      </td>
+    </tr>""" % { "rowClass": "even" if _i%2 == 0 else "odd",
+               "delegateGuid": urllib.quote(records[_i].guid),
+               "name": records[_i].fullName,
+               "type": { "users"     : "User",
+                         "groups"    : "Group",
+                         "locations" : "Place",
+                         "resources" : "Resource",
+                       }.get(records[_i].recordType),
+               "shortNames": str(", ".join(records[_i].shortNames),),
+               "emails": str(", ".join(records[_i].emailAddresses),)
+             }
+                delegateSearchHtml += "  </table>\n</form>\n"
+            else:
+                delegateSearchHtml += "<div style=\"margin-top:4px\">No matches found for delegate resource <b>%(delegateSearch)s</b>.</div>\n" % { "delegateSearch": delegateSearch }
+        
+        ###
+        # Put it all together
+        ###
+        detailHtml = "%s%s%s%s%s" % (headerHtml, propertyHtml, autoScheduleHtml, currentDelegatesHtml, delegateSearchHtml)
+
+        returnValue(detailHtml)
+
+    def render(self, request):
+
+        # Prepare the ResourceWrapper, which will be used to get and modify resource info.
+        directory, root = setup(config)
+        resourceWrapper = ResourceWrapper(root)
+        
+        # The response-generation will be deferred.
+        def _defer(htmlContent):
+            response = Response()
+            response.stream = MemoryStream(str(htmlContent))
+            for (header, value) in (
+                ("content-type", self.contentType()),
+                ("content-encoding", self.contentEncoding()),
+            ):
+                if value is not None:
+                    response.headers.setHeader(header, value)
+            return response
+
+        # Generate the HTML and return the response when it's ready.
+        htmlContent = self.htmlContent(resourceWrapper, directory, request)
+        htmlContent.addCallback(_defer)
+        return htmlContent

Modified: CalendarServer/trunk/conf/caldavd-apple.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-apple.plist	2009-05-11 21:38:13 UTC (rev 4229)
+++ CalendarServer/trunk/conf/caldavd-apple.plist	2009-05-11 22:12:48 UTC (rev 4230)
@@ -494,6 +494,10 @@
     <key>EnableSACLs</key>
     <true/>
 
+    <!-- Web-based administration -->
+    <key>EnableWebAdmin</key>
+    <true/>
 
+
   </dict>
 </plist>

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2009-05-11 21:38:13 UTC (rev 4229)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2009-05-11 22:12:48 UTC (rev 4230)
@@ -655,6 +655,10 @@
     <key>EnableSACLs</key>
     <false/>
 
+    <!-- Web-based administration -->
+    <key>EnableWebAdmin</key>
+    <true/>
+
     <!-- Support for Content-Encoding compression options as specified in RFC2616 Section 3.5 -->
     <key>ResponseCompression</key>
     <false/>

Modified: CalendarServer/trunk/conf/caldavd.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd.plist	2009-05-11 21:38:13 UTC (rev 4229)
+++ CalendarServer/trunk/conf/caldavd.plist	2009-05-11 22:12:48 UTC (rev 4230)
@@ -470,5 +470,14 @@
     <true/>
 
 
+    <!--
+        Miscellaneous items
+      -->
+
+    <!-- Web-based administration -->
+    <key>EnableWebAdmin</key>
+    <true/>
+
+
   </dict>
 </plist>

Modified: CalendarServer/trunk/twistedcaldav/config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/config.py	2009-05-11 21:38:13 UTC (rev 4229)
+++ CalendarServer/trunk/twistedcaldav/config.py	2009-05-11 22:12:48 UTC (rev 4230)
@@ -241,8 +241,13 @@
     "EnableDropBox"           : False, # Calendar Drop Box
     "EnablePrivateEvents"     : False, # Private Events
     "EnableTimezoneService"   : False, # Timezone service
-    
+
     #
+    # Web-based administration
+    #
+    "EnableWebAdmin"          : True,
+
+    #
     # Scheduling related options
     #
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090511/a9197b55/attachment-0001.html>


More information about the calendarserver-changes mailing list