<!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">         <name>trunk</name>
</span><span class="cx">         <comment></comment>
</span><span class="cx">         <projects>
</span><ins>+                <project>caldavclientlibrary</project>
+                <project>kerberos</project>
+                <project>pycalendar</project>
+                <project>twextpy</project>
</ins><span class="cx">         </projects>
</span><span class="cx">         <buildSpec>
</span><span class="cx">                 <buildCommand>
</span><span class="lines">@@ -16,7 +20,7 @@
</span><span class="cx">         </natures>
</span><span class="cx">         <filteredResources>
</span><span class="cx">                 <filter>
</span><del>-                        <id>1396665409305</id>
</del><ins>+                        <id>1396668930421</id>
</ins><span class="cx">                         <name></name>
</span><span class="cx">                         <type>10</type>
</span><span class="cx">                         <matcher>
</span><span class="lines">@@ -25,7 +29,7 @@
</span><span class="cx">                         </matcher>
</span><span class="cx">                 </filter>
</span><span class="cx">                 <filter>
</span><del>-                        <id>1396665409305</id>
</del><ins>+                        <id>1396668930422</id>
</ins><span class="cx">                         <name></name>
</span><span class="cx">                         <type>10</type>
</span><span class="cx">                         <matcher>
</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("doAddressBookDirectoryQuery: vCard did not match filter:\n{vcard}", vcard=vCardResult.vCard())
</span><span class="cx">
</span><span class="lines">@@ -249,7 +249,7 @@
</span><span class="cx"> if maxQueryRecords and maxRecords > maxQueryRecords:
</span><span class="cx"> maxRecords = maxQueryRecords
</span><span class="cx">
</span><del>- results = sorted(list(filteredResults), key=lambda result: result.vCard().propertyValue("UID"))
</del><ins>+ results = sorted(filteredResults, key=lambda result: result.vCard().propertyValue("UID"))
</ins><span class="cx"> limited = maxResults and len(results) >= maxResults
</span><span class="cx">
</span><span class="cx"> log.info("limited={l} #results={n}", 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["pycalendar"]
</span><span class="cx">
</span><span class="cx"> if not isinstance(pyobj, PyProperty):
</span><del>- raise TypeError("Not a Property: %r" % (property,))
</del><ins>+ raise TypeError("Not a Property: {0!r}".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 "<%s: %r: %r>" % (self.__class__.__name__, self.name(), self.value())
</del><ins>+ return (
+ "<{self.__class__.__name__}: {name!r}: {value!r}>"
+ .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), "Not a date/date-time value: %r" % (self,)
</del><ins>+ assert isinstance(dt, DateTime), "Not a date/date-time value: {0!r}".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 = "%s: %s" % (e.mReason, e.mData,)
</del><ins>+ errmsg = "{0}: {1}".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("%s\n%s" % (errmsg, data,))
</del><ins>+ raise InvalidICalendarDataError("{0}\n{1}".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("Not a ComponentBase: %r" % (pyobj,))
</del><ins>+ raise TypeError("Not a ComponentBase: {0!r}".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("Not a Component: %r" % (parent,))
</del><ins>+ raise TypeError("Not a Component: {0!r}".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 "<%s: %r>" % (self.__class__.__name__, str(self._pycalendar))
</del><ins>+ return (
+ "<{self.__class__.__name__}: {pycal!r}>"
+ .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"> """
</span><span class="cx"> Return text representation and include timezones if the option is on.
</span><span class="cx"> """
</span><del>- assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".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"> """
</span><del>- assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".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("Component contains more than one type of primary type: %r" % (self,))
</del><ins>+ raise InvalidICalendarDataError("Component contains more than one type of primary type: {0!r}".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"> """
</span><del>- assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".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"> """
</span><del>- assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".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"> """
</span><del>- assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".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"> """
</span><del>- assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".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) > 1:
</span><del>- raise InvalidICalendarDataError("More than one %s property in component %r" % (name, self))
</del><ins>+ raise InvalidICalendarDataError("More than one {0} property in component {1!r}".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) > 1:
</span><del>- raise InvalidICalendarDataError("More than one %s property in component %r" % (name, self))
</del><ins>+ raise InvalidICalendarDataError("More than one {0} property in component {1!r}".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"> """
</span><del>- assert self.name() == "VALARM", "Component is not a VAlARM: %r" % (self,)
</del><ins>+ assert self.name() == "VALARM", "Component is not a VAlARM: {0!r}".format(self,)
</ins><span class="cx">
</span><span class="cx"> # The trigger value
</span><span class="cx"> trigger = self.propertyValue("TRIGGER")
</span><span class="cx"> if trigger is None:
</span><del>- raise InvalidICalendarDataError("VALARM has no TRIGGER property: %r" % (self,))
</del><ins>+ raise InvalidICalendarDataError("VALARM has no TRIGGER property: {0!r}".format(self,))
</ins><span class="cx">
</span><span class="cx"> # The related parameter
</span><span class="cx"> related = self.getProperty("TRIGGER").parameterValue("RELATED")
</span><span class="lines">@@ -951,7 +957,7 @@
</span><span class="cx"> duration = self.propertyValue("DURATION")
</span><span class="cx">
</span><span class="cx"> if repeat > 0 and duration is None:
</span><del>- raise InvalidICalendarDataError("VALARM has invalid REPEAT/DURATIOn properties: %r" % (self,))
</del><ins>+ raise InvalidICalendarDataError("VALARM has invalid REPEAT/DURATIOn properties: {0!r}".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"> """
</span><span class="cx">
</span><del>- assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".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"> """
</span><span class="cx"> @return: the UID of the subcomponents in this component.
</span><span class="cx"> """
</span><del>- assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)
</ins><span class="cx">
</span><span class="cx"> if not hasattr(self, "_resource_uid"):
</span><span class="cx"> for subcomponent in self.subcomponents():
</span><span class="lines">@@ -1706,7 +1712,7 @@
</span><span class="cx"> """
</span><span class="cx"> Generate a new UID for all components in this VCALENDAR
</span><span class="cx"> """
</span><del>- assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".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"> """
</span><del>- assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)
</ins><span class="cx">
</span><span class="cx"> if not hasattr(self, "_resource_type"):
</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 = "VTIMEZONE"
</span><span class="cx"> else:
</span><del>- raise InvalidICalendarDataError("No component type found for calendar component: %r" % (self,))
</del><ins>+ raise InvalidICalendarDataError("No component type found for calendar component: {0!r}".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"> """
</span><span class="cx"> if self.name() != "VCALENDAR":
</span><del>- log.debug("Not a calendar: %s" % (self,))
</del><ins>+ log.debug("Not a calendar: {0}".format(self,))
</ins><span class="cx"> raise InvalidICalendarDataError("Not a calendar")
</span><span class="cx"> if not self.resourceType():
</span><del>- log.debug("Unknown resource type: %s" % (self,))
</del><ins>+ log.debug("Unknown resource type: {0}".format(self,))
</ins><span class="cx"> raise InvalidICalendarDataError("Unknown resource type")
</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("Calendar data had unfixable problems:\n %s" % ("\n ".join(unfixed),))
</del><ins>+ log.debug("Calendar data had unfixable problems:\n {0}".format("\n ".join(unfixed),))
</ins><span class="cx"> if doRaise:
</span><del>- raise InvalidICalendarDataError("Calendar data had unfixable problems:\n %s" % ("\n ".join(unfixed),))
</del><ins>+ raise InvalidICalendarDataError("Calendar data had unfixable problems:\n {0}".format("\n ".join(unfixed),))
</ins><span class="cx"> if fixed:
</span><del>- log.debug("Calendar data had fixable problems:\n %s" % ("\n ".join(fixed),))
</del><ins>+ log.debug("Calendar data had fixable problems:\n {0}".format("\n ".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()) > 0:
</span><span class="cx"> master.addProperty(property)
</span><span class="cx"> del exdates[rid]
</span><del>- fixed.append("Removed EXDATE for valid override: %s" % (rid,))
</del><ins>+ fixed.append("Removed EXDATE for valid override: {0}".format(rid,))
</ins><span class="cx"> else:
</span><del>- unfixed.append("EXDATE for valid override: %s" % (rid,))
</del><ins>+ unfixed.append("EXDATE for valid override: {0}".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 < dtstart:
</span><span class="cx"> if doFix:
</span><del>- fixed.append("Removed earlier EXDATE: %s" % (exdateValue,))
</del><ins>+ fixed.append("Removed earlier EXDATE: {0}".format(exdateValue,))
</ins><span class="cx"> else:
</span><del>- unfixed.append("EXDATE earlier than master: %s" % (exdateValue,))
</del><ins>+ unfixed.append("EXDATE earlier than master: {0}".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("RECURRENCE-ID")
</span><span class="cx"> if doFix:
</span><span class="cx"> master.addProperty(Property("RDATE", [brokenRID, ]))
</span><del>- fixed.append("Added RDATE for invalid occurrence: %s" %
- (brokenRID,))
</del><ins>+ fixed.append(
+ "Added RDATE for invalid occurrence: {0}".format(
+ brokenRID,
+ )
+ )
</ins><span class="cx"> else:
</span><del>- unfixed.append("Invalid occurrence: %s" % (brokenRID,))
</del><ins>+ unfixed.append("Invalid occurrence: {0}".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 = "Calendar resources may not contain more than one type of calendar component (%s and %s found)" % (ctype, subcomponent.name())
</del><ins>+ msg = "Calendar resources may not contain more than one type of calendar component ({0} and {1} found)".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 = "Component type: %s not allowed" % (ctype,)
</del><ins>+ msg = "Component type: {0} not allowed".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 = "Calendar resources may not contain components with different UIDs (%s and %s found)" % (component_id, subcomponent.propertyValue("UID"))
</del><ins>+ msg = "Calendar resources may not contain components with different UIDs ({0} and {1} found)".format(
+ component_id, subcomponent.propertyValue("UID")
+ )
</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 = "Calendar resources may not contain components with the same UIDs and no Recurrence-IDs (%s and %s found)" % (component_id, subcomponent.propertyValue("UID"))
</del><ins>+ msg = "Calendar resources may not contain components with the same UIDs and no Recurrence-IDs ({0} and {1} found)".format(
+ component_id, subcomponent.propertyValue("UID")
+ )
</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 = "Calendar resources may not contain components with the same Recurrence-IDs (%s)" % (rid,)
</del><ins>+ msg = "Calendar resources may not contain components with the same Recurrence-IDs ({0})".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 = "Timezone ID %s is referenced but not defined: %s" % (timezone_ref, self,)
</del><ins>+ msg = "Timezone ID {0} is referenced but not defined: {1}".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>- "Timezone %s is not referenced by any non-timezone component" % (timezone,)
</del><ins>+ "Timezone {0} is not referenced by any non-timezone component".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 = "Only one ORGANIZER is allowed in an iCalendar object:\n%s" % (self,)
</del><ins>+ msg = "Only one ORGANIZER is allowed in an iCalendar object:\n{0}".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"> """
</span><span class="cx">
</span><del>- assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".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"> """
</span><span class="cx">
</span><del>- assert from_calendar.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
</del><ins>+ assert from_calendar.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)
</ins><span class="cx">
</span><span class="cx"> if self.name() == "VCALENDAR":
</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"> """
</span><span class="cx">
</span><del>- assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".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() == "VCALENDAR", "Not a calendar: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".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"> """
</span><span class="cx">
</span><del>- assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".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"> """
</span><span class="cx">
</span><del>- assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
</del><ins>+ assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".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"> """
</span><span class="cx"> Test whether the component has a VALARM as an immediate sub-component.
</span><span class="cx"> """
</span><del>- assert self.name().upper() in ("VEVENT", "VTODO",), "Not a VEVENT or VTODO: %r" % (self,)
</del><ins>+ assert self.name().upper() in ("VEVENT", "VTODO",), "Not a VEVENT or VTODO: {0!r}".format(self,)
</ins><span class="cx">
</span><span class="cx"> for component in self.subcomponents():
</span><span class="cx"> if component.name().upper() == "VALARM":
</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>-""".replace("\n", "\r\n") % (alarm,)
</del><ins>+""".replace("\n", "\r\n").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("EMAIL")
</span><span class="cx"> if oldemail:
</span><del>- oldemail = "mailto:%s" % (oldemail,)
</del><ins>+ oldemail = "mailto:{0}".format(oldemail,)
</ins><span class="cx">
</span><span class="cx"> # Get any CN parameter
</span><span class="cx"> oldCN = prop.parameterValue("CN")
</span><span class="cx">
</span><del>- cutype = prop.parameterValue("CUTYPE")
-
</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("EMAIL")
</span><span class="cx">
</span><ins>+ if cutype == "INDIVIDUAL":
+ cutype = None
+
+ if cutype != prop.parameterValue("CUTYPE"):
+ if cutype:
+ prop.setParameter("CUTYPE", cutype)
+ else:
+ prop.removeParameter("CUTYPE")
+
</ins><span class="cx"> # For VPOLL also do immediate children
</span><span class="cx"> if component.name() == "VPOLL":
</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(["urn:uuid:" + str(memberGUID) for memberGUID in memberGUIDs])
+ groupUUID = "urn:uuid:" + str(groupGUID)
+ changed = False
+ for component in self.subcomponents():
+ if component.name() in ignoredComponents:
+ continue
+
+ oldAttendeeProps = tuple(component.properties("ATTENDEE"))
+ 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={"MEMBER": 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("MEMBER"):
+ parameterValues = tuple(attendeeProp.parameterValues("MEMBER"))
+ if groupUUID in parameterValues:
+ if attendeeProp.value() not in memberUUIDs:
+ attendeeProp.removeParameterValue("MEMBER", groupUUID)
+ if not attendeeProp.parameterValues("MEMBER"):
+ component.removeProperty(attendeeProp)
+ changed = True
+ else:
+ if attendeeProp.value() in memberUUIDs:
+ attendeeProp.setParameter("MEMBER", 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("No VTIMEZONE component in %s" % (tzdata,))
</del><ins>+ raise InvalidICalendarDataError("No VTIMEZONE component in {0}".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("No VTIMEZONE component in %s" % (tzdata,))
</del><ins>+ raise InvalidICalendarDataError("No VTIMEZONE component in {0}".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("urn:uuid:%s" % (guid,))
</del><ins>+ returnValue("urn:uuid:{0}".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("urn:uuid:"):
</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"> "urn:uuid:foo" : (
</span><span class="cx"> "Foo",
</span><span class="cx"> "foo",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:foo", "http://example.com/foo", "/foo")
</span><span class="cx"> ),
</span><span class="cx"> "urn:uuid:bar" : (
</span><span class="cx"> "Bar",
</span><span class="cx"> "bar",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:bar", "mailto:bar@example.com", "http://example.com/bar", "/bar")
</span><span class="cx"> ),
</span><span class="cx"> "urn:uuid:baz" : (
</span><span class="cx"> "Baz",
</span><span class="cx"> "baz",
</span><del>- ("urn:uuid:baz", "http://example.com/baz")
</del><ins>+ "INDIVIDUAL",
+ ("urn:uuid:baz", "http://example.com/baz")
</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"> "/principals/users/foo" : (
</span><span class="cx"> "Foo",
</span><span class="cx"> "foo",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:foo",)
</span><span class="cx"> ),
</span><span class="cx"> "http://example.com/principals/users/bar" : (
</span><span class="cx"> "Bar",
</span><span class="cx"> "bar",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:bar",)
</span><span class="cx"> ),
</span><span class="cx"> "http://example.com/principals/locations/buzz" : (
</span><span class="cx"> "{Restricted} Buzz",
</span><span class="cx"> "buzz",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:buzz",)
</span><span class="cx"> ),
</span><span class="cx"> }[cuaddr]
</span><span class="lines">@@ -7641,16 +7647,19 @@
</span><span class="cx"> "/principals/users/foo" : (
</span><span class="cx"> "Foo",
</span><span class="cx"> "foo",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:foo",)
</span><span class="cx"> ),
</span><span class="cx"> "http://example.com/principals/users/bar" : (
</span><span class="cx"> "Bar",
</span><span class="cx"> "bar",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:bar",)
</span><span class="cx"> ),
</span><span class="cx"> "http://example.com/principals/locations/buzz" : (
</span><span class="cx"> "{Restricted} Buzz",
</span><span class="cx"> "buzz",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:buzz",)
</span><span class="cx"> ),
</span><span class="cx"> }[cuaddr]
</span><span class="lines">@@ -7697,16 +7706,19 @@
</span><span class="cx"> "/principals/users/foo" : (
</span><span class="cx"> "Foo",
</span><span class="cx"> "foo",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:foo",)
</span><span class="cx"> ),
</span><span class="cx"> "http://example.com/principals/users/bar" : (
</span><span class="cx"> "Bar",
</span><span class="cx"> "bar",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:bar",)
</span><span class="cx"> ),
</span><span class="cx"> "http://example.com/principals/locations/buzz" : (
</span><span class="cx"> "{Restricted} Buzz",
</span><span class="cx"> "buzz",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:buzz",)
</span><span class="cx"> ),
</span><span class="cx"> }[cuaddr]
</span><span class="lines">@@ -8439,21 +8451,25 @@
</span><span class="cx"> "urn:uuid:foo" : (
</span><span class="cx"> "Foo",
</span><span class="cx"> "foo",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:foo", "http://example.com/foo", "/foo")
</span><span class="cx"> ),
</span><span class="cx"> "urn:uuid:bar" : (
</span><span class="cx"> "Bar",
</span><span class="cx"> "bar",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:bar", "mailto:bar@example.com", "http://example.com/bar", "/bar")
</span><span class="cx"> ),
</span><span class="cx"> "urn:uuid:baz" : (
</span><span class="cx"> "Baz",
</span><span class="cx"> "baz",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:baz", "http://example.com/baz")
</span><span class="cx"> ),
</span><span class="cx"> "urn:uuid:buz" : (
</span><span class="cx"> "Buz",
</span><span class="cx"> "buz",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:buz",)
</span><span class="cx"> ),
</span><span class="cx"> }[cuaddr]
</span><span class="lines">@@ -8483,11 +8499,13 @@
</span><span class="cx"> "/principals/users/foo" : (
</span><span class="cx"> "Foo",
</span><span class="cx"> "foo",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:foo",)
</span><span class="cx"> ),
</span><span class="cx"> "http://example.com/principals/users/buz" : (
</span><span class="cx"> "Buz",
</span><span class="cx"> "buz",
</span><ins>+ "INDIVIDUAL",
</ins><span class="cx"> ("urn:uuid:buz",)
</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 "INDIVIDUAL"
+
</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 = """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("%d dropbox ids to migrate" % (total,))
</del><ins>+ log.warn("{0} dropbox ids to migrate".format(total,))
</ins><span class="cx"> except RuntimeError, e:
</span><del>- log.error("Dropbox migration failed when cleaning out dropbox ids: %s" % (e,))
</del><ins>+ log.error("Dropbox migration failed when cleaning out dropbox ids: {0}".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("CalendarStoreFeatures.upgradeToManagedAttachments - attachment loop count: %d" % (count,))
</del><ins>+ txn = self._store.newTransaction("CalendarStoreFeatures.upgradeToManagedAttachments - attachment loop count: {0}".format(count,))
</ins><span class="cx"> try:
</span><span class="cx"> dropbox_id = "Batched select"
</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("%d of %d dropbox ids migrated" % (count, total,))
</del><ins>+ log.warn("{0} of {1} dropbox ids migrated".format(count, total,))
</ins><span class="cx"> except RuntimeError, e:
</span><del>- log.error("Dropbox migration failed for '%s': %s" % (dropbox_id, e,))
</del><ins>+ log.error("Dropbox migration failed for '{0}': {1}".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"> """
</span><span class="cx">
</span><del>- log.debug("Processing dropbox id: %s" % (dropbox_id,))
</del><ins>+ log.debug("Processing dropbox id: {0}".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(" %d affected calendar objects" % (len(cobjs),))
</del><ins>+ log.debug(" {0} affected calendar objects".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(" %d associated attachment objects" % (len(names),))
</del><ins>+ log.debug(" {0} associated attachment objects".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(" processing attachment object: %s" % (name,))
</del><ins>+ log.debug(" processing attachment object: {0}".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("Orphaned dropbox id removed: %s" % (attachment._path,))
</del><ins>+ log.warn("Orphaned dropbox id removed: {0}".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(" %d owner calendar objects" % (len(owners),))
- log.debug(" %d UIDs" % (len(cobj_by_UID),))
- log.debug(" %d total calendar objects" % (sum([len(items) for items in cobj_by_UID.values()]),))
</del><ins>+ log.debug(" {0} owner calendar objects".format(len(owners),))
+ log.debug(" {0} UIDs".format(len(cobj_by_UID),))
+ log.debug(" {0} total calendar objects".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(" converted attachment: %r" % (attachment,))
</del><ins>+ log.debug(" converted attachment: {0!r}".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(" added reference for: %r" % (owner_obj,))
</del><ins>+ log.debug(" added reference for: {0!r}".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(" re-wrote calendar object: %r" % (cobj,))
</del><ins>+ log.debug(" re-wrote calendar object: {0!r}".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("Unowned dropbox id removed: %s" % (attachment._path,))
</del><ins>+ log.warn("Unowned dropbox id removed: {0}".format(attachment._path,))
</ins><span class="cx"> yield attachment.remove()
</span><span class="cx"> continue
</span><span class="cx">
</span><del>- log.debug(" finished dropbox id: %s" % (dropbox_id,))
</del><ins>+ log.debug(" finished dropbox id: {0}".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"> """
</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("Splitting calendars for user %s" % (self._ownerUID,))
</del><ins>+ self.log.warn("Splitting calendars for user {0}".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(" Calendar: '%s', split into %d" % (calendar.name(), split_count + 1,))
</del><ins>+ self.log.warn(" Calendar: '{0}', split into {1}".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("Search falls outside range of index for %s %s to %s" % (name, minDate, maxDate))
</del><ins>+ self.log.info("Search falls outside range of index for {0} {1} to {2}".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("%s-%s" % (self._name, component.lower(),))
</del><ins>+ newcalendar = yield self._home.createCalendarWithName("{0}-{1}".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("%s-%s-%d" % (self._name, component.lower(), ctr + 1,))
</del><ins>+ newcalendar = yield self._home.createCalendarWithName("{0}-{1}-[2}".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] = "%s-%s" % (columnMap[cb.CALENDAR_RESOURCE_NAME], component.lower(),)
</del><ins>+ columnMap[cb.CALENDAR_RESOURCE_NAME] = "{0}-{1}".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("Invalid component type %s for calendar: %s" % (component.mainType(), self.calendar(),))
</del><ins>+ raise InvalidComponentTypeError("Invalid component type {0} for calendar: {1}".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):
+ """
+ Expand group attendees
+ """
+
+ if not config.Scheduling.Options.AllowGroupAsAttendee:
+ return
+
+ attendeeProps = component.getAllAttendeeProperties()
+ groupGUIDs = set([
+ uuid.UUID(attendeeProp.value()[len("urn:uuid:"):]) for attendeeProp in attendeeProps
+ if attendeeProp.parameterValue("CUTYPE") == "GROUP"
+ ])
+
+ 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"> """
</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("Wrong type of object: %s" % (type(component),))
</del><ins>+ raise InvalidObjectResourceError("Wrong type of object: {0}".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 > oldAttendeeListLength:
</span><del>- raise TooManyAttendeesError("Attendee list size %d is larger than allowed limit %d" % (attendeeListLength, config.MaxAttendeesPerInstance))
</del><ins>+ raise TooManyAttendeesError(
+ "Attendee list size {0} is larger than allowed limit {1}".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 == "ROOM" and not config.Scheduling.Options.AllowLocationWithoutOrganizer or
</span><span class="cx"> cutype == "RESOURCE" and not config.Scheduling.Options.AllowResourceWithoutOrganizer):
</span><del>- raise ValidOrganizerError("Organizer required in calendar data for a %s" % (cutype.lower(),))
</del><ins>+ raise ValidOrganizerError("Organizer required in calendar data for a {0}".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"> "X-TITLE": title,
</span><span class="cx"> }
</span><span class="cx"> structured = Property("X-APPLE-STRUCTURED-LOCATION",
</span><del>- "geo:%s" % (geo.encode("utf-8"),), params=params,
</del><ins>+ "geo:{0}".format(geo.encode("utf-8"),), params=params,
</ins><span class="cx"> valuetype=Value.VALUETYPE_URI)
</span><span class="cx"> sub.replaceProperty(structured)
</span><span class="cx"> newLocProperty = Property("LOCATION",
</span><del>- "%s\n%s" % (title, street.encode("utf-8")))
</del><ins>+ "{0}\n{1}".format(title, street.encode("utf-8")))
</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, "ImplicitUIDLock:%s" % (hashlib.md5(new_uid).hexdigest(),))
</del><ins>+ yield NamedLock.acquire(self._txn, "ImplicitUIDLock:{0}".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("UID already exists in same calendar.")
</span><span class="cx"> else:
</span><del>- raise UIDExistsElsewhereError("UID already exists in different calendar: %s." % (elsewhere.calendar().name(),))
</del><ins>+ raise UIDExistsElsewhereError("UID already exists in different calendar: {0}".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("Resource modified but immediately deleted by the server.")
</span><span class="cx"> else:
</span><del>- raise AttendeeAllowedError("Attendee cannot create event for Organizer: %s" % (implicit_result,))
</del><ins>+ raise AttendeeAllowedError("Attendee cannot create event for Organizer: {0}".format(implicit_result,))
</ins><span class="cx">
</span><span class="cx"> else:
</span><del>- msg = "Invalid return status code from ImplicitScheduler: %s" % (implicit_result,)
</del><ins>+ msg = "Invalid return status code from ImplicitScheduler: {0}".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("Invalid instance %s when indexing %s in %s" %
- (e.rid, self._name, self._calendar,))
</del><ins>+ self.log.error("Invalid instance {0} when indexing {1} in {2}".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>- "Data corruption detected (%s) in id: %s"
- % (e, self._resourceID)
</del><ins>+ "Data corruption detected ({0}) in id: {1}".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("Calendar data id=%s had unfixable problems:\n %s" %
- (self._resourceID, "\n ".join(unfixed),))
</del><ins>+ self.log.error(
+ "Calendar data id={0} had unfixable problems:\n {1}".format(
+ self._resourceID, "\n ".join(unfixed),
+ )
+ )
</ins><span class="cx">
</span><span class="cx"> if fixed:
</span><del>- self.log.error("Calendar data id=%s had fixable problems:\n %s" %
- (self._resourceID, "\n ".join(fixed),))
</del><ins>+ self.log.error(
+ "Calendar data id={0} had fixable problems:\n {1}".format(
+ self._resourceID, "\n ".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, "ImplicitUIDLock:%s" % (hashlib.md5(calendar.resourceUID()).hexdigest(),))
</del><ins>+ yield NamedLock.acquire(self._txn, "ImplicitUIDLock:{0}".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("Unable to store attachment: %s" % (e,))
</del><ins>+ self.log.error("Unable to store attachment: {0}".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("Missing managed attachment even though ATTACHMENT_CALENDAR_OBJECT indicates it is present: %s" % (managed_id,))
</del><ins>+ self.log.error("Missing managed attachment even though ATTACHMENT_CALENDAR_OBJECT indicates it is present: {0}".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("Unable to store attachment: %s" % (e,))
</del><ins>+ self.log.error("Unable to store attachment: {0}".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("ATTACH")
</span><span class="cx"> removed = False
</span><span class="cx"> for attachment in tuple(attachments):
</span><del>- if attachment.value().endswith("/dropbox/%s/%s" % (
</del><ins>+ if attachment.value().endswith("/dropbox/{0}/{1}".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"> """
</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, "ImplicitUIDLock:%s" % (hashlib.md5(self._uid).hexdigest(),))
</del><ins>+ yield NamedLock.acquire(self._txn, "ImplicitUIDLock:{0}".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("%s.ics" % (olderUID,), calendar_old, ComponentUpdateState.SPLIT_OWNER, split_details=(rid, newerUID, False,))
</del><ins>+ olderObject = yield self.calendar()._createCalendarObjectWithNameInternal(
+ "{0}.ics".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 "inbox", 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("%s.ics" % (olderUID,), ical_old, ComponentUpdateState.SPLIT_ATTENDEE)
</del><ins>+ yield self.calendar()._createCalendarObjectWithNameInternal("{0}.ics".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: "CalendarObjectSplitterWork:%s" % (self.resourceID,))
</del><ins>+ group = property(lambda self: "CalendarObjectSplitterWork:{0}".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 "<%s: %s>" % (self.__class__.__name__, self._attachmentID)
</del><ins>+ return (
+ "<{self.__class__.__name__}: {self._attachmentID}>"
+ .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(".", 1)
</span><span class="cx"> fname = splits[0]
</span><span class="cx"> suffix = splits[1] if len(splits) == 2 else "unknown"
</span><del>- return "%s-%s.%s" % (fname, managed_id[:8], suffix)
</del><ins>+ return "{0}-{1}.{2}".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("MANAGED-ID", self.managedID())
</span><del>- attach.setParameter("FMTTYPE", "%s/%s" % (self.contentType().mediaType, self.contentType().mediaSubtype))
</del><ins>+ attach.setParameter("FMTTYPE", "{0}/{1}".format(self.contentType().mediaType, self.contentType().mediaSubtype))
</ins><span class="cx"> attach.setParameter("FILENAME", self.name())
</span><span class="cx"> attach.setParameter("SIZE", 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"> """
</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"> "il1", ("il1",), "1 Infinite Loop", [],
</span><ins>+ cutype="ROOM",
</ins><span class="cx"> geographicLocation="37.331741,-122.030333",
</span><span class="cx"> streetAddress="1 Infinite Loop, Cupertino, CA 95014"
</span><span class="cx"> ))
</span><span class="cx"> directory.addRecord(TestCalendarStoreDirectoryRecord(
</span><span class="cx"> "il2", ("il2",), "2 Infinite Loop", [],
</span><ins>+ cutype="ROOM",
</ins><span class="cx"> geographicLocation="37.332633,-122.030502",
</span><span class="cx"> streetAddress="2 Infinite Loop, Cupertino, CA 95014"
</span><span class="cx"> ))
</span><span class="cx"> directory.addRecord(TestCalendarStoreDirectoryRecord(
</span><span class="cx"> "room1", ("room1",), "Conference Room One",
</span><span class="cx"> frozenset(("urn:uuid:room1",)),
</span><ins>+ cutype="ROOM",
</ins><span class="cx"> associatedAddress="il1",
</span><span class="cx"> ))
</span><span class="cx"> directory.addRecord(TestCalendarStoreDirectoryRecord(
</span><span class="cx"> "room2", ("room2",), "Conference Room Two",
</span><span class="cx"> frozenset(("urn:uuid:room2",)),
</span><ins>+ cutype="ROOM",
</ins><span class="cx"> associatedAddress="il2",
</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>- "urn:uuid:%s" % (uid,),
- "mailto:%s@example.com" % (uid,),
</del><ins>+ "urn:uuid:{0}".format(uid,),
+ "mailto:{0}@example.com".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"> """
</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"> """
</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("mailto:"):
</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("/principals/"):
</span><span class="cx"> parts = address.split("/")
</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"> "address": "addresses",
</span><span class="cx"> "group": "groups",
</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"> """
</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>- ["mailto:%s" % (emailAddress,)
</del><ins>+ ["mailto:{0}".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):
+ """
+ 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}
+ """
+ params = {} if params is None else params.copy()
+
+ if "PARTSTAT" not in params:
+ params["PARTSTAT"] = "NEEDS-ACTION"
+ if "CN"not in params:
+ if self.fullNames:
+ params["CN"] = list(self.fullNames)[0]
+ if "EMAIL" not in params:
+ if self.emailAddresses:
+ params["EMAIL"] = list(self.emailAddresses)[0]
+ if "CUTYPE" not in params:
+ cuType = self.getCUType()
+ if cuType is not "INDIVIDUAL":
+ params["CUTYPE"] = cuType
+
+ return Property("ATTENDEE", "urn:uuid:" + self.uid.encode("utf-8"), 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"> """
</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>+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2014 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+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 "AS IS" 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.
+ -->
+
+<!DOCTYPE accounts SYSTEM "accounts.dtd">
+
+<directory realm="Test Realm">
+        <record type="user">
+         <short-name>user01</short-name>
+         <uid>10000000-0000-0000-0000-000000000001</uid>
+         <guid>10000000-0000-0000-0000-000000000001</guid>
+         <password>user01</password>
+         <full-name>User 01</full-name>
+         <email>user01@example.com</email>
+        </record>
+        <record type="user">
+         <short-name>user02</short-name>
+         <uid>10000000-0000-0000-0000-000000000002</uid>
+         <guid>10000000-0000-0000-0000-000000000002</guid>
+         <password>user02</password>
+         <full-name>User 02</full-name>
+         <email>user02@example.com</email>
+        </record>
+        <record type="user">
+         <short-name>user03</short-name>
+         <uid>10000000-0000-0000-0000-000000000003</uid>
+         <guid>10000000-0000-0000-0000-000000000003</guid>
+         <password>user03</password>
+         <full-name>User 03</full-name>
+         <email>user03@example.com</email>
+        </record>
+        <record type="user">
+         <short-name>user04</short-name>
+         <uid>10000000-0000-0000-0000-000000000004</uid>
+         <guid>10000000-0000-0000-0000-000000000004</guid>
+         <password>user04</password>
+         <full-name>User 04</full-name>
+         <email>user04@example.com</email>
+        </record>
+        <record type="user">
+         <short-name>user05</short-name>
+         <uid>10000000-0000-0000-0000-000000000005</uid>
+         <guid>10000000-0000-0000-0000-000000000005</guid>
+         <password>user05</password>
+         <full-name>User 05</full-name>
+         <email>user05@example.com</email>
+        </record>
+        <record type="user">
+         <short-name>user06</short-name>
+         <uid>10000000-0000-0000-0000-000000000006</uid>
+         <guid>10000000-0000-0000-0000-000000000006</guid>
+         <password>user06</password>
+         <full-name>User 06</full-name>
+         <email>user06@example.com</email>
+        </record>
+        <record type="user">
+         <short-name>user07</short-name>
+         <uid>10000000-0000-0000-0000-000000000007</uid>
+         <guid>10000000-0000-0000-0000-000000000007</guid>
+         <password>user07</password>
+         <full-name>User 07</full-name>
+         <email>user07@example.com</email>
+        </record>
+        <record type="user">
+         <short-name>user08</short-name>
+         <uid>10000000-0000-0000-0000-000000000008</uid>
+         <guid>10000000-0000-0000-0000-000000000008</guid>
+         <password>user08</password>
+         <full-name>User 08</full-name>
+         <email>user08@example.com</email>
+        </record>
+        <record type="user">
+         <short-name>user09</short-name>
+         <uid>10000000-0000-0000-0000-000000000009</uid>
+         <guid>10000000-0000-0000-0000-000000000009</guid>
+         <password>user09</password>
+         <full-name>User 09</full-name>
+         <email>user09@example.com</email>
+        </record>
+        <record type="user">
+         <short-name>user10</short-name>
+         <uid>10000000-0000-0000-0000-000000000010</uid>
+         <guid>10000000-0000-0000-0000-000000000010</guid>
+         <password>user10</password>
+         <full-name>User 10</full-name>
+         <email>user10@example.com</email>
+        </record>
+        <record type="group">
+         <short-name>group01</short-name>
+         <uid>20000000-0000-0000-0000-000000000001</uid>
+         <guid>20000000-0000-0000-0000-000000000001</guid>
+         <full-name>Group 01</full-name>
+         <email>group01@example.com</email>
+         <member-uid>10000000-0000-0000-0000-000000000001</member-uid>
+        </record>
+        <record type="group">
+         <short-name>group02</short-name>
+         <uid>20000000-0000-0000-0000-000000000002</uid>
+         <guid>20000000-0000-0000-0000-000000000002</guid>
+         <full-name>Group 02</full-name>
+         <email>group02@example.com</email>
+         <member-uid>10000000-0000-0000-0000-000000000006</member-uid>
+         <member-uid>10000000-0000-0000-0000-000000000007</member-uid>
+        </record>
+        <record type="group">
+         <short-name>group03</short-name>
+         <uid>20000000-0000-0000-0000-000000000003</uid>
+         <guid>20000000-0000-0000-0000-000000000003</guid>
+         <full-name>Group 03</full-name>
+         <email>group03@example.com</email>
+         <member-uid>10000000-0000-0000-0000-000000000008</member-uid>
+         <member-uid>10000000-0000-0000-0000-000000000009</member-uid>
+        </record>
+        <record type="group">
+         <short-name>group04</short-name>
+         <uid>20000000-0000-0000-0000-000000000004</uid>
+         <guid>20000000-0000-0000-0000-000000000004</guid>
+         <full-name>Group 04</full-name>
+         <member-uid>20000000-0000-0000-0000-000000000002</member-uid>
+         <member-uid>20000000-0000-0000-0000-000000000003</member-uid>
+         <member-uid>10000000-0000-0000-0000-000000000010</member-uid>
+        </record>
+</directory>
</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 "License");
+# 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 "AS IS" 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.
+##
+
+"""
+ group attendee tests
+"""
+
+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):
+ """
+ GroupAttendeeReconciliation tests
+ """
+
+ @inlineCallbacks
+ def setUp(self):
+ self.patch(config.Scheduling.Options, "AllowGroupAsAttendee", "True")
+
+ yield super(GroupAttendeeReconciliation, self).setUp()
+ self.xmlService = xmlService(self.mktemp(), xmlData=None)
+
+ self.patch(config.DirectoryService.params, "xmlFile",
+ os.path.join(
+ os.path.dirname(__file__), "accounts", "groupAttendeeAccounts.xml"
+ )
+ )
+ self.patch(config.ResourceService.params, "xmlFile",
+ os.path.join(
+ os.path.dirname(__file__), "accounts", "resources.xml"
+ )
+ )
+ self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory, directoryFromConfig(config))
+ yield self.populate()
+
+ self.paths = {}
+
+
+ def storeUnderTest(self):
+ """
+ Create and return a L{CalendarStore} for testing.
+ """
+ return self._sqlCalendarStore
+
+
+ @inlineCallbacks
+ def populate(self):
+ yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+ self.notifierFactory.reset()
+
+ requirements = {
+ "10000000-0000-0000-0000-000000000001" : {
+ "calendar" : {}
+ },
+ }
+
+ @inlineCallbacks
+ def test_simplePUT(self):
+ """
+ Test that group attendee is expanded on PUT
+ """
+ calendar = yield self.calendarUnderTest(name="calendar", home="10000000-0000-0000-0000-000000000001")
+
+ data_put_1 = """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"""
+
+ data_get_1 = """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="urn:uuid:20000000-0000-0000-0000-000000000002";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="urn:uuid:20000000-0000-0000-0000-000000000002";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
+"""
+
+ vcalendar1 = Component.fromString(data_put_1)
+ yield calendar.createCalendarObjectWithName("data1.ics", vcalendar1)
+ yield self.commit()
+
+ cobj1 = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="10000000-0000-0000-0000-000000000001")
+ vcalendar2 = yield cobj1.component()
+ self.assertEqual(normalize_iCalStr(vcalendar2), normalize_iCalStr(data_get_1))
+
+
+ @inlineCallbacks
+ def test_unknownPUT(self):
+ """
+ Test unknown group with CUTYPE=GROUP handled
+ """
+ calendar = yield self.calendarUnderTest(name="calendar", home="10000000-0000-0000-0000-000000000001")
+
+ data_put_1 = """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"""
+
+ data_get_1 = """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
+"""
+
+ vcalendar1 = Component.fromString(data_put_1)
+ yield calendar.createCalendarObjectWithName("data1.ics", vcalendar1)
+ yield self.commit()
+
+ cobj1 = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="10000000-0000-0000-0000-000000000001")
+ vcalendar2 = yield cobj1.component()
+ self.assertEqual(normalize_iCalStr(vcalendar2), normalize_iCalStr(data_get_1))
+
+
+ @inlineCallbacks
+ def test_primaryAttendeeInGroupPUT(self):
+ """
+ Test that primary attendee also in group remains primary
+ """
+ calendar = yield self.calendarUnderTest(name="calendar", home="10000000-0000-0000-0000-000000000001")
+
+ data_put_1 = """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"""
+
+ data_get_1 = """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
+"""
+ vcalendar1 = Component.fromString(data_put_1)
+ yield calendar.createCalendarObjectWithName("data1.ics", vcalendar1)
+ yield self.commit()
+
+ cobj1 = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="10000000-0000-0000-0000-000000000001")
+ vcalendar1 = yield cobj1.component()
+ self.assertEqual(normalize_iCalStr(vcalendar1), normalize_iCalStr(data_get_1))
+
+
+ @inlineCallbacks
+ def test_nestedPUT(self):
+ """
+ Test that nested groups are expanded expanded on PUT
+ """
+ calendar = yield self.calendarUnderTest(name="calendar", home="10000000-0000-0000-0000-000000000001")
+
+ data_put_1 = """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"""
+
+ data_get_1 = """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="urn:uuid:20000000-0000-0000-0000-000000000004";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="urn:uuid:20000000-0000-0000-0000-000000000004";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="urn:uuid:20000000-0000-0000-0000-000000000004";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="urn:uuid:20000000-0000-0000-0000-000000000004";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="urn:uuid:20000000-0000-0000-0000-000000000004";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
+"""
+
+ vcalendar1 = Component.fromString(data_put_1)
+ yield calendar.createCalendarObjectWithName("data1.ics", vcalendar1)
+ yield self.commit()
+
+ cobj1 = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="10000000-0000-0000-0000-000000000001")
+ vcalendar2 = yield cobj1.component()
+ self.assertEqual(normalize_iCalStr(vcalendar2), normalize_iCalStr(data_get_1))
+
+
+ @inlineCallbacks
+ def test_twoGroupPUT(self):
+ """
+ Test that expanded users in two primary groups have groups in MEMBERS param
+ """
+ calendar = yield self.calendarUnderTest(name="calendar", home="10000000-0000-0000-0000-000000000001")
+
+ data_put_1 = """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"""
+
+ data_get_1 = """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="urn:uuid:20000000-0000-0000-0000-000000000002","urn:uuid:20000000-0000-0000-0000-000000000004";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="urn:uuid:20000000-0000-0000-0000-000000000002","urn:uuid:20000000-0000-0000-0000-000000000004";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="urn:uuid:20000000-0000-0000-0000-000000000004";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="urn:uuid:20000000-0000-0000-0000-000000000004";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="urn:uuid:20000000-0000-0000-0000-000000000004";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
+"""
+
+ vcalendar1 = Component.fromString(data_put_1)
+ yield calendar.createCalendarObjectWithName("data1.ics", vcalendar1)
+ yield self.commit()
+
+ cobj1 = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="10000000-0000-0000-0000-000000000001")
+ 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"__top_group_1__")
</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, "553eb54e3bbb26582198ee04541dbee4")
</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"__top_group_1__"
</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"__top_group_1__"
</span><span class="cx"> hash = "553eb54e3bbb26582198ee04541dbee4"
</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"Top Group 1", 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"> """
</span><span class="cx"> Ensure external proxy assignment diffing works
</span><span class="lines">@@ -389,3 +390,4 @@
</span><span class="cx"> {"D": ("7", "8"), "C": ("4", "5"), "A": ("1", "2")},
</span><span class="cx"> )
</span><span class="cx"> )
</span><ins>+
</ins></span></pre>
</div>
</div>
</body>
</html>