[CalendarServer-changes] [9826] CalendarServer/trunk/twistedcaldav/ical.py
source_changes at macosforge.org
source_changes at macosforge.org
Thu Sep 20 09:05:16 PDT 2012
Revision: 9826
http://trac.calendarserver.org//changeset/9826
Author: cdaboo at apple.com
Date: 2012-09-20 09:05:16 -0700 (Thu, 20 Sep 2012)
Log Message:
-----------
Whitespace clean-up.
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/ical.py
Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py 2012-09-20 01:53:25 UTC (rev 9825)
+++ CalendarServer/trunk/twistedcaldav/ical.py 2012-09-20 16:05:16 UTC (rev 9826)
@@ -39,7 +39,7 @@
from twext.web2.dav.util import allDataFromStream
from twistedcaldav.config import config
-from twistedcaldav.dateops import timeRangesOverlap, normalizeForIndex, differenceDateTime,\
+from twistedcaldav.dateops import timeRangesOverlap, normalizeForIndex, differenceDateTime, \
normalizeForExpand
from twistedcaldav.instance import InstanceList
from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
@@ -76,66 +76,66 @@
# Structure: propname: (<default value>, <parameter defaults dict>)
normalizeProps = {
- "CALSCALE": ("GREGORIAN", {"VALUE": "TEXT"}),
- "METHOD": (None, {"VALUE": "TEXT"}),
- "PRODID": (None, {"VALUE": "TEXT"}),
- "VERSION": (None, {"VALUE": "TEXT"}),
- "ATTACH": (None, {"VALUE": "URI"}),
- "CATEGORIES": (None, {"VALUE": "TEXT"}),
- "CLASS": (None, {"VALUE": "TEXT"}),
- "COMMENT": (None, {"VALUE": "TEXT"}),
- "DESCRIPTION": (None, {"VALUE": "TEXT"}),
- "GEO": (None, {"VALUE": "FLOAT"}),
- "LOCATION": (None, {"VALUE": "TEXT"}),
+ "CALSCALE": ("GREGORIAN", {"VALUE": "TEXT"}),
+ "METHOD": (None, {"VALUE": "TEXT"}),
+ "PRODID": (None, {"VALUE": "TEXT"}),
+ "VERSION": (None, {"VALUE": "TEXT"}),
+ "ATTACH": (None, {"VALUE": "URI"}),
+ "CATEGORIES": (None, {"VALUE": "TEXT"}),
+ "CLASS": (None, {"VALUE": "TEXT"}),
+ "COMMENT": (None, {"VALUE": "TEXT"}),
+ "DESCRIPTION": (None, {"VALUE": "TEXT"}),
+ "GEO": (None, {"VALUE": "FLOAT"}),
+ "LOCATION": (None, {"VALUE": "TEXT"}),
"PERCENT-COMPLETE": (None, {"VALUE": "INTEGER"}),
- "PRIORITY": (0, {"VALUE": "INTEGER"}),
- "RESOURCES": (None, {"VALUE": "TEXT"}),
- "STATUS": (None, {"VALUE": "TEXT"}),
- "SUMMARY": (None, {"VALUE": "TEXT"}),
- "COMPLETED": (None, {"VALUE": "DATE-TIME"}),
- "DTEND": (None, {"VALUE": "DATE-TIME"}),
- "DUE": (None, {"VALUE": "DATE-TIME"}),
- "DTSTART": (None, {"VALUE": "DATE-TIME"}),
- "DURATION": (None, {"VALUE": "DURATION"}),
- "FREEBUSY": (None, {"VALUE": "PERIOD"}),
- "TRANSP": ("OPAQUE", {"VALUE": "TEXT"}),
- "TZID": (None, {"VALUE": "TEXT"}),
- "TZNAME": (None, {"VALUE": "TEXT"}),
+ "PRIORITY": (0, {"VALUE": "INTEGER"}),
+ "RESOURCES": (None, {"VALUE": "TEXT"}),
+ "STATUS": (None, {"VALUE": "TEXT"}),
+ "SUMMARY": (None, {"VALUE": "TEXT"}),
+ "COMPLETED": (None, {"VALUE": "DATE-TIME"}),
+ "DTEND": (None, {"VALUE": "DATE-TIME"}),
+ "DUE": (None, {"VALUE": "DATE-TIME"}),
+ "DTSTART": (None, {"VALUE": "DATE-TIME"}),
+ "DURATION": (None, {"VALUE": "DURATION"}),
+ "FREEBUSY": (None, {"VALUE": "PERIOD"}),
+ "TRANSP": ("OPAQUE", {"VALUE": "TEXT"}),
+ "TZID": (None, {"VALUE": "TEXT"}),
+ "TZNAME": (None, {"VALUE": "TEXT"}),
"TZOFFSETFROM": (None, {"VALUE": "UTC-OFFSET"}),
- "TZOFFSETTO": (None, {"VALUE": "UTC-OFFSET"}),
- "TZURL": (None, {"VALUE": "URI"}),
- "ATTENDEE": (None, {
- "VALUE": "CAL-ADDRESS",
- "CUTYPE": "INDIVIDUAL",
- "ROLE": "REQ-PARTICIPANT",
- "PARTSTAT": "NEEDS-ACTION",
- "RSVP": "FALSE",
+ "TZOFFSETTO": (None, {"VALUE": "UTC-OFFSET"}),
+ "TZURL": (None, {"VALUE": "URI"}),
+ "ATTENDEE": (None, {
+ "VALUE": "CAL-ADDRESS",
+ "CUTYPE": "INDIVIDUAL",
+ "ROLE": "REQ-PARTICIPANT",
+ "PARTSTAT": "NEEDS-ACTION",
+ "RSVP": "FALSE",
"SCHEDULE-AGENT": "SERVER",
}),
- "CONTACT": (None, {"VALUE": "TEXT"}),
- "ORGANIZER": (None, {"VALUE": "CAL-ADDRESS"}),
+ "CONTACT": (None, {"VALUE": "TEXT"}),
+ "ORGANIZER": (None, {"VALUE": "CAL-ADDRESS"}),
"RECURRENCE-ID": (None, {"VALUE": "DATE-TIME"}),
- "RELATED-TO": (None, {"VALUE": "TEXT"}),
- "URL": (None, {"VALUE": "URI"}),
- "UID": (None, {"VALUE": "TEXT"}),
- "EXDATE": (None, {"VALUE": "DATE-TIME"}),
- "EXRULE": (None, {"VALUE": "RECUR"}),
- "RDATE": (None, {"VALUE": "DATE-TIME"}),
- "RRULE": (None, {"VALUE": "RECUR"}),
- "ACTION": (None, {"VALUE": "TEXT"}),
- "REPEAT": (0, {"VALUE": "INTEGER"}),
- "TRIGGER": (None, {"VALUE": "DURATION"}),
- "CREATED": (None, {"VALUE": "DATE-TIME"}),
- "DTSTAMP": (None, {"VALUE": "DATE-TIME"}),
+ "RELATED-TO": (None, {"VALUE": "TEXT"}),
+ "URL": (None, {"VALUE": "URI"}),
+ "UID": (None, {"VALUE": "TEXT"}),
+ "EXDATE": (None, {"VALUE": "DATE-TIME"}),
+ "EXRULE": (None, {"VALUE": "RECUR"}),
+ "RDATE": (None, {"VALUE": "DATE-TIME"}),
+ "RRULE": (None, {"VALUE": "RECUR"}),
+ "ACTION": (None, {"VALUE": "TEXT"}),
+ "REPEAT": (0, {"VALUE": "INTEGER"}),
+ "TRIGGER": (None, {"VALUE": "DURATION"}),
+ "CREATED": (None, {"VALUE": "DATE-TIME"}),
+ "DTSTAMP": (None, {"VALUE": "DATE-TIME"}),
"LAST-MODIFIED": (None, {"VALUE": "DATE-TIME"}),
- "SEQUENCE": (0, {"VALUE": "INTEGER"}),
+ "SEQUENCE": (0, {"VALUE": "INTEGER"}),
"REQUEST-STATUS": (None, {"VALUE": "TEXT"}),
}
# transformations to apply to property values
normalizePropsValue = {
- "ATTENDEE": normalizeCUAddr,
- "ORGANIZER": normalizeCUAddr,
+ "ATTENDEE": normalizeCUAddr,
+ "ORGANIZER": normalizeCUAddr,
}
ignoredComponents = ("VTIMEZONE", "X-CALENDARSERVER-PERUSER",)
@@ -147,6 +147,8 @@
class InvalidICalendarDataError(ValueError):
pass
+
+
class Property (object):
"""
iCalendar Property
@@ -176,30 +178,53 @@
self._parent = parent
- def __str__(self): return str(self._pycalendar)
- def __repr__(self): return "<%s: %r: %r>" % (self.__class__.__name__, self.name(), self.value())
+ def __str__(self):
+ return str(self._pycalendar)
+
+
+ def __repr__(self):
+ return "<%s: %r: %r>" % (self.__class__.__name__, self.name(), self.value())
+
+
def __hash__(self):
return hash(str(self))
- def __ne__(self, other): return not self.__eq__(other)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
def __eq__(self, other):
- if not isinstance(other, Property): return False
+ if not isinstance(other, Property):
+ return False
return self._pycalendar == other._pycalendar
- def __gt__(self, other): return not (self.__eq__(other) or self.__lt__(other))
+
+ def __gt__(self, other):
+ return not (self.__eq__(other) or self.__lt__(other))
+
+
def __lt__(self, other):
my_name = self.name()
other_name = other.name()
- if my_name < other_name: return True
- if my_name > other_name: return False
+ if my_name < other_name:
+ return True
+ if my_name > other_name:
+ return False
return self.value() < other.value()
- def __ge__(self, other): return self.__eq__(other) or self.__gt__(other)
- def __le__(self, other): return self.__eq__(other) or self.__lt__(other)
+ def __ge__(self, other):
+ return self.__eq__(other) or self.__gt__(other)
+
+
+ def __le__(self, other):
+ return self.__eq__(other) or self.__lt__(other)
+
+
def duplicate(self):
"""
Duplicate this object and all its contents.
@@ -208,21 +233,30 @@
# FIXME: does the parent need to be set in this case?
return Property(None, None, None, pycalendar=self._pycalendar.duplicate())
- def name(self): return self._pycalendar.getName()
- def value(self): return self._pycalendar.getValue().getValue()
+ def name(self):
+ return self._pycalendar.getName()
- def strvalue(self): return str(self._pycalendar.getValue())
+ def value(self):
+ return self._pycalendar.getValue().getValue()
+
+
+ def strvalue(self):
+ return str(self._pycalendar.getValue())
+
+
def _markAsDirty(self):
parent = getattr(self, "_parent", None)
if parent is not None:
parent._markAsDirty()
+
def setValue(self, value):
self._pycalendar.setValue(value)
self._markAsDirty()
+
def parameterNames(self):
"""
Returns a set containing parameter names for this property.
@@ -233,6 +267,7 @@
result.add(pyattr.getName())
return result
+
def parameterValue(self, name, default=None):
"""
Returns a single value for the given parameter. Raises
@@ -243,21 +278,26 @@
except KeyError:
return default
+
def hasParameter(self, paramname):
return self._pycalendar.hasAttribute(paramname)
+
def setParameter(self, paramname, paramvalue):
self._pycalendar.replaceAttribute(PyCalendarAttribute(paramname, paramvalue))
self._markAsDirty()
+
def removeParameter(self, paramname):
self._pycalendar.removeAttributes(paramname)
self._markAsDirty()
+
def removeAllParameters(self):
self._pycalendar.setAttributes({})
self._markAsDirty()
+
def removeParameterValue(self, paramname, paramvalue):
paramname = paramname.upper()
@@ -270,6 +310,7 @@
self._pycalendar.removeAttributes(paramname)
self._markAsDirty()
+
def containsTimeRange(self, start, end, defaulttz=None):
"""
Determines whether this property contains a date/date-time within the specified
@@ -288,25 +329,26 @@
allowedNames = ["COMPLETED", "CREATED", "DTSTAMP", "LAST-MODIFIED"]
if self.name() not in allowedNames:
return False
-
+
# get date/date-time value
dt = self._pycalendar.getValue().getValue()
assert isinstance(dt, PyCalendarDateTime), "Not a date/date-time value: %r" % (self,)
-
+
return timeRangesOverlap(dt, None, start, end, defaulttz)
+
class Component (object):
"""
X{iCalendar} component.
"""
# Private Event access levels.
- ACCESS_PROPERTY = "X-CALENDARSERVER-ACCESS"
- ACCESS_PUBLIC = "PUBLIC"
- ACCESS_PRIVATE = "PRIVATE"
+ ACCESS_PROPERTY = "X-CALENDARSERVER-ACCESS"
+ ACCESS_PUBLIC = "PUBLIC"
+ ACCESS_PRIVATE = "PRIVATE"
ACCESS_CONFIDENTIAL = "CONFIDENTIAL"
- ACCESS_RESTRICTED = "RESTRICTED"
+ ACCESS_RESTRICTED = "RESTRICTED"
accessMap = {
"PUBLIC" : ACCESS_PUBLIC,
@@ -317,10 +359,10 @@
confidentialPropertiesMap = {
"VCALENDAR": ("PRODID", "VERSION", "CALSCALE", ACCESS_PROPERTY),
- "VEVENT": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "TRANSP", "DTSTART", "DTEND", "DURATION", "RRULE", "RDATE", "EXRULE", "EXDATE", ),
- "VTODO": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "DTSTART", "COMPLETED", "DUE", "DURATION", "RRULE", "RDATE", "EXRULE", "EXDATE", ),
- "VJOURNAL": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "DTSTART", "RRULE", "RDATE", "EXRULE", "EXDATE", ),
- "VFREEBUSY": ("UID", "DTSTAMP", "DTSTART", "DTEND", "DURATION", "FREEBUSY", ),
+ "VEVENT": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "TRANSP", "DTSTART", "DTEND", "DURATION", "RRULE", "RDATE", "EXRULE", "EXDATE",),
+ "VTODO": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "DTSTART", "COMPLETED", "DUE", "DURATION", "RRULE", "RDATE", "EXRULE", "EXDATE",),
+ "VJOURNAL": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "DTSTART", "RRULE", "RDATE", "EXRULE", "EXDATE",),
+ "VFREEBUSY": ("UID", "DTSTAMP", "DTSTART", "DTEND", "DURATION", "FREEBUSY",),
"VTIMEZONE": None,
}
extraRestrictedProperties = ("SUMMARY", "LOCATION",)
@@ -335,6 +377,7 @@
"""
return clazz.fromString(string)
+
@classmethod
def allFromStream(clazz, stream):
"""
@@ -342,6 +385,7 @@
"""
return clazz.fromStream(stream)
+
@classmethod
def fromString(clazz, string):
"""
@@ -355,13 +399,14 @@
else:
# Valid utf-8 please
string.decode("utf-8")
-
+
# No BOMs please
if string[:3] == codecs.BOM_UTF8:
string = string[3:]
return clazz.fromStream(StringIO.StringIO(string))
+
@classmethod
def fromStream(clazz, stream):
"""
@@ -382,6 +427,7 @@
raise InvalidICalendarDataError("%s\n%s" % (errmsg, stream.read(),))
return clazz(None, pycalendar=cal)
+
@classmethod
def fromIStream(clazz, stream):
"""
@@ -396,7 +442,8 @@
# A better solution would parse directly and incrementally from the
# request stream.
#
- def parse(data): return clazz.fromString(data)
+ def parse(data):
+ return clazz.fromString(data)
return allDataFromStream(IStream(stream), parse)
@@ -437,11 +484,11 @@
if "parent" in kwargs:
parent = kwargs["parent"]
-
+
if parent is not None:
if not isinstance(parent, Component):
raise TypeError("Not a Component: %r" % (parent,))
-
+
self._parent = parent
else:
self._parent = None
@@ -450,6 +497,7 @@
self._pycalendar = PyCalendar(add_defaults=False) if name == "VCALENDAR" else PyCalendar.makeComponent(name, None)
self._parent = None
+
def __str__(self):
"""
NB This does not automatically include timezones in VCALENDAR objects.
@@ -460,6 +508,7 @@
self._cachedCopy = str(self._pycalendar)
return self._cachedCopy
+
def _markAsDirty(self):
"""
Invalidate the cached copy of serialized icalendar data
@@ -469,38 +518,46 @@
if parent is not None:
parent._markAsDirty()
+
def __repr__(self):
return "<%s: %r>" % (self.__class__.__name__, str(self._pycalendar))
+
def __hash__(self):
return hash(str(self))
+
def __ne__(self, other):
return not self.__eq__(other)
+
def __eq__(self, other):
if not isinstance(other, Component):
return False
return self._pycalendar == other._pycalendar
+
def getTextWithTimezones(self, includeTimezones):
"""
Return text representation and include timezones if the option is on
"""
assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
-
+
return self._pycalendar.getText(includeTimezones=includeTimezones)
+
# FIXME: Should this not be in __eq__?
def same(self, other):
return self._pycalendar == other._pycalendar
-
+
+
def name(self):
"""
@return: the name of the iCalendar type of this component.
"""
return self._pycalendar.getType()
+
def mainType(self):
"""
Determine the primary type of iCal component in this calendar.
@@ -508,7 +565,7 @@
@raise: L{InvalidICalendarDataError} if there is more than one primary type.
"""
assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
-
+
mtype = None
for component in self.subcomponents():
if component.name() in ignoredComponents:
@@ -517,9 +574,10 @@
raise InvalidICalendarDataError("Component contains more than one type of primary type: %r" % (self,))
else:
mtype = component.name()
-
+
return mtype
-
+
+
def mainComponent(self, allow_multiple=False):
"""
Return the primary iCal component in this calendar.
@@ -527,7 +585,7 @@
@raise: L{InvalidICalendarDataError} if there is more than one primary type.
"""
assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
-
+
result = None
for component in self.subcomponents():
if component.name() in ignoredComponents:
@@ -538,9 +596,10 @@
result = component
if allow_multiple:
break
-
+
return result
-
+
+
def masterComponent(self):
"""
Return the master iCal component in this calendar.
@@ -548,15 +607,16 @@
or C{None} if there isn't one.
"""
assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
-
+
for component in self.subcomponents():
if component.name() in ignoredComponents:
continue
if not component.hasProperty("RECURRENCE-ID"):
return component
-
+
return None
-
+
+
def overriddenComponent(self, recurrence_id):
"""
Return the overridden iCal component in this calendar matching the supplied RECURRENCE-ID property.
@@ -568,7 +628,7 @@
or C{None} if there isn't one.
"""
assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
-
+
if isinstance(recurrence_id, str):
recurrence_id = PyCalendarDateTime.parseText(recurrence_id) if recurrence_id else None
@@ -580,21 +640,23 @@
return component
elif rid is None and recurrence_id is None:
return component
-
+
return None
-
+
+
def accessLevel(self, default=ACCESS_PUBLIC):
"""
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,)
-
+
access = self.propertyValue(Component.ACCESS_PROPERTY)
if access:
access = access.upper()
return Component.accessMap.get(access, default)
-
+
+
def duplicate(self):
"""
Duplicate this object and all its contents.
@@ -604,7 +666,8 @@
if hasattr(self, "noInstanceIndexing"):
result.noInstanceIndexing = True
return result
-
+
+
def subcomponents(self):
"""
@return: an iterable of L{Component} objects, one for each subcomponent
@@ -615,6 +678,7 @@
for c in self._pycalendar.getComponents()
)
+
def addComponent(self, component):
"""
Adds a subcomponent to this component.
@@ -625,6 +689,7 @@
component._parent = self
self._markAsDirty()
+
def removeComponent(self, component):
"""
Removes a subcomponent from this component.
@@ -634,6 +699,7 @@
component._parent = None
self._markAsDirty()
+
def hasProperty(self, name):
"""
@param name: the name of the property whose existence is being tested.
@@ -641,6 +707,7 @@
"""
return self._pycalendar.hasProperty(name)
+
def getProperty(self, name):
"""
Get one property from the property list.
@@ -649,10 +716,13 @@
@raise: L{InvalidICalendarDataError} if there is more than one property of the given name.
"""
properties = tuple(self.properties(name))
- if len(properties) == 1: return properties[0]
- if len(properties) > 1: raise InvalidICalendarDataError("More than one %s property in component %r" % (name, self))
+ if len(properties) == 1:
+ return properties[0]
+ if len(properties) > 1:
+ raise InvalidICalendarDataError("More than one %s property in component %r" % (name, self))
return None
-
+
+
def properties(self, name=None):
"""
@param name: if given and not C{None}, restricts the returned properties
@@ -671,6 +741,7 @@
for p in properties
)
+
def propertyValue(self, name):
properties = tuple(self.properties(name))
if len(properties) == 1:
@@ -689,7 +760,8 @@
"""
dtstart = self.propertyValue("DTSTART")
return dtstart.duplicateAsUTC() if dtstart is not None else None
-
+
+
def getEndDateUTC(self):
"""
Return the end date or date-time for the specified component,
@@ -707,6 +779,7 @@
return dtend.duplicateAsUTC() if dtend is not None else None
+
def getDueDateUTC(self):
"""
Return the due date or date-time for the specified component
@@ -722,7 +795,8 @@
due = dtstart + duration
return due.duplicateAsUTC() if due is not None else None
-
+
+
def getCompletedDateUTC(self):
"""
Return the completed date or date-time for the specified component
@@ -732,7 +806,8 @@
"""
completed = self.propertyValue("COMPLETED")
return completed.duplicateAsUTC() if completed is not None else None
-
+
+
def getCreatedDateUTC(self):
"""
Return the created date or date-time for the specified component
@@ -742,7 +817,8 @@
"""
created = self.propertyValue("CREATED")
return created.duplicateAsUTC() if created is not None else None
-
+
+
def getRecurrenceIDUTC(self):
"""
Return the recurrence-id for the specified component.
@@ -751,7 +827,8 @@
"""
rid = self.propertyValue("RECURRENCE-ID")
return rid.duplicateAsUTC() if rid is not None else None
-
+
+
def getRange(self):
"""
Determine whether a RANGE=THISANDFUTURE parameter is present
@@ -765,7 +842,8 @@
return (range == "THISANDFUTURE")
return False
-
+
+
def getExdates(self):
"""
Get the set of all EXDATEs in this (master) component.
@@ -776,6 +854,7 @@
exdates.add(exdate.getValue())
return exdates
+
def getTriggerDetails(self):
"""
Return the trigger information for the specified alarm component.
@@ -787,24 +866,26 @@
duration: the repeat duration if present, otherwise None
"""
assert self.name() == "VALARM", "Component is not a VAlARM: %r" % (self,)
-
+
# The trigger value
trigger = self.propertyValue("TRIGGER")
if trigger is None:
raise InvalidICalendarDataError("VALARM has no TRIGGER property: %r" % (self,))
-
+
# The related parameter
related = self.getProperty("TRIGGER").parameterValue("RELATED")
if related is None:
related = True
else:
related = (related == "START")
-
+
# Repeat property
repeat = self.propertyValue("REPEAT")
- if repeat is None: repeat = 0
- else: repeat = int(repeat)
-
+ if repeat is None:
+ repeat = 0
+ else:
+ repeat = int(repeat)
+
# Duration property
duration = self.propertyValue("DURATION")
@@ -812,10 +893,12 @@
raise InvalidICalendarDataError("VALARM has invalid REPEAT/DURATIOn properties: %r" % (self,))
return (trigger, related, repeat, duration)
-
+
+
def getRecurrenceSet(self):
return self._pycalendar.getRecurrenceSet()
+
def getEffectiveStartEnd(self):
# Get the start/end range needed for instance comparisons
@@ -831,12 +914,13 @@
else:
return None, None
+
def getFBType(self):
-
+
# Only VEVENTs block time
- if self.name() not in ("VEVENT", ):
+ if self.name() not in ("VEVENT",):
return "FREE"
-
+
# Handle status
status = self.propertyValue("STATUS")
if status == "CANCELLED":
@@ -846,6 +930,7 @@
else:
return "BUSY"
+
def addProperty(self, property):
"""
Adds a property to this component.
@@ -856,6 +941,7 @@
property._parent = self
self._markAsDirty()
+
def removeProperty(self, property):
"""
Remove a property from this component.
@@ -866,17 +952,19 @@
property._parent = None
self._markAsDirty()
+
def replaceProperty(self, property):
"""
Add or replace a property in this component.
@param property: the L{Property} to add or replace in this component.
"""
-
+
# Remove all existing ones first
self._pycalendar.removeProperties(property.name())
self.addProperty(property)
self._markAsDirty()
+
def timezoneIDs(self):
"""
Returns the set of TZID parameter values appearing in any property in
@@ -890,35 +978,37 @@
if tzid is not None:
result.add(tzid)
break
-
+
return result
-
+
+
def timezones(self):
"""
Returns the set of TZID's for each VTIMEZONE component.
@return: a set of strings, one for each unique TZID value.
"""
-
+
assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
results = set()
for component in self.subcomponents():
if component.name() == "VTIMEZONE":
results.add(component.propertyValue("TZID"))
-
+
return results
-
+
+
def truncateRecurrence(self, maximumCount):
"""
Truncate RRULEs etc to make sure there are no more than the given number
of instances.
-
+
@param maximumCount: the maximum number of instances to allow
@type maximumCount: C{int}
@return: a C{bool} indicating whether a change was made or not
"""
-
+
changed = False
master = self.masterComponent()
if master and master.isRecurring():
@@ -936,17 +1026,17 @@
start = master.getStartDateUTC()
diff = differenceDateTime(start, rrule.getUntil())
diff = diff.getDays() * 24 * 60 * 60 + diff.getSeconds()
-
+
period = {
- definitions.eRecurrence_YEARLY: 365 * 24 * 60 * 60,
- definitions.eRecurrence_MONTHLY: 30 * 24 * 60 * 60,
- definitions.eRecurrence_WEEKLY: 7 * 24 * 60 * 60,
- definitions.eRecurrence_DAILY: 1 * 24 * 60 * 60,
- definitions.eRecurrence_HOURLY: 60 * 60,
+ definitions.eRecurrence_YEARLY: 365 * 24 * 60 * 60,
+ definitions.eRecurrence_MONTHLY: 30 * 24 * 60 * 60,
+ definitions.eRecurrence_WEEKLY: 7 * 24 * 60 * 60,
+ definitions.eRecurrence_DAILY: 1 * 24 * 60 * 60,
+ definitions.eRecurrence_HOURLY: 60 * 60,
definitions.eRecurrence_MINUTELY: 60,
definitions.eRecurrence_SECONDLY: 1
}[rrule.getFreq()] * rrule.getInterval()
-
+
if diff / period > maximumCount:
rrule.setUseUntil(False)
rrule.setUseCount(True)
@@ -965,6 +1055,7 @@
self._markAsDirty()
return changed
+
def expand(self, start, end, timezone=None):
"""
Expand the components into a set of new components, one for each
@@ -976,7 +1067,7 @@
@param timezone: the L{Component} or L{PyCalendarTimezone} of the VTIMEZONE to use for floating/all-day.
@return: the L{Component} for the new calendar with expanded instances.
"""
-
+
if timezone is not None and isinstance(timezone, Component):
pytz = PyCalendarTimezone(tzid=timezone.propertyValue("TZID"))
else:
@@ -989,7 +1080,7 @@
calendar.removeProperty(property)
for property in self.properties():
calendar.addProperty(property)
-
+
# Expand the instances and add each one - use the normalizeForExpand date/time normalization method here
# so that all-day date/times remain that way. However, when doing the timeRangesOverlap test below, we
# Need to convert the all-days to floating (T000000) so that the timezone overlap calculation can be done
@@ -1001,9 +1092,10 @@
if timeRangesOverlap(normalizeForIndex(instance.start), normalizeForIndex(instance.end), start, end, pytz):
calendar.addComponent(self.expandComponent(instance, first))
first = False
-
+
return calendar
-
+
+
def expandComponent(self, instance, first):
"""
Create an expanded component based on the instance provided.
@@ -1011,22 +1103,22 @@
@param instance: an L{Instance} for the instance being expanded.
@return: a new L{Component} for the expanded instance.
"""
-
+
# Duplicate the component from the instance
newcomp = instance.component.duplicate()
-
+
# Strip out unwanted recurrence properties
for property in tuple(newcomp.properties()):
if property.name() in ["RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID"]:
newcomp.removeProperty(property)
-
+
# Convert all datetime properties to UTC unless they are floating
for property in newcomp.properties():
value = property.value()
if isinstance(value, PyCalendarDateTime) and value.local():
property.removeParameter("TZID")
property.setValue(value.duplicateAsUTC())
-
+
# Now reset DTSTART, DTEND/DURATION
for property in newcomp.properties("DTSTART"):
property.setValue(instance.start)
@@ -1034,24 +1126,25 @@
property.setValue(instance.end)
for property in newcomp.properties("DURATION"):
property.setValue(instance.end - instance.start)
-
+
# Add RECURRENCE-ID if not master instance
if not instance.isMasterInstance():
newcomp.addProperty(Property("RECURRENCE-ID", instance.rid))
return newcomp
+
def cacheExpandedTimeRanges(self, limit, ignoreInvalidInstances=False):
"""
Expand instances up to the specified limit and cache the results in this object
so we can return cached results in the future. The limit value is the actual value
that the requester needs, but we will cache an addition 365-days worth to give us some
breathing room to return results for future instances.
-
+
@param limit: the max datetime to cache up to.
@type limit: L{PyCalendarDateTime}
"""
-
+
# Checked for cached values first
if hasattr(self, "cachedInstances"):
cachedLimit = self.cachedInstances.limit
@@ -1059,7 +1152,7 @@
# We have already fully expanded, or cached up to the requested time,
# so return cached instances
return self.cachedInstances
-
+
lookAheadLimit = limit + PyCalendarDuration(days=365)
self.cachedInstances = self.expandTimeRanges(
lookAheadLimit,
@@ -1067,6 +1160,7 @@
)
return self.cachedInstances
+
def expandTimeRanges(self, limit, lowerLimit=None, ignoreInvalidInstances=False, normalizeFunction=normalizeForIndex):
"""
Expand the set of recurrence instances for the components
@@ -1077,10 +1171,11 @@
@param ignoreInvalidInstances: C{bool} whether to ignore instance errors.
@return: a set of Instances for each recurrence in the set.
"""
-
+
componentSet = self.subcomponents()
return self.expandSetTimeRanges(componentSet, limit, lowerLimit=lowerLimit, ignoreInvalidInstances=ignoreInvalidInstances, normalizeFunction=normalizeFunction)
-
+
+
def expandSetTimeRanges(self, componentSet, limit, lowerLimit=None, ignoreInvalidInstances=False, normalizeFunction=normalizeForIndex):
"""
Expand the set of recurrence instances up to the specified date limit.
@@ -1105,16 +1200,17 @@
@type normalizeFunction: C{function}
@return: L{InstanceList} containing expanded L{Instance} for each recurrence in the set.
"""
-
+
# Set of instances to return
instances = InstanceList(ignoreInvalidInstances=ignoreInvalidInstances, normalizeFunction=normalizeFunction)
instances.expandTimeRanges(componentSet, limit, lowerLimit=lowerLimit)
return instances
+
def getComponentInstances(self):
"""
Get the R-ID value for each component.
-
+
@return: a tuple of recurrence-ids
"""
@@ -1129,6 +1225,7 @@
rid = self.getRecurrenceIDUTC()
return (rid,)
+
def isRecurring(self):
"""
Check whether any recurrence properties are present in any component.
@@ -1144,7 +1241,8 @@
if self.hasProperty(propname):
return True
return False
-
+
+
def isRecurringUnbounded(self):
"""
Check for unbounded recurrence.
@@ -1157,7 +1255,8 @@
if not rrule.value().getUseCount() and not rrule.value().getUseUntil():
return True
return False
-
+
+
def deriveInstance(self, rid, allowCancelled=False, newcomp=None):
"""
Derive an instance from the master component that has the provided RECURRENCE-ID, but
@@ -1169,10 +1268,10 @@
@type rid: L{PyCalendarDateTime}
@param allowCancelled: whether to allow a STATUS:CANCELLED override
@type allowCancelled: C{bool}
-
+
@return: L{Component} for newly derived instance, or None if not valid override
"""
-
+
if allowCancelled and newcomp is not None:
raise ValueError("Cannot re-use master component with allowCancelled")
@@ -1200,7 +1299,7 @@
else:
# Cannot derive from an existing EXDATE
return None
-
+
# Check whether recurrence-id matches an RDATE - if so it is OK
rdates = set()
for rdate in master.properties("RDATE"):
@@ -1217,31 +1316,31 @@
else:
# No RRULE and no match to an RDATE => error
return None
-
+
# If we were fed an already derived component, use that, otherwise make a new one
if newcomp is None:
newcomp = self.masterDerived()
-
+
# New DTSTART is the RECURRENCE-ID we are deriving but adjusted to the
# original DTSTART's localtime
dtstart = newcomp.getProperty("DTSTART")
if newcomp.hasProperty("DTEND"):
dtend = newcomp.getProperty("DTEND")
oldduration = dtend.value() - dtstart.value()
-
+
newdtstartValue = rid.duplicate()
if not dtstart.value().isDateOnly():
if dtstart.value().local():
newdtstartValue.adjustTimezone(dtstart.value().getTimezone())
else:
newdtstartValue.setDateOnly(True)
-
+
dtstart.setValue(newdtstartValue)
if newcomp.hasProperty("DTEND"):
dtend.setValue(newdtstartValue + oldduration)
newcomp.replaceProperty(Property("RECURRENCE-ID", dtstart.value(), params={}))
-
+
if didCancel:
newcomp.replaceProperty(Property("STATUS", "CANCELLED"))
@@ -1249,13 +1348,14 @@
newcomp._pycalendar.finalise()
return newcomp
-
+
+
def masterDerived(self):
"""
Generate a component from the master instance that can be fed repeatedly to
deriveInstance in the case where the result of deriveInstance is not going
to be inserted into the component. This provides an optimization for avoiding
- unnecessary .duplicate() calls on the master for each deriveInstance.
+ unnecessary .duplicate() calls on the master for each deriveInstance.
"""
# Must have a master component
@@ -1270,19 +1370,20 @@
for property in tuple(newcomp.properties()):
if property.name() in ("RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID",):
newcomp.removeProperty(property)
-
+
return newcomp
+
def validInstances(self, rids, ignoreInvalidInstances=False):
"""
Test whether the specified recurrence-ids are valid instances in this event.
@param rid: recurrence-id values
@type rid: iterable
-
+
@return: C{set} of valid rids
"""
-
+
valid = set()
non_master_rids = [rid for rid in rids if rid is not None]
if non_master_rids:
@@ -1297,6 +1398,7 @@
valid.add(rid)
return valid
+
def validInstance(self, rid, clear_cache=True, ignoreInvalidInstances=False):
"""
Test whether the specified recurrence-id is a valid instance in this event.
@@ -1321,6 +1423,7 @@
new_rids = set([instances[key].rid for key in instances])
return rid in new_rids
+
def resourceUID(self):
"""
@return: the UID of the subcomponents in this component.
@@ -1337,6 +1440,7 @@
return self._resource_uid
+
def resourceType(self):
"""
@return: the name of the iCalendar type of the subcomponents in this
@@ -1364,11 +1468,12 @@
return self._resource_type
+
def stripKnownTimezones(self):
"""
Remove timezones that this server knows about
"""
-
+
changed = False
for subcomponent in tuple(self.subcomponents()):
if subcomponent.name() == "VTIMEZONE":
@@ -1385,6 +1490,7 @@
return changed
+
def validCalendarData(self, doFix=True, doRaise=True, validateRecurrences=False):
"""
@return: tuple of fixed, unfixed issues
@@ -1413,7 +1519,7 @@
raise InvalidICalendarDataError("Calendar data had unfixable problems:\n %s" % ("\n ".join(unfixed),))
if fixed:
log.debug("Calendar data had fixable problems:\n %s" % ("\n ".join(fixed),))
-
+
return fixed, unfixed
@@ -1435,7 +1541,7 @@
# Remove all EXDATEs with a matching RECURRENCE-ID. Do this before we start
# processing of valid instances just in case the matching R-ID is also not valid and
- # thus will need RDATE added.
+ # thus will need RDATE added.
exdates = {}
for property in list(master.properties("EXDATE")):
for exdate in property.value():
@@ -1455,17 +1561,16 @@
fixed.append("Removed EXDATE for valid override: %s" % (rid,))
else:
unfixed.append("EXDATE for valid override: %s" % (rid,))
-
+
# Get the set of all valid recurrence IDs
valid_rids = self.validInstances(all_rids, ignoreInvalidInstances=True)
-
+
# Get the set of all RDATEs and add those to the valid set
rdates = []
for property in master.properties("RDATE"):
rdates.extend([_rdate.getValue() for _rdate in property.value()])
valid_rids.update(set(rdates))
-
# Remove EXDATEs predating master
dtstart = master.propertyValue("DTSTART")
if dtstart is not None:
@@ -1491,7 +1596,6 @@
property.setValue(newValues)
master.addProperty(property)
-
else:
valid_rids = set()
@@ -1503,7 +1607,7 @@
brokenComponent = self.overriddenComponent(invalid_rid)
brokenRID = brokenComponent.propertyValue("RECURRENCE-ID")
if doFix:
- master.addProperty(Property("RDATE", [brokenRID,]))
+ master.addProperty(Property("RDATE", [brokenRID, ]))
fixed.append("Added RDATE for invalid occurrence: %s" %
(brokenRID,))
else:
@@ -1511,9 +1615,10 @@
return fixed, unfixed
+
def validCalendarForCalDAV(self, methodAllowed):
"""
- @param methodAllowed: True if METHOD property is allowed, False otherwise.
+ @param methodAllowed: True if METHOD property is allowed, False otherwise.
@raise InvalidICalendarDataError: if the given calendar component is not valid for
use as a X{CalDAV} resource.
"""
@@ -1528,15 +1633,15 @@
# Must not contain more than one type of iCalendar component, except for
# the required timezone components, and component UIDs must match
#
- ctype = None
- component_id = None
- component_rids = set()
- timezone_refs = set()
- timezones = set()
- got_master = False
+ ctype = None
+ component_id = None
+ component_rids = set()
+ timezone_refs = set()
+ timezones = set()
+ got_master = False
#got_override = False
#master_recurring = False
-
+
for subcomponent in self.subcomponents():
if subcomponent.name() == "VTIMEZONE":
timezones.add(subcomponent.propertyValue("TZID"))
@@ -1550,19 +1655,19 @@
msg = "Calendar resources may not contain more than one type of calendar component (%s and %s found)" % (ctype, subcomponent.name())
log.debug(msg)
raise InvalidICalendarDataError(msg)
-
+
if ctype not in allowedComponents:
msg = "Component type: %s not allowed" % (ctype,)
log.debug(msg)
raise InvalidICalendarDataError(msg)
-
+
uid = subcomponent.propertyValue("UID")
if uid is None:
msg = "All components must have UIDs"
log.debug(msg)
raise InvalidICalendarDataError(msg)
rid = subcomponent.getRecurrenceIDUTC()
-
+
# Verify that UIDs are the same
if component_id is None:
component_id = uid
@@ -1582,7 +1687,7 @@
# master_recurring = subcomponent.hasProperty("RRULE") or subcomponent.hasProperty("RDATE")
else:
pass # got_override = True
-
+
# Check that if an override is present then the master is recurring
# Leopard iCal sometimes does this for overridden instances that an Attendee receives and
# it creates a "fake" (invalid) master. We are going to skip this test here. Instead implicit
@@ -1594,8 +1699,8 @@
# msg = "Calendar resources must have a recurring master component if there is an overridden one (%s)" % (subcomponent.propertyValue("UID"),)
# log.debug(msg)
# raise InvalidICalendarDataError(msg)
-
- # Check for duplicate RECURRENCE-IDs
+
+ # Check for duplicate RECURRENCE-IDs
if rid in component_rids:
msg = "Calendar resources may not contain components with the same Recurrence-IDs (%s)" % (rid,)
log.debug(msg)
@@ -1604,7 +1709,7 @@
component_rids.add(rid)
timezone_refs.update(subcomponent.timezoneIDs())
-
+
#
# Make sure required timezone components are present
#
@@ -1614,7 +1719,7 @@
msg = "Timezone ID %s is referenced but not defined: %s" % (timezone_ref, self,)
log.debug(msg)
raise InvalidICalendarDataError(msg)
-
+
#
# FIXME:
# This test is not part of the spec; it appears to be legal (but
@@ -1631,11 +1736,12 @@
if len(s.translate(None, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F")) != len(s):
raise InvalidICalendarDataError("iCalendar contains illegal control character")
+
def validOrganizerForScheduling(self, doFix=True):
"""
- Check that the ORGANIZER property is valid for scheduling
+ Check that the ORGANIZER property is valid for scheduling
"""
-
+
organizers = self.getOrganizersByInstance()
foundOrganizer = None
foundRid = None
@@ -1653,7 +1759,7 @@
foundRid = rid
else:
missingRids.add(rid)
-
+
# If there are some components without an ORGANIZER we will fix the data
if foundOrganizer and missingRids:
if doFix:
@@ -1666,6 +1772,7 @@
return foundOrganizer
+
def gettimezone(self):
"""
Get the PyCalendarTimezone for a Timezone component.
@@ -1686,61 +1793,64 @@
##
# iTIP stuff
##
-
+
+
def isValidMethod(self):
"""
Verify that this calendar component has a valid iTIP METHOD property.
-
+
@return: True if valid, False if not
"""
-
+
try:
method = self.propertyValue("METHOD")
if method not in ("PUBLISH", "REQUEST", "REPLY", "ADD", "CANCEL", "REFRESH", "COUNTER", "DECLINECOUNTER"):
return False
except InvalidICalendarDataError:
return False
-
+
return True
+
def isValidITIP(self):
"""
Verify that this calendar component is valid according to iTIP.
-
+
@return: True if valid, False if not
"""
-
+
try:
method = self.propertyValue("METHOD")
if method not in ("PUBLISH", "REQUEST", "REPLY", "ADD", "CANCEL", "REFRESH", "COUNTER", "DECLINECOUNTER"):
return False
-
+
# First make sure components are all of the same time (excluding VTIMEZONE)
self.validCalendarForCalDAV(methodAllowed=True)
-
+
# Next we could check the iTIP status for each type of method/component pair, however
# we can also leave that up to the server except for the REQUEST/VFREEBUSY case which
# the server will handle itself.
-
+
if (method == "REQUEST") and (self.mainType() == "VFREEBUSY"):
# TODO: verify REQUEST/VFREEBUSY as being OK
-
+
# Only one VFREEBUSY (actually multiple X-'s are allowed but we will reject)
if len([c for c in self.subcomponents()]) != 1:
return False
except InvalidICalendarDataError:
return False
-
+
return True
-
+
+
def getOrganizer(self):
"""
Get the organizer value. Works on either a VCALENDAR or on a component.
-
+
@return: the string value of the Organizer property, or None
"""
-
+
# Extract appropriate sub-component if this is a VCALENDAR
if self.name() == "VCALENDAR":
for component in self.subcomponents():
@@ -1755,13 +1865,14 @@
return None
+
def getOrganizersByInstance(self):
"""
Get the organizer value for each instance.
-
+
@return: a list of tuples of (organizer value, recurrence-id)
"""
-
+
# Extract appropriate sub-component if this is a VCALENDAR
if self.name() == "VCALENDAR":
result = ()
@@ -1780,13 +1891,14 @@
return ()
+
def getOrganizerProperty(self):
"""
Get the organizer value. Works on either a VCALENDAR or on a component.
-
+
@return: the string value of the Organizer property, or None
"""
-
+
# Extract appropriate sub-component if this is a VCALENDAR
if self.name() == "VCALENDAR":
for component in self.subcomponents():
@@ -1801,6 +1913,7 @@
return None
+
def getOrganizerScheduleAgent(self):
is_server = False
@@ -1814,14 +1927,15 @@
return is_server
+
def getAttendees(self):
"""
Get the attendee value. Works on either a VCALENDAR or on a component.
-
+
@param match: a C{list} of calendar user address strings to try and match.
@return: a C{list} of the string values of the Attendee property, or None
"""
-
+
# Extract appropriate sub-component if this is a VCALENDAR
if self.name() == "VCALENDAR":
for component in self.subcomponents():
@@ -1833,17 +1947,18 @@
return None
+
def getAttendeesByInstance(self, makeUnique=False, onlyScheduleAgentServer=False):
"""
Get the attendee values for each instance. Optionally remove duplicates.
-
+
@param makeUnique: if C{True} remove duplicate ATTENDEEs in each component
@type makeUnique: C{bool}
@param onlyScheduleAgentServer: if C{True} only return ATETNDEEs with SCHEDULE-AGENT=SERVER set
@type onlyScheduleAgentServer: C{bool}
@return: a list of tuples of (organizer value, recurrence-id)
"""
-
+
# Extract appropriate sub-component if this is a VCALENDAR
if self.name() == "VCALENDAR":
result = ()
@@ -1856,7 +1971,7 @@
attendees = set()
rid = self.getRecurrenceIDUTC()
for attendee in tuple(self.properties("ATTENDEE")):
-
+
if onlyScheduleAgentServer:
if attendee.hasParameter("SCHEDULE-AGENT"):
if attendee.parameterValue("SCHEDULE-AGENT") != "SERVER":
@@ -1870,19 +1985,20 @@
attendees.add(cuaddr)
return result
+
def getAttendeeProperty(self, match):
"""
Get the attendees matching a value. Works on either a VCALENDAR or on a component.
-
+
@param match: a C{list} of calendar user address strings to try and match.
@return: the matching Attendee property, or None
"""
-
+
# Need to normalize http/https cu addresses
test = set()
for item in match:
test.add(normalizeCUAddr(item))
-
+
# Extract appropriate sub-component if this is a VCALENDAR
if self.name() == "VCALENDAR":
for component in self.subcomponents():
@@ -1898,14 +2014,15 @@
return None
+
def getAttendeeProperties(self, match):
"""
Get all the attendees matching a value in each component. Works on a VCALENDAR component only.
-
+
@param match: a C{list} of calendar user address strings to try and match.
@return: the string value of the Organizer property, or None
"""
-
+
assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
# Extract appropriate sub-component if this is a VCALENDAR
@@ -1918,6 +2035,7 @@
return results
+
def getAllAttendeeProperties(self):
"""
Yield all attendees as Property objects. Works on either a VCALENDAR or
@@ -1944,13 +2062,14 @@
attendees.add(attendee)
return attendees
+
def getMaskUID(self):
"""
Get the X-CALENDARSEREVR-MASK-UID value. Works on either a VCALENDAR or on a component.
-
+
@return: the string value of the X-CALENDARSEREVR-MASK-UID property, or None
"""
-
+
# Extract appropriate sub-component if this is a VCALENDAR
if self.name() == "VCALENDAR":
for component in self.subcomponents():
@@ -1965,13 +2084,14 @@
return None
+
def getExtendedFreeBusy(self):
"""
Get the X-CALENDARSERVER-EXTENDED-FREEBUSY value. Works on either a VCALENDAR or on a component.
-
+
@return: the string value of the X-CALENDARSERVER-EXTENDED-FREEBUSY property, or None
"""
-
+
# Extract appropriate sub-component if this is a VCALENDAR
if self.name() == "VCALENDAR":
for component in self.subcomponents():
@@ -1986,10 +2106,11 @@
return None
+
def setParameterToValueForPropertyWithValue(self, paramname, paramvalue, propname, propvalue):
"""
Add or change the parameter to the specified value on the property having the specified value.
-
+
@param paramname: the parameter name
@type paramname: C{str}
@param paramvalue: the parameter value to set
@@ -1999,25 +2120,26 @@
@param propvalue: the property value to test
@type propvalue: C{str} or C{None}
"""
-
+
for component in self.subcomponents():
if component.name() in ignoredComponents:
continue
for property in component.properties(propname):
if propvalue is None or property.value() == propvalue:
property.setParameter(paramname, paramvalue)
-
+
+
def hasPropertyInAnyComponent(self, properties):
"""
Test for the existence of one or more properties in any component.
-
+
@param properties: property name(s) to test for
@type properties: C{list}, C{tuple} or C{str}
"""
if isinstance(properties, str):
properties = (properties,)
-
+
for property in properties:
if self.hasProperty(property):
return True
@@ -2028,17 +2150,18 @@
return False
+
def getFirstPropertyInAnyComponent(self, properties):
"""
Get the first of any set of properties in any component.
-
+
@param properties: property name(s) to test for
@type properties: C{list}, C{tuple} or C{str}
"""
if isinstance(properties, str):
properties = (properties,)
-
+
for property in properties:
props = tuple(self.properties(property))
if props:
@@ -2051,15 +2174,16 @@
return None
+
def getAllPropertiesInAnyComponent(self, properties, depth=2):
"""
Get the all of any set of properties in any component down to a
specified depth.
-
+
@param properties: property name(s) to test for
@type properties: C{list}, C{tuple} or C{str}
@param depth: how deep to go in looking at sub-components:
- 0: do not go into sub-components, 1: go into one level of sub-components,
+ 0: do not go into sub-components, 1: go into one level of sub-components,
2: two levels (which is effectively all the levels supported in iCalendar)
@type depth: int
"""
@@ -2068,7 +2192,7 @@
if isinstance(properties, str):
properties = (properties,)
-
+
for property in properties:
props = tuple(self.properties(property))
if props:
@@ -2080,10 +2204,11 @@
return results
+
def hasPropertyValueInAllComponents(self, property):
"""
Test for the existence of a property with a specific value in any sub-component.
-
+
@param property: property to test for
@type property: L{Property}
"""
@@ -2097,6 +2222,7 @@
return True
+
def addPropertyToAllComponents(self, property):
"""
Add a property to all top-level components except VTIMEZONE.
@@ -2104,28 +2230,30 @@
@param property: the property to add
@type property: L{Property}
"""
-
+
for component in self.subcomponents():
if component.name() in ignoredComponents:
continue
component.addProperty(property)
+
def replacePropertyInAllComponents(self, property):
"""
Replace a property in all components.
@param property: the L{Property} to replace in this component.
"""
-
+
for component in self.subcomponents():
if component.name() in ignoredComponents:
continue
component.replaceProperty(property)
-
+
+
def transferProperties(self, from_calendar, properties):
"""
Transfer specified properties from old calendar into all components
of this calendar, synthesizing any for new overridden instances.
-
+
@param from_calendar: the old calendar to copy from
@type from_calendar: L{Component}
@param properties: the property names to copy over
@@ -2133,7 +2261,7 @@
"""
assert from_calendar.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
-
+
if self.name() == "VCALENDAR":
for component in self.subcomponents():
if component.name() in ignoredComponents:
@@ -2143,7 +2271,7 @@
# Is there a matching component
rid = self.getRecurrenceIDUTC()
matched = from_calendar.overriddenComponent(rid)
-
+
# If no match found, we are processing a new overridden instance so copy from the original master
if not matched:
matched = from_calendar.masterComponent()
@@ -2152,7 +2280,8 @@
for propname in properties:
for prop in matched.properties(propname):
self.addProperty(prop)
-
+
+
def attendeesView(self, attendees, onlyScheduleAgentServer=False):
"""
Filter out any components that all attendees are not present in. Use EXDATEs
@@ -2160,7 +2289,7 @@
"""
assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
-
+
# Modify any components that reference the attendee, make note of the ones that don't
remove_components = []
master_component = None
@@ -2185,7 +2314,7 @@
master_component = component
if not found_all_attendees:
removed_master = True
-
+
# Now remove the unwanted components - but we may need to EXDATE the master
exdates = []
for component in remove_components:
@@ -2193,19 +2322,20 @@
if rid is not None:
exdates.append(rid)
self.removeComponent(component)
-
+
if not removed_master and master_component is not None:
for exdate in exdates:
- master_component.addProperty(Property("EXDATE", [exdate,]))
-
+ master_component.addProperty(Property("EXDATE", [exdate, ]))
+
+
def filterComponents(self, rids):
-
+
# If master is in rids do nothing
if not rids or "" in rids:
return True
-
+
assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
-
+
# Remove components not in the list
components = tuple(self.subcomponents())
remaining = len(components)
@@ -2217,9 +2347,10 @@
if (rid.getText() if rid else "") not in rids:
self.removeComponent(component)
remaining -= 1
-
+
return remaining != 0
-
+
+
def removeAllButOneAttendee(self, attendee):
"""
Remove all ATTENDEE properties except for the one specified.
@@ -2231,7 +2362,8 @@
if component.name() in ignoredComponents:
continue
[component.removeProperty(p) for p in tuple(component.properties("ATTENDEE")) if p.value().lower() != attendee.lower()]
-
+
+
def hasAlarm(self):
"""
Test whether the component has a VALARM as an immediate sub-component.
@@ -2243,20 +2375,21 @@
return True
return False
+
def addAlarms(self, alarm, ignoreActionNone=True):
"""
Add an alarm to any VEVENT or VTODO subcomponents that do not already have any.
@param alarm: the text for a VALARM component
@type alarm: C{str}
-
+
@param ignoreActionNone: whether or not to skip ACTION:NONE alarms
@type ignoreActionNone: C{bool}
-
+
@return: indicate whether a change was made
@rtype: C{bool}
"""
-
+
# Create a fake component for the alarm text
caldata = """BEGIN:VCALENDAR
VERSION:2.0
@@ -2270,7 +2403,7 @@
%sEND:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n") % (alarm,)
-
+
try:
calendar = Component.fromString(caldata)
if calendar is None:
@@ -2297,9 +2430,10 @@
continue
component.addComponent(valarm.duplicate())
changed = True
-
+
return changed
+
def removeAlarms(self):
"""
Remove all Alarms components
@@ -2315,6 +2449,7 @@
if component.name() == "VALARM":
self.removeComponent(component)
+
def hasDuplicateAlarms(self, doFix=False):
"""
Test and optionally remove alarms that have the same ACTION and TRIGGER values in the same component.
@@ -2338,6 +2473,7 @@
action_trigger.add(item)
return changed
+
def filterProperties(self, remove=None, keep=None, do_subcomponents=True):
"""
Remove all properties that do not match the provided set.
@@ -2349,11 +2485,12 @@
else:
if self.name() in ignoredComponents:
return
-
+
for p in tuple(self.properties()):
if (keep and p.name() not in keep) or (remove and p.name() in remove):
self.removeProperty(p)
-
+
+
def removeXComponents(self, keep_components=()):
"""
Remove all X- components except the specified ones
@@ -2362,7 +2499,8 @@
for component in tuple(self.subcomponents()):
if component.name().startswith("X-") and component.name() not in keep_components:
self.removeComponent(component)
-
+
+
def removeXProperties(self, keep_properties=(), remove_x_parameters=True, do_subcomponents=True):
"""
Remove all X- properties except the specified ones
@@ -2382,7 +2520,8 @@
for paramname in p.parameterNames():
if paramname.startswith("X-"):
p.removeParameter(paramname)
-
+
+
def removePropertyParameters(self, property, params):
"""
Remove all specified property parameters
@@ -2399,6 +2538,7 @@
for param in params:
prop.removeParameter(param)
+
def removePropertyParametersByValue(self, property, paramvalues):
"""
Remove all specified property parameters
@@ -2415,10 +2555,11 @@
for param, value in paramvalues:
prop.removeParameterValue(param, value)
+
def getITIPInfo(self):
"""
Get property value details needed to synchronize iTIP components.
-
+
@return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
"""
try:
@@ -2429,46 +2570,48 @@
seq = int(seq)
dtstamp = self.propertyValue("DTSTAMP")
rid = self.propertyValue("RECURRENCE-ID")
-
+
except ValueError:
return (None, None, None, None)
-
+
return (uid, seq, dtstamp, rid)
+
@staticmethod
def compareComponentsForITIP(component1, component2, use_dtstamp=True):
"""
Compare synchronization information for two components to see if they match according to iTIP.
-
+
@param component1: first component to check.
@type component1: L{Component}
@param component2: second component to check.
@type component2: L{Component}
@param use_dtstamp: whether DTSTAMP is used in addition to SEQUENCE.
@type component2: C{bool}
-
+
@return: 0, 1, -1 as per compareSyncInfo.
"""
info1 = (None,) + Component.getITIPInfo(component1)
info2 = (None,) + Component.getITIPInfo(component2)
return Component.compareITIPInfo(info1, info2, use_dtstamp)
-
+
+
@staticmethod
def compareITIPInfo(info1, info2, use_dtstamp=True):
"""
Compare two synchronization information records.
-
+
@param info1: a C{tuple} as returned by L{getSyncInfo}.
@param info2: a C{tuple} as returned by L{getSyncInfo}.
@return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
"""
-
+
_ignore_name1, uid1, seq1, dtstamp1, _ignore_rid1 = info1
_ignore_name2, uid2, seq2, dtstamp2, _ignore_rid2 = info2
-
+
# UIDs MUST match
assert uid1 == uid2
-
+
# Look for sequence
if (seq1 is not None) and (seq2 is not None):
if seq1 > seq2:
@@ -2479,7 +2622,7 @@
return 1
elif (seq1 is None) and (seq2 is not None):
return -1
-
+
# Look for DTSTAMP
if use_dtstamp:
if (dtstamp1 is not None) and (dtstamp2 is not None):
@@ -2491,15 +2634,16 @@
return 1
elif (dtstamp1 is None) and (dtstamp2 is not None):
return -1
-
+
return 0
+
def needsiTIPSequenceChange(self, oldcalendar):
"""
Compare this calendar with the old one and indicate whether the current one has SEQUENCE
that is always greater than the old.
"""
-
+
for component in self.subcomponents():
if component.name() in ignoredComponents:
continue
@@ -2519,49 +2663,53 @@
return False
+
def bumpiTIPInfo(self, oldcalendar=None, doSequence=False):
"""
Change DTSTAMP and optionally SEQUENCE on all components.
"""
-
+
if doSequence:
-
+
+
def maxSequence(calendar):
seqs = calendar.getAllPropertiesInAnyComponent("SEQUENCE", depth=1)
- return max(seqs, key=lambda x:x.value()).value() if seqs else 0
+ return max(seqs, key=lambda x: x.value()).value() if seqs else 0
# Determine value to bump to from old calendar (if exists) or self
- newseq = maxSequence(oldcalendar if oldcalendar is not None else self) + 1
-
+ newseq = maxSequence(oldcalendar if oldcalendar is not None else self) + 1
+
# Bump all components
self.replacePropertyInAllComponents(Property("SEQUENCE", newseq))
-
+
self.replacePropertyInAllComponents(Property("DTSTAMP", PyCalendarDateTime.getNowUTC()))
-
+
+
def sequenceInSync(self, oldcalendar):
"""
Make sure SEQUENCE does not decrease in any components.
"""
-
-
+
def maxSequence(calendar):
seqs = calendar.getAllPropertiesInAnyComponent("SEQUENCE", depth=1)
- return max(seqs, key=lambda x:x.value()).value() if seqs else 0
+ return max(seqs, key=lambda x: x.value()).value() if seqs else 0
+
def minSequence(calendar):
seqs = calendar.getAllPropertiesInAnyComponent("SEQUENCE", depth=1)
- return min(seqs, key=lambda x:x.value()).value() if seqs else 0
+ return min(seqs, key=lambda x: x.value()).value() if seqs else 0
# Determine value to bump to from old calendar (if exists) or self
oldseq = maxSequence(oldcalendar)
currentseq = minSequence(self)
-
+
# Sync all components
if oldseq and currentseq < oldseq:
self.replacePropertyInAllComponents(Property("SEQUENCE", oldseq))
-
+
+
def normalizeAll(self):
-
+
# Normalize all properties
for prop in tuple(self.properties()):
result = normalizeProps.get(prop.name())
@@ -2571,13 +2719,13 @@
# Assume default VALUE is TEXT
default_value = None
default_params = {"VALUE": "TEXT"}
-
+
# Remove any default parameters
for name in prop.parameterNames():
value = prop.parameterValue(name)
if value == default_params.get(name):
prop.removeParameter(name)
-
+
# If there are no parameters, remove the property if it has the default value
if len(prop.parameterNames()) == 0:
if default_value is not None and prop.value() == default_value:
@@ -2596,29 +2744,30 @@
for component in self.subcomponents():
component.normalizeAll()
+
def normalizeDateTimes(self):
"""
Normalize various datetime properties into UTC and handle DTEND/DURATION variants in such
a way that we can compare objects with slight differences.
-
+
Also normalize the RRULE value parts.
-
+
Strictly speaking we should not need to do this as clients should not be messing with
these properties - i.e. they should round trip them. Unfortunately some do...
"""
-
+
# TODO: what about VJOURNAL and VTODO?
if self.name() == "VEVENT":
-
+
# Basic time properties
dtstart = self.getProperty("DTSTART")
dtend = self.getProperty("DTEND")
duration = self.getProperty("DURATION")
-
+
timeRange = PyCalendarPeriod(
- start = dtstart.value(),
- end = dtend.value() if dtend is not None else None,
- duration = duration.value() if duration is not None else None,
+ start=dtstart.value(),
+ end=dtend.value() if dtend is not None else None,
+ duration=duration.value() if duration is not None else None,
)
# Have to fake the TZID value here when we convert date-times to UTC
@@ -2664,12 +2813,13 @@
# sortedValue = ";".join(["%s=%s" % (key, value,) for key, value in sorted(indexedTokens.iteritems(), key=lambda x:x[0])])
# rrule.setValue(sortedValue)
+
def normalizePropertyValueLists(self, propname):
"""
Convert properties that have a list of values into single properties, to make it easier
to do comparisons between two ical objects.
"""
-
+
if self.name() == "VCALENDAR":
for component in self.subcomponents():
if component.name() in ignoredComponents:
@@ -2680,13 +2830,14 @@
if type(prop.value()) is list and len(prop.value()) > 1:
self.removeProperty(prop)
for value in prop.value():
- self.addProperty(Property(propname, [value.getValue(),]))
+ self.addProperty(Property(propname, [value.getValue(), ]))
+
def normalizeAttachments(self):
"""
Remove any ATTACH properties that relate to a dropbox.
"""
-
+
if self.name() == "VCALENDAR":
for component in self.subcomponents():
if component.name() in ignoredComponents:
@@ -2703,6 +2854,7 @@
if dataValue.find(dropboxPrefix) != -1:
self.removeProperty(attachment)
+
def normalizeCalendarUserAddresses(self, lookupFunction, principalFunction,
toUUID=True):
"""
@@ -2817,20 +2969,21 @@
def allPerUserUIDs(self):
-
+
results = set()
for component in self.subcomponents():
if component.name() == "X-CALENDARSERVER-PERUSER":
results.add(component.propertyValue("X-CALENDARSERVER-PERUSER-UID"))
return results
+
def perUserTransparency(self, rid):
-
+
# We will create a cache of all user/rid/transparency values as we will likely
# be calling this a lot
if not hasattr(self, "_perUserTransparency"):
self._perUserTransparency = {}
-
+
# Do per-user data
for component in self.subcomponents():
if component.name() == "X-CALENDARSERVER-PERUSER":
@@ -2838,24 +2991,24 @@
for subcomponent in component.subcomponents():
if subcomponent.name() == "X-CALENDARSERVER-PERINSTANCE":
instancerid = subcomponent.propertyValue("RECURRENCE-ID")
- transp = subcomponent.propertyValue("TRANSP") == "TRANSPARENT"
+ transp = subcomponent.propertyValue("TRANSP") == "TRANSPARENT"
self._perUserTransparency.setdefault(uid, {})[instancerid] = transp
elif component.name() not in ignoredComponents:
instancerid = component.propertyValue("RECURRENCE-ID")
- transp = component.propertyValue("TRANSP") == "TRANSPARENT"
+ transp = component.propertyValue("TRANSP") == "TRANSPARENT"
self._perUserTransparency.setdefault("", {})[instancerid] = transp
# Now lookup in cache
results = []
- for uid, cachedRids in sorted(self._perUserTransparency.items(), key=lambda x:x[0]):
+ for uid, cachedRids in sorted(self._perUserTransparency.items(), key=lambda x: x[0]):
lookupRid = rid
if lookupRid not in cachedRids:
lookupRid = None
if lookupRid in cachedRids:
results.append((uid, cachedRids[lookupRid],))
else:
- results.append((uid, False,))
-
+ results.append((uid, False,))
+
return tuple(results)
@@ -2884,6 +3037,8 @@
# Exists completely prior to limit
return False
+
+
##
# Timezones
##
@@ -2899,10 +3054,10 @@
@type start: C{date}
@param end: date for the end of the expansion.
@type end: C{date}
-
+
@return: a C{list} of tuples of (C{datetime}, C{str})
"""
-
+
icalobj = Component.fromString(tzdata)
tzcomp = None
for comp in icalobj.subcomponents():
@@ -2913,9 +3068,9 @@
raise InvalidICalendarDataError("No VTIMEZONE component in %s" % (tzdata,))
tzexpanded = tzcomp._pycalendar.expandAll(start, end)
-
+
results = []
-
+
# Always need to ensure the start appears in the result
start.setDateOnly(False)
if tzexpanded:
@@ -2928,9 +3083,11 @@
tzstart.getText(),
PyCalendarUTCOffsetValue(tzoffsetto).getText(),
))
-
+
return results
+
+
def tzexpandlocal(tzdata, start, end):
"""
Expand a timezone to get onset(local)/utc-offset-from/utc-offset-to/name observance tuples within the specified
@@ -2942,10 +3099,10 @@
@type start: C{date}
@param end: date for the end of the expansion.
@type end: C{date}
-
+
@return: a C{list} of tuples
"""
-
+
icalobj = Component(None, pycalendar=tzdata)
tzcomp = None
for comp in icalobj.subcomponents():
@@ -2956,9 +3113,9 @@
raise InvalidICalendarDataError("No VTIMEZONE component in %s" % (tzdata,))
tzexpanded = tzcomp._pycalendar.expandAll(start, end, with_name=True)
-
+
results = []
-
+
# Always need to ensure the start appears in the result
start.setDateOnly(False)
if tzexpanded:
@@ -2983,9 +3140,11 @@
PyCalendarUTCOffsetValue(tzoffsetto).getText(),
name,
))
-
+
return results
+
+
##
# Utilities
##
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120920/25aa620f/attachment-0001.html>
More information about the calendarserver-changes
mailing list