[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