[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