<!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>[14641] CalendarServer/trunk</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/14641">14641</a></dd>
<dt>Author</dt> <dd>sagen@apple.com</dd>
<dt>Date</dt> <dd>2015-03-31 15:19:14 -0700 (Tue, 31 Mar 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Adds trash API via POST on calendar home (so far, viewing trash contents and emptying trash)</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkcalendarservertoolstrashpy">CalendarServer/trunk/calendarserver/tools/trash.py</a></li>
<li><a href="#CalendarServertrunkcalendarservertoolsutilpy">CalendarServer/trunk/calendarserver/tools/util.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavdirectorycalendarpy">CalendarServer/trunk/twistedcaldav/directory/calendar.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavresourcepy">CalendarServer/trunk/twistedcaldav/resource.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoresqlpy">CalendarServer/trunk/txdav/caldav/datastore/sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresqlpy">CalendarServer/trunk/txdav/common/datastore/sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoretesttest_trashpy">CalendarServer/trunk/txdav/common/datastore/test/test_trash.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcalendarservertoolstrashpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/trash.py (14640 => 14641)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -21,12 +21,10 @@
</span><span class="cx"> import datetime
</span><span class="cx"> 
</span><span class="cx"> from calendarserver.tools.cmdline import utilityMain, WorkerService
</span><del>-from calendarserver.tools.util import prettyRecord
</del><ins>+from calendarserver.tools.util import prettyRecord, displayNameForCollection, agoString, locationString
</ins><span class="cx"> from pycalendar.datetime import DateTime, Timezone
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><del>-from txdav.base.propertystore.base import PropertyName
-from txdav.xml import element
</del><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><span class="lines">@@ -147,38 +145,10 @@
</span><span class="cx">     yield store.inTransaction(label=&quot;List trashed collections&quot;, operation=doIt)
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-def agoString(delta):
-    if delta.days:
-        agoString = &quot;{} days ago&quot;.format(delta.days)
-    elif delta.seconds:
-        if delta.seconds &lt; 60:
-            agoString = &quot;{} second{} ago&quot;.format(delta.seconds, &quot;s&quot; if delta.seconds &gt; 1 else &quot;&quot;)
-        else:
-            minutesAgo = delta.seconds / 60
-            if minutesAgo &lt; 60:
-                agoString = &quot;{} minute{} ago&quot;.format(minutesAgo, &quot;s&quot; if minutesAgo &gt; 1 else &quot;&quot;)
-            else:
-                hoursAgo = minutesAgo / 60
-                agoString = &quot;{} hour{} ago&quot;.format(hoursAgo, &quot;s&quot; if hoursAgo &gt; 1 else &quot;&quot;)
-    return agoString
-
-
</del><span class="cx"> def startString(pydt):
</span><span class="cx">     return pydt.getLocaleDateTime(DateTime.FULLDATE, False, True, pydt.getTimezoneID())
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-def locationString(component):
-    locationProps = component.properties(&quot;LOCATION&quot;)
-    if locationProps is not None:
-        locations = []
-        for locationProp in locationProps:
-            locations.append(locationProp.value())
-        locationString = &quot;, &quot;.join(locations)
-    else:
-        locationString = &quot;&quot;
-    return locationString
-
-
</del><span class="cx"> @inlineCallbacks
</span><span class="cx"> def printEventDetails(event):
</span><span class="cx">     nowPyDT = DateTime.getNowUTC()
</span><span class="lines">@@ -231,6 +201,7 @@
</span><span class="cx">         print(&quot;         {} {}&quot;.format(startString(dtstart), location))
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx"> @inlineCallbacks
</span><span class="cx"> def listTrashedEventsForPrincipal(service, store, principalUID):
</span><span class="cx">     directory = store.directoryService()
</span><span class="lines">@@ -268,7 +239,7 @@
</span><span class="cx">             for child in children:
</span><span class="cx">                 print()
</span><span class="cx">                 yield printEventDetails(child)
</span><del>-            print()
</del><ins>+            print(&quot;&quot;)
</ins><span class="cx"> 
</span><span class="cx">     yield store.inTransaction(label=&quot;List trashed events&quot;, operation=doIt)
</span><span class="cx"> 
</span><span class="lines">@@ -337,7 +308,6 @@
</span><span class="cx">     yield store.inTransaction(label=&quot;Restore trashed event&quot;, operation=doIt)
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-
</del><span class="cx"> @inlineCallbacks
</span><span class="cx"> def emptyTrashForPrincipal(service, store, principalUID, days, txn=None, verbose=True):
</span><span class="cx">     directory = store.directoryService()
</span><span class="lines">@@ -347,7 +317,6 @@
</span><span class="cx">             print(&quot;No record found for:&quot;, principalUID)
</span><span class="cx">         returnValue(None)
</span><span class="cx"> 
</span><del>-
</del><span class="cx">     @inlineCallbacks
</span><span class="cx">     def doIt(txn):
</span><span class="cx">         home = yield txn.calendarHomeWithUID(principalUID)
</span><span class="lines">@@ -356,37 +325,8 @@
</span><span class="cx">                 print(&quot;No home for principal&quot;)
</span><span class="cx">             returnValue(None)
</span><span class="cx"> 
</span><del>-        trash = yield home.getTrash()
-        if trash is None:
-            if verbose:
-                print(&quot;No trash available&quot;)
-            returnValue(None)
</del><ins>+        yield home.emptyTrash(days=days, verbose=verbose)
</ins><span class="cx"> 
</span><del>-        untrashedCollections = yield home.children(onlyInTrash=False)
-        if len(untrashedCollections) == 0:
-            if verbose:
-                print(&quot;No untrashed collections for:&quot;, 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(&quot;Collection \&quot;{}\&quot;:&quot;.format(displayName.encode(&quot;utf-8&quot;)))
-            for child in children:
-                component = yield child.component()
-                summary = component.mainComponent().propertyValue(&quot;SUMMARY&quot;, &quot;&lt;no title&gt;&quot;)
-                if verbose:
-                    print(&quot;   Removing \&quot;{}\&quot;...&quot;.format(summary))
-                yield child.reallyRemove()
-            print()
-
</del><span class="cx">     if txn is None:
</span><span class="cx">         yield store.inTransaction(label=&quot;Empty trash&quot;, operation=doIt)
</span><span class="cx">     else:
</span><span class="lines">@@ -394,16 +334,7 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-def displayNameForCollection(collection):
-    try:
-        displayName = collection.properties()[
-            PropertyName.fromElement(element.DisplayName)
-        ]
-        displayName = displayName.toString()
-    except:
-        displayName = collection.name()
</del><span class="cx"> 
</span><del>-    return displayName
</del><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> if __name__ == &quot;__main__&quot;:
</span></span></pre></div>
<a id="CalendarServertrunkcalendarservertoolsutilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/util.py (14640 => 14641)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -24,6 +24,7 @@
</span><span class="cx">     &quot;booleanArgument&quot;,
</span><span class="cx"> ]
</span><span class="cx"> 
</span><ins>+import datetime
</ins><span class="cx"> import os
</span><span class="cx"> from time import sleep
</span><span class="cx"> import socket
</span><span class="lines">@@ -42,7 +43,9 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> from twistedcaldav import memcachepool
</span><del>-from txdav.who.groups import GroupCacherPollingWork
</del><ins>+from txdav.base.propertystore.base import PropertyName
+from txdav.xml import element
+from pycalendar.datetime import DateTime, Timezone
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="lines">@@ -384,6 +387,7 @@
</span><span class="cx"> 
</span><span class="cx">     # Schedule work the PeerConnectionPool will pick up as overdue
</span><span class="cx">     def groupPollNow(txn):
</span><ins>+        from txdav.who.groups import GroupCacherPollingWork
</ins><span class="cx">         return GroupCacherPollingWork.reschedule(txn, 0, force=True)
</span><span class="cx">     yield store.inTransaction(&quot;addProxy groupPollNow&quot;, groupPollNow)
</span><span class="cx"> 
</span><span class="lines">@@ -424,6 +428,7 @@
</span><span class="cx">     if removed:
</span><span class="cx">         # Schedule work the PeerConnectionPool will pick up as overdue
</span><span class="cx">         def groupPollNow(txn):
</span><ins>+            from txdav.who.groups import GroupCacherPollingWork
</ins><span class="cx">             return GroupCacherPollingWork.reschedule(txn, 0, force=True)
</span><span class="cx">         yield store.inTransaction(&quot;removeProxy groupPollNow&quot;, groupPollNow)
</span><span class="cx">     returnValue(removed)
</span><span class="lines">@@ -448,7 +453,99 @@
</span><span class="cx">     )
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+def displayNameForCollection(collection):
+    try:
+        displayName = collection.properties()[
+            PropertyName.fromElement(element.DisplayName)
+        ]
+        displayName = displayName.toString()
+    except:
+        displayName = collection.name()
</ins><span class="cx"> 
</span><ins>+    return displayName
+
+
+def agoString(delta):
+    if delta.days:
+        agoString = &quot;{} days ago&quot;.format(delta.days)
+    elif delta.seconds:
+        if delta.seconds &lt; 60:
+            agoString = &quot;{} second{} ago&quot;.format(delta.seconds, &quot;s&quot; if delta.seconds &gt; 1 else &quot;&quot;)
+        else:
+            minutesAgo = delta.seconds / 60
+            if minutesAgo &lt; 60:
+                agoString = &quot;{} minute{} ago&quot;.format(minutesAgo, &quot;s&quot; if minutesAgo &gt; 1 else &quot;&quot;)
+            else:
+                hoursAgo = minutesAgo / 60
+                agoString = &quot;{} hour{} ago&quot;.format(hoursAgo, &quot;s&quot; if hoursAgo &gt; 1 else &quot;&quot;)
+    return agoString
+
+
+def locationString(component):
+    locationProps = component.properties(&quot;LOCATION&quot;)
+    if locationProps is not None:
+        locations = []
+        for locationProp in locationProps:
+            locations.append(locationProp.value())
+        locationString = &quot;, &quot;.join(locations)
+    else:
+        locationString = &quot;&quot;
+    return locationString
+
+
+@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(&quot;SUMMARY&quot;, u&quot;&lt;no title&gt;&quot;)
+    whenTrashed = yield event.whenTrashed()
+    ago = nowDT - whenTrashed
+
+    detail[&quot;summary&quot;] = mainSummary
+    detail[&quot;whenTrashed&quot;] = agoString(ago)
+    detail[&quot;recoveryID&quot;] = event._resourceID
+
+    if component.isRecurring():
+        detail[&quot;recurring&quot;] = True
+        detail[&quot;instances&quot;] = []
+        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 &gt;= nowPyDT:
+                summary = instance.component.propertyValue(&quot;SUMMARY&quot;, u&quot;&lt;no title&gt;&quot;)
+                location = locationString(instance.component)
+                tzid = instance.component.getProperty(&quot;DTSTART&quot;).parameterValue(&quot;TZID&quot;, None)
+                dtstart = instance.start
+                if tzid is not None:
+                    timezone = Timezone(tzid=tzid)
+                    dtstart.adjustTimezone(timezone)
+                detail[&quot;instances&quot;].append({
+                    &quot;summary&quot;: summary,
+                    &quot;starttime&quot;: dtstart.getLocaleDateTime(DateTime.FULLDATE, False, True, dtstart.getTimezoneID()),
+                    &quot;location&quot;: location
+                })
+                count += 1
+                limit -= 1
+            if limit == 0:
+                break
+
+    else:
+        detail[&quot;recurring&quot;] = False
+        dtstart = component.mainComponent().propertyValue(&quot;DTSTART&quot;)
+        detail[&quot;starttime&quot;] = dtstart.getLocaleDateTime(DateTime.FULLDATE, False, True, dtstart.getTimezoneID())
+        detail[&quot;location&quot;] = locationString(component.mainComponent())
+
+    returnValue(detail)
+
+
</ins><span class="cx"> class ProxyError(Exception):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Raised when proxy assignments cannot be performed
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavdirectorycalendarpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/directory/calendar.py (14640 => 14641)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -29,7 +29,7 @@
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from txweb2 import responsecode
</span><span class="cx"> from txweb2.dav.util import joinURL
</span><del>-from txweb2.http import HTTPError
</del><ins>+from txweb2.http import HTTPError, JSONResponse
</ins><span class="cx"> from txweb2.http_headers import ETag, MimeType
</span><span class="cx"> 
</span><span class="cx"> from twisted.internet.defer import succeed, inlineCallbacks, returnValue
</span><span class="lines">@@ -44,7 +44,10 @@
</span><span class="cx"> from twistedcaldav.resource import CalendarHomeResource
</span><span class="cx"> 
</span><span class="cx"> from uuid import uuid4
</span><ins>+from txweb2.dav.http import ErrorResponse
+from twistedcaldav.caldavxml import caldav_namespace
</ins><span class="cx"> 
</span><ins>+
</ins><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -290,3 +293,44 @@
</span><span class="cx"> 
</span><span class="cx">     def principalForRecord(self):
</span><span class="cx">         return self.parent.principalForRecord(self.record)
</span><ins>+
+
+    @inlineCallbacks
+    def POST_handler_action(self, request, action):
+        &quot;&quot;&quot;
+        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}
+        &quot;&quot;&quot;
+        if action == &quot;emptytrash&quot;:
+            days = int(request.args.get(&quot;days&quot;, (&quot;0&quot;,))[0])
+            yield self._newStoreHome.emptyTrash(days=days)
+            returnValue(
+                self._ok(&quot;ok&quot;, &quot;Empty Trash&quot;)
+            )
+
+        elif action == &quot;gettrashcontents&quot;:
+            contents = yield self._newStoreHome.getTrashContents()
+            returnValue(
+                self._ok(&quot;ok&quot;, &quot;Trash Contents&quot;, contents)
+            )
+
+        else:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (caldav_namespace, &quot;valid-action-parameter&quot;,),
+                &quot;The action parameter in the request-URI is not valid&quot;,
+            ))
+
+    def _ok(self, status, description, result=None):
+        if result is None:
+            result = {}
+        result[&quot;status&quot;] = status
+        result[&quot;description&quot;] = description
+        return JSONResponse(
+            responsecode.OK,
+            result,
+        )
</ins></span></pre></div>
<a id="CalendarServertrunktwistedcaldavresourcepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/resource.py (14640 => 14641)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -53,7 +53,9 @@
</span><span class="cx">     davPrivilegeSet
</span><span class="cx"> from txweb2.dav.resource import TwistedACLInheritable
</span><span class="cx"> from txweb2.dav.util import joinURL, parentForURL, normalizeURL
</span><del>-from txweb2.http import HTTPError, RedirectResponse, StatusResponse, Response
</del><ins>+from txweb2.http import (
+    HTTPError, RedirectResponse, StatusResponse, Response
+)
</ins><span class="cx"> from txweb2.dav.http import ErrorResponse
</span><span class="cx"> from txweb2.http_headers import MimeType, ETag
</span><span class="cx"> from txweb2.stream import MemoryStream
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py (14640 => 14641)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -4110,11 +4110,15 @@
</span><span class="cx">         return succeed(None)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
</ins><span class="cx">     def remove(self, implicitly=True, bypassTrash=False):
</span><del>-        return self._removeInternal(
-            internal_state=ComponentRemoveState.NORMAL if implicitly else ComponentRemoveState.NORMAL_NO_IMPLICIT,
-            bypassTrash=bypassTrash
-        )
</del><ins>+        if (yield self.isInTrash()):
+            implicitly = False
+        returnValue((
+            yield self._removeInternal(
+                internal_state=ComponentRemoveState.NORMAL if implicitly else ComponentRemoveState.NORMAL_NO_IMPLICIT, bypassTrash=bypassTrash
+            )
+        ))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def purge(self):
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/sql.py (14640 => 14641)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -80,6 +80,7 @@
</span><span class="cx"> from txdav.common.idirectoryservice import IStoreDirectoryService, \
</span><span class="cx">     DirectoryRecordNotFoundError
</span><span class="cx"> from txdav.idav import ChangeCategory
</span><ins>+from calendarserver.tools.util import displayNameForCollection, getEventDetails, agoString
</ins><span class="cx"> 
</span><span class="cx"> from zope.interface import implements, directlyProvides
</span><span class="cx"> 
</span><span class="lines">@@ -2921,7 +2922,108 @@
</span><span class="cx">             returnValue((None, None))
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def emptyTrash(self, days=0, verbose=False):
+        trash = yield self.getTrash()
+        if trash is None:
+            if verbose:
+                print(&quot;No trash collection for principal&quot;)
+            returnValue(None)
</ins><span class="cx"> 
</span><ins>+        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(&quot;Collection \&quot;{}\&quot;:&quot;.format(displayName.encode(&quot;utf-8&quot;)))
+            for child in children:
+                component = yield child.component()
+                summary = component.mainComponent().propertyValue(&quot;SUMMARY&quot;, &quot;&lt;no title&gt;&quot;)
+                if verbose:
+                    print(&quot;   Removing \&quot;{}\&quot;...&quot;.format(summary))
+                yield child.reallyRemove()
+            if verbose:
+                print(&quot;&quot;)
+
+        trashedCollections = yield self.children(onlyInTrash=True)
+        for collection in trashedCollections:
+            displayName = displayNameForCollection(collection)
+            children = yield trash.trashForCollection(
+                collection._resourceID, end=endTime
+            )
+            if verbose:
+                print(&quot;Collection \&quot;{}\&quot;:&quot;.format(displayName.encode(&quot;utf-8&quot;)))
+            for child in children:
+                component = yield child.component()
+                summary = component.mainComponent().propertyValue(&quot;SUMMARY&quot;, &quot;&lt;no title&gt;&quot;)
+                if verbose:
+                    print(&quot;   Removing \&quot;{}\&quot;...&quot;.format(summary))
+                yield child.reallyRemove()
+            if verbose:
+                print(&quot;&quot;)
+
+            if collection.whenTrashed() &lt; endTime:
+                if verbose:
+                    print(&quot;Removing collection \&quot;{}\&quot;...&quot;.format(displayName.encode(&quot;utf-8&quot;)))
+                yield collection.reallyRemove()
+
+
+    @inlineCallbacks
+    def getTrashContents(self):
+        result = {
+            &quot;trashedcollections&quot;: [],
+            &quot;untrashedcollections&quot;: []
+        }
+
+        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 = {
+                &quot;displayName&quot;: displayNameForCollection(collection),
+                &quot;recoveryID&quot;: collection._resourceID,
+                &quot;whenTrashed&quot;: agoString(nowDT - whenTrashed),
+                &quot;children&quot;: [],
+            }
+            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(&quot;SUMMARY&quot;, &quot;&lt;no title&gt;&quot;)
+                detail[&quot;children&quot;].append(summary.encode(&quot;utf-8&quot;))
+            result[&quot;trashedcollections&quot;].append(detail)
+
+        untrashedCollections = yield self.children(onlyInTrash=False)
+        for collection in untrashedCollections:
+            children = yield trash.trashForCollection(collection._resourceID)
+            if len(children) == 0:
+                continue
+            detail = {
+                &quot;displayName&quot;: displayNameForCollection(collection),
+                &quot;children&quot;: [],
+            }
+            for child in children:
+                childDetail = yield getEventDetails(child)
+                detail[&quot;children&quot;].append(childDetail)
+            result[&quot;untrashedcollections&quot;].append(detail)
+
+        returnValue(result)
+
+
</ins><span class="cx"> class CommonHomeChild(FancyEqMixin, Memoizable, _SharedSyncLogic, HomeChildBase, SharingMixIn):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Common ancestor class of AddressBooks and Calendars.
</span><span class="lines">@@ -3512,6 +3614,7 @@
</span><span class="cx"> 
</span><span class="cx">         for resource in (yield self.objectResources()):
</span><span class="cx">             yield resource.toTrash()
</span><ins>+
</ins><span class="cx">         whenTrashed = datetime.datetime.utcnow()
</span><span class="cx">         yield self._updateIsInTrashQuery.on(
</span><span class="cx">             self._txn, isInTrash=True, trashed=whenTrashed, resourceID=self._resourceID
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoretesttest_trashpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/test/test_trash.py (14640 => 14641)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -2019,4 +2019,36 @@
</span><span class="cx">         yield emptyTrashForPrincipal(None, self.store, &quot;user01&quot;, 0, txn=txn, verbose=False)
</span><span class="cx">         names = yield self._getResourceNames(txn, &quot;user01&quot;, trashName)
</span><span class="cx">         self.assertEquals(len(names), 0)
</span><ins>+        result = yield txn.execSQL(&quot;select * from calendar_object&quot;, [])
+        self.assertEquals(len(result), 0)
</ins><span class="cx">         yield txn.commit()
</span><ins>+
+        # Add event again, and this time remove the containing calendar
+        txn = self.store.newTransaction()
+        calendar = yield self._collectionForUser(txn, &quot;user01&quot;, &quot;calendar&quot;)
+        yield calendar.createObjectResourceWithName(
+            &quot;test.ics&quot;,
+            Component.allFromString(data1)
+        )
+        yield txn.commit()
+
+        txn = self.store.newTransaction()
+        calendar = yield self._collectionForUser(txn, &quot;user01&quot;, &quot;calendar&quot;)
+        result = yield txn.execSQL(&quot;select * from calendar_object&quot;, [])
+        yield calendar.remove()
+        home = yield self._homeForUser(txn, &quot;user01&quot;)
+        trashedCollections = yield home.children(onlyInTrash=True)
+        self.assertEquals(len(trashedCollections), 1)
+        yield txn.commit()
+
+        txn = self.store.newTransaction()
+        yield emptyTrashForPrincipal(None, self.store, &quot;user01&quot;, 0, txn=txn, verbose=False)
+        yield txn.commit()
+
+        txn = self.store.newTransaction()
+        home = yield self._homeForUser(txn, &quot;user01&quot;)
+        trashedCollections = yield home.children(onlyInTrash=True)
+        self.assertEquals(len(trashedCollections), 0)
+        result = yield txn.execSQL(&quot;select * from calendar_object&quot;, [])
+        self.assertEquals(len(result), 0)
+        yield txn.commit()
</ins></span></pre>
</div>
</div>

</body>
</html>