Skip to content
Permalink
Browse files

Keep location/attendee/x-apple-structured-location properties in sync

  • Loading branch information...
m0rgen committed Jun 26, 2015
1 parent 3ed51df commit 53dcad1699a144f4589e01fb23dac1966844071c
Showing with 248 additions and 34 deletions.
  1. +0 −7 twistedcaldav/ical.py
  2. +0 −3 twistedcaldav/test/test_icalendar.py
  3. +101 −19 txdav/caldav/datastore/sql.py
  4. +147 −5 txdav/caldav/datastore/test/test_sql.py
@@ -3541,13 +3541,6 @@ def normalizeCalendarUserAddresses(
if name:
if name != oldCN:
prop.setParameter("CN", name)

# Also adjust any previously matching location property
if cutype == "ROOM":
location = component.getProperty("LOCATION")
if location is not None:
if location.value() == oldCN:
location.setValue(name)
else:
prop.removeParameter("CN")

@@ -8241,9 +8241,6 @@ def lookupFunction(cuaddr, ignored1, ignored2):

yield component.normalizeCalendarUserAddresses(lookupFunction, None, toCanonical=True)

# Location value changed
prop = component.mainComponent().getProperty("LOCATION")
self.assertEquals(prop.value(), "{Restricted} Buzz")
prop = component.getAttendeeProperty(("urn:x-uid:buzz",))
self.assertEquals("urn:x-uid:buzz", prop.value())
self.assertEquals(prop.parameterValue("CN"), "{Restricted} Buzz")
@@ -3296,14 +3296,39 @@ def addStructuredLocation(self, component):
Scan the component for ROOM attendees; if any are associated with an
address record which has street address and geo coordinates, add an
X-APPLE-STRUCTURED-LOCATION property and update the LOCATION property
to contain the name and street address.
to contain the name and street address. X-APPLE-STRUCTURED-LOCATION
with X-CUADDR but no corresponding ATTENDEE are removed.
"""
dir = self.directoryService()

changed = False
cache = {}
dir = self.directoryService()

for sub in component.subcomponents():
locations = []
removed = False
existingLocationProps = list(sub.properties("LOCATION"))
if len(existingLocationProps) == 0:
existingLocationValue = ""
else:
existingLocationValue = existingLocationProps[0].value()
existingLocations = []
for value in existingLocationValue.split(";"):
if value:
existingLocations.append(value.strip())

# index the structured locations on X-CUADDR and X-TITLE
allStructured = {
"cua": {},
"title": {}
}
for structured in sub.properties("X-APPLE-STRUCTURED-LOCATION"):
cuAddr = structured.parameterValue("X-CUADDR")
if cuAddr:
allStructured["cua"][cuAddr] = structured
else:
title = structured.parameterValue("X-TITLE")
if title:
allStructured["title"][title] = structured

for attendee in sub.getAllAttendeeProperties():
if attendee.parameterValue("CUTYPE") == "ROOM":
value = attendee.value()
@@ -3326,32 +3351,89 @@ def addStructuredLocation(self, component):
# Use the cached data if present
entry = cache[value]
if entry is not None:

street, geo, title = entry
newLocationValue = "{0}\n{1}".format(title, street.encode("utf-8"))

# Is there already a structured location property for
# this attendee? If so, we'll update it.
# Unfortunately, we can only depend on X-CUADDR
# going forward, but there is going to be old existing
# X-APPLE-STRUCTURED-LOCATIONs that haven't yet had
# those added. So let's first look up by X-CUADDR and
# then by X-TITLE.
if value in allStructured["cua"]:
structured = allStructured["cua"][value]
elif title in allStructured["title"]:
structured = allStructured["title"][title]
else:
structured = None

params = {
"X-ADDRESS": street,
"X-APPLE-RADIUS": "71",
"X-TITLE": title,
"X-CUADDR": value,
}
structured = Property(
"X-APPLE-STRUCTURED-LOCATION",
geo.encode("utf-8"), params=params,
valuetype=Value.VALUETYPE_URI
)

# The first time we have any X- prop, remove all existing ones
if not removed:
sub.removeProperty("X-APPLE-STRUCTURED-LOCATION")
removed = True
sub.addProperty(structured)
locations.append("{0}\n{1}".format(title, street.encode("utf-8")))

# Update the LOCATION if X-'s were added
if locations:
if structured is None:
# Create a new one
prevTitle = attendee.parameterValue("CN")
structured = Property(
"X-APPLE-STRUCTURED-LOCATION",
geo.encode("utf-8"), params=params,
valuetype=Value.VALUETYPE_URI
)
changed = True
sub.addProperty(structured)
else:
# Update existing one
prevTitle = structured.parameterValue("X-TITLE")
for paramName, paramValue in params.iteritems():
prevValue = structured.parameterValue(paramName)
if paramValue != prevValue:
structured.setParameter(paramName, paramValue)
changed = True

if changed:
# Replace old location values with the new ones
for i in xrange(len(existingLocations)):
existingLocation = existingLocations[i]
if (
prevTitle is not None and
(
# it's either an exact match or matches
# up to the newline which precedes the
# street address
existingLocation == prevTitle or
existingLocation.startswith("{}\n".format(prevTitle))
)
):
existingLocations[i] = newLocationValue
break
else:
existingLocations.append(newLocationValue)

# Remove any server-generated structured locations without an ATTENDEE
for structured in sub.properties("X-APPLE-STRUCTURED-LOCATION"):
cuAddr = structured.parameterValue("X-CUADDR")
if cuAddr is not None: # therefore it's one that requires an ATTENDEE...
attendeeProp = sub.getAttendeeProperty((cuAddr,))
if attendeeProp is None: # ...remove it if no matching ATTENDEE
sub.removeProperty(structured)

# Update the LOCATION
newLocationValue = ";".join(existingLocations)
if newLocationValue != existingLocationValue:
newLocProperty = Property(
"LOCATION",
"; ".join(locations)
newLocationValue
)
sub.replaceProperty(newLocProperty)
changed = True

if changed:
self._componentChanged = True


@inlineCallbacks
@@ -21,6 +21,7 @@

from pycalendar.datetime import DateTime
from pycalendar.timezone import Timezone
from pycalendar.value import Value


from txweb2 import responsecode
@@ -38,7 +39,7 @@
from twistedcaldav.caldavxml import CalendarDescription
from twistedcaldav.stdconfig import config
from twistedcaldav.dateops import datetimeMktime
from twistedcaldav.ical import Component, normalize_iCalStr, diff_iCalStrs
from twistedcaldav.ical import Component, normalize_iCalStr, diff_iCalStrs, Property
from twistedcaldav.instance import InvalidOverriddenInstanceError
from twistedcaldav.timezones import TimezoneCache, readVTZ, TimezoneException

@@ -2682,7 +2683,7 @@ def test_setComponent_structuredLocation(self):
calendar_name="calendar",
home="user01"
)
comp = yield cobj.component()
comp = yield cobj.componentForUser()
components = list(comp.subcomponents())

# Check first component
@@ -2773,14 +2774,14 @@ def test_setComponent_structuredLocation_Multiple(self):
calendar_name="calendar",
home="user01"
)
comp = yield cobj.component()
comp = yield cobj.componentForUser()
components = list(comp.subcomponents())

# Check first component
locProp = components[0].getProperty("LOCATION")
self.assertEquals(
locProp.value(),
"Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014; Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
"Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014;Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
)
structProps = tuple(components[0].properties("X-APPLE-STRUCTURED-LOCATION"))
self.assertEqual(len(structProps), 2)
@@ -2817,7 +2818,7 @@ def test_setComponent_structuredLocation_Multiple(self):
locProp = components[0].getProperty("LOCATION")
self.assertEquals(
locProp.value(),
"Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014; Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
"Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014;Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
)
structProps = tuple(components[0].properties("X-APPLE-STRUCTURED-LOCATION"))
self.assertEqual(len(structProps), 2)
@@ -2829,6 +2830,147 @@ def test_setComponent_structuredLocation_Multiple(self):
yield self.commit()


@inlineCallbacks
def test_setComponent_structuredLocation_Mixed(self):
"""
Verify adding a location that's not in the directory to an event which
already has a location that's in the directory keeps them both.
X-APPLE-STRUCTURED-LOCATION properties which have X-CUADDR but no
corresponding ATTENDEE are removed.
"""

data = """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN
BEGIN:VEVENT
UID:561F5DBB-3F38-4B3A-986F-DD05CBAF554F
DTSTART:20131211T164500Z
DURATION:PT1H
ATTENDEE;CN=Old Room with Address 1;CUTYPE=ROOM;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPAN
T;SCHEDULE-STATUS=2.0:urn:x-uid:room-addr-1
ATTENDEE;CN=Room with Address 2;CUTYPE=ROOM;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPAN
T;SCHEDULE-STATUS=2.0:urn:x-uid:room-addr-2
ATTENDEE;CN=Mercury Seven;CUTYPE=ROOM;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPAN
T;SCHEDULE-STATUS=2.0:urn:x-uid:mercury
ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;EMAIL=user01@example.com;PARTSTAT=AC
CEPTED:urn:x-uid:user01
X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS="1 Infinite Loop, Cupertin
o, CA 95014";X-APPLE-RADIUS=71;X-CUADDR="urn:x-uid:room-addr-1";X-TITLE=O
ld Room with Address 1:geo:37.331741,-122.030333
X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS="2 Infinite Loop, Cupertin
o, CA 95014";X-APPLE-RADIUS=71;X-TITLE=Room with Address 2:geo:37.332633,
-122.030502
X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS=123 Main St;X-APPLE-RADIUS
=14164;X-TITLE=Mercury Seven:geo:37.351164,-122.032686
CREATED:20131211T221854Z
DTSTAMP:20131211T230632Z
ORGANIZER;CN=User 01;EMAIL=user01@example.com:urn:x-uid:user01
SEQUENCE:1
SUMMARY:locations
LOCATION:Old Room with Address 1;Unstructured Location; Mercury Seven; Room with Address 2
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n")

calendar = yield self.calendarUnderTest(name="calendar", home="user01")
yield calendar.createCalendarObjectWithName(
"structured.ics",
Component.fromString(data)
)

yield self.commit()

cobj = yield self.calendarObjectUnderTest(
name="structured.ics",
calendar_name="calendar",
home="user01"
)
comp = yield cobj.componentForUser()
components = list(comp.subcomponents())

# Check first component -- LOCATION now has the street addresses, and
# location values that don't have an ATTENDEE or X-APPLE-STRUCTURED-LOCATIONs
# are retained
locProp = components[0].getProperty("LOCATION")
self.assertEquals(
locProp.value(),
"Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014;Unstructured Location;Mercury Seven;Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
)
structProps = tuple(components[0].properties("X-APPLE-STRUCTURED-LOCATION"))
self.assertEqual(len(structProps), 3)
self.assertEquals(
structProps[0].value(),
"geo:37.331741,-122.030333",
)
# Make sure server has also added X-CUADDR
self.assertEquals(
structProps[0].parameterValue("X-CUADDR"),
"urn:x-uid:room-addr-1"
)

# Client now adds a location not in the directory:
comp = comp.duplicate()
main = comp.mainComponent()
main.replaceProperty(Property("LOCATION", "Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014; Unstructured Location; Falafel Stop\n1325 Sunnyvale Saratoga, Sunnyvale, CA 94087;Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"))

params = {
"X-ADDRESS": "1325 Sunnyvale Saratoga Rd",
"X-APPLE-RADIUS": "14164",
"X-TITLE": "Falafel Stop",
}
structured = Property(
"X-APPLE-STRUCTURED-LOCATION",
"geo:37.351164,-122.032686", params=params,
valuetype=Value.VALUETYPE_URI
)
main.addProperty(structured)

# ...plus let's prove we clean up structured locations which have X-CUADDR
# but no matching ATTENDEE
params = {
"X-ADDRESS": "1122 Boogie Woogie Ave",
"X-APPLE-RADIUS": "14164",
"X-TITLE": "Home of the Boogie, House of the Funk",
"X-CUADDR": "urn:x-uid:boogie-home",
}
structured = Property(
"X-APPLE-STRUCTURED-LOCATION",
"geo:37.351164,-122.032686", params=params,
valuetype=Value.VALUETYPE_URI
)
main.addProperty(structured)

# Store the new component and let the server do its thing
yield cobj.setComponent(comp)
yield self.commit()

cobj = yield self.calendarObjectUnderTest(
name="structured.ics",
calendar_name="calendar",
home="user01"
)
comp = yield cobj.componentForUser()
components = list(comp.subcomponents())

# Check first component
structProps = tuple(components[0].properties("X-APPLE-STRUCTURED-LOCATION"))
self.assertEqual(len(structProps), 4)
self.assertEquals(
set([structProp.parameterValue("X-TITLE") for structProp in structProps]),
set(("Room with Address 1", "Room with Address 2", "Falafel Stop", "Mercury Seven"))
)

locProp = components[0].getProperty("LOCATION")
self.assertEquals(
locProp.value(),
"Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014;Unstructured Location;Falafel Stop\n1325 Sunnyvale Saratoga, Sunnyvale, CA 94087;Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
)

yield self.commit()


@inlineCallbacks
def test_setComponent_externalPrincipal(self):
"""

0 comments on commit 53dcad1

Please sign in to comment.
You can’t perform that action at this time.