[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