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