[CalendarServer-changes] [5238] CalendarServer/branches/users/cdaboo/shared-calendars-5187/ twistedcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Wed Mar 3 17:50:43 PST 2010
Revision: 5238
http://trac.macosforge.org/projects/calendarserver/changeset/5238
Author: cdaboo at apple.com
Date: 2010-03-03 17:50:42 -0800 (Wed, 03 Mar 2010)
Log Message:
-----------
Store invitations (details of sharees) in a database. Support POST to change sharee state and
use db results for the CS:invite property.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/customxml.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/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-04 00:37:56 UTC (rev 5237)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/customxml.py 2010-03-04 01:50:42 UTC (rev 5238)
@@ -576,6 +576,20 @@
namespace = calendarserver_namespace
name = "auto-schedule"
+class ReadAccess (davxml.WebDAVEmptyElement):
+ """
+ Denotes read and update attendee partstat on a shared calendar.
+ """
+ namespace = calendarserver_namespace
+ name = "read"
+
+class ReadWriteAccess (davxml.WebDAVEmptyElement):
+ """
+ Denotes read and write access on a shared calendar.
+ """
+ namespace = calendarserver_namespace
+ name = "read-write"
+
class UID (davxml.WebDAVTextElement):
namespace = calendarserver_namespace
name = "uid"
@@ -660,9 +674,13 @@
name = "user"
allowed_children = {
- (calendarserver_namespace, "summary" ) : (0, 1),
- (calendarserver_namespace, "attendee" ) : (1, 1),
- (calendarserver_namespace, "access" ) : (0, 1),
+ (calendarserver_namespace, "href" ) : (1, 1),
+ (calendarserver_namespace, "access" ) : (0, 1),
+ (calendarserver_namespace, "summary" ) : (0, 1),
+ (calendarserver_namespace, "invite-noresponse" ) : (0, 1),
+ (calendarserver_namespace, "invite-deleted" ) : (0, 1),
+ (calendarserver_namespace, "invite-accepted" ) : (0, 1),
+ (calendarserver_namespace, "invite-declined" ) : (0, 1),
}
class InviteAccess (davxml.WebDAVElement):
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/resource.py 2010-03-04 00:37:56 UTC (rev 5237)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/resource.py 2010-03-04 01:50:42 UTC (rev 5238)
@@ -294,10 +294,6 @@
if p is not None:
returnValue(p)
- if qname == customxml.Invite.qname():
- isShared = (yield self.isShared(request))
- if not isShared:
- returnValue(None)
res = (yield self._readGlobalProperty(qname, property, request))
returnValue(res)
@@ -351,9 +347,8 @@
elif namespace == calendarserver_namespace:
if name == "invite":
- shared = (yield self.isShared(request))
- if not shared:
- returnValue(None)
+ result = (yield self.inviteProperty(request))
+ returnValue(result)
result = (yield super(CalDAVResource, self).readProperty(property, request))
returnValue(result)
Modified: CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py 2010-03-04 00:37:56 UTC (rev 5237)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py 2010-03-04 01:50:42 UTC (rev 5238)
@@ -13,7 +13,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
-from twisted.internet.defer import succeed
+from twisted.internet.defer import succeed, inlineCallbacks, DeferredList
+from twext.web2 import responsecode
+from twext.web2.http import HTTPError, Response
+from twext.web2.dav.http import ErrorResponse
+from twext.web2.dav.util import allDataFromStream
+from twext.web2.dav.element.base import PCDATAElement
+from twistedcaldav.sql import AbstractSQLDatabase, db_prefix
+from twext.python.log import LoggingMixIn
+import os
+from twistedcaldav.config import config
+from uuid import uuid4
__all__ = [
"SharingMixin",
@@ -28,6 +38,24 @@
class SharingMixin(object):
+ def invitesDB(self):
+
+ if not hasattr(self, "_invitesDB"):
+ self._invitesDB = InvitesDatabase(self)
+ return self._invitesDB
+
+ def inviteProperty(self, request):
+
+ # Build the CS:invite property from our DB
+ def sharedOK(isShared):
+ if config.EnableSharing and isShared:
+ return customxml.Invite(
+ *[record.makePropertyElement() for record in self.invitesDB().allRecords()]
+ )
+ else:
+ return None
+ return self.isShared(request).addCallback(sharedOK)
+
def upgradeToShare(self, request):
""" Upgrade this collection to a shared state """
@@ -36,8 +64,8 @@
rtype = davxml.ResourceType(*(rtype.children + (customxml.SharedOwner(),)))
self.writeDeadProperty(rtype)
- # Create empty invite property
- self.writeDeadProperty(customxml.Invite())
+ # Create invites database
+ self.invitesDB().create()
return succeed(True)
@@ -50,17 +78,16 @@
# Remove all invitees
- # Remove invite property
- self.removeDeadProperty(customxml.Invite)
+ # Remove invites database
+ self.invitesDB().remove()
+ delattr(self, "_invitesDB")
return succeed(True)
- def addUserToShare(self, userid, request, ace):
- """ Add a user to this shared calendar """
- return succeed(True)
+ def removeUserFromInvite(self, userid, request):
+ """ Remove a user from this shared calendar """
+ self.invitesDB().removeRecordForUserID(userid)
- def removeUserFromShare(self, userid, request):
- """ Remove a user from this shared calendar """
return succeed(True)
def isShared(self, request):
@@ -81,3 +108,369 @@
def sendNotificationOnChange(self, icalendarComponent, request, state="added"):
""" Possibly send a push and or email notification on a change to a resource in a shared collection """
return succeed(True)
+
+ def inviteUserToShare(self, userid, ace, summary, request, commonName="", shareName="", add=True):
+ """ Send out in invite first, and then add this user to the share list
+ @param userid:
+ @param ace: Must be one of customxml.ReadWriteAccess or customxml.ReadAccess
+ """
+ # 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:
+ commonName = [commonName]
+ 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)]
+ return DeferredList(dl)
+
+ def uninviteUserToShare(self, userid, ace, request):
+ """ Send out in uninvite first, and then remove this user from the share list."""
+ # TODO: Check if this collection is shared, and error out if it isn't
+ if type(userid) is not list:
+ userid = [userid]
+ return DeferredList([self.uninviteSingleUserFromShare(user, ace, request) for user in userid])
+
+ def inviteUserUpdateToShare(self, userid, aceOLD, aceNEW, summary, request, commonName="", shareName=""):
+ 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)]
+ return DeferredList(dl)
+
+ def inviteSingleUserToShare(self, userid, ace, summary, hosturl, request, cn="", sn=""):
+
+ # Send invite
+ inviteuid = str(uuid4())
+
+ # Add to database
+ self.invitesDB().addOrUpdateRecord(Invite(inviteuid, userid, inviteAccessMapFromXML[type(ace)], "NEEDS-ACTION", summary))
+
+ return succeed(True)
+
+ def uninviteSingleUserFromShare(self, userid, aces, request):
+
+ # Cancel invites
+
+ # Remove from database
+ self.invitesDB().removeRecordForUserID(userid)
+
+ return succeed(True)
+
+ def inviteSingleUserUpdateToShare(self, userid, acesOLD, aceNEW, summary, hosturl, request, commonName="", shareName=""):
+
+ # Just update existing
+ return self.inviteSingleUserToShare(userid, aceNEW, summary, hosturl, request, commonName, shareName)
+
+ def xmlPOSTNoAuth(self, encoding, request):
+ def _handleResponse(result):
+ return Response(code=responsecode.OK)
+
+ def _handleErrorResponse(error):
+ if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
+ return error.value.response
+ return Response(code=responsecode.BAD_REQUEST)
+
+ def _handleInvite(invitedoc):
+ def _handleInviteSet(inviteset):
+ userid = None
+ access = None
+ summary = None
+ for item in inviteset.children:
+ if isinstance(item, davxml.HRef):
+ for attendeeItem in item.children:
+ if isinstance(attendeeItem, PCDATAElement):
+ userid = attendeeItem.data
+ continue
+ if isinstance(item, customxml.InviteSummary):
+ for summaryItem in item.children:
+ if isinstance(summaryItem, PCDATAElement):
+ summary = summaryItem.data
+ continue
+ if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
+ access = item
+ continue
+ if userid and access and summary:
+ return (userid, access, summary)
+ else:
+ if userid is None:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (customxml.calendarserver_namespace, "valid-request-content-type"),
+ "missing href: %s" % (inviteset,),
+ ))
+ if access is None:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (customxml.calendarserver_namespace, "valid-request-content-type"),
+ "missing access: %s" % (inviteset,),
+ ))
+ if summary is None:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (customxml.calendarserver_namespace, "valid-request-content-type"),
+ "missing summary: %s" % (inviteset,),
+ ))
+
+ def _handleInviteRemove(inviteremove):
+ userid = None
+ access = []
+ for item in inviteremove.children:
+ if isinstance(item, davxml.HRef):
+ for attendeeItem in item.children:
+ if isinstance(attendeeItem, PCDATAElement):
+ userid = attendeeItem.data
+ continue
+ if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
+ access.append(item)
+ continue
+ if userid is None:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (customxml.calendarserver_namespace, "valid-request-content-type"),
+ "missing href: %s" % (inviteremove,),
+ ))
+ if len(access) == 0:
+ access = None
+ else:
+ access = set(access)
+ return (userid, access)
+
+ def _autoShare(isShared, request):
+ if not isShared:
+ return self.upgradeToShare(request)
+ else:
+ return succeed(True)
+
+ @inlineCallbacks
+ def _processInviteDoc(_, request):
+ setDict, removeDict, updateinviteDict = {}, {}, {}
+ for item in invitedoc.children:
+ if isinstance(item, customxml.InviteSet):
+ userid, access, summary = _handleInviteSet(item)
+ setDict[userid] = (access, summary)
+ elif isinstance(item, customxml.InviteRemove):
+ userid, access = _handleInviteRemove(item)
+ removeDict[userid] = access
+
+ # Special case removing and adding the same user and treat that as an add
+ sameUseridInRemoveAndSet = [u for u in removeDict.keys() if u in setDict]
+ for u in sameUseridInRemoveAndSet:
+ removeACL = removeDict[u]
+ newACL, summary = setDict[u]
+ updateinviteDict[u] = (removeACL, newACL, summary)
+ del removeDict[u]
+ del setDict[u]
+ for userid, access in removeDict.iteritems():
+ yield self.uninviteUserToShare(userid, access, request)
+ for userid, (access, summary) in setDict.iteritems():
+ yield self.inviteUserToShare(userid, access, summary, request)
+ for userid, (removeACL, newACL, summary) in updateinviteDict.iteritems():
+ yield self.inviteUserUpdateToShare(userid, removeACL, newACL, summary, request)
+
+ return self.isShared(request).addCallback(_autoShare, request).addCallback(_processInviteDoc, request)
+
+ def _getData(data):
+ try:
+ doc = davxml.WebDAVDocument.fromString(data)
+ except ValueError, e:
+ self.log_error("Error parsing doc (%s) Doc:\n %s" % (str(e), data,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request-content")))
+
+ root = doc.root_element
+ xmlDocHanders = {
+ customxml.InviteShare: _handleInvite,
+ }
+ if type(root) in xmlDocHanders:
+ return xmlDocHanders[type(root)](root).addCallbacks(_handleResponse, errback=_handleErrorResponse)
+ else:
+ self.log_error("Unsupported XML (%s)" % (root,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request-content")))
+
+ return allDataFromStream(request.stream).addCallback(_getData)
+
+ def xmlPOSTPreconditions(self, _, request):
+ if request.headers.hasHeader("Content-Type"):
+ mimetype = request.headers.getHeader("Content-Type")
+ if mimetype.mediaType in ("application", "text",) and mimetype.mediaSubtype == "xml":
+ encoding = mimetype.params["charset"] if "charset" in mimetype.params else "utf8"
+ return succeed(encoding)
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request-content-type")))
+
+ def xmlPOSTAuth(self, request):
+ d = self.authorize(request, (davxml.Read(), davxml.Write()))
+ d.addCallback(self.xmlPOSTPreconditions, request)
+ d.addCallback(self.xmlPOSTNoAuth, request)
+ return d
+
+ def http_POST(self, request):
+ if self.isCollection():
+ contentType = request.headers.getHeader("content-type")
+ if contentType:
+ contentType = (contentType.mediaType, contentType.mediaSubtype)
+ if contentType in self._postHandlers:
+ return self._postHandlers[contentType](self, request)
+ else:
+ self.log_info("Get a POST of an unsupported content type on a collection type: %s" % (contentType,))
+ else:
+ self.log_info("Get a POST with no content type on a collection")
+ return responsecode.FORBIDDEN
+
+ _postHandlers = {
+ ("application", "xml") : xmlPOSTAuth,
+ ("text", "xml") : xmlPOSTAuth,
+ }
+
+inviteAccessMapToXML = {
+ "read-only" : customxml.ReadAccess,
+ "read-write" : customxml.ReadWriteAccess,
+}
+inviteAccessMapFromXML = dict([(v,k) for k,v in inviteAccessMapToXML.iteritems()])
+
+inviteAcceptMapToXML = {
+ "NEEDS-ACTION" : customxml.InviteStatusNoResponse,
+ "ACCEPTED" : customxml.InviteStatusAccepted,
+ "DECLINED" : customxml.InviteStatusDeclined,
+ "DELETED" : customxml.InviteStatusDeleted,
+}
+inviteAcceptMapFromXML = dict([(v,k) for k,v in inviteAcceptMapToXML.iteritems()])
+
+class Invite(object):
+
+ def __init__(self, inviteuid, userid, access, state, summary):
+ self.inviteuid = inviteuid
+ self.userid = userid
+ self.access = access
+ self.state = state
+ self.summary = summary
+
+ def makePropertyElement(self):
+
+ return customxml.InviteUser(
+ customxml.UID.fromString(self.inviteuid),
+ davxml.HRef.fromString(self.userid),
+ customxml.InviteAccess(inviteAccessMapToXML[self.access]()),
+ inviteAcceptMapToXML[self.state](),
+ )
+
+class InvitesDatabase(AbstractSQLDatabase, LoggingMixIn):
+
+ db_basename = db_prefix + "invites"
+ schema_version = "1"
+ db_type = "invites"
+
+ def __init__(self, resource):
+ """
+ @param resource: the L{twistedcaldav.static.CalDAVFile} resource for
+ the shared collection. C{resource} must be a calendar/addressbook collection.)
+ """
+ self.resource = resource
+ db_filename = os.path.join(self.resource.fp.path, InvitesDatabase.db_basename)
+ super(InvitesDatabase, self).__init__(db_filename, True)
+
+ def create(self):
+ """
+ Create the index and initialize it.
+ """
+ self._db()
+
+ def allRecords(self):
+
+ records = self._db_execute("select * from INVITE order by USERID")
+ return [self._makeRecord(row) for row in (records if records is not None else ())]
+
+ 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
+
+ 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
+
+ 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,
+ )
+
+ def removeRecordForUserID(self, userid):
+
+ self._db_execute("delete from INVITE where USERID = :1", userid)
+
+ def removeRecordForInviteUID(self, inviteUID):
+
+ self._db_execute("delete from INVITE where INVITEUID = :1", inviteUID)
+
+ def remove(self):
+
+ self._db_close()
+ os.remove(self.dbpath)
+
+ def _db_version(self):
+ """
+ @return: the schema version assigned to this index.
+ """
+ return InvitesDatabase.schema_version
+
+ def _db_type(self):
+ """
+ @return: the collection type assigned to this index.
+ """
+ return InvitesDatabase.db_type
+
+ def _db_init_data_tables(self, q):
+ """
+ Initialise the underlying database tables.
+ @param q: a database cursor to use.
+ """
+ #
+ # INVITE table is the primary table
+ # NAME: identifier of invitee
+ # INVITEUID: UID for this invite
+ # ACCESS: Access mode for share
+ # STATE: Invite response status
+ # SUMMARY: Invite summary
+ #
+ q.execute(
+ """
+ create table INVITE (
+ INVITEUID text unique,
+ USERID text unique,
+ ACCESS text,
+ STATE text,
+ SUMMARY text
+ )
+ """
+ )
+
+ q.execute(
+ """
+ create index USERID on INVITE (USERID)
+ """
+ )
+ q.execute(
+ """
+ create index INVITEUID on INVITE (INVITEUID)
+ """
+ )
+
+ 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 Invite(*row)
+
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-04 00:37:56 UTC (rev 5237)
+++ CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/test/test_sharing.py 2010-03-04 01:50:42 UTC (rev 5238)
@@ -20,28 +20,55 @@
from twext.web2.dav import davxml
from twistedcaldav.static import CalDAVFile
from twistedcaldav import customxml
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet.defer import inlineCallbacks, returnValue
import os
+from twistedcaldav.config import config
+from twext.web2.test.test_server import SimpleRequest
+from twext.web2.stream import MemoryStream
+from twext.web2.http_headers import MimeType
+from twext.web2 import responsecode
class SharingTests(TestCase):
def setUp(self):
TestCase.setUp(self)
+ config.EnableSharing = True
+
collection = self.mktemp()
os.mkdir(collection)
- self.resource = CalDAVFile(collection)
+ self.resource = CalDAVFile(collection, self.site.resource)
self.resource._dead_properties = InMemoryPropertyStore()
+ self.site.resource.putChild("calendar", self.resource)
@inlineCallbacks
+ def _doPOST(self, body):
+ request = SimpleRequest(self.site, "POST", "/calendar/")
+ request.headers.setHeader("content-type", MimeType("text", "xml"))
+ request.stream = MemoryStream(body)
+
+ response = (yield self.send(request, None))
+ self.assertEqual(response.code, responsecode.OK)
+
+ def _clearUIDElementValue(self, xml):
+
+ for user in xml.children:
+ for element in user.children:
+ if type(element) == customxml.UID:
+ element.children[0].data = ""
+ return xml
+
+ @inlineCallbacks
def test_upgradeToShare(self):
self.resource.writeDeadProperty(davxml.ResourceType.calendar)
self.assertEquals(self.resource.resourceType(), davxml.ResourceType.calendar)
- self.assertFalse(self.resource.hasDeadProperty(customxml.Invite()))
+ propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+ self.assertEquals(propInvite, None)
yield self.resource.upgradeToShare(None)
self.assertEquals(self.resource.resourceType(), davxml.ResourceType.sharedcalendar)
- self.assertEquals(self.resource.readDeadProperty(customxml.Invite), customxml.Invite())
+ propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+ self.assertEquals(propInvite, customxml.Invite())
isShared = (yield self.resource.isShared(None))
self.assertTrue(isShared)
@@ -53,14 +80,282 @@
self.resource.writeDeadProperty(davxml.ResourceType.sharedcalendar)
self.resource.writeDeadProperty(customxml.Invite())
self.assertEquals(self.resource.resourceType(), davxml.ResourceType.sharedcalendar)
- self.assertEquals(self.resource.readDeadProperty(customxml.Invite), customxml.Invite())
+ propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+ self.assertEquals(propInvite, customxml.Invite())
yield self.resource.downgradeFromShare(None)
self.assertEquals(self.resource.resourceType(), davxml.ResourceType.calendar)
- self.assertFalse(self.resource.hasDeadProperty(customxml.Invite()))
+ propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+ self.assertEquals(propInvite, None)
isShared = (yield self.resource.isShared(None))
self.assertFalse(isShared)
isVShared = (yield self.resource.isVirtualShare(None))
self.assertFalse(isVShared)
+
+ @inlineCallbacks
+ def test_POSTaddInviteeAlreadyShared(self):
+
+ yield self.resource.upgradeToShare(None)
+
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+</CS:share>
+""")
+
+ propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+ self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+ customxml.InviteUser(
+ customxml.UID.fromString(""),
+ davxml.HRef.fromString("mailto:user02 at example.com"),
+ customxml.InviteAccess(customxml.ReadWriteAccess()),
+ customxml.InviteStatusNoResponse(),
+ )
+ ))
+
+ isShared = (yield self.resource.isShared(None))
+ self.assertTrue(isShared)
+ isVShared = (yield self.resource.isVirtualShare(None))
+ self.assertFalse(isVShared)
+
+ @inlineCallbacks
+ def test_POSTaddInviteeNotAlreadyShared(self):
+
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+</CS:share>
+""")
+
+ propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+ self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+ customxml.InviteUser(
+ customxml.UID.fromString(""),
+ davxml.HRef.fromString("mailto:user02 at example.com"),
+ customxml.InviteAccess(customxml.ReadWriteAccess()),
+ customxml.InviteStatusNoResponse(),
+ )
+ ))
+
+ isShared = (yield self.resource.isShared(None))
+ self.assertTrue(isShared)
+ isVShared = (yield self.resource.isVirtualShare(None))
+ self.assertFalse(isVShared)
+
+ @inlineCallbacks
+ def test_POSTupdateInvitee(self):
+
+ yield self.resource.upgradeToShare(None)
+
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+</CS:share>
+""")
+
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read/>
+ </CS:set>
+</CS:share>
+""")
+
+ propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+ self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+ customxml.InviteUser(
+ customxml.UID.fromString(""),
+ davxml.HRef.fromString("mailto:user02 at example.com"),
+ customxml.InviteAccess(customxml.ReadAccess()),
+ customxml.InviteStatusNoResponse(),
+ )
+ ))
+
+ @inlineCallbacks
+ def test_POSTremoveInvitee(self):
+
+ yield self.resource.upgradeToShare(None)
+
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+</CS:share>
+""")
+
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:remove>
+ <D:href>mailto:user02 at example.com</D:href>
+ </CS:remove>
+</CS:share>
+""")
+
+ propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+ self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite())
+
+ @inlineCallbacks
+ def test_POSTaddMoreInvitees(self):
+
+ yield self.resource.upgradeToShare(None)
+
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+</CS:share>
+""")
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user03 at example.com</D:href>
+ <CS:summary>Your Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ <CS:set>
+ <D:href>mailto:user04 at example.com</D:href>
+ <CS:summary>Your Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+</CS:share>
+""")
+
+ propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+ self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+ customxml.InviteUser(
+ customxml.UID.fromString(""),
+ davxml.HRef.fromString("mailto:user02 at example.com"),
+ customxml.InviteAccess(customxml.ReadWriteAccess()),
+ customxml.InviteStatusNoResponse(),
+ ),
+ customxml.InviteUser(
+ customxml.UID.fromString(""),
+ davxml.HRef.fromString("mailto:user03 at example.com"),
+ customxml.InviteAccess(customxml.ReadWriteAccess()),
+ customxml.InviteStatusNoResponse(),
+ ),
+ customxml.InviteUser(
+ customxml.UID.fromString(""),
+ davxml.HRef.fromString("mailto:user04 at example.com"),
+ customxml.InviteAccess(customxml.ReadWriteAccess()),
+ customxml.InviteStatusNoResponse(),
+ ),
+ ))
+
+ @inlineCallbacks
+ def test_POSTaddRemoveInvitees(self):
+
+ yield self.resource.upgradeToShare(None)
+
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ <CS:set>
+ <D:href>mailto:user03 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+</CS:share>
+""")
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:remove>
+ <D:href>mailto:user03 at example.com</D:href>
+ </CS:remove>
+ <CS:set>
+ <D:href>mailto:user04 at example.com</D:href>
+ <CS:summary>Your Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+</CS:share>
+""")
+
+ propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+ self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+ customxml.InviteUser(
+ customxml.UID.fromString(""),
+ davxml.HRef.fromString("mailto:user02 at example.com"),
+ customxml.InviteAccess(customxml.ReadWriteAccess()),
+ customxml.InviteStatusNoResponse(),
+ ),
+ customxml.InviteUser(
+ customxml.UID.fromString(""),
+ davxml.HRef.fromString("mailto:user04 at example.com"),
+ customxml.InviteAccess(customxml.ReadWriteAccess()),
+ customxml.InviteStatusNoResponse(),
+ ),
+ ))
+
+ @inlineCallbacks
+ def test_POSTaddRemoveSameInvitee(self):
+
+ yield self.resource.upgradeToShare(None)
+
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:set>
+ <D:href>mailto:user02 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+ <CS:set>
+ <D:href>mailto:user03 at example.com</D:href>
+ <CS:summary>My Shared Calendar</CS:summary>
+ <CS:read-write/>
+ </CS:set>
+</CS:share>
+""")
+ yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+ <CS:remove>
+ <D:href>mailto:user03 at example.com</D:href>
+ </CS:remove>
+ <CS:set>
+ <D:href>mailto:user03 at example.com</D:href>
+ <CS:summary>Your Shared Calendar</CS:summary>
+ <CS:read/>
+ </CS:set>
+</CS:share>
+""")
+
+ propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+ self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+ customxml.InviteUser(
+ customxml.UID.fromString(""),
+ davxml.HRef.fromString("mailto:user02 at example.com"),
+ customxml.InviteAccess(customxml.ReadWriteAccess()),
+ customxml.InviteStatusNoResponse(),
+ ),
+ customxml.InviteUser(
+ customxml.UID.fromString(""),
+ davxml.HRef.fromString("mailto:user03 at example.com"),
+ customxml.InviteAccess(customxml.ReadAccess()),
+ customxml.InviteStatusNoResponse(),
+ ),
+ ))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100303/489d1692/attachment-0001.html>
More information about the calendarserver-changes
mailing list