[CalendarServer-changes] [14641] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Tue Mar 31 15:19:14 PDT 2015
Revision: 14641
http://trac.calendarserver.org//changeset/14641
Author: sagen at apple.com
Date: 2015-03-31 15:19:14 -0700 (Tue, 31 Mar 2015)
Log Message:
-----------
Adds trash API via POST on calendar home (so far, viewing trash contents and emptying trash)
Modified Paths:
--------------
CalendarServer/trunk/calendarserver/tools/trash.py
CalendarServer/trunk/calendarserver/tools/util.py
CalendarServer/trunk/twistedcaldav/directory/calendar.py
CalendarServer/trunk/twistedcaldav/resource.py
CalendarServer/trunk/txdav/caldav/datastore/sql.py
CalendarServer/trunk/txdav/common/datastore/sql.py
CalendarServer/trunk/txdav/common/datastore/test/test_trash.py
Modified: CalendarServer/trunk/calendarserver/tools/trash.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/trash.py 2015-03-31 20:20:48 UTC (rev 14640)
+++ CalendarServer/trunk/calendarserver/tools/trash.py 2015-03-31 22:19:14 UTC (rev 14641)
@@ -21,12 +21,10 @@
import datetime
from calendarserver.tools.cmdline import utilityMain, WorkerService
-from calendarserver.tools.util import prettyRecord
+from calendarserver.tools.util import prettyRecord, displayNameForCollection, agoString, locationString
from pycalendar.datetime import DateTime, Timezone
from twext.python.log import Logger
from twisted.internet.defer import inlineCallbacks, returnValue
-from txdav.base.propertystore.base import PropertyName
-from txdav.xml import element
log = Logger()
@@ -147,38 +145,10 @@
yield store.inTransaction(label="List trashed collections", operation=doIt)
-def agoString(delta):
- if delta.days:
- agoString = "{} days ago".format(delta.days)
- elif delta.seconds:
- if delta.seconds < 60:
- agoString = "{} second{} ago".format(delta.seconds, "s" if delta.seconds > 1 else "")
- else:
- minutesAgo = delta.seconds / 60
- if minutesAgo < 60:
- agoString = "{} minute{} ago".format(minutesAgo, "s" if minutesAgo > 1 else "")
- else:
- hoursAgo = minutesAgo / 60
- agoString = "{} hour{} ago".format(hoursAgo, "s" if hoursAgo > 1 else "")
- return agoString
-
-
def startString(pydt):
return pydt.getLocaleDateTime(DateTime.FULLDATE, False, True, pydt.getTimezoneID())
-def locationString(component):
- locationProps = component.properties("LOCATION")
- if locationProps is not None:
- locations = []
- for locationProp in locationProps:
- locations.append(locationProp.value())
- locationString = ", ".join(locations)
- else:
- locationString = ""
- return locationString
-
-
@inlineCallbacks
def printEventDetails(event):
nowPyDT = DateTime.getNowUTC()
@@ -231,6 +201,7 @@
print(" {} {}".format(startString(dtstart), location))
+
@inlineCallbacks
def listTrashedEventsForPrincipal(service, store, principalUID):
directory = store.directoryService()
@@ -268,7 +239,7 @@
for child in children:
print()
yield printEventDetails(child)
- print()
+ print("")
yield store.inTransaction(label="List trashed events", operation=doIt)
@@ -337,7 +308,6 @@
yield store.inTransaction(label="Restore trashed event", operation=doIt)
-
@inlineCallbacks
def emptyTrashForPrincipal(service, store, principalUID, days, txn=None, verbose=True):
directory = store.directoryService()
@@ -347,7 +317,6 @@
print("No record found for:", principalUID)
returnValue(None)
-
@inlineCallbacks
def doIt(txn):
home = yield txn.calendarHomeWithUID(principalUID)
@@ -356,37 +325,8 @@
print("No home for principal")
returnValue(None)
- trash = yield home.getTrash()
- if trash is None:
- if verbose:
- print("No trash available")
- returnValue(None)
+ yield home.emptyTrash(days=days, verbose=verbose)
- untrashedCollections = yield home.children(onlyInTrash=False)
- if len(untrashedCollections) == 0:
- if verbose:
- print("No untrashed collections for:", prettyRecord(record))
- returnValue(None)
-
- endTime = datetime.datetime.utcnow() - datetime.timedelta(days=-days)
- for collection in untrashedCollections:
- displayName = displayNameForCollection(collection)
- children = yield trash.trashForCollection(
- collection._resourceID, end=endTime
- )
- if len(children) == 0:
- continue
-
- if verbose:
- print("Collection \"{}\":".format(displayName.encode("utf-8")))
- for child in children:
- component = yield child.component()
- summary = component.mainComponent().propertyValue("SUMMARY", "<no title>")
- if verbose:
- print(" Removing \"{}\"...".format(summary))
- yield child.reallyRemove()
- print()
-
if txn is None:
yield store.inTransaction(label="Empty trash", operation=doIt)
else:
@@ -394,16 +334,7 @@
-def displayNameForCollection(collection):
- try:
- displayName = collection.properties()[
- PropertyName.fromElement(element.DisplayName)
- ]
- displayName = displayName.toString()
- except:
- displayName = collection.name()
- return displayName
if __name__ == "__main__":
Modified: CalendarServer/trunk/calendarserver/tools/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/util.py 2015-03-31 20:20:48 UTC (rev 14640)
+++ CalendarServer/trunk/calendarserver/tools/util.py 2015-03-31 22:19:14 UTC (rev 14641)
@@ -24,6 +24,7 @@
"booleanArgument",
]
+import datetime
import os
from time import sleep
import socket
@@ -42,7 +43,9 @@
from twistedcaldav import memcachepool
-from txdav.who.groups import GroupCacherPollingWork
+from txdav.base.propertystore.base import PropertyName
+from txdav.xml import element
+from pycalendar.datetime import DateTime, Timezone
log = Logger()
@@ -384,6 +387,7 @@
# Schedule work the PeerConnectionPool will pick up as overdue
def groupPollNow(txn):
+ from txdav.who.groups import GroupCacherPollingWork
return GroupCacherPollingWork.reschedule(txn, 0, force=True)
yield store.inTransaction("addProxy groupPollNow", groupPollNow)
@@ -424,6 +428,7 @@
if removed:
# Schedule work the PeerConnectionPool will pick up as overdue
def groupPollNow(txn):
+ from txdav.who.groups import GroupCacherPollingWork
return GroupCacherPollingWork.reschedule(txn, 0, force=True)
yield store.inTransaction("removeProxy groupPollNow", groupPollNow)
returnValue(removed)
@@ -448,7 +453,99 @@
)
+def displayNameForCollection(collection):
+ try:
+ displayName = collection.properties()[
+ PropertyName.fromElement(element.DisplayName)
+ ]
+ displayName = displayName.toString()
+ except:
+ displayName = collection.name()
+ return displayName
+
+
+def agoString(delta):
+ if delta.days:
+ agoString = "{} days ago".format(delta.days)
+ elif delta.seconds:
+ if delta.seconds < 60:
+ agoString = "{} second{} ago".format(delta.seconds, "s" if delta.seconds > 1 else "")
+ else:
+ minutesAgo = delta.seconds / 60
+ if minutesAgo < 60:
+ agoString = "{} minute{} ago".format(minutesAgo, "s" if minutesAgo > 1 else "")
+ else:
+ hoursAgo = minutesAgo / 60
+ agoString = "{} hour{} ago".format(hoursAgo, "s" if hoursAgo > 1 else "")
+ return agoString
+
+
+def locationString(component):
+ locationProps = component.properties("LOCATION")
+ if locationProps is not None:
+ locations = []
+ for locationProp in locationProps:
+ locations.append(locationProp.value())
+ locationString = ", ".join(locations)
+ else:
+ locationString = ""
+ return locationString
+
+
+ at inlineCallbacks
+def getEventDetails(event):
+ detail = {}
+
+ nowPyDT = DateTime.getNowUTC()
+ nowDT = datetime.datetime.utcnow()
+ oneYearInFuture = DateTime.getNowUTC()
+ oneYearInFuture.offsetDay(365)
+
+ component = yield event.component()
+ mainSummary = component.mainComponent().propertyValue("SUMMARY", u"<no title>")
+ whenTrashed = yield event.whenTrashed()
+ ago = nowDT - whenTrashed
+
+ detail["summary"] = mainSummary
+ detail["whenTrashed"] = agoString(ago)
+ detail["recoveryID"] = event._resourceID
+
+ if component.isRecurring():
+ detail["recurring"] = True
+ detail["instances"] = []
+ instances = component.cacheExpandedTimeRanges(oneYearInFuture)
+ instances = sorted(instances.instances.values(), key=lambda x: x.start)
+ limit = 3
+ count = 0
+ for instance in instances:
+ if instance.start >= nowPyDT:
+ summary = instance.component.propertyValue("SUMMARY", u"<no title>")
+ location = locationString(instance.component)
+ tzid = instance.component.getProperty("DTSTART").parameterValue("TZID", None)
+ dtstart = instance.start
+ if tzid is not None:
+ timezone = Timezone(tzid=tzid)
+ dtstart.adjustTimezone(timezone)
+ detail["instances"].append({
+ "summary": summary,
+ "starttime": dtstart.getLocaleDateTime(DateTime.FULLDATE, False, True, dtstart.getTimezoneID()),
+ "location": location
+ })
+ count += 1
+ limit -= 1
+ if limit == 0:
+ break
+
+ else:
+ detail["recurring"] = False
+ dtstart = component.mainComponent().propertyValue("DTSTART")
+ detail["starttime"] = dtstart.getLocaleDateTime(DateTime.FULLDATE, False, True, dtstart.getTimezoneID())
+ detail["location"] = locationString(component.mainComponent())
+
+ returnValue(detail)
+
+
class ProxyError(Exception):
"""
Raised when proxy assignments cannot be performed
Modified: CalendarServer/trunk/twistedcaldav/directory/calendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendar.py 2015-03-31 20:20:48 UTC (rev 14640)
+++ CalendarServer/trunk/twistedcaldav/directory/calendar.py 2015-03-31 22:19:14 UTC (rev 14641)
@@ -29,7 +29,7 @@
from twext.python.log import Logger
from txweb2 import responsecode
from txweb2.dav.util import joinURL
-from txweb2.http import HTTPError
+from txweb2.http import HTTPError, JSONResponse
from txweb2.http_headers import ETag, MimeType
from twisted.internet.defer import succeed, inlineCallbacks, returnValue
@@ -44,7 +44,10 @@
from twistedcaldav.resource import CalendarHomeResource
from uuid import uuid4
+from txweb2.dav.http import ErrorResponse
+from twistedcaldav.caldavxml import caldav_namespace
+
log = Logger()
@@ -290,3 +293,44 @@
def principalForRecord(self):
return self.parent.principalForRecord(self.record)
+
+
+ @inlineCallbacks
+ def POST_handler_action(self, request, action):
+ """
+ Handle a POST request with an action= query parameter
+
+ @param request: the request to process
+ @type request: L{Request}
+ @param action: the action to execute
+ @type action: C{str}
+ """
+ if action == "emptytrash":
+ days = int(request.args.get("days", ("0",))[0])
+ yield self._newStoreHome.emptyTrash(days=days)
+ returnValue(
+ self._ok("ok", "Empty Trash")
+ )
+
+ elif action == "gettrashcontents":
+ contents = yield self._newStoreHome.getTrashContents()
+ returnValue(
+ self._ok("ok", "Trash Contents", contents)
+ )
+
+ else:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "valid-action-parameter",),
+ "The action parameter in the request-URI is not valid",
+ ))
+
+ def _ok(self, status, description, result=None):
+ if result is None:
+ result = {}
+ result["status"] = status
+ result["description"] = description
+ return JSONResponse(
+ responsecode.OK,
+ result,
+ )
Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py 2015-03-31 20:20:48 UTC (rev 14640)
+++ CalendarServer/trunk/twistedcaldav/resource.py 2015-03-31 22:19:14 UTC (rev 14641)
@@ -53,7 +53,9 @@
davPrivilegeSet
from txweb2.dav.resource import TwistedACLInheritable
from txweb2.dav.util import joinURL, parentForURL, normalizeURL
-from txweb2.http import HTTPError, RedirectResponse, StatusResponse, Response
+from txweb2.http import (
+ HTTPError, RedirectResponse, StatusResponse, Response
+)
from txweb2.dav.http import ErrorResponse
from txweb2.http_headers import MimeType, ETag
from txweb2.stream import MemoryStream
Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py 2015-03-31 20:20:48 UTC (rev 14640)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py 2015-03-31 22:19:14 UTC (rev 14641)
@@ -4110,11 +4110,15 @@
return succeed(None)
+ @inlineCallbacks
def remove(self, implicitly=True, bypassTrash=False):
- return self._removeInternal(
- internal_state=ComponentRemoveState.NORMAL if implicitly else ComponentRemoveState.NORMAL_NO_IMPLICIT,
- bypassTrash=bypassTrash
- )
+ if (yield self.isInTrash()):
+ implicitly = False
+ returnValue((
+ yield self._removeInternal(
+ internal_state=ComponentRemoveState.NORMAL if implicitly else ComponentRemoveState.NORMAL_NO_IMPLICIT, bypassTrash=bypassTrash
+ )
+ ))
def purge(self):
Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py 2015-03-31 20:20:48 UTC (rev 14640)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py 2015-03-31 22:19:14 UTC (rev 14641)
@@ -80,6 +80,7 @@
from txdav.common.idirectoryservice import IStoreDirectoryService, \
DirectoryRecordNotFoundError
from txdav.idav import ChangeCategory
+from calendarserver.tools.util import displayNameForCollection, getEventDetails, agoString
from zope.interface import implements, directlyProvides
@@ -2921,7 +2922,108 @@
returnValue((None, None))
+ @inlineCallbacks
+ def emptyTrash(self, days=0, verbose=False):
+ trash = yield self.getTrash()
+ if trash is None:
+ if verbose:
+ print("No trash collection for principal")
+ returnValue(None)
+ endTime = datetime.datetime.utcnow() - datetime.timedelta(days=days)
+
+ untrashedCollections = yield self.children(onlyInTrash=False)
+ for collection in untrashedCollections:
+ displayName = displayNameForCollection(collection)
+ children = yield trash.trashForCollection(
+ collection._resourceID, end=endTime
+ )
+ if len(children) == 0:
+ continue
+
+ if verbose:
+ print("Collection \"{}\":".format(displayName.encode("utf-8")))
+ for child in children:
+ component = yield child.component()
+ summary = component.mainComponent().propertyValue("SUMMARY", "<no title>")
+ if verbose:
+ print(" Removing \"{}\"...".format(summary))
+ yield child.reallyRemove()
+ if verbose:
+ print("")
+
+ trashedCollections = yield self.children(onlyInTrash=True)
+ for collection in trashedCollections:
+ displayName = displayNameForCollection(collection)
+ children = yield trash.trashForCollection(
+ collection._resourceID, end=endTime
+ )
+ if verbose:
+ print("Collection \"{}\":".format(displayName.encode("utf-8")))
+ for child in children:
+ component = yield child.component()
+ summary = component.mainComponent().propertyValue("SUMMARY", "<no title>")
+ if verbose:
+ print(" Removing \"{}\"...".format(summary))
+ yield child.reallyRemove()
+ if verbose:
+ print("")
+
+ if collection.whenTrashed() < endTime:
+ if verbose:
+ print("Removing collection \"{}\"...".format(displayName.encode("utf-8")))
+ yield collection.reallyRemove()
+
+
+ @inlineCallbacks
+ def getTrashContents(self):
+ result = {
+ "trashedcollections": [],
+ "untrashedcollections": []
+ }
+
+ trash = yield self.getTrash()
+ if trash is None:
+ returnValue(result)
+
+ nowDT = datetime.datetime.utcnow()
+
+ trashedCollections = yield self.children(onlyInTrash=True)
+ for collection in trashedCollections:
+ whenTrashed = collection.whenTrashed()
+ detail = {
+ "displayName": displayNameForCollection(collection),
+ "recoveryID": collection._resourceID,
+ "whenTrashed": agoString(nowDT - whenTrashed),
+ "children": [],
+ }
+ startTime = whenTrashed - datetime.timedelta(minutes=5)
+ children = yield trash.trashForCollection(
+ collection._resourceID, start=startTime
+ )
+ for child in children:
+ component = yield child.component()
+ summary = component.mainComponent().propertyValue("SUMMARY", "<no title>")
+ detail["children"].append(summary.encode("utf-8"))
+ result["trashedcollections"].append(detail)
+
+ untrashedCollections = yield self.children(onlyInTrash=False)
+ for collection in untrashedCollections:
+ children = yield trash.trashForCollection(collection._resourceID)
+ if len(children) == 0:
+ continue
+ detail = {
+ "displayName": displayNameForCollection(collection),
+ "children": [],
+ }
+ for child in children:
+ childDetail = yield getEventDetails(child)
+ detail["children"].append(childDetail)
+ result["untrashedcollections"].append(detail)
+
+ returnValue(result)
+
+
class CommonHomeChild(FancyEqMixin, Memoizable, _SharedSyncLogic, HomeChildBase, SharingMixIn):
"""
Common ancestor class of AddressBooks and Calendars.
@@ -3512,6 +3614,7 @@
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
Modified: CalendarServer/trunk/txdav/common/datastore/test/test_trash.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/test/test_trash.py 2015-03-31 20:20:48 UTC (rev 14640)
+++ CalendarServer/trunk/txdav/common/datastore/test/test_trash.py 2015-03-31 22:19:14 UTC (rev 14641)
@@ -2019,4 +2019,36 @@
yield emptyTrashForPrincipal(None, self.store, "user01", 0, txn=txn, verbose=False)
names = yield self._getResourceNames(txn, "user01", trashName)
self.assertEquals(len(names), 0)
+ result = yield txn.execSQL("select * from calendar_object", [])
+ self.assertEquals(len(result), 0)
yield txn.commit()
+
+ # Add event again, and this time remove the containing calendar
+ txn = self.store.newTransaction()
+ calendar = yield self._collectionForUser(txn, "user01", "calendar")
+ yield calendar.createObjectResourceWithName(
+ "test.ics",
+ Component.allFromString(data1)
+ )
+ yield txn.commit()
+
+ txn = self.store.newTransaction()
+ calendar = yield self._collectionForUser(txn, "user01", "calendar")
+ result = yield txn.execSQL("select * from calendar_object", [])
+ yield calendar.remove()
+ home = yield self._homeForUser(txn, "user01")
+ trashedCollections = yield home.children(onlyInTrash=True)
+ self.assertEquals(len(trashedCollections), 1)
+ yield txn.commit()
+
+ txn = self.store.newTransaction()
+ yield emptyTrashForPrincipal(None, self.store, "user01", 0, txn=txn, verbose=False)
+ yield txn.commit()
+
+ txn = self.store.newTransaction()
+ home = yield self._homeForUser(txn, "user01")
+ trashedCollections = yield home.children(onlyInTrash=True)
+ self.assertEquals(len(trashedCollections), 0)
+ result = yield txn.execSQL("select * from calendar_object", [])
+ self.assertEquals(len(result), 0)
+ yield txn.commit()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150331/12027407/attachment-0001.html>
More information about the calendarserver-changes
mailing list