<!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="List trashed collections", operation=doIt)
</span><span class="cx">
</span><span class="cx">
</span><del>-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
-
-
</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("LOCATION")
- if locationProps is not None:
- locations = []
- for locationProp in locationProps:
- locations.append(locationProp.value())
- locationString = ", ".join(locations)
- else:
- locationString = ""
- 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(" {} {}".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("")
</ins><span class="cx">
</span><span class="cx"> yield store.inTransaction(label="List trashed events", operation=doIt)
</span><span class="cx">
</span><span class="lines">@@ -337,7 +308,6 @@
</span><span class="cx"> yield store.inTransaction(label="Restore trashed event", 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("No record found for:", 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("No home for principal")
</span><span class="cx"> returnValue(None)
</span><span class="cx">
</span><del>- trash = yield home.getTrash()
- if trash is None:
- if verbose:
- print("No trash available")
- 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("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()
-
</del><span class="cx"> if txn is None:
</span><span class="cx"> yield store.inTransaction(label="Empty trash", 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__ == "__main__":
</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"> "booleanArgument",
</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("addProxy groupPollNow", 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("removeProxy groupPollNow", 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 = "{} 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
+
+
+@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)
+
+
</ins><span class="cx"> class ProxyError(Exception):
</span><span class="cx"> """
</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):
+ """
+ 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,
+ )
</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("No trash collection for principal")
+ 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("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)
+
+
</ins><span class="cx"> class CommonHomeChild(FancyEqMixin, Memoizable, _SharedSyncLogic, HomeChildBase, SharingMixIn):
</span><span class="cx"> """
</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, "user01", 0, txn=txn, verbose=False)
</span><span class="cx"> names = yield self._getResourceNames(txn, "user01", trashName)
</span><span class="cx"> self.assertEquals(len(names), 0)
</span><ins>+ result = yield txn.execSQL("select * from calendar_object", [])
+ 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, "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()
</ins></span></pre>
</div>
</div>
</body>
</html>