[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