<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[14515] CalendarServer/branches/users/sagen/trashcan-5/txdav</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/14515">14515</a></dd>
<dt>Author</dt> <dd>sagen@apple.com</dd>
<dt>Date</dt> <dd>2015-03-05 14:11:06 -0800 (Thu, 05 Mar 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Trashed events now belong to the trash collection, with a reference to the original parent; collections can move to and from trash</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserssagentrashcan5txdavcaldavdatastoresqlpy">CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py</a></li>
<li><a href="#CalendarServerbranchesuserssagentrashcan5txdavcommondatastoresqlpy">CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py</a></li>
<li><a href="#CalendarServerbranchesuserssagentrashcan5txdavcommondatastoresql_schemacurrentsql">CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql_schema/current.sql</a></li>
<li><a href="#CalendarServerbranchesuserssagentrashcan5txdavcommondatastoretesttest_sqlpy">CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/test/test_sql.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServerbranchesuserssagentrashcan5txdavcaldavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py (14514 => 14515)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -1045,6 +1045,8 @@
</span><span class="cx">             cls._homeChildMetaDataSchema.CREATED,
</span><span class="cx">             cls._homeChildMetaDataSchema.MODIFIED,
</span><span class="cx">             cls._homeChildMetaDataSchema.CHILD_TYPE,
</span><ins>+            cls._homeChildMetaDataSchema.TRASHED,
+            cls._homeChildMetaDataSchema.IS_IN_TRASH,
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -1063,6 +1065,8 @@
</span><span class="cx">             &quot;_created&quot;,
</span><span class="cx">             &quot;_modified&quot;,
</span><span class="cx">             &quot;_childType&quot;,
</span><ins>+            &quot;_trashed&quot;,
+            &quot;_isInTrash&quot;,
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -2362,7 +2366,7 @@
</span><span class="cx">             obj.MODIFIED,
</span><span class="cx">             obj.DATAVERSION,
</span><span class="cx">             obj.TRASHED,
</span><del>-            obj.IS_IN_TRASH,
</del><ins>+            obj.ORIGINAL_COLLECTION,
</ins><span class="cx">         ]
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -2385,7 +2389,7 @@
</span><span class="cx">             &quot;_modified&quot;,
</span><span class="cx">             &quot;_dataversion&quot;,
</span><span class="cx">             &quot;_trashed&quot;,
</span><del>-            &quot;_is_in_trash&quot;,
</del><ins>+            &quot;_original_collection&quot;,
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -4908,7 +4912,7 @@
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def fromTrash(self):
</span><del>-        yield super(CalendarObject, self).fromTrash()
</del><ins>+        name = yield super(CalendarObject, self).fromTrash()
</ins><span class="cx"> 
</span><span class="cx">         caldata = yield self.componentForUser()
</span><span class="cx">         organizer = caldata.getOrganizer()
</span><span class="lines">@@ -4967,8 +4971,10 @@
</span><span class="cx">                     self
</span><span class="cx">                 )
</span><span class="cx"> 
</span><ins>+        returnValue(name)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx"> class AttachmentStorageTransport(StorageTransportBase):
</span><span class="cx"> 
</span><span class="cx">     _TEMPORARY_UPLOADS_DIRECTORY = &quot;Temporary&quot;
</span><span class="lines">@@ -5868,67 +5874,44 @@
</span><span class="cx"> 
</span><span class="cx">     _childType = &quot;trash&quot;  # FIXME: make childType an enumeration
</span><span class="cx"> 
</span><ins>+    def isTrash(self):
+        return True
</ins><span class="cx"> 
</span><ins>+
</ins><span class="cx">     @classproperty
</span><del>-    def _trashInHomeQuery(cls):
</del><ins>+    def _trashForCollectionQuery(cls):
</ins><span class="cx">         obj = cls._objectSchema
</span><del>-        bind = cls._bindSchema
</del><span class="cx">         return Select(
</span><del>-            [
-                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(&quot;resourceID&quot;)
-            ).And(
-                bind.BIND_MODE == _BIND_MODE_OWN
-            )
</del><ins>+            [obj.RESOURCE_ID], From=obj,
+            Where=(
+                obj.ORIGINAL_COLLECTION == Parameter(&quot;resourceID&quot;)).And(
+                obj.TRASHED &gt;= Parameter(&quot;start&quot;)).And(
+                obj.TRASHED &lt;= Parameter(&quot;end&quot;)),
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def isTrash(self):
-        return True
</del><ins>+    @inlineCallbacks
+    def trashForCollection(self, resourceID, start=None, end=None):
</ins><span class="cx"> 
</span><ins>+        if start is None:
+            start = datetime.datetime(datetime.MINYEAR, 1, 1)
</ins><span class="cx"> 
</span><del>-    def nameForResource(self, collection, objectResource):
-        return &quot;{}-{}&quot;.format(collection._resourceID, objectResource.name())
</del><ins>+        if end is None:
+            end = datetime.datetime.utcnow()
</ins><span class="cx"> 
</span><del>-    def originalParentForResource(self, objectResource):
-        parentID, resourceName = self.parseName(objectResource._name)
-        return self._home.childWithID(parentID)
</del><ins>+        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)
</ins><span class="cx"> 
</span><del>-    def parseName(self, name):
-        parentID, resourceName = name.split(&quot;-&quot;, 1)
-        return int(parentID), resourceName
</del><span class="cx"> 
</span><span class="cx"> 
</span><del>-    @inlineCallbacks
-    def listObjectResources(self):
-        &quot;&quot;&quot;
-        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.
-        &quot;&quot;&quot;
-        home = self._calendarHome
</del><span class="cx"> 
</span><del>-        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))
</del><span class="cx"> 
</span><del>-        returnValue(results)
-
-
-
</del><span class="cx"> # Hook-up class relationships at the end after they have all been defined
</span><span class="cx"> from txdav.caldav.datastore.sql_external import CalendarHomeExternal, CalendarExternal, CalendarObjectExternal
</span><span class="cx"> CalendarHome._externalClass = CalendarHomeExternal
</span></span></pre></div>
<a id="CalendarServerbranchesuserssagentrashcan5txdavcommondatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py (14514 => 14515)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -87,7 +87,7 @@
</span><span class="cx"> 
</span><span class="cx"> from zope.interface import implements, directlyProvides
</span><span class="cx"> 
</span><del>-from collections import namedtuple
</del><ins>+from collections import namedtuple, defaultdict
</ins><span class="cx"> import datetime
</span><span class="cx"> import inspect
</span><span class="cx"> import itertools
</span><span class="lines">@@ -3033,7 +3033,7 @@
</span><span class="cx">         self._status = _HOME_STATUS_NORMAL
</span><span class="cx">         self._dataVersion = None
</span><span class="cx">         self._childrenLoaded = False
</span><del>-        self._children = {}
</del><ins>+        self._children = defaultdict(dict)
</ins><span class="cx">         self._notifiers = None
</span><span class="cx">         self._quotaUsedBytes = None
</span><span class="cx">         self._created = None
</span><span class="lines">@@ -3425,17 +3425,21 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def children(self):
</del><ins>+    def children(self, onlyInTrash=False):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Retrieve children contained in this home.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         x = []
</span><del>-        names = yield self.listChildren()
</del><ins>+        names = yield self.listChildren(onlyInTrash=onlyInTrash)
</ins><span class="cx">         for name in names:
</span><del>-            x.append((yield self.childWithName(name)))
</del><ins>+            x.append((yield self.childWithName(name, onlyInTrash=onlyInTrash)))
</ins><span class="cx">         returnValue(x)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def _childrenKey(self, onlyInTrash):
+        return &quot;TRASHED&quot; if onlyInTrash else &quot;NOTTRASHED&quot;
+
+
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def loadChildren(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="lines">@@ -3443,27 +3447,29 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         results = (yield self._childClass.loadAllObjects(self))
</span><span class="cx">         for result in results:
</span><del>-            self._children[result.name()] = result
-            self._children[result._resourceID] = result
</del><ins>+            key = self._childrenKey(result.isInTrash())
+            self._children[key][result.name()] = result
+            self._children[key][result._resourceID] = result
</ins><span class="cx">         self._childrenLoaded = True
</span><span class="cx">         returnValue(results)
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def listChildren(self):
</del><ins>+    @inlineCallbacks
+    def listChildren(self, onlyInTrash=False):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Retrieve the names of the children in this home.
</span><span class="cx"> 
</span><span class="cx">         @return: an iterable of C{str}s.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        if self._childrenLoaded:
-            return succeed([k for k in self._children.keys() if isinstance(k, str)])
-        else:
-            return self._childClass.listObjects(self)
</del><ins>+        if not self._childrenLoaded:
+            yield self.loadChildren()
+        names = [k for k in self._children[self._childrenKey(onlyInTrash)].keys() if isinstance(k, str)]
+        returnValue(names)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-    @memoizedKey(&quot;name&quot;, &quot;_children&quot;)
-    def childWithName(self, name):
</del><ins>+    @inlineCallbacks
+    def childWithName(self, name, onlyInTrash=False):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Retrieve the child with the given C{name} contained in this
</span><span class="cx">         home.
</span><span class="lines">@@ -3471,10 +3477,15 @@
</span><span class="cx">         @param name: a string.
</span><span class="cx">         @return: an L{ICalendar} or C{None} if no such child exists.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        return self._childClass.objectWithName(self, name)
</del><ins>+        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))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def anyObjectWithShareUID(self, shareUID):
</del><ins>+    def anyObjectWithShareUID(self, shareUID, onlyInTrash=False):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Retrieve the child accepted or otherwise with the given bind identifier contained in this
</span><span class="cx">         home.
</span><span class="lines">@@ -3482,11 +3493,11 @@
</span><span class="cx">         @param name: a string.
</span><span class="cx">         @return: an L{ICalendar} or C{None} if no such child exists.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        return self._childClass.objectWithName(self, shareUID, accepted=None)
</del><ins>+        return self._childClass.objectWithName(self, shareUID, accepted=None, onlyInTrash=onlyInTrash)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-    @memoizedKey(&quot;resourceID&quot;, &quot;_children&quot;)
-    def childWithID(self, resourceID):
</del><ins>+    @inlineCallbacks
+    def childWithID(self, resourceID, onlyInTrash=False):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Retrieve the child with the given C{resourceID} contained in this
</span><span class="cx">         home.
</span><span class="lines">@@ -3494,10 +3505,15 @@
</span><span class="cx">         @param name: a string.
</span><span class="cx">         @return: an L{ICalendar} or C{None} if no such child exists.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        return self._childClass.objectWithID(self, resourceID)
</del><ins>+        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))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def childWithExternalID(self, externalID):
</del><ins>+    def childWithExternalID(self, externalID, onlyInTrash=False):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Retrieve the child with the given C{externalID} contained in this
</span><span class="cx">         home.
</span><span class="lines">@@ -3505,10 +3521,10 @@
</span><span class="cx">         @param name: a string.
</span><span class="cx">         @return: an L{ICalendar} or C{None} if no such child exists.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        return self._childClass.objectWithExternalID(self, externalID)
</del><ins>+        return self._childClass.objectWithExternalID(self, externalID, onlyInTrash=onlyInTrash)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def allChildWithID(self, resourceID):
</del><ins>+    def allChildWithID(self, resourceID, onlyInTrash=False):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Retrieve the child with the given C{resourceID} contained in this
</span><span class="cx">         home.
</span><span class="lines">@@ -3516,7 +3532,7 @@
</span><span class="cx">         @param name: a string.
</span><span class="cx">         @return: an L{ICalendar} or C{None} if no such child exists.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        return self._childClass.objectWithID(self, resourceID, accepted=None)
</del><ins>+        return self._childClass.objectWithID(self, resourceID, accepted=None, onlyInTrash=onlyInTrash)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -3536,8 +3552,9 @@
</span><span class="cx">         resourceID = child._resourceID
</span><span class="cx"> 
</span><span class="cx">         yield child.remove()
</span><del>-        self._children.pop(name, None)
-        self._children.pop(resourceID, None)
</del><ins>+        for d in self._children:
+            d.pop(name, None)
+            d.pop(resourceID, None)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -5144,15 +5161,16 @@
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def _changedStatus(self, previouslyAcceptedCount):
</span><ins>+        key = self._home._childrenKey(self.isInTrash())
</ins><span class="cx">         if self._bindStatus == _BIND_STATUS_ACCEPTED:
</span><span class="cx">             yield self._initSyncToken()
</span><span class="cx">             yield self._initBindRevision()
</span><del>-            self._home._children[self._name] = self
-            self._home._children[self._resourceID] = self
</del><ins>+            self._home._children[key][self._name] = self
+            self._home._children[key][self._resourceID] = self
</ins><span class="cx">         elif self._bindStatus in (_BIND_STATUS_INVITED, _BIND_STATUS_DECLINED):
</span><span class="cx">             yield self._deletedSyncToken(sharedRemoval=True)
</span><del>-            self._home._children.pop(self._name, None)
-            self._home._children.pop(self._resourceID, None)
</del><ins>+            self._home._children[key].pop(self._name, None)
+            self._home._children[key].pop(self._resourceID, None)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -5167,12 +5185,13 @@
</span><span class="cx"> 
</span><span class="cx">         @return: a L{Deferred} which will fire with the previous shareUID
</span><span class="cx">         &quot;&quot;&quot;
</span><ins>+        key = self._home._childrenKey(self.isInTrash())
</ins><span class="cx"> 
</span><span class="cx">         # remove sync tokens
</span><span class="cx">         shareeHome = shareeView.viewerHome()
</span><span class="cx">         yield shareeView._deletedSyncToken(sharedRemoval=True)
</span><del>-        shareeHome._children.pop(shareeView._name, None)
-        shareeHome._children.pop(shareeView._resourceID, None)
</del><ins>+        shareeHome._children[key].pop(shareeView._name, None)
+        shareeHome._children[key].pop(shareeView._resourceID, None)
</ins><span class="cx"> 
</span><span class="cx">         # Must send notification to ensure cache invalidation occurs
</span><span class="cx">         yield self.notifyPropertyChanged()
</span><span class="lines">@@ -5766,22 +5785,6 @@
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><span class="cx">     @inlineCallbacks
</span><del>-    def listObjects(cls, home):
-        &quot;&quot;&quot;
-        Retrieve the names of the children that exist in the given home.
-
-        @return: an iterable of C{str}s.
-        &quot;&quot;&quot;
-        # 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
</del><span class="cx">     def loadAllObjects(cls, home):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Load all L{CommonHomeChild} instances which are children of a given
</span><span class="lines">@@ -5823,29 +5826,32 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><del>-    def objectWithName(cls, home, name, accepted=True):
-        return cls.objectWith(home, name=name, accepted=accepted)
</del><ins>+    def objectWithName(cls, home, name, accepted=True, onlyInTrash=False):
+        return cls.objectWith(home, name=name, accepted=accepted, onlyInTrash=onlyInTrash)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><del>-    def objectWithID(cls, home, resourceID, accepted=True):
-        return cls.objectWith(home, resourceID=resourceID, accepted=accepted)
</del><ins>+    def objectWithID(cls, home, resourceID, accepted=True, onlyInTrash=False):
+        return cls.objectWith(home, resourceID=resourceID, accepted=accepted, onlyInTrash=onlyInTrash)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><del>-    def objectWithExternalID(cls, home, externalID, accepted=True):
-        return cls.objectWith(home, externalID=externalID, accepted=accepted)
</del><ins>+    def objectWithExternalID(cls, home, externalID, accepted=True, onlyInTrash=False):
+        return cls.objectWith(home, externalID=externalID, accepted=accepted, onlyInTrash=onlyInTrash)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><span class="cx">     @inlineCallbacks
</span><del>-    def objectWith(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
</del><ins>+    def objectWith(
+        cls, home, name=None, resourceID=None, externalID=None, accepted=True,
+        onlyInTrash=False
+    ):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Create the object using one of the specified arguments as the key to load it. One
</span><span class="cx">         and only one of the keyword arguments must be set.
</span><span class="cx"> 
</span><del>-        @param parent: parent collection
-        @type parent: L{CommonHomeChild}
</del><ins>+        @param home: home
+        @type home: L{CommonHome}
</ins><span class="cx">         @param name: name of the resource, or C{None}
</span><span class="cx">         @type name: C{str}
</span><span class="cx">         @param uid: resource data UID, or C{None}
</span><span class="lines">@@ -5868,6 +5874,18 @@
</span><span class="cx">         if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
</span><span class="cx">             returnValue(None)
</span><span class="cx"> 
</span><ins>+        # 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)
+
</ins><span class="cx">         child = yield cls.makeClass(home, bindData, additionalBindData, metadataData)
</span><span class="cx">         returnValue(child)
</span><span class="cx"> 
</span><span class="lines">@@ -6074,8 +6092,9 @@
</span><span class="cx">                                    homeID=self._home._resourceID)
</span><span class="cx">         self._name = name
</span><span class="cx">         # update memos
</span><del>-        del self._home._children[oldName]
-        self._home._children[name] = self
</del><ins>+        key = self._home._childrenKey(self.isInTrash())
+        del self._home._children[key][oldName]
+        self._home._children[key][name] = self
</ins><span class="cx">         yield self._renameSyncToken()
</span><span class="cx"> 
</span><span class="cx">         yield self.notifyPropertyChanged()
</span><span class="lines">@@ -6098,7 +6117,8 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         if config.EnableTrashCollection:
</span><del>-            if self.isInTrash():
</del><ins>+            isInTrash = yield self.isInTrash()
+            if isInTrash:
</ins><span class="cx">                 raise AlreadyInTrashError
</span><span class="cx">             else:
</span><span class="cx">                 yield self.toTrash()
</span><span class="lines">@@ -6130,6 +6150,110 @@
</span><span class="cx">         yield self._home.notifyChanged()
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @classproperty
+    def _updateIsInTrashQuery(cls):
+        table = cls._homeChildMetaDataSchema
+        return Update(
+            {table.IS_IN_TRASH: Parameter(&quot;isInTrash&quot;), table.TRASHED: Parameter(&quot;trashed&quot;)},
+            Where=table.RESOURCE_ID == Parameter(&quot;resourceID&quot;),
+        )
+
+
+    @inlineCallbacks
+    def toTrash(self):
+        # print(&quot;XYZZY collection toTrash&quot;)
+        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 = &quot;{}-{}&quot;.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(&quot;XYZZY collection fromTrash&quot;)
+        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(&quot;trash&quot;)
+            childrenToRestore = yield trash.trashForCollection(
+                self._resourceID, start=startTime
+            )
+            for child in childrenToRestore:
+                # print(&quot;Restoring&quot;, 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(&quot;resourceID&quot;))
+
+
+
+    def isInTrash(self):
+        return self._isInTrash
+
+
+
+    def whenTrashed(self):
+        if self._trashed is None:
+            return None
+        return parseSQLTimestamp(self._trashed)
+
+
</ins><span class="cx">     def ownerHome(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         @see: L{ICalendar.ownerCalendarHome}
</span><span class="lines">@@ -6192,8 +6316,7 @@
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def listObjectResources(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        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.
</del><ins>+        Returns a list of names of object resources in this collection
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         if self._objectNames is None:
</span><span class="cx">             self._objectNames = yield self._objectResourceClass.listObjects(self)
</span><span class="lines">@@ -6900,28 +7023,19 @@
</span><span class="cx"> 
</span><span class="cx">         rows = None
</span><span class="cx">         if name:
</span><del>-            if parent.isTrash():
-                # the name needs to be split
-                parentID, name = parent.parseName(name)
-            else:
-                parentID = parent._resourceID
-
</del><ins>+            parentID = parent._resourceID
</ins><span class="cx">             rows = yield cls._allColumnsWithParentAndName.on(
</span><span class="cx">                 parent._txn,
</span><span class="cx">                 name=name,
</span><span class="cx">                 parentID=parentID
</span><span class="cx">             )
</span><span class="cx">         elif uid:
</span><del>-            assert not parent.isTrash(), &quot;UID lookup in Trash not supported&quot;
-
</del><span class="cx">             rows = yield cls._allColumnsWithParentAndUID.on(
</span><span class="cx">                 parent._txn,
</span><span class="cx">                 uid=uid,
</span><span class="cx">                 parentID=parent._resourceID
</span><span class="cx">             )
</span><span class="cx">         elif resourceID:
</span><del>-            assert not parent.isTrash(), &quot;ID lookup in Trash not supported&quot;
-
</del><span class="cx">             rows = yield cls._allColumnsWithParentAndID.on(
</span><span class="cx">                 parent._txn,
</span><span class="cx">                 resourceID=resourceID,
</span><span class="lines">@@ -7030,7 +7144,7 @@
</span><span class="cx">         rows = yield Select(
</span><span class="cx">             [obj.RESOURCE_NAME],
</span><span class="cx">             From=obj,
</span><del>-            Where=(obj.PARENT_RESOURCE_ID == Parameter('parentID')).And(obj.IS_IN_TRASH == False)
</del><ins>+            Where=(obj.PARENT_RESOURCE_ID == Parameter('parentID'))
</ins><span class="cx">         ).on(parent._txn, parentID=parent.id())
</span><span class="cx">         returnValue(sorted([row[0] for row in rows]))
</span><span class="cx"> 
</span><span class="lines">@@ -7145,13 +7259,6 @@
</span><span class="cx"> 
</span><span class="cx">         if row:
</span><span class="cx">             child = yield cls.makeClass(parent, row)
</span><del>-
-            # Use the name that was passed in, especially to cover the case
-            # where the &quot;trashed&quot; name is prepended with the original collection
-            # id (otherwise the name will be missing that id)
-            if name is not None:
-                child._name = name
-
</del><span class="cx">             returnValue(child)
</span><span class="cx">         else:
</span><span class="cx">             returnValue(None)
</span><span class="lines">@@ -7510,83 +7617,79 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @classproperty
</span><del>-    def _updateIsInTrashQuery(cls):
</del><ins>+    def _updateToTrashQuery(cls):
</ins><span class="cx">         obj = cls._objectSchema
</span><span class="cx">         return Update(
</span><del>-            {obj.IS_IN_TRASH: Parameter(&quot;isInTrash&quot;), obj.TRASHED: Parameter(&quot;trashed&quot;)},
</del><ins>+            {obj.ORIGINAL_COLLECTION: Parameter(&quot;originalCollection&quot;), obj.TRASHED: Parameter(&quot;trashed&quot;)},
</ins><span class="cx">             Where=obj.RESOURCE_ID == Parameter(&quot;resourceID&quot;),
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    @inlineCallbacks
-    def toTrash(self):
-        yield self._updateIsInTrashQuery.on(
-            self._txn, isInTrash=True, trashed=datetime.datetime.utcnow(), resourceID=self._resourceID
</del><ins>+    @classproperty
+    def _updateFromTrashQuery(cls):
+        obj = cls._objectSchema
+        return Update(
+            {obj.ORIGINAL_COLLECTION: None, obj.TRASHED: None},
+            Where=obj.RESOURCE_ID == Parameter(&quot;resourceID&quot;),
</ins><span class="cx">         )
</span><del>-        yield self._parentCollection.removedObjectResource(self)
-        yield self._parentCollection._deleteRevision(self.name())
</del><span class="cx"> 
</span><del>-        trash = yield self._parentCollection._home.childWithName(&quot;trash&quot;)
-        if trash is not None:
-            yield trash._insertRevision(
-                trash.nameForResource(
-                    self._parentCollection,
-                    self
-                )
-            )
</del><span class="cx"> 
</span><ins>+    @classproperty
+    def _selectTrashDataQuery(cls):
+        obj = cls._objectSchema
+        return Select((obj.ORIGINAL_COLLECTION, obj.TRASHED), From=obj, Where=obj.RESOURCE_ID == Parameter(&quot;resourceID&quot;))
</ins><span class="cx"> 
</span><ins>+
</ins><span class="cx">     @inlineCallbacks
</span><del>-    def fromTrash(self):
</del><ins>+    def originalCollection(self):
+        originalCollectionID, whenTrashed = (
+            yield self._selectTrashDataQuery.on(
+                self._txn, resourceID=self._resourceID
+            )
+        )[0]
+        originalCollection = yield self._parentCollection._home.childWithID(originalCollectionID)
+        returnValue(originalCollection)
</ins><span class="cx"> 
</span><del>-        # First make sure this is actually in the trash
-        isInTrash = yield self.isInTrash()
-        if not isInTrash:
-            returnValue(None)
</del><span class="cx"> 
</span><del>-        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(&quot;trash&quot;)
-            trashedName = trash.nameForResource(self._parentCollection, self)
-            newName = self._name
</del><span class="cx"> 
</span><del>-        yield self._updateIsInTrashQuery.on(
-            self._txn, isInTrash=False, trashed=None, resourceID=self._resourceID
</del><ins>+    @inlineCallbacks
+    def toTrash(self):
+        originalCollection = self._parentCollection._resourceID
+        trash = yield self._parentCollection._home.childWithName(&quot;trash&quot;)
+        newName = str(uuid4())
+        yield self.moveTo(trash, name=newName)
+        yield self._updateToTrashQuery.on(
+            self._txn, originalCollection=originalCollection, trashed=datetime.datetime.utcnow(), resourceID=self._resourceID
</ins><span class="cx">         )
</span><del>-        yield trash._deleteRevision(trashedName)
</del><ins>+        returnValue(newName)
</ins><span class="cx"> 
</span><del>-        self._name = newName
</del><span class="cx"> 
</span><del>-        yield self._parentCollection.addedObjectResource(self)
-        yield self._parentCollection._insertRevision(self.name())
</del><ins>+    @inlineCallbacks
+    def fromTrash(self):
+        originalCollection = yield self.originalCollection()
+        yield self.moveTo(originalCollection)
+        yield self._updateFromTrashQuery.on(
+            self._txn, resourceID=self._resourceID
+        )
+        returnValue(self._name)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-
-
</del><span class="cx">     @classproperty
</span><span class="cx">     def _selectIsInTrashQuery(cls):
</span><span class="cx">         obj = cls._objectSchema
</span><del>-        return Select((obj.IS_IN_TRASH, obj.TRASHED), From=obj, Where=obj.RESOURCE_ID == Parameter(&quot;resourceID&quot;))
</del><ins>+        return Select((obj.ORIGINAL_COLLECTION, obj.TRASHED), From=obj, Where=obj.RESOURCE_ID == Parameter(&quot;resourceID&quot;))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def isInTrash(self):
</span><del>-        returnValue(
-            (
-                yield self._selectIsInTrashQuery.on(
-                    self._txn, resourceID=self._resourceID
-                )
-            )[0][0]
-        )
</del><ins>+        originalCollectionID = (
+            yield self._selectIsInTrashQuery.on(
+                self._txn, resourceID=self._resourceID
+            )
+        )[0][0]
+        returnValue(originalCollectionID is not None)
</ins><span class="cx"> 
</span><ins>+
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def whenTrashed(self):
</span><span class="cx">         returnValue(
</span></span></pre></div>
<a id="CalendarServerbranchesuserssagentrashcan5txdavcommondatastoresql_schemacurrentsql"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql_schema/current.sql (14514 => 14515)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -263,7 +263,7 @@
</span><span class="cx">   MODIFIED             timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
</span><span class="cx">   DATAVERSION          integer      default 0 not null,
</span><span class="cx">   TRASHED              timestamp    default null,
</span><del>-  IS_IN_TRASH          boolean      default false not null, -- entire resource is in the trash
</del><ins>+  ORIGINAL_COLLECTION  integer      default null, -- calendar_resource_id prior to trash
</ins><span class="cx"> 
</span><span class="cx">   unique (CALENDAR_RESOURCE_ID, RESOURCE_NAME) -- implicit index
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServerbranchesuserssagentrashcan5txdavcommondatastoretesttest_sqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/test/test_sql.py (14514 => 14515)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -517,10 +517,16 @@
</span><span class="cx"> 
</span><span class="cx"> class CommonTrashTests(StoreTestCase):
</span><span class="cx"> 
</span><ins>+    def _homeForUser(self, txn, userName):
+        return txn.calendarHomeWithUID(userName, create=True)
+
</ins><span class="cx">     @inlineCallbacks
</span><del>-    def _collectionForUser(self, txn, userName, collectionName):
</del><ins>+    def _collectionForUser(self, txn, userName, collectionName, create=False, onlyInTrash=False):
</ins><span class="cx">         home = yield txn.calendarHomeWithUID(userName, create=True)
</span><del>-        collection = yield home.childWithName(collectionName)
</del><ins>+        collection = yield home.childWithName(collectionName, onlyInTrash=onlyInTrash)
+        if collection is None:
+            if create:
+                collection = yield home.createCalendarWithName(collectionName)
</ins><span class="cx">         returnValue(collection)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -637,7 +643,7 @@
</span><span class="cx">         self.assertTrue(trashed is None)
</span><span class="cx"> 
</span><span class="cx">         # Move object to trash
</span><del>-        yield resource.toTrash()
</del><ins>+        newName = yield resource.toTrash()
</ins><span class="cx"> 
</span><span class="cx">         yield txn.commit()
</span><span class="cx">         yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
</span><span class="lines">@@ -645,7 +651,7 @@
</span><span class="cx">         txn = self.store.newTransaction()
</span><span class="cx"> 
</span><span class="cx">         # Verify it's in the trash
</span><del>-        resource = yield self._getResource(txn, &quot;user01&quot;, &quot;trash&quot;, &quot;&quot;)
</del><ins>+        resource = yield self._getResource(txn, &quot;user01&quot;, &quot;trash&quot;, newName)
</ins><span class="cx">         self.assertTrue((yield resource.isInTrash()))
</span><span class="cx">         trashed = yield resource.whenTrashed()
</span><span class="cx">         self.assertFalse(trashed is None)
</span><span class="lines">@@ -674,7 +680,7 @@
</span><span class="cx">         # One object in collection
</span><span class="cx">         resourceNames = yield self._getResourceNames(txn, &quot;user01&quot;, &quot;calendar&quot;)
</span><span class="cx">         self.assertEqual(len(resourceNames), 1)
</span><del>-        resource = yield self._getResource(txn, &quot;user01&quot;, &quot;calendar&quot;, &quot;test.ics&quot;)
</del><ins>+        resource = yield self._getResource(txn, &quot;user01&quot;, &quot;calendar&quot;, newName)
</ins><span class="cx">         self.assertFalse((yield resource.isInTrash()))
</span><span class="cx">         trashed = yield resource.whenTrashed()
</span><span class="cx">         self.assertTrue(trashed is None)
</span><span class="lines">@@ -887,11 +893,11 @@
</span><span class="cx">         txn = self.store.newTransaction()
</span><span class="cx"> 
</span><span class="cx">         # user01's copy should be back on their calendar
</span><del>-        resource = yield self._getResource(txn, &quot;user01&quot;, &quot;calendar&quot;, &quot;test.ics&quot;)
</del><ins>+        resource = yield self._getResource(txn, &quot;user01&quot;, &quot;calendar&quot;, &quot;&quot;)
</ins><span class="cx">         self.assertFalse((yield resource.isInTrash()))
</span><span class="cx">         trashed = yield resource.whenTrashed()
</span><span class="cx">         self.assertTrue(trashed is None)
</span><del>-        data = yield self._getResourceData(txn, &quot;user01&quot;, &quot;calendar&quot;, &quot;test.ics&quot;)
</del><ins>+        data = yield self._getResourceData(txn, &quot;user01&quot;, &quot;calendar&quot;, &quot;&quot;)
</ins><span class="cx">         self.assertTrue(&quot;PARTSTAT=NEEDS-ACTION&quot; in data)
</span><span class="cx"> 
</span><span class="cx">         # user02's copy should be back on their calendar
</span><span class="lines">@@ -1670,7 +1676,7 @@
</span><span class="cx">         txn = self.store.newTransaction()
</span><span class="cx"> 
</span><span class="cx">         # user01's copy should be back on their calendar
</span><del>-        data = yield self._getResourceData(txn, &quot;user01&quot;, &quot;calendar&quot;, &quot;test.ics&quot;)
</del><ins>+        data = yield self._getResourceData(txn, &quot;user01&quot;, &quot;calendar&quot;, &quot;&quot;)
</ins><span class="cx">         self.assertTrue(&quot;PARTSTAT=TENTATIVE&quot; in data)
</span><span class="cx"> 
</span><span class="cx">         # user02's copy should be back on their calendar
</span><span class="lines">@@ -1965,7 +1971,7 @@
</span><span class="cx">         # user01 restores event from the trash
</span><span class="cx">         txn = self.store.newTransaction()
</span><span class="cx">         resource = yield self._getResource(txn, &quot;user01&quot;, &quot;trash&quot;, &quot;&quot;)
</span><del>-        yield resource.fromTrash()
</del><ins>+        trashedName = yield resource.fromTrash()
</ins><span class="cx">         yield txn.commit()
</span><span class="cx"> 
</span><span class="cx">         yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
</span><span class="lines">@@ -1976,16 +1982,16 @@
</span><span class="cx">         resourceNames = yield self._getResourceNames(txn, &quot;user01&quot;, &quot;trash&quot;)
</span><span class="cx">         self.assertEquals(len(resourceNames), 0)
</span><span class="cx"> 
</span><del>-        # user01 should have test.ics and a new .ics
</del><ins>+        # user01 should have two .ics
</ins><span class="cx">         resourceNames = yield self._getResourceNames(txn, &quot;user01&quot;, &quot;calendar&quot;)
</span><span class="cx">         self.assertEquals(len(resourceNames), 2)
</span><del>-        self.assertTrue(&quot;test.ics&quot; in resourceNames)
-        resourceNames.remove(&quot;test.ics&quot;)
</del><ins>+        self.assertTrue(trashedName in resourceNames)
+        resourceNames.remove(trashedName)
</ins><span class="cx">         newName = resourceNames[0]
</span><span class="cx"> 
</span><span class="cx">         # user01's test.ics -- verify it got split correctly, by making sure
</span><span class="cx">         # it's got a count other than 20 now
</span><del>-        data = yield self._getResourceData(txn, &quot;user01&quot;, &quot;calendar&quot;, &quot;test.ics&quot;)
</del><ins>+        data = yield self._getResourceData(txn, &quot;user01&quot;, &quot;calendar&quot;, trashedName)
</ins><span class="cx">         self.assertTrue(&quot;COUNT=&quot; in data)
</span><span class="cx">         self.assertFalse(&quot;COUNT=20&quot; in data)
</span><span class="cx"> 
</span><span class="lines">@@ -2165,3 +2171,166 @@
</span><span class="cx">         self.assertTrue(&quot;PARTSTAT=ACCEPTED&quot; in data)
</span><span class="cx"> 
</span><span class="cx">         yield txn.commit()
</span><ins>+
+
+    @inlineCallbacks
+    def test_trashCalendar(self):
+
+        from twistedcaldav.stdconfig import config
+        self.patch(config, &quot;EnableTrashCollection&quot;, True)
+
+        txn = self.store.newTransaction()
+
+        collection = yield self._collectionForUser(txn, &quot;user01&quot;, &quot;test&quot;, create=True)
+        isInTrash = collection.isInTrash()
+        self.assertFalse(isInTrash)
+        whenTrashed = collection.whenTrashed()
+        self.assertEquals(whenTrashed, None)
+
+        home = yield self._homeForUser(txn, &quot;user01&quot;)
+        names = yield home.listChildren()
+        self.assertTrue(&quot;test&quot; in names)
+        names = yield home.listChildren(onlyInTrash=True)
+        self.assertFalse(&quot;test&quot; in names)
+
+        yield collection.remove()
+        isInTrash = collection.isInTrash()
+        self.assertTrue(isInTrash)
+        whenTrashed = collection.whenTrashed()
+        self.assertNotEquals(whenTrashed, None)
+
+        collection = yield self._collectionForUser(txn, &quot;user01&quot;, &quot;test&quot;)
+        self.assertEquals(collection, None)
+
+        yield txn.commit()
+
+        yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
+
+        txn = self.store.newTransaction()
+
+        collection = yield self._collectionForUser(txn, &quot;user01&quot;, &quot;test&quot;)
+        self.assertEquals(collection, None)
+        home = yield self._homeForUser(txn, &quot;user01&quot;)
+        names = yield home.listChildren(onlyInTrash=True)
+        trashedName = names[0]
+        collection = yield self._collectionForUser(txn, &quot;user01&quot;, trashedName, onlyInTrash=True)
+        self.assertNotEquals(collection, None)
+        home = yield self._homeForUser(txn, &quot;user01&quot;)
+        names = yield home.listChildren()
+        self.assertFalse(&quot;test&quot; 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, &quot;user01&quot;)
+        names = yield home.listChildren()
+        self.assertTrue(trashedName in names)
+        names = yield home.listChildren(onlyInTrash=True)
+        self.assertFalse(&quot;test&quot; in names)
+        yield txn.commit()
+
+
+    @inlineCallbacks
+    def test_trashCalendarWithUnscheduled(self):
+
+        from twistedcaldav.stdconfig import config
+        self.patch(config, &quot;EnableTrashCollection&quot;, True)
+
+        txn = self.store.newTransaction()
+
+        collection = yield self._collectionForUser(txn, &quot;user01&quot;, &quot;test&quot;, create=True)
+
+        data1 = &quot;&quot;&quot;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
+&quot;&quot;&quot;
+
+        # Create an object
+        resource = yield collection.createObjectResourceWithName(
+            &quot;test.ics&quot;,
+            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, &quot;user01&quot;, &quot;trash&quot;)
+        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, &quot;user01&quot;, &quot;test&quot;)
+        resources = yield trash.trashForCollection(collection._resourceID)
+        self.assertEquals(len(resources), 0)
+
+        yield txn.commit()
+
+        txn = self.store.newTransaction()
+        collection = yield self._collectionForUser(txn, &quot;user01&quot;, &quot;test&quot;)
+        yield collection.remove()
+        yield txn.commit()
+
+        txn = self.store.newTransaction()
+        # One object in trash
+        trash = yield self._collectionForUser(txn, &quot;user01&quot;, &quot;trash&quot;)
+        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, &quot;user01&quot;)
+        names = yield home.listChildren(onlyInTrash=True)
+        trashedName = names[0]
+        collection = yield self._collectionForUser(txn, &quot;user01&quot;, 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, &quot;user01&quot;)
+        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, &quot;user01&quot;, trashedName)
+        self.assertEqual(len(resourceNames), 1)
+
+        yield txn.commit()
</ins></span></pre>
</div>
</div>

</body>
</html>