[CalendarServer-changes] [10001] CalendarServer/branches/users/cdaboo/managed-attachments
source_changes at macosforge.org
source_changes at macosforge.org
Fri Nov 2 13:50:49 PDT 2012
Revision: 10001
http://trac.calendarserver.org//changeset/10001
Author: cdaboo at apple.com
Date: 2012-11-02 13:50:49 -0700 (Fri, 02 Nov 2012)
Log Message:
-----------
Latest snapshot of managed attachments.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/ical.py
CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py
CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/test/test_icalendar.py
CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py
CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/ical.py 2012-11-02 20:40:27 UTC (rev 10000)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/ical.py 2012-11-02 20:50:49 UTC (rev 10001)
@@ -67,9 +67,9 @@
"VEVENT",
"VTODO",
"VTIMEZONE",
- #"VJOURNAL",
+ # "VJOURNAL",
"VFREEBUSY",
- #"VAVAILABILITY",
+ # "VAVAILABILITY",
)
# 2445 default values and parameters
@@ -1640,8 +1640,8 @@
timezone_refs = set()
timezones = set()
got_master = False
- #got_override = False
- #master_recurring = False
+ # got_override = False
+ # master_recurring = False
for subcomponent in self.subcomponents():
if subcomponent.name() == "VTIMEZONE":
@@ -1791,9 +1791,9 @@
else:
return None
- ##
+ # #
# iTIP stuff
- ##
+ # #
def isValidMethod(self):
@@ -2250,6 +2250,77 @@
component.replaceProperty(property)
+ def hasPropertyWithParameterMatch(self, propname, param_name, param_value, param_value_is_default=False):
+ """
+ See if property whose name, and parameter name, value match in any components.
+
+ @param property: the L{Property} to replace in this component.
+ @param param_name: the C{str} of parameter name to match.
+ @param param_value: the C{str} of parameter value to match, if C{None} then just match on the
+ presence of the parameter name.
+ @param param_value_is_default: C{bool} to indicate whether absence of the named parameter
+ also implies a match
+
+ @return: C{True} if matching property found, C{False} if not
+ @rtype: C{bool}
+ """
+
+ if self.name() == "VCALENDAR":
+ for component in self.subcomponents():
+ if component.name() in ignoredComponents:
+ continue
+ if component.hasPropertyWithParameterMatch(propname, param_name, param_value, param_value_is_default):
+ return True
+ else:
+ for oldprop in tuple(self.properties(propname)):
+ pvalue = oldprop.parameterValue(param_name)
+ if pvalue is None and param_value_is_default or pvalue == param_value or param_value is None:
+ return True
+
+ return False
+
+
+ def replaceAllPropertiesWithParameterMatch(self, property, param_name, param_value, param_value_is_default=False):
+ """
+ Replace a property whose name, and parameter name, value match in all components.
+
+ @param property: the L{Property} to replace in this component.
+ @param param_name: the C{str} of parameter name to match.
+ @param param_value: the C{str} of parameter value to match.
+ @param param_value_is_default: C{bool} to indicate whether absence of the named parameter
+ also implies a match
+ """
+
+ if self.name() == "VCALENDAR":
+ for component in self.subcomponents():
+ if component.name() in ignoredComponents:
+ continue
+ component.replaceAllPropertiesWithParameterMatch(property, param_name, param_value, param_value_is_default)
+ else:
+ for oldprop in tuple(self.properties(property.name())):
+ pvalue = oldprop.parameterValue(param_name)
+ if pvalue is None and param_value_is_default or pvalue == param_value:
+ self.removeProperty(oldprop)
+ self.addProperty(property)
+
+
+ def removeAllPropertiesWithParameterMatch(self, propname, param_name, param_value, param_value_is_default=False):
+ """
+ Remove all properties whose name, and parameter name, value match in all components.
+ """
+
+ if self.name() == "VCALENDAR":
+ for component in self.subcomponents():
+ if component.name() in ignoredComponents:
+ continue
+ component.removeAllPropertiesWithParameterMatch(propname, param_name, param_value, param_value_is_default)
+ else:
+ for oldprop in tuple(self.properties(propname)):
+ pvalue = oldprop.parameterValue(param_name)
+ if pvalue is None and param_value_is_default or pvalue == param_value:
+ self.removeProperty(oldprop)
+
+
def transferProperties(self, from_calendar, properties):
"""
Transfer specified properties from old calendar into all components
@@ -3055,9 +3126,9 @@
-##
+# #
# Timezones
-##
+# #
def tzexpand(tzdata, start, end):
"""
@@ -3161,9 +3232,9 @@
-##
+# #
# Utilities
-##
+# #
def normalizeCUAddress(cuaddr, lookupFunction, principalFunction, toUUID=True):
# Check that we can lookup this calendar user address - if not
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py 2012-11-02 20:40:27 UTC (rev 10000)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py 2012-11-02 20:50:49 UTC (rev 10001)
@@ -1,5 +1,5 @@
# -*- test-case-name: twistedcaldav.test.test_wrapping -*-
-# #
+##
# Copyright (c) 2005-2012 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,7 @@
# 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.
-# #
+##
import time
import hashlib
@@ -30,12 +30,12 @@
from txdav.xml import element as davxml
from txdav.xml.base import dav_namespace, WebDAVUnknownElement, encodeXMLName
from txdav.base.propertystore.base import PropertyName
-from txdav.caldav.icalendarstore import QuotaExceeded, AttachmentStoreFailed
+from txdav.caldav.icalendarstore import QuotaExceeded, AttachmentStoreFailed, \
+ AttachmentStoreValidManagedID, AttachmentRemoveFailed
from txdav.common.icommondatastore import NoSuchObjectResourceError
from txdav.common.datastore.sql_tables import _BIND_MODE_READ, _BIND_MODE_WRITE
from txdav.idav import PropertyChangeNotAllowedError
-from twext.web2 import responsecode
from twext.web2.stream import ProducerStream, readStream, MemoryStream
from twext.web2.http import HTTPError, StatusResponse, Response
from twext.web2.http_headers import ETag, MimeType, MimeDisposition
@@ -398,7 +398,7 @@
if not self.exists():
log.debug("Resource not found: %s" % (self,))
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
depth = request.headers.getHeader("depth", "infinity")
if depth != "infinity":
@@ -510,7 +510,7 @@
"""
if not self.exists():
log.debug("Resource not found: %s" % (self,))
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
# Can not move outside of home or to existing collection
sourceURI = request.uri
@@ -562,7 +562,7 @@
testctag = testctag.split(">", 1)[0]
ctag = (yield self.getInternalSyncToken())
if testctag != ctag:
- raise HTTPError(StatusResponse(responsecode.PRECONDITION_FAILED, "CTag pre-condition failure"))
+ raise HTTPError(StatusResponse(PRECONDITION_FAILED, "CTag pre-condition failure"))
def checkReturnChanged(self, request):
@@ -588,7 +588,7 @@
components = self.componentsFromData(data)
if components is None:
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body"))
+ raise HTTPError(StatusResponse(BAD_REQUEST, "Could not parse valid data from request body"))
# Build response
xmlresponses = []
@@ -613,7 +613,7 @@
error = e.response.error
error = (error.namespace, error.name,)
except Exception:
- code = responsecode.BAD_REQUEST
+ code = BAD_REQUEST
if code is None:
@@ -627,7 +627,7 @@
davxml.GETETag.fromString(etag.generate()),
customxml.UID.fromString(component.resourceUID()),
),
- davxml.Status.fromResponseCode(responsecode.OK),
+ davxml.Status.fromResponseCode(OK),
)
)
)
@@ -640,7 +640,7 @@
davxml.GETETag.fromString(etag.generate()),
self.xmlDataElementType().fromTextData(dataChanged),
),
- davxml.Status.fromResponseCode(responsecode.OK),
+ davxml.Status.fromResponseCode(OK),
)
)
)
@@ -701,7 +701,7 @@
if href is None:
if xmldata is None:
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body without a DAV:Href present"))
+ raise HTTPError(StatusResponse(BAD_REQUEST, "Could not parse valid data from request body without a DAV:Href present"))
# Do privilege check on collection once
if checkedBindPrivelege is None:
@@ -721,9 +721,9 @@
ifmatch = str(ifmatch.children[0]) if len(ifmatch.children) == 1 else None
if delete is None:
if set_items is None:
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body - no set_items of delete operation"))
+ raise HTTPError(StatusResponse(BAD_REQUEST, "Could not parse valid data from request body - no set_items of delete operation"))
if xmldata is None:
- raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body for set_items operation"))
+ raise HTTPError(StatusResponse(BAD_REQUEST, "Could not parse valid data from request body for set_items operation"))
yield self.crudUpdate(request, str(href), xmldata, ifmatch, return_changed, xmlresponses)
updateCount += 1
else:
@@ -788,7 +788,7 @@
error = (error.namespace, error.name,)
except Exception:
- code = responsecode.BAD_REQUEST
+ code = BAD_REQUEST
if code is None:
etag = (yield newchild.etag())
@@ -800,7 +800,7 @@
davxml.GETETag.fromString(etag.generate()),
customxml.UID.fromString(component.resourceUID()),
),
- davxml.Status.fromResponseCode(responsecode.OK),
+ davxml.Status.fromResponseCode(OK),
)
)
)
@@ -827,7 +827,7 @@
updateResource = (yield request.locateResource(href))
if not updateResource.exists():
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
# Check privilege
yield updateResource.authorize(request, (davxml.Write(),))
@@ -835,7 +835,7 @@
# Check if match
etag = (yield updateResource.etag())
if ifmatch and ifmatch != etag.generate():
- raise HTTPError(responsecode.PRECONDITION_FAILED)
+ raise HTTPError(PRECONDITION_FAILED)
yield self.storeResourceData(request, updateResource, href, component, componentdata)
@@ -849,7 +849,7 @@
error = (error.namespace, error.name,)
except Exception:
- code = responsecode.BAD_REQUEST
+ code = BAD_REQUEST
if code is None:
xmlresponses.append(
@@ -859,7 +859,7 @@
davxml.PropertyContainer(
davxml.GETETag.fromString(etag.generate()),
),
- davxml.Status.fromResponseCode(responsecode.OK),
+ davxml.Status.fromResponseCode(OK),
)
)
)
@@ -885,12 +885,12 @@
deleteResource = (yield request.locateResource(href))
if not deleteResource.exists():
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
# Check if match
etag = (yield deleteResource.etag())
if ifmatch and ifmatch != etag.generate():
- raise HTTPError(responsecode.PRECONDITION_FAILED)
+ raise HTTPError(PRECONDITION_FAILED)
yield deleteResource.storeRemove(
request,
@@ -906,13 +906,13 @@
error = (error.namespace, error.name,)
except Exception:
- code = responsecode.BAD_REQUEST
+ code = BAD_REQUEST
if code is None:
xmlresponses.append(
davxml.StatusResponse(
davxml.HRef.fromString(href),
- davxml.Status.fromResponseCode(responsecode.OK),
+ davxml.Status.fromResponseCode(OK),
)
)
else:
@@ -971,7 +971,7 @@
# Validate them first - raise on failure
if not self.validSupportedComponents(components):
- raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Invalid CALDAV:supported-calendar-component-set"))
+ raise HTTPError(StatusResponse(FORBIDDEN, "Invalid CALDAV:supported-calendar-component-set"))
support_components = ",".join(sorted([comp.upper() for comp in components]))
return maybeDeferred(self._newStoreObject.setSupportedComponents, support_components)
@@ -1727,7 +1727,7 @@
if not self.exists():
log.debug("Resource not found: %s" % (self,))
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
stream = ProducerStream()
class StreamProtocol(Protocol):
@@ -1741,7 +1741,7 @@
self._newStoreAttachment.retrieve(StreamProtocol())
except IOError, e:
log.error("Unable to read attachment: %s, due to: %s" % (self, e,))
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
headers = {"content-type": self.contentType()}
headers["content-disposition"] = MimeDisposition("attachment", params={"filename": self.displayName()})
@@ -1757,7 +1757,7 @@
if not self.exists():
log.debug("Resource not found: %s" % (self,))
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
yield self._newStoreCalendarObject.removeAttachmentWithName(
self._newStoreAttachment.name()
@@ -1837,11 +1837,11 @@
def render(self, request):
if not self.exists():
log.debug("Resource not found: %s" % (self,))
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
output = yield self.component()
- response = Response(responsecode.OK, {}, str(output))
+ response = Response(OK, {}, str(output))
response.headers.setHeader("content-type", self.contentType())
returnValue(response)
@@ -1853,7 +1853,7 @@
"""
if not self.exists():
log.debug("Resource not found: %s" % (self,))
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
return self.storeRemove(request, True, request.uri)
@@ -1929,7 +1929,7 @@
self._newStoreObject.name()
)
except NoSuchObjectResourceError:
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
# Re-initialize to get stuff setup again now we have no object
self._initializeWithObject(None, self._newStoreParent)
@@ -2141,12 +2141,12 @@
# Resource must exist to allow attachment operations
if not self.exists():
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
def _getRIDs():
rids = request.args.get("rid")
if rids is not None:
- rids = rids.split(",")
+ rids = rids[0].split(",")
try:
rids = [PyCalendarDateTime.parseText(rid) if rid != "M" else None for rid in rids]
except ValueError:
@@ -2157,6 +2157,16 @@
))
return rids
+ def _getMID():
+ mid = request.args.get("managed-id")
+ if mid is None:
+ raise HTTPError(ErrorResponse(
+ FORBIDDEN,
+ (caldav_namespace, "valid-managed-id-parameter",),
+ "The managed-id parameter is missing from the request-URI",
+ ))
+ return mid[0]
+
def _getContentInfo():
content_type = request.headers.getHeader("content-type")
if content_type is None:
@@ -2191,8 +2201,9 @@
# Look for Prefer header
if "return-representation" in request.headers.getHeader("prefer", {}):
result = (yield self.render(request))
- result.code = responsecode.OK
+ result.code = OK
result.headers.setHeader("content-location", request.path)
+ result.headers.setHeader("location", location)
else:
result = Response(CREATED)
result.headers.setHeader("location", location)
@@ -2200,11 +2211,68 @@
returnValue(result)
elif action == "attachment-update":
- pass
+ mid = _getMID()
+ content_type, filename = _getContentInfo()
+ uri = "https://caldav.corp.apple.com:8443/calendars/__uids__/%s/attachments/%s"
+ try:
+ attachment, location = (yield self._newStoreObject.updateAttachment(uri, mid, content_type, filename, request.stream))
+ except AttachmentStoreValidManagedID:
+ raise HTTPError(ErrorResponse(
+ FORBIDDEN,
+ (caldav_namespace, "valid-managed-id-parameter",),
+ "The managed-id parameter does not refer to an attachment in this calendar object resource",
+ ))
+ except AttachmentStoreFailed:
+ raise HTTPError(ErrorResponse(
+ FORBIDDEN,
+ (caldav_namespace, "valid-attachment-update",),
+ "Could not store the supplied attachment",
+ ))
+ except QuotaExceeded:
+ raise HTTPError(ErrorResponse(
+ INSUFFICIENT_STORAGE_SPACE,
+ (dav_namespace, "quota-not-exceeded"),
+ "Could not store the supplied attachment because user quota would be exceeded",
+ ))
+ # Look for Prefer header
+ if "return-representation" in request.headers.getHeader("prefer", {}):
+ result = (yield self.render(request))
+ result.code = OK
+ result.headers.setHeader("content-location", request.path)
+ else:
+ result = Response(NO_CONTENT)
+ result.headers.setHeader("location", location)
+ result.headers.addRawHeader("Cal-Managed-ID", attachment.dropboxID())
+ returnValue(result)
+
elif action == "attachment-remove":
- pass
+ rids = _getRIDs()
+ mid = _getMID()
+ try:
+ yield self._newStoreObject.removeAttachment(rids, mid)
+ except AttachmentStoreValidManagedID:
+ raise HTTPError(ErrorResponse(
+ FORBIDDEN,
+ (caldav_namespace, "valid-managed-id-parameter",),
+ "The managed-id parameter does not refer to an attachment in this calendar object resource",
+ ))
+ except AttachmentRemoveFailed:
+ raise HTTPError(ErrorResponse(
+ FORBIDDEN,
+ (caldav_namespace, "valid-attachment-remove",),
+ "Could not remove the specified attachment",
+ ))
+ # Look for Prefer header
+ if "return-representation" in request.headers.getHeader("prefer", {}):
+ result = (yield self.render(request))
+ result.code = OK
+ result.headers.setHeader("content-location", request.path)
+ else:
+ result = Response(NO_CONTENT)
+ returnValue(result)
+
else:
raise HTTPError(ErrorResponse(
FORBIDDEN,
@@ -2579,7 +2647,7 @@
def http_GET(self, request):
if not self.exists():
log.debug("Resource not found: %s" % (self,))
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
returnValue(
Response(OK, {"content-type": self.contentType()},
@@ -2594,7 +2662,7 @@
"""
if not self.exists():
log.debug("Resource not found: %s" % (self,))
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
return self.storeRemove(request, request.uri)
@@ -2627,6 +2695,6 @@
except MemcacheLockTimeoutError:
raise HTTPError(StatusResponse(CONFLICT, "Resource: %s currently in use on the server." % (where,)))
except NoSuchObjectResourceError:
- raise HTTPError(responsecode.NOT_FOUND)
+ raise HTTPError(NOT_FOUND)
returnValue(NO_CONTENT)
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/test/test_icalendar.py 2012-11-02 20:40:27 UTC (rev 10000)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/test/test_icalendar.py 2012-11-02 20:50:49 UTC (rev 10001)
@@ -652,7 +652,7 @@
self.assertEqual(end, PyCalendarDateTime(2004, 11, 27))
break
- #test_component_timerange.todo = "recurrence expansion should give us no end date here"
+ # test_component_timerange.todo = "recurrence expansion should give us no end date here"
def test_parse_date(self):
@@ -694,7 +694,7 @@
"""
self.assertEqual(PyCalendarDuration.parseText("P15DT5H0M20S"), PyCalendarDuration(days=15, hours=5, minutes=0, seconds=20))
self.assertEqual(PyCalendarDuration.parseText("+P15DT5H0M20S"), PyCalendarDuration(days=15, hours=5, minutes=0, seconds=20))
- self.assertEqual(PyCalendarDuration.parseText("-P15DT5H0M20S"), PyCalendarDuration(days=-15, hours=-5, minutes=0, seconds=-20))
+ self.assertEqual(PyCalendarDuration.parseText("-P15DT5H0M20S"), PyCalendarDuration(days=15 * -1, hours=5 * -1, minutes=0, seconds=20 * -1))
self.assertEqual(PyCalendarDuration.parseText("P7W"), PyCalendarDuration(weeks=7))
@@ -8200,3 +8200,838 @@
for cuaddr, result in data:
new_cuaddr = normalizeCUAddress(cuaddr, lookupFunction, None, toUUID=True)
self.assertEquals(new_cuaddr, result)
+
+
+ def test_hasPropertyWithParameterMatch(self):
+
+ data = (
+ (
+ "1.1 - nothing to match, with param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ False,
+ ),
+ (
+ "1.2 - nothing to match, without param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", None, False,
+ False,
+ ),
+ (
+ "1.3 - match with param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ True,
+ ),
+ (
+ "1.4 - match without param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", None, False,
+ True,
+ ),
+ (
+ "1.5 - simple not match with param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=3;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ False,
+ ),
+ (
+ "1.6 - simple match with default param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", True,
+ True,
+ ),
+ (
+ "2.1 - overrides no match with param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ False,
+ ),
+ (
+ "2.2 - overrides no match without param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", None, False,
+ False,
+ ),
+ (
+ "2.3 - overrides match in all with param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ True,
+ ),
+ (
+ "2.4 - overrides match in all without param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", None, False,
+ True,
+ ),
+ (
+ "2.5 - match in one override with param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=3;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ True,
+ ),
+ (
+ "2.6 - match in one override without param value",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", None, False,
+ True,
+ ),
+ )
+
+ for title, calendar, property, param_name, param_value, param_default, result in data:
+ ical = Component.fromString(calendar)
+ has_property = ical.hasPropertyWithParameterMatch(property, param_name, param_value, param_default)
+ self.assertEqual(has_property, result, "Failed has property: %s" % (title,))
+
+
+ def test_replaceAllPropertiesWithParameterMatch(self):
+
+ data = (
+ (
+ "1.1 - nothing to change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ Property("ATTACH", "http://example.com/attachment", {"MANAGED-ID": "1", "MTAG": "2"}),
+ "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "1.2 - simple change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ Property("ATTACH", "http://example.com/attachment", {"MANAGED-ID": "1", "MTAG": "2"}),
+ "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=1;MTAG=2:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "1.3 - simple no change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=3;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ Property("ATTACH", "http://example.com/attachment", {"MANAGED-ID": "1", "MTAG": "2"}),
+ "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=3;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "1.4 - simple change default",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ Property("ATTACH", "http://example.com/attachment", {"MTAG": "2"}),
+ "MANAGED-ID", "1", True,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+ATTACH;MTAG=2:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.1 - overrides nothing to change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ Property("ATTACH", "http://example.com/attachment", {"MANAGED-ID": "1", "MTAG": "2"}),
+ "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.2 - overrides change in all",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ Property("ATTACH", "http://example.com/attachment", {"MANAGED-ID": "1", "MTAG": "2"}),
+ "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=1;MTAG=2:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=1;MTAG=2:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.3 - overrides change one",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=3;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ Property("ATTACH", "http://example.com/attachment", {"MANAGED-ID": "1", "MTAG": "2"}),
+ "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=3;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=1;MTAG=2:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ )
+
+ for title, calendar, property, param_name, param_value, param_default, result in data:
+ ical = Component.fromString(calendar)
+ ical.replaceAllPropertiesWithParameterMatch(property, param_name, param_value, param_default)
+ self.assertEqual(str(ical), result.replace("\n", "\r\n"), "Failed replace property: %s" % (title,))
+
+
+ def test_removeAllPropertiesWithParameterMatch(self):
+
+ data = (
+ (
+ "1.1 - nothing to change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "1.2 - simple change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "1.3 - simple no change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=3;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=3;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "1.4 - simple change default",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", True,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.1 - overrides nothing to change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.2 - overrides change in all",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.3 - overrides change one",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=3;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=1;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ "ATTACH", "MANAGED-ID", "1", False,
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T080000Z
+DTEND:20090101T090000Z
+ATTACH;MANAGED-ID=3;MTAG=1:http://example.com/attachment
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20090102T080000Z
+DTSTART:20090102T080000Z
+DTEND:20090102T090000Z
+ATTACH;MANAGED-ID=2;MTAG=1:http://example.com/attachment
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ )
+
+ for title, calendar, property, param_name, param_value, param_default, result in data:
+ ical = Component.fromString(calendar)
+ ical.removeAllPropertiesWithParameterMatch(property, param_name, param_value, param_default)
+ self.assertEqual(str(ical), result.replace("\n", "\r\n"), "Failed remove property: %s" % (title,))
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py 2012-11-02 20:40:27 UTC (rev 10000)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py 2012-11-02 20:50:49 UTC (rev 10001)
@@ -1,5 +1,5 @@
# -*- test-case-name: txdav.caldav.datastore.test.test_sql -*-
-# #
+##
# Copyright (c) 2010-2012 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,7 @@
# 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.
-# #
+##
from twext.web2.stream import readStream
from pycalendar.value import PyCalendarValue
@@ -49,7 +49,8 @@
from txdav.caldav.datastore.util import validateCalendarComponent, \
dropboxIDFromCalendarObject
from txdav.caldav.icalendarstore import ICalendarHome, ICalendar, ICalendarObject, \
- IAttachment, AttachmentStoreFailed
+ IAttachment, AttachmentStoreFailed, AttachmentStoreValidManagedID, \
+ AttachmentRemoveFailed
from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
CommonObjectResource, ECALENDARTYPE
from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator, \
@@ -285,7 +286,7 @@
@inlineCallbacks
def attachmentObjectWithName(self, name):
- attach = (yield Attachment.loadWithName(self._txn, name))
+ attach = (yield Attachment.load(self._txn, dropboxID=name))
returnValue(attach)
@@ -1175,6 +1176,13 @@
returnValue(component)
+ @inlineCallbacks
+ def remove(self):
+ # Need to also remove attachments
+ yield Attachment.removeAllReferencesTo(self._txn, self._resourceID)
+ yield super(CalendarObject, self).remove()
+
+
@classproperty
def _recurrenceMinMaxByIDQuery(cls): # @NoSelf
"""
@@ -1326,7 +1334,7 @@
attach = Property("ATTACH", location, params={
"MANAGED-ID": attachment.dropboxID(),
"MTAG": attachment.md5(),
- "FMT-TYPE": "%s/%s" % (attachment.contentType().mediaType, attachment.contentType().mediaSubtype),
+ "FMTTYPE": "%s/%s" % (attachment.contentType().mediaType, attachment.contentType().mediaSubtype),
"FILENAME": attachment.dispositionName(),
"SIZE": str(attachment.size()),
}, valuetype=PyCalendarValue.VALUETYPE_URI)
@@ -1339,14 +1347,69 @@
returnValue((attachment, location,))
- def updateAttachment(self, managed_id, content_type, filename, stream):
- pass
+ @inlineCallbacks
+ def updateAttachment(self, pathpattern, managed_id, content_type, filename, stream):
+ # First check the supplied managed-id is associated with this resource
+ cobjs = (yield Attachment.referencesTo(self._txn, managed_id))
+ if self._resourceID not in cobjs:
+ raise AttachmentStoreValidManagedID
+ # Next write the data stream to existing attachment
+
+ # We need to know the resource_ID of the home collection of the owner
+ # (not sharee) of this event
+ try:
+ attachment = (yield self.attachmentWithManagedID(managed_id))
+ if attachment is None:
+ self.log_error("Missing managed attachment even though ATTACHMENT_CALENDAR_OBJECT indicates it is present: %s" % (managed_id,))
+ raise AttachmentStoreFailed
+ t = attachment.store(content_type, filename)
+ yield readStream(stream, t.write)
+ except Exception, e:
+ self.log_error("Unable to store attachment: %s" % (e,))
+ raise AttachmentStoreFailed
+ yield t.loseConnection()
+
+ # Now try and adjust the actual calendar data
+ calendar = (yield self.component())
+
+ location = pathpattern % (self._parentCollection.ownerHome().name(), attachment.dropboxID(),)
+ attach = Property("ATTACH", location, params={
+ "MANAGED-ID": attachment.dropboxID(),
+ "MTAG": attachment.md5(),
+ "FMTTYPE": "%s/%s" % (attachment.contentType().mediaType, attachment.contentType().mediaSubtype),
+ "FILENAME": attachment.dispositionName(),
+ "SIZE": str(attachment.size()),
+ }, valuetype=PyCalendarValue.VALUETYPE_URI)
+ calendar.replaceAllPropertiesWithParameterMatch(attach, "MANAGED-ID", managed_id)
+
+ # Store the data
+ yield self.setComponent(calendar)
+
+ returnValue((attachment, location,))
+
+
+ @inlineCallbacks
def removeAttachment(self, rids, managed_id):
- pass
+ # First check the supplied managed-id is associated with this resource
+ cobjs = (yield Attachment.referencesTo(self._txn, managed_id))
+ if self._resourceID not in cobjs:
+ raise AttachmentStoreValidManagedID
+ # Now try and adjust the actual calendar data
+ calendar = (yield self.component())
+ calendar.removeAllPropertiesWithParameterMatch("ATTACH", "MANAGED-ID", managed_id)
+
+ # Store the data
+ yield self.setComponent(calendar)
+
+ # Remove it - this will take care of actually removing it from the store if there are
+ # no more references to the attachment
+ yield self.removeManagedAttachmentWithName(managed_id)
+
+
@inlineCallbacks
def createManagedAttachment(self):
@@ -1361,7 +1424,17 @@
))
+ def attachmentWithManagedID(self, managed_id):
+ return Attachment.load(self._txn, dropboxID=managed_id)
+
+
@inlineCallbacks
+ def removeManagedAttachmentWithName(self, managed_id):
+ attachment = (yield self.attachmentWithManagedID(managed_id))
+ yield attachment.removeFromResource(self._resourceID)
+
+
+ @inlineCallbacks
def createAttachmentWithName(self, name):
# We need to know the resource_ID of the home collection of the owner
@@ -1382,7 +1455,7 @@
def attachmentWithName(self, name):
- return Attachment.loadWithName(self._txn, self._dropboxID, name)
+ return Attachment.load(self._txn, dropboxID=self._dropboxID, name=name)
def attendeesCanManageAttachments(self):
@@ -1543,12 +1616,15 @@
self._txn = txn
self._attachmentID = a_id
self._attachmentStatus = status
+ self._ownerHomeID = ownerHomeID
self._dropboxID = dropboxID
- self._name = name
- self._ownerHomeID = ownerHomeID
+ self._contentType = None
self._size = 0
+ self._md5 = None
self._created = None
self._modified = None
+ self._name = name
+ self._dispositionName = None
self._justCreated = justCreated
@@ -1622,13 +1698,50 @@
@classmethod
@inlineCallbacks
- def loadWithName(cls, txn, dropboxID, name="data"):
- attachment = cls(txn, None, None, dropboxID, name)
+ def load(cls, txn, dropboxID=None, name="data", attachmentID=None):
+ attachment = cls(txn, attachmentID, None, dropboxID, name)
attachment = (yield attachment.initFromStore())
returnValue(attachment)
+ @classmethod
@inlineCallbacks
+ def referencesTo(cls, txn, managedID):
+ """
+ Find all the calendar object resourceIds referenced by this supplied managed-id.
+ """
+ att = schema.ATTACHMENT
+ attco = schema.ATTACHMENT_CALENDAR_OBJECT
+ rows = (yield Select(
+ [attco.CALENDAR_OBJECT_RESOURCE_ID, ],
+ From=att.join(attco, att.ATTACHMENT_ID == attco.ATTACHMENT_ID, "inner"),
+ Where=(att.DROPBOX_ID == managedID),
+ ).on(txn))
+ cobjs = set([row[0] for row in rows]) if rows is not None else set()
+ returnValue(cobjs)
+
+
+ @classmethod
+ @inlineCallbacks
+ def removeAllReferencesTo(cls, txn, resourceID):
+ """
+ Remove all attachments referencing the specified resource.
+ """
+
+ # Find all reference attachment-ids and dereference
+ attco = schema.ATTACHMENT_CALENDAR_OBJECT
+ rows = (yield Select(
+ [attco.ATTACHMENT_ID, ],
+ From=attco,
+ Where=(attco.CALENDAR_OBJECT_RESOURCE_ID == resourceID),
+ ).on(txn))
+ aids = set([row[0] for row in rows]) if rows is not None else set()
+ for aid in aids:
+ attachment = (yield Attachment.load(txn, attachmentID=aid))
+ (yield attachment.removeFromResource(resourceID))
+
+
+ @inlineCallbacks
def initFromStore(self):
"""
Execute necessary SQL queries to retrieve attributes.
@@ -1636,21 +1749,27 @@
@return: C{True} if this attachment exists, C{False} otherwise.
"""
att = schema.ATTACHMENT
+ if self._dropboxID is not None:
+ where = (att.DROPBOX_ID == self._dropboxID).And(
+ att.PATH == self._name)
+ else:
+ where = (att.ATTACHMENT_ID == self._attachmentID)
rows = (yield Select(
[
att.ATTACHMENT_ID,
att.STATUS,
+ att.DROPBOX_ID,
att.CALENDAR_HOME_RESOURCE_ID,
att.CONTENT_TYPE,
att.SIZE,
att.MD5,
att.CREATED,
att.MODIFIED,
+ att.PATH,
att.DISPLAYNAME,
],
From=att,
- Where=(att.DROPBOX_ID == self._dropboxID).And(
- att.PATH == self._name)
+ Where=where
).on(self._txn))
if not rows:
@@ -1659,13 +1778,16 @@
row_iter = iter(rows[0])
self._attachmentID = row_iter.next()
self._attachmentStatus = row_iter.next()
+ self._dropboxID = row_iter.next()
self._ownerHomeID = row_iter.next()
self._contentType = MimeType.fromString(row_iter.next())
self._size = row_iter.next()
self._md5 = row_iter.next()
self._created = sqltime(row_iter.next())
self._modified = sqltime(row_iter.next())
+ self._name = row_iter.next()
self._dispositionName = row_iter.next()
+
returnValue(self)
@@ -1708,7 +1830,7 @@
@inlineCallbacks
def remove(self):
oldSize = self._size
- self._txn.postCommit(self._path.remove)
+ self._txn.postCommit(self.removePaths)
yield self._internalRemove()
# Adjust quota
home = (yield self._txn.calendarHomeWithResourceID(self._ownerHomeID))
@@ -1719,6 +1841,45 @@
yield home.notifyChanged()
+ def removePaths(self):
+ """
+ Remove the actual file and up to three parent directories if empty.
+ """
+ self._path.remove()
+ parent = self._path.parent()
+ for _ignore in range(3):
+ if len(parent.listdir()) == 0:
+ parent.remove()
+ parent = parent.parent()
+ else:
+ break
+
+
+ @inlineCallbacks
+ def removeFromResource(self, resourceID):
+
+ # This must only be called for a managed attachment
+ if self._attachmentStatus != _ATTACHMENT_STATUS_MANAGED:
+ raise AttachmentRemoveFailed
+
+ # Delete the reference
+ attco = schema.ATTACHMENT_CALENDAR_OBJECT
+ yield Delete(
+ From=attco,
+ Where=(attco.ATTACHMENT_ID == self._attachmentID).And(
+ attco.CALENDAR_OBJECT_RESOURCE_ID == resourceID),
+ ).on(self._txn)
+
+ # References still exist - if not remove actual attachment
+ rows = (yield Select(
+ [attco.CALENDAR_OBJECT_RESOURCE_ID, ],
+ From=attco,
+ Where=(attco.ATTACHMENT_ID == self._attachmentID),
+ ).on(self._txn))
+ if len(rows) == 0:
+ yield self.remove()
+
+
def _internalRemove(self):
"""
Just delete the row; don't do any accounting / bookkeeping. (This is
Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py 2012-11-02 20:40:27 UTC (rev 10000)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py 2012-11-02 20:50:49 UTC (rev 10001)
@@ -1,5 +1,5 @@
# -*- test-case-name: txdav.caldav.datastore -*-
-# #
+##
# Copyright (c) 2010-2012 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,7 @@
# 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 store interfaces
@@ -63,6 +63,20 @@
+class AttachmentStoreValidManagedID(Exception):
+ """
+ Specified attachment managed-id is not valid.
+ """
+
+
+
+class AttachmentRemoveFailed(Exception):
+ """
+ Unable to remove an attachment.
+ """
+
+
+
class QuotaExceeded(Exception):
"""
The quota for a particular user has been exceeded.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121102/347b4e90/attachment-0001.html>
More information about the calendarserver-changes
mailing list