<!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">         &quot;&quot;&quot;
</span><span class="cx">         Returns the calendar data derived from this element.
</span><span class="cx">         &quot;&quot;&quot;
</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):
+    &quot;&quot;&quot;
+    Specifies a time zone id on a calendar collection.
+    (draft-ietf-tzdist-caldav-timezone-ref-01, Section-5.2)
+    &quot;&quot;&quot;
+    name = &quot;calendar-timezone-id&quot;
+    hidden = True
+
+
+
+@registerElement
</ins><span class="cx"> class SupportedCalendarComponentSets (CalDAVElement):
</span><span class="cx">     &quot;&quot;&quot;
</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, &quot;propname&quot;): (0, None),
</span><span class="cx">         (dav_namespace, &quot;prop&quot;): (0, None),
</span><span class="cx">         (caldav_namespace, &quot;timezone&quot;): (0, 1),
</span><ins>+        (caldav_namespace, &quot;timezone-id&quot;): (0, 1),
</ins><span class="cx">         (caldav_namespace, &quot;filter&quot;): (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, &quot;timezone&quot;):
</span><ins>+                if timezone_id is not None:
+                    raise ValueError(&quot;Only one of CalDAV:timezone and CalDAV:timezone-id allowed&quot;)
</ins><span class="cx">                 timezone = child
</span><span class="cx"> 
</span><ins>+            elif qname == (caldav_namespace, &quot;timezone-id&quot;):
+                if timezone is not None:
+                    raise ValueError(&quot;Only one of CalDAV:timezone and CalDAV:timezone-id allowed&quot;)
+                timezone_id = child
+
</ins><span class="cx">             else:
</span><span class="cx">                 raise AssertionError(&quot;We shouldn't be here&quot;)
</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):
+    &quot;&quot;&quot;
+    Specifies a time zone id.
+    (draft-ietf-tzdist-caldav-timezone-ref-01, Section-5.2)
+    &quot;&quot;&quot;
+    name = &quot;timezone-id&quot;
+
+
+
+@registerElement
</ins><span class="cx"> class TimeRange (CalDAVTimeRangeElement):
</span><span class="cx">     &quot;&quot;&quot;
</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"> &quot;&quot;&quot;
</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 = &quot;CalDAV:timezone must contain one VTIMEZONE component only: %s&quot; % (query_tz,)
-        log.error(msg)
-        raise HTTPError(ErrorResponse(
-            responsecode.FORBIDDEN,
-            (caldav_namespace, &quot;valid-calendar-data&quot;),
-            &quot;Invalid calendar-data&quot;,
-        ))
-    if query_tz:
</del><ins>+    if calendar_query.timezone:
+        query_tz = calendar_query.timezone
+        if not query_tz.valid():
+            msg = &quot;CalDAV:timezone must contain one VTIMEZONE component only: %s&quot; % (query_tz,)
+            log.error(msg)
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (caldav_namespace, &quot;valid-calendar-data&quot;),
+                &quot;Invalid calendar-data&quot;,
+            ))
</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, &quot;valid-timezone&quot;),
+                &quot;Invalid timezone-id&quot;,
+            ))
+        filter.settimezone(query_tz)
+        query_timezone = tuple(query_tz.subcomponents())[0]
</ins><span class="cx"> 
</span><span class="cx">     if props.qname() == (&quot;DAV:&quot;, &quot;allprop&quot;):
</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">                         &quot;Component %s is not supported by this server&quot; % (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"> &quot;&quot;&quot;
</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, &quot;valid-timezone&quot;),
+                    description=&quot;Invalid property&quot;
+                ))
+            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):
+        &quot;&quot;&quot;
+        Partial retrieval of events by time range.
+        (CalDAV-access-09, section 7.6.1)
+        &quot;&quot;&quot;
+        TimezoneCache.create()
+        self.addCleanup(TimezoneCache.clear)
+
+        tzid1 = &quot;Etc/GMT+1&quot;
+        tz1 = Component(None, pycalendar=readVTZ(tzid1))
+
+        calendar_properties = (
+            davxml.GETETag(),
+            caldavxml.CalendarData(),
+        )
+
+        query_timerange = caldavxml.TimeRange(
+            start=&quot;%04d1001T000000Z&quot; % (DateTime.getToday().getYear(),),
+            end=&quot;%04d1101T000000Z&quot; % (DateTime.getToday().getYear(),),
+        )
+
+        query = caldavxml.CalendarQuery(
+            davxml.PropertyContainer(*calendar_properties),
+            caldavxml.Filter(
+                caldavxml.ComponentFilter(
+                    caldavxml.ComponentFilter(
+                        query_timerange,
+                        name=&quot;VEVENT&quot;,
+                    ),
+                    name=&quot;VCALENDAR&quot;,
+                ),
+            ),
+            caldavxml.TimeZone.fromCalendar(tz1),
+        )
+
+        def got_xml(doc):
+            if not isinstance(doc.root_element, davxml.MultiStatus):
+                self.fail(&quot;REPORT response XML root element is not multistatus: %r&quot; % (doc.root_element,))
+
+        return self.calendar_query(query, got_xml)
+
+
+    def test_calendar_query_timezone_id(self):
+        &quot;&quot;&quot;
+        Partial retrieval of events by time range.
+        (CalDAV-access-09, section 7.6.1)
+        &quot;&quot;&quot;
+        TimezoneCache.create()
+        self.addCleanup(TimezoneCache.clear)
+
+        tzid1 = &quot;Etc/GMT+1&quot;
+
+        calendar_properties = (
+            davxml.GETETag(),
+            caldavxml.CalendarData(),
+        )
+
+        query_timerange = caldavxml.TimeRange(
+            start=&quot;%04d1001T000000Z&quot; % (DateTime.getToday().getYear(),),
+            end=&quot;%04d1101T000000Z&quot; % (DateTime.getToday().getYear(),),
+        )
+
+        query = caldavxml.CalendarQuery(
+            davxml.PropertyContainer(*calendar_properties),
+            caldavxml.Filter(
+                caldavxml.ComponentFilter(
+                    caldavxml.ComponentFilter(
+                        query_timerange,
+                        name=&quot;VEVENT&quot;,
+                    ),
+                    name=&quot;VCALENDAR&quot;,
+                ),
+            ),
+            caldavxml.TimeZoneID.fromString(tzid1),
+        )
+
+        def got_xml(doc):
+            if not isinstance(doc.root_element, davxml.MultiStatus):
+                self.fail(&quot;REPORT response XML root element is not multistatus: %r&quot; % (doc.root_element,))
+
+        return self.calendar_query(query, got_xml)
+
+
+    @inlineCallbacks
+    def test_calendar_query_bogus_timezone_id(self):
+        &quot;&quot;&quot;
+        Partial retrieval of events by time range.
+        (CalDAV-access-09, section 7.6.1)
+        &quot;&quot;&quot;
+        TimezoneCache.create()
+        self.addCleanup(TimezoneCache.clear)
+
+        calendar_properties = (
+            davxml.GETETag(),
+            caldavxml.CalendarData(),
+        )
+
+        query_timerange = caldavxml.TimeRange(
+            start=&quot;%04d1001T000000Z&quot; % (DateTime.getToday().getYear(),),
+            end=&quot;%04d1101T000000Z&quot; % (DateTime.getToday().getYear(),),
+        )
+
+        query = caldavxml.CalendarQuery(
+            davxml.PropertyContainer(*calendar_properties),
+            caldavxml.Filter(
+                caldavxml.ComponentFilter(
+                    caldavxml.ComponentFilter(
+                        query_timerange,
+                        name=&quot;VEVENT&quot;,
+                    ),
+                    name=&quot;VCALENDAR&quot;,
+                ),
+            ),
+            caldavxml.TimeZoneID.fromString(&quot;bogus&quot;),
+        )
+
+        result = yield self.calendar_query(query, got_xml=None, expected_code=responsecode.FORBIDDEN)
+        self.assertTrue(&quot;valid-timezone&quot; in result)
+
+
+    @inlineCallbacks
+    def test_calendar_query_wrong_timezone_elements(self):
+        &quot;&quot;&quot;
+        Partial retrieval of events by time range.
+        (CalDAV-access-09, section 7.6.1)
+        &quot;&quot;&quot;
+        TimezoneCache.create()
+        self.addCleanup(TimezoneCache.clear)
+
+        tzid1 = &quot;Etc/GMT+1&quot;
+        tz1 = Component(None, pycalendar=readVTZ(tzid1))
+
+        calendar_properties = (
+            davxml.GETETag(),
+            caldavxml.CalendarData(),
+        )
+
+        query_timerange = caldavxml.TimeRange(
+            start=&quot;%04d1001T000000Z&quot; % (DateTime.getToday().getYear(),),
+            end=&quot;%04d1101T000000Z&quot; % (DateTime.getToday().getYear(),),
+        )
+
+        query = caldavxml.CalendarQuery(
+            davxml.PropertyContainer(*calendar_properties),
+            caldavxml.Filter(
+                caldavxml.ComponentFilter(
+                    caldavxml.ComponentFilter(
+                        query_timerange,
+                        name=&quot;VEVENT&quot;,
+                    ),
+                    name=&quot;VCALENDAR&quot;,
+                ),
+            ),
+            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(&quot;Only one of&quot; in result)
+
+
</ins><span class="cx">     def test_calendar_query_partial_recurring(self):
</span><span class="cx">         &quot;&quot;&quot;
</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(&quot;wsanchez&quot;)
</span><span class="cx">         request = SimpleStoreRequest(self, &quot;REPORT&quot;, &quot;/calendars/users/wsanchez/calendar/&quot;, 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(&quot;REPORT failed: %s&quot; % (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">         &quot;&quot;&quot;
</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">         &quot;&quot;&quot;
</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(&quot;BEGIN:VCALENDAR&quot;):
+                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):
+        &quot;&quot;&quot;
+        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}
+        &quot;&quot;&quot;
+
+        if self._timezone:
+            if self._timezone.startswith(&quot;BEGIN:VCALENDAR&quot;):
+                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">         &quot;&quot;&quot;
</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">         &quot;&quot;&quot;
</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):
+        &quot;&quot;&quot;
+        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
+        &quot;&quot;&quot;
+
+        if tzid is not None:
+            hasTZ(tzid)
+            self._timezone = tzid
+        else:
+            self._timezone = None
+        return self._setTimezoneValue()
+
+
+    @inlineCallbacks
+    def _setTimezoneValue(self):
+        &quot;&quot;&quot;
+        Store the current L{self._timezone} value in the database.
+        &quot;&quot;&quot;
</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">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        tz1 = Component.fromString(&quot;&quot;&quot;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
-&quot;&quot;&quot;)
</del><ins>+        TimezoneCache.create()
+        self.addCleanup(TimezoneCache.clear)
</ins><span class="cx"> 
</span><ins>+        tzid1 = &quot;Etc/GMT+1&quot;
+        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):
+        &quot;&quot;&quot;
+        Make sure a L{CalendarHomeChild}.setTimezoneID() works.
+        &quot;&quot;&quot;
+
+        TimezoneCache.create()
+        self.addCleanup(TimezoneCache.clear)
+
+        tzid1 = &quot;Etc/GMT+1&quot;
+        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, &quot;bogus&quot;), 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">         &quot;&quot;&quot;
</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"> &quot;&quot;&quot;
</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(&quot;&quot;&quot;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
-&quot;&quot;&quot;)
-        tz2 = Component.fromString(&quot;&quot;&quot;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
-&quot;&quot;&quot;)
-        tz3 = Component.fromString(&quot;&quot;&quot;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
-&quot;&quot;&quot;)
</del><ins>+        TimezoneCache.create()
+        self.addCleanup(TimezoneCache.clear)
</ins><span class="cx"> 
</span><ins>+        tz1 = Component(None, pycalendar=readVTZ(&quot;Etc/GMT+1&quot;))
+        tz2 = Component(None, pycalendar=readVTZ(&quot;Etc/GMT+2&quot;))
+        tz3 = Component(None, pycalendar=readVTZ(&quot;Etc/GMT+3&quot;))
+
</ins><span class="cx">         # Share user01 calendar with user03
</span><span class="cx">         calendar = (yield self.calendarUnderTest(name=&quot;calendar_1&quot;, home=&quot;user01&quot;))
</span><span class="cx">         home3 = yield self.homeUnderTest(name=&quot;user03&quot;)
</span></span></pre>
</div>
</div>

</body>
</html>