[CalendarServer-changes] [14515] CalendarServer/branches/users/sagen/trashcan-5/txdav
source_changes at macosforge.org
source_changes at macosforge.org
Thu Mar 5 14:11:06 PST 2015
Revision: 14515
http://trac.calendarserver.org//changeset/14515
Author: sagen at apple.com
Date: 2015-03-05 14:11:06 -0800 (Thu, 05 Mar 2015)
Log Message:
-----------
Trashed events now belong to the trash collection, with a reference to the original parent; collections can move to and from trash
Modified Paths:
--------------
CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py
CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py
CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql_schema/current.sql
CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/test/test_sql.py
Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py 2015-03-05 21:26:24 UTC (rev 14514)
+++ CalendarServer/branches/users/sagen/trashcan-5/txdav/caldav/datastore/sql.py 2015-03-05 22:11:06 UTC (rev 14515)
@@ -1045,6 +1045,8 @@
cls._homeChildMetaDataSchema.CREATED,
cls._homeChildMetaDataSchema.MODIFIED,
cls._homeChildMetaDataSchema.CHILD_TYPE,
+ cls._homeChildMetaDataSchema.TRASHED,
+ cls._homeChildMetaDataSchema.IS_IN_TRASH,
)
@@ -1063,6 +1065,8 @@
"_created",
"_modified",
"_childType",
+ "_trashed",
+ "_isInTrash",
)
@@ -2362,7 +2366,7 @@
obj.MODIFIED,
obj.DATAVERSION,
obj.TRASHED,
- obj.IS_IN_TRASH,
+ obj.ORIGINAL_COLLECTION,
]
@@ -2385,7 +2389,7 @@
"_modified",
"_dataversion",
"_trashed",
- "_is_in_trash",
+ "_original_collection",
)
@@ -4908,7 +4912,7 @@
@inlineCallbacks
def fromTrash(self):
- yield super(CalendarObject, self).fromTrash()
+ name = yield super(CalendarObject, self).fromTrash()
caldata = yield self.componentForUser()
organizer = caldata.getOrganizer()
@@ -4967,8 +4971,10 @@
self
)
+ returnValue(name)
+
class AttachmentStorageTransport(StorageTransportBase):
_TEMPORARY_UPLOADS_DIRECTORY = "Temporary"
@@ -5868,67 +5874,44 @@
_childType = "trash" # FIXME: make childType an enumeration
+ def isTrash(self):
+ return True
+
@classproperty
- def _trashInHomeQuery(cls):
+ def _trashForCollectionQuery(cls):
obj = cls._objectSchema
- bind = cls._bindSchema
return Select(
- [
- obj.PARENT_RESOURCE_ID, obj.RESOURCE_ID
- ],
- From=obj.join(
- bind, obj.PARENT_RESOURCE_ID == bind.RESOURCE_ID
- ),
- Where=(obj.IS_IN_TRASH == True).And(
- bind.HOME_RESOURCE_ID == Parameter("resourceID")
- ).And(
- bind.BIND_MODE == _BIND_MODE_OWN
- )
+ [obj.RESOURCE_ID], From=obj,
+ Where=(
+ obj.ORIGINAL_COLLECTION == Parameter("resourceID")).And(
+ obj.TRASHED >= Parameter("start")).And(
+ obj.TRASHED <= Parameter("end")),
)
- def isTrash(self):
- return True
+ @inlineCallbacks
+ def trashForCollection(self, resourceID, start=None, end=None):
+ if start is None:
+ start = datetime.datetime(datetime.MINYEAR, 1, 1)
- def nameForResource(self, collection, objectResource):
- return "{}-{}".format(collection._resourceID, objectResource.name())
+ if end is None:
+ end = datetime.datetime.utcnow()
- def originalParentForResource(self, objectResource):
- parentID, resourceName = self.parseName(objectResource._name)
- return self._home.childWithID(parentID)
+ results = yield self._trashForCollectionQuery.on(
+ self._txn, resourceID=resourceID, start=start, end=end
+ )
+ resources = []
+ for (objectResourceID,) in results:
+ resource = yield self.objectResourceWithID(objectResourceID)
+ resources.append(resource)
+ returnValue(resources)
- def parseName(self, name):
- parentID, resourceName = name.split("-", 1)
- return int(parentID), resourceName
- @inlineCallbacks
- def listObjectResources(self):
- """
- Return a list of names of child object resources in this trash; the
- list is computed from all the homeChildren in the trash's parent home.
- """
- home = self._calendarHome
- results = []
- rows = (yield self._trashInHomeQuery.on(
- self._txn, resourceID=home._resourceID
- ))
- if rows:
- for childID, objectID in rows:
- child = (yield home.childWithID(childID))
- if child:
- objectResource = (
- yield child.objectResourceWithID(objectID)
- )
- results.append(self.nameForResource(child, objectResource))
- returnValue(results)
-
-
-
# Hook-up class relationships at the end after they have all been defined
from txdav.caldav.datastore.sql_external import CalendarHomeExternal, CalendarExternal, CalendarObjectExternal
CalendarHome._externalClass = CalendarHomeExternal
Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py 2015-03-05 21:26:24 UTC (rev 14514)
+++ CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql.py 2015-03-05 22:11:06 UTC (rev 14515)
@@ -87,7 +87,7 @@
from zope.interface import implements, directlyProvides
-from collections import namedtuple
+from collections import namedtuple, defaultdict
import datetime
import inspect
import itertools
@@ -3033,7 +3033,7 @@
self._status = _HOME_STATUS_NORMAL
self._dataVersion = None
self._childrenLoaded = False
- self._children = {}
+ self._children = defaultdict(dict)
self._notifiers = None
self._quotaUsedBytes = None
self._created = None
@@ -3425,17 +3425,21 @@
@inlineCallbacks
- def children(self):
+ def children(self, onlyInTrash=False):
"""
Retrieve children contained in this home.
"""
x = []
- names = yield self.listChildren()
+ names = yield self.listChildren(onlyInTrash=onlyInTrash)
for name in names:
- x.append((yield self.childWithName(name)))
+ x.append((yield self.childWithName(name, onlyInTrash=onlyInTrash)))
returnValue(x)
+ def _childrenKey(self, onlyInTrash):
+ return "TRASHED" if onlyInTrash else "NOTTRASHED"
+
+
@inlineCallbacks
def loadChildren(self):
"""
@@ -3443,27 +3447,29 @@
"""
results = (yield self._childClass.loadAllObjects(self))
for result in results:
- self._children[result.name()] = result
- self._children[result._resourceID] = result
+ key = self._childrenKey(result.isInTrash())
+ self._children[key][result.name()] = result
+ self._children[key][result._resourceID] = result
self._childrenLoaded = True
returnValue(results)
- def listChildren(self):
+ @inlineCallbacks
+ def listChildren(self, onlyInTrash=False):
"""
Retrieve the names of the children in this home.
@return: an iterable of C{str}s.
"""
- if self._childrenLoaded:
- return succeed([k for k in self._children.keys() if isinstance(k, str)])
- else:
- return self._childClass.listObjects(self)
+ if not self._childrenLoaded:
+ yield self.loadChildren()
+ names = [k for k in self._children[self._childrenKey(onlyInTrash)].keys() if isinstance(k, str)]
+ returnValue(names)
- @memoizedKey("name", "_children")
- def childWithName(self, name):
+ @inlineCallbacks
+ def childWithName(self, name, onlyInTrash=False):
"""
Retrieve the child with the given C{name} contained in this
home.
@@ -3471,10 +3477,15 @@
@param name: a string.
@return: an L{ICalendar} or C{None} if no such child exists.
"""
- return self._childClass.objectWithName(self, name)
+ childrenKey = self._childrenKey(onlyInTrash)
+ if name not in self._children[childrenKey]:
+ child = yield self._childClass.objectWithName(self, name, onlyInTrash=onlyInTrash)
+ if child is not None:
+ self._children[childrenKey][name] = child
+ returnValue(self._children[childrenKey].get(name, None))
- def anyObjectWithShareUID(self, shareUID):
+ def anyObjectWithShareUID(self, shareUID, onlyInTrash=False):
"""
Retrieve the child accepted or otherwise with the given bind identifier contained in this
home.
@@ -3482,11 +3493,11 @@
@param name: a string.
@return: an L{ICalendar} or C{None} if no such child exists.
"""
- return self._childClass.objectWithName(self, shareUID, accepted=None)
+ return self._childClass.objectWithName(self, shareUID, accepted=None, onlyInTrash=onlyInTrash)
- @memoizedKey("resourceID", "_children")
- def childWithID(self, resourceID):
+ @inlineCallbacks
+ def childWithID(self, resourceID, onlyInTrash=False):
"""
Retrieve the child with the given C{resourceID} contained in this
home.
@@ -3494,10 +3505,15 @@
@param name: a string.
@return: an L{ICalendar} or C{None} if no such child exists.
"""
- return self._childClass.objectWithID(self, resourceID)
+ childrenKey = self._childrenKey(onlyInTrash)
+ if resourceID not in self._children[childrenKey]:
+ child = yield self._childClass.objectWithID(self, resourceID, onlyInTrash=onlyInTrash)
+ if child is not None:
+ self._children[childrenKey][resourceID] = child
+ returnValue(self._children[childrenKey].get(resourceID, None))
- def childWithExternalID(self, externalID):
+ def childWithExternalID(self, externalID, onlyInTrash=False):
"""
Retrieve the child with the given C{externalID} contained in this
home.
@@ -3505,10 +3521,10 @@
@param name: a string.
@return: an L{ICalendar} or C{None} if no such child exists.
"""
- return self._childClass.objectWithExternalID(self, externalID)
+ return self._childClass.objectWithExternalID(self, externalID, onlyInTrash=onlyInTrash)
- def allChildWithID(self, resourceID):
+ def allChildWithID(self, resourceID, onlyInTrash=False):
"""
Retrieve the child with the given C{resourceID} contained in this
home.
@@ -3516,7 +3532,7 @@
@param name: a string.
@return: an L{ICalendar} or C{None} if no such child exists.
"""
- return self._childClass.objectWithID(self, resourceID, accepted=None)
+ return self._childClass.objectWithID(self, resourceID, accepted=None, onlyInTrash=onlyInTrash)
@inlineCallbacks
@@ -3536,8 +3552,9 @@
resourceID = child._resourceID
yield child.remove()
- self._children.pop(name, None)
- self._children.pop(resourceID, None)
+ for d in self._children:
+ d.pop(name, None)
+ d.pop(resourceID, None)
@inlineCallbacks
@@ -5144,15 +5161,16 @@
@inlineCallbacks
def _changedStatus(self, previouslyAcceptedCount):
+ key = self._home._childrenKey(self.isInTrash())
if self._bindStatus == _BIND_STATUS_ACCEPTED:
yield self._initSyncToken()
yield self._initBindRevision()
- self._home._children[self._name] = self
- self._home._children[self._resourceID] = self
+ self._home._children[key][self._name] = self
+ self._home._children[key][self._resourceID] = self
elif self._bindStatus in (_BIND_STATUS_INVITED, _BIND_STATUS_DECLINED):
yield self._deletedSyncToken(sharedRemoval=True)
- self._home._children.pop(self._name, None)
- self._home._children.pop(self._resourceID, None)
+ self._home._children[key].pop(self._name, None)
+ self._home._children[key].pop(self._resourceID, None)
@inlineCallbacks
@@ -5167,12 +5185,13 @@
@return: a L{Deferred} which will fire with the previous shareUID
"""
+ key = self._home._childrenKey(self.isInTrash())
# remove sync tokens
shareeHome = shareeView.viewerHome()
yield shareeView._deletedSyncToken(sharedRemoval=True)
- shareeHome._children.pop(shareeView._name, None)
- shareeHome._children.pop(shareeView._resourceID, None)
+ shareeHome._children[key].pop(shareeView._name, None)
+ shareeHome._children[key].pop(shareeView._resourceID, None)
# Must send notification to ensure cache invalidation occurs
yield self.notifyPropertyChanged()
@@ -5766,22 +5785,6 @@
@classmethod
@inlineCallbacks
- def listObjects(cls, home):
- """
- Retrieve the names of the children that exist in the given home.
-
- @return: an iterable of C{str}s.
- """
- # FIXME: tests don't cover this as directly as they should.
- rows = yield cls._acceptedBindForHomeID.on(
- home._txn, homeID=home._resourceID
- )
- names = [row[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] for row in rows]
- returnValue(names)
-
-
- @classmethod
- @inlineCallbacks
def loadAllObjects(cls, home):
"""
Load all L{CommonHomeChild} instances which are children of a given
@@ -5823,29 +5826,32 @@
@classmethod
- def objectWithName(cls, home, name, accepted=True):
- return cls.objectWith(home, name=name, accepted=accepted)
+ def objectWithName(cls, home, name, accepted=True, onlyInTrash=False):
+ return cls.objectWith(home, name=name, accepted=accepted, onlyInTrash=onlyInTrash)
@classmethod
- def objectWithID(cls, home, resourceID, accepted=True):
- return cls.objectWith(home, resourceID=resourceID, accepted=accepted)
+ def objectWithID(cls, home, resourceID, accepted=True, onlyInTrash=False):
+ return cls.objectWith(home, resourceID=resourceID, accepted=accepted, onlyInTrash=onlyInTrash)
@classmethod
- def objectWithExternalID(cls, home, externalID, accepted=True):
- return cls.objectWith(home, externalID=externalID, accepted=accepted)
+ def objectWithExternalID(cls, home, externalID, accepted=True, onlyInTrash=False):
+ return cls.objectWith(home, externalID=externalID, accepted=accepted, onlyInTrash=onlyInTrash)
@classmethod
@inlineCallbacks
- def objectWith(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
+ def objectWith(
+ cls, home, name=None, resourceID=None, externalID=None, accepted=True,
+ onlyInTrash=False
+ ):
"""
Create the object using one of the specified arguments as the key to load it. One
and only one of the keyword arguments must be set.
- @param parent: parent collection
- @type parent: L{CommonHomeChild}
+ @param home: home
+ @type home: L{CommonHome}
@param name: name of the resource, or C{None}
@type name: C{str}
@param uid: resource data UID, or C{None}
@@ -5868,6 +5874,18 @@
if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
returnValue(None)
+ # Suppress if the collection is trash-capable and is in the trash
+ try:
+ isInTrash = metadataData[cls.metadataColumns().index(cls._homeChildMetaDataSchema.IS_IN_TRASH)]
+ except (AttributeError, ValueError):
+ isInTrash = False
+ if onlyInTrash:
+ if not isInTrash:
+ returnValue(None)
+ else:
+ if isInTrash:
+ returnValue(None)
+
child = yield cls.makeClass(home, bindData, additionalBindData, metadataData)
returnValue(child)
@@ -6074,8 +6092,9 @@
homeID=self._home._resourceID)
self._name = name
# update memos
- del self._home._children[oldName]
- self._home._children[name] = self
+ key = self._home._childrenKey(self.isInTrash())
+ del self._home._children[key][oldName]
+ self._home._children[key][name] = self
yield self._renameSyncToken()
yield self.notifyPropertyChanged()
@@ -6098,7 +6117,8 @@
"""
if config.EnableTrashCollection:
- if self.isInTrash():
+ isInTrash = yield self.isInTrash()
+ if isInTrash:
raise AlreadyInTrashError
else:
yield self.toTrash()
@@ -6130,6 +6150,110 @@
yield self._home.notifyChanged()
+ @classproperty
+ def _updateIsInTrashQuery(cls):
+ table = cls._homeChildMetaDataSchema
+ return Update(
+ {table.IS_IN_TRASH: Parameter("isInTrash"), table.TRASHED: Parameter("trashed")},
+ Where=table.RESOURCE_ID == Parameter("resourceID"),
+ )
+
+
+ @inlineCallbacks
+ def toTrash(self):
+ # print("XYZZY collection toTrash")
+ for resource in (yield self.objectResources()):
+ yield resource.toTrash()
+ whenTrashed = datetime.datetime.utcnow()
+ yield self._updateIsInTrashQuery.on(
+ self._txn, isInTrash=True, trashed=whenTrashed, resourceID=self._resourceID
+ )
+ newName = "{}-{}".format(self._name[:200], str(uuid4()))
+ yield self.rename(newName)
+ # yield self.notifyPropertyChanged()
+ # yield self.invalidateQueryCache()
+ yield self._deletedSyncToken()
+ # yield self._home.notifyChanged()
+
+ # Update _children cache to reflect moving to trash
+ try:
+ del self._home._children[self._home._childrenKey(False)][newName]
+ except KeyError:
+ pass
+ try:
+ del self._home._children[self._home._childrenKey(False)][self._resourceID]
+ except KeyError:
+ pass
+ self._home._children[self._home._childrenKey(True)][newName] = self
+ self._home._children[self._home._childrenKey(True)][self._resourceID] = self
+
+ self._isInTrash = True
+ self._trashed = str(whenTrashed)
+
+
+ @inlineCallbacks
+ def fromTrash(self, restoreChildren=True, delta=datetime.timedelta(minutes=3)):
+
+ # print("XYZZY collection fromTrash")
+ if not self._isInTrash:
+ returnValue(None)
+
+ startTime = self.whenTrashed()
+ if delta is not None:
+ startTime = startTime - delta
+
+
+ yield self._updateIsInTrashQuery.on(
+ self._txn, isInTrash=False, trashed=None, resourceID=self._resourceID
+ )
+ yield self._initSyncToken()
+ yield self.notifyPropertyChanged()
+ yield self.invalidateQueryCache()
+ yield self._home.notifyChanged()
+ self._isInTrash = False
+ self._trashed = None
+
+ # Update _children cache to reflect moving from trash
+ try:
+ del self._home._children[self._home._childrenKey(True)][self._name]
+ except KeyError:
+ pass
+ try:
+ del self._home._children[self._home._childrenKey(True)][self._resourceID]
+ except KeyError:
+ pass
+ self._home._children[self._home._childrenKey(False)][self._name] = self
+ self._home._children[self._home._childrenKey(False)][self._resourceID] = self
+
+ if restoreChildren:
+ trash = yield self._home.childWithName("trash")
+ childrenToRestore = yield trash.trashForCollection(
+ self._resourceID, start=startTime
+ )
+ for child in childrenToRestore:
+ # print("Restoring", child)
+ yield child.fromTrash()
+
+
+
+ @classproperty
+ def _selectIsInTrashQuery(cls):
+ table = cls._homeChildMetaDataSchema
+ return Select((table.IS_IN_TRASH, table.TRASHED), From=table, Where=table.RESOURCE_ID == Parameter("resourceID"))
+
+
+
+ def isInTrash(self):
+ return self._isInTrash
+
+
+
+ def whenTrashed(self):
+ if self._trashed is None:
+ return None
+ return parseSQLTimestamp(self._trashed)
+
+
def ownerHome(self):
"""
@see: L{ICalendar.ownerCalendarHome}
@@ -6192,8 +6316,7 @@
@inlineCallbacks
def listObjectResources(self):
"""
- Returns a list of names of object resources in this collection, taking
- into account the IS_IN_TRASH flag and skipping those in the trash.
+ Returns a list of names of object resources in this collection
"""
if self._objectNames is None:
self._objectNames = yield self._objectResourceClass.listObjects(self)
@@ -6900,28 +7023,19 @@
rows = None
if name:
- if parent.isTrash():
- # the name needs to be split
- parentID, name = parent.parseName(name)
- else:
- parentID = parent._resourceID
-
+ parentID = parent._resourceID
rows = yield cls._allColumnsWithParentAndName.on(
parent._txn,
name=name,
parentID=parentID
)
elif uid:
- assert not parent.isTrash(), "UID lookup in Trash not supported"
-
rows = yield cls._allColumnsWithParentAndUID.on(
parent._txn,
uid=uid,
parentID=parent._resourceID
)
elif resourceID:
- assert not parent.isTrash(), "ID lookup in Trash not supported"
-
rows = yield cls._allColumnsWithParentAndID.on(
parent._txn,
resourceID=resourceID,
@@ -7030,7 +7144,7 @@
rows = yield Select(
[obj.RESOURCE_NAME],
From=obj,
- Where=(obj.PARENT_RESOURCE_ID == Parameter('parentID')).And(obj.IS_IN_TRASH == False)
+ Where=(obj.PARENT_RESOURCE_ID == Parameter('parentID'))
).on(parent._txn, parentID=parent.id())
returnValue(sorted([row[0] for row in rows]))
@@ -7145,13 +7259,6 @@
if row:
child = yield cls.makeClass(parent, row)
-
- # Use the name that was passed in, especially to cover the case
- # where the "trashed" name is prepended with the original collection
- # id (otherwise the name will be missing that id)
- if name is not None:
- child._name = name
-
returnValue(child)
else:
returnValue(None)
@@ -7510,83 +7617,79 @@
@classproperty
- def _updateIsInTrashQuery(cls):
+ def _updateToTrashQuery(cls):
obj = cls._objectSchema
return Update(
- {obj.IS_IN_TRASH: Parameter("isInTrash"), obj.TRASHED: Parameter("trashed")},
+ {obj.ORIGINAL_COLLECTION: Parameter("originalCollection"), obj.TRASHED: Parameter("trashed")},
Where=obj.RESOURCE_ID == Parameter("resourceID"),
)
- @inlineCallbacks
- def toTrash(self):
- yield self._updateIsInTrashQuery.on(
- self._txn, isInTrash=True, trashed=datetime.datetime.utcnow(), resourceID=self._resourceID
+ @classproperty
+ def _updateFromTrashQuery(cls):
+ obj = cls._objectSchema
+ return Update(
+ {obj.ORIGINAL_COLLECTION: None, obj.TRASHED: None},
+ Where=obj.RESOURCE_ID == Parameter("resourceID"),
)
- yield self._parentCollection.removedObjectResource(self)
- yield self._parentCollection._deleteRevision(self.name())
- trash = yield self._parentCollection._home.childWithName("trash")
- if trash is not None:
- yield trash._insertRevision(
- trash.nameForResource(
- self._parentCollection,
- self
- )
- )
+ @classproperty
+ def _selectTrashDataQuery(cls):
+ obj = cls._objectSchema
+ return Select((obj.ORIGINAL_COLLECTION, obj.TRASHED), From=obj, Where=obj.RESOURCE_ID == Parameter("resourceID"))
+
@inlineCallbacks
- def fromTrash(self):
+ def originalCollection(self):
+ originalCollectionID, whenTrashed = (
+ yield self._selectTrashDataQuery.on(
+ self._txn, resourceID=self._resourceID
+ )
+ )[0]
+ originalCollection = yield self._parentCollection._home.childWithID(originalCollectionID)
+ returnValue(originalCollection)
- # First make sure this is actually in the trash
- isInTrash = yield self.isInTrash()
- if not isInTrash:
- returnValue(None)
- if self._parentCollection.isTrash():
- # The parent is indeed the trash collection
- trash = self._parentCollection
- self._parentCollection = yield trash.originalParentForResource(self)
- trashedName = self._name # for deleting trash revision below
- _ignore, newName = trash.parseName(self._name)
- else:
- # The parent is the original collection because it was retrieved
- # via that parent, not the trash collection
- home = self._parentCollection.viewerHome()
- trash = yield home.childWithName("trash")
- trashedName = trash.nameForResource(self._parentCollection, self)
- newName = self._name
- yield self._updateIsInTrashQuery.on(
- self._txn, isInTrash=False, trashed=None, resourceID=self._resourceID
+ @inlineCallbacks
+ def toTrash(self):
+ originalCollection = self._parentCollection._resourceID
+ trash = yield self._parentCollection._home.childWithName("trash")
+ newName = str(uuid4())
+ yield self.moveTo(trash, name=newName)
+ yield self._updateToTrashQuery.on(
+ self._txn, originalCollection=originalCollection, trashed=datetime.datetime.utcnow(), resourceID=self._resourceID
)
- yield trash._deleteRevision(trashedName)
+ returnValue(newName)
- self._name = newName
- yield self._parentCollection.addedObjectResource(self)
- yield self._parentCollection._insertRevision(self.name())
+ @inlineCallbacks
+ def fromTrash(self):
+ originalCollection = yield self.originalCollection()
+ yield self.moveTo(originalCollection)
+ yield self._updateFromTrashQuery.on(
+ self._txn, resourceID=self._resourceID
+ )
+ returnValue(self._name)
-
-
@classproperty
def _selectIsInTrashQuery(cls):
obj = cls._objectSchema
- return Select((obj.IS_IN_TRASH, obj.TRASHED), From=obj, Where=obj.RESOURCE_ID == Parameter("resourceID"))
+ return Select((obj.ORIGINAL_COLLECTION, obj.TRASHED), From=obj, Where=obj.RESOURCE_ID == Parameter("resourceID"))
@inlineCallbacks
def isInTrash(self):
- returnValue(
- (
- yield self._selectIsInTrashQuery.on(
- self._txn, resourceID=self._resourceID
- )
- )[0][0]
- )
+ originalCollectionID = (
+ yield self._selectIsInTrashQuery.on(
+ self._txn, resourceID=self._resourceID
+ )
+ )[0][0]
+ returnValue(originalCollectionID is not None)
+
@inlineCallbacks
def whenTrashed(self):
returnValue(
Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql_schema/current.sql 2015-03-05 21:26:24 UTC (rev 14514)
+++ CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/sql_schema/current.sql 2015-03-05 22:11:06 UTC (rev 14515)
@@ -263,7 +263,7 @@
MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
DATAVERSION integer default 0 not null,
TRASHED timestamp default null,
- IS_IN_TRASH boolean default false not null, -- entire resource is in the trash
+ ORIGINAL_COLLECTION integer default null, -- calendar_resource_id prior to trash
unique (CALENDAR_RESOURCE_ID, RESOURCE_NAME) -- implicit index
Modified: CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/test/test_sql.py 2015-03-05 21:26:24 UTC (rev 14514)
+++ CalendarServer/branches/users/sagen/trashcan-5/txdav/common/datastore/test/test_sql.py 2015-03-05 22:11:06 UTC (rev 14515)
@@ -517,10 +517,16 @@
class CommonTrashTests(StoreTestCase):
+ def _homeForUser(self, txn, userName):
+ return txn.calendarHomeWithUID(userName, create=True)
+
@inlineCallbacks
- def _collectionForUser(self, txn, userName, collectionName):
+ def _collectionForUser(self, txn, userName, collectionName, create=False, onlyInTrash=False):
home = yield txn.calendarHomeWithUID(userName, create=True)
- collection = yield home.childWithName(collectionName)
+ collection = yield home.childWithName(collectionName, onlyInTrash=onlyInTrash)
+ if collection is None:
+ if create:
+ collection = yield home.createCalendarWithName(collectionName)
returnValue(collection)
@@ -637,7 +643,7 @@
self.assertTrue(trashed is None)
# Move object to trash
- yield resource.toTrash()
+ newName = yield resource.toTrash()
yield txn.commit()
yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
@@ -645,7 +651,7 @@
txn = self.store.newTransaction()
# Verify it's in the trash
- resource = yield self._getResource(txn, "user01", "trash", "")
+ resource = yield self._getResource(txn, "user01", "trash", newName)
self.assertTrue((yield resource.isInTrash()))
trashed = yield resource.whenTrashed()
self.assertFalse(trashed is None)
@@ -674,7 +680,7 @@
# One object in collection
resourceNames = yield self._getResourceNames(txn, "user01", "calendar")
self.assertEqual(len(resourceNames), 1)
- resource = yield self._getResource(txn, "user01", "calendar", "test.ics")
+ resource = yield self._getResource(txn, "user01", "calendar", newName)
self.assertFalse((yield resource.isInTrash()))
trashed = yield resource.whenTrashed()
self.assertTrue(trashed is None)
@@ -887,11 +893,11 @@
txn = self.store.newTransaction()
# user01's copy should be back on their calendar
- resource = yield self._getResource(txn, "user01", "calendar", "test.ics")
+ resource = yield self._getResource(txn, "user01", "calendar", "")
self.assertFalse((yield resource.isInTrash()))
trashed = yield resource.whenTrashed()
self.assertTrue(trashed is None)
- data = yield self._getResourceData(txn, "user01", "calendar", "test.ics")
+ data = yield self._getResourceData(txn, "user01", "calendar", "")
self.assertTrue("PARTSTAT=NEEDS-ACTION" in data)
# user02's copy should be back on their calendar
@@ -1670,7 +1676,7 @@
txn = self.store.newTransaction()
# user01's copy should be back on their calendar
- data = yield self._getResourceData(txn, "user01", "calendar", "test.ics")
+ data = yield self._getResourceData(txn, "user01", "calendar", "")
self.assertTrue("PARTSTAT=TENTATIVE" in data)
# user02's copy should be back on their calendar
@@ -1965,7 +1971,7 @@
# user01 restores event from the trash
txn = self.store.newTransaction()
resource = yield self._getResource(txn, "user01", "trash", "")
- yield resource.fromTrash()
+ trashedName = yield resource.fromTrash()
yield txn.commit()
yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
@@ -1976,16 +1982,16 @@
resourceNames = yield self._getResourceNames(txn, "user01", "trash")
self.assertEquals(len(resourceNames), 0)
- # user01 should have test.ics and a new .ics
+ # user01 should have two .ics
resourceNames = yield self._getResourceNames(txn, "user01", "calendar")
self.assertEquals(len(resourceNames), 2)
- self.assertTrue("test.ics" in resourceNames)
- resourceNames.remove("test.ics")
+ self.assertTrue(trashedName in resourceNames)
+ resourceNames.remove(trashedName)
newName = resourceNames[0]
# user01's test.ics -- verify it got split correctly, by making sure
# it's got a count other than 20 now
- data = yield self._getResourceData(txn, "user01", "calendar", "test.ics")
+ data = yield self._getResourceData(txn, "user01", "calendar", trashedName)
self.assertTrue("COUNT=" in data)
self.assertFalse("COUNT=20" in data)
@@ -2165,3 +2171,166 @@
self.assertTrue("PARTSTAT=ACCEPTED" in data)
yield txn.commit()
+
+
+ @inlineCallbacks
+ def test_trashCalendar(self):
+
+ from twistedcaldav.stdconfig import config
+ self.patch(config, "EnableTrashCollection", True)
+
+ txn = self.store.newTransaction()
+
+ collection = yield self._collectionForUser(txn, "user01", "test", create=True)
+ isInTrash = collection.isInTrash()
+ self.assertFalse(isInTrash)
+ whenTrashed = collection.whenTrashed()
+ self.assertEquals(whenTrashed, None)
+
+ home = yield self._homeForUser(txn, "user01")
+ names = yield home.listChildren()
+ self.assertTrue("test" in names)
+ names = yield home.listChildren(onlyInTrash=True)
+ self.assertFalse("test" in names)
+
+ yield collection.remove()
+ isInTrash = collection.isInTrash()
+ self.assertTrue(isInTrash)
+ whenTrashed = collection.whenTrashed()
+ self.assertNotEquals(whenTrashed, None)
+
+ collection = yield self._collectionForUser(txn, "user01", "test")
+ self.assertEquals(collection, None)
+
+ yield txn.commit()
+
+ yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
+
+ txn = self.store.newTransaction()
+
+ collection = yield self._collectionForUser(txn, "user01", "test")
+ self.assertEquals(collection, None)
+ home = yield self._homeForUser(txn, "user01")
+ names = yield home.listChildren(onlyInTrash=True)
+ trashedName = names[0]
+ collection = yield self._collectionForUser(txn, "user01", trashedName, onlyInTrash=True)
+ self.assertNotEquals(collection, None)
+ home = yield self._homeForUser(txn, "user01")
+ names = yield home.listChildren()
+ self.assertFalse("test" in names)
+ names = yield home.listChildren(onlyInTrash=True)
+ self.assertTrue(trashedName in names)
+
+ yield collection.fromTrash()
+
+ yield txn.commit()
+
+ yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
+
+ txn = self.store.newTransaction()
+ home = yield self._homeForUser(txn, "user01")
+ names = yield home.listChildren()
+ self.assertTrue(trashedName in names)
+ names = yield home.listChildren(onlyInTrash=True)
+ self.assertFalse("test" in names)
+ yield txn.commit()
+
+
+ @inlineCallbacks
+ def test_trashCalendarWithUnscheduled(self):
+
+ from twistedcaldav.stdconfig import config
+ self.patch(config, "EnableTrashCollection", True)
+
+ txn = self.store.newTransaction()
+
+ collection = yield self._collectionForUser(txn, "user01", "test", create=True)
+
+ data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:5CE3B280-DBC9-4E8E-B0B2-996754020E5F
+DTSTART;TZID=America/Los_Angeles:20141108T093000
+DTEND;TZID=America/Los_Angeles:20141108T103000
+CREATED:20141106T192546Z
+DTSTAMP:20141106T192546Z
+RRULE:FREQ=DAILY
+SEQUENCE:0
+SUMMARY:repeating event
+TRANSP:OPAQUE
+END:VEVENT
+BEGIN:VEVENT
+UID:5CE3B280-DBC9-4E8E-B0B2-996754020E5F
+RECURRENCE-ID;TZID=America/Los_Angeles:20141111T093000
+DTSTART;TZID=America/Los_Angeles:20141111T110000
+DTEND;TZID=America/Los_Angeles:20141111T120000
+CREATED:20141106T192546Z
+DTSTAMP:20141106T192546Z
+SEQUENCE:0
+SUMMARY:repeating event
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+ # Create an object
+ resource = yield collection.createObjectResourceWithName(
+ "test.ics",
+ Component.allFromString(data1)
+ )
+
+ # One object in collection
+ objects = yield collection.listObjectResources()
+ self.assertEquals(len(objects), 1)
+
+ # No objects in trash
+ trash = yield self._collectionForUser(txn, "user01", "trash")
+ objects = yield trash.listObjectResources()
+ self.assertEquals(len(objects), 0)
+
+ # Verify it's not in the trash
+ self.assertFalse((yield resource.isInTrash()))
+ trashed = yield resource.whenTrashed()
+ self.assertTrue(trashed is None)
+
+ collection = yield self._collectionForUser(txn, "user01", "test")
+ resources = yield trash.trashForCollection(collection._resourceID)
+ self.assertEquals(len(resources), 0)
+
+ yield txn.commit()
+
+ txn = self.store.newTransaction()
+ collection = yield self._collectionForUser(txn, "user01", "test")
+ yield collection.remove()
+ yield txn.commit()
+
+ txn = self.store.newTransaction()
+ # One object in trash
+ trash = yield self._collectionForUser(txn, "user01", "trash")
+ objects = yield trash.listObjectResources()
+ self.assertEquals(len(objects), 1)
+
+ resources = yield trash.trashForCollection(collection._resourceID)
+ self.assertEquals(len(resources), 1)
+
+ home = yield self._homeForUser(txn, "user01")
+ names = yield home.listChildren(onlyInTrash=True)
+ trashedName = names[0]
+ collection = yield self._collectionForUser(txn, "user01", trashedName, onlyInTrash=True)
+ yield collection.fromTrash()
+
+ yield txn.commit()
+
+ yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
+
+ txn = self.store.newTransaction()
+ home = yield self._homeForUser(txn, "user01")
+ names = yield home.listChildren()
+ self.assertTrue(trashedName in names)
+ names = yield home.listChildren(onlyInTrash=True)
+ self.assertFalse(trashedName in names)
+ resourceNames = yield self._getResourceNames(txn, "user01", trashedName)
+ self.assertEqual(len(resourceNames), 1)
+
+ yield txn.commit()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150305/58dc39e8/attachment-0001.html>
More information about the calendarserver-changes
mailing list