[CalendarServer-changes] [8363] CalendarServer/trunk/twistedcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Thu Dec 1 13:55:31 PST 2011
Revision: 8363
http://trac.macosforge.org/projects/calendarserver/changeset/8363
Author: cdaboo at apple.com
Date: 2011-12-01 13:55:30 -0800 (Thu, 01 Dec 2011)
Log Message:
-----------
Default alarm support.
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/caldavxml.py
CalendarServer/trunk/twistedcaldav/ical.py
CalendarServer/trunk/twistedcaldav/method/put_common.py
CalendarServer/trunk/twistedcaldav/resource.py
CalendarServer/trunk/twistedcaldav/stdconfig.py
CalendarServer/trunk/twistedcaldav/storebridge.py
Modified: CalendarServer/trunk/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/caldavxml.py 2011-12-01 21:53:26 UTC (rev 8362)
+++ CalendarServer/trunk/twistedcaldav/caldavxml.py 2011-12-01 21:55:30 UTC (rev 8363)
@@ -1042,6 +1042,117 @@
protected = True
##
+# draft-daboo-valarm-extensions
+##
+
+caldav_default_alarms_compliance = (
+ "calendar-default-alarms",
+)
+
+class DefaultAlarmBase (CalDAVTextElement):
+ """
+ Common behavior for default alarm properties.
+ """
+
+ calendartxt = None
+
+ def calendar(self):
+ """
+ Returns a calendar component derived from this element, which contains
+ exactly one VEVENT with the VALARM embedded component, or C{None} if empty.
+ """
+ valarm = str(self)
+ return iComponent.fromString(self.calendartxt % str(self)) if valarm else None
+
+ def valid(self):
+ """
+ Determine whether the content of this element is a valid single VALARM component or empty.
+
+ @return: True if valid, False if not.
+ """
+
+ if str(self):
+ try:
+ calendar = self.calendar()
+ if calendar is None:
+ return False
+ except ValueError:
+ return False
+
+ return True
+
+class DefaultAlarmVEventDateTime (DefaultAlarmBase):
+
+ name = "default-alarm-vevent-datetime"
+
+ calendartxt = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:bogus
+DTSTART:20111129T220000Z
+DURATION:PT1H
+DTSTAMP:20111129T220000Z
+SUMMARY:bogus
+%sEND:VEVENT
+END:VCALENDAR
+"""
+
+class DefaultAlarmVEventDate (DefaultAlarmBase):
+
+ name = "default-alarm-vevent-date"
+
+ calendartxt = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:bogus
+DTSTART:20111129
+DURATION:PT1H
+DTSTAMP:20111129T220000Z
+SUMMARY:bogus
+%sEND:VEVENT
+END:VCALENDAR
+"""
+
+class DefaultAlarmVToDoDateTime (DefaultAlarmBase):
+
+ name = "default-alarm-vtodo-datetime"
+
+
+ calendartxt = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTODO
+UID:bogus
+DUE:20111129T220000Z
+DTSTAMP:20111129T220000Z
+SUMMARY:bogus
+%sEND:VTODO
+END:VCALENDAR
+"""
+
+class DefaultAlarmVToDoDate (DefaultAlarmBase):
+
+ name = "default-alarm-vtodo-date"
+
+ calendartxt = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTODO
+UID:bogus
+DUE:20111129
+DTSTAMP:20111129T220000Z
+SUMMARY:bogus
+%sEND:VTODO
+END:VCALENDAR
+"""
+
+##
# Extensions to davxml.ResourceType
##
Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py 2011-12-01 21:53:26 UTC (rev 8362)
+++ CalendarServer/trunk/twistedcaldav/ical.py 2011-12-01 21:55:30 UTC (rev 8363)
@@ -2002,7 +2002,63 @@
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.
+ """
+ assert self.name().upper() in ("VEVENT", "VTODO",), "Not a VEVENT or VTODO: %r" % (self,)
+
+ for component in self.subcomponents():
+ if component.name().upper() == "VALARM":
+ return True
+ return False
+
+ def addAlarms(self, alarm):
+ """
+ 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}
+
+ @return: indicate whether a change was made
+ @rtype: C{bool}
+ """
+
+ # Create a fake component for the alarm text
+ caldata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:bogus
+DTSTART:20110427T000000Z
+DURATION:PT1H
+DTSTAMP:20110427T000000Z
+SUMMARY:bogus
+%sEND:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % (alarm,)
+
+ try:
+ calendar = Component.fromString(caldata)
+ if calendar is None:
+ return False
+ except ValueError:
+ return
+
+ valarm = tuple(tuple(calendar.subcomponents())[0].subcomponents())[0]
+
+ changed = False
+ for component in self.subcomponents():
+ if component.name().upper() not in ("VEVENT", "VTODO",):
+ continue
+ if component.hasAlarm():
+ continue
+ component.addComponent(valarm.duplicate())
+ changed = True
+
+ return changed
+
def removeAlarms(self):
"""
Remove all Alarms components
Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py 2011-12-01 21:53:26 UTC (rev 8362)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py 2011-12-01 21:55:30 UTC (rev 8363)
@@ -744,6 +744,44 @@
returnValue(changed)
+ def addDefaultAlarm(self):
+ """
+ Add a default alarm if required.
+
+ @return: indicate whether a change was made
+ @rtype: C{bool}
+ """
+
+ # Only if feature enabled
+ if not config.EnableDefaultAlarms:
+ return False
+
+ # Check that we are creating and this is not the inbox
+ if not self.destinationcal or self.destination.exists() or self.isiTIP:
+ return False
+
+ # Add default alarm for VEVENT and VTODO only
+ mtype = self.calendar.mainType().upper()
+ if self.calendar.mainType().upper() not in ("VEVENT", "VTODO"):
+ return False
+ vevent = mtype == "VEVENT"
+
+ # Check timed or all-day
+ start, _ignore_end = self.calendar.mainComponent(allow_multiple=True).getEffectiveStartEnd()
+ if start is None:
+ # Yes VTODOs might have no DTSTART or DUE - in this case we do not add a default
+ return False
+ timed = not start.isDateOnly()
+
+ # See if default exists and add using appropriate logic
+ changed = False
+ alarm = self.destinationparent.getDefaultAlarm(vevent, timed)
+ if alarm:
+ changed = self.calendar.addAlarms(alarm)
+ if changed:
+ self.calendardata = None
+ return changed
+
@inlineCallbacks
def noUIDConflict(self, uid):
"""
@@ -1063,6 +1101,9 @@
# Handle sharing dropbox normalization
dropboxChanged = (yield self.dropboxPathNormalization())
+ # Default alarms
+ alarmChanged = self.addDefaultAlarm()
+
# Do scheduling
implicit_result = (yield self.doImplicitScheduling())
if isinstance(implicit_result, int):
@@ -1105,7 +1146,7 @@
response = (yield self.doStore(data_changed))
# Must not set ETag in response if data changed
- if did_implicit_action or rruleChanged or dropboxChanged:
+ if did_implicit_action or rruleChanged or dropboxChanged or alarmChanged:
def _removeEtag(request, response):
response.headers.removeHeader('etag')
return response
Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py 2011-12-01 21:53:26 UTC (rev 8362)
+++ CalendarServer/trunk/twistedcaldav/resource.py 2011-12-01 21:55:30 UTC (rev 8363)
@@ -404,6 +404,15 @@
if self.isCalendarCollection():
baseProperties += (
davxml.ResourceID.qname(),
+
+ # These are "live" properties in the sense of WebDAV, however "live" for twext actually means
+ # ones that are also always present, but the default alarm properties are allowed to be absent
+ # and are in fact stored in the property store.
+ #caldavxml.DefaultAlarmVEventDateTime.qname(),
+ #caldavxml.DefaultAlarmVEventDate.qname(),
+ #caldavxml.DefaultAlarmVToDoDateTime.qname(),
+ #caldavxml.DefaultAlarmVToDoDate.qname(),
+
customxml.PubSubXMPPPushKeyProperty.qname(),
)
@@ -492,13 +501,29 @@
returnValue(p)
elif (not self.isGlobalProperty(qname)):
- ownerPrincipal = (yield self.resourceOwnerPrincipal(request))
- p = self.deadProperties().contains(qname, uid=ownerPrincipal.principalUID())
- returnValue(p)
+ result = (yield self._hasSharedProperty(qname, request))
+ returnValue(result)
res = (yield self._hasGlobalProperty(property, request))
returnValue(res)
+ @inlineCallbacks
+ def _hasSharedProperty(self, qname, request):
+
+ # Always have disabled default alarms on shared calendars
+ if qname in (
+ caldavxml.DefaultAlarmVEventDateTime.qname(),
+ caldavxml.DefaultAlarmVEventDate.qname(),
+ caldavxml.DefaultAlarmVToDoDateTime.qname(),
+ caldavxml.DefaultAlarmVToDoDate.qname(),
+ ):
+ if self.isCalendarCollection():
+ returnValue(True)
+
+ ownerPrincipal = (yield self.resourceOwnerPrincipal(request))
+ p = self.deadProperties().contains(qname, uid=ownerPrincipal.principalUID())
+ returnValue(p)
+
def _hasGlobalProperty(self, property, request):
"""
Need to special case schedule-calendar-transp for backwards compatability.
@@ -512,6 +537,7 @@
# Force calendar collections to always appear to have the property
if qname == caldavxml.ScheduleCalendarTransp.qname() and self.isCalendarCollection():
return succeed(True)
+
else:
return super(CalDAVResource, self).hasProperty(property, request)
@@ -554,14 +580,38 @@
pass
elif (not self.isGlobalProperty(qname)):
- ownerPrincipal = (yield self.resourceOwnerPrincipal(request))
- p = self.deadProperties().get(qname, uid=ownerPrincipal.principalUID())
- returnValue(p)
+ result = (yield self._readSharedProperty(qname, request))
+ returnValue(result)
res = (yield self._readGlobalProperty(qname, property, request))
returnValue(res)
@inlineCallbacks
+ def _readSharedProperty(self, qname, request):
+
+ # Validate default alarm properties (do this even if the default alarm feature is off)
+ if qname in (
+ caldavxml.DefaultAlarmVEventDateTime.qname(),
+ caldavxml.DefaultAlarmVEventDate.qname(),
+ caldavxml.DefaultAlarmVToDoDateTime.qname(),
+ caldavxml.DefaultAlarmVToDoDate.qname(),
+ ):
+ if self.isCalendarCollection():
+ # Always disable default alarms on shared calendars
+ propclass = {
+ caldavxml.DefaultAlarmVEventDateTime.qname() : caldavxml.DefaultAlarmVEventDateTime,
+ caldavxml.DefaultAlarmVEventDate.qname() : caldavxml.DefaultAlarmVEventDate,
+ caldavxml.DefaultAlarmVToDoDateTime.qname() : caldavxml.DefaultAlarmVToDoDateTime,
+ caldavxml.DefaultAlarmVToDoDate.qname() : caldavxml.DefaultAlarmVToDoDate,
+ }[qname]
+ returnValue(propclass.fromString(""))
+
+ # Default behavior - read per-user dead property
+ ownerPrincipal = (yield self.resourceOwnerPrincipal(request))
+ p = self.deadProperties().get(qname, uid=ownerPrincipal.principalUID())
+ returnValue(p)
+
+ @inlineCallbacks
def _readGlobalProperty(self, qname, property, request):
if qname == davxml.Owner.qname():
@@ -689,7 +739,7 @@
# Per-user Dav props currently only apply to a sharee's copy of a calendar
isvirt = self.isVirtualShare()
if isvirt and (self.isShadowableProperty(property.qname()) or (not self.isGlobalProperty(property.qname()))):
- yield self._preProcessWriteProperty(property, request)
+ yield self._preProcessWriteProperty(property, request, isShare=True)
ownerPrincipal = (yield self.resourceOwnerPrincipal(request))
p = self.deadProperties().set(property, uid=ownerPrincipal.principalUID())
returnValue(p)
@@ -698,7 +748,7 @@
returnValue(res)
@inlineCallbacks
- def _preProcessWriteProperty(self, property, request):
+ def _preProcessWriteProperty(self, property, request, isShare=False):
if property.qname() == caldavxml.SupportedCalendarComponentSet.qname():
if not self.isPseudoCalendarCollection():
raise HTTPError(StatusResponse(
@@ -729,6 +779,33 @@
description="Invalid property"
))
+ # Validate default alarm properties (do this even if the default alarm feature is off)
+ elif property.qname() in (
+ caldavxml.DefaultAlarmVEventDateTime.qname(),
+ caldavxml.DefaultAlarmVEventDate.qname(),
+ caldavxml.DefaultAlarmVToDoDateTime.qname(),
+ caldavxml.DefaultAlarmVToDoDate.qname(),
+ ):
+ if not self.isCalendarCollection() and not isinstance(self, CalendarHomeResource):
+ raise HTTPError(StatusResponse(
+ responsecode.FORBIDDEN,
+ "Property %s may only be set on calendar or home collection." % (property,)
+ ))
+
+ # Do not allow default alarms by sharees
+ if isShare:
+ raise HTTPError(StatusResponse(
+ responsecode.FORBIDDEN,
+ "Property %s is protected on shared calendar collections." % (property,)
+ ))
+
+ if not property.valid():
+ raise HTTPError(ErrorResponse(
+ responsecode.CONFLICT,
+ (caldav_namespace, "valid-calendar-data"),
+ description="Invalid property"
+ ))
+
elif property.qname() == caldavxml.ScheduleCalendarTransp.qname():
if not self.isCalendarCollection():
raise HTTPError(StatusResponse(
@@ -1972,6 +2049,43 @@
"""
return None
+class DefaultAlarmPropertyMixin(object):
+ """
+ A mixin for use with calendar home and calendars to allow direct access to
+ the default alarm properties in a more useful way that using readProperty.
+ In particular it will handle inheritance of the property from the home if a
+ calendar does not explicitly have the property.
+ """
+
+ def getDefaultAlarm(self, vevent, timed):
+
+ if vevent:
+ propname = caldavxml.DefaultAlarmVEventDateTime if timed else caldavxml.DefaultAlarmVEventDate
+ else:
+ propname = caldavxml.DefaultAlarmVToDoDateTime if timed else caldavxml.DefaultAlarmVToDoDate
+
+ if self.isCalendarCollection():
+
+ # Sharees never have default alarms
+ if self.isVirtualShare():
+ return None
+
+ # Get from calendar or inherit from home
+ try:
+ prop = self.deadProperties().get(propname.qname())
+ except HTTPError:
+ prop = None
+ if prop is None:
+ prop = self.parentResource().getDefaultAlarm(vevent, timed)
+ else:
+ # Just return whatever is on the home
+ try:
+ prop = self.deadProperties().get(propname.qname())
+ except HTTPError:
+ prop = None
+
+ return str(prop) if prop is not None else None
+
class CommonHomeResource(PropfindCacheMixin, SharedHomeMixin, CalDAVResource):
"""
Logic common to Calendar and Addressbook home resources.
@@ -2381,7 +2495,7 @@
http_MOVE = None
-class CalendarHomeResource(CommonHomeResource):
+class CalendarHomeResource(DefaultAlarmPropertyMixin, CommonHomeResource):
"""
Calendar home collection classmethod.
"""
@@ -2401,8 +2515,19 @@
def liveProperties(self):
existing = super(CalendarHomeResource, self).liveProperties()
- existing += (caldavxml.SupportedCalendarComponentSets.qname(),)
existing += (
+ caldavxml.SupportedCalendarComponentSets.qname(),
+
+ # These are "live" properties in the sense of WebDAV, however "live" for twext actually means
+ # ones that are also always present, but the default alarm properties are allowed to be absent
+ # and are in fact stored in the property store.
+ #caldavxml.DefaultAlarmVEventDateTime.qname(),
+ #caldavxml.DefaultAlarmVEventDate.qname(),
+ #caldavxml.DefaultAlarmVToDoDateTime.qname(),
+ #caldavxml.DefaultAlarmVToDoDate.qname(),
+
+ )
+ existing += (
(customxml.calendarserver_namespace, "xmpp-uri"),
(customxml.calendarserver_namespace, "xmpp-heartbeat-uri"),
(customxml.calendarserver_namespace, "xmpp-server"),
Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py 2011-12-01 21:53:26 UTC (rev 8362)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py 2011-12-01 21:55:30 UTC (rev 8363)
@@ -530,6 +530,8 @@
# split existing calendars into multiples based on component type.
# If on, it will also cause new accounts to provision with separate
# calendars for events and tasks.
+
+ "EnableDefaultAlarms" : True, # Support for default alarms generated by the server
# CardDAV Features
"DirectoryAddressBook": {
@@ -1346,6 +1348,8 @@
compliance += customxml.calendarserver_sharing_no_scheduling_compliance
if configDict.EnableCalendarQueryExtended:
compliance += caldavxml.caldav_query_extended_compliance
+ if configDict.EnableDefaultAlarms:
+ compliance += caldavxml.caldav_default_alarms_compliance
else:
compliance = ()
Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py 2011-12-01 21:53:26 UTC (rev 8362)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py 2011-12-01 21:55:30 UTC (rev 8363)
@@ -50,7 +50,8 @@
from twistedcaldav.notifications import (
NotificationCollectionResource, NotificationResource
)
-from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource
+from twistedcaldav.resource import CalDAVResource, GlobalAddressBookResource,\
+ DefaultAlarmPropertyMixin
from twistedcaldav.schedule import ScheduleInboxResource
from twistedcaldav.scheduling.implicit import ImplicitScheduler
from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
@@ -251,6 +252,9 @@
def url(self):
return joinURL(self._parentResource.url(), self._name, "/")
+ def parentResource(self):
+ return self._parentResource
+
def index(self):
"""
Retrieve the new-style index wrapper.
@@ -949,7 +953,7 @@
return True
-class CalendarCollectionResource(_CalendarCollectionBehaviorMixin, _CommonHomeChildCollectionMixin, CalDAVResource):
+class CalendarCollectionResource(DefaultAlarmPropertyMixin, _CalendarCollectionBehaviorMixin, _CommonHomeChildCollectionMixin, CalDAVResource):
"""
Wrapper around a L{txdav.caldav.icalendar.ICalendar}.
"""
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20111201/c2543056/attachment-0001.html>
More information about the calendarserver-changes
mailing list