<!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"> "_created",
</span><span class="cx"> "_modified",
</span><span class="cx"> "_childType",
</span><ins>+ "_trashed",
+ "_isInTrash",
</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"> "_modified",
</span><span class="cx"> "_dataversion",
</span><span class="cx"> "_trashed",
</span><del>- "_is_in_trash",
</del><ins>+ "_original_collection",
</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 = "Temporary"
</span><span class="lines">@@ -5868,67 +5874,44 @@
</span><span class="cx">
</span><span class="cx"> _childType = "trash" # 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("resourceID")
- ).And(
- bind.BIND_MODE == _BIND_MODE_OWN
- )
</del><ins>+ [obj.RESOURCE_ID], From=obj,
+ Where=(
+ obj.ORIGINAL_COLLECTION == Parameter("resourceID")).And(
+ obj.TRASHED >= Parameter("start")).And(
+ obj.TRASHED <= Parameter("end")),
</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 "{}-{}".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("-", 1)
- return int(parentID), resourceName
</del><span class="cx">
</span><span class="cx">
</span><del>- @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
</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"> """
</span><span class="cx"> Retrieve children contained in this home.
</span><span class="cx"> """
</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 "TRASHED" if onlyInTrash else "NOTTRASHED"
+
+
</ins><span class="cx"> @inlineCallbacks
</span><span class="cx"> def loadChildren(self):
</span><span class="cx"> """
</span><span class="lines">@@ -3443,27 +3447,29 @@
</span><span class="cx"> """
</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"> """
</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"> """
</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("name", "_children")
- def childWithName(self, name):
</del><ins>+ @inlineCallbacks
+ def childWithName(self, name, onlyInTrash=False):
</ins><span class="cx"> """
</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"> """
</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"> """
</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"> """
</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("resourceID", "_children")
- def childWithID(self, resourceID):
</del><ins>+ @inlineCallbacks
+ def childWithID(self, resourceID, onlyInTrash=False):
</ins><span class="cx"> """
</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"> """
</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"> """
</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"> """
</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"> """
</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"> """
</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"> """
</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):
- """
- 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
</del><span class="cx"> def loadAllObjects(cls, home):
</span><span class="cx"> """
</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"> """
</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"> """
</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("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)
+
+
</ins><span class="cx"> def ownerHome(self):
</span><span class="cx"> """
</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"> """
</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"> """
</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(), "UID lookup in Trash not supported"
-
</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(), "ID lookup in Trash not supported"
-
</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 "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
-
</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("isInTrash"), obj.TRASHED: Parameter("trashed")},
</del><ins>+ {obj.ORIGINAL_COLLECTION: Parameter("originalCollection"), obj.TRASHED: Parameter("trashed")},
</ins><span class="cx"> Where=obj.RESOURCE_ID == Parameter("resourceID"),
</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("resourceID"),
</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("trash")
- 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("resourceID"))
</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("trash")
- 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("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
</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("resourceID"))
</del><ins>+ return Select((obj.ORIGINAL_COLLECTION, obj.TRASHED), From=obj, Where=obj.RESOURCE_ID == Parameter("resourceID"))
</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, "user01", "trash", "")
</del><ins>+ resource = yield self._getResource(txn, "user01", "trash", 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, "user01", "calendar")
</span><span class="cx"> self.assertEqual(len(resourceNames), 1)
</span><del>- resource = yield self._getResource(txn, "user01", "calendar", "test.ics")
</del><ins>+ resource = yield self._getResource(txn, "user01", "calendar", 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, "user01", "calendar", "test.ics")
</del><ins>+ resource = yield self._getResource(txn, "user01", "calendar", "")
</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, "user01", "calendar", "test.ics")
</del><ins>+ data = yield self._getResourceData(txn, "user01", "calendar", "")
</ins><span class="cx"> self.assertTrue("PARTSTAT=NEEDS-ACTION" 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, "user01", "calendar", "test.ics")
</del><ins>+ data = yield self._getResourceData(txn, "user01", "calendar", "")
</ins><span class="cx"> self.assertTrue("PARTSTAT=TENTATIVE" 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, "user01", "trash", "")
</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, "user01", "trash")
</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, "user01", "calendar")
</span><span class="cx"> self.assertEquals(len(resourceNames), 2)
</span><del>- self.assertTrue("test.ics" in resourceNames)
- resourceNames.remove("test.ics")
</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, "user01", "calendar", "test.ics")
</del><ins>+ data = yield self._getResourceData(txn, "user01", "calendar", trashedName)
</ins><span class="cx"> self.assertTrue("COUNT=" in data)
</span><span class="cx"> self.assertFalse("COUNT=20" in data)
</span><span class="cx">
</span><span class="lines">@@ -2165,3 +2171,166 @@
</span><span class="cx"> self.assertTrue("PARTSTAT=ACCEPTED" 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, "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()
</ins></span></pre>
</div>
</div>
</body>
</html>