Back to index

plone3  3.1.7
calendarsupport.py
Go to the documentation of this file.
00001 #  ATContentTypes http://plone.org/products/atcontenttypes/
00002 #  Archetypes reimplementation of the CMF core types
00003 #  Copyright (c) 2003-2006 AT Content Types development team
00004 #
00005 #  This program is free software; you can redistribute it and/or modify
00006 #  it under the terms of the GNU General Public License as published by
00007 #  the Free Software Foundation; either version 2 of the License, or
00008 #  (at your option) any later version.
00009 #
00010 #  This program is distributed in the hope that it will be useful,
00011 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
00012 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00013 #  GNU General Public License for more details.
00014 #
00015 #  You should have received a copy of the GNU General Public License
00016 #  along with this program; if not, write to the Free Software
00017 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
00018 #
00019 """Calendar export / import
00020 
00021 """
00022 __author__  = 'Christian Heimes <tiran@cheimes.de>'
00023 __docformat__ = 'restructuredtext'
00024 
00025 from cStringIO import StringIO
00026 
00027 from DateTime import DateTime
00028 from Globals import InitializeClass
00029 
00030 from Products.CMFCore.permissions import View
00031 from AccessControl import ClassSecurityInfo
00032 
00033 from Products.ATContentTypes.interfaces import ICalendarSupport
00034 
00035 PRODID = "-//AT Content Types//AT Event//EN"
00036 
00037 # iCal header and footer
00038 ICS_HEADER = """\
00039 BEGIN:VCALENDAR
00040 PRODID:%(prodid)s
00041 VERSION:2.0
00042 METHOD:PUBLISH
00043 """
00044 
00045 ICS_FOOTER = """\
00046 END:VCALENDAR
00047 """
00048 
00049 # Note: a previous version of event start set "SEQUENCE:0"
00050 # That's not necessary unless we're supporting recurrence.
00051 
00052 # iCal event
00053 ICS_EVENT_START = """\
00054 BEGIN:VEVENT
00055 DTSTAMP:%(dtstamp)s
00056 CREATED:%(created)s
00057 UID:ATEvent-%(uid)s
00058 LAST-MODIFIED:%(modified)s
00059 SUMMARY:%(summary)s
00060 DTSTART:%(startdate)s
00061 DTEND:%(enddate)s
00062 """
00063 
00064 # Note: a previous version of event end set "TRANSP:OPAQUE", which would cause
00065 # blocking of the time slot. That's not appropriate for an event
00066 # calendar.
00067 # Also, previous version set "PRIORITY:3", which the RFC interprets as a high
00068 # priority. In absence of a priority field in the event, there's no justification
00069 # for that.
00070 
00071 ICS_EVENT_END = """\
00072 CLASS:PUBLIC
00073 END:VEVENT
00074 """
00075 
00076 # vCal header and footer
00077 VCS_HEADER = """\
00078 BEGIN:VCALENDAR
00079 PRODID:%(prodid)s
00080 VERSION:1.0
00081 """
00082 
00083 VCS_FOOTER = """\
00084 END:VCALENDAR
00085 """
00086 
00087 # vCal event
00088 VCS_EVENT_START = """\
00089 BEGIN:VEVENT
00090 DTSTART:%(startdate)s
00091 DTEND:%(enddate)s
00092 DCREATED:%(created)s
00093 UID:ATEvent-%(uid)s
00094 SEQUENCE:0
00095 LAST-MODIFIED:%(modified)s
00096 SUMMARY:%(summary)s
00097 """
00098 
00099 VCS_EVENT_END = """\
00100 PRIORITY:3
00101 TRANSP:0
00102 END:VEVENT
00103 """
00104 
00105 class CalendarSupportMixin:
00106     """Mixin class for iCal/vCal support
00107     """
00108 
00109     __implements__ = (ICalendarSupport, )
00110 
00111     security       = ClassSecurityInfo()
00112 
00113     actions = ({
00114         'id'          : 'ics',
00115         'name'        : 'iCalendar',
00116         'action'      : 'string:${object_url}/ics_view',
00117         'permissions' : (View, ),
00118         'category'    : 'document_actions',
00119          },
00120          {
00121         'id'          : 'vcs',
00122         'name'        : 'vCalendar',
00123         'action'      : 'string:${object_url}/vcs_view',
00124         'permissions' : (View, ),
00125         'category'    : 'document_actions',
00126          },
00127     )
00128 
00129     _at_action_icons = ({
00130         'category'  : 'plone',
00131         'action_id' : 'ics',
00132         'icon_expr' : 'ical_icon.gif',
00133         'title'     : 'iCalendar export',
00134         'priority'  : 0,
00135         },
00136         {
00137         'category'  : 'plone',
00138         'action_id' : 'vcs',
00139         'icon_expr' : 'vcal_icon.gif',
00140         'title'     : 'vCalendar export',
00141         'priority'  : 0,
00142         },
00143         )
00144 
00145     security.declareProtected(View, 'getICal')
00146     def getICal(self):
00147         """get iCal data
00148         """
00149         out = StringIO()
00150         map = {
00151             'dtstamp'   : rfc2445dt(DateTime()),
00152             'created'   : rfc2445dt(DateTime(self.CreationDate())),
00153             'uid'       : self.UID(),
00154             'modified'  : rfc2445dt(DateTime(self.ModificationDate())),
00155             'summary'   : vformat(self.Title()),
00156             'startdate' : rfc2445dt(self.start()),
00157             'enddate'   : rfc2445dt(self.end()),
00158             }
00159         out.write(ICS_EVENT_START % map)
00160         
00161         description = self.Description()
00162         if description:
00163             out.write(foldLine('DESCRIPTION:%s\n' % vformat(description)))
00164 
00165         location = self.getLocation()
00166         if location:
00167             out.write('LOCATION:%s\n' % vformat(location))
00168 
00169         eventType = self.getEventType()
00170         if eventType:
00171             out.write('CATEGORIES:%s\n' % ','.join(eventType))
00172 
00173         # TODO  -- NO! see the RFC; ORGANIZER field is not to be used for non-group-scheduled entities
00174         #ORGANIZER;CN=%(name):MAILTO=%(email)
00175         #ATTENDEE;CN=%(name);ROLE=REQ-PARTICIPANT:mailto:%(email)
00176 
00177         cn = []
00178         contact = self.contact_name()
00179         if contact:
00180             cn.append(contact)
00181         phone = self.contact_phone()
00182         if phone:
00183             cn.append(phone)
00184         email = self.contact_email()
00185         if email:
00186             cn.append(email)
00187         if cn:
00188             out.write('CONTACT:%s\n' % vformat(', '.join(cn)))
00189 
00190         url = self.event_url()
00191         if url:
00192             out.write('URL:%s\n' % url)
00193 
00194         out.write(ICS_EVENT_END)
00195         return out.getvalue()
00196 
00197 
00198     security.declareProtected(View, 'ics_view')
00199     def ics_view(self, REQUEST, RESPONSE):
00200         """iCalendar output
00201         """
00202         RESPONSE.setHeader('Content-Type', 'text/calendar')
00203         RESPONSE.setHeader('Content-Disposition', 'attachment; filename="%s.ics"' % self.getId())
00204         out = StringIO()
00205         out.write(ICS_HEADER % { 'prodid' : PRODID, })
00206         out.write(self.getICal())
00207         out.write(ICS_FOOTER)
00208         return n2rn(out.getvalue())
00209 
00210     security.declareProtected(View, 'getVCal')
00211     def getVCal(self):
00212         """get vCal data
00213         """
00214         out = StringIO()
00215         map = {
00216             'dtstamp'   : rfc2445dt(DateTime()),
00217             'created'   : rfc2445dt(DateTime(self.CreationDate())),
00218             'uid'       : self.UID(),
00219             'modified'  : rfc2445dt(DateTime(self.ModificationDate())),
00220             'summary'   : vformat(self.Title()),
00221             'startdate' : rfc2445dt(self.start()),
00222             'enddate'   : rfc2445dt(self.end()),
00223             }
00224         out.write(VCS_EVENT_START % map)
00225         description = self.Description()
00226         if description:
00227             out.write(foldLine('DESCRIPTION:%s\n' % vformat(description)))
00228         location = self.getLocation()
00229         if location:
00230             out.write('LOCATION:%s\n' % vformat(location))
00231         out.write(VCS_EVENT_END)
00232         # TODO
00233         # Insert missing code here :]
00234         return out.getvalue()
00235 
00236     security.declareProtected(View, 'vcs_view')
00237     def vcs_view(self, REQUEST, RESPONSE):
00238         """vCalendar output
00239         """
00240         RESPONSE.setHeader('Content-Type', 'text/x-vCalendar')
00241         RESPONSE.setHeader('Content-Disposition', 'attachment; filename="%s.vcs"' % self.getId())
00242         out = StringIO()
00243         out.write(VCS_HEADER % { 'prodid' : PRODID, })
00244         out.write(self.getVCal())
00245         out.write(VCS_FOOTER)
00246         return n2rn(out.getvalue())
00247 
00248 InitializeClass(CalendarSupportMixin)
00249 
00250 def vformat(s):
00251     # return string with escaped commas, colons and semicolons
00252     return s.strip().replace(',','\,').replace(':','\:').replace(';','\;')
00253 
00254 def n2rn(s):
00255     return s.replace('\n', '\r\n')
00256 
00257 def rfc2445dt(dt):
00258     # return UTC in RFC2445 format YYYYMMDDTHHMMSSZ
00259     return dt.HTML4().replace('-','').replace(':','')
00260 
00261 def foldLine(s):
00262     # returns string folded per RFC2445 (each line must be less than 75 octets)
00263     # This code is a minor modification of MakeICS.py, available at:
00264     # http://www.zope.org/Members/Feneric/MakeICS/
00265     
00266     lineLen = 70
00267     
00268     workStr = s.strip().replace('\r\n','\n').replace('\r','\n').replace('\n','\\n')
00269     numLinesToBeProcessed = len(workStr)/lineLen
00270     startingChar = 0
00271     res = ''
00272     while numLinesToBeProcessed >= 1:
00273         res = '%s%s\n '%(res, workStr[startingChar:startingChar+lineLen])
00274         startingChar += lineLen
00275         numLinesToBeProcessed -= 1
00276     return '%s%s\n' % (res, workStr[startingChar:])
00277