<!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>[12281] 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/12281">12281</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2014-01-10 07:34:01 -0800 (Fri, 10 Jan 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>Client initiated smart recurrence spitting.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkcalendarservertoolstesttest_calverifypy">CalendarServer/trunk/calendarserver/tools/test/test_calverify.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavcustomxmlpy">CalendarServer/trunk/twistedcaldav/customxml.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavicalpy">CalendarServer/trunk/twistedcaldav/ical.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavmethodpostpy">CalendarServer/trunk/twistedcaldav/method/post.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavstdconfigpy">CalendarServer/trunk/twistedcaldav/stdconfig.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavstorebridgepy">CalendarServer/trunk/twistedcaldav/storebridge.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingicalsplitterpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_icalsplitterpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.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="#CalendarServertrunktxdavcaldavicalendarstorepy">CalendarServer/trunk/txdav/caldav/icalendarstore.py</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#CalendarServertrunkdocExtensionscaldavrecursplittxt">CalendarServer/trunk/doc/Extensions/caldav-recursplit.txt</a></li>
<li><a href="#CalendarServertrunkdocExtensionscaldavrecursplitxml">CalendarServer/trunk/doc/Extensions/caldav-recursplit.xml</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcalendarservertoolstesttest_calverifypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/test/test_calverify.py (12280 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/test/test_calverify.py        2014-01-10 15:30:43 UTC (rev 12280)
+++ CalendarServer/trunk/calendarserver/tools/test/test_calverify.py        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -3066,7 +3066,8 @@
</span><span class="cx">         }
</span><span class="cx">         output = StringIO()
</span><span class="cx">         calverify = EventSplitService(self._sqlCalendarStore, options, output, reactor, config)
</span><del>-        oldUID = yield calverify.doAction()
</del><ins>+        oldObj = yield calverify.doAction()
+        oldUID = oldObj.uid()
</ins><span class="cx"> 
</span><span class="cx">         relsubs = dict(self.subs)
</span><span class="cx">         relsubs[&quot;relID&quot;] = oldUID
</span></span></pre></div>
<a id="CalendarServertrunkdocExtensionscaldavrecursplittxt"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/doc/Extensions/caldav-recursplit.txt (0 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/doc/Extensions/caldav-recursplit.txt                                (rev 0)
+++ CalendarServer/trunk/doc/Extensions/caldav-recursplit.txt        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -0,0 +1,504 @@
</span><ins>+
+
+
+Calendar Server Extension                                       C. Daboo
+                                                                   Apple
+                                                        January 10, 2014
+
+
+             Smart Splitting of Recurring Events in CalDAV
+
+Abstract
+
+   This document defines an extension to CalDAV that allows calendar
+   clients to split recurring events on the server in such a way as to
+   preserve the original per-attendee data, such as alarms and
+   participation status.
+
+
+Table of Contents
+
+   1.  Introduction  . . . . . . . . . . . . . . . . . . . . . . . . . 2
+   2.  Conventions Used in This Document . . . . . . . . . . . . . . . 3
+   3.  New behavior  . . . . . . . . . . . . . . . . . . . . . . . . . 3
+     3.1.  Example . . . . . . . . . . . . . . . . . . . . . . . . . . 6
+   4.  Security Considerations . . . . . . . . . . . . . . . . . . . . 8
+   5.  IANA Considerations . . . . . . . . . . . . . . . . . . . . . . 8
+   6.  Normative References  . . . . . . . . . . . . . . . . . . . . . 8
+   Appendix A.  Acknowledgments  . . . . . . . . . . . . . . . . . . . 9
+   Author's Address  . . . . . . . . . . . . . . . . . . . . . . . . . 9
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Daboo                                                           [Page 1]
+
+                       CalDAV Recurrence Splitting          January 2014
+
+
+1.  Introduction
+
+   Internet calendaring and scheduling standards are defined by
+   iCalendar [RFC5545] and iTIP [RFC5546].  The CalDAV Access [RFC4791]
+   standard defines a way to access calendar data stored on a server,
+   and the CalDAV Scheduling [RFC6638] draft defines how scheduling
+   occurs between users of a CalDAV server.
+
+   When dealing with recurring events defined in iCalendar, it is often
+   necessary for the organizer of the events to apply a change from one
+   of the recurrence instances on into the future (e.g., add a new
+   attendee part way through the series of meetings). iCalendar supports
+   a &quot;RANGE&quot; property parameter on the &quot;RECURRENCE-ID&quot; property that
+   allows for a &quot;THIS-AND-FUTURE&quot; change to an existing set of recurring
+   events.  However, the &quot;RANGE&quot; behavior is not always implemented.
+   Instead, clients split the recurring series into two separate events
+   (different UIDs and thus different calendar object resources stored
+   in a CalDAV calendar).  One series contains all the instances up to
+   the point where the change is made, and the other contains all the
+   instances from that point onwards.  Typically this is done by
+   truncating the recurrence rule in the existing resource and creating
+   a new resource for the ongoing recurrence.  However, when done that
+   way, it results in any per-attendee data in the original resource for
+   recurrence instances after the &quot;split point&quot; being lost - i.e.,
+   attendee loose all their status for the ongoing instances.  What is
+   more, due to the requirements of CalDAV scheduling, the &quot;PARTSTAT&quot;
+   parameter on the &quot;ATTENDEE&quot; properties in the new calendar object
+   resource will be forcibly reset to &quot;NEEDS-ACTION&quot;, requiring
+   attendees to re-accept/decline the ongoing recurrence series in the
+   new calendar object resource.  This is obviously a poor user
+   experience that needs to be corrected.
+
+   This specification defines a new CalDAV POST request action that
+   causes the server to split an existing calendar object resource
+   containing a recurring event, into two pieces at a specified &quot;split
+   point&quot;, such that per-attendee data and attendee participation status
+   is preserved in the new calendar object resource created by the
+   split.  Once that is done, clients can then modify the new resource
+   to make the actual &quot;this-and-future&quot; change that triggers the split.
+
+   This specification does not cover the case of splitting of scheduled
+   calendar data for attendees not hosted on the CalDAV server.
+   Instead, existing existing procedures for sending iTIP messages have
+   to be used to update the attendee data to account for the split,
+   likely resulting in loss of per-attendee data.  A future extension to
+   iTIP might be possible to address that, but is not covered here.
+
+
+
+
+
+Daboo                                                           [Page 2]
+
+                       CalDAV Recurrence Splitting          January 2014
+
+
+2.  Conventions Used in This Document
+
+   The key words &quot;MUST&quot;, &quot;MUST NOT&quot;, &quot;REQUIRED&quot;, &quot;SHALL&quot;, &quot;SHALL NOT&quot;,
+   &quot;SHOULD&quot;, &quot;SHOULD NOT&quot;, &quot;RECOMMENDED&quot;, &quot;MAY&quot;, and &quot;OPTIONAL&quot; in this
+   document are to be interpreted as described in [RFC2119].
+
+   When XML element types in the namespaces &quot;DAV:&quot; and
+   &quot;urn:ietf:params:xml:ns:caldav&quot; are referenced in this document
+   outside of the context of an XML fragment, the string &quot;DAV:&quot; and
+   &quot;CALDAV:&quot; will be prefixed to the element type names respectively.
+
+   The namespace &quot;http://calendarserver.org/ns/&quot; is used for XML
+   elements defined in this specification.  When XML element types in
+   this namespace are referenced in this document outside of the context
+   of an XML fragment, the string &quot;CS:&quot; will be prefixed to the element
+   type names respectively.
+
+   The prefix X-CALENDARSERVER- is used for all iCalendar properties,
+   parameters and enumerated values defined in this extension.
+
+
+3.  New behavior
+
+   A server supporting the features described in this specification MUST
+   include &quot;calendarserver-recurrence-split&quot; as a field in the DAV
+   response header from an OPTIONS request on a calendar home
+   collection.
+
+   To split an existing calendar object resource containing a recurring
+   event, a client issues an HTTP POST resource with the request-uri set
+   to the URI of the calendar object resource.  The client also includes
+   the following two URI query parameters:
+
+   1.  &quot;action&quot; set to the value &quot;split&quot;
+
+   2.  &quot;rid&quot; set to an iCalendar format DATE-TIME value in UTC
+       (&quot;YYYYMMDDTHHMMSSZ&quot; style format).
+
+   The &quot;action&quot; parameter is used to distinguish this operation from
+   others that might be defined in the future for calendar object
+   resources.  The &quot;rid&quot; parameter specified the UTC date-time where the
+   split is to occur.  The actual split occurs at the next recurrence
+   instance on or after the &quot;rid&quot; parameter value - the &quot;split point&quot;.
+
+   Client MUST include both parameters in the POST request and MUST
+   ensure a valid date-time value is used.  The date-time value MUST NOT
+   be earlier than the start time of the first instance of the
+   recurrence set, and it MUST NOT be later than the start time of the
+
+
+
+Daboo                                                           [Page 3]
+
+                       CalDAV Recurrence Splitting          January 2014
+
+
+   last instance of the recurrence set.  If the &quot;rid&quot; parameter value is
+   not of the correct format or missing, the server MUST return a DAV:
+   error response with the CALDAV:valid-rid-parameter pre-condition
+   code.  If the &quot;rid&quot; parameter is valid, but outside of the allowed
+   range, or the targeted calendar object resource is not recurring,
+   then the server MUST return a DAV:error response with the CS:invalid-
+   split pre-condition code.  The server MUST reject any attempt by an
+   attendee to split their copy of a scheduled calendar object resource
+   - only organizers are allowed to split events.
+
+   Clients MAY include an HTTP &quot;Prefer&quot; request header including the
+   value &quot;return=representation&quot; (see [I-D.snell-http-prefer]).  That
+   instructs the server to return a WebDAV multistatus response
+   containing two responses: one for the targeted resource and one for
+   the new resource created as a result of the split.  The multistatus
+   response MUST include the DAV:getetag and CALDAV:calendar-data
+   properties for each resource.  In the absence of the &quot;Prefer:
+   return=representation&quot; request header, the server MUST return an HTTP
+   &quot;Split-Component-URL&quot; response header whose value is the URI of the
+   new resource created as a result of the split.
+
+   When a server receives a valid split request as described above, it
+   does the equivalent of the following:
+
+   1.  The existing resource is duplicated, preserving any per-user data
+       such as alarms or transparency status.
+
+   2.  The calendar data in the existing resource is modified as
+       follows:
+
+       A.  Any overridden components with a &quot;RECURRENCE-ID&quot; property
+           value prior to the split point are removed.
+
+       B.  Any &quot;RDATE&quot; or &quot;EXDATE&quot; property values prior to the split
+           point are removed.
+
+       C.  Any &quot;RRULE&quot; property that only generates instances prior to
+           the split point is removed.
+
+       D.  Any remaining &quot;RRULE&quot; property has its &quot;COUNT&quot; value adjusted
+           by subtracting the number of instances prior to the split
+           point.
+
+       E.  The &quot;DTSTART&quot; property of the master instance is adjusted to
+           the value of the first instance of the &quot;RRULE&quot; on or after
+           the split point, or, in the absence of an &quot;RRULE&quot;, to the
+           first &quot;RDATE&quot; property value on or after the split point.
+
+
+
+
+Daboo                                                           [Page 4]
+
+                       CalDAV Recurrence Splitting          January 2014
+
+
+   3.  The calendar data in the new resource is modified as follows:
+
+       A.  Any overridden components with a &quot;RECURRENCE-ID&quot; property
+           value on or after the split point are removed.
+
+       B.  Any &quot;RDATE&quot; or &quot;EXDATE&quot; property values on or after the split
+           point are removed.
+
+       C.  Any &quot;RRULE&quot; property that only generates instances on or
+           after the split point is removed.
+
+       D.  Any remaining &quot;RRULE&quot; property has an &quot;UNTIL&quot; value applied,
+           with the until value being one second less than the split
+           point.
+
+       E.  The &quot;UID&quot; property of all components is changed to (the same)
+           new value.
+
+       F.  Attendee participation status MUST NOT be changed.
+
+   4.  The server MUST add a &quot;RELATED&quot; property, with a &quot;RELTYPE&quot;
+       parameter set to &quot;X-CALENDARSERVER-RECURRENCE-SET&quot;, and with its
+       value set to the &quot;UID&quot; property value of the new resource to all
+       components of the existing and new resources, if the existing
+       resource does not contain a &quot;RELATED&quot; property with a &quot;RELTYPE&quot;
+       parameter value set to &quot;X-CALENDARSERVER-RECURRENCE-SET&quot;.
+
+   When an organizer splits a scheduled event, the server performs the
+   following actions:
+
+   1.  For each attendee hosted on the same server, the server MUST
+       split the attendee's copy of the event in accordance with the
+       split point used by the organizer.  In doing so, it MUST preserve
+       any per-attendee state (alarms, transparency, participation
+       status) in the new resource (which also MUST have the same &quot;UID&quot;
+       property value as the organizer's new resource).
+
+   2.  For each attendee not hosted on the same server, the server will
+       need to send a series of iTIP messages to correctly update the
+       attendee with both the modified (old) resource data and the new
+       resource.  This will likely result in loss of per-attendee data
+       such as alarms (though the participation status might be
+       preserved if the calendar user agent processing the new iTIP
+       message for the new resource allows it).
+
+   Note that, since attendees can be invited to specific instances of a
+   recurring meeting (not necessarily the entire set), it is possible
+   that either the old or new calendar data no longer contains any valid
+
+
+
+Daboo                                                           [Page 5]
+
+                       CalDAV Recurrence Splitting          January 2014
+
+
+   components since the attendee was not invited to the corresponding
+   portions of the original split recurrence.  In such cases, the server
+   MUST remove the original resource, or MUST NOT create the new
+   resource.
+
+3.1.  Example
+
+   Assume the following iCalendar data is stored in the resource with
+   URI &quot;/event.ics&quot;:
+   BEGIN:VCALENDAR
+   PRODID:-//Example Inc.//Example Calendar//EN
+   VERSION:2.0
+   BEGIN:VEVENT
+   DTSTAMP:20140110T135358Z
+   DTSTART:20140101T120000Z
+   DURATION:PT1H
+   SUMMARY:Example
+   UID:DF400028-1223-4D26-92CA-B0ED3CC161F3
+   RRULE:FREQ=DAILY;COUNT=20
+   END:VEVENT
+   END:VCALENDAR
+
+   The client issues a POST split request to split the recurring event
+   at the instance with date-time &quot;20140110T120000Z&quot;:
+
+   &gt;&gt; Request &lt;&lt;
+
+   POST /event.ics?action=split&amp;rid=20140110T120000Z HTTP/1.1
+   Host: cal.example.com
+   Content-Length: 0
+   Prefer: return=representation
+
+
+   &gt;&gt; Response &lt;&lt;
+
+   HTTP/1.1 207 Multi-Status
+   ETag: &quot;5bc9a2b55081eba0a9cd34f742aa1c11&quot;
+   Date: Fri, 10 Jan 2014 14:02:20 GMT
+   Content-Type: text/xml
+   Content-Length: xxxx
+
+   &lt;?xml version='1.0' encoding='UTF-8'?&gt;
+   &lt;multistatus xmlns='DAV:'&gt;
+     &lt;response&gt;
+       &lt;href&gt;/event.ics&lt;/href&gt;
+       &lt;propstat&gt;
+         &lt;prop&gt;
+           &lt;getetag&gt;&quot;5bc9a2b55081eba0a9cd34f742aa1c11&quot;&lt;/getetag&gt;
+
+
+
+Daboo                                                           [Page 6]
+
+                       CalDAV Recurrence Splitting          January 2014
+
+
+           &lt;calendar-data xmlns='urn:ietf:params:xml:ns:caldav'
+   &gt;BEGIN:VCALENDAR
+   VERSION:2.0
+   PRODID:-//Example Inc.//Example Calendar//EN
+   BEGIN:VEVENT
+   UID:DF400028-1223-4D26-92CA-B0ED3CC161F3
+   DTSTART:20140110T120000Z
+   DURATION:PT1H
+   DTSTAMP:20140110T135358Z
+   RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:E3B9D6D4-E19F-
+    47AA-9088-1A29A9A7030F
+   RRULE:FREQ=DAILY;COUNT=11
+   SUMMARY:Example
+   END:VEVENT
+   END:VCALENDAR
+   &lt;/calendar-data&gt;
+         &lt;/prop&gt;
+         &lt;status&gt;HTTP/1.1 200 OK&lt;/status&gt;
+       &lt;/propstat&gt;
+     &lt;/response&gt;
+     &lt;response&gt;
+       &lt;href&gt;/event-split.ics&lt;/href&gt;
+       &lt;propstat&gt;
+         &lt;prop&gt;
+           &lt;getetag&gt;&quot;8b352ad2e0ca26c99cb205373b3dc34c&quot;&lt;/getetag&gt;
+           &lt;calendar-data xmlns='urn:ietf:params:xml:ns:caldav'
+   &gt;BEGIN:VCALENDAR
+   VERSION:2.0
+   PRODID:-//Example Inc.//Example Calendar//EN
+   BEGIN:VEVENT
+   UID:E3B9D6D4-E19F-47AA-9088-1A29A9A7030F
+   DTSTART:20140110T120000Z
+   DURATION:PT1H
+   DTSTAMP:20140110T135358Z
+   RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:E3B9D6D4-E19F-
+    47AA-9088-1A29A9A7030F
+   RRULE:FREQ=DAILY;UNTIL=20140110T115959Z
+   SUMMARY:Example
+   END:VEVENT
+   END:VCALENDAR
+   &lt;/calendar-data&gt;
+         &lt;/prop&gt;
+         &lt;status&gt;HTTP/1.1 200 OK&lt;/status&gt;
+       &lt;/propstat&gt;
+     &lt;/response&gt;
+   &lt;/multistatus&gt;
+
+   The original resource is changed to have a start date-time value of
+
+
+
+Daboo                                                           [Page 7]
+
+                       CalDAV Recurrence Splitting          January 2014
+
+
+   &quot;20140110T120000Z&quot;, and the &quot;COUNT&quot; component of the &quot;RRULE&quot; property
+   is adjusted to the value &quot;11&quot; (which represents the number of
+   remaining instances for that event, 9 early instances having been
+   removed).  The new resource has the original start date-time value,
+   and its &quot;RRULE&quot; property has an &quot;UNTIL&quot; value one second prior to the
+   split point.  Both resources have a &quot;RELATED&quot; property with a
+   &quot;RELTYPE&quot; parameter set to &quot;X-CALENDARSERVER-RECURRENCE-SET&quot; and a
+   value set to the &quot;UID&quot; property value of the new resource.
+
+
+4.  Security Considerations
+
+   This specification does not introduce any more security
+   considerations beyond those already listed in iCalendar [RFC5545],
+   iTIP [RFC5546] and CalDAV Access [RFC4791] and CalDAV Scheduling
+   [RFC6638].
+
+
+5.  IANA Considerations
+
+   TBD: new HTTP request header registration.
+
+
+6.  Normative References
+
+   [I-D.snell-http-prefer]
+              Snell, J., &quot;Prefer Header for HTTP&quot;,
+              draft-snell-http-prefer-18 (work in progress),
+              January 2013.
+
+   [RFC2119]  Bradner, S., &quot;Key words for use in RFCs to Indicate
+              Requirement Levels&quot;, BCP 14, RFC 2119, March 1997.
+
+   [RFC4791]  Daboo, C., Desruisseaux, B., and L. Dusseault,
+              &quot;Calendaring Extensions to WebDAV (CalDAV)&quot;, RFC 4791,
+              March 2007.
+
+   [RFC5545]  Desruisseaux, B., &quot;Internet Calendaring and Scheduling
+              Core Object Specification (iCalendar)&quot;, RFC 5545,
+              September 2009.
+
+   [RFC5546]  Daboo, C., &quot;iCalendar Transport-Independent
+              Interoperability Protocol (iTIP)&quot;, RFC 5546,
+              December 2009.
+
+   [RFC6638]  Daboo, C. and B. Desruisseaux, &quot;Scheduling Extensions to
+              CalDAV&quot;, RFC 6638, June 2012.
+
+
+
+
+Daboo                                                           [Page 8]
+
+                       CalDAV Recurrence Splitting          January 2014
+
+
+Appendix A.  Acknowledgments
+
+   This specification is the result of discussions between the Apple
+   calendar server and client teams.
+
+
+Author's Address
+
+   Cyrus Daboo
+   Apple Inc.
+   1 Infinite Loop
+   Cupertino, CA  95014
+   USA
+
+   Email: cyrus@daboo.name
+   URI:   http://www.apple.com/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Daboo                                                           [Page 9]
+
</ins></span></pre></div>
<a id="CalendarServertrunkdocExtensionscaldavrecursplitxml"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/doc/Extensions/caldav-recursplit.xml (0 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/doc/Extensions/caldav-recursplit.xml                                (rev 0)
+++ CalendarServer/trunk/doc/Extensions/caldav-recursplit.xml        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -0,0 +1,267 @@
</span><ins>+&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+&lt;?xml-stylesheet type=&quot;text/xsl&quot; href=&quot;../rfc2629.xslt&quot;?&gt;
+&lt;!DOCTYPE rfc SYSTEM 'rfc2629.dtd' [
+&lt;!ENTITY rfc2119 PUBLIC '' 'bibxml/reference.RFC.2119.xml'&gt;
+&lt;!ENTITY rfc5545 PUBLIC '' 'bibxml/reference.RFC.5545.xml'&gt;
+&lt;!ENTITY rfc5546 PUBLIC '' 'bibxml/reference.RFC.5546.xml'&gt;
+&lt;!ENTITY rfc4791 PUBLIC '' 'bibxml/reference.RFC.4791.xml'&gt;
+&lt;!ENTITY rfc6638 PUBLIC '' 'bibxml/reference.RFC.6638.xml'&gt;
+&lt;!ENTITY idPreferHeader SYSTEM 'http://xml.resource.org/public/rfc/bibxml3/reference.I-D.snell-http-prefer.xml'&gt;
+]&gt; 
+&lt;?rfc toc=&quot;yes&quot;?&gt;
+&lt;?rfc tocdepth=&quot;4&quot;?&gt;
+&lt;?rfc strict=&quot;yes&quot;?&gt;
+&lt;?rfc comments=&quot;yes&quot;?&gt;
+&lt;?rfc inline=&quot;yes&quot;?&gt;
+&lt;?rfc symrefs=&quot;yes&quot;?&gt;
+&lt;?rfc sortrefs=&quot;yes&quot;?&gt;
+&lt;?rfc compact=&quot;yes&quot;?&gt;
+&lt;?rfc subcompact=&quot;no&quot;?&gt;
+&lt;?rfc private=&quot;Calendar Server Extension&quot;?&gt;
+&lt;rfc ipr=&quot;none&quot; docName='caldav-recursplit-00'&gt;
+    &lt;front&gt;
+        &lt;title abbrev=&quot;CalDAV Recurrence Splitting&quot;&gt;Smart Splitting of Recurring Events in CalDAV&lt;/title&gt; 
+        &lt;author initials=&quot;C.&quot; surname=&quot;Daboo&quot; fullname=&quot;Cyrus Daboo&quot;&gt;
+            &lt;organization abbrev=&quot;Apple&quot;&gt;
+                Apple Inc.
+            &lt;/organization&gt;
+            &lt;address&gt;
+                &lt;postal&gt;
+                    &lt;street&gt;1 Infinite Loop&lt;/street&gt;
+                    &lt;city&gt;Cupertino&lt;/city&gt;
+                    &lt;region&gt;CA&lt;/region&gt;
+                    &lt;code&gt;95014&lt;/code&gt; 
+                    &lt;country&gt;USA&lt;/country&gt;
+                &lt;/postal&gt;
+                &lt;email&gt;cyrus@daboo.name&lt;/email&gt;
+                &lt;uri&gt;http://www.apple.com/&lt;/uri&gt;
+            &lt;/address&gt;
+        &lt;/author&gt;
+        &lt;date /&gt;
+        &lt;abstract&gt;
+            &lt;t&gt;
+                This document defines an extension to CalDAV that allows calendar clients to split recurring events on the server in such a way as to preserve the original per-attendee data, such as alarms and participation status.
+            &lt;/t&gt;
+        &lt;/abstract&gt;
+    &lt;/front&gt;
+    &lt;middle&gt;
+        &lt;section title='Introduction'&gt;
+            &lt;t&gt;
+                Internet calendaring and scheduling standards are defined by &lt;xref target=&quot;RFC5545&quot;&gt;iCalendar&lt;/xref&gt; and &lt;xref target=&quot;RFC5546&quot;&gt;iTIP&lt;/xref&gt;. The &lt;xref target=&quot;RFC4791&quot;&gt;CalDAV Access&lt;/xref&gt; standard defines a way to access calendar data stored on a server, and the &lt;xref target=&quot;RFC6638&quot;&gt;CalDAV Scheduling&lt;/xref&gt; draft defines how scheduling occurs between users of a CalDAV server.
+            &lt;/t&gt;
+            &lt;t&gt;
+                When dealing with recurring events defined in iCalendar, it is often necessary for the organizer of the events to apply a change from one of the recurrence instances on into the future (e.g., add a new attendee part way through the series of meetings). iCalendar supports a &quot;RANGE&quot; property parameter on the &quot;RECURRENCE-ID&quot; property that allows for a &quot;THIS-AND-FUTURE&quot; change to an existing set of recurring events. However, the &quot;RANGE&quot; behavior is not always implemented. Instead, clients split the recurring series into two separate events (different UIDs and thus different calendar object resources stored in a CalDAV calendar). One series contains all the instances up to the point where the change is made, and the other contains all the instances from that point onwards. Typically this is done by truncating the recurrence rule in the existing resource and creating a new resource for the ongoing recurrence. However, when done that way, it results in any per-attendee data in the original resource for recurrence instances after the &quot;split point&quot; being lost - i.e., attendee loose all their status for the ongoing instances. What is more, due to the requirements of CalDAV scheduling, the &quot;PARTSTAT&quot; parameter on the &quot;ATTENDEE&quot; properties in the new calendar object resource will be forcibly reset to &quot;NEEDS-ACTION&quot;, requiring attendees to re-accept/decline the ongoing recurrence series in the new calendar object resource. This is obviously a poor user experience that needs to be corrected.
+            &lt;/t&gt;
+            &lt;t&gt;
+                This specification defines a new CalDAV POST request action that causes the server to split an existing calendar object resource containing a recurring event, into two pieces at a specified &quot;split point&quot;, such that per-attendee data and attendee participation status is preserved in the new calendar object resource created by the split. Once that is done, clients can then modify the new resource to make the actual &quot;this-and-future&quot; change that triggers the split.
+            &lt;/t&gt;
+            &lt;t&gt;
+              This specification does not cover the case of splitting of scheduled calendar data for attendees not hosted on the CalDAV server. Instead, existing existing procedures for sending iTIP messages have to be used to update the attendee data to account for the split, likely resulting in loss of per-attendee data. A future extension to iTIP might be possible to address that, but is not covered here.
+            &lt;/t&gt;
+        &lt;/section&gt;
+        &lt;section title='Conventions Used in This Document'&gt;
+            &lt;t&gt;
+                The key words &quot;MUST&quot;, &quot;MUST NOT&quot;, &quot;REQUIRED&quot;, &quot;SHALL&quot;, &quot;SHALL NOT&quot;, &quot;SHOULD&quot;, &quot;SHOULD NOT&quot;, &quot;RECOMMENDED&quot;, &quot;MAY&quot;, and &quot;OPTIONAL&quot; in this document are to be interpreted as described in &lt;xref target='RFC2119' /&gt;. 
+            &lt;/t&gt;
+            &lt;t&gt;
+                When XML element types in the namespaces &quot;DAV:&quot; and &quot;urn:ietf:params:xml:ns:caldav&quot; are referenced in this document outside of the context of an XML fragment, the string &quot;DAV:&quot; and &quot;CALDAV:&quot; will be prefixed to the element type names respectively. 
+            &lt;/t&gt;
+            &lt;t&gt;
+                The namespace &quot;http://calendarserver.org/ns/&quot; is used for XML elements defined in this specification.  When XML element types in this namespace are referenced in this document outside of the context of an XML fragment, the string &quot;CS:&quot; will be prefixed to the element type names respectively. 
+            &lt;/t&gt;
+            &lt;t&gt;
+                The prefix X-CALENDARSERVER- is used for all iCalendar properties, parameters and enumerated values defined in this extension. 
+            &lt;/t&gt;
+        &lt;/section&gt;
+        
+        &lt;section title=&quot;New behavior&quot;&gt;
+          &lt;t&gt;A server supporting the features described in this specification MUST include &quot;calendarserver-recurrence-split&quot; as a field in the DAV response header from an OPTIONS request on a calendar home collection.&lt;/t&gt;
+          &lt;t&gt;To split an existing calendar object resource containing a recurring event, a client issues an HTTP POST resource with the request-uri set to the URI of the calendar object resource. The client also includes the following two URI query parameters:
+            &lt;list style='numbers'&gt;
+              &lt;t&gt;&quot;action&quot; set to the value &quot;split&quot;&lt;/t&gt;
+              &lt;t&gt;&quot;rid&quot; set to an iCalendar format DATE-TIME value in UTC (&quot;YYYYMMDDTHHMMSSZ&quot; style format).&lt;/t&gt;
+            &lt;/list&gt;
+            The &quot;action&quot; parameter is used to distinguish this operation from others that might be defined in the future for calendar object resources. The &quot;rid&quot; parameter specified the UTC date-time where the split is to occur. The actual split occurs at the next recurrence instance on or after the &quot;rid&quot; parameter value - the &quot;split point&quot;.&lt;/t&gt;
+            &lt;t&gt;
+            Client MUST include both parameters in the POST request and MUST ensure a valid date-time value is used. The date-time value MUST NOT be earlier than the start time of the first instance of the recurrence set, and it MUST NOT be later than the start time of the last instance of the recurrence set. If the &quot;rid&quot; parameter value is not of the correct format or missing, the server MUST return a DAV:error response with the CALDAV:valid-rid-parameter pre-condition code. If the &quot;rid&quot; parameter is valid, but outside of the allowed range, or the targeted calendar object resource is not recurring, then the server MUST return a DAV:error response with the CS:invalid-split pre-condition code. The server MUST reject any attempt by an attendee to split their copy of a scheduled calendar object resource - only organizers are allowed to split events.
+          &lt;/t&gt;
+          &lt;t&gt;
+            Clients MAY include an HTTP &quot;Prefer&quot; request header including the value &quot;return=representation&quot; (see &lt;xref target='I-D.snell-http-prefer'/&gt;). That instructs the server to return a WebDAV multistatus response containing two responses: one for the targeted resource and one for the new resource created as a result of the split. The multistatus response MUST include the DAV:getetag and CALDAV:calendar-data properties for each resource. In the absence of the &quot;Prefer:return=representation&quot; request header, the server MUST return an HTTP &quot;Split-Component-URL&quot; response header whose value is the URI of the new resource created as a result of the split.
+          &lt;/t&gt;
+          &lt;t&gt;
+            When a server receives a valid split request as described above, it does the equivalent of the following:
+            &lt;list style='numbers'&gt;
+              &lt;t&gt;The existing resource is duplicated, preserving any per-user data such as alarms or transparency status.&lt;/t&gt;
+              &lt;t&gt;The calendar data in the existing resource is modified as follows:
+                &lt;list style='letters'&gt;
+                  &lt;t&gt;Any overridden components with a &quot;RECURRENCE-ID&quot; property value prior to the split point are removed.&lt;/t&gt;
+                  &lt;t&gt;Any &quot;RDATE&quot; or &quot;EXDATE&quot; property values prior to the split point are removed.&lt;/t&gt;
+                  &lt;t&gt;Any &quot;RRULE&quot; property that only generates instances prior to the split point is removed.&lt;/t&gt;
+                  &lt;t&gt;Any remaining &quot;RRULE&quot; property has its &quot;COUNT&quot; value adjusted by subtracting the number of instances prior to the split point.&lt;/t&gt;
+                  &lt;t&gt;The &quot;DTSTART&quot; property of the master instance is adjusted to the value of the first instance of the &quot;RRULE&quot; on or after the split point, or, in the absence of an &quot;RRULE&quot;, to the first &quot;RDATE&quot; property value on or after the split point.&lt;/t&gt;
+                &lt;/list&gt;
+              &lt;/t&gt;
+              &lt;t&gt;The calendar data in the new resource is modified as follows:
+                &lt;list style='letters'&gt;
+                  &lt;t&gt;Any overridden components with a &quot;RECURRENCE-ID&quot; property value on or after the split point are removed.&lt;/t&gt;
+                  &lt;t&gt;Any &quot;RDATE&quot; or &quot;EXDATE&quot; property values on or after the split point are removed.&lt;/t&gt;
+                  &lt;t&gt;Any &quot;RRULE&quot; property that only generates instances on or after the split point is removed.&lt;/t&gt;
+                  &lt;t&gt;Any remaining &quot;RRULE&quot; property has an &quot;UNTIL&quot; value applied, with the until value being one second less than the split point.&lt;/t&gt;
+                  &lt;t&gt;The &quot;UID&quot; property of all components is changed to (the same) new value.&lt;/t&gt;
+                  &lt;t&gt;Attendee participation status MUST NOT be changed.&lt;/t&gt;
+                &lt;/list&gt;
+              &lt;/t&gt;
+              &lt;t&gt;
+                The server MUST add a &quot;RELATED&quot; property, with a &quot;RELTYPE&quot; parameter set to &quot;X-CALENDARSERVER-RECURRENCE-SET&quot;, and with its value set to the &quot;UID&quot; property value of the new resource to all components of the existing and new resources, if the existing resource does not contain a &quot;RELATED&quot; property with a &quot;RELTYPE&quot; parameter value set to &quot;X-CALENDARSERVER-RECURRENCE-SET&quot;. 
+              &lt;/t&gt;
+            &lt;/list&gt;
+          &lt;/t&gt;
+          &lt;t&gt;
+              When an organizer splits a scheduled event, the server performs the following actions:
+            &lt;list style=&quot;numbers&quot;&gt;
+              &lt;t&gt;For each attendee hosted on the same server, the server MUST split the attendee's copy of the event in accordance with the split point used by the organizer. In doing so, it MUST preserve any per-attendee state (alarms, transparency, participation status) in the new resource (which also MUST have the same &quot;UID&quot; property value as the organizer's new resource).&lt;/t&gt;
+              &lt;t&gt;For each attendee not hosted on the same server, the server will need to send a series of iTIP messages to correctly update the attendee with both the modified (old) resource data and the new resource. This will likely result in loss of per-attendee data such as alarms (though the participation status might be preserved if the calendar user agent processing the new iTIP message for the new resource allows it).&lt;/t&gt;
+            &lt;/list&gt;
+          &lt;/t&gt;
+          &lt;t&gt;
+            Note that, since attendees can be invited to specific instances of a recurring meeting (not necessarily the entire set), it is possible that either the old or new calendar data no longer contains any valid components since the attendee was not invited to the corresponding portions of the original split recurrence. In such cases, the server MUST remove the original resource, or MUST NOT create the new resource.
+          &lt;/t&gt;
+          &lt;section title='Example'&gt;
+            &lt;t&gt;Assume the following iCalendar data is stored in the resource with URI &quot;/event.ics&quot;:
+            &lt;figure&gt;&lt;artwork&gt;&lt;![CDATA[BEGIN:VCALENDAR
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20140110T135358Z
+DTSTART:20140101T120000Z
+DURATION:PT1H
+SUMMARY:Example
+UID:DF400028-1223-4D26-92CA-B0ED3CC161F3
+RRULE:FREQ=DAILY;COUNT=20
+END:VEVENT
+END:VCALENDAR
+]]&gt;&lt;/artwork&gt;&lt;/figure&gt;
+            &lt;/t&gt;
+            &lt;t&gt;
+              The client issues a POST split request to split the recurring event at the instance with date-time &quot;20140110T120000Z&quot;:
+        &lt;figure&gt;
+          &lt;preamble&gt;
+            &amp;gt;&amp;gt; Request &amp;lt;&amp;lt;
+          &lt;/preamble&gt;
+          &lt;artwork&gt;
+&lt;![CDATA[POST /event.ics?action=split&amp;rid=20140110T120000Z HTTP/1.1
+Host: cal.example.com
+Content-Length: 0
+Prefer: return=representation
+
+]]&gt;&lt;/artwork&gt; &lt;/figure&gt;
+        &lt;figure&gt;
+          &lt;preamble&gt;
+            &amp;gt;&amp;gt; Response &amp;lt;&amp;lt;
+          &lt;/preamble&gt;
+          &lt;artwork&gt;
+&lt;![CDATA[HTTP/1.1 207 Multi-Status
+ETag: &quot;5bc9a2b55081eba0a9cd34f742aa1c11&quot;
+Date: Fri, 10 Jan 2014 14:02:20 GMT
+Content-Type: text/xml
+Content-Length: xxxx
+
+&lt;?xml version='1.0' encoding='UTF-8'?&gt;
+&lt;multistatus xmlns='DAV:'&gt;
+  &lt;response&gt;
+    &lt;href&gt;/event.ics&lt;/href&gt;
+    &lt;propstat&gt;
+      &lt;prop&gt;
+        &lt;getetag&gt;&quot;5bc9a2b55081eba0a9cd34f742aa1c11&quot;&lt;/getetag&gt;
+        &lt;calendar-data xmlns='urn:ietf:params:xml:ns:caldav'
+&gt;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VEVENT
+UID:DF400028-1223-4D26-92CA-B0ED3CC161F3
+DTSTART:20140110T120000Z
+DURATION:PT1H
+DTSTAMP:20140110T135358Z
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:E3B9D6D4-E19F-
+ 47AA-9088-1A29A9A7030F
+RRULE:FREQ=DAILY;COUNT=11
+SUMMARY:Example
+END:VEVENT
+END:VCALENDAR
+&lt;/calendar-data&gt;
+      &lt;/prop&gt;
+      &lt;status&gt;HTTP/1.1 200 OK&lt;/status&gt;
+    &lt;/propstat&gt;
+  &lt;/response&gt;
+  &lt;response&gt;
+    &lt;href&gt;/event-split.ics&lt;/href&gt;
+    &lt;propstat&gt;
+      &lt;prop&gt;
+        &lt;getetag&gt;&quot;8b352ad2e0ca26c99cb205373b3dc34c&quot;&lt;/getetag&gt;
+        &lt;calendar-data xmlns='urn:ietf:params:xml:ns:caldav'
+&gt;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VEVENT
+UID:E3B9D6D4-E19F-47AA-9088-1A29A9A7030F
+DTSTART:20140110T120000Z
+DURATION:PT1H
+DTSTAMP:20140110T135358Z
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:E3B9D6D4-E19F-
+ 47AA-9088-1A29A9A7030F
+RRULE:FREQ=DAILY;UNTIL=20140110T115959Z
+SUMMARY:Example
+END:VEVENT
+END:VCALENDAR
+&lt;/calendar-data&gt;
+      &lt;/prop&gt;
+      &lt;status&gt;HTTP/1.1 200 OK&lt;/status&gt;
+    &lt;/propstat&gt;
+  &lt;/response&gt;
+&lt;/multistatus&gt;
+]]&gt;&lt;/artwork&gt;&lt;/figure&gt;
+            &lt;/t&gt;
+            &lt;t&gt;
+              The original resource is changed to have a start date-time value of &quot;20140110T120000Z&quot;, and the &quot;COUNT&quot; component of the &quot;RRULE&quot; property is adjusted to the value &quot;11&quot; (which represents the number of remaining instances for that event, 9 early instances having been removed). The new resource has the original start date-time value, and its &quot;RRULE&quot; property has an &quot;UNTIL&quot; value one second prior to the split point. Both resources have a &quot;RELATED&quot; property with a &quot;RELTYPE&quot; parameter set to &quot;X-CALENDARSERVER-RECURRENCE-SET&quot; and a value set to the &quot;UID&quot; property value of the new resource.
+            &lt;/t&gt;
+          &lt;/section&gt;
+        &lt;/section&gt;
+
+        &lt;section title='Security Considerations'&gt;
+            &lt;t&gt;This specification does not introduce any more security considerations beyond those already listed in &lt;xref target=&quot;RFC5545&quot;&gt;iCalendar&lt;/xref&gt;, &lt;xref target=&quot;RFC5546&quot;&gt;iTIP&lt;/xref&gt; and &lt;xref target=&quot;RFC4791&quot;&gt;CalDAV Access&lt;/xref&gt; and &lt;xref target=&quot;RFC6638&quot;&gt;CalDAV Scheduling&lt;/xref&gt;.&lt;/t&gt;
+        &lt;/section&gt;
+        &lt;section title='IANA Considerations'&gt;
+            &lt;t&gt;
+                TBD: new HTTP request header registration.
+            &lt;/t&gt;
+        &lt;/section&gt;
+    &lt;/middle&gt;
+    &lt;back&gt;
+        &lt;references title='Normative References'&gt;
+            &amp;rfc2119;
+            &amp;rfc5545;
+            &amp;rfc5546;
+            &amp;rfc4791;
+            &amp;rfc6638;
+            &amp;idPreferHeader;
+        &lt;/references&gt;
+&lt;!--
+&lt;references title='Informative References'&gt;
+&lt;/references&gt;
+--&gt;
+        &lt;section title='Acknowledgments'&gt;
+            &lt;t&gt;
+                This specification is the result of discussions between the Apple calendar server and client teams.
+            &lt;/t&gt;
+        &lt;/section&gt;
+        &lt;!--
+        &lt;section title='Change History'&gt;
+          &lt;t&gt;Changes since -00
+            &lt;list style='numbers'&gt;
+              &lt;t&gt;&lt;/t&gt;
+            &lt;/list&gt;
+          &lt;/t&gt;
+        &lt;/section&gt;
+        --&gt;
+    &lt;/back&gt;
+&lt;/rfc&gt;
</ins></span></pre></div>
<a id="CalendarServertrunktwistedcaldavcustomxmlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/customxml.py (12280 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/customxml.py        2014-01-10 15:30:43 UTC (rev 12280)
+++ CalendarServer/trunk/twistedcaldav/customxml.py        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -76,7 +76,11 @@
</span><span class="cx">     &quot;calendarserver-home-sync&quot;,
</span><span class="cx"> )
</span><span class="cx"> 
</span><ins>+calendarserver_recurrence_split = (
+    &quot;calendarserver-recurrence-split&quot;,
+)
</ins><span class="cx"> 
</span><ins>+
</ins><span class="cx"> @registerElement
</span><span class="cx"> class TwistedCalendarSupportedComponents (WebDAVTextElement):
</span><span class="cx">     &quot;&quot;&quot;
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavicalpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/ical.py (12280 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/ical.py        2014-01-10 15:30:43 UTC (rev 12280)
+++ CalendarServer/trunk/twistedcaldav/ical.py        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -1213,34 +1213,42 @@
</span><span class="cx">         if master:
</span><span class="cx">             # Check if cut-off matches an RDATE
</span><span class="cx">             adjusted_rid = rid
</span><ins>+            adjust_rrule = None
+            adjust_count = 0
</ins><span class="cx">             continuing_rrule = True
</span><del>-            rdates = set([v.getValue() for v in itertools.chain(*[rdate.value() for rdate in master.properties(&quot;RDATE&quot;)])])
-            if rid in rdates:
-                # Need to detect the first valid RRULE instance after the cut-off
-                rrules = master._pycalendar.getRecurrenceSet()
-                if rrules and len(rrules.getRules()) != 0:
-                    rrule = rrules.getRules()[0]
-                    upperlimit = rid.duplicate()
-                    upperlimit.offsetYear(1)
-                    rrule_expanded = []
-                    rrule.expand(
-                        master.propertyValue(&quot;DTSTART&quot;),
-                        Period(DateTime(1900, 1, 1), upperlimit),
-                        rrule_expanded,
-                    )
-                    for i in sorted(rrule_expanded):
-                        if i &gt; rid:
-                            adjusted_rid = i
-                            break
-                    else:
-                        # RRULE not needed in derived master
-                        continuing_rrule = False
</del><span class="cx"> 
</span><ins>+            # Need to detect the first valid RRULE instance after the cut-off as that needs to be the new DTSTART
+            rrules = master._pycalendar.getRecurrenceSet()
+            if rrules and len(rrules.getRules()) != 0:
+                rrule = rrules.getRules()[0]
+                upperlimit = rid.duplicate()
+                upperlimit.offsetYear(1)
+                rrule_expanded = []
+                rrule.expand(
+                    master.propertyValue(&quot;DTSTART&quot;),
+                    Period(DateTime(1900, 1, 1), upperlimit),
+                    rrule_expanded,
+                )
+                for ctr, i in enumerate(sorted(rrule_expanded)):
+                    if i &gt;= rid:
+                        adjusted_rid = i
+                        adjust_rrule = rrule
+                        adjust_count = ctr
+                        break
+                else:
+                    # RRULE not needed in derived master
+                    continuing_rrule = False
+
</ins><span class="cx">             # Adjust master to previously derived instance
</span><span class="cx">             derived = self.deriveInstance(adjusted_rid, allowExcluded=True)
</span><span class="cx">             if derived is None:
</span><span class="cx">                 return
</span><span class="cx"> 
</span><ins>+            # Adjust any COUNT to exclude the earlier instances - note we do this after
+            # deriving the instance otherwise it might truncate the instance we care about
+            if adjust_rrule is not None and rrule.getUseCount():
+                adjust_rrule.setCount(adjust_rrule.getCount() - adjust_count)
+
</ins><span class="cx">             # Fix up recurrence properties so the derived one looks like the master
</span><span class="cx">             derived.removeProperty(derived.getProperty(&quot;RECURRENCE-ID&quot;))
</span><span class="cx">             for property in list(itertools.chain(
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavmethodpostpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/method/post.py (12280 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/method/post.py        2014-01-10 15:30:43 UTC (rev 12280)
+++ CalendarServer/trunk/twistedcaldav/method/post.py        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -22,7 +22,6 @@
</span><span class="cx"> __all__ = [&quot;http_POST&quot;]
</span><span class="cx"> 
</span><span class="cx"> from txweb2 import responsecode
</span><del>-from txweb2.http import StatusResponse
</del><span class="cx"> 
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> 
</span><span class="lines">@@ -45,13 +44,9 @@
</span><span class="cx">         action = request.args.get(&quot;action&quot;, (&quot;&quot;,))
</span><span class="cx">         if len(action) == 1:
</span><span class="cx">             action = action[0]
</span><del>-            if action in (&quot;attachment-add&quot;, &quot;attachment-update&quot;, &quot;attachment-remove&quot;) and \
-                hasattr(self, &quot;POST_handler_attachment&quot;):
-                if config.EnableManagedAttachments:
-                    result = (yield self.POST_handler_attachment(request, action))
-                    returnValue(result)
-                else:
-                    returnValue(StatusResponse(responsecode.FORBIDDEN, &quot;Managed Attachments not supported.&quot;))
</del><ins>+            if hasattr(self, &quot;POST_handler_action&quot;):
+                result = (yield self.POST_handler_action(request, action))
+                returnValue(result)
</ins><span class="cx"> 
</span><span class="cx">     # Content-type handlers
</span><span class="cx">     contentType = request.headers.getHeader(&quot;content-type&quot;)
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavstdconfigpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py (12280 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/stdconfig.py        2014-01-10 15:30:43 UTC (rev 12280)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -1626,6 +1626,7 @@
</span><span class="cx">             compliance += customxml.calendarserver_partstat_changes_compliance
</span><span class="cx">         if configDict.EnableTimezonesByReference:
</span><span class="cx">             compliance += caldavxml.caldav_timezones_by_reference_compliance
</span><ins>+        compliance += customxml.calendarserver_recurrence_split
</ins><span class="cx">     else:
</span><span class="cx">         compliance = ()
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavstorebridgepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/storebridge.py (12280 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/storebridge.py        2014-01-10 15:30:43 UTC (rev 12280)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -67,7 +67,7 @@
</span><span class="cx">     TooManyAttendeesError, InvalidCalendarAccessError, ValidOrganizerError, \
</span><span class="cx">     InvalidPerUserDataMerge, \
</span><span class="cx">     AttendeeAllowedError, ResourceDeletedError, InvalidAttachmentOperation, \
</span><del>-    ShareeAllowedError, DuplicatePrivateCommentsError
</del><ins>+    ShareeAllowedError, DuplicatePrivateCommentsError, InvalidSplit
</ins><span class="cx"> from txdav.carddav.iaddressbookstore import KindChangeNotAllowedError, \
</span><span class="cx">     GroupWithUnsharedAddressNotAllowedError
</span><span class="cx"> from txdav.common.datastore.sql_tables import _BIND_MODE_READ, _BIND_MODE_WRITE, \
</span><span class="lines">@@ -2921,8 +2921,130 @@
</span><span class="cx">         returnValue(result)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def POST_handler_action(self, request, action):
+        &quot;&quot;&quot;
+        Handle a POST request with an action= query parameter
+
+        @param request: the request to process
+        @type request: L{Request}
+        @param action: the action to execute
+        @type action: C{str}
+        &quot;&quot;&quot;
+        if action.startswith(&quot;attachment-&quot;):
+            result = (yield self.POST_handler_attachment(request, action))
+            returnValue(result)
+        else:
+            actioner = {
+                &quot;split&quot;: self.POST_handler_split,
+            }
+            if action in actioner:
+                result = (yield actioner[action](request, action))
+                returnValue(result)
+            else:
+                raise HTTPError(ErrorResponse(
+                    FORBIDDEN,
+                    (caldav_namespace, &quot;valid-action-parameter&quot;,),
+                    &quot;The action parameter in the request-URI is not valid&quot;,
+                ))
+
+
</ins><span class="cx">     @requiresPermissions(davxml.WriteContent())
</span><span class="cx">     @inlineCallbacks
</span><ins>+    def POST_handler_split(self, request, action):
+        &quot;&quot;&quot;
+        Handle a split of a calendar object resource.
+
+        @param request: HTTP request object
+        @type request: L{Request}
+        @param action: The request-URI 'action' argument
+        @type action: C{str}
+
+        @return: an HTTP response
+        &quot;&quot;&quot;
+
+        # Resource must exist
+        if not self.exists():
+            raise HTTPError(NOT_FOUND)
+
+        # Split point is in the rid query parameter
+        rid = request.args.get(&quot;rid&quot;)
+        if rid is None:
+            raise HTTPError(ErrorResponse(
+                FORBIDDEN,
+                (caldav_namespace, &quot;valid-rid-parameter&quot;,),
+                &quot;The rid parameter in the request-URI contains an invalid value&quot;,
+            ))
+
+        try:
+            rid = DateTime.parseText(rid[0])
+        except ValueError:
+            raise HTTPError(ErrorResponse(
+                FORBIDDEN,
+                (caldav_namespace, &quot;valid-rid-parameter&quot;,),
+                &quot;The rid parameter in the request-URI contains an invalid value&quot;,
+            ))
+        try:
+            otherStoreObject = yield self._newStoreObject.splitAt(rid)
+        except InvalidSplit:
+            raise HTTPError(ErrorResponse(
+                FORBIDDEN,
+                (calendarserver_namespace, &quot;invalid-split&quot;,),
+                &quot;The rid parameter in the request-URI contains an invalid value&quot;,
+            ))
+
+        other = yield request.locateChildResource(self._parentResource, otherStoreObject.name())
+        if other is None:
+            raise responsecode.INTERNAL_SERVER_ERROR
+
+        # Look for Prefer header
+        prefer = request.headers.getHeader(&quot;prefer&quot;, {})
+        returnRepresentation = any([key == &quot;return&quot; and value == &quot;representation&quot; for key, value, _ignore_args in prefer])
+
+        if returnRepresentation:
+            # Accept header handling
+            accepted_type = bestAcceptType(request.headers.getHeader(&quot;accept&quot;), Component.allowedTypes())
+            if accepted_type is None:
+                raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, &quot;Cannot generate requested data type&quot;))
+            etag1 = yield self.etag()
+            etag2 = yield other.etag()
+            cal1 = yield self.component()
+            cal2 = yield other.component()
+
+            xml_responses = [
+                davxml.PropertyStatusResponse(
+                    davxml.HRef.fromString(self.url()),
+                    davxml.PropertyStatus(
+                        davxml.PropertyContainer(
+                            davxml.GETETag.fromString(etag1.generate()),
+                            caldavxml.CalendarData.fromComponent(cal1, accepted_type),
+                        ),
+                        davxml.Status.fromResponseCode(OK),
+                    )
+                ),
+                davxml.PropertyStatusResponse(
+                    davxml.HRef.fromString(other.url()),
+                    davxml.PropertyStatus(
+                        davxml.PropertyContainer(
+                            davxml.GETETag.fromString(etag2.generate()),
+                            caldavxml.CalendarData.fromComponent(cal2, accepted_type),
+                        ),
+                        davxml.Status.fromResponseCode(OK),
+                    )
+                ),
+            ]
+
+            # Return multistatus with calendar data for this resource and the new one
+            result = MultiStatusResponse(xml_responses)
+        else:
+            result = Response(responsecode.NO_CONTENT)
+            result.headers.addRawHeader(&quot;Split-Component-URL&quot;, other.url())
+
+        returnValue(result)
+
+
+    @requiresPermissions(davxml.WriteContent())
+    @inlineCallbacks
</ins><span class="cx">     def POST_handler_attachment(self, request, action):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Handle a managed attachments request on the calendar object resource.
</span><span class="lines">@@ -2935,6 +3057,9 @@
</span><span class="cx">         @return: an HTTP response
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><ins>+        if not config.EnableManagedAttachments:
+            returnValue(StatusResponse(responsecode.FORBIDDEN, &quot;Managed Attachments not supported.&quot;))
+
</ins><span class="cx">         # Resource must exist to allow attachment operations
</span><span class="cx">         if not self.exists():
</span><span class="cx">             raise HTTPError(NOT_FOUND)
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingicalsplitterpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py (12280 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py        2014-01-10 15:30:43 UTC (rev 12280)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -105,8 +105,12 @@
</span><span class="cx">         instances = sorted(instances.instances.values(), key=lambda x: x.start)
</span><span class="cx">         rid = instances[0].rid
</span><span class="cx">         for instance in instances:
</span><del>-            rid = instance.rid
</del><span class="cx">             if instance.start &gt;= break_point:
</span><ins>+                rid = instance.rid
+
+                # Do not allow a rid prior to the first instance
+                if break_point and rid == instances[0].rid:
+                    rid = None
</ins><span class="cx">                 break
</span><span class="cx">         else:
</span><span class="cx">             # We can get here when splitting an event for overrides only in the past,
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_icalsplitterpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.py (12280 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.py        2014-01-10 15:30:43 UTC (rev 12280)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.py        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -1840,6 +1840,160 @@
</span><span class="cx"> &quot;&quot;&quot;,
</span><span class="cx">             ),
</span><span class="cx">             (
</span><ins>+                &quot;1.8 - RRULE with COUNT&quot;,
+                &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE:mailto:user1@example.com
+ATTENDEE:mailto:user2@example.com
+ATTENDEE:mailto:user3@example.com
+ATTENDEE:mailto:user4@example.com
+ATTENDEE:mailto:user5@example.com
+ATTENDEE:mailto:user6@example.com
+ATTENDEE:mailto:user7@example.com
+ATTENDEE:mailto:user8@example.com
+ATTENDEE:mailto:user9@example.com
+ATTENDEE:mailto:user10@example.com
+ATTENDEE:mailto:user11@example.com
+ATTENDEE:mailto:user12@example.com
+ATTENDEE:mailto:user13@example.com
+ATTENDEE:mailto:user14@example.com
+ATTENDEE:mailto:user15@example.com
+ATTENDEE:mailto:user16@example.com
+ATTENDEE:mailto:user17@example.com
+ATTENDEE:mailto:user18@example.com
+ATTENDEE:mailto:user19@example.com
+ATTENDEE:mailto:user20@example.com
+ATTENDEE:mailto:user21@example.com
+ATTENDEE:mailto:user22@example.com
+ATTENDEE:mailto:user23@example.com
+ATTENDEE:mailto:user24@example.com
+ATTENDEE:mailto:user25@example.com
+ORGANIZER:mailto:user1@example.com
+RRULE:FREQ=DAILY;COUNT=40
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE:mailto:user1@example.com
+ATTENDEE:mailto:user2@example.com
+ORGANIZER:mailto:user1@example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1@example.com
+ATTENDEE:mailto:user2@example.com
+ORGANIZER:mailto:user1@example.com
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;,
+                &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE:mailto:user1@example.com
+ATTENDEE:mailto:user2@example.com
+ATTENDEE:mailto:user3@example.com
+ATTENDEE:mailto:user4@example.com
+ATTENDEE:mailto:user5@example.com
+ATTENDEE:mailto:user6@example.com
+ATTENDEE:mailto:user7@example.com
+ATTENDEE:mailto:user8@example.com
+ATTENDEE:mailto:user9@example.com
+ATTENDEE:mailto:user10@example.com
+ATTENDEE:mailto:user11@example.com
+ATTENDEE:mailto:user12@example.com
+ATTENDEE:mailto:user13@example.com
+ATTENDEE:mailto:user14@example.com
+ATTENDEE:mailto:user15@example.com
+ATTENDEE:mailto:user16@example.com
+ATTENDEE:mailto:user17@example.com
+ATTENDEE:mailto:user18@example.com
+ATTENDEE:mailto:user19@example.com
+ATTENDEE:mailto:user20@example.com
+ATTENDEE:mailto:user21@example.com
+ATTENDEE:mailto:user22@example.com
+ATTENDEE:mailto:user23@example.com
+ATTENDEE:mailto:user24@example.com
+ATTENDEE:mailto:user25@example.com
+ORGANIZER:mailto:user1@example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;COUNT=24
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;,
+                &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE:mailto:user1@example.com
+ATTENDEE:mailto:user2@example.com
+ATTENDEE:mailto:user3@example.com
+ATTENDEE:mailto:user4@example.com
+ATTENDEE:mailto:user5@example.com
+ATTENDEE:mailto:user6@example.com
+ATTENDEE:mailto:user7@example.com
+ATTENDEE:mailto:user8@example.com
+ATTENDEE:mailto:user9@example.com
+ATTENDEE:mailto:user10@example.com
+ATTENDEE:mailto:user11@example.com
+ATTENDEE:mailto:user12@example.com
+ATTENDEE:mailto:user13@example.com
+ATTENDEE:mailto:user14@example.com
+ATTENDEE:mailto:user15@example.com
+ATTENDEE:mailto:user16@example.com
+ATTENDEE:mailto:user17@example.com
+ATTENDEE:mailto:user18@example.com
+ATTENDEE:mailto:user19@example.com
+ATTENDEE:mailto:user20@example.com
+ATTENDEE:mailto:user21@example.com
+ATTENDEE:mailto:user22@example.com
+ATTENDEE:mailto:user23@example.com
+ATTENDEE:mailto:user24@example.com
+ATTENDEE:mailto:user25@example.com
+ORGANIZER:mailto:user1@example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE:mailto:user1@example.com
+ATTENDEE:mailto:user2@example.com
+ORGANIZER:mailto:user1@example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1@example.com
+ATTENDEE:mailto:user2@example.com
+ORGANIZER:mailto:user1@example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;,
+            ),
+            (
</ins><span class="cx">                 &quot;2.1 - Overrides only - one future, one past&quot;,
</span><span class="cx">                 &quot;&quot;&quot;BEGIN:VCALENDAR
</span><span class="cx"> VERSION:2.0
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py (12280 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/sql.py        2014-01-10 15:30:43 UTC (rev 12280)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -77,7 +77,7 @@
</span><span class="cx">     ValidOrganizerError, ShareeAllowedError, ComponentRemoveState, \
</span><span class="cx">     InvalidDefaultCalendar, \
</span><span class="cx">     InvalidAttachmentOperation, DuplicatePrivateCommentsError, \
</span><del>-    TimeRangeUpperLimit, TimeRangeLowerLimit
</del><ins>+    TimeRangeUpperLimit, TimeRangeLowerLimit, InvalidSplit
</ins><span class="cx"> from txdav.caldav.icalendarstore import QuotaExceeded
</span><span class="cx"> from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
</span><span class="cx">     CommonObjectResource, ECALENDARTYPE
</span><span class="lines">@@ -3742,6 +3742,40 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><ins>+    def splitAt(self, rid):
+        &quot;&quot;&quot;
+        User initiated split. We need to verify it is OK to do so first. We will allow any recurring item to
+        be split, but will not allow attendees to split invites.
+
+        @param rid: the date-time where the split should occur. This need not be a specific instance
+            date-time - this method will choose the next instance on or after this value.
+        @type rid: L{DateTime}
+        &quot;&quot;&quot;
+
+        # Must be recurring
+        component = yield self.component()
+        if not component.isRecurring():
+            raise InvalidSplit()
+
+        # Cannot be attendee
+        ownerPrincipal = self.calendar().ownerHome().directoryRecord()
+        organizer = component.getOrganizer()
+        organizerPrincipal = self.directoryService().recordWithCalendarUserAddress(organizer) if organizer else None
+        if organizer is not None and organizerPrincipal.uid != ownerPrincipal.uid:
+            raise InvalidSplit()
+
+        # Determine valid split point
+        splitter = iCalSplitter(1024, 14)
+        rid = splitter.whereSplit(component, break_point=rid, allow_past_the_end=False)
+        if rid is None:
+            raise InvalidSplit()
+
+        # Do split and return new resource
+        olderObject = yield self.split(rid=rid)
+        returnValue(olderObject)
+
+
+    @inlineCallbacks
</ins><span class="cx">     def split(self, onlyThis=False, rid=None, olderUID=None):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Split this and all matching UID calendar objects as per L{iCalSplitter}.
</span><span class="lines">@@ -3784,8 +3818,9 @@
</span><span class="cx">         # Now process this resource, but do implicit scheduling for attendees not hosted on this server.
</span><span class="cx">         # We need to do this before processing attendee copies.
</span><span class="cx">         calendar_old, calendar_new = splitter.split(calendar, rid=rid, olderUID=olderUID)
</span><del>-        calendar_new.bumpiTIPInfo(oldcalendar=calendar, doSequence=True)
-        calendar_old.bumpiTIPInfo(oldcalendar=None, doSequence=True)
</del><ins>+        if calendar_new.getOrganizer() is not None:
+            calendar_new.bumpiTIPInfo(oldcalendar=calendar, doSequence=True)
+            calendar_old.bumpiTIPInfo(oldcalendar=None, doSequence=True)
</ins><span class="cx"> 
</span><span class="cx">         # If the split results in nothing either resource, then there is really nothing
</span><span class="cx">         # to actually split
</span><span class="lines">@@ -3794,7 +3829,7 @@
</span><span class="cx"> 
</span><span class="cx">         # Store changed data
</span><span class="cx">         yield self._setComponentInternal(calendar_new, internal_state=ComponentUpdateState.SPLIT_OWNER, split_details=(rid, olderUID, True,))
</span><del>-        yield self.calendar()._createCalendarObjectWithNameInternal(&quot;%s.ics&quot; % (olderUID,), calendar_old, ComponentUpdateState.SPLIT_OWNER, split_details=(rid, newerUID, False,))
</del><ins>+        olderObject = yield self.calendar()._createCalendarObjectWithNameInternal(&quot;%s.ics&quot; % (olderUID,), calendar_old, ComponentUpdateState.SPLIT_OWNER, split_details=(rid, newerUID, False,))
</ins><span class="cx"> 
</span><span class="cx">         # Split each one - but not this resource
</span><span class="cx">         for resource in resources:
</span><span class="lines">@@ -3802,7 +3837,7 @@
</span><span class="cx">                 continue
</span><span class="cx">             yield resource.splitForAttendee(rid, olderUID)
</span><span class="cx"> 
</span><del>-        returnValue(olderUID)
</del><ins>+        returnValue(olderObject)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoretesttest_sqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py (12280 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py        2014-01-10 15:30:43 UTC (rev 12280)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -57,7 +57,8 @@
</span><span class="cx"> from txdav.caldav.datastore.test.test_file import setUpCalendarStore
</span><span class="cx"> from txdav.caldav.datastore.test.util import buildCalendarStore
</span><span class="cx"> from txdav.caldav.datastore.util import _migrateCalendar, migrateHome
</span><del>-from txdav.caldav.icalendarstore import ComponentUpdateState, InvalidDefaultCalendar
</del><ins>+from txdav.caldav.icalendarstore import ComponentUpdateState, InvalidDefaultCalendar, \
+    InvalidSplit
</ins><span class="cx"> from txdav.common.datastore.sql import ECALENDARTYPE, CommonObjectResource, \
</span><span class="cx">     CommonStoreTransactionMonitor
</span><span class="cx"> from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
</span><span class="lines">@@ -2418,7 +2419,8 @@
</span><span class="cx">         will = yield cobj.willSplit()
</span><span class="cx">         self.assertTrue(will)
</span><span class="cx"> 
</span><del>-        newUID = yield cobj.split()
</del><ins>+        newObj = yield cobj.split()
+        newUID = newObj.uid()
</ins><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx">         cobj1 = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
</span><span class="lines">@@ -5570,3 +5572,662 @@
</span><span class="cx">         self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future2) % relsubs, &quot;Failed future: %s&quot; % (title,))
</span><span class="cx">         self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past2) % relsubs, &quot;Failed past: %s&quot; % (title,))
</span><span class="cx">         self.assertEqual(normalize_iCalStr(ical_inbox), normalize_iCalStr(data_inbox2) % relsubs, &quot;Failed inbox: %s&quot; % (title,))
</span><ins>+
+
+    @inlineCallbacks
+    def _setupSplitAt(self):
+        &quot;&quot;&quot;
+        Test that user triggered splitting of calendar objects works.
+        &quot;&quot;&quot;
+        self.patch(config.Scheduling.Options.Splitting, &quot;Enabled&quot;, False)
+        self.patch(config.Scheduling.Options.Splitting, &quot;Size&quot;, 1024)
+        self.patch(config.Scheduling.Options.Splitting, &quot;PastDays&quot;, 14)
+        self.patch(config.Scheduling.Options.Splitting, &quot;Delay&quot;, 2)
+
+        # Create one event that will split
+        calendar = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;user01&quot;)
+
+        data = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back28)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01@example.com
+ATTENDEE:mailto:user02@example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01@example.com
+RRULE:FREQ=DAILY;COUNT=50
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01@example.com
+ATTENDEE:mailto:user02@example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01@example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01@example.com
+ATTENDEE:mailto:user02@example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01@example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01@example.com
+ATTENDEE:mailto:user02@example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01@example.com
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        data_future = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;COUNT=36
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        data_past = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back28)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        data_future2 = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;COUNT=36
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        data_past2 = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back28)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:%(relID)s
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        data_inbox2 = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;COUNT=36
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        # Create it
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName(&quot;data1.ics&quot;, component)
+        self.assertFalse(hasattr(cobj, &quot;_workItems&quot;))
+        yield self.commit()
+
+        w = schema.CALENDAR_OBJECT_SPLITTER_WORK
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        returnValue((data_future, data_past, data_future2, data_past2, data_inbox2,))
+
+
+    @inlineCallbacks
+    def _setupSplitAt2(self):
+        &quot;&quot;&quot;
+        Test that user triggered splitting of calendar objects works.
+        &quot;&quot;&quot;
+        self.patch(config.Scheduling.Options.Splitting, &quot;Enabled&quot;, False)
+        self.patch(config.Scheduling.Options.Splitting, &quot;Size&quot;, 1024)
+        self.patch(config.Scheduling.Options.Splitting, &quot;PastDays&quot;, 14)
+        self.patch(config.Scheduling.Options.Splitting, &quot;Delay&quot;, 2)
+
+        # Create one event that will split
+        calendar = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;user01&quot;)
+
+        data = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back28)s
+DURATION:PT1H
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=DAILY;COUNT=50
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+DTSTAMP:20051222T210507Z
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+DTSTAMP:20051222T210507Z
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+DTSTAMP:20051222T210507Z
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        data_future = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+DTSTAMP:20051222T210507Z
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;COUNT=36
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+DTSTAMP:20051222T210507Z
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        data_past = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back28)s
+DURATION:PT1H
+DTSTAMP:20051222T210507Z
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+DTSTAMP:20051222T210507Z
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+DTSTAMP:20051222T210507Z
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        # Create it
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName(&quot;data1.ics&quot;, component)
+        self.assertFalse(hasattr(cobj, &quot;_workItems&quot;))
+        yield self.commit()
+
+        w = schema.CALENDAR_OBJECT_SPLITTER_WORK
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        returnValue((data_future, data_past,))
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_splitat_ok(self):
+        &quot;&quot;&quot;
+        Test that user triggered splitting of calendar objects works.
+        &quot;&quot;&quot;
+
+        data_future, data_past, data_future2, data_past2, data_inbox2 = yield self._setupSplitAt()
+
+        # Update it
+        cobj = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        oldobj = yield cobj.splitAt(DateTime.parseText(&quot;%(now_back14)s&quot; % self.subs))
+        oldname = oldobj.name()
+        self.assertFalse(hasattr(cobj, &quot;_workItems&quot;))
+        yield self.commit()
+
+        w = schema.CALENDAR_OBJECT_SPLITTER_WORK
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        # Get the existing and new object data
+        cobj1 = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        self.assertTrue(cobj1.isScheduleObject)
+        ical1 = yield cobj1.component()
+        newUID = ical1.masterComponent().propertyValue(&quot;RELATED-TO&quot;)
+
+        cobj2 = yield self.calendarObjectUnderTest(name=oldname, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        self.assertTrue(cobj2 is not None)
+        self.assertTrue(cobj2.isScheduleObject)
+
+        ical_future = yield cobj1.component()
+        ical_past = yield cobj2.component()
+
+        # Verify user01 data
+        title = &quot;user01&quot;
+        relsubs = dict(self.subs)
+        relsubs[&quot;relID&quot;] = newUID
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, &quot;Failed future: %s&quot; % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % relsubs, &quot;Failed past: %s&quot; % (title,))
+
+        # Get user02 data
+        cal = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;user02&quot;)
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 2)
+        for cobj in cobjs:
+            ical = yield cobj.component()
+            if ical.resourceUID() == &quot;12345-67890&quot;:
+                ical_future = ical
+            else:
+                ical_past = ical
+
+        cal = yield self.calendarUnderTest(name=&quot;inbox&quot;, home=&quot;user02&quot;)
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        ical_inbox = yield cobjs[0].component()
+
+        # Verify user02 data
+        title = &quot;user02&quot;
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future2) % relsubs, &quot;Failed future: %s&quot; % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past2) % relsubs, &quot;Failed past: %s&quot; % (title,))
+        self.assertEqual(normalize_iCalStr(ical_inbox), normalize_iCalStr(data_inbox2) % relsubs, &quot;Failed inbox: %s&quot; % (title,))
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_splitat_ok_not_instance_rid(self):
+        &quot;&quot;&quot;
+        Test that user triggered splitting of calendar objects works when split between instances.
+        &quot;&quot;&quot;
+
+        data_future, data_past, data_future2, data_past2, data_inbox2 = yield self._setupSplitAt()
+
+        # Update it
+        cobj = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        oldobj = yield cobj.splitAt(DateTime.parseText(&quot;%(now_back15_12h)s&quot; % self.subs))
+        oldname = oldobj.name()
+        self.assertFalse(hasattr(cobj, &quot;_workItems&quot;))
+        yield self.commit()
+
+        w = schema.CALENDAR_OBJECT_SPLITTER_WORK
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        # Get the existing and new object data
+        cobj1 = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        self.assertTrue(cobj1.isScheduleObject)
+        ical1 = yield cobj1.component()
+        newUID = ical1.masterComponent().propertyValue(&quot;RELATED-TO&quot;)
+
+        cobj2 = yield self.calendarObjectUnderTest(name=oldname, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        self.assertTrue(cobj2 is not None)
+        self.assertTrue(cobj2.isScheduleObject)
+
+        ical_future = yield cobj1.component()
+        ical_past = yield cobj2.component()
+
+        # Verify user01 data
+        title = &quot;user01&quot;
+        relsubs = dict(self.subs)
+        relsubs[&quot;relID&quot;] = newUID
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, &quot;Failed future: %s&quot; % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % relsubs, &quot;Failed past: %s&quot; % (title,))
+
+        # Get user02 data
+        cal = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;user02&quot;)
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 2)
+        for cobj in cobjs:
+            ical = yield cobj.component()
+            if ical.resourceUID() == &quot;12345-67890&quot;:
+                ical_future = ical
+            else:
+                ical_past = ical
+
+        cal = yield self.calendarUnderTest(name=&quot;inbox&quot;, home=&quot;user02&quot;)
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        ical_inbox = yield cobjs[0].component()
+
+        # Verify user02 data
+        title = &quot;user02&quot;
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future2) % relsubs, &quot;Failed future: %s&quot; % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past2) % relsubs, &quot;Failed past: %s&quot; % (title,))
+        self.assertEqual(normalize_iCalStr(ical_inbox), normalize_iCalStr(data_inbox2) % relsubs, &quot;Failed inbox: %s&quot; % (title,))
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_splitat_no_organizer(self):
+        &quot;&quot;&quot;
+        Test that user triggered splitting of calendar objects works when no organizer is present.
+        &quot;&quot;&quot;
+
+        data_future, data_past = yield self._setupSplitAt2()
+
+        # Update it
+        cobj = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        oldobj = yield cobj.splitAt(DateTime.parseText(&quot;%(now_back14)s&quot; % self.subs))
+        oldname = oldobj.name()
+        self.assertFalse(hasattr(cobj, &quot;_workItems&quot;))
+        yield self.commit()
+
+        w = schema.CALENDAR_OBJECT_SPLITTER_WORK
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        # Get the existing and new object data
+        cobj1 = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        self.assertTrue(cobj1.isScheduleObject)
+        ical1 = yield cobj1.component()
+        newUID = ical1.masterComponent().propertyValue(&quot;RELATED-TO&quot;)
+
+        cobj2 = yield self.calendarObjectUnderTest(name=oldname, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        self.assertTrue(cobj2 is not None)
+        self.assertTrue(cobj2.isScheduleObject)
+
+        ical_future = yield cobj1.component()
+        ical_past = yield cobj2.component()
+
+        # Verify user01 data
+        title = &quot;user01&quot;
+        relsubs = dict(self.subs)
+        relsubs[&quot;relID&quot;] = newUID
+        self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future) % relsubs, &quot;Failed future: %s&quot; % (title,))
+        self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past) % relsubs, &quot;Failed past: %s&quot; % (title,))
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_splitat_no_attendee_split(self):
+        &quot;&quot;&quot;
+        Test that user triggered splitting of calendar objects does not work if done by attendee.
+        &quot;&quot;&quot;
+
+        yield self._setupSplitAt()
+
+        # Update it
+        cal = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;user02&quot;)
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        yield self.failUnlessFailure(cobjs[0].splitAt(DateTime.parseText(&quot;%(now_back14)s&quot; % self.subs)), InvalidSplit)
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_splitat_too_old(self):
+        &quot;&quot;&quot;
+        Test that user triggered splitting of calendar objects does not work if split before first instance.
+        &quot;&quot;&quot;
+
+        yield self._setupSplitAt()
+
+        # Update it
+        cal = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;user02&quot;)
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        yield self.failUnlessFailure(cobjs[0].splitAt(DateTime.parseText(&quot;%(now_back30)s&quot; % self.subs)), InvalidSplit)
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_splitat_too_new(self):
+        &quot;&quot;&quot;
+        Test that user triggered splitting of calendar objects does not work if split after last instance.
+        &quot;&quot;&quot;
+
+        yield self._setupSplitAt()
+
+        # Update it
+        cal = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;user02&quot;)
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        yield self.failUnlessFailure(cobjs[0].splitAt(DateTime.parseText(&quot;%(now_fwd25)s&quot; % self.subs)), InvalidSplit)
</ins></span></pre></div>
<a id="CalendarServertrunktxdavcaldavicalendarstorepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/icalendarstore.py (12280 => 12281)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/icalendarstore.py        2014-01-10 15:30:43 UTC (rev 12280)
+++ CalendarServer/trunk/txdav/caldav/icalendarstore.py        2014-01-10 15:34:01 UTC (rev 12281)
</span><span class="lines">@@ -843,6 +843,13 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+class InvalidSplit(CommonStoreError):
+    &quot;&quot;&quot;
+    A user triggered split operation is invalid.
+    &quot;&quot;&quot;
+
+
+
</ins><span class="cx"> #
</span><span class="cx"> # FIXME: These may belong elsewhere.
</span><span class="cx"> #
</span></span></pre>
</div>
</div>

</body>
</html>