[CalendarServer-changes] [14515] CalendarServer/branches/users/sagen/trashcan-5/txdav

source_changes at macosforge.org source_changes at macosforge.org
Thu Mar 5 14:11:06 PST 2015


Revision: 14515
          http://trac.calendarserver.org//changeset/14515
Author:   sagen at apple.com
Date:     2015-03-05 14:11:06 -0800 (Thu, 05 Mar 2015)
Log Message:
-----------
Trashed events now belong to the trash collection, with a reference to the original parent; collections can move to and from trash

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py
    CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/test/test_sql.py

Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py	2015-03-05 21:26:24 UTC (rev 14514)
+++ CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py	2015-03-05 22:11:06 UTC (rev 14515)
@@ -1045,6 +1045,8 @@
             cls._homeChildMetaDataSchema.CREATED,
             cls._homeChildMetaDataSchema.MODIFIED,
             cls._homeChildMetaDataSchema.CHILD_TYPE,
+            cls._homeChildMetaDataSchema.TRASHED,
+            cls._homeChildMetaDataSchema.IS_IN_TRASH,
         )
 
 
@@ -1063,6 +1065,8 @@
             "_created",
             "_modified",
             "_childType",
+            "_trashed",
+            "_isInTrash",
         )
 
 
@@ -2362,7 +2366,7 @@
             obj.MODIFIED,
             obj.DATAVERSION,
             obj.TRASHED,
-            obj.IS_IN_TRASH,
+            obj.ORIGINAL_COLLECTION,
         ]
 
 
@@ -2385,7 +2389,7 @@
             "_modified",
             "_dataversion",
             "_trashed",
-            "_is_in_trash",
+            "_original_collection",
         )
 
 
@@ -4908,7 +4912,7 @@
 
     @inlineCallbacks
     def fromTrash(self):
-        yield super(CalendarObject, self).fromTrash()
+        name = yield super(CalendarObject, self).fromTrash()
 
         caldata = yield self.componentForUser()
         organizer = caldata.getOrganizer()
@@ -4967,8 +4971,10 @@
                     self
                 )
 
+        returnValue(name)
 
 
+
 class AttachmentStorageTransport(StorageTransportBase):
 
     _TEMPORARY_UPLOADS_DIRECTORY = "Temporary"
@@ -5868,67 +5874,44 @@
 
     _childType = "trash"  # FIXME: make childType an enumeration
 
+    def isTrash(self):
+        return True
 
+
     @classproperty
-    def _trashInHomeQuery(cls):
+    def _trashForCollectionQuery(cls):
         obj = cls._objectSchema
-        bind = cls._bindSchema
         return Select(
-            [
-                obj.PARENT_RESOURCE_ID, obj.RESOURCE_ID
-            ],
-            From=obj.join(
-                bind, obj.PARENT_RESOURCE_ID == bind.RESOURCE_ID
-            ),
-            Where=(obj.IS_IN_TRASH == True).And(
-                bind.HOME_RESOURCE_ID == Parameter("resourceID")
-            ).And(
-                bind.BIND_MODE == _BIND_MODE_OWN
-            )
+            [obj.RESOURCE_ID], From=obj,
+            Where=(
+                obj.ORIGINAL_COLLECTION == Parameter("resourceID")).And(
+                obj.TRASHED >= Parameter("start")).And(
+                obj.TRASHED <= Parameter("end")),
         )
 
 
-    def isTrash(self):
-        return True
+    @inlineCallbacks
+    def trashForCollection(self, resourceID, start=None, end=None):
 
+        if start is None:
+            start = datetime.datetime(datetime.MINYEAR, 1, 1)
 
-    def nameForResource(self, collection, objectResource):
-        return "{}-{}".format(collection._resourceID, objectResource.name())
+        if end is None:
+            end = datetime.datetime.utcnow()
 
-    def originalParentForResource(self, objectResource):
-        parentID, resourceName = self.parseName(objectResource._name)
-        return self._home.childWithID(parentID)
+        results = yield self._trashForCollectionQuery.on(
+            self._txn, resourceID=resourceID, start=start, end=end
+        )
+        resources = []
+        for (objectResourceID,) in results:
+            resource = yield self.objectResourceWithID(objectResourceID)
+            resources.append(resource)
+        returnValue(resources)
 
-    def parseName(self, name):
-        parentID, resourceName = name.split("-", 1)
-        return int(parentID), resourceName
 
 
-    @inlineCallbacks
-    def listObjectResources(self):
-        """
-        Return a list of names of child object resources in this trash; the
-        list is computed from all the homeChildren in the trash's parent home.
-        """
-        home = self._calendarHome
 
-        results = []
-        rows = (yield self._trashInHomeQuery.on(
-            self._txn, resourceID=home._resourceID
-        ))
-        if rows:
-            for childID, objectID in rows:
-                child = (yield home.childWithID(childID))
-                if child:
-                    objectResource = (
-                        yield child.objectResourceWithID(objectID)
-                    )
-                    results.append(self.nameForResource(child, objectResource))
 
-        returnValue(results)
-
-
-
 # Hook-up class relationships at the end after they have all been defined
 from txdav.caldav.datastore.sql_external import CalendarHomeExternal, CalendarExternal, CalendarObjectExternal
 CalendarHome._externalClass = CalendarHomeExternal

Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py	2015-03-05 21:26:24 UTC (rev 14514)
+++ CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py	2015-03-05 22:11:06 UTC (rev 14515)
@@ -87,7 +87,7 @@
 
 from zope.interface import implements, directlyProvides
 
-from collections import namedtuple
+from collections import namedtuple, defaultdict
 import datetime
 import inspect
 import itertools
@@ -3033,7 +3033,7 @@
         self._status = _HOME_STATUS_NORMAL
         self._dataVersion = None
         self._childrenLoaded = False
-        self._children = {}
+        self._children = defaultdict(dict)
         self._notifiers = None
         self._quotaUsedBytes = None
         self._created = None
@@ -3425,17 +3425,21 @@
 
 
     @inlineCallbacks
-    def children(self):
+    def children(self, onlyInTrash=False):
         """
         Retrieve children contained in this home.
         """
         x = []
-        names = yield self.listChildren()
+        names = yield self.listChildren(onlyInTrash=onlyInTrash)
         for name in names:
-            x.append((yield self.childWithName(name)))
+            x.append((yield self.childWithName(name, onlyInTrash=onlyInTrash)))
         returnValue(x)
 
 
+    def _childrenKey(self, onlyInTrash):
+        return "TRASHED" if onlyInTrash else "NOTTRASHED"
+
+
     @inlineCallbacks
     def loadChildren(self):
         """
@@ -3443,27 +3447,29 @@
         """
         results = (yield self._childClass.loadAllObjects(self))
         for result in results:
-            self._children[result.name()] = result
-            self._children[result._resourceID] = result
+            key = self._childrenKey(result.isInTrash())
+            self._children[key][result.name()] = result
+            self._children[key][result._resourceID] = result
         self._childrenLoaded = True
         returnValue(results)
 
 
-    def listChildren(self):
+    @inlineCallbacks
+    def listChildren(self, onlyInTrash=False):
         """
         Retrieve the names of the children in this home.
 
         @return: an iterable of C{str}s.
         """
 
-        if self._childrenLoaded:
-            return succeed([k for k in self._children.keys() if isinstance(k, str)])
-        else:
-            return self._childClass.listObjects(self)
+        if not self._childrenLoaded:
+            yield self.loadChildren()
+        names = [k for k in self._children[self._childrenKey(onlyInTrash)].keys() if isinstance(k, str)]
+        returnValue(names)
 
 
-    @memoizedKey("name", "_children")
-    def childWithName(self, name):
+    @inlineCallbacks
+    def childWithName(self, name, onlyInTrash=False):
         """
         Retrieve the child with the given C{name} contained in this
         home.
@@ -3471,10 +3477,15 @@
         @param name: a string.
         @return: an L{ICalendar} or C{None} if no such child exists.
         """
-        return self._childClass.objectWithName(self, name)
+        childrenKey = self._childrenKey(onlyInTrash)
+        if name not in self._children[childrenKey]:
+            child = yield self._childClass.objectWithName(self, name, onlyInTrash=onlyInTrash)
+            if child is not None:
+                self._children[childrenKey][name] = child
+        returnValue(self._children[childrenKey].get(name, None))
 
 
-    def anyObjectWithShareUID(self, shareUID):
+    def anyObjectWithShareUID(self, shareUID, onlyInTrash=False):
         """
         Retrieve the child accepted or otherwise with the given bind identifier contained in this
         home.
@@ -3482,11 +3493,11 @@
         @param name: a string.
         @return: an L{ICalendar} or C{None} if no such child exists.
         """
-        return self._childClass.objectWithName(self, shareUID, accepted=None)
+        return self._childClass.objectWithName(self, shareUID, accepted=None, onlyInTrash=onlyInTrash)
 
 
-    @memoizedKey("resourceID", "_children")
-    def childWithID(self, resourceID):
+    @inlineCallbacks
+    def childWithID(self, resourceID, onlyInTrash=False):
         """
         Retrieve the child with the given C{resourceID} contained in this
         home.
@@ -3494,10 +3505,15 @@
         @param name: a string.
         @return: an L{ICalendar} or C{None} if no such child exists.
         """
-        return self._childClass.objectWithID(self, resourceID)
+        childrenKey = self._childrenKey(onlyInTrash)
+        if resourceID not in self._children[childrenKey]:
+            child = yield self._childClass.objectWithID(self, resourceID, onlyInTrash=onlyInTrash)
+            if child is not None:
+                self._children[childrenKey][resourceID] = child
+        returnValue(self._children[childrenKey].get(resourceID, None))
 
 
-    def childWithExternalID(self, externalID):
+    def childWithExternalID(self, externalID, onlyInTrash=False):
         """
         Retrieve the child with the given C{externalID} contained in this
         home.
@@ -3505,10 +3521,10 @@
         @param name: a string.
         @return: an L{ICalendar} or C{None} if no such child exists.
         """
-        return self._childClass.objectWithExternalID(self, externalID)
+        return self._childClass.objectWithExternalID(self, externalID, onlyInTrash=onlyInTrash)
 
 
-    def allChildWithID(self, resourceID):
+    def allChildWithID(self, resourceID, onlyInTrash=False):
         """
         Retrieve the child with the given C{resourceID} contained in this
         home.
@@ -3516,7 +3532,7 @@
         @param name: a string.
         @return: an L{ICalendar} or C{None} if no such child exists.
         """
-        return self._childClass.objectWithID(self, resourceID, accepted=None)
+        return self._childClass.objectWithID(self, resourceID, accepted=None, onlyInTrash=onlyInTrash)
 
 
     @inlineCallbacks
@@ -3536,8 +3552,9 @@
         resourceID = child._resourceID
 
         yield child.remove()
-        self._children.pop(name, None)
-        self._children.pop(resourceID, None)
+        for d in self._children:
+            d.pop(name, None)
+            d.pop(resourceID, None)
 
 
     @inlineCallbacks
@@ -5144,15 +5161,16 @@
 
     @inlineCallbacks
     def _changedStatus(self, previouslyAcceptedCount):
+        key = self._home._childrenKey(self.isInTrash())
         if self._bindStatus == _BIND_STATUS_ACCEPTED:
             yield self._initSyncToken()
             yield self._initBindRevision()
-            self._home._children[self._name] = self
-            self._home._children[self._resourceID] = self
+            self._home._children[key][self._name] = self
+            self._home._children[key][self._resourceID] = self
         elif self._bindStatus in (_BIND_STATUS_INVITED, _BIND_STATUS_DECLINED):
             yield self._deletedSyncToken(sharedRemoval=True)
-            self._home._children.pop(self._name, None)
-            self._home._children.pop(self._resourceID, None)
+            self._home._children[key].pop(self._name, None)
+            self._home._children[key].pop(self._resourceID, None)
 
 
     @inlineCallbacks
@@ -5167,12 +5185,13 @@
 
         @return: a L{Deferred} which will fire with the previous shareUID
         """
+        key = self._home._childrenKey(self.isInTrash())
 
         # remove sync tokens
         shareeHome = shareeView.viewerHome()
         yield shareeView._deletedSyncToken(sharedRemoval=True)
-        shareeHome._children.pop(shareeView._name, None)
-        shareeHome._children.pop(shareeView._resourceID, None)
+        shareeHome._children[key].pop(shareeView._name, None)
+        shareeHome._children[key].pop(shareeView._resourceID, None)
 
         # Must send notification to ensure cache invalidation occurs
         yield self.notifyPropertyChanged()
@@ -5766,22 +5785,6 @@
 
     @classmethod
     @inlineCallbacks
-    def listObjects(cls, home):
-        """
-        Retrieve the names of the children that exist in the given home.
-
-        @return: an iterable of C{str}s.
-        """
-        # FIXME: tests don't cover this as directly as they should.
-        rows = yield cls._acceptedBindForHomeID.on(
-            home._txn, homeID=home._resourceID
-        )
-        names = [row[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] for row in rows]
-        returnValue(names)
-
-
-    @classmethod
-    @inlineCallbacks
     def loadAllObjects(cls, home):
         """
         Load all L{CommonHomeChild} instances which are children of a given
@@ -5823,29 +5826,32 @@
 
 
     @classmethod
-    def objectWithName(cls, home, name, accepted=True):
-        return cls.objectWith(home, name=name, accepted=accepted)
+    def objectWithName(cls, home, name, accepted=True, onlyInTrash=False):
+        return cls.objectWith(home, name=name, accepted=accepted, onlyInTrash=onlyInTrash)
 
 
     @classmethod
-    def objectWithID(cls, home, resourceID, accepted=True):
-        return cls.objectWith(home, resourceID=resourceID, accepted=accepted)
+    def objectWithID(cls, home, resourceID, accepted=True, onlyInTrash=False):
+        return cls.objectWith(home, resourceID=resourceID, accepted=accepted, onlyInTrash=onlyInTrash)
 
 
     @classmethod
-    def objectWithExternalID(cls, home, externalID, accepted=True):
-        return cls.objectWith(home, externalID=externalID, accepted=accepted)
+    def objectWithExternalID(cls, home, externalID, accepted=True, onlyInTrash=False):
+        return cls.objectWith(home, externalID=externalID, accepted=accepted, onlyInTrash=onlyInTrash)
 
 
     @classmethod
     @inlineCallbacks
-    def objectWith(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
+    def objectWith(
+        cls, home, name=None, resourceID=None, externalID=None, accepted=True,
+        onlyInTrash=False
+    ):
         """
         Create the object using one of the specified arguments as the key to load it. One
         and only one of the keyword arguments must be set.
 
-        @param parent: parent collection
-        @type parent: L{CommonHomeChild}
+        @param home: home
+        @type home: L{CommonHome}
         @param name: name of the resource, or C{None}
         @type name: C{str}
         @param uid: resource data UID, or C{None}
@@ -5868,6 +5874,18 @@
         if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
             returnValue(None)
 
+        # Suppress if the collection is trash-capable and is in the trash
+        try:
+            isInTrash = metadataData[cls.metadataColumns().index(cls._homeChildMetaDataSchema.IS_IN_TRASH)]
+        except (AttributeError, ValueError):
+            isInTrash = False
+        if onlyInTrash:
+            if not isInTrash:
+                returnValue(None)
+        else:
+            if isInTrash:
+                returnValue(None)
+
         child = yield cls.makeClass(home, bindData, additionalBindData, metadataData)
         returnValue(child)
 
@@ -6074,8 +6092,9 @@
                                    homeID=self._home._resourceID)
         self._name = name
         # update memos
-        del self._home._children[oldName]
-        self._home._children[name] = self
+        key = self._home._childrenKey(self.isInTrash())
+        del self._home._children[key][oldName]
+        self._home._children[key][name] = self
         yield self._renameSyncToken()
 
         yield self.notifyPropertyChanged()
@@ -6098,7 +6117,8 @@
         """
 
         if config.EnableTrashCollection:
-            if self.isInTrash():
+            isInTrash = yield self.isInTrash()
+            if isInTrash:
                 raise AlreadyInTrashError
             else:
                 yield self.toTrash()
@@ -6130,6 +6150,110 @@
         yield self._home.notifyChanged()
 
 
+    @classproperty
+    def _updateIsInTrashQuery(cls):
+        table = cls._homeChildMetaDataSchema
+        return Update(
+            {table.IS_IN_TRASH: Parameter("isInTrash"), table.TRASHED: Parameter("trashed")},
+            Where=table.RESOURCE_ID == Parameter("resourceID"),
+        )
+
+
+    @inlineCallbacks
+    def toTrash(self):
+        # print("XYZZY collection toTrash")
+        for resource in (yield self.objectResources()):
+            yield resource.toTrash()
+        whenTrashed = datetime.datetime.utcnow()
+        yield self._updateIsInTrashQuery.on(
+            self._txn, isInTrash=True, trashed=whenTrashed, resourceID=self._resourceID
+        )
+        newName = "{}-{}".format(self._name[:200], str(uuid4()))
+        yield self.rename(newName)
+        # yield self.notifyPropertyChanged()
+        # yield self.invalidateQueryCache()
+        yield self._deletedSyncToken()
+        # yield self._home.notifyChanged()
+
+        # Update _children cache to reflect moving to trash
+        try:
+            del self._home._children[self._home._childrenKey(False)][newName]
+        except KeyError:
+            pass
+        try:
+            del self._home._children[self._home._childrenKey(False)][self._resourceID]
+        except KeyError:
+            pass
+        self._home._children[self._home._childrenKey(True)][newName] = self
+        self._home._children[self._home._childrenKey(True)][self._resourceID] = self
+
+        self._isInTrash = True
+        self._trashed = str(whenTrashed)
+
+
+    @inlineCallbacks
+    def fromTrash(self, restoreChildren=True, delta=datetime.timedelta(minutes=3)):
+
+        # print("XYZZY collection fromTrash")
+        if not self._isInTrash:
+            returnValue(None)
+
+        startTime = self.whenTrashed()
+        if delta is not None:
+            startTime = startTime - delta
+
+
+        yield self._updateIsInTrashQuery.on(
+            self._txn, isInTrash=False, trashed=None, resourceID=self._resourceID
+        )
+        yield self._initSyncToken()
+        yield self.notifyPropertyChanged()
+        yield self.invalidateQueryCache()
+        yield self._home.notifyChanged()
+        self._isInTrash = False
+        self._trashed = None
+
+        # Update _children cache to reflect moving from trash
+        try:
+            del self._home._children[self._home._childrenKey(True)][self._name]
+        except KeyError:
+            pass
+        try:
+            del self._home._children[self._home._childrenKey(True)][self._resourceID]
+        except KeyError:
+            pass
+        self._home._children[self._home._childrenKey(False)][self._name] = self
+        self._home._children[self._home._childrenKey(False)][self._resourceID] = self
+
+        if restoreChildren:
+            trash = yield self._home.childWithName("trash")
+            childrenToRestore = yield trash.trashForCollection(
+                self._resourceID, start=startTime
+            )
+            for child in childrenToRestore:
+                # print("Restoring", child)
+                yield child.fromTrash()
+
+
+
+    @classproperty
+    def _selectIsInTrashQuery(cls):
+        table = cls._homeChildMetaDataSchema
+        return Select((table.IS_IN_TRASH, table.TRASHED), From=table, Where=table.RESOURCE_ID == Parameter("resourceID"))
+
+
+
+    def isInTrash(self):
+        return self._isInTrash
+
+
+
+    def whenTrashed(self):
+        if self._trashed is None:
+            return None
+        return parseSQLTimestamp(self._trashed)
+
+
     def ownerHome(self):
         """
         @see: L{ICalendar.ownerCalendarHome}
@@ -6192,8 +6316,7 @@
     @inlineCallbacks
     def listObjectResources(self):
         """
-        Returns a list of names of object resources in this collection, taking
-        into account the IS_IN_TRASH flag and skipping those in the trash.
+        Returns a list of names of object resources in this collection
         """
         if self._objectNames is None:
             self._objectNames = yield self._objectResourceClass.listObjects(self)
@@ -6900,28 +7023,19 @@
 
         rows = None
         if name:
-            if parent.isTrash():
-                # the name needs to be split
-                parentID, name = parent.parseName(name)
-            else:
-                parentID = parent._resourceID
-
+            parentID = parent._resourceID
             rows = yield cls._allColumnsWithParentAndName.on(
                 parent._txn,
                 name=name,
                 parentID=parentID
             )
         elif uid:
-            assert not parent.isTrash(), "UID lookup in Trash not supported"
-
             rows = yield cls._allColumnsWithParentAndUID.on(
                 parent._txn,
                 uid=uid,
                 parentID=parent._resourceID
             )
         elif resourceID:
-            assert not parent.isTrash(), "ID lookup in Trash not supported"
-
             rows = yield cls._allColumnsWithParentAndID.on(
                 parent._txn,
                 resourceID=resourceID,
@@ -7030,7 +7144,7 @@
         rows = yield Select(
             [obj.RESOURCE_NAME],
             From=obj,
-            Where=(obj.PARENT_RESOURCE_ID == Parameter('parentID')).And(obj.IS_IN_TRASH == False)
+            Where=(obj.PARENT_RESOURCE_ID == Parameter('parentID'))
         ).on(parent._txn, parentID=parent.id())
         returnValue(sorted([row[0] for row in rows]))
 
@@ -7145,13 +7259,6 @@
 
         if row:
             child = yield cls.makeClass(parent, row)
-
-            # Use the name that was passed in, especially to cover the case
-            # where the "trashed" name is prepended with the original collection
-            # id (otherwise the name will be missing that id)
-            if name is not None:
-                child._name = name
-
             returnValue(child)
         else:
             returnValue(None)
@@ -7510,83 +7617,79 @@
 
 
     @classproperty
-    def _updateIsInTrashQuery(cls):
+    def _updateToTrashQuery(cls):
         obj = cls._objectSchema
         return Update(
-            {obj.IS_IN_TRASH: Parameter("isInTrash"), obj.TRASHED: Parameter("trashed")},
+            {obj.ORIGINAL_COLLECTION: Parameter("originalCollection"), obj.TRASHED: Parameter("trashed")},
             Where=obj.RESOURCE_ID == Parameter("resourceID"),
         )
 
 
-    @inlineCallbacks
-    def toTrash(self):
-        yield self._updateIsInTrashQuery.on(
-            self._txn, isInTrash=True, trashed=datetime.datetime.utcnow(), resourceID=self._resourceID
+    @classproperty
+    def _updateFromTrashQuery(cls):
+        obj = cls._objectSchema
+        return Update(
+            {obj.ORIGINAL_COLLECTION: None, obj.TRASHED: None},
+            Where=obj.RESOURCE_ID == Parameter("resourceID"),
         )
-        yield self._parentCollection.removedObjectResource(self)
-        yield self._parentCollection._deleteRevision(self.name())
 
-        trash = yield self._parentCollection._home.childWithName("trash")
-        if trash is not None:
-            yield trash._insertRevision(
-                trash.nameForResource(
-                    self._parentCollection,
-                    self
-                )
-            )
 
+    @classproperty
+    def _selectTrashDataQuery(cls):
+        obj = cls._objectSchema
+        return Select((obj.ORIGINAL_COLLECTION, obj.TRASHED), From=obj, Where=obj.RESOURCE_ID == Parameter("resourceID"))
 
+
     @inlineCallbacks
-    def fromTrash(self):
+    def originalCollection(self):
+        originalCollectionID, whenTrashed = (
+            yield self._selectTrashDataQuery.on(
+                self._txn, resourceID=self._resourceID
+            )
+        )[0]
+        originalCollection = yield self._parentCollection._home.childWithID(originalCollectionID)
+        returnValue(originalCollection)
 
-        # First make sure this is actually in the trash
-        isInTrash = yield self.isInTrash()
-        if not isInTrash:
-            returnValue(None)
 
-        if self._parentCollection.isTrash():
-            # The parent is indeed the trash collection
-            trash = self._parentCollection
-            self._parentCollection = yield trash.originalParentForResource(self)
-            trashedName = self._name # for deleting trash revision below
-            _ignore, newName = trash.parseName(self._name)
-        else:
-            # The parent is the original collection because it was retrieved
-            # via that parent, not the trash collection
-            home = self._parentCollection.viewerHome()
-            trash = yield home.childWithName("trash")
-            trashedName = trash.nameForResource(self._parentCollection, self)
-            newName = self._name
 
-        yield self._updateIsInTrashQuery.on(
-            self._txn, isInTrash=False, trashed=None, resourceID=self._resourceID
+    @inlineCallbacks
+    def toTrash(self):
+        originalCollection = self._parentCollection._resourceID
+        trash = yield self._parentCollection._home.childWithName("trash")
+        newName = str(uuid4())
+        yield self.moveTo(trash, name=newName)
+        yield self._updateToTrashQuery.on(
+            self._txn, originalCollection=originalCollection, trashed=datetime.datetime.utcnow(), resourceID=self._resourceID
         )
-        yield trash._deleteRevision(trashedName)
+        returnValue(newName)
 
-        self._name = newName
 
-        yield self._parentCollection.addedObjectResource(self)
-        yield self._parentCollection._insertRevision(self.name())
+    @inlineCallbacks
+    def fromTrash(self):
+        originalCollection = yield self.originalCollection()
+        yield self.moveTo(originalCollection)
+        yield self._updateFromTrashQuery.on(
+            self._txn, resourceID=self._resourceID
+        )
+        returnValue(self._name)
 
 
-
-
     @classproperty
     def _selectIsInTrashQuery(cls):
         obj = cls._objectSchema
-        return Select((obj.IS_IN_TRASH, obj.TRASHED), From=obj, Where=obj.RESOURCE_ID == Parameter("resourceID"))
+        return Select((obj.ORIGINAL_COLLECTION, obj.TRASHED), From=obj, Where=obj.RESOURCE_ID == Parameter("resourceID"))
 
 
     @inlineCallbacks
     def isInTrash(self):
-        returnValue(
-            (
-                yield self._selectIsInTrashQuery.on(
-                    self._txn, resourceID=self._resourceID
-                )
-            )[0][0]
-        )
+        originalCollectionID = (
+            yield self._selectIsInTrashQuery.on(
+                self._txn, resourceID=self._resourceID
+            )
+        )[0][0]
+        returnValue(originalCollectionID is not None)
 
+
     @inlineCallbacks
     def whenTrashed(self):
         returnValue(

Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql_schema/current.sql	2015-03-05 21:26:24 UTC (rev 14514)
+++ CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql_schema/current.sql	2015-03-05 22:11:06 UTC (rev 14515)
@@ -263,7 +263,7 @@
   MODIFIED             timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
   DATAVERSION          integer      default 0 not null,
   TRASHED              timestamp    default null,
-  IS_IN_TRASH          boolean      default false not null, -- entire resource is in the trash
+  ORIGINAL_COLLECTION  integer      default null, -- calendar_resource_id prior to trash
 
   unique (CALENDAR_RESOURCE_ID, RESOURCE_NAME) -- implicit index
 

Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/test/test_sql.py	2015-03-05 21:26:24 UTC (rev 14514)
+++ CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/test/test_sql.py	2015-03-05 22:11:06 UTC (rev 14515)
@@ -517,10 +517,16 @@
 
 class CommonTrashTests(StoreTestCase):
 
+    def _homeForUser(self, txn, userName):
+        return txn.calendarHomeWithUID(userName, create=True)
+
     @inlineCallbacks
-    def _collectionForUser(self, txn, userName, collectionName):
+    def _collectionForUser(self, txn, userName, collectionName, create=False, onlyInTrash=False):
         home = yield txn.calendarHomeWithUID(userName, create=True)
-        collection = yield home.childWithName(collectionName)
+        collection = yield home.childWithName(collectionName, onlyInTrash=onlyInTrash)
+        if collection is None:
+            if create:
+                collection = yield home.createCalendarWithName(collectionName)
         returnValue(collection)
 
 
@@ -637,7 +643,7 @@
         self.assertTrue(trashed is None)
 
         # Move object to trash
-        yield resource.toTrash()
+        newName = yield resource.toTrash()
 
         yield txn.commit()
         yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
@@ -645,7 +651,7 @@
         txn = self.store.newTransaction()
 
         # Verify it's in the trash
-        resource = yield self._getResource(txn, "user01", "trash", "")
+        resource = yield self._getResource(txn, "user01", "trash", newName)
         self.assertTrue((yield resource.isInTrash()))
         trashed = yield resource.whenTrashed()
         self.assertFalse(trashed is None)
@@ -674,7 +680,7 @@
         # One object in collection
         resourceNames = yield self._getResourceNames(txn, "user01", "calendar")
         self.assertEqual(len(resourceNames), 1)
-        resource = yield self._getResource(txn, "user01", "calendar", "test.ics")
+        resource = yield self._getResource(txn, "user01", "calendar", newName)
         self.assertFalse((yield resource.isInTrash()))
         trashed = yield resource.whenTrashed()
         self.assertTrue(trashed is None)
@@ -887,11 +893,11 @@
         txn = self.store.newTransaction()
 
         # user01's copy should be back on their calendar
-        resource = yield self._getResource(txn, "user01", "calendar", "test.ics")
+        resource = yield self._getResource(txn, "user01", "calendar", "")
         self.assertFalse((yield resource.isInTrash()))
         trashed = yield resource.whenTrashed()
         self.assertTrue(trashed is None)
-        data = yield self._getResourceData(txn, "user01", "calendar", "test.ics")
+        data = yield self._getResourceData(txn, "user01", "calendar", "")
         self.assertTrue("PARTSTAT=NEEDS-ACTION" in data)
 
         # user02's copy should be back on their calendar
@@ -1670,7 +1676,7 @@
         txn = self.store.newTransaction()
 
         # user01's copy should be back on their calendar
-        data = yield self._getResourceData(txn, "user01", "calendar", "test.ics")
+        data = yield self._getResourceData(txn, "user01", "calendar", "")
         self.assertTrue("PARTSTAT=TENTATIVE" in data)
 
         # user02's copy should be back on their calendar
@@ -1965,7 +1971,7 @@
         # user01 restores event from the trash
         txn = self.store.newTransaction()
         resource = yield self._getResource(txn, "user01", "trash", "")
-        yield resource.fromTrash()
+        trashedName = yield resource.fromTrash()
         yield txn.commit()
 
         yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
@@ -1976,16 +1982,16 @@
         resourceNames = yield self._getResourceNames(txn, "user01", "trash")
         self.assertEquals(len(resourceNames), 0)
 
-        # user01 should have test.ics and a new .ics
+        # user01 should have two .ics
         resourceNames = yield self._getResourceNames(txn, "user01", "calendar")
         self.assertEquals(len(resourceNames), 2)
-        self.assertTrue("test.ics" in resourceNames)
-        resourceNames.remove("test.ics")
+        self.assertTrue(trashedName in resourceNames)
+        resourceNames.remove(trashedName)
         newName = resourceNames[0]
 
         # user01's test.ics -- verify it got split correctly, by making sure
         # it's got a count other than 20 now
-        data = yield self._getResourceData(txn, "user01", "calendar", "test.ics")
+        data = yield self._getResourceData(txn, "user01", "calendar", trashedName)
         self.assertTrue("COUNT=" in data)
         self.assertFalse("COUNT=20" in data)
 
@@ -2165,3 +2171,166 @@
         self.assertTrue("PARTSTAT=ACCEPTED" in data)
 
         yield txn.commit()
+
+
+    @inlineCallbacks
+    def test_trashCalendar(self):
+
+        from twistedcaldav.stdconfig import config
+        self.patch(config, "EnableTrashCollection", True)
+
+        txn = self.store.newTransaction()
+
+        collection = yield self._collectionForUser(txn, "user01", "test", create=True)
+        isInTrash = collection.isInTrash()
+        self.assertFalse(isInTrash)
+        whenTrashed = collection.whenTrashed()
+        self.assertEquals(whenTrashed, None)
+
+        home = yield self._homeForUser(txn, "user01")
+        names = yield home.listChildren()
+        self.assertTrue("test" in names)
+        names = yield home.listChildren(onlyInTrash=True)
+        self.assertFalse("test" in names)
+
+        yield collection.remove()
+        isInTrash = collection.isInTrash()
+        self.assertTrue(isInTrash)
+        whenTrashed = collection.whenTrashed()
+        self.assertNotEquals(whenTrashed, None)
+
+        collection = yield self._collectionForUser(txn, "user01", "test")
+        self.assertEquals(collection, None)
+
+        yield txn.commit()
+
+        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
+
+        txn = self.store.newTransaction()
+
+        collection = yield self._collectionForUser(txn, "user01", "test")
+        self.assertEquals(collection, None)
+        home = yield self._homeForUser(txn, "user01")
+        names = yield home.listChildren(onlyInTrash=True)
+        trashedName = names[0]
+        collection = yield self._collectionForUser(txn, "user01", trashedName, onlyInTrash=True)
+        self.assertNotEquals(collection, None)
+        home = yield self._homeForUser(txn, "user01")
+        names = yield home.listChildren()
+        self.assertFalse("test" in names)
+        names = yield home.listChildren(onlyInTrash=True)
+        self.assertTrue(trashedName in names)
+
+        yield collection.fromTrash()
+
+        yield txn.commit()
+
+        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
+
+        txn = self.store.newTransaction()
+        home = yield self._homeForUser(txn, "user01")
+        names = yield home.listChildren()
+        self.assertTrue(trashedName in names)
+        names = yield home.listChildren(onlyInTrash=True)
+        self.assertFalse("test" in names)
+        yield txn.commit()
+
+
+    @inlineCallbacks
+    def test_trashCalendarWithUnscheduled(self):
+
+        from twistedcaldav.stdconfig import config
+        self.patch(config, "EnableTrashCollection", True)
+
+        txn = self.store.newTransaction()
+
+        collection = yield self._collectionForUser(txn, "user01", "test", create=True)
+
+        data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:5CE3B280-DBC9-4E8E-B0B2-996754020E5F
+DTSTART;TZID=America/Los_Angeles:20141108T093000
+DTEND;TZID=America/Los_Angeles:20141108T103000
+CREATED:20141106T192546Z
+DTSTAMP:20141106T192546Z
+RRULE:FREQ=DAILY
+SEQUENCE:0
+SUMMARY:repeating event
+TRANSP:OPAQUE
+END:VEVENT
+BEGIN:VEVENT
+UID:5CE3B280-DBC9-4E8E-B0B2-996754020E5F
+RECURRENCE-ID;TZID=America/Los_Angeles:20141111T093000
+DTSTART;TZID=America/Los_Angeles:20141111T110000
+DTEND;TZID=America/Los_Angeles:20141111T120000
+CREATED:20141106T192546Z
+DTSTAMP:20141106T192546Z
+SEQUENCE:0
+SUMMARY:repeating event
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+        # Create an object
+        resource = yield collection.createObjectResourceWithName(
+            "test.ics",
+            Component.allFromString(data1)
+        )
+
+        # One object in collection
+        objects = yield collection.listObjectResources()
+        self.assertEquals(len(objects), 1)
+
+        # No objects in trash
+        trash = yield self._collectionForUser(txn, "user01", "trash")
+        objects = yield trash.listObjectResources()
+        self.assertEquals(len(objects), 0)
+
+        # Verify it's not in the trash
+        self.assertFalse((yield resource.isInTrash()))
+        trashed = yield resource.whenTrashed()
+        self.assertTrue(trashed is None)
+
+        collection = yield self._collectionForUser(txn, "user01", "test")
+        resources = yield trash.trashForCollection(collection._resourceID)
+        self.assertEquals(len(resources), 0)
+
+        yield txn.commit()
+
+        txn = self.store.newTransaction()
+        collection = yield self._collectionForUser(txn, "user01", "test")
+        yield collection.remove()
+        yield txn.commit()
+
+        txn = self.store.newTransaction()
+        # One object in trash
+        trash = yield self._collectionForUser(txn, "user01", "trash")
+        objects = yield trash.listObjectResources()
+        self.assertEquals(len(objects), 1)
+
+        resources = yield trash.trashForCollection(collection._resourceID)
+        self.assertEquals(len(resources), 1)
+
+        home = yield self._homeForUser(txn, "user01")
+        names = yield home.listChildren(onlyInTrash=True)
+        trashedName = names[0]
+        collection = yield self._collectionForUser(txn, "user01", trashedName, onlyInTrash=True)
+        yield collection.fromTrash()
+
+        yield txn.commit()
+
+        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
+
+        txn = self.store.newTransaction()
+        home = yield self._homeForUser(txn, "user01")
+        names = yield home.listChildren()
+        self.assertTrue(trashedName in names)
+        names = yield home.listChildren(onlyInTrash=True)
+        self.assertFalse(trashedName in names)
+        resourceNames = yield self._getResourceNames(txn, "user01", trashedName)
+        self.assertEqual(len(resourceNames), 1)
+
+        yield txn.commit()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150305/58dc39e8/attachment-0001.html>


More information about the calendarserver-changes mailing list