<!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>[14669] 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/14669">14669</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2015-04-09 09:57:35 -0700 (Thu, 09 Apr 2015)</dd>
</dl>
<h3>Log Message</h3>
<pre>Support timezone-id properties for timezones-by-reference.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkrequirementsdevtxt">CalendarServer/trunk/requirements-dev.txt</a></li>
<li><a href="#CalendarServertrunktwistedcaldavcaldavxmlpy">CalendarServer/trunk/twistedcaldav/caldavxml.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavmethodreport_calendar_querypy">CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavresourcepy">CalendarServer/trunk/twistedcaldav/resource.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavstorebridgepy">CalendarServer/trunk/twistedcaldav/storebridge.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavtesttest_calendarquerypy">CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoresqlpy">CalendarServer/trunk/txdav/caldav/datastore/sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoretesttest_sqlpy">CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoreupgradesqlupgradestesttest_upgrade_from_4_to_5py">CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/test/test_upgrade_from_4_to_5.py</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkrequirementsdevtxt"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/requirements-dev.txt (14668 => 14669)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/requirements-dev.txt        2015-04-09 16:55:50 UTC (rev 14668)
+++ CalendarServer/trunk/requirements-dev.txt        2015-04-09 16:57:35 UTC (rev 14669)
</span><span class="lines">@@ -8,4 +8,4 @@
</span><span class="cx"> q
</span><span class="cx"> tl.eggdeps
</span><span class="cx"> --editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVClientLibrary/trunk@13420#egg=CalDAVClientLibrary
</span><del>---editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVTester/trunk@14659#egg=CalDAVTester
</del><ins>+--editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVTester/trunk@14668#egg=CalDAVTester
</ins></span></pre></div>
<a id="CalendarServertrunktwistedcaldavcaldavxmlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/caldavxml.py (14668 => 14669)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/caldavxml.py        2015-04-09 16:55:50 UTC (rev 14668)
+++ CalendarServer/trunk/twistedcaldav/caldavxml.py        2015-04-09 16:57:35 UTC (rev 14669)
</span><span class="lines">@@ -212,6 +212,7 @@
</span><span class="cx"> """
</span><span class="cx"> Returns the calendar data derived from this element.
</span><span class="cx"> """
</span><ins>+ data = None
</ins><span class="cx"> for data in self.children:
</span><span class="cx"> if not isinstance(data, PCDATAElement):
</span><span class="cx"> return None
</span><span class="lines">@@ -318,6 +319,17 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @registerElement
</span><ins>+class CalendarTimeZoneID (CalDAVTextElement):
+ """
+ Specifies a time zone id on a calendar collection.
+ (draft-ietf-tzdist-caldav-timezone-ref-01, Section-5.2)
+ """
+ name = "calendar-timezone-id"
+ hidden = True
+
+
+
+@registerElement
</ins><span class="cx"> class SupportedCalendarComponentSets (CalDAVElement):
</span><span class="cx"> """
</span><span class="cx"> Indicates what set of calendar components the server is willing to allow
</span><span class="lines">@@ -469,6 +481,7 @@
</span><span class="cx"> (dav_namespace, "propname"): (0, None),
</span><span class="cx"> (dav_namespace, "prop"): (0, None),
</span><span class="cx"> (caldav_namespace, "timezone"): (0, 1),
</span><ins>+ (caldav_namespace, "timezone-id"): (0, 1),
</ins><span class="cx"> (caldav_namespace, "filter"): (0, 1), # Actually (1, 1) unless element is empty
</span><span class="cx"> }
</span><span class="cx">
</span><span class="lines">@@ -479,6 +492,7 @@
</span><span class="cx"> props = None
</span><span class="cx"> filter = None
</span><span class="cx"> timezone = None
</span><ins>+ timezone_id = None
</ins><span class="cx">
</span><span class="cx"> for child in self.children:
</span><span class="cx"> qname = child.qname()
</span><span class="lines">@@ -496,8 +510,15 @@
</span><span class="cx"> filter = child
</span><span class="cx">
</span><span class="cx"> elif qname == (caldav_namespace, "timezone"):
</span><ins>+ if timezone_id is not None:
+ raise ValueError("Only one of CalDAV:timezone and CalDAV:timezone-id allowed")
</ins><span class="cx"> timezone = child
</span><span class="cx">
</span><ins>+ elif qname == (caldav_namespace, "timezone-id"):
+ if timezone is not None:
+ raise ValueError("Only one of CalDAV:timezone and CalDAV:timezone-id allowed")
+ timezone_id = child
+
</ins><span class="cx"> else:
</span><span class="cx"> raise AssertionError("We shouldn't be here")
</span><span class="cx">
</span><span class="lines">@@ -508,6 +529,7 @@
</span><span class="cx"> self.props = props
</span><span class="cx"> self.filter = filter
</span><span class="cx"> self.timezone = timezone
</span><ins>+ self.timezone_id = timezone_id
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -906,6 +928,16 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @registerElement
</span><ins>+class TimeZoneID (CalDAVTextElement):
+ """
+ Specifies a time zone id.
+ (draft-ietf-tzdist-caldav-timezone-ref-01, Section-5.2)
+ """
+ name = "timezone-id"
+
+
+
+@registerElement
</ins><span class="cx"> class TimeRange (CalDAVTimeRangeElement):
</span><span class="cx"> """
</span><span class="cx"> Specifies a time for testing components against.
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavmethodreport_calendar_querypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py (14668 => 14669)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py        2015-04-09 16:55:50 UTC (rev 14668)
+++ CalendarServer/trunk/twistedcaldav/method/report_calendar_query.py        2015-04-09 16:57:35 UTC (rev 14669)
</span><span class="lines">@@ -13,6 +13,8 @@
</span><span class="cx"> # See the License for the specific language governing permissions and
</span><span class="cx"> # limitations under the License.
</span><span class="cx"> ##
</span><ins>+from twistedcaldav.timezones import TimezoneException, readVTZ
+from twistedcaldav.ical import Component
</ins><span class="cx">
</span><span class="cx"> """
</span><span class="cx"> CalDAV calendar-query report
</span><span class="lines">@@ -72,18 +74,30 @@
</span><span class="cx">
</span><span class="cx"> # Get the original timezone provided in the query, if any, and validate it now
</span><span class="cx"> query_timezone = None
</span><del>- query_tz = calendar_query.timezone
- if query_tz is not None and not query_tz.valid():
- msg = "CalDAV:timezone must contain one VTIMEZONE component only: %s" % (query_tz,)
- log.error(msg)
- raise HTTPError(ErrorResponse(
- responsecode.FORBIDDEN,
- (caldav_namespace, "valid-calendar-data"),
- "Invalid calendar-data",
- ))
- if query_tz:
</del><ins>+ if calendar_query.timezone:
+ query_tz = calendar_query.timezone
+ if not query_tz.valid():
+ msg = "CalDAV:timezone must contain one VTIMEZONE component only: %s" % (query_tz,)
+ log.error(msg)
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "valid-calendar-data"),
+ "Invalid calendar-data",
+ ))
</ins><span class="cx"> filter.settimezone(query_tz)
</span><del>- query_timezone = tuple(calendar_query.timezone.calendar().subcomponents())[0]
</del><ins>+ query_timezone = tuple(query_tz.calendar().subcomponents())[0]
+ elif calendar_query.timezone_id:
+ query_tzid = calendar_query.timezone_id.toString()
+ try:
+ query_tz = Component(None, pycalendar=readVTZ(query_tzid))
+ except TimezoneException:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "valid-timezone"),
+ "Invalid timezone-id",
+ ))
+ filter.settimezone(query_tz)
+ query_timezone = tuple(query_tz.subcomponents())[0]
</ins><span class="cx">
</span><span class="cx"> if props.qname() == ("DAV:", "allprop"):
</span><span class="cx"> propertiesForResource = report_common.allPropertiesForResource
</span><span class="lines">@@ -171,12 +185,13 @@
</span><span class="cx"> if calresource.isPseudoCalendarCollection():
</span><span class="cx"> # Get the timezone property from the collection if one was not set in the query,
</span><span class="cx"> # and store in the query filter for later use
</span><del>- has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request))
</del><span class="cx"> timezone = query_timezone
</span><del>- if query_tz is None and has_prop:
- tz = (yield calresource.readProperty(CalendarTimeZone(), request))
- filter.settimezone(tz)
- timezone = tuple(tz.calendar().subcomponents())[0]
</del><ins>+ if timezone is None:
+ has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request))
+ if has_prop:
+ tz = (yield calresource.readProperty(CalendarTimeZone(), request))
+ filter.settimezone(tz)
+ timezone = tuple(tz.calendar().subcomponents())[0]
</ins><span class="cx">
</span><span class="cx"> # Do some optimization of access control calculation by determining any inherited ACLs outside of
</span><span class="cx"> # the child resource loop and supply those to the checkPrivileges on each child.
</span><span class="lines">@@ -226,7 +241,7 @@
</span><span class="cx"> # Get the timezone property from the collection if one was not set in the query,
</span><span class="cx"> # and store in the query object for later use
</span><span class="cx"> timezone = query_timezone
</span><del>- if query_tz is None:
</del><ins>+ if timezone is None:
</ins><span class="cx">
</span><span class="cx"> parent = (yield calresource.locateParent(request, uri))
</span><span class="cx"> assert parent is not None and parent.isPseudoCalendarCollection()
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavresourcepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/resource.py (14668 => 14669)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/resource.py        2015-04-09 16:55:50 UTC (rev 14668)
+++ CalendarServer/trunk/twistedcaldav/resource.py        2015-04-09 16:57:35 UTC (rev 14669)
</span><span class="lines">@@ -690,11 +690,11 @@
</span><span class="cx"> "Component %s is not supported by this server" % (component.toxml(),)
</span><span class="cx"> ))
</span><span class="cx">
</span><del>- # Strictly speaking CalDAV:timezone is a live property in the sense that the
- # server enforces what can be stored, however it need not actually
- # exist so we cannot list it in liveProperties on this resource, since its
- # its presence there means that hasProperty will always return True for it.
- elif property.qname() == caldavxml.CalendarTimeZone.qname():
</del><ins>+ # Strictly speaking CalDAV:calendar-timezone and CalDAV:calendar-timezone-id are live properties
+ # in the sense that the server enforces what can be stored, however they need not actually
+ # exist so we cannot list them in liveProperties on this resource, since their presence there
+ # means that hasProperty will always return True for it.
+ elif property.qname() in (caldavxml.CalendarTimeZone.qname(), caldavxml.CalendarTimeZoneID.qname(),):
</ins><span class="cx"> if not self.isCalendarCollection():
</span><span class="cx"> raise HTTPError(StatusResponse(
</span><span class="cx"> responsecode.FORBIDDEN,
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavstorebridgepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/storebridge.py (14668 => 14669)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/storebridge.py        2015-04-09 16:55:50 UTC (rev 14668)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py        2015-04-09 16:57:35 UTC (rev 14669)
</span><span class="lines">@@ -96,6 +96,7 @@
</span><span class="cx"> BAD_REQUEST, OK, INSUFFICIENT_STORAGE_SPACE, SERVICE_UNAVAILABLE
</span><span class="cx"> )
</span><span class="cx"> from txweb2.stream import ProducerStream, readStream, MemoryStream
</span><ins>+from twistedcaldav.timezones import TimezoneException
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> """
</span><span class="lines">@@ -1199,6 +1200,7 @@
</span><span class="cx"> DefaultAlarmPropertyMixin.ALARM_PROPERTIES.keys()
</span><span class="cx"> ) + (
</span><span class="cx"> caldavxml.CalendarTimeZone.qname(),
</span><ins>+ caldavxml.CalendarTimeZoneID.qname(),
</ins><span class="cx"> )
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -1212,7 +1214,7 @@
</span><span class="cx"> if qname in DefaultAlarmPropertyMixin.ALARM_PROPERTIES:
</span><span class="cx"> return succeed(self.getDefaultAlarmProperty(qname) is not None)
</span><span class="cx">
</span><del>- elif qname == caldavxml.CalendarTimeZone.qname():
</del><ins>+ elif qname in (caldavxml.CalendarTimeZone.qname(), caldavxml.CalendarTimeZoneID.qname(),):
</ins><span class="cx"> return succeed(self._newStoreObject.getTimezone() is not None)
</span><span class="cx">
</span><span class="cx"> else:
</span><span class="lines">@@ -1234,6 +1236,10 @@
</span><span class="cx"> format = property.content_type if isinstance(property, caldavxml.CalendarTimeZone) else None
</span><span class="cx"> returnValue(caldavxml.CalendarTimeZone.fromCalendar(timezone, format=format) if timezone else None)
</span><span class="cx">
</span><ins>+ elif qname == caldavxml.CalendarTimeZoneID.qname():
+ tzid = self._newStoreObject.getTimezoneID()
+ returnValue(caldavxml.CalendarTimeZoneID.fromString(tzid) if tzid else None)
+
</ins><span class="cx"> result = (yield super(CalendarCollectionResource, self).readProperty(property, request))
</span><span class="cx"> returnValue(result)
</span><span class="cx">
</span><span class="lines">@@ -1261,6 +1267,18 @@
</span><span class="cx"> yield self._newStoreObject.setTimezone(property.calendar())
</span><span class="cx"> returnValue(None)
</span><span class="cx">
</span><ins>+ elif property.qname() == caldavxml.CalendarTimeZoneID.qname():
+ tzid = property.toString()
+ try:
+ yield self._newStoreObject.setTimezoneID(tzid)
+ except TimezoneException:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "valid-timezone"),
+ description="Invalid property"
+ ))
+ returnValue(None)
+
</ins><span class="cx"> elif property.qname() == caldavxml.ScheduleCalendarTransp.qname():
</span><span class="cx"> yield self._newStoreObject.setUsedForFreeBusy(property == caldavxml.ScheduleCalendarTransp(caldavxml.Opaque()))
</span><span class="cx"> returnValue(None)
</span><span class="lines">@@ -1280,7 +1298,7 @@
</span><span class="cx"> result = (yield self.removeDefaultAlarmProperty(qname))
</span><span class="cx"> returnValue(result)
</span><span class="cx">
</span><del>- elif qname == caldavxml.CalendarTimeZone.qname():
</del><ins>+ elif qname in (caldavxml.CalendarTimeZone.qname(), caldavxml.CalendarTimeZoneID.qname(),):
</ins><span class="cx"> yield self._newStoreObject.setTimezone(None)
</span><span class="cx"> returnValue(None)
</span><span class="cx">
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavtesttest_calendarquerypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py (14668 => 14669)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py        2015-04-09 16:55:50 UTC (rev 14668)
+++ CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py        2015-04-09 16:57:35 UTC (rev 14669)
</span><span class="lines">@@ -22,7 +22,7 @@
</span><span class="cx"> from txweb2.iweb import IResponse
</span><span class="cx"> from txweb2.stream import MemoryStream
</span><span class="cx"> from txdav.xml import element as davxml
</span><del>-from txweb2.dav.util import davXMLFromStream
</del><ins>+from txweb2.dav.util import davXMLFromStream, allDataFromStream
</ins><span class="cx">
</span><span class="cx"> from twistedcaldav import caldavxml
</span><span class="cx"> from twistedcaldav import ical
</span><span class="lines">@@ -36,6 +36,7 @@
</span><span class="cx"> from txdav.caldav.icalendarstore import ComponentUpdateState
</span><span class="cx"> from txdav.caldav.datastore.query.filter import TimeRange
</span><span class="cx"> from twext.who.idirectory import RecordType
</span><ins>+from twistedcaldav.timezones import readVTZ, TimezoneCache
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -174,6 +175,167 @@
</span><span class="cx"> return self.calendar_query(query, got_xml)
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def test_calendar_query_timezone(self):
+ """
+ Partial retrieval of events by time range.
+ (CalDAV-access-09, section 7.6.1)
+ """
+ TimezoneCache.create()
+ self.addCleanup(TimezoneCache.clear)
+
+ tzid1 = "Etc/GMT+1"
+ tz1 = Component(None, pycalendar=readVTZ(tzid1))
+
+ calendar_properties = (
+ davxml.GETETag(),
+ caldavxml.CalendarData(),
+ )
+
+ query_timerange = caldavxml.TimeRange(
+ start="%04d1001T000000Z" % (DateTime.getToday().getYear(),),
+ end="%04d1101T000000Z" % (DateTime.getToday().getYear(),),
+ )
+
+ query = caldavxml.CalendarQuery(
+ davxml.PropertyContainer(*calendar_properties),
+ caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ caldavxml.ComponentFilter(
+ query_timerange,
+ name="VEVENT",
+ ),
+ name="VCALENDAR",
+ ),
+ ),
+ caldavxml.TimeZone.fromCalendar(tz1),
+ )
+
+ def got_xml(doc):
+ if not isinstance(doc.root_element, davxml.MultiStatus):
+ self.fail("REPORT response XML root element is not multistatus: %r" % (doc.root_element,))
+
+ return self.calendar_query(query, got_xml)
+
+
+ def test_calendar_query_timezone_id(self):
+ """
+ Partial retrieval of events by time range.
+ (CalDAV-access-09, section 7.6.1)
+ """
+ TimezoneCache.create()
+ self.addCleanup(TimezoneCache.clear)
+
+ tzid1 = "Etc/GMT+1"
+
+ calendar_properties = (
+ davxml.GETETag(),
+ caldavxml.CalendarData(),
+ )
+
+ query_timerange = caldavxml.TimeRange(
+ start="%04d1001T000000Z" % (DateTime.getToday().getYear(),),
+ end="%04d1101T000000Z" % (DateTime.getToday().getYear(),),
+ )
+
+ query = caldavxml.CalendarQuery(
+ davxml.PropertyContainer(*calendar_properties),
+ caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ caldavxml.ComponentFilter(
+ query_timerange,
+ name="VEVENT",
+ ),
+ name="VCALENDAR",
+ ),
+ ),
+ caldavxml.TimeZoneID.fromString(tzid1),
+ )
+
+ def got_xml(doc):
+ if not isinstance(doc.root_element, davxml.MultiStatus):
+ self.fail("REPORT response XML root element is not multistatus: %r" % (doc.root_element,))
+
+ return self.calendar_query(query, got_xml)
+
+
+ @inlineCallbacks
+ def test_calendar_query_bogus_timezone_id(self):
+ """
+ Partial retrieval of events by time range.
+ (CalDAV-access-09, section 7.6.1)
+ """
+ TimezoneCache.create()
+ self.addCleanup(TimezoneCache.clear)
+
+ calendar_properties = (
+ davxml.GETETag(),
+ caldavxml.CalendarData(),
+ )
+
+ query_timerange = caldavxml.TimeRange(
+ start="%04d1001T000000Z" % (DateTime.getToday().getYear(),),
+ end="%04d1101T000000Z" % (DateTime.getToday().getYear(),),
+ )
+
+ query = caldavxml.CalendarQuery(
+ davxml.PropertyContainer(*calendar_properties),
+ caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ caldavxml.ComponentFilter(
+ query_timerange,
+ name="VEVENT",
+ ),
+ name="VCALENDAR",
+ ),
+ ),
+ caldavxml.TimeZoneID.fromString("bogus"),
+ )
+
+ result = yield self.calendar_query(query, got_xml=None, expected_code=responsecode.FORBIDDEN)
+ self.assertTrue("valid-timezone" in result)
+
+
+ @inlineCallbacks
+ def test_calendar_query_wrong_timezone_elements(self):
+ """
+ Partial retrieval of events by time range.
+ (CalDAV-access-09, section 7.6.1)
+ """
+ TimezoneCache.create()
+ self.addCleanup(TimezoneCache.clear)
+
+ tzid1 = "Etc/GMT+1"
+ tz1 = Component(None, pycalendar=readVTZ(tzid1))
+
+ calendar_properties = (
+ davxml.GETETag(),
+ caldavxml.CalendarData(),
+ )
+
+ query_timerange = caldavxml.TimeRange(
+ start="%04d1001T000000Z" % (DateTime.getToday().getYear(),),
+ end="%04d1101T000000Z" % (DateTime.getToday().getYear(),),
+ )
+
+ query = caldavxml.CalendarQuery(
+ davxml.PropertyContainer(*calendar_properties),
+ caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ caldavxml.ComponentFilter(
+ query_timerange,
+ name="VEVENT",
+ ),
+ name="VCALENDAR",
+ ),
+ ),
+ caldavxml.TimeZone.fromCalendar(tz1),
+ )
+ query.children += (caldavxml.TimeZoneID.fromString(tzid1),)
+
+ result = yield self.calendar_query(query, got_xml=None, expected_code=responsecode.BAD_REQUEST)
+ self.assertTrue("Only one of" in result)
+
+
</ins><span class="cx"> def test_calendar_query_partial_recurring(self):
</span><span class="cx"> """
</span><span class="cx"> Partial retrieval of recurring events.
</span><span class="lines">@@ -343,7 +505,7 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><del>- def calendar_query(self, query, got_xml):
</del><ins>+ def calendar_query(self, query, got_xml, expected_code=responsecode.MULTI_STATUS):
</ins><span class="cx">
</span><span class="cx"> principal = yield self.actualRoot.findPrincipalForAuthID("wsanchez")
</span><span class="cx"> request = SimpleStoreRequest(self, "REPORT", "/calendars/users/wsanchez/calendar/", authPrincipal=principal)
</span><span class="lines">@@ -352,9 +514,14 @@
</span><span class="cx">
</span><span class="cx"> response = IResponse(response)
</span><span class="cx">
</span><del>- if response.code != responsecode.MULTI_STATUS:
</del><ins>+ if response.code != expected_code:
</ins><span class="cx"> self.fail("REPORT failed: %s" % (response.code,))
</span><span class="cx">
</span><del>- returnValue(
- (yield davXMLFromStream(response.stream).addCallback(got_xml))
- )
</del><ins>+ if got_xml is not None:
+ returnValue(
+ (yield davXMLFromStream(response.stream).addCallback(got_xml))
+ )
+ else:
+ returnValue(
+ (yield allDataFromStream(response.stream))
+ )
</ins></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py (14668 => 14669)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/sql.py        2015-04-09 16:55:50 UTC (rev 14668)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py        2015-04-09 16:57:35 UTC (rev 14669)
</span><span class="lines">@@ -50,7 +50,7 @@
</span><span class="cx"> pyCalendarToSQLTimestamp, parseSQLDateToPyCalendar
</span><span class="cx"> from twistedcaldav.ical import Component, InvalidICalendarDataError, Property
</span><span class="cx"> from twistedcaldav.instance import InvalidOverriddenInstanceError
</span><del>-from twistedcaldav.timezones import TimezoneException
</del><ins>+from twistedcaldav.timezones import TimezoneException, readVTZ, hasTZ
</ins><span class="cx">
</span><span class="cx"> from txdav.base.propertystore.base import PropertyName
</span><span class="cx"> from txdav.caldav.datastore.query.builder import buildExpression
</span><span class="lines">@@ -1368,26 +1368,88 @@
</span><span class="cx">
</span><span class="cx"> def getTimezone(self):
</span><span class="cx"> """
</span><del>- Return the VTIMEZONE data.
</del><ins>+ Return the VTIMEZONE data. L{self._timezone} will either be a full iCalendar component
+ (BEGIN:VCALENDAR ... END:VCALENDAR) or just the timezone id.
</ins><span class="cx">
</span><del>- @return: the component (text)
</del><ins>+ @return: the component
</ins><span class="cx"> @rtype: L{Component}
</span><span class="cx"> """
</span><span class="cx">
</span><del>- return Component.fromString(self._timezone) if self._timezone else None
</del><ins>+ if self._timezone:
+ if self._timezone.startswith("BEGIN:VCALENDAR"):
+ return Component.fromString(self._timezone)
+ else:
+ try:
+ return Component(None, pycalendar=readVTZ(self._timezone))
+ except TimezoneException:
+ return None
+ else:
+ return None
</ins><span class="cx">
</span><span class="cx">
</span><del>- @inlineCallbacks
</del><ins>+ def getTimezoneID(self):
+ """
+ Return the VTIMEZONE TZID value. L{self._timezone} will either be a full iCalendar component
+ (BEGIN:VCALENDAR ... END:VCALENDAR) or just the timezone id.
+
+ @return: the timezone id
+ @rtype: L{str}
+ """
+
+ if self._timezone:
+ if self._timezone.startswith("BEGIN:VCALENDAR"):
+ tz = Component.fromString(self._timezone)
+ return list(tz.timezones())[0]
+ else:
+ return self._timezone
+ else:
+ return None
+
+
</ins><span class="cx"> def setTimezone(self, timezone):
</span><span class="cx"> """
</span><del>- Set VTIMEZONE data.
</del><ins>+ Set VTIMEZONE data. If the TZID is in our database, then just store the TZID value, otherwise store the
+ entire iCalendar object as text.
</ins><span class="cx">
</span><span class="cx"> @param timezone: the component
</span><span class="cx"> @type timezone: L{Component}
</span><span class="cx"> """
</span><span class="cx">
</span><del>- self._timezone = str(timezone) if timezone else None
</del><ins>+ if timezone is not None:
+ try:
+ tzid = list(timezone.timezones())[0]
+ if hasTZ(tzid):
+ self._timezone = tzid
+ except TimezoneException:
+ self._timezone = str(timezone)
+ else:
+ self._timezone = None
+ return self._setTimezoneValue()
</ins><span class="cx">
</span><ins>+
+ def setTimezoneID(self, tzid):
+ """
+ Set VTIMEZONE via a TZID value. Make sure the TZID is in our TZ database.
+
+ @param tzid: the component
+ @type tzid: L{Component}
+
+ @raise L{TimezoneException} if tzid is not in our database
+ """
+
+ if tzid is not None:
+ hasTZ(tzid)
+ self._timezone = tzid
+ else:
+ self._timezone = None
+ return self._setTimezoneValue()
+
+
+ @inlineCallbacks
+ def _setTimezoneValue(self):
+ """
+ Store the current L{self._timezone} value in the database.
+ """
</ins><span class="cx"> cal = self._bindSchema
</span><span class="cx"> yield Update(
</span><span class="cx"> {
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoretesttest_sqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py (14668 => 14669)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py        2015-04-09 16:55:50 UTC (rev 14668)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py        2015-04-09 16:57:35 UTC (rev 14669)
</span><span class="lines">@@ -30,7 +30,7 @@
</span><span class="cx"> from twisted.python.filepath import FilePath
</span><span class="cx"> from twisted.internet import reactor
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue, DeferredList, \
</span><del>- succeed
</del><ins>+ succeed, maybeDeferred
</ins><span class="cx"> from twisted.internet.task import deferLater, Clock
</span><span class="cx"> from twisted.trial import unittest
</span><span class="cx">
</span><span class="lines">@@ -40,7 +40,7 @@
</span><span class="cx"> from twistedcaldav.dateops import datetimeMktime
</span><span class="cx"> from twistedcaldav.ical import Component, normalize_iCalStr, diff_iCalStrs
</span><span class="cx"> from twistedcaldav.instance import InvalidOverriddenInstanceError
</span><del>-from twistedcaldav.timezones import TimezoneCache
</del><ins>+from twistedcaldav.timezones import TimezoneCache, readVTZ, TimezoneException
</ins><span class="cx">
</span><span class="cx"> from txdav.base.propertystore.base import PropertyName
</span><span class="cx"> from txdav.caldav.datastore.query.filter import Filter
</span><span class="lines">@@ -1844,41 +1844,74 @@
</span><span class="cx"> Make sure a L{CalendarHomeChild}.setTimezone() works.
</span><span class="cx"> """
</span><span class="cx">
</span><del>- tz1 = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//calendarserver.org//Zonal//EN
-BEGIN:VTIMEZONE
-TZID:Etc/GMT+1
-X-LIC-LOCATION:Etc/GMT+1
-BEGIN:STANDARD
-DTSTART:18000101T000000
-RDATE:18000101T000000
-TZNAME:GMT+1
-TZOFFSETFROM:-0100
-TZOFFSETTO:-0100
-END:STANDARD
-END:VTIMEZONE
-END:VCALENDAR
-""")
</del><ins>+ TimezoneCache.create()
+ self.addCleanup(TimezoneCache.clear)
</ins><span class="cx">
</span><ins>+ tzid1 = "Etc/GMT+1"
+ tz1 = Component(None, pycalendar=readVTZ(tzid1))
+
</ins><span class="cx"> cal = yield self.calendarUnderTest()
</span><span class="cx"> self.assertEqual(cal.getTimezone(), None)
</span><ins>+ self.assertEqual(cal.getTimezoneID(), None)
</ins><span class="cx"> yield cal.setTimezone(tz1)
</span><span class="cx"> self.assertEqual(cal.getTimezone(), tz1)
</span><ins>+ self.assertEqual(cal.getTimezoneID(), tzid1)
</ins><span class="cx"> yield self.commit()
</span><span class="cx">
</span><span class="cx"> cal = yield self.calendarUnderTest()
</span><span class="cx"> self.assertEqual(cal.getTimezone(), tz1)
</span><ins>+ self.assertEqual(cal.getTimezoneID(), tzid1)
</ins><span class="cx"> yield cal.setTimezone(None)
</span><span class="cx"> yield self.commit()
</span><span class="cx">
</span><span class="cx"> cal = yield self.calendarUnderTest()
</span><span class="cx"> self.assertEqual(cal.getTimezone(), None)
</span><ins>+ self.assertEqual(cal.getTimezoneID(), None)
</ins><span class="cx"> yield self.commit()
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><ins>+ def test_setTimezoneID(self):
+ """
+ Make sure a L{CalendarHomeChild}.setTimezoneID() works.
+ """
+
+ TimezoneCache.create()
+ self.addCleanup(TimezoneCache.clear)
+
+ tzid1 = "Etc/GMT+1"
+ tz1 = Component(None, pycalendar=readVTZ(tzid1))
+
+ cal = yield self.calendarUnderTest()
+ self.assertEqual(cal.getTimezone(), None)
+ self.assertEqual(cal.getTimezoneID(), None)
+ yield cal.setTimezoneID(tzid1)
+ self.assertEqual(cal.getTimezone(), tz1)
+ self.assertEqual(cal.getTimezoneID(), tzid1)
+ yield self.commit()
+
+ cal = yield self.calendarUnderTest()
+ self.assertEqual(cal.getTimezone(), tz1)
+ self.assertEqual(cal.getTimezoneID(), tzid1)
+ yield cal.setTimezoneID(None)
+ yield self.commit()
+
+ cal = yield self.calendarUnderTest()
+ self.assertEqual(cal.getTimezone(), None)
+ self.assertEqual(cal.getTimezoneID(), None)
+ yield self.commit()
+
+ # Invalid TZID
+ cal = yield self.calendarUnderTest()
+ self.assertEqual(cal.getTimezone(), None)
+ self.assertEqual(cal.getTimezoneID(), None)
+ yield self.failUnlessFailure(maybeDeferred(cal.setTimezoneID, "bogus"), TimezoneException)
+ self.assertEqual(cal.getTimezone(), None)
+ self.assertEqual(cal.getTimezoneID(), None)
+ yield self.commit()
+
+
+ @inlineCallbacks
</ins><span class="cx"> def test_calendarRevisionChangeConcurrency(self):
</span><span class="cx"> """
</span><span class="cx"> Test that two concurrent attempts to add resources in two separate
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoreupgradesqlupgradestesttest_upgrade_from_4_to_5py"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/test/test_upgrade_from_4_to_5.py (14668 => 14669)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/test/test_upgrade_from_4_to_5.py        2015-04-09 16:55:50 UTC (rev 14668)
+++ CalendarServer/trunk/txdav/common/datastore/upgrade/sql/upgrades/test/test_upgrade_from_4_to_5.py        2015-04-09 16:57:35 UTC (rev 14669)
</span><span class="lines">@@ -13,6 +13,7 @@
</span><span class="cx"> # See the License for the specific language governing permissions and
</span><span class="cx"> # limitations under the License.
</span><span class="cx"> ##
</span><ins>+from twistedcaldav.timezones import readVTZ, TimezoneCache
</ins><span class="cx">
</span><span class="cx"> """
</span><span class="cx"> Tests for L{txdav.common.datastore.upgrade.sql.upgrade}.
</span><span class="lines">@@ -40,58 +41,13 @@
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def _calendarTimezoneUpgrade_setup(self):
</span><span class="cx">
</span><del>- tz1 = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//calendarserver.org//Zonal//EN
-BEGIN:VTIMEZONE
-TZID:Etc/GMT+1
-X-LIC-LOCATION:Etc/GMT+1
-BEGIN:STANDARD
-DTSTART:18000101T000000
-RDATE:18000101T000000
-TZNAME:GMT+1
-TZOFFSETFROM:-0100
-TZOFFSETTO:-0100
-END:STANDARD
-END:VTIMEZONE
-END:VCALENDAR
-""")
- tz2 = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//calendarserver.org//Zonal//EN
-BEGIN:VTIMEZONE
-TZID:Etc/GMT+2
-X-LIC-LOCATION:Etc/GMT+2
-BEGIN:STANDARD
-DTSTART:18000101T000000
-RDATE:18000101T000000
-TZNAME:GMT+2
-TZOFFSETFROM:-0200
-TZOFFSETTO:-0200
-END:STANDARD
-END:VTIMEZONE
-END:VCALENDAR
-""")
- tz3 = Component.fromString("""BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//calendarserver.org//Zonal//EN
-BEGIN:VTIMEZONE
-TZID:Etc/GMT+3
-X-LIC-LOCATION:Etc/GMT+3
-BEGIN:STANDARD
-DTSTART:18000101T000000
-RDATE:18000101T000000
-TZNAME:GMT+3
-TZOFFSETFROM:-0300
-TZOFFSETTO:-0300
-END:STANDARD
-END:VTIMEZONE
-END:VCALENDAR
-""")
</del><ins>+ TimezoneCache.create()
+ self.addCleanup(TimezoneCache.clear)
</ins><span class="cx">
</span><ins>+ tz1 = Component(None, pycalendar=readVTZ("Etc/GMT+1"))
+ tz2 = Component(None, pycalendar=readVTZ("Etc/GMT+2"))
+ tz3 = Component(None, pycalendar=readVTZ("Etc/GMT+3"))
+
</ins><span class="cx"> # Share user01 calendar with user03
</span><span class="cx"> calendar = (yield self.calendarUnderTest(name="calendar_1", home="user01"))
</span><span class="cx"> home3 = yield self.homeUnderTest(name="user03")
</span></span></pre>
</div>
</div>
</body>
</html>