[CalendarServer-changes] [5261] CalendarServer/branches/users/cdaboo/shared-calendars-5187/ twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Fri Mar 5 13:20:01 PST 2010


Revision: 5261
          http://trac.macosforge.org/projects/calendarserver/changeset/5261
Author:   cdaboo at apple.com
Date:     2010-03-05 13:20:01 -0800 (Fri, 05 Mar 2010)
Log Message:
-----------
Basic sending of invitations from sharer to sharee. More logic is needed here to handle actual state
changes.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/customxml.py
    CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/notifications.py
    CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/resource.py
    CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py
    CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/static.py
    CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/test/test_sharing.py

Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/customxml.py	2010-03-05 21:15:53 UTC (rev 5260)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/customxml.py	2010-03-05 21:20:01 UTC (rev 5261)
@@ -594,6 +594,10 @@
     namespace = calendarserver_namespace
     name = "uid"
 
+class Sequence (davxml.WebDAVTextElement):
+    namespace = calendarserver_namespace
+    name = "sequence"
+
 ##
 # Notifications
 ##
@@ -821,7 +825,16 @@
 
     allowed_children = { (davxml.dav_namespace, "href"): (0, 1) }
 
+class NotificationType (davxml.WebDAVTextElement):
+    """
+    A property to indicate what type of notification the resource represents.
+    """
+    namespace = calendarserver_namespace
+    name = "notification-type"
+    hidden = True
+    protected = True
 
+
 ##
 # Extensions to davxml.ResourceType
 ##

Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/notifications.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/notifications.py	2010-03-05 21:15:53 UTC (rev 5260)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/notifications.py	2010-03-05 21:20:01 UTC (rev 5261)
@@ -23,43 +23,180 @@
     "NotificationCollectionResource",
 ]
 
+from twext.python.log import Logger, LoggingMixIn
 from twext.web2 import responsecode
 from twext.web2.dav import davxml
 from twext.web2.dav.resource import DAVResource
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
+from twistedcaldav.sql import AbstractSQLDatabase, db_prefix
+import os
+import types
 
-from twext.python.log import Logger
-
-from twisted.internet.defer import succeed
-
 log = Logger()
 
 class NotificationResource(DAVResource):
     """
     An xml resource in a Notification collection.
     """
+    def __init__(self, parent):
+        self._parent = parent
+        DAVResource.__init__(self)
+
     def principalCollections(self):
         return self._parent.principalCollections()
 
     def isCollection(self):
         return False
 
+    def resourceName(self):
+        raise NotImplementedError
+        
     def http_PUT(self, request):
         return responsecode.FORBIDDEN
 
+    @inlineCallbacks
+    def http_DELETE(self, request):
+        
+        response = (yield super(NotificationResource, self).http_DELETE(request))
+        if response == responsecode.NO_CONTENT:
+            self._parent.deleteNotifictionMessageByName(request, self.resourceName())
+        returnValue(response)
+
 class NotificationCollectionResource(DAVResource):
     
+    def notificationsDB(self):
+        
+        if not hasattr(self, "_notificationsDB"):
+            self._notificationsDB = NotificationsDatabase(self)
+        return self._notificationsDB
+
     def isCollection(self):
         return True
 
     def resourceType(self):
         return davxml.ResourceType.notification
 
+    def addNotification(self, request, uid, xmltype, xmldata):
+        
+        # Write data to file
+        rname = uid + ".xml"
+        self._writeNotification(request, uid, rname, xmltype, xmldata)
+
+        # Update database
+        self.notificationsDB().addOrUpdateRecord(NotificationRecord(uid, rname, xmltype))
+
+        return succeed(None)
+
+    def _writeNotification(self, request, uid, rname, xmltype, xmldata):
+        raise NotImplementedError
+
     def getNotifictionMessages(self, request, componentType=None, returnLatestVersion=True):
         return succeed([])
 
-    def getNotifictionMessagesByUID(self, request, uid):
-        return succeed([])
+    def getNotifictionMessageByUID(self, request, uid):
+        return succeed(self.notificationsDB().recordForUID(uid))
 
-    def deleteSchedulingMessagesByUID(self, request, uid):
-        return succeed(True)
+    def deleteNotifictionMessageByUID(self, request, uid):
+        return succeed(self.notificationsDB().removeRecordForUID(uid))
 
+    def deleteNotifictionMessageByName(self, request, rname):
+        return succeed(self.notificationsDB().removeRecordForName(rname))
+
+class NotificationRecord(object):
+    
+    def __init__(self, uid, name, xmltype):
+        self.uid = uid
+        self.name = name
+        self.xmltype = xmltype
+
+class NotificationsDatabase(AbstractSQLDatabase, LoggingMixIn):
+    
+    db_basename = db_prefix + "notifications"
+    schema_version = "1"
+    db_type = "notifications"
+
+    def __init__(self, resource):
+        """
+        @param resource: the L{twistedcaldav.static.CalDAVFile} resource for
+            the notifications collection.)
+        """
+        self.resource = resource
+        db_filename = os.path.join(self.resource.fp.path, NotificationsDatabase.db_basename)
+        super(NotificationsDatabase, self).__init__(db_filename, True, autocommit=True)
+
+    def allRecords(self):
+        
+        records = self._db_execute("select * from NOTIFICATIONS")
+        return [self._makeRecord(row) for row in (records if records is not None else ())]
+    
+    def recordForUID(self, uid):
+        
+        row = self._db_execute("select * from NOTIFICATIONS where UID = :1", uid)
+        return self._makeRecord(row[0]) if row else None
+    
+    def addOrUpdateRecord(self, record):
+
+        self._db_execute("""insert or replace into NOTIFICATIONS (UID, NAME, TYPE)
+            values (:1, :2, :3)
+            """, record.uid, record.name, record.xmltype,
+        )
+    
+    def removeRecordForUID(self, uid):
+
+        self._db_execute("delete from NOTIFICATIONS where UID = :1", uid)
+    
+    def removeRecordForName(self, rname):
+
+        self._db_execute("delete from NOTIFICATIONS where NAME = :1", rname)
+    
+    def _db_version(self):
+        """
+        @return: the schema version assigned to this index.
+        """
+        return NotificationsDatabase.schema_version
+
+    def _db_type(self):
+        """
+        @return: the collection type assigned to this index.
+        """
+        return NotificationsDatabase.db_type
+
+    def _db_init_data_tables(self, q):
+        """
+        Initialise the underlying database tables.
+        @param q:           a database cursor to use.
+        """
+        #
+        # NOTIFICATIONS table is the primary table
+        #   UID: UID for this notification
+        #   NAME: child resource name
+        #   TYPE: type of notification
+        #
+        q.execute(
+            """
+            create table NOTIFICATIONS (
+                UID            text unique,
+                NAME           text unique,
+                TYPE           text
+            )
+            """
+        )
+
+        q.execute(
+            """
+            create index UID on NOTIFICATIONS (UID)
+            """
+        )
+
+    def _db_upgrade_data_tables(self, q, old_version):
+        """
+        Upgrade the data from an older version of the DB.
+        """
+
+        # Nothing to do as we have not changed the schema
+        pass
+
+    def _makeRecord(self, row):
+        
+        return NotificationRecord(*[str(item) if type(item) == types.UnicodeType else item for item in row])
+

Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/resource.py	2010-03-05 21:15:53 UTC (rev 5260)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/resource.py	2010-03-05 21:20:01 UTC (rev 5261)
@@ -429,6 +429,22 @@
 
                 # Check if adding or removing share
                 shared = (yield self.isShared(request))
+                for child in property.children:
+                    if child.qname() == davxml.Collection.qname():
+                        break
+                else:
+                    raise HTTPError(StatusResponse(
+                        responsecode.FORBIDDEN,
+                        "Protected property %s may not be set." % (property.sname(),)
+                    ))
+                for child in property.children:
+                    if child.qname() == caldavxml.Calendar.qname():
+                        break
+                else:
+                    raise HTTPError(StatusResponse(
+                        responsecode.FORBIDDEN,
+                        "Protected property %s may not be set." % (property.sname(),)
+                    ))
                 sawShare = [child for child in property.children if child.qname() == (calendarserver_namespace, "shared-owner")]
                 if not shared and sawShare:
                     # Owner is trying to share a collection
@@ -936,8 +952,11 @@
             lastpath = path.split("/")[-1]
             
             parent = (yield request.locateResource(parentForURL(myurl)))
-            canonical_parent = (yield parent.canonicalURL(request))
-            self._canonical_url = joinURL(canonical_parent, lastpath)
+            if parent:
+                canonical_parent = (yield parent.canonicalURL(request))
+                self._canonical_url = joinURL(canonical_parent, lastpath)
+            else:
+                self._canonical_url = myurl
 
         returnValue(self._canonical_url)
 

Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py	2010-03-05 21:15:53 UTC (rev 5260)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py	2010-03-05 21:20:01 UTC (rev 5261)
@@ -13,18 +13,22 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from twisted.internet.defer import succeed, inlineCallbacks, DeferredList,\
-    returnValue, fail
+
+from twext.python.log import LoggingMixIn
 from twext.web2 import responsecode
-from twext.web2.http import HTTPError, Response, StatusResponse
+from twext.web2.dav.element.base import PCDATAElement
 from twext.web2.dav.http import ErrorResponse, MultiStatusResponse
 from twext.web2.dav.util import allDataFromStream
-from twext.web2.dav.element.base import PCDATAElement
+from twext.web2.http import HTTPError, Response, StatusResponse
+from twisted.internet.defer import succeed, inlineCallbacks, DeferredList,\
+    returnValue, fail
+from twistedcaldav.config import config
 from twistedcaldav.sql import AbstractSQLDatabase, db_prefix
-from twext.python.log import LoggingMixIn
+from uuid import uuid4
+from vobject.icalendar import dateTimeToString, utc
+import datetime
 import os
-from twistedcaldav.config import config
-from uuid import uuid4
+import types
 
 __all__ = [
     "SharingMixin",
@@ -104,6 +108,18 @@
         """ Return True if this is a shared calendar collection """
         return succeed(self.isSpecialCollection(customxml.Shared))
 
+    def sharedResourceType(self):
+        """
+        Return the DAV:resourcetype stripped of any shared elements.
+        """
+        
+        rtype = self.resourceType()
+        newchildren = [child for child in rtype.children if child.qname() not in (
+            customxml.SharedOwner.qname(),
+        )] if rtype.children else ()
+        rtype.children = newchildren if newchildren else None
+        return rtype
+
     def validUserIDForShare(self, userid):
         """
         Test the user id to see if it is a valid identifier for sharing and return a "normalized"
@@ -162,7 +178,6 @@
             return succeed(False)
 
         # TODO: Check if this collection is shared, and error out if it isn't
-        hosturl = self.fp.path
         if type(userid) is not list:
             userid = [userid]
         if type(commonName) is not list:
@@ -170,7 +185,7 @@
         if type(shareName) is not list:
             shareName = [shareName]
             
-        dl = [self.inviteSingleUserToShare(user, ace, summary, hosturl, request, cn=cn, sn=sn) for user, cn, sn in zip(userid, commonName, shareName)]
+        dl = [self.inviteSingleUserToShare(user, ace, summary, request, cn=cn, sn=sn) for user, cn, sn in zip(userid, commonName, shareName)]
         return DeferredList(dl).addCallback(lambda _:True)
 
     def uninviteUserToShare(self, userid, ace, request):
@@ -192,25 +207,34 @@
         if userid is None:
             return succeed(False)
 
-        hosturl = self.fp.path
         if type(userid) is not list:
             userid = [userid]
         if type(commonName) is not list:
             commonName = [commonName]
         if type(shareName) is not list:
             shareName = [shareName]
-        dl = [self.inviteSingleUserUpdateToShare(user, aceOLD, aceNEW, summary, hosturl, request, commonName=cn, shareName=sn) for user, cn, sn in zip(userid, commonName, shareName)]
+        dl = [self.inviteSingleUserUpdateToShare(user, aceOLD, aceNEW, summary, request, commonName=cn, shareName=sn) for user, cn, sn in zip(userid, commonName, shareName)]
         return DeferredList(dl).addCallback(lambda _:True)
 
-    def inviteSingleUserToShare(self, userid, ace, summary, hosturl, request, cn="", sn=""):
+    @inlineCallbacks
+    def inviteSingleUserToShare(self, userid, ace, summary, request, cn="", sn=""):
         
+        # Look for existing invite and update its fields or create new one
+        record = self.invitesDB().recordForUserID(userid)
+        if record:
+            record.sequence += 1
+            record.access = inviteAccessMapFromXML[type(ace)]
+            record.summary = summary
+        else:
+            record = Invite(str(uuid4()), 1, userid, inviteAccessMapFromXML[type(ace)], "NEEDS-ACTION", summary)
+        
         # Send invite
-        inviteuid = str(uuid4())
+        yield self.sendInvite(record, request)
         
         # Add to database
-        self.invitesDB().addOrUpdateRecord(Invite(inviteuid, userid, inviteAccessMapFromXML[type(ace)], "NEEDS-ACTION", summary))
+        self.invitesDB().addOrUpdateRecord(record)
         
-        return succeed(True)            
+        returnValue(True)            
 
     def uninviteSingleUserFromShare(self, userid, aces, request):
         
@@ -221,11 +245,54 @@
         
         return succeed(True)            
 
-    def inviteSingleUserUpdateToShare(self, userid, acesOLD, aceNEW, summary, hosturl, request, commonName="", shareName=""):
+    def inviteSingleUserUpdateToShare(self, userid, acesOLD, aceNEW, summary, request, commonName="", shareName=""):
         
         # Just update existing
-        return self.inviteSingleUserToShare(userid, aceNEW, summary, hosturl, request, commonName, shareName) 
+        return self.inviteSingleUserToShare(userid, aceNEW, summary, request, commonName, shareName) 
 
+    @inlineCallbacks
+    def sendInvite(self, record, request):
+        
+        owner = (yield self.ownerPrincipal(request))
+        owner = owner.principalURL()
+        hosturl = (yield self.canonicalURL(request))
+
+        # Locate notifications collection for user
+        sharee = self.principalForCalendarUserAddress(record.userid)
+        if sharee is None:
+            raise ValueError("sharee is None but userid was valid before")
+        notifications = (yield request.locateResource(sharee.notificationURL()))
+        
+        # Look for existing notification
+        oldnotification = (yield notifications.getNotifictionMessageByUID(request, record.inviteuid))
+        if oldnotification:
+            # TODO: rollup changes?
+            pass
+        
+        # Generate invite XML
+        xmltype = customxml.InviteNotification.name
+        xmldata = customxml.Notification(
+            customxml.DTStamp.fromString(dateTimeToString(datetime.datetime.now(tz=utc))),
+            customxml.InviteNotification(
+                davxml.HRef.fromString(record.userid),
+                inviteStatusMapToXML[record.state](),
+                customxml.InviteAccess(inviteAccessMapToXML[record.access]()),
+                customxml.HostURL(
+                    davxml.HRef.fromString(hosturl),
+                ),
+                self.sharedResourceType(),
+                customxml.Organizer(
+                    davxml.HRef.fromString(owner),
+                ),
+                customxml.InviteSummary.fromString(record.summary),
+                customxml.UID.fromString(record.inviteuid),
+                customxml.Sequence.fromString(str(record.sequence)),
+            ),
+        ).toxml()
+        
+        # Add to collections
+        yield notifications.addNotification(request, record.inviteuid, xmltype, xmldata)
+
     def xmlPOSTNoAuth(self, encoding, request):
         def _handleErrorResponse(error):
             if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
@@ -429,8 +496,9 @@
 
 class Invite(object):
     
-    def __init__(self, inviteuid, userid, access, state, summary):
+    def __init__(self, inviteuid, sequence, userid, access, state, summary):
         self.inviteuid = inviteuid
+        self.sequence = sequence
         self.userid = userid
         self.access = access
         self.state = state
@@ -458,7 +526,7 @@
         """
         self.resource = resource
         db_filename = os.path.join(self.resource.fp.path, InvitesDatabase.db_basename)
-        super(InvitesDatabase, self).__init__(db_filename, True)
+        super(InvitesDatabase, self).__init__(db_filename, True, autocommit=True)
 
     def create(self):
         """
@@ -473,19 +541,19 @@
     
     def recordForUserID(self, userid):
         
-        row = self._db_value_for_sql("select * from INVITE where USERID = :1", userid)
-        return self._makeRecord(row) if row else None
+        row = self._db_execute("select * from INVITE where USERID = :1", userid)
+        return self._makeRecord(row[0]) if row else None
     
     def recordForInviteUID(self, inviteUID):
 
-        row = self._db_value_for_sql("select * from INVITE where INVITEUID = :1", inviteUID)
-        return self._makeRecord(row) if row else None
+        row = self._db_execute("select * from INVITE where INVITEUID = :1", inviteUID)
+        return self._makeRecord(row[0]) if row else None
     
     def addOrUpdateRecord(self, record):
 
-        self._db_execute("""insert or replace into INVITE (USERID, INVITEUID, ACCESS, STATE, SUMMARY)
-            values (:1, :2, :3, :4, :5)
-            """, record.userid, record.inviteuid, record.access, record.state, record.summary,
+        self._db_execute("""insert or replace into INVITE (INVITEUID, SEQUENCE, USERID, ACCESS, STATE, SUMMARY)
+            values (:1, :2, :3, :4, :5, :6)
+            """, record.inviteuid, record.sequence, record.userid, record.access, record.state, record.summary,
         )
     
     def removeRecordForUserID(self, userid):
@@ -520,8 +588,9 @@
         """
         #
         # INVITE table is the primary table
+        #   INVITEUID: UID for this invite
+        #   SEQUENCE: sequence number for this invite
         #   NAME: identifier of invitee
-        #   INVITEUID: UID for this invite
         #   ACCESS: Access mode for share
         #   STATE: Invite response status
         #   SUMMARY: Invite summary
@@ -530,6 +599,7 @@
             """
             create table INVITE (
                 INVITEUID      text unique,
+                SEQUENCE       integer,
                 USERID         text unique,
                 ACCESS         text,
                 STATE          text,
@@ -559,5 +629,5 @@
 
     def _makeRecord(self, row):
         
-        return Invite(*row)
+        return Invite(*[str(item) if type(item) == types.UnicodeType else item for item in row])
 

Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/static.py	2010-03-05 21:15:53 UTC (rev 5260)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/static.py	2010-03-05 21:20:01 UTC (rev 5261)
@@ -47,7 +47,6 @@
 from uuid import uuid4
 
 from twext.python.log import Logger
-from twext.web2.dav.http import ErrorResponse
 
 from twisted.internet.defer import fail, succeed, inlineCallbacks, returnValue, maybeDeferred
 from twisted.python.failure import Failure
@@ -57,11 +56,13 @@
 from twext.web2.dav import davxml
 from twext.web2.dav.element.base import dav_namespace
 from twext.web2.dav.fileop import mkcollection, rmdir
+from twext.web2.dav.http import ErrorResponse
 from twext.web2.dav.idav import IDAVResource
 from twext.web2.dav.noneprops import NonePropertyStore
 from twext.web2.dav.resource import AccessDeniedError
 from twext.web2.dav.resource import davPrivilegeSet
 from twext.web2.dav.util import parentForURL, bindMethods, joinURL
+from twext.web2.http_headers import generateContentType, MimeType
 
 from twistedcaldav import caldavxml
 from twistedcaldav import carddavxml
@@ -1309,6 +1310,13 @@
     def __repr__(self):
         return "<%s (notification collection): %s>" % (self.__class__.__name__, self.fp.path)
 
+    def _writeNotification(self, request, uid, rname, xmltype, xmldata):
+        
+        child = self.createSimilarFile(self.fp.child(rname).path)
+        child.fp.setContent(xmldata)
+        child.writeDeadProperty(davxml.GETContentType.fromString(generateContentType(MimeType("text", "xml", params={"charset":"utf-8"}))))
+        child.writeDeadProperty(customxml.NotificationType.fromString(xmltype))
+
 class NotificationFile(NotificationResource, CalDAVFile):
 
     def __init__(self, path, parent):
@@ -1325,6 +1333,9 @@
 
     def __repr__(self):
         return "<%s (notification file): %s>" % (self.__class__.__name__, self.fp.path)
+        
+    def resourceName(self):
+        return self.fp.basename()
 
 class AddressBookHomeProvisioningFile (DirectoryAddressBookHomeProvisioningResource, DAVFile):
     """

Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/test/test_sharing.py	2010-03-05 21:15:53 UTC (rev 5260)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/test/test_sharing.py	2010-03-05 21:20:01 UTC (rev 5261)
@@ -20,7 +20,7 @@
 from twext.web2.http_headers import MimeType
 from twext.web2.stream import MemoryStream
 from twext.web2.test.test_server import SimpleRequest
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from twistedcaldav import customxml
 from twistedcaldav.config import config
 from twistedcaldav.static import CalDAVFile
@@ -44,6 +44,7 @@
         self.site.resource.putChild("calendar", self.resource)
         
         self.resource.validUserIDForShare = self._fakeValidUserID
+        self.resource.sendInvite = lambda record, request:succeed(True)
         
     def _fakeValidUserID(self, userid):
         if userid.endswith("@example.com"):
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100305/9337a7d3/attachment-0001.html>


More information about the calendarserver-changes mailing list