<!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>[13218] 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/13218">13218</a></dd>
<dt>Author</dt> <dd>gaya@apple.com</dd>
<dt>Date</dt> <dd>2014-04-08 22:18:45 -0700 (Tue, 08 Apr 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>group attendees</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkproject">CalendarServer/trunk/.project</a></li>
<li><a href="#CalendarServertrunktwistedcaldavdirectorybackedaddressbookpy">CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavicalpy">CalendarServer/trunk/twistedcaldav/ical.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavtesttest_icalendarpy">CalendarServer/trunk/twistedcaldav/test/test_icalendar.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavtesttest_upgradepy">CalendarServer/trunk/twistedcaldav/test/test_upgrade.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoresqlpy">CalendarServer/trunk/txdav/caldav/datastore/sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoretestutilpy">CalendarServer/trunk/txdav/caldav/datastore/test/util.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreutilpy">CalendarServer/trunk/txdav/caldav/datastore/util.py</a></li>
<li><a href="#CalendarServertrunktxdavwhodirectorypy">CalendarServer/trunk/txdav/who/directory.py</a></li>
<li><a href="#CalendarServertrunktxdavwhogroupspy">CalendarServer/trunk/txdav/who/groups.py</a></li>
<li><a href="#CalendarServertrunktxdavwhotesttest_groupspy">CalendarServer/trunk/txdav/who/test/test_groups.py</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#CalendarServertrunktxdavwhotestaccountsgroupAttendeeAccountsxml">CalendarServer/trunk/txdav/who/test/accounts/groupAttendeeAccounts.xml</a></li>
<li><a href="#CalendarServertrunktxdavwhotesttest_group_attendeespy">CalendarServer/trunk/txdav/who/test/test_group_attendees.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkproject"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/.project (13217 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/.project        2014-04-09 05:17:43 UTC (rev 13217)
+++ CalendarServer/trunk/.project        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -3,6 +3,10 @@
</span><span class="cx">         &lt;name&gt;trunk&lt;/name&gt;
</span><span class="cx">         &lt;comment&gt;&lt;/comment&gt;
</span><span class="cx">         &lt;projects&gt;
</span><ins>+                &lt;project&gt;caldavclientlibrary&lt;/project&gt;
+                &lt;project&gt;kerberos&lt;/project&gt;
+                &lt;project&gt;pycalendar&lt;/project&gt;
+                &lt;project&gt;twextpy&lt;/project&gt;
</ins><span class="cx">         &lt;/projects&gt;
</span><span class="cx">         &lt;buildSpec&gt;
</span><span class="cx">                 &lt;buildCommand&gt;
</span><span class="lines">@@ -16,7 +20,7 @@
</span><span class="cx">         &lt;/natures&gt;
</span><span class="cx">         &lt;filteredResources&gt;
</span><span class="cx">                 &lt;filter&gt;
</span><del>-                        &lt;id&gt;1396665409305&lt;/id&gt;
</del><ins>+                        &lt;id&gt;1396668930421&lt;/id&gt;
</ins><span class="cx">                         &lt;name&gt;&lt;/name&gt;
</span><span class="cx">                         &lt;type&gt;10&lt;/type&gt;
</span><span class="cx">                         &lt;matcher&gt;
</span><span class="lines">@@ -25,7 +29,7 @@
</span><span class="cx">                         &lt;/matcher&gt;
</span><span class="cx">                 &lt;/filter&gt;
</span><span class="cx">                 &lt;filter&gt;
</span><del>-                        &lt;id&gt;1396665409305&lt;/id&gt;
</del><ins>+                        &lt;id&gt;1396668930422&lt;/id&gt;
</ins><span class="cx">                         &lt;name&gt;&lt;/name&gt;
</span><span class="cx">                         &lt;type&gt;10&lt;/type&gt;
</span><span class="cx">                         &lt;matcher&gt;
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavdirectorybackedaddressbookpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py (13217 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py        2014-04-09 05:17:43 UTC (rev 13217)
+++ CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -221,10 +221,10 @@
</span><span class="cx"> 
</span><span class="cx">                 vCardsResults = [(yield ABDirectoryQueryResult(self).generate(record)) for record in records]
</span><span class="cx"> 
</span><del>-                filteredResults = []
</del><ins>+                filteredResults = set()
</ins><span class="cx">                 for vCardResult in vCardsResults:
</span><span class="cx">                     if addressBookFilter.match(vCardResult.vCard()):
</span><del>-                        filteredResults.append(vCardResult)
</del><ins>+                        filteredResults.add(vCardResult)
</ins><span class="cx">                     else:
</span><span class="cx">                         log.debug(&quot;doAddressBookDirectoryQuery: vCard did not match filter:\n{vcard}&quot;, vcard=vCardResult.vCard())
</span><span class="cx"> 
</span><span class="lines">@@ -249,7 +249,7 @@
</span><span class="cx">                 if maxQueryRecords and maxRecords &gt; maxQueryRecords:
</span><span class="cx">                     maxRecords = maxQueryRecords
</span><span class="cx"> 
</span><del>-            results = sorted(list(filteredResults), key=lambda result: result.vCard().propertyValue(&quot;UID&quot;))
</del><ins>+            results = sorted(filteredResults, key=lambda result: result.vCard().propertyValue(&quot;UID&quot;))
</ins><span class="cx">             limited = maxResults and len(results) &gt;= maxResults
</span><span class="cx"> 
</span><span class="cx">         log.info(&quot;limited={l} #results={n}&quot;, l=limited, n=len(results))
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavicalpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/ical.py (13217 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/ical.py        2014-04-09 05:17:43 UTC (rev 13217)
+++ CalendarServer/trunk/twistedcaldav/ical.py        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -183,7 +183,7 @@
</span><span class="cx">             pyobj = kwargs[&quot;pycalendar&quot;]
</span><span class="cx"> 
</span><span class="cx">             if not isinstance(pyobj, PyProperty):
</span><del>-                raise TypeError(&quot;Not a Property: %r&quot; % (property,))
</del><ins>+                raise TypeError(&quot;Not a Property: {0!r}&quot;.format(property,))
</ins><span class="cx"> 
</span><span class="cx">             self._pycalendar = pyobj
</span><span class="cx">         else:
</span><span class="lines">@@ -201,7 +201,10 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def __repr__(self):
</span><del>-        return &quot;&lt;%s: %r: %r&gt;&quot; % (self.__class__.__name__, self.name(), self.value())
</del><ins>+        return (
+            &quot;&lt;{self.__class__.__name__}: {name!r}: {value!r}&gt;&quot;
+            .format(self=self, name=self.name(), value=self.value())
+        )
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def __hash__(self):
</span><span class="lines">@@ -358,7 +361,7 @@
</span><span class="cx"> 
</span><span class="cx">         # get date/date-time value
</span><span class="cx">         dt = self._pycalendar.getValue().getValue()
</span><del>-        assert isinstance(dt, DateTime), &quot;Not a date/date-time value: %r&quot; % (self,)
</del><ins>+        assert isinstance(dt, DateTime), &quot;Not a date/date-time value: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         return timeRangesOverlap(dt, None, start, end, defaulttz)
</span><span class="cx"> 
</span><span class="lines">@@ -473,13 +476,13 @@
</span><span class="cx">         try:
</span><span class="cx">             result = Calendar.parseData(data, format)
</span><span class="cx">         except ErrorBase, e:
</span><del>-            errmsg = &quot;%s: %s&quot; % (e.mReason, e.mData,)
</del><ins>+            errmsg = &quot;{0}: {1}&quot;.format(e.mReason, e.mData,)
</ins><span class="cx">             result = None
</span><span class="cx">         if not result:
</span><span class="cx">             if isstream:
</span><span class="cx">                 data.seek(0)
</span><span class="cx">                 data = data.read()
</span><del>-            raise InvalidICalendarDataError(&quot;%s\n%s&quot; % (errmsg, data,))
</del><ins>+            raise InvalidICalendarDataError(&quot;{0}\n{1}&quot;.format(errmsg, data,))
</ins><span class="cx">         return clazz(None, pycalendar=result)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -531,7 +534,7 @@
</span><span class="cx"> 
</span><span class="cx">                 if pyobj is not None:
</span><span class="cx">                     if not isinstance(pyobj, ComponentBase):
</span><del>-                        raise TypeError(&quot;Not a ComponentBase: %r&quot; % (pyobj,))
</del><ins>+                        raise TypeError(&quot;Not a ComponentBase: {0!r}&quot;.format(pyobj,))
</ins><span class="cx"> 
</span><span class="cx">                 self._pycalendar = pyobj
</span><span class="cx">             else:
</span><span class="lines">@@ -542,7 +545,7 @@
</span><span class="cx"> 
</span><span class="cx">                 if parent is not None:
</span><span class="cx">                     if not isinstance(parent, Component):
</span><del>-                        raise TypeError(&quot;Not a Component: %r&quot; % (parent,))
</del><ins>+                        raise TypeError(&quot;Not a Component: {0!r}&quot;.format(parent,))
</ins><span class="cx"> 
</span><span class="cx">                 self._parent = parent
</span><span class="cx">             else:
</span><span class="lines">@@ -575,7 +578,10 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def __repr__(self):
</span><del>-        return &quot;&lt;%s: %r&gt;&quot; % (self.__class__.__name__, str(self._pycalendar))
</del><ins>+        return (
+            &quot;&lt;{self.__class__.__name__}: {pycal!r}&gt;&quot;
+            .format(self=self, pycal=str(self._pycalendar))
+        )
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def __hash__(self):
</span><span class="lines">@@ -600,7 +606,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Return text representation and include timezones if the option is on.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         result = self._pycalendar.getText(includeTimezones=includeTimezones, format=format)
</span><span class="cx">         if result is None:
</span><span class="lines">@@ -626,14 +632,14 @@
</span><span class="cx">         @return: the name of the primary type.
</span><span class="cx">         @raise: L{InvalidICalendarDataError} if there is more than one primary type.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         mtype = None
</span><span class="cx">         for component in self.subcomponents():
</span><span class="cx">             if component.name() in ignoredComponents:
</span><span class="cx">                 continue
</span><span class="cx">             elif mtype and (mtype != component.name()):
</span><del>-                raise InvalidICalendarDataError(&quot;Component contains more than one type of primary type: %r&quot; % (self,))
</del><ins>+                raise InvalidICalendarDataError(&quot;Component contains more than one type of primary type: {0!r}&quot;.format(self,))
</ins><span class="cx">             else:
</span><span class="cx">                 mtype = component.name()
</span><span class="cx"> 
</span><span class="lines">@@ -647,7 +653,7 @@
</span><span class="cx"> 
</span><span class="cx">         @return: the L{Component} of the primary type.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         result = None
</span><span class="cx">         for component in self.subcomponents():
</span><span class="lines">@@ -667,7 +673,7 @@
</span><span class="cx">         @return: the L{Component} for the master component,
</span><span class="cx">             or C{None} if there isn't one.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         for component in self.subcomponents():
</span><span class="cx">             if component.name() in ignoredComponents:
</span><span class="lines">@@ -688,7 +694,7 @@
</span><span class="cx">         @return: the L{Component} for the overridden component,
</span><span class="cx">             or C{None} if there isn't one.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         if isinstance(recurrence_id, str):
</span><span class="cx">             recurrence_id = DateTime.parseText(recurrence_id) if recurrence_id else None
</span><span class="lines">@@ -710,7 +716,7 @@
</span><span class="cx">         Return the access level for this component.
</span><span class="cx">         @return: the access level for the calendar data.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Must be a VCALENDAR: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         access = self.propertyValue(Component.ACCESS_PROPERTY)
</span><span class="cx">         if access:
</span><span class="lines">@@ -780,7 +786,7 @@
</span><span class="cx">         if len(properties) == 1:
</span><span class="cx">             return properties[0]
</span><span class="cx">         if len(properties) &gt; 1:
</span><del>-            raise InvalidICalendarDataError(&quot;More than one %s property in component %r&quot; % (name, self))
</del><ins>+            raise InvalidICalendarDataError(&quot;More than one {0} property in component {1!r}&quot;.format(name, self))
</ins><span class="cx">         return None
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -808,7 +814,7 @@
</span><span class="cx">         if len(properties) == 1:
</span><span class="cx">             return properties[0].value()
</span><span class="cx">         if len(properties) &gt; 1:
</span><del>-            raise InvalidICalendarDataError(&quot;More than one %s property in component %r&quot; % (name, self))
</del><ins>+            raise InvalidICalendarDataError(&quot;More than one {0} property in component {1!r}&quot;.format(name, self))
</ins><span class="cx">         return None
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -926,12 +932,12 @@
</span><span class="cx">             repeat : an integer for the REPEAT count
</span><span class="cx">             duration: the repeat duration if present, otherwise None
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        assert self.name() == &quot;VALARM&quot;, &quot;Component is not a VAlARM: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VALARM&quot;, &quot;Component is not a VAlARM: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         # The trigger value
</span><span class="cx">         trigger = self.propertyValue(&quot;TRIGGER&quot;)
</span><span class="cx">         if trigger is None:
</span><del>-            raise InvalidICalendarDataError(&quot;VALARM has no TRIGGER property: %r&quot; % (self,))
</del><ins>+            raise InvalidICalendarDataError(&quot;VALARM has no TRIGGER property: {0!r}&quot;.format(self,))
</ins><span class="cx"> 
</span><span class="cx">         # The related parameter
</span><span class="cx">         related = self.getProperty(&quot;TRIGGER&quot;).parameterValue(&quot;RELATED&quot;)
</span><span class="lines">@@ -951,7 +957,7 @@
</span><span class="cx">         duration = self.propertyValue(&quot;DURATION&quot;)
</span><span class="cx"> 
</span><span class="cx">         if repeat &gt; 0 and duration is None:
</span><del>-            raise InvalidICalendarDataError(&quot;VALARM has invalid REPEAT/DURATIOn properties: %r&quot; % (self,))
</del><ins>+            raise InvalidICalendarDataError(&quot;VALARM has invalid REPEAT/DURATIOn properties: {0!r}&quot;.format(self,))
</ins><span class="cx"> 
</span><span class="cx">         return (trigger, related, repeat, duration)
</span><span class="cx"> 
</span><span class="lines">@@ -1070,7 +1076,7 @@
</span><span class="cx">         @return: a set of strings, one for each unique TZID value.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         results = set()
</span><span class="cx">         for component in self.subcomponents():
</span><span class="lines">@@ -1689,7 +1695,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         @return: the UID of the subcomponents in this component.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         if not hasattr(self, &quot;_resource_uid&quot;):
</span><span class="cx">             for subcomponent in self.subcomponents():
</span><span class="lines">@@ -1706,7 +1712,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Generate a new UID for all components in this VCALENDAR
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         newUID = str(uuid.uuid4()) if newUID is None else newUID
</span><span class="cx">         self._pycalendar.changeUID(self.resourceUID(), newUID)
</span><span class="lines">@@ -1720,7 +1726,7 @@
</span><span class="cx">         @return: the name of the iCalendar type of the subcomponents in this
</span><span class="cx">             component.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         if not hasattr(self, &quot;_resource_type&quot;):
</span><span class="cx">             has_timezone = False
</span><span class="lines">@@ -1738,7 +1744,7 @@
</span><span class="cx">                 if has_timezone:
</span><span class="cx">                     self._resource_type = &quot;VTIMEZONE&quot;
</span><span class="cx">                 else:
</span><del>-                    raise InvalidICalendarDataError(&quot;No component type found for calendar component: %r&quot; % (self,))
</del><ins>+                    raise InvalidICalendarDataError(&quot;No component type found for calendar component: {0!r}&quot;.format(self,))
</ins><span class="cx"> 
</span><span class="cx">         return self._resource_type
</span><span class="cx"> 
</span><span class="lines">@@ -1772,10 +1778,10 @@
</span><span class="cx">             cannot be fixed.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         if self.name() != &quot;VCALENDAR&quot;:
</span><del>-            log.debug(&quot;Not a calendar: %s&quot; % (self,))
</del><ins>+            log.debug(&quot;Not a calendar: {0}&quot;.format(self,))
</ins><span class="cx">             raise InvalidICalendarDataError(&quot;Not a calendar&quot;)
</span><span class="cx">         if not self.resourceType():
</span><del>-            log.debug(&quot;Unknown resource type: %s&quot; % (self,))
</del><ins>+            log.debug(&quot;Unknown resource type: {0}&quot;.format(self,))
</ins><span class="cx">             raise InvalidICalendarDataError(&quot;Unknown resource type&quot;)
</span><span class="cx"> 
</span><span class="cx">         # Do underlying iCalendar library validation with data fix
</span><span class="lines">@@ -1788,11 +1794,11 @@
</span><span class="cx">             unfixed.extend(runfixed)
</span><span class="cx"> 
</span><span class="cx">         if unfixed:
</span><del>-            log.debug(&quot;Calendar data had unfixable problems:\n  %s&quot; % (&quot;\n  &quot;.join(unfixed),))
</del><ins>+            log.debug(&quot;Calendar data had unfixable problems:\n  {0}&quot;.format(&quot;\n  &quot;.join(unfixed),))
</ins><span class="cx">             if doRaise:
</span><del>-                raise InvalidICalendarDataError(&quot;Calendar data had unfixable problems:\n  %s&quot; % (&quot;\n  &quot;.join(unfixed),))
</del><ins>+                raise InvalidICalendarDataError(&quot;Calendar data had unfixable problems:\n  {0}&quot;.format(&quot;\n  &quot;.join(unfixed),))
</ins><span class="cx">         if fixed:
</span><del>-            log.debug(&quot;Calendar data had fixable problems:\n  %s&quot; % (&quot;\n  &quot;.join(fixed),))
</del><ins>+            log.debug(&quot;Calendar data had fixable problems:\n  {0}&quot;.format(&quot;\n  &quot;.join(fixed),))
</ins><span class="cx"> 
</span><span class="cx">         return fixed, unfixed
</span><span class="cx"> 
</span><span class="lines">@@ -1832,9 +1838,9 @@
</span><span class="cx">                             if len(property.value()) &gt; 0:
</span><span class="cx">                                 master.addProperty(property)
</span><span class="cx">                             del exdates[rid]
</span><del>-                            fixed.append(&quot;Removed EXDATE for valid override: %s&quot; % (rid,))
</del><ins>+                            fixed.append(&quot;Removed EXDATE for valid override: {0}&quot;.format(rid,))
</ins><span class="cx">                         else:
</span><del>-                            unfixed.append(&quot;EXDATE for valid override: %s&quot; % (rid,))
</del><ins>+                            unfixed.append(&quot;EXDATE for valid override: {0}&quot;.format(rid,))
</ins><span class="cx"> 
</span><span class="cx">                 # Get the set of all valid recurrence IDs
</span><span class="cx">                 valid_rids = self.validInstances(all_rids, ignoreInvalidInstances=True)
</span><span class="lines">@@ -1855,9 +1861,9 @@
</span><span class="cx">                             exdateValue = exdate.getValue()
</span><span class="cx">                             if exdateValue &lt; dtstart:
</span><span class="cx">                                 if doFix:
</span><del>-                                    fixed.append(&quot;Removed earlier EXDATE: %s&quot; % (exdateValue,))
</del><ins>+                                    fixed.append(&quot;Removed earlier EXDATE: {0}&quot;.format(exdateValue,))
</ins><span class="cx">                                 else:
</span><del>-                                    unfixed.append(&quot;EXDATE earlier than master: %s&quot; % (exdateValue,))
</del><ins>+                                    unfixed.append(&quot;EXDATE earlier than master: {0}&quot;.format(exdateValue,))
</ins><span class="cx">                                 changed = True
</span><span class="cx">                             else:
</span><span class="cx">                                 newValues.append(exdateValue)
</span><span class="lines">@@ -1882,10 +1888,13 @@
</span><span class="cx">                 brokenRID = brokenComponent.propertyValue(&quot;RECURRENCE-ID&quot;)
</span><span class="cx">                 if doFix:
</span><span class="cx">                     master.addProperty(Property(&quot;RDATE&quot;, [brokenRID, ]))
</span><del>-                    fixed.append(&quot;Added RDATE for invalid occurrence: %s&quot; %
-                        (brokenRID,))
</del><ins>+                    fixed.append(
+                        &quot;Added RDATE for invalid occurrence: {0}&quot;.format(
+                            brokenRID,
+                        )
+                    )
</ins><span class="cx">                 else:
</span><del>-                    unfixed.append(&quot;Invalid occurrence: %s&quot; % (brokenRID,))
</del><ins>+                    unfixed.append(&quot;Invalid occurrence: {0}&quot;.format(brokenRID,))
</ins><span class="cx"> 
</span><span class="cx">         return fixed, unfixed
</span><span class="cx"> 
</span><span class="lines">@@ -1926,12 +1935,14 @@
</span><span class="cx">                     ctype = subcomponent.name()
</span><span class="cx">                 else:
</span><span class="cx">                     if ctype != subcomponent.name():
</span><del>-                        msg = &quot;Calendar resources may not contain more than one type of calendar component (%s and %s found)&quot; % (ctype, subcomponent.name())
</del><ins>+                        msg = &quot;Calendar resources may not contain more than one type of calendar component ({0} and {1} found)&quot;.format(
+                            ctype, subcomponent.name()
+                        )
</ins><span class="cx">                         log.debug(msg)
</span><span class="cx">                         raise InvalidICalendarDataError(msg)
</span><span class="cx"> 
</span><span class="cx">                 if ctype not in allowedComponents:
</span><del>-                    msg = &quot;Component type: %s not allowed&quot; % (ctype,)
</del><ins>+                    msg = &quot;Component type: {0} not allowed&quot;.format(ctype,)
</ins><span class="cx">                     log.debug(msg)
</span><span class="cx">                     raise InvalidICalendarDataError(msg)
</span><span class="cx"> 
</span><span class="lines">@@ -1946,14 +1957,18 @@
</span><span class="cx">                 if component_id is None:
</span><span class="cx">                     component_id = uid
</span><span class="cx">                 elif component_id != uid:
</span><del>-                    msg = &quot;Calendar resources may not contain components with different UIDs (%s and %s found)&quot; % (component_id, subcomponent.propertyValue(&quot;UID&quot;))
</del><ins>+                    msg = &quot;Calendar resources may not contain components with different UIDs ({0} and {1} found)&quot;.format(
+                        component_id, subcomponent.propertyValue(&quot;UID&quot;)
+                    )
</ins><span class="cx">                     log.debug(msg)
</span><span class="cx">                     raise InvalidICalendarDataError(msg)
</span><span class="cx"> 
</span><span class="cx">                 # Verify that there is only one master component
</span><span class="cx">                 if rid is None:
</span><span class="cx">                     if got_master:
</span><del>-                        msg = &quot;Calendar resources may not contain components with the same UIDs and no Recurrence-IDs (%s and %s found)&quot; % (component_id, subcomponent.propertyValue(&quot;UID&quot;))
</del><ins>+                        msg = &quot;Calendar resources may not contain components with the same UIDs and no Recurrence-IDs ({0} and {1} found)&quot;.format(
+                            component_id, subcomponent.propertyValue(&quot;UID&quot;)
+                        )
</ins><span class="cx">                         log.debug(msg)
</span><span class="cx">                         raise InvalidICalendarDataError(msg)
</span><span class="cx">                     else:
</span><span class="lines">@@ -1976,7 +1991,7 @@
</span><span class="cx"> 
</span><span class="cx">                 # Check for duplicate RECURRENCE-IDs
</span><span class="cx">                 if rid in component_rids:
</span><del>-                    msg = &quot;Calendar resources may not contain components with the same Recurrence-IDs (%s)&quot; % (rid,)
</del><ins>+                    msg = &quot;Calendar resources may not contain components with the same Recurrence-IDs ({0})&quot;.format(rid,)
</ins><span class="cx">                     log.debug(msg)
</span><span class="cx">                     raise InvalidICalendarDataError(msg)
</span><span class="cx">                 else:
</span><span class="lines">@@ -1990,7 +2005,7 @@
</span><span class="cx">         if not config.EnableTimezonesByReference:
</span><span class="cx">             for timezone_ref in timezone_refs:
</span><span class="cx">                 if timezone_ref not in timezones:
</span><del>-                    msg = &quot;Timezone ID %s is referenced but not defined: %s&quot; % (timezone_ref, self,)
</del><ins>+                    msg = &quot;Timezone ID {0} is referenced but not defined: {1}&quot;.format(timezone_ref, self,)
</ins><span class="cx">                     log.debug(msg)
</span><span class="cx">                     raise InvalidICalendarDataError(msg)
</span><span class="cx"> 
</span><span class="lines">@@ -2002,7 +2017,7 @@
</span><span class="cx">         for timezone in timezones:
</span><span class="cx">             if timezone not in timezone_refs:
</span><span class="cx">                 log.debug(
</span><del>-                    &quot;Timezone %s is not referenced by any non-timezone component&quot; % (timezone,)
</del><ins>+                    &quot;Timezone {0} is not referenced by any non-timezone component&quot;.format(timezone,)
</ins><span class="cx">                 )
</span><span class="cx"> 
</span><span class="cx">         # Control character check - only HTAB, CR, LF allowed for characters in the range 0x00-0x1F
</span><span class="lines">@@ -2025,7 +2040,7 @@
</span><span class="cx">                 if foundOrganizer:
</span><span class="cx">                     if organizer != foundOrganizer:
</span><span class="cx">                         # We have different ORGANIZERs in the same iCalendar object - this is an error
</span><del>-                        msg = &quot;Only one ORGANIZER is allowed in an iCalendar object:\n%s&quot; % (self,)
</del><ins>+                        msg = &quot;Only one ORGANIZER is allowed in an iCalendar object:\n{0}&quot;.format(self,)
</ins><span class="cx">                         log.debug(msg)
</span><span class="cx">                         raise InvalidICalendarDataError(msg)
</span><span class="cx">                 else:
</span><span class="lines">@@ -2322,7 +2337,7 @@
</span><span class="cx">         @return: the string value of the Organizer property, or None
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         # Extract appropriate sub-component if this is a VCALENDAR
</span><span class="cx">         results = []
</span><span class="lines">@@ -2630,7 +2645,7 @@
</span><span class="cx">         @type properties: C{tuple} or C{list}
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        assert from_calendar.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: %r&quot; % (self,)
</del><ins>+        assert from_calendar.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         if self.name() == &quot;VCALENDAR&quot;:
</span><span class="cx">             for component in self.subcomponents():
</span><span class="lines">@@ -2658,7 +2673,7 @@
</span><span class="cx">         on the master to account for changes.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         # Modify any components that reference the attendee, make note of the ones that don't
</span><span class="cx">         remove_components = []
</span><span class="lines">@@ -2704,7 +2719,7 @@
</span><span class="cx">         if not rids or None in rids:
</span><span class="cx">             return True
</span><span class="cx"> 
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         # Remove components not in the list
</span><span class="cx">         components = tuple(self.subcomponents())
</span><span class="lines">@@ -2726,7 +2741,7 @@
</span><span class="cx">         Remove all ATTENDEE properties except for the one specified.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         for component in self.subcomponents():
</span><span class="cx">             if component.name() in ignoredComponents:
</span><span class="lines">@@ -2739,7 +2754,7 @@
</span><span class="cx">         Remove all ATTENDEE properties except for the ones specified.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: %r&quot; % (self,)
</del><ins>+        assert self.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         attendees = set([attendee.lower() for attendee in attendees])
</span><span class="cx"> 
</span><span class="lines">@@ -2753,7 +2768,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Test whether the component has a VALARM as an immediate sub-component.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        assert self.name().upper() in (&quot;VEVENT&quot;, &quot;VTODO&quot;,), &quot;Not a VEVENT or VTODO: %r&quot; % (self,)
</del><ins>+        assert self.name().upper() in (&quot;VEVENT&quot;, &quot;VTODO&quot;,), &quot;Not a VEVENT or VTODO: {0!r}&quot;.format(self,)
</ins><span class="cx"> 
</span><span class="cx">         for component in self.subcomponents():
</span><span class="cx">             if component.name().upper() == &quot;VALARM&quot;:
</span><span class="lines">@@ -2785,9 +2800,9 @@
</span><span class="cx"> DURATION:PT1H
</span><span class="cx"> DTSTAMP:20110427T000000Z
</span><span class="cx"> SUMMARY:bogus
</span><del>-%sEND:VEVENT
</del><ins>+{0}END:VEVENT
</ins><span class="cx"> END:VCALENDAR
</span><del>-&quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;) % (alarm,)
</del><ins>+&quot;&quot;&quot;.replace(&quot;\n&quot;, &quot;\r\n&quot;).format(alarm,)
</ins><span class="cx"> 
</span><span class="cx">         try:
</span><span class="cx">             calendar = Component.fromString(caldata)
</span><span class="lines">@@ -3262,20 +3277,18 @@
</span><span class="cx">                 # Check that we can lookup this calendar user address - if not
</span><span class="cx">                 # we cannot do anything with it
</span><span class="cx">                 cuaddr = normalizeCUAddr(prop.value())
</span><del>-                name, guid, cuaddrs = yield lookupFunction(cuaddr, recordFunction, config)
</del><ins>+                name, guid, cutype, cuaddrs = yield lookupFunction(cuaddr, recordFunction, config)
</ins><span class="cx">                 if guid is None:
</span><span class="cx">                     continue
</span><span class="cx"> 
</span><span class="cx">                 # Get any EMAIL parameter
</span><span class="cx">                 oldemail = prop.parameterValue(&quot;EMAIL&quot;)
</span><span class="cx">                 if oldemail:
</span><del>-                    oldemail = &quot;mailto:%s&quot; % (oldemail,)
</del><ins>+                    oldemail = &quot;mailto:{0}&quot;.format(oldemail,)
</ins><span class="cx"> 
</span><span class="cx">                 # Get any CN parameter
</span><span class="cx">                 oldCN = prop.parameterValue(&quot;CN&quot;)
</span><span class="cx"> 
</span><del>-                cutype = prop.parameterValue(&quot;CUTYPE&quot;)
-
</del><span class="cx">                 if toUUID:
</span><span class="cx">                     # Always re-write value to urn:uuid
</span><span class="cx">                     if isinstance(guid, uuid.UUID):
</span><span class="lines">@@ -3356,11 +3369,58 @@
</span><span class="cx">                     else:
</span><span class="cx">                         prop.removeParameter(&quot;EMAIL&quot;)
</span><span class="cx"> 
</span><ins>+                if cutype == &quot;INDIVIDUAL&quot;:
+                    cutype = None
+
+                if cutype != prop.parameterValue(&quot;CUTYPE&quot;):
+                    if cutype:
+                        prop.setParameter(&quot;CUTYPE&quot;, cutype)
+                    else:
+                        prop.removeParameter(&quot;CUTYPE&quot;)
+
</ins><span class="cx">             # For VPOLL also do immediate children
</span><span class="cx">             if component.name() == &quot;VPOLL&quot;:
</span><span class="cx">                 yield component.normalizeCalendarUserAddresses(lookupFunction, recordFunction, toUUID)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def expandGroupAttendee(self, groupGUID, memberGUIDs, recordFunction):
+
+        memberUUIDs = set([&quot;urn:uuid:&quot; + str(memberGUID) for memberGUID in memberGUIDs])
+        groupUUID = &quot;urn:uuid:&quot; + str(groupGUID)
+        changed = False
+        for component in self.subcomponents():
+            if component.name() in ignoredComponents:
+                continue
+
+            oldAttendeeProps = tuple(component.properties(&quot;ATTENDEE&quot;))
+            oldAttendeeUUIDs = set([attendeeProp.value() for attendeeProp in oldAttendeeProps])
+
+            # add new member attendees
+            for memberUUID in sorted(memberUUIDs - oldAttendeeUUIDs):
+                directoryRecord = yield recordFunction(memberUUID)
+                newAttendeeProp = directoryRecord.attendee(params={&quot;MEMBER&quot;: groupUUID})
+                component.addProperty(newAttendeeProp)
+                changed = True
+
+            # remove attendee or update MEMBER attribute for non-primary attendees in this group,
+            for attendeeProp in oldAttendeeProps:
+                if attendeeProp.hasParameter(&quot;MEMBER&quot;):
+                    parameterValues = tuple(attendeeProp.parameterValues(&quot;MEMBER&quot;))
+                    if groupUUID in parameterValues:
+                        if attendeeProp.value() not in memberUUIDs:
+                            attendeeProp.removeParameterValue(&quot;MEMBER&quot;, groupUUID)
+                            if not attendeeProp.parameterValues(&quot;MEMBER&quot;):
+                                component.removeProperty(attendeeProp)
+                            changed = True
+                    else:
+                        if attendeeProp.value() in memberUUIDs:
+                            attendeeProp.setParameter(&quot;MEMBER&quot;, parameterValues + (groupUUID,))
+                            changed = True
+
+        returnValue(changed)
+
+
</ins><span class="cx">     def allPerUserUIDs(self):
</span><span class="cx"> 
</span><span class="cx">         results = set()
</span><span class="lines">@@ -3484,7 +3544,7 @@
</span><span class="cx">             tzcomp = comp
</span><span class="cx">             break
</span><span class="cx">     else:
</span><del>-        raise InvalidICalendarDataError(&quot;No VTIMEZONE component in %s&quot; % (tzdata,))
</del><ins>+        raise InvalidICalendarDataError(&quot;No VTIMEZONE component in {0}&quot;.format(tzdata,))
</ins><span class="cx"> 
</span><span class="cx">     tzexpanded = tzcomp._pycalendar.expandAll(start, end)
</span><span class="cx"> 
</span><span class="lines">@@ -3529,7 +3589,7 @@
</span><span class="cx">             tzcomp = comp
</span><span class="cx">             break
</span><span class="cx">     else:
</span><del>-        raise InvalidICalendarDataError(&quot;No VTIMEZONE component in %s&quot; % (tzdata,))
</del><ins>+        raise InvalidICalendarDataError(&quot;No VTIMEZONE component in {0}&quot;.format(tzdata,))
</ins><span class="cx"> 
</span><span class="cx">     tzexpanded = tzcomp._pycalendar.expandAll(start, end, with_name=True)
</span><span class="cx"> 
</span><span class="lines">@@ -3566,18 +3626,18 @@
</span><span class="cx"> 
</span><span class="cx"> # #
</span><span class="cx"> # Utilities
</span><del>-# #
</del><ins>+# #p
</ins><span class="cx"> 
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def normalizeCUAddress(cuaddr, lookupFunction, recordFunction, toUUID=True):
</span><span class="cx">     # Check that we can lookup this calendar user address - if not
</span><span class="cx">     # we cannot do anything with it
</span><del>-    _ignore_name, guid, cuaddrs = (yield lookupFunction(normalizeCUAddr(cuaddr), recordFunction, config))
</del><ins>+    _ignore_name, guid, _ignore_cuType, cuaddrs = (yield lookupFunction(normalizeCUAddr(cuaddr), recordFunction, config))
</ins><span class="cx"> 
</span><span class="cx">     if toUUID:
</span><span class="cx">         # Always re-write value to urn:uuid
</span><span class="cx">         if guid:
</span><del>-            returnValue(&quot;urn:uuid:%s&quot; % (guid,))
</del><ins>+            returnValue(&quot;urn:uuid:{0}&quot;.format(guid,))
</ins><span class="cx"> 
</span><span class="cx">     # If it is already a non-UUID address leave it be
</span><span class="cx">     elif cuaddr.startswith(&quot;urn:uuid:&quot;):
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavtesttest_icalendarpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/test/test_icalendar.py (13217 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/test/test_icalendar.py        2014-04-09 05:17:43 UTC (rev 13217)
+++ CalendarServer/trunk/twistedcaldav/test/test_icalendar.py        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -7529,17 +7529,20 @@
</span><span class="cx">                     &quot;urn:uuid:foo&quot; : (
</span><span class="cx">                         &quot;Foo&quot;,
</span><span class="cx">                         &quot;foo&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:foo&quot;, &quot;http://example.com/foo&quot;, &quot;/foo&quot;)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;urn:uuid:bar&quot; : (
</span><span class="cx">                         &quot;Bar&quot;,
</span><span class="cx">                         &quot;bar&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:bar&quot;, &quot;mailto:bar@example.com&quot;, &quot;http://example.com/bar&quot;, &quot;/bar&quot;)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;urn:uuid:baz&quot; : (
</span><span class="cx">                         &quot;Baz&quot;,
</span><span class="cx">                         &quot;baz&quot;,
</span><del>-                        (&quot;urn:uuid:baz&quot;, &quot;http://example.com/baz&quot;)
</del><ins>+                        &quot;INDIVIDUAL&quot;,
+                       (&quot;urn:uuid:baz&quot;, &quot;http://example.com/baz&quot;)
</ins><span class="cx">                     ),
</span><span class="cx">                 }[cuaddr]
</span><span class="cx">             )
</span><span class="lines">@@ -7585,16 +7588,19 @@
</span><span class="cx">                     &quot;/principals/users/foo&quot; : (
</span><span class="cx">                         &quot;Foo&quot;,
</span><span class="cx">                         &quot;foo&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:foo&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;http://example.com/principals/users/bar&quot; : (
</span><span class="cx">                         &quot;Bar&quot;,
</span><span class="cx">                         &quot;bar&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:bar&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;http://example.com/principals/locations/buzz&quot; : (
</span><span class="cx">                         &quot;{Restricted} Buzz&quot;,
</span><span class="cx">                         &quot;buzz&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:buzz&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                 }[cuaddr]
</span><span class="lines">@@ -7641,16 +7647,19 @@
</span><span class="cx">                     &quot;/principals/users/foo&quot; : (
</span><span class="cx">                         &quot;Foo&quot;,
</span><span class="cx">                         &quot;foo&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:foo&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;http://example.com/principals/users/bar&quot; : (
</span><span class="cx">                         &quot;Bar&quot;,
</span><span class="cx">                         &quot;bar&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:bar&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;http://example.com/principals/locations/buzz&quot; : (
</span><span class="cx">                         &quot;{Restricted} Buzz&quot;,
</span><span class="cx">                         &quot;buzz&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:buzz&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                 }[cuaddr]
</span><span class="lines">@@ -7697,16 +7706,19 @@
</span><span class="cx">                     &quot;/principals/users/foo&quot; : (
</span><span class="cx">                         &quot;Foo&quot;,
</span><span class="cx">                         &quot;foo&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:foo&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;http://example.com/principals/users/bar&quot; : (
</span><span class="cx">                         &quot;Bar&quot;,
</span><span class="cx">                         &quot;bar&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:bar&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;http://example.com/principals/locations/buzz&quot; : (
</span><span class="cx">                         &quot;{Restricted} Buzz&quot;,
</span><span class="cx">                         &quot;buzz&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:buzz&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                 }[cuaddr]
</span><span class="lines">@@ -8439,21 +8451,25 @@
</span><span class="cx">                     &quot;urn:uuid:foo&quot; : (
</span><span class="cx">                         &quot;Foo&quot;,
</span><span class="cx">                         &quot;foo&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:foo&quot;, &quot;http://example.com/foo&quot;, &quot;/foo&quot;)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;urn:uuid:bar&quot; : (
</span><span class="cx">                         &quot;Bar&quot;,
</span><span class="cx">                         &quot;bar&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:bar&quot;, &quot;mailto:bar@example.com&quot;, &quot;http://example.com/bar&quot;, &quot;/bar&quot;)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;urn:uuid:baz&quot; : (
</span><span class="cx">                         &quot;Baz&quot;,
</span><span class="cx">                         &quot;baz&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:baz&quot;, &quot;http://example.com/baz&quot;)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;urn:uuid:buz&quot; : (
</span><span class="cx">                         &quot;Buz&quot;,
</span><span class="cx">                         &quot;buz&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:buz&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                 }[cuaddr]
</span><span class="lines">@@ -8483,11 +8499,13 @@
</span><span class="cx">                     &quot;/principals/users/foo&quot; : (
</span><span class="cx">                         &quot;Foo&quot;,
</span><span class="cx">                         &quot;foo&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:foo&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                     &quot;http://example.com/principals/users/buz&quot; : (
</span><span class="cx">                         &quot;Buz&quot;,
</span><span class="cx">                         &quot;buz&quot;,
</span><ins>+                        &quot;INDIVIDUAL&quot;,
</ins><span class="cx">                         (&quot;urn:uuid:buz&quot;,)
</span><span class="cx">                     ),
</span><span class="cx">                 }[cuaddr]
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavtesttest_upgradepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/test/test_upgrade.py (13217 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/test/test_upgrade.py        2014-04-09 05:17:43 UTC (rev 13217)
+++ CalendarServer/trunk/twistedcaldav/test/test_upgrade.py        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -1516,6 +1516,9 @@
</span><span class="cx">                 self.uid = uid
</span><span class="cx">                 self.calendarUserAddresses = cuas
</span><span class="cx"> 
</span><ins>+            def getCUType(self):
+                return &quot;INDIVIDUAL&quot;
+
</ins><span class="cx">             @property
</span><span class="cx">             def displayName(self):
</span><span class="cx">                 return self.fullNames[0]
</span><span class="lines">@@ -1592,7 +1595,6 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-
</del><span class="cx"> normalizeEvent = &quot;&quot;&quot;BEGIN:VCALENDAR
</span><span class="cx"> VERSION:2.0
</span><span class="cx"> BEGIN:VEVENT
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py (13217 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/sql.py        2014-04-09 05:17:43 UTC (rev 13217)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -91,7 +91,7 @@
</span><span class="cx">     ObjectResourceNameNotAllowedError, TooManyObjectResourcesError, \
</span><span class="cx">     InvalidUIDError, UIDExistsError, UIDExistsElsewhereError, \
</span><span class="cx">     InvalidResourceMove, InvalidComponentForStoreError, \
</span><del>-    NoSuchObjectResourceError
</del><ins>+    NoSuchObjectResourceError, AllRetriesFailed
</ins><span class="cx"> from txdav.xml import element
</span><span class="cx"> 
</span><span class="cx"> from txdav.idav import ChangeCategory
</span><span class="lines">@@ -189,9 +189,9 @@
</span><span class="cx">             ).on(txn))
</span><span class="cx">             total = len(rows)
</span><span class="cx">             count = 0
</span><del>-            log.warn(&quot;%d dropbox ids to migrate&quot; % (total,))
</del><ins>+            log.warn(&quot;{0} dropbox ids to migrate&quot;.format(total,))
</ins><span class="cx">         except RuntimeError, e:
</span><del>-            log.error(&quot;Dropbox migration failed when cleaning out dropbox ids: %s&quot; % (e,))
</del><ins>+            log.error(&quot;Dropbox migration failed when cleaning out dropbox ids: {0}&quot;.format(e,))
</ins><span class="cx">             yield txn.abort()
</span><span class="cx">             raise
</span><span class="cx">         else:
</span><span class="lines">@@ -200,7 +200,7 @@
</span><span class="cx">         # For each remaining attachment
</span><span class="cx">         rows = -1
</span><span class="cx">         while rows:
</span><del>-            txn = self._store.newTransaction(&quot;CalendarStoreFeatures.upgradeToManagedAttachments - attachment loop count: %d&quot; % (count,))
</del><ins>+            txn = self._store.newTransaction(&quot;CalendarStoreFeatures.upgradeToManagedAttachments - attachment loop count: {0}&quot;.format(count,))
</ins><span class="cx">             try:
</span><span class="cx">                 dropbox_id = &quot;Batched select&quot;
</span><span class="cx">                 rows = (yield Select(
</span><span class="lines">@@ -214,9 +214,9 @@
</span><span class="cx">                     for dropbox_id in rows:
</span><span class="cx">                         (yield self._upgradeDropbox(txn, dropbox_id))
</span><span class="cx">                     count += len(rows)
</span><del>-                    log.warn(&quot;%d of %d dropbox ids migrated&quot; % (count, total,))
</del><ins>+                    log.warn(&quot;{0} of {1} dropbox ids migrated&quot;.format(count, total,))
</ins><span class="cx">             except RuntimeError, e:
</span><del>-                log.error(&quot;Dropbox migration failed for '%s': %s&quot; % (dropbox_id, e,))
</del><ins>+                log.error(&quot;Dropbox migration failed for '{0}': {1}&quot;.format(dropbox_id, e,))
</ins><span class="cx">                 yield txn.abort()
</span><span class="cx">                 raise
</span><span class="cx">             else:
</span><span class="lines">@@ -236,11 +236,11 @@
</span><span class="cx">         @type dropbox_id: C{str}
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        log.debug(&quot;Processing dropbox id: %s&quot; % (dropbox_id,))
</del><ins>+        log.debug(&quot;Processing dropbox id: {0}&quot;.format(dropbox_id,))
</ins><span class="cx"> 
</span><span class="cx">         # Get all affected calendar objects
</span><span class="cx">         cobjs = (yield self._loadCalendarObjectsForDropboxID(txn, dropbox_id))
</span><del>-        log.debug(&quot;  %d affected calendar objects&quot; % (len(cobjs),))
</del><ins>+        log.debug(&quot;  {0} affected calendar objects&quot;.format(len(cobjs),))
</ins><span class="cx"> 
</span><span class="cx">         # Get names of each matching attachment
</span><span class="cx">         at = schema.ATTACHMENT
</span><span class="lines">@@ -249,18 +249,18 @@
</span><span class="cx">             From=at,
</span><span class="cx">             Where=at.DROPBOX_ID == dropbox_id,
</span><span class="cx">         ).on(txn))
</span><del>-        log.debug(&quot;  %d associated attachment objects&quot; % (len(names),))
</del><ins>+        log.debug(&quot;  {0} associated attachment objects&quot;.format(len(names),))
</ins><span class="cx"> 
</span><span class="cx">         # For each attachment, update each calendar object
</span><span class="cx">         for name in names:
</span><span class="cx">             name = name[0]
</span><del>-            log.debug(&quot;  processing attachment object: %s&quot; % (name,))
</del><ins>+            log.debug(&quot;  processing attachment object: {0}&quot;.format(name,))
</ins><span class="cx">             attachment = (yield DropBoxAttachment.load(txn, dropbox_id, name))
</span><span class="cx"> 
</span><span class="cx">             # Check for orphans
</span><span class="cx">             if len(cobjs) == 0:
</span><span class="cx">                 # Just remove the attachment
</span><del>-                log.warn(&quot;Orphaned dropbox id removed: %s&quot; % (attachment._path,))
</del><ins>+                log.warn(&quot;Orphaned dropbox id removed: {0}&quot;.format(attachment._path,))
</ins><span class="cx">                 yield attachment.remove()
</span><span class="cx">                 continue
</span><span class="cx"> 
</span><span class="lines">@@ -271,35 +271,35 @@
</span><span class="cx">                 if cobj._parentCollection.ownerHome()._resourceID == attachment._ownerHomeID:
</span><span class="cx">                     owners.append(cobj)
</span><span class="cx">                 cobj_by_UID[cobj.uid()].append(cobj)
</span><del>-            log.debug(&quot;    %d owner calendar objects&quot; % (len(owners),))
-            log.debug(&quot;    %d UIDs&quot; % (len(cobj_by_UID),))
-            log.debug(&quot;    %d total calendar objects&quot; % (sum([len(items) for items in cobj_by_UID.values()]),))
</del><ins>+            log.debug(&quot;    {0} owner calendar objects&quot;.format(len(owners),))
+            log.debug(&quot;    {0} UIDs&quot;.format(len(cobj_by_UID),))
+            log.debug(&quot;    {0} total calendar objects&quot;.format(sum([len(items) for items in cobj_by_UID.values()]),))
</ins><span class="cx"> 
</span><span class="cx">             if owners:
</span><span class="cx">                 # Create the managed attachment without references to calendar objects.
</span><span class="cx">                 managed = (yield attachment.convertToManaged())
</span><del>-                log.debug(&quot;    converted attachment: %r&quot; % (attachment,))
</del><ins>+                log.debug(&quot;    converted attachment: {0!r}&quot;.format(attachment,))
</ins><span class="cx"> 
</span><span class="cx">                 # Do conversion for each owner object
</span><span class="cx">                 for owner_obj in owners:
</span><span class="cx"> 
</span><span class="cx">                     # Add a reference to the managed attachment
</span><span class="cx">                     mattachment = (yield managed.newReference(owner_obj._resourceID))
</span><del>-                    log.debug(&quot;    added reference for: %r&quot; % (owner_obj,))
</del><ins>+                    log.debug(&quot;    added reference for: {0!r}&quot;.format(owner_obj,))
</ins><span class="cx"> 
</span><span class="cx">                     # Rewrite calendar data
</span><span class="cx">                     for cobj in cobj_by_UID[owner_obj.uid()]:
</span><span class="cx">                         (yield cobj.convertAttachments(attachment, mattachment))
</span><del>-                        log.debug(&quot;    re-wrote calendar object: %r&quot; % (cobj,))
</del><ins>+                        log.debug(&quot;    re-wrote calendar object: {0!r}&quot;.format(cobj,))
</ins><span class="cx">             else:
</span><span class="cx">                 # TODO: look for cobjs that were not changed and remove their ATTACH properties.
</span><span class="cx">                 # These could happen if the owner object no longer exists.
</span><span class="cx">                 # For now just remove the attachment
</span><del>-                log.warn(&quot;Unowned dropbox id removed: %s&quot; % (attachment._path,))
</del><ins>+                log.warn(&quot;Unowned dropbox id removed: {0}&quot;.format(attachment._path,))
</ins><span class="cx">                 yield attachment.remove()
</span><span class="cx">                 continue
</span><span class="cx"> 
</span><del>-        log.debug(&quot;  finished dropbox id: %s&quot; % (dropbox_id,))
</del><ins>+        log.debug(&quot;  finished dropbox id: {0}&quot;.format(dropbox_id,))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -679,7 +679,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # Make sure the loop does not operate on any new calendars created during the loop
</span><del>-        self.log.warn(&quot;Splitting calendars for user %s&quot; % (self._ownerUID,))
</del><ins>+        self.log.warn(&quot;Splitting calendars for user {0}&quot;.format(self._ownerUID,))
</ins><span class="cx">         calendars = yield self.calendars()
</span><span class="cx">         for calendar in calendars:
</span><span class="cx"> 
</span><span class="lines">@@ -687,7 +687,7 @@
</span><span class="cx">             if calendar.isInbox():
</span><span class="cx">                 continue
</span><span class="cx">             split_count = yield calendar.splitCollectionByComponentTypes()
</span><del>-            self.log.warn(&quot;  Calendar: '%s', split into %d&quot; % (calendar.name(), split_count + 1,))
</del><ins>+            self.log.warn(&quot;  Calendar: '{0}', split into {1}&quot;.format(calendar.name(), split_count + 1,))
</ins><span class="cx"> 
</span><span class="cx">         yield self.ensureDefaultCalendarsExist()
</span><span class="cx"> 
</span><span class="lines">@@ -1569,7 +1569,7 @@
</span><span class="cx"> 
</span><span class="cx">         # Actually expand recurrence max
</span><span class="cx">         for name in names:
</span><del>-            self.log.info(&quot;Search falls outside range of index for %s %s to %s&quot; % (name, minDate, maxDate))
</del><ins>+            self.log.info(&quot;Search falls outside range of index for {0} {1} to {2}&quot;.format(name, minDate, maxDate))
</ins><span class="cx">             yield self.reExpandResource(name, minDate, maxDate)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -1642,12 +1642,12 @@
</span><span class="cx"> 
</span><span class="cx">         # Create the new calendar
</span><span class="cx">         try:
</span><del>-            newcalendar = yield self._home.createCalendarWithName(&quot;%s-%s&quot; % (self._name, component.lower(),))
</del><ins>+            newcalendar = yield self._home.createCalendarWithName(&quot;{0}-{1}&quot;.format(self._name, component.lower(),))
</ins><span class="cx">         except HomeChildNameAlreadyExistsError:
</span><span class="cx">             # If the name we want exists, try repeating with up to ten more
</span><span class="cx">             for ctr in range(10):
</span><span class="cx">                 try:
</span><del>-                    newcalendar = yield self._home.createCalendarWithName(&quot;%s-%s-%d&quot; % (self._name, component.lower(), ctr + 1,))
</del><ins>+                    newcalendar = yield self._home.createCalendarWithName(&quot;{0}-{1}-[2}&quot;.format(self._name, component.lower(), ctr + 1,))
</ins><span class="cx">                 except HomeChildNameAlreadyExistsError:
</span><span class="cx">                     continue
</span><span class="cx">             else:
</span><span class="lines">@@ -1694,7 +1694,7 @@
</span><span class="cx">         for row in rows:
</span><span class="cx">             columnMap = dict(zip(columns, row))
</span><span class="cx">             columnMap[cb.CALENDAR_RESOURCE_ID] = newcalendar._resourceID
</span><del>-            columnMap[cb.CALENDAR_RESOURCE_NAME] = &quot;%s-%s&quot; % (columnMap[cb.CALENDAR_RESOURCE_NAME], component.lower(),)
</del><ins>+            columnMap[cb.CALENDAR_RESOURCE_NAME] = &quot;{0}-{1}&quot;.format(columnMap[cb.CALENDAR_RESOURCE_NAME], component.lower(),)
</ins><span class="cx">             yield Insert(columnMap).on(self._txn)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -1908,27 +1908,73 @@
</span><span class="cx"> 
</span><span class="cx">             # Valid calendar component for check
</span><span class="cx">             if not self.calendar().isSupportedComponent(component.mainType()):
</span><del>-                raise InvalidComponentTypeError(&quot;Invalid component type %s for calendar: %s&quot; % (component.mainType(), self.calendar(),))
</del><ins>+                raise InvalidComponentTypeError(&quot;Invalid component type {0} for calendar: {1}&quot;.format(component.mainType(), self.calendar(),))
</ins><span class="cx"> 
</span><del>-            # Valid attendee list size check
-            yield self.validAttendeeListSizeCheck(component, inserting)
-
</del><span class="cx">             # Normalize the calendar user addresses once we know we have valid
</span><span class="cx">             # calendar data
</span><span class="cx">             yield component.normalizeCalendarUserAddresses(normalizationLookup, self.directoryService().recordWithCalendarUserAddress)
</span><span class="cx"> 
</span><ins>+            # Expand groups
+            yield self.expandGroupAttendees(component)
+
+            # Valid attendee list size check
+            yield self.validAttendeeListSizeCheck(component, inserting)
+
</ins><span class="cx">         # Possible timezone stripping
</span><span class="cx">         if config.EnableTimezonesByReference:
</span><span class="cx">             component.stripKnownTimezones()
</span><span class="cx"> 
</span><span class="cx">         # Check location/resource organizer requirement
</span><del>-        yield self.validLocationResourceOrganizer(component, inserting, internal_state)
</del><ins>+        self.validLocationResourceOrganizer(component, inserting, internal_state)
</ins><span class="cx"> 
</span><span class="cx">         # Check access
</span><span class="cx">         if config.EnablePrivateEvents:
</span><span class="cx">             self.validAccess(component, inserting, internal_state)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def expandGroupAttendees(self, component):
+        &quot;&quot;&quot;
+        Expand group attendees
+        &quot;&quot;&quot;
+
+        if not config.Scheduling.Options.AllowGroupAsAttendee:
+            return
+
+        attendeeProps = component.getAllAttendeeProperties()
+        groupGUIDs = set([
+            uuid.UUID(attendeeProp.value()[len(&quot;urn:uuid:&quot;):]) for attendeeProp in attendeeProps
+            if attendeeProp.parameterValue(&quot;CUTYPE&quot;) == &quot;GROUP&quot;
+        ])
+
+        for groupGUID in groupGUIDs:
+
+            groupRecord = yield self.directoryService().recordWithGUID(groupGUID)
+            if groupRecord:
+                members = yield groupRecord.expandedMembers()
+                memberGUIDs = sorted([member.guid for member in members])
+
+                membershipHashContent = hashlib.md5()
+                for memberGUID in memberGUIDs:
+                    membershipHashContent.update(str(memberGUID))
+                membershipHash = membershipHashContent.hexdigest()
+
+                # associate group ID with self
+                groupID, _ignore_name, membershipHash, _ignore_modDate = yield self._txn.groupByUID(str(groupGUID))
+                try:
+                    groupAttendee = schema.GROUP_ATTENDEE
+                    yield Insert({
+                        groupAttendee.RESOURCE_ID: self._resourceID,
+                        groupAttendee.GROUP_ID: groupID,
+                        groupAttendee.MEMBERSHIP_HASH: membershipHash,
+                    })
+                except AllRetriesFailed:
+                    pass
+
+                # get members
+                yield component.expandGroupAttendee(groupGUID, memberGUIDs, self.directoryService().recordWithCalendarUserAddress)
+
+
</ins><span class="cx">     def validCalendarDataCheck(self, component, inserting):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Check that the calendar data is valid iCalendar.
</span><span class="lines">@@ -1938,7 +1984,7 @@
</span><span class="cx"> 
</span><span class="cx">         # Valid calendar data checks
</span><span class="cx">         if not isinstance(component, VComponent):
</span><del>-            raise InvalidObjectResourceError(&quot;Wrong type of object: %s&quot; % (type(component),))
</del><ins>+            raise InvalidObjectResourceError(&quot;Wrong type of object: {0}&quot;.format(type(component),))
</ins><span class="cx"> 
</span><span class="cx">         try:
</span><span class="cx">             component.validCalendarData(validateRecurrences=self._txn._migrating)
</span><span class="lines">@@ -1983,7 +2029,11 @@
</span><span class="cx">                     oldAttendeeListLength = 0
</span><span class="cx"> 
</span><span class="cx">                 if attendeeListLength &gt; oldAttendeeListLength:
</span><del>-                    raise TooManyAttendeesError(&quot;Attendee list size %d is larger than allowed limit %d&quot; % (attendeeListLength, config.MaxAttendeesPerInstance))
</del><ins>+                    raise TooManyAttendeesError(
+                        &quot;Attendee list size {0} is larger than allowed limit {1}&quot;.format(
+                            attendeeListLength, config.MaxAttendeesPerInstance
+                        )
+                )
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -2001,7 +2051,7 @@
</span><span class="cx">             if organizer is None and (
</span><span class="cx">                 cutype == &quot;ROOM&quot; and not config.Scheduling.Options.AllowLocationWithoutOrganizer or
</span><span class="cx">                 cutype == &quot;RESOURCE&quot; and not config.Scheduling.Options.AllowResourceWithoutOrganizer):
</span><del>-                raise ValidOrganizerError(&quot;Organizer required in calendar data for a %s&quot; % (cutype.lower(),))
</del><ins>+                raise ValidOrganizerError(&quot;Organizer required in calendar data for a {0}&quot;.format(cutype.lower(),))
</ins><span class="cx"> 
</span><span class="cx">             # Check for tracking the modifier
</span><span class="cx">             if organizer is None and (
</span><span class="lines">@@ -2294,11 +2344,11 @@
</span><span class="cx">                                         &quot;X-TITLE&quot;: title,
</span><span class="cx">                                     }
</span><span class="cx">                                     structured = Property(&quot;X-APPLE-STRUCTURED-LOCATION&quot;,
</span><del>-                                        &quot;geo:%s&quot; % (geo.encode(&quot;utf-8&quot;),), params=params,
</del><ins>+                                        &quot;geo:{0}&quot;.format(geo.encode(&quot;utf-8&quot;),), params=params,
</ins><span class="cx">                                         valuetype=Value.VALUETYPE_URI)
</span><span class="cx">                                     sub.replaceProperty(structured)
</span><span class="cx">                                     newLocProperty = Property(&quot;LOCATION&quot;,
</span><del>-                                        &quot;%s\n%s&quot; % (title, street.encode(&quot;utf-8&quot;)))
</del><ins>+                                        &quot;{0}\n{1}&quot;.format(title, street.encode(&quot;utf-8&quot;)))
</ins><span class="cx">                                     sub.replaceProperty(newLocProperty)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -2435,7 +2485,7 @@
</span><span class="cx"> 
</span><span class="cx">         new_uid = component.resourceUID()
</span><span class="cx">         if internal_state == ComponentUpdateState.NORMAL:
</span><del>-            yield NamedLock.acquire(self._txn, &quot;ImplicitUIDLock:%s&quot; % (hashlib.md5(new_uid).hexdigest(),))
</del><ins>+            yield NamedLock.acquire(self._txn, &quot;ImplicitUIDLock:{0}&quot;.format(hashlib.md5(new_uid).hexdigest(),))
</ins><span class="cx"> 
</span><span class="cx">         # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
</span><span class="cx">         # try to write the same calendar data to two different resource URIs.
</span><span class="lines">@@ -2452,7 +2502,7 @@
</span><span class="cx">                     if elsewhere.calendar().id() == self.calendar().id():
</span><span class="cx">                         raise UIDExistsError(&quot;UID already exists in same calendar.&quot;)
</span><span class="cx">                     else:
</span><del>-                        raise UIDExistsElsewhereError(&quot;UID already exists in different calendar: %s.&quot; % (elsewhere.calendar().name(),))
</del><ins>+                        raise UIDExistsElsewhereError(&quot;UID already exists in different calendar: {0}&quot;.format(elsewhere.calendar().name(),))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -2555,10 +2605,10 @@
</span><span class="cx">                         yield self._removeInternal(internal_state=ComponentRemoveState.INTERNAL)
</span><span class="cx">                         raise ResourceDeletedError(&quot;Resource modified but immediately deleted by the server.&quot;)
</span><span class="cx">                     else:
</span><del>-                        raise AttendeeAllowedError(&quot;Attendee cannot create event for Organizer: %s&quot; % (implicit_result,))
</del><ins>+                        raise AttendeeAllowedError(&quot;Attendee cannot create event for Organizer: {0}&quot;.format(implicit_result,))
</ins><span class="cx"> 
</span><span class="cx">                 else:
</span><del>-                    msg = &quot;Invalid return status code from ImplicitScheduler: %s&quot; % (implicit_result,)
</del><ins>+                    msg = &quot;Invalid return status code from ImplicitScheduler: {0}&quot;.format(implicit_result,)
</ins><span class="cx">                     log.error(msg)
</span><span class="cx">                     raise InvalidObjectResourceError(msg)
</span><span class="cx">             else:
</span><span class="lines">@@ -2700,8 +2750,9 @@
</span><span class="cx">                 recurrenceLimit = instances.limit
</span><span class="cx">                 recurrenceLowerLimit = instances.lowerLimit
</span><span class="cx">             except InvalidOverriddenInstanceError, e:
</span><del>-                self.log.error(&quot;Invalid instance %s when indexing %s in %s&quot; %
-                               (e.rid, self._name, self._calendar,))
</del><ins>+                self.log.error(&quot;Invalid instance {0} when indexing {1} in {2}&quot;.format(
+                    e.rid, self._name, self._calendar,)
+                )
</ins><span class="cx"> 
</span><span class="cx">                 if txn._migrating:
</span><span class="cx">                     # TODO: fix the data here by re-writing component then re-index
</span><span class="lines">@@ -2924,20 +2975,27 @@
</span><span class="cx">             except InvalidICalendarDataError, e:
</span><span class="cx">                 # This is a really bad situation, so do raise
</span><span class="cx">                 raise InternalDataStoreError(
</span><del>-                    &quot;Data corruption detected (%s) in id: %s&quot;
-                    % (e, self._resourceID)
</del><ins>+                    &quot;Data corruption detected ({0}) in id: {1}&quot;.format(
+                        e, self._resourceID
+                    )
</ins><span class="cx">                 )
</span><span class="cx"> 
</span><span class="cx">             # Fix any bogus data we can
</span><span class="cx">             fixed, unfixed = component.validCalendarData(doFix=True, doRaise=False)
</span><span class="cx"> 
</span><span class="cx">             if unfixed:
</span><del>-                self.log.error(&quot;Calendar data id=%s had unfixable problems:\n  %s&quot; %
-                               (self._resourceID, &quot;\n  &quot;.join(unfixed),))
</del><ins>+                self.log.error(
+                    &quot;Calendar data id={0} had unfixable problems:\n  {1}&quot;.format(
+                        self._resourceID, &quot;\n  &quot;.join(unfixed),
+                        )
+                    )
</ins><span class="cx"> 
</span><span class="cx">             if fixed:
</span><del>-                self.log.error(&quot;Calendar data id=%s had fixable problems:\n  %s&quot; %
-                               (self._resourceID, &quot;\n  &quot;.join(fixed),))
</del><ins>+                self.log.error(
+                    &quot;Calendar data id={0} had fixable problems:\n  {1}&quot;.format(
+                        self._resourceID, &quot;\n  &quot;.join(fixed),
+                    )
+                )
</ins><span class="cx"> 
</span><span class="cx">             self._cachedComponent = component
</span><span class="cx">             self._cachedCommponentPerUser = {}
</span><span class="lines">@@ -3028,7 +3086,7 @@
</span><span class="cx">                 internal_request=(internal_state != ComponentUpdateState.NORMAL),
</span><span class="cx">             ))
</span><span class="cx">             if do_implicit_action:
</span><del>-                yield NamedLock.acquire(self._txn, &quot;ImplicitUIDLock:%s&quot; % (hashlib.md5(calendar.resourceUID()).hexdigest(),))
</del><ins>+                yield NamedLock.acquire(self._txn, &quot;ImplicitUIDLock:{0}&quot;.format(hashlib.md5(calendar.resourceUID()).hexdigest(),))
</ins><span class="cx"> 
</span><span class="cx">         # Need to also remove attachments
</span><span class="cx">         if internal_state != ComponentRemoveState.INTERNAL:
</span><span class="lines">@@ -3386,7 +3444,7 @@
</span><span class="cx">             t = attachment.store(content_type, filename)
</span><span class="cx">             yield readStream(stream, t.write)
</span><span class="cx">         except Exception, e:
</span><del>-            self.log.error(&quot;Unable to store attachment: %s&quot; % (e,))
</del><ins>+            self.log.error(&quot;Unable to store attachment: {0}&quot;.format(e,))
</ins><span class="cx">             raise AttachmentStoreFailed
</span><span class="cx">         yield t.loseConnection()
</span><span class="cx"> 
</span><span class="lines">@@ -3444,7 +3502,7 @@
</span><span class="cx">             # Check that this is a proper update
</span><span class="cx">             oldattachment = (yield self.attachmentWithManagedID(managed_id))
</span><span class="cx">             if oldattachment is None:
</span><del>-                self.log.error(&quot;Missing managed attachment even though ATTACHMENT_CALENDAR_OBJECT indicates it is present: %s&quot; % (managed_id,))
</del><ins>+                self.log.error(&quot;Missing managed attachment even though ATTACHMENT_CALENDAR_OBJECT indicates it is present: {0}&quot;.format(managed_id,))
</ins><span class="cx">                 raise AttachmentStoreFailed
</span><span class="cx"> 
</span><span class="cx">             # We actually create a brand new attachment object for the update, but with the same managed-id. That way, other resources
</span><span class="lines">@@ -3453,7 +3511,7 @@
</span><span class="cx">             t = attachment.store(content_type, filename)
</span><span class="cx">             yield readStream(stream, t.write)
</span><span class="cx">         except Exception, e:
</span><del>-            self.log.error(&quot;Unable to store attachment: %s&quot; % (e,))
</del><ins>+            self.log.error(&quot;Unable to store attachment: {0}&quot;.format(e,))
</ins><span class="cx">             raise AttachmentStoreFailed
</span><span class="cx">         yield t.loseConnection()
</span><span class="cx"> 
</span><span class="lines">@@ -3535,7 +3593,7 @@
</span><span class="cx">             attachments = component.properties(&quot;ATTACH&quot;)
</span><span class="cx">             removed = False
</span><span class="cx">             for attachment in tuple(attachments):
</span><del>-                if attachment.value().endswith(&quot;/dropbox/%s/%s&quot; % (
</del><ins>+                if attachment.value().endswith(&quot;/dropbox/{0}/{1}&quot;.format(
</ins><span class="cx">                     urllib.quote(oldattachment.dropboxID()),
</span><span class="cx">                     urllib.quote(oldattachment.name()),
</span><span class="cx">                 )):
</span><span class="lines">@@ -3800,7 +3858,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # First job is to grab a UID lock on this entire series of events
</span><del>-        yield NamedLock.acquire(self._txn, &quot;ImplicitUIDLock:%s&quot; % (hashlib.md5(self._uid).hexdigest(),))
</del><ins>+        yield NamedLock.acquire(self._txn, &quot;ImplicitUIDLock:{0}&quot;.format(hashlib.md5(self._uid).hexdigest(),))
</ins><span class="cx"> 
</span><span class="cx">         # Find all other calendar objects on this server with the same UID
</span><span class="cx">         if onlyThis:
</span><span class="lines">@@ -3832,7 +3890,12 @@
</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>-        olderObject = 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;{0}.ics&quot;.format(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">@@ -3863,12 +3926,12 @@
</span><span class="cx"> 
</span><span class="cx">         # Create a new resource and store its data (but not if the parent is &quot;inbox&quot;, or if it is empty)
</span><span class="cx">         if not self.calendar().isInbox() and ical_old.mainType() is not None:
</span><del>-            yield self.calendar()._createCalendarObjectWithNameInternal(&quot;%s.ics&quot; % (olderUID,), ical_old, ComponentUpdateState.SPLIT_ATTENDEE)
</del><ins>+            yield self.calendar()._createCalendarObjectWithNameInternal(&quot;{0}.ics&quot;.format(olderUID,), ical_old, ComponentUpdateState.SPLIT_ATTENDEE)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     class CalendarObjectSplitterWork(WorkItem, fromTable(schema.CALENDAR_OBJECT_SPLITTER_WORK)):
</span><span class="cx"> 
</span><del>-        group = property(lambda self: &quot;CalendarObjectSplitterWork:%s&quot; % (self.resourceID,))
</del><ins>+        group = property(lambda self: &quot;CalendarObjectSplitterWork:{0}&quot;.format(self.resourceID,))
</ins><span class="cx"> 
</span><span class="cx">         @inlineCallbacks
</span><span class="cx">         def doWork(self):
</span><span class="lines">@@ -4013,7 +4076,10 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def __repr__(self):
</span><del>-        return &quot;&lt;%s: %s&gt;&quot; % (self.__class__.__name__, self._attachmentID)
</del><ins>+        return (
+            &quot;&lt;{self.__class__.__name__}: {self._attachmentID}&gt;&quot;
+            .format(self=self)
+        )
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def _attachmentPathRoot(self):
</span><span class="lines">@@ -4677,7 +4743,7 @@
</span><span class="cx">         splits = name.rsplit(&quot;.&quot;, 1)
</span><span class="cx">         fname = splits[0]
</span><span class="cx">         suffix = splits[1] if len(splits) == 2 else &quot;unknown&quot;
</span><del>-        return &quot;%s-%s.%s&quot; % (fname, managed_id[:8], suffix)
</del><ins>+        return &quot;{0}-{1}.{2}&quot;.format(fname, managed_id[:8], suffix)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -4770,7 +4836,7 @@
</span><span class="cx">         location = (yield self.location())
</span><span class="cx"> 
</span><span class="cx">         attach.setParameter(&quot;MANAGED-ID&quot;, self.managedID())
</span><del>-        attach.setParameter(&quot;FMTTYPE&quot;, &quot;%s/%s&quot; % (self.contentType().mediaType, self.contentType().mediaSubtype))
</del><ins>+        attach.setParameter(&quot;FMTTYPE&quot;, &quot;{0}/{1}&quot;.format(self.contentType().mediaType, self.contentType().mediaSubtype))
</ins><span class="cx">         attach.setParameter(&quot;FILENAME&quot;, self.name())
</span><span class="cx">         attach.setParameter(&quot;SIZE&quot;, str(self.size()))
</span><span class="cx">         attach.setValue(location)
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoretestutilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/test/util.py (13217 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/test/util.py        2014-04-09 05:17:43 UTC (rev 13217)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/util.py        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -78,7 +78,6 @@
</span><span class="cx">         self.geographicLocation = geographicLocation
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-
</del><span class="cx">     def canonicalCalendarUserAddress(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">             Return a CUA for this record, preferring in this order:
</span><span class="lines">@@ -177,22 +176,26 @@
</span><span class="cx">     # Structured Locations
</span><span class="cx">     directory.addRecord(TestCalendarStoreDirectoryRecord(
</span><span class="cx">         &quot;il1&quot;, (&quot;il1&quot;,), &quot;1 Infinite Loop&quot;, [],
</span><ins>+        cutype=&quot;ROOM&quot;,
</ins><span class="cx">         geographicLocation=&quot;37.331741,-122.030333&quot;,
</span><span class="cx">         streetAddress=&quot;1 Infinite Loop, Cupertino, CA 95014&quot;
</span><span class="cx">     ))
</span><span class="cx">     directory.addRecord(TestCalendarStoreDirectoryRecord(
</span><span class="cx">         &quot;il2&quot;, (&quot;il2&quot;,), &quot;2 Infinite Loop&quot;, [],
</span><ins>+        cutype=&quot;ROOM&quot;,
</ins><span class="cx">         geographicLocation=&quot;37.332633,-122.030502&quot;,
</span><span class="cx">         streetAddress=&quot;2 Infinite Loop, Cupertino, CA 95014&quot;
</span><span class="cx">     ))
</span><span class="cx">     directory.addRecord(TestCalendarStoreDirectoryRecord(
</span><span class="cx">         &quot;room1&quot;, (&quot;room1&quot;,), &quot;Conference Room One&quot;,
</span><span class="cx">         frozenset((&quot;urn:uuid:room1&quot;,)),
</span><ins>+        cutype=&quot;ROOM&quot;,
</ins><span class="cx">         associatedAddress=&quot;il1&quot;,
</span><span class="cx">     ))
</span><span class="cx">     directory.addRecord(TestCalendarStoreDirectoryRecord(
</span><span class="cx">         &quot;room2&quot;, (&quot;room2&quot;,), &quot;Conference Room Two&quot;,
</span><span class="cx">         frozenset((&quot;urn:uuid:room2&quot;,)),
</span><ins>+        cutype=&quot;ROOM&quot;,
</ins><span class="cx">         associatedAddress=&quot;il2&quot;,
</span><span class="cx">     ))
</span><span class="cx"> 
</span><span class="lines">@@ -206,8 +209,8 @@
</span><span class="cx">         (uid,),
</span><span class="cx">         uid.capitalize(),
</span><span class="cx">         frozenset((
</span><del>-            &quot;urn:uuid:%s&quot; % (uid,),
-            &quot;mailto:%s@example.com&quot; % (uid,),
</del><ins>+            &quot;urn:uuid:{0}&quot;.format(uid,),
+            &quot;mailto:{0}@example.com&quot;.format(uid,),
</ins><span class="cx">         )),
</span><span class="cx">     )
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreutilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/util.py (13217 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/util.py        2014-04-09 05:17:43 UTC (rev 13217)
+++ CalendarServer/trunk/txdav/caldav/datastore/util.py        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -104,9 +104,9 @@
</span><span class="cx"> def normalizationLookup(cuaddr, recordFunction, config):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Lookup function to be passed to ical.normalizeCalendarUserAddresses.
</span><del>-    Returns a tuple of (Full name C{str}, guid C{UUID}, and calendar user address list C{str})
-    for the given cuaddr.  The recordFunction is called to retrieve the
-    record for the cuaddr.
</del><ins>+    Returns a tuple of (Full name C{str}, guid C{UUID}, cudtype C{str}, and
+    calendar user address list C{str}) for the given cuaddr.
+    recordFunction is called to retrieve the record for the cuaddr.
</ins><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     try:
</span><span class="cx">         record = yield recordFunction(cuaddr)
</span><span class="lines">@@ -115,7 +115,7 @@
</span><span class="cx">         record = None
</span><span class="cx"> 
</span><span class="cx">     if record is None:
</span><del>-        returnValue((None, None, None))
</del><ins>+        returnValue((None, None, None, None))
</ins><span class="cx">     else:
</span><span class="cx"> 
</span><span class="cx">         # RFC5545 syntax does not allow backslash escaping in
</span><span class="lines">@@ -132,7 +132,7 @@
</span><span class="cx">         except AttributeError:
</span><span class="cx">             guid = None
</span><span class="cx"> 
</span><del>-        returnValue((fullName, guid, cuas))
</del><ins>+        returnValue((fullName, guid, record.getCUType(), cuas))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavwhodirectorypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/who/directory.py (13217 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/directory.py        2014-04-09 05:17:43 UTC (rev 13217)
+++ CalendarServer/trunk/txdav/who/directory.py        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -27,6 +27,8 @@
</span><span class="cx"> from twext.who.idirectory import RecordType as BaseRecordType
</span><span class="cx"> from twisted.cred.credentials import UsernamePassword
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><ins>+from twistedcaldav.config import config
+from twistedcaldav.ical import Property
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
</span><span class="cx"> from txdav.who.delegates import RecordType as DelegateRecordType
</span><span class="cx"> from txdav.who.idirectory import (
</span><span class="lines">@@ -75,10 +77,7 @@
</span><span class="cx">             record = yield self.recordWithGUID(guid)
</span><span class="cx">         elif address.startswith(&quot;mailto:&quot;):
</span><span class="cx">             records = yield self.recordsWithEmailAddress(address[7:])
</span><del>-            if records:
-                record = records[0]
-            else:
-                returnValue(None)
</del><ins>+            record = records[0] if records else None
</ins><span class="cx">         elif address.startswith(&quot;/principals/&quot;):
</span><span class="cx">             parts = address.split(&quot;/&quot;)
</span><span class="cx">             if len(parts) == 4:
</span><span class="lines">@@ -90,9 +89,16 @@
</span><span class="cx">                     recordType = self.oldNameToRecordType(parts[2])
</span><span class="cx">                     record = yield self.recordWithShortName(recordType, parts[3])
</span><span class="cx"> 
</span><del>-        returnValue(record if record and record.hasCalendars else None)
</del><ins>+        if record:
+            if record.hasCalendars or (
+                config.Scheduling.Options.AllowGroupAsAttendee and
+                record.recordType == BaseRecordType.group
+            ):
+                returnValue(record)
</ins><span class="cx"> 
</span><ins>+        returnValue(None)
</ins><span class="cx"> 
</span><ins>+
</ins><span class="cx">     def recordsMatchingTokens(self, tokens, context=None, limitResults=50,
</span><span class="cx">                               timeoutSeconds=10):
</span><span class="cx">         fields = [
</span><span class="lines">@@ -178,7 +184,6 @@
</span><span class="cx">             )
</span><span class="cx">         return self.recordsFromExpression(expression)
</span><span class="cx"> 
</span><del>-
</del><span class="cx">     _oldRecordTypeNames = {
</span><span class="cx">         &quot;address&quot;: &quot;addresses&quot;,
</span><span class="cx">         &quot;group&quot;: &quot;groups&quot;,
</span><span class="lines">@@ -199,6 +204,7 @@
</span><span class="cx">     def recordTypeToOldName(self, recordType):
</span><span class="cx">         return self._oldRecordTypeNames[recordType.name]
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx">     def oldNameToRecordType(self, oldName):
</span><span class="cx">         for name, value in self._oldRecordTypeNames.iteritems():
</span><span class="cx">             if oldName == value:
</span><span class="lines">@@ -213,7 +219,6 @@
</span><span class="cx">     class
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-
</del><span class="cx">     @inlineCallbacks
</span><span class="cx">     def verifyCredentials(self, credentials):
</span><span class="cx"> 
</span><span class="lines">@@ -244,14 +249,19 @@
</span><span class="cx">     @property
</span><span class="cx">     def calendarUserAddresses(self):
</span><span class="cx">         try:
</span><del>-            if not self.hasCalendars:
</del><ins>+            if not (
+                self.hasCalendars or (
+                    config.Scheduling.Options.AllowGroupAsAttendee and
+                    self.recordType == BaseRecordType.group
+                )
+            ):
</ins><span class="cx">                 return frozenset()
</span><span class="cx">         except AttributeError:
</span><span class="cx">             pass
</span><span class="cx"> 
</span><span class="cx">         try:
</span><span class="cx">             cuas = set(
</span><del>-                [&quot;mailto:%s&quot; % (emailAddress,)
</del><ins>+                [&quot;mailto:{0}&quot;.format(emailAddress,)
</ins><span class="cx">                  for emailAddress in self.emailAddresses]
</span><span class="cx">             )
</span><span class="cx">         except AttributeError:
</span><span class="lines">@@ -275,7 +285,6 @@
</span><span class="cx">             )
</span><span class="cx">         return frozenset(cuas)
</span><span class="cx"> 
</span><del>-
</del><span class="cx">     # Mapping from directory record.recordType to RFC2445 CUTYPE values
</span><span class="cx">     _cuTypes = {
</span><span class="cx">         BaseRecordType.user: 'INDIVIDUAL',
</span><span class="lines">@@ -314,7 +323,6 @@
</span><span class="cx">         #                        % (username,))
</span><span class="cx">         #         self.enabledForAddressBooks = False
</span><span class="cx"> 
</span><del>-
</del><span class="cx">     @property
</span><span class="cx">     def displayName(self):
</span><span class="cx">         return self.fullNames[0]
</span><span class="lines">@@ -417,7 +425,6 @@
</span><span class="cx">         return config.EnableCalDAV and self.hasCalendars
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-
</del><span class="cx">     @inlineCallbacks
</span><span class="cx">     def canAutoSchedule(self, organizer=None):
</span><span class="cx">         # FIXME:
</span><span class="lines">@@ -496,3 +503,31 @@
</span><span class="cx">             if delegatorGroup:
</span><span class="cx">                 if other in (yield delegatorGroup.members()):
</span><span class="cx">                     returnValue(True)
</span><ins>+
+
+    def attendee(self, params=None):
+        &quot;&quot;&quot;
+        Returns a pycalendar ATTENDEE property for this record.
+
+        @param groupUIDs: group uids for the MEMBER parameter of returned property
+        @type organizer: C{List}
+
+        @return: the attendee property
+        @rtype: C{Property}
+        &quot;&quot;&quot;
+        params = {} if params is None else params.copy()
+
+        if &quot;PARTSTAT&quot; not in params:
+            params[&quot;PARTSTAT&quot;] = &quot;NEEDS-ACTION&quot;
+        if &quot;CN&quot;not in params:
+            if self.fullNames:
+                params[&quot;CN&quot;] = list(self.fullNames)[0]
+        if &quot;EMAIL&quot; not in params:
+            if self.emailAddresses:
+                params[&quot;EMAIL&quot;] = list(self.emailAddresses)[0]
+        if &quot;CUTYPE&quot; not in params:
+            cuType = self.getCUType()
+            if cuType is not &quot;INDIVIDUAL&quot;:
+                params[&quot;CUTYPE&quot;] = cuType
+
+        return Property(&quot;ATTENDEE&quot;, &quot;urn:uuid:&quot; + self.uid.encode(&quot;utf-8&quot;), params=params)
</ins></span></pre></div>
<a id="CalendarServertrunktxdavwhogroupspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/who/groups.py (13217 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/groups.py        2014-04-09 05:17:43 UTC (rev 13217)
+++ CalendarServer/trunk/txdav/who/groups.py        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -169,11 +169,39 @@
</span><span class="cx">             )
</span><span class="cx">         ).on(self.transaction)
</span><span class="cx"> 
</span><del>-    # MOVE2WHO
-    # TODO: Pull this over from groupcacher branch
</del><ins>+        # get calendar Object
+        calObject = schema.CALENDAR_OBJECT
+        rows = yield Select(
+                [calObject.CALENDAR_RESOURCE_ID, ],
+                From=calObject,
+                Where=calObject.RESOURCE_ID == self.eventID,
+        ).on(self.transaction)
</ins><span class="cx"> 
</span><ins>+        calendarID = rows[0][0]
+        calendarHome = (yield self.Calendar._ownerHomeWithResourceID.on(
+            self.transaction, resourceID=calendarID)
+        )[0][0]
</ins><span class="cx"> 
</span><ins>+        calendar = yield calendarHome.childWithID(calendarID)
+        calendarObject = yield calendar.objectResourceWithID(self.eventID)
</ins><span class="cx"> 
</span><ins>+        # get group individual UIDs
+        groupMemember = schema.GROUP_MEMBERSHIP
+        rows = yield Select(
+                [groupMemember.MEMBER_GUID, ],
+                From=groupMemember,
+                Where=groupMemember.GROUP_ID == self.groupID,
+        ).on(self.transaction)
+        memberGUIDs = [row[0] for row in rows]
+
+        component = yield calendarObject.component()
+        changed = component.expandGroupAttendee(self.groupGUID, memberGUIDs, self.directoryService().recordWithCalendarUserAddress)
+
+        if changed:
+            yield calendarObject.setComponent(component)
+
+
+
</ins><span class="cx"> def diffAssignments(old, new):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Compare two proxy assignment lists and return their differences in the form
</span></span></pre></div>
<a id="CalendarServertrunktxdavwhotestaccountsgroupAttendeeAccountsxml"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/txdav/who/test/accounts/groupAttendeeAccounts.xml (0 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/test/accounts/groupAttendeeAccounts.xml                                (rev 0)
+++ CalendarServer/trunk/txdav/who/test/accounts/groupAttendeeAccounts.xml        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -0,0 +1,137 @@
</span><ins>+&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
+
+&lt;!--
+Copyright (c) 2006-2014 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ --&gt;
+
+&lt;!DOCTYPE accounts SYSTEM &quot;accounts.dtd&quot;&gt;
+
+&lt;directory realm=&quot;Test Realm&quot;&gt;
+        &lt;record type=&quot;user&quot;&gt;
+            &lt;short-name&gt;user01&lt;/short-name&gt;
+            &lt;uid&gt;10000000-0000-0000-0000-000000000001&lt;/uid&gt;
+            &lt;guid&gt;10000000-0000-0000-0000-000000000001&lt;/guid&gt;
+            &lt;password&gt;user01&lt;/password&gt;
+            &lt;full-name&gt;User 01&lt;/full-name&gt;
+            &lt;email&gt;user01@example.com&lt;/email&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;user&quot;&gt;
+            &lt;short-name&gt;user02&lt;/short-name&gt;
+            &lt;uid&gt;10000000-0000-0000-0000-000000000002&lt;/uid&gt;
+            &lt;guid&gt;10000000-0000-0000-0000-000000000002&lt;/guid&gt;
+            &lt;password&gt;user02&lt;/password&gt;
+            &lt;full-name&gt;User 02&lt;/full-name&gt;
+            &lt;email&gt;user02@example.com&lt;/email&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;user&quot;&gt;
+            &lt;short-name&gt;user03&lt;/short-name&gt;
+            &lt;uid&gt;10000000-0000-0000-0000-000000000003&lt;/uid&gt;
+            &lt;guid&gt;10000000-0000-0000-0000-000000000003&lt;/guid&gt;
+            &lt;password&gt;user03&lt;/password&gt;
+            &lt;full-name&gt;User 03&lt;/full-name&gt;
+            &lt;email&gt;user03@example.com&lt;/email&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;user&quot;&gt;
+            &lt;short-name&gt;user04&lt;/short-name&gt;
+            &lt;uid&gt;10000000-0000-0000-0000-000000000004&lt;/uid&gt;
+            &lt;guid&gt;10000000-0000-0000-0000-000000000004&lt;/guid&gt;
+            &lt;password&gt;user04&lt;/password&gt;
+            &lt;full-name&gt;User 04&lt;/full-name&gt;
+            &lt;email&gt;user04@example.com&lt;/email&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;user&quot;&gt;
+            &lt;short-name&gt;user05&lt;/short-name&gt;
+            &lt;uid&gt;10000000-0000-0000-0000-000000000005&lt;/uid&gt;
+            &lt;guid&gt;10000000-0000-0000-0000-000000000005&lt;/guid&gt;
+            &lt;password&gt;user05&lt;/password&gt;
+            &lt;full-name&gt;User 05&lt;/full-name&gt;
+            &lt;email&gt;user05@example.com&lt;/email&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;user&quot;&gt;
+            &lt;short-name&gt;user06&lt;/short-name&gt;
+            &lt;uid&gt;10000000-0000-0000-0000-000000000006&lt;/uid&gt;
+            &lt;guid&gt;10000000-0000-0000-0000-000000000006&lt;/guid&gt;
+            &lt;password&gt;user06&lt;/password&gt;
+            &lt;full-name&gt;User 06&lt;/full-name&gt;
+            &lt;email&gt;user06@example.com&lt;/email&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;user&quot;&gt;
+            &lt;short-name&gt;user07&lt;/short-name&gt;
+            &lt;uid&gt;10000000-0000-0000-0000-000000000007&lt;/uid&gt;
+            &lt;guid&gt;10000000-0000-0000-0000-000000000007&lt;/guid&gt;
+            &lt;password&gt;user07&lt;/password&gt;
+            &lt;full-name&gt;User 07&lt;/full-name&gt;
+            &lt;email&gt;user07@example.com&lt;/email&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;user&quot;&gt;
+            &lt;short-name&gt;user08&lt;/short-name&gt;
+            &lt;uid&gt;10000000-0000-0000-0000-000000000008&lt;/uid&gt;
+            &lt;guid&gt;10000000-0000-0000-0000-000000000008&lt;/guid&gt;
+            &lt;password&gt;user08&lt;/password&gt;
+            &lt;full-name&gt;User 08&lt;/full-name&gt;
+            &lt;email&gt;user08@example.com&lt;/email&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;user&quot;&gt;
+            &lt;short-name&gt;user09&lt;/short-name&gt;
+            &lt;uid&gt;10000000-0000-0000-0000-000000000009&lt;/uid&gt;
+            &lt;guid&gt;10000000-0000-0000-0000-000000000009&lt;/guid&gt;
+            &lt;password&gt;user09&lt;/password&gt;
+            &lt;full-name&gt;User 09&lt;/full-name&gt;
+            &lt;email&gt;user09@example.com&lt;/email&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;user&quot;&gt;
+            &lt;short-name&gt;user10&lt;/short-name&gt;
+            &lt;uid&gt;10000000-0000-0000-0000-000000000010&lt;/uid&gt;
+            &lt;guid&gt;10000000-0000-0000-0000-000000000010&lt;/guid&gt;
+            &lt;password&gt;user10&lt;/password&gt;
+            &lt;full-name&gt;User 10&lt;/full-name&gt;
+            &lt;email&gt;user10@example.com&lt;/email&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;group&quot;&gt;
+            &lt;short-name&gt;group01&lt;/short-name&gt;
+            &lt;uid&gt;20000000-0000-0000-0000-000000000001&lt;/uid&gt;
+            &lt;guid&gt;20000000-0000-0000-0000-000000000001&lt;/guid&gt;
+            &lt;full-name&gt;Group 01&lt;/full-name&gt;
+            &lt;email&gt;group01@example.com&lt;/email&gt;
+            &lt;member-uid&gt;10000000-0000-0000-0000-000000000001&lt;/member-uid&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;group&quot;&gt;
+            &lt;short-name&gt;group02&lt;/short-name&gt;
+            &lt;uid&gt;20000000-0000-0000-0000-000000000002&lt;/uid&gt;
+            &lt;guid&gt;20000000-0000-0000-0000-000000000002&lt;/guid&gt;
+            &lt;full-name&gt;Group 02&lt;/full-name&gt;
+            &lt;email&gt;group02@example.com&lt;/email&gt;
+            &lt;member-uid&gt;10000000-0000-0000-0000-000000000006&lt;/member-uid&gt;
+            &lt;member-uid&gt;10000000-0000-0000-0000-000000000007&lt;/member-uid&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;group&quot;&gt;
+            &lt;short-name&gt;group03&lt;/short-name&gt;
+            &lt;uid&gt;20000000-0000-0000-0000-000000000003&lt;/uid&gt;
+            &lt;guid&gt;20000000-0000-0000-0000-000000000003&lt;/guid&gt;
+            &lt;full-name&gt;Group 03&lt;/full-name&gt;
+            &lt;email&gt;group03@example.com&lt;/email&gt;
+            &lt;member-uid&gt;10000000-0000-0000-0000-000000000008&lt;/member-uid&gt;
+            &lt;member-uid&gt;10000000-0000-0000-0000-000000000009&lt;/member-uid&gt;
+        &lt;/record&gt;
+        &lt;record type=&quot;group&quot;&gt;
+            &lt;short-name&gt;group04&lt;/short-name&gt;
+            &lt;uid&gt;20000000-0000-0000-0000-000000000004&lt;/uid&gt;
+            &lt;guid&gt;20000000-0000-0000-0000-000000000004&lt;/guid&gt;
+            &lt;full-name&gt;Group 04&lt;/full-name&gt;
+            &lt;member-uid&gt;20000000-0000-0000-0000-000000000002&lt;/member-uid&gt;
+            &lt;member-uid&gt;20000000-0000-0000-0000-000000000003&lt;/member-uid&gt;
+            &lt;member-uid&gt;10000000-0000-0000-0000-000000000010&lt;/member-uid&gt;
+        &lt;/record&gt;
+&lt;/directory&gt;
</ins></span></pre></div>
<a id="CalendarServertrunktxdavwhotesttest_group_attendeespy"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/txdav/who/test/test_group_attendees.py (0 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/test/test_group_attendees.py                                (rev 0)
+++ CalendarServer/trunk/txdav/who/test/test_group_attendees.py        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -0,0 +1,338 @@
</span><ins>+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+&quot;&quot;&quot;
+    group attendee tests
+&quot;&quot;&quot;
+
+from twext.who.test.test_xml import xmlService
+from twisted.internet.defer import inlineCallbacks
+from twisted.trial import unittest
+from twistedcaldav.config import config
+from twistedcaldav.ical import Component, normalize_iCalStr
+from txdav.caldav.datastore.test.util import buildCalendarStore, populateCalendarsFrom, CommonCommonTests
+from txdav.who.util import directoryFromConfig
+import os
+
+class GroupAttendeeReconciliation(CommonCommonTests, unittest.TestCase):
+    &quot;&quot;&quot;
+    GroupAttendeeReconciliation tests
+    &quot;&quot;&quot;
+
+    @inlineCallbacks
+    def setUp(self):
+        self.patch(config.Scheduling.Options, &quot;AllowGroupAsAttendee&quot;, &quot;True&quot;)
+
+        yield super(GroupAttendeeReconciliation, self).setUp()
+        self.xmlService = xmlService(self.mktemp(), xmlData=None)
+
+        self.patch(config.DirectoryService.params, &quot;xmlFile&quot;,
+            os.path.join(
+                os.path.dirname(__file__), &quot;accounts&quot;, &quot;groupAttendeeAccounts.xml&quot;
+            )
+        )
+        self.patch(config.ResourceService.params, &quot;xmlFile&quot;,
+            os.path.join(
+                os.path.dirname(__file__), &quot;accounts&quot;, &quot;resources.xml&quot;
+            )
+        )
+        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory, directoryFromConfig(config))
+        yield self.populate()
+
+        self.paths = {}
+
+
+    def storeUnderTest(self):
+        &quot;&quot;&quot;
+        Create and return a L{CalendarStore} for testing.
+        &quot;&quot;&quot;
+        return self._sqlCalendarStore
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+    requirements = {
+        &quot;10000000-0000-0000-0000-000000000001&quot; : {
+            &quot;calendar&quot; : {}
+        },
+    }
+
+    @inlineCallbacks
+    def test_simplePUT(self):
+        &quot;&quot;&quot;
+        Test that group attendee is expanded on PUT
+        &quot;&quot;&quot;
+        calendar = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;10000000-0000-0000-0000-000000000001&quot;)
+
+        data_put_1 = &quot;&quot;&quot;BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART;TZID=US/Eastern:20140101T100000
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1@ninevah.local
+ORGANIZER:MAILTO:user01@example.com
+ATTENDEE:mailto:user01@example.com
+ATTENDEE:MAILTO:group02@example.com
+END:VEVENT
+END:VCALENDAR&quot;&quot;&quot;
+
+        data_get_1 = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VEVENT
+UID:event1@ninevah.local
+DTSTART;TZID=US/Eastern:20140101T100000
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;RSVP=TRUE:urn:uuid:10000000-0000-0000-0000-000000000001
+ATTENDEE;CN=Group 02;CUTYPE=GROUP;EMAIL=group02@example.com;RSVP=TRUE;SCHEDULE-STATUS=3.7:urn:uuid:20000000-0000-0000-0000-000000000002
+ATTENDEE;CN=User 06;EMAIL=user06@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000002&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000006
+ATTENDEE;CN=User 07;EMAIL=user07@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000002&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000007
+CREATED:20060101T150000Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:10000000-0000-0000-0000-000000000001
+SUMMARY:event 1
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        vcalendar1 = Component.fromString(data_put_1)
+        yield calendar.createCalendarObjectWithName(&quot;data1.ics&quot;, vcalendar1)
+        yield self.commit()
+
+        cobj1 = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;10000000-0000-0000-0000-000000000001&quot;)
+        vcalendar2 = yield cobj1.component()
+        self.assertEqual(normalize_iCalStr(vcalendar2), normalize_iCalStr(data_get_1))
+
+
+    @inlineCallbacks
+    def test_unknownPUT(self):
+        &quot;&quot;&quot;
+        Test unknown group with CUTYPE=GROUP handled
+        &quot;&quot;&quot;
+        calendar = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;10000000-0000-0000-0000-000000000001&quot;)
+
+        data_put_1 = &quot;&quot;&quot;BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART;TZID=US/Eastern:20140101T100000
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1@ninevah.local
+ORGANIZER:MAILTO:user01@example.com
+ATTENDEE:mailto:user01@example.com
+ATTENDEE;CUTYPE=GROUP:urn:uuid:FFFFFFFF-EEEE-DDDD-CCCC-BBBBBBBBBBBB
+END:VEVENT
+END:VCALENDAR&quot;&quot;&quot;
+
+        data_get_1 = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VEVENT
+UID:event1@ninevah.local
+DTSTART;TZID=US/Eastern:20140101T100000
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;RSVP=TRUE:urn:uuid:10000000-0000-0000-0000-000000000001
+ATTENDEE;CUTYPE=GROUP;RSVP=TRUE;SCHEDULE-STATUS=3.7:urn:uuid:FFFFFFFF-EEEE-DDDD-CCCC-BBBBBBBBBBBB
+CREATED:20060101T150000Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:10000000-0000-0000-0000-000000000001
+SUMMARY:event 1
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        vcalendar1 = Component.fromString(data_put_1)
+        yield calendar.createCalendarObjectWithName(&quot;data1.ics&quot;, vcalendar1)
+        yield self.commit()
+
+        cobj1 = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;10000000-0000-0000-0000-000000000001&quot;)
+        vcalendar2 = yield cobj1.component()
+        self.assertEqual(normalize_iCalStr(vcalendar2), normalize_iCalStr(data_get_1))
+
+
+    @inlineCallbacks
+    def test_primaryAttendeeInGroupPUT(self):
+        &quot;&quot;&quot;
+        Test that primary attendee also in group remains primary
+        &quot;&quot;&quot;
+        calendar = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;10000000-0000-0000-0000-000000000001&quot;)
+
+        data_put_1 = &quot;&quot;&quot;BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART;TZID=US/Eastern:20140101T100000
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1@ninevah.local
+ORGANIZER:MAILTO:user01@example.com
+ATTENDEE:mailto:user01@example.com
+ATTENDEE:mailto:user02@example.com
+ATTENDEE:MAILTO:group01@example.com
+END:VEVENT
+END:VCALENDAR&quot;&quot;&quot;
+
+        data_get_1 = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VEVENT
+UID:event1@ninevah.local
+DTSTART;TZID=US/Eastern:20140101T100000
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;RSVP=TRUE:urn:uuid:10000000-0000-0000-0000-000000000001
+ATTENDEE;CN=User 02;EMAIL=user02@example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000002
+ATTENDEE;CN=Group 01;CUTYPE=GROUP;EMAIL=group01@example.com;RSVP=TRUE;SCHEDULE-STATUS=3.7:urn:uuid:20000000-0000-0000-0000-000000000001
+CREATED:20060101T150000Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:10000000-0000-0000-0000-000000000001
+SUMMARY:event 1
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+        vcalendar1 = Component.fromString(data_put_1)
+        yield calendar.createCalendarObjectWithName(&quot;data1.ics&quot;, vcalendar1)
+        yield self.commit()
+
+        cobj1 = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;10000000-0000-0000-0000-000000000001&quot;)
+        vcalendar1 = yield cobj1.component()
+        self.assertEqual(normalize_iCalStr(vcalendar1), normalize_iCalStr(data_get_1))
+
+
+    @inlineCallbacks
+    def test_nestedPUT(self):
+        &quot;&quot;&quot;
+        Test that nested groups are expanded expanded on PUT
+        &quot;&quot;&quot;
+        calendar = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;10000000-0000-0000-0000-000000000001&quot;)
+
+        data_put_1 = &quot;&quot;&quot;BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART;TZID=US/Eastern:20140101T100000
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1@ninevah.local
+ORGANIZER:MAILTO:user01@example.com
+ATTENDEE:mailto:user01@example.com
+ATTENDEE:urn:uuid:20000000-0000-0000-0000-000000000004
+END:VEVENT
+END:VCALENDAR&quot;&quot;&quot;
+
+        data_get_1 = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VEVENT
+UID:event1@ninevah.local
+DTSTART;TZID=US/Eastern:20140101T100000
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;RSVP=TRUE:urn:uuid:10000000-0000-0000-0000-000000000001
+ATTENDEE;CN=Group 04;CUTYPE=GROUP;RSVP=TRUE;SCHEDULE-STATUS=3.7:urn:uuid:20000000-0000-0000-0000-000000000004
+ATTENDEE;CN=User 06;EMAIL=user06@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000004&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000006
+ATTENDEE;CN=User 07;EMAIL=user07@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000004&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000007
+ATTENDEE;CN=User 08;EMAIL=user08@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000004&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000008
+ATTENDEE;CN=User 09;EMAIL=user09@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000004&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000009
+ATTENDEE;CN=User 10;EMAIL=user10@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000004&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000010
+CREATED:20060101T150000Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:10000000-0000-0000-0000-000000000001
+SUMMARY:event 1
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        vcalendar1 = Component.fromString(data_put_1)
+        yield calendar.createCalendarObjectWithName(&quot;data1.ics&quot;, vcalendar1)
+        yield self.commit()
+
+        cobj1 = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;10000000-0000-0000-0000-000000000001&quot;)
+        vcalendar2 = yield cobj1.component()
+        self.assertEqual(normalize_iCalStr(vcalendar2), normalize_iCalStr(data_get_1))
+
+
+    @inlineCallbacks
+    def test_twoGroupPUT(self):
+        &quot;&quot;&quot;
+        Test that expanded users in two primary groups have groups in MEMBERS param
+        &quot;&quot;&quot;
+        calendar = yield self.calendarUnderTest(name=&quot;calendar&quot;, home=&quot;10000000-0000-0000-0000-000000000001&quot;)
+
+        data_put_1 = &quot;&quot;&quot;BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART;TZID=US/Eastern:20140101T100000
+DURATION:PT1H
+SUMMARY:event 1
+UID:event1@ninevah.local
+ORGANIZER:MAILTO:user01@example.com
+ATTENDEE:mailto:user01@example.com
+ATTENDEE:MAILTO:group02@example.com
+ATTENDEE:urn:uuid:20000000-0000-0000-0000-000000000004
+END:VEVENT
+END:VCALENDAR&quot;&quot;&quot;
+
+        data_get_1 = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VEVENT
+UID:event1@ninevah.local
+DTSTART;TZID=US/Eastern:20140101T100000
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01@example.com;RSVP=TRUE:urn:uuid:10000000-0000-0000-0000-000000000001
+ATTENDEE;CN=Group 02;CUTYPE=GROUP;EMAIL=group02@example.com;RSVP=TRUE;SCHEDULE-STATUS=3.7:urn:uuid:20000000-0000-0000-0000-000000000002
+ATTENDEE;CN=Group 04;CUTYPE=GROUP;RSVP=TRUE;SCHEDULE-STATUS=3.7:urn:uuid:20000000-0000-0000-0000-000000000004
+ATTENDEE;CN=User 06;EMAIL=user06@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000002&quot;,&quot;urn:uuid:20000000-0000-0000-0000-000000000004&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000006
+ATTENDEE;CN=User 07;EMAIL=user07@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000002&quot;,&quot;urn:uuid:20000000-0000-0000-0000-000000000004&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000007
+ATTENDEE;CN=User 08;EMAIL=user08@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000004&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000008
+ATTENDEE;CN=User 09;EMAIL=user09@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000004&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000009
+ATTENDEE;CN=User 10;EMAIL=user10@example.com;MEMBER=&quot;urn:uuid:20000000-0000-0000-0000-000000000004&quot;;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:10000000-0000-0000-0000-000000000010
+CREATED:20060101T150000Z
+ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:uuid:10000000-0000-0000-0000-000000000001
+SUMMARY:event 1
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        vcalendar1 = Component.fromString(data_put_1)
+        yield calendar.createCalendarObjectWithName(&quot;data1.ics&quot;, vcalendar1)
+        yield self.commit()
+
+        cobj1 = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;10000000-0000-0000-0000-000000000001&quot;)
+        vcalendar2 = yield cobj1.component()
+        self.assertEqual(normalize_iCalStr(vcalendar2), normalize_iCalStr(data_get_1))
</ins></span></pre></div>
<a id="CalendarServertrunktxdavwhotesttest_groupspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/who/test/test_groups.py (13217 => 13218)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/test/test_groups.py        2014-04-09 05:17:43 UTC (rev 13217)
+++ CalendarServer/trunk/txdav/who/test/test_groups.py        2014-04-09 05:18:45 UTC (rev 13218)
</span><span class="lines">@@ -25,6 +25,7 @@
</span><span class="cx"> from txdav.common.icommondatastore import NotFoundError
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx"> class GroupCacherTest(StoreTestCase):
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -46,7 +47,7 @@
</span><span class="cx">         record = yield self.directory.recordWithUID(u&quot;__top_group_1__&quot;)
</span><span class="cx">         yield self.groupCacher.refreshGroup(txn, record.uid)
</span><span class="cx"> 
</span><del>-        groupID, name, membershipHash, modified = (yield txn.groupByUID(record.uid))
</del><ins>+        groupID, _ignore_name, membershipHash, _ignore_modified = (yield txn.groupByUID(record.uid))
</ins><span class="cx"> 
</span><span class="cx">         self.assertEquals(membershipHash, &quot;553eb54e3bbb26582198ee04541dbee4&quot;)
</span><span class="cx"> 
</span><span class="lines">@@ -88,7 +89,7 @@
</span><span class="cx">         # Refresh the group so it's assigned a group_id
</span><span class="cx">         uid = u&quot;__top_group_1__&quot;
</span><span class="cx">         yield self.groupCacher.refreshGroup(txn, uid)
</span><del>-        groupID, name, membershipHash, modified = (yield txn.groupByUID(uid))
</del><ins>+        groupID, name, _ignore_membershipHash, _ignore_modified = (yield txn.groupByUID(uid))
</ins><span class="cx"> 
</span><span class="cx">         # Remove two members, and add one member
</span><span class="cx">         newSet = set()
</span><span class="lines">@@ -135,7 +136,7 @@
</span><span class="cx">         uid = u&quot;__top_group_1__&quot;
</span><span class="cx">         hash = &quot;553eb54e3bbb26582198ee04541dbee4&quot;
</span><span class="cx">         yield self.groupCacher.refreshGroup(txn, uid)
</span><del>-        groupID, name, membershipHash, modified = yield txn.groupByUID(uid)
</del><ins>+        groupID, _ignore_name, _ignore_membershipHash, _ignore_modified = yield txn.groupByUID(uid)
</ins><span class="cx">         results = (yield txn.groupByID(groupID))
</span><span class="cx">         self.assertEquals((uid, u&quot;Top Group 1&quot;, hash), results)
</span><span class="cx"> 
</span><span class="lines">@@ -242,7 +243,6 @@
</span><span class="cx">             )
</span><span class="cx">         )
</span><span class="cx"> 
</span><del>-
</del><span class="cx">         #
</span><span class="cx">         # Now, remove some external assignments
</span><span class="cx">         #
</span><span class="lines">@@ -310,6 +310,7 @@
</span><span class="cx">             )
</span><span class="cx">         )
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx">     def test_diffAssignments(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Ensure external proxy assignment diffing works
</span><span class="lines">@@ -389,3 +390,4 @@
</span><span class="cx">                 {&quot;D&quot;: (&quot;7&quot;, &quot;8&quot;), &quot;C&quot;: (&quot;4&quot;, &quot;5&quot;), &quot;A&quot;: (&quot;1&quot;, &quot;2&quot;)},
</span><span class="cx">             )
</span><span class="cx">         )
</span><ins>+
</ins></span></pre>
</div>
</div>

</body>
</html>