PyXR

c:\projects\bitpim\src \ ical_calendar.py



0001 ### BITPIM
0002 ###
0003 ### Copyright (C) 2006 Joe Pham <djpham@bitpim.org>
0004 ###
0005 ### This program is free software; you can redistribute it and/or modify
0006 ### it under the terms of the BitPim license as detailed in the LICENSE file.
0007 ###
0008 ### $Id: ical_calendar.py 4396 2007-09-12 21:44:00Z djpham $
0009 
0010 "Deals with iCalendar calendar import stuff"
0011 
0012 # system modules
0013 from __future__ import with_statement
0014 import datetime
0015 import time
0016 
0017 # site modules
0018 
0019 # local modules
0020 import bpcalendar
0021 import bptime
0022 import common_calendar
0023 import guihelper
0024 import vcal_calendar as vcal
0025 import vcard
0026 
0027 module_debug=False
0028 
0029 #-------------------------------------------------------------------------------
0030 class ImportDataSource(common_calendar.ImportDataSource):
0031     # how to define, and retrieve calendar import data source
0032     message_str="Pick an iCal Calendar File"
0033     wildcard='*.ics'
0034 
0035 #-------------------------------------------------------------------------------
0036 class Duration(object):
0037     def __init__(self, data):
0038         # Got a dict, compute the time duration in seconds
0039         self._duration=0
0040         self._neg=False
0041         self._extract_data(data)
0042     _funcs={
0043         'W': lambda x: x*604800,    # 7*24*60*60
0044         'H': lambda x: x*3600,      # 60*60
0045         'M': lambda x: x*60,
0046         'S': lambda x: x,
0047         'D': lambda x: x*86400,     # 24*60*60
0048         'T': lambda x: 0,
0049         'P': lambda x: 0,
0050         }
0051     def _extract_data(self, data):
0052         _i=0
0053         for _ch in data.get('value', ''):
0054             if _ch=='+':
0055                 self._neg=False
0056             elif _ch=='-':
0057                 self._neg=True
0058             elif _ch.isdigit():
0059                 _i=_i*10+int(_ch)
0060             else:
0061                 self._duration+=self._funcs.get(_ch, lambda _: 0)(_i)
0062                 _i=0
0063     def get(self):
0064         if self._neg:
0065             return -self._duration
0066         return self._duration
0067 
0068 #-------------------------------------------------------------------------------
0069 parentclass=vcal.VCalendarImportData
0070 class iCalendarImportData(parentclass):
0071 
0072     def __init__(self, file_name=None):
0073         super(iCalendarImportData, self).__init__(file_name)
0074 
0075     def _conv_alarm(self, v, dd):
0076         # return True if there's valid alarm and set dd['alarm_value']
0077         # False otherwise
0078         # Only supports negative alarm duration value.
0079         try:
0080             _params=v.get('params', {})
0081             if _params.get('RELATED', None)=='END':
0082                 return False
0083             if _params.get('VALUE', 'DURATION')!='DURATION':
0084                 return False
0085             _d=Duration(v)
0086             if _d.get()>0:
0087                 return False
0088             dd['alarm_value']=abs(_d.get()/60)
0089             return True
0090         except:
0091             if __debug__:
0092                 raise
0093             return False
0094 
0095     def _conv_valarm(self, v, dd):
0096         # convert a VALARM block to alarm value, if available/applicable
0097         if v.get('value', None)!='VALARM':
0098             return False
0099         _trigger=v.get('params', {}).get('TRIGGER', None)
0100         if _trigger:
0101             return self._conv_alarm(_trigger, dd)
0102         return False
0103         
0104     def _conv_duration(self, v, dd):
0105         # compute the 'end' date based on the duration
0106         return (datetime.datetime(*dd['start'])+\
0107                 datetime.timedelta(seconds=Duration(v).get())).timetuple()[:5]
0108 
0109     def _conv_date(self, v, dd):
0110         if v.get('params', {}).get('VALUE', None)=='DATE':
0111             # allday event
0112             dd['allday']=True
0113         return bptime.BPTime(v['value']).get()
0114 
0115     # building repeat data
0116     def _build_value_dict(self, data):
0117         _value={}
0118         for _item in data.get('value', '').split(';'):
0119             _l=_item.split('=')
0120             if len(_l)>1:
0121                 _value[_l[0]]=_l[1].split(',')
0122             else:
0123                 _value[_l[0]]=[]
0124         return _value
0125 
0126     _sorted_weekdays=['FR', 'MO', 'TH', 'TU', 'WE']
0127     _dow_bitmap={
0128         'SU': 1,
0129         'MO': 2,
0130         'TU': 4,
0131         'WE': 8,
0132         'TH': 0x10,
0133         'FR': 0x20,
0134         'SA': 0x40
0135         }
0136 
0137     def _build_daily(self, value, dd):
0138         # build a daily repeat event
0139         dd['repeat_type']='daily'
0140         # only support either every nth day or every weekday
0141         # is this every weekday?
0142         _days=value.get('BYDAY', [])
0143         _days.sort()
0144         if _days==self._sorted_weekdays:
0145             _interval=0
0146         else:
0147             try:
0148                 _interval=int(value.get('INTERVAL', [1])[0])
0149             except ValueError:
0150                 _interval=1
0151         dd['repeat_interval']=_interval
0152         return True
0153 
0154     def _build_weekly(self, value, dd):
0155         # build a weekly repeat event
0156         dd['repeat_type']='weekly'
0157         try:
0158             _interval=int(value.get('INTERVAL', [1])[0])
0159         except ValueError:
0160             _interval=1
0161         dd['repeat_interval']=_interval
0162         _dow=0
0163         for _day in value.get('BYDAY', []):
0164             _dow|=self._dow_bitmap.get(_day, 0)
0165         dd['repeat_dow']=_dow
0166         return True
0167 
0168     def _build_monthly(self, value, dd):
0169         dd['repeat_type']='monthly'
0170         try:
0171             _interval2=int(value.get('INTERVAL', [1])[0])
0172         except ValueError:
0173             _interval2=1
0174         dd['repeat_interval2']=_interval2
0175         # nth day of the month by default
0176         _nth=0
0177         _dow=0
0178         _daystr=value.get('BYDAY', [None])[0]
0179         if _daystr:
0180             # every nth day-of-week ie 1st Monday
0181             _dow=self._dow_bitmap.get(_daystr[-2:], 0)
0182             _nth=1
0183             try:
0184                 if len(_daystr)>2:
0185                     _nth=int(_daystr[:-2])
0186                 elif value.get('BYSETPOS', [None])[0]:
0187                     _nth=int(value['BYSETPOS'][0])
0188             except ValueError:
0189                 pass
0190             if _nth==-1:
0191                 _nth=5
0192             if _nth<1 or _nth>5:
0193                 _nth=1
0194         dd['repeat_dow']=_dow
0195         dd['repeat_interval']=_nth
0196         return True
0197 
0198     def _build_yearly(self, value, dd):
0199         dd['repeat_type']='yearly'
0200         return True
0201 
0202     _funcs={
0203         'DAILY': _build_daily,
0204         'WEEKLY': _build_weekly,
0205         'MONTHLY': _build_monthly,
0206         'YEARLY': _build_yearly,
0207         }
0208     def _conv_repeat(self, v, dd):
0209         _params=v.get('params', {})
0210         _value=self._build_value_dict(v)
0211         _rep=self._funcs.get(
0212             _value.get('FREQ', [None])[0], lambda *_: False)(self, _value, dd)
0213         if _rep:
0214             if _value.get('COUNT', [None])[0]:
0215                 dd['repeat_num']=int(_value['COUNT'][0])
0216             elif _value.get('UNTIL', [None])[0]:
0217                 dd['repeat_end']=bptime.BPTime(_value['UNTIL'][0]).get()
0218             dd['repeat_wkst']=_value.get('WKST', ['MO'])[0]
0219         return _rep
0220 
0221     def _conv_exceptions(self, v, _):
0222         try:
0223             l=v['value'].split(',')
0224             r=[]
0225             for n in l:
0226                 r.append(bptime.BPTime(n).get())
0227             return r
0228         except:
0229             if __debug__:
0230                 raise
0231             return []
0232 
0233     def _conv_start_date(self, v, dd):
0234         _dt=bptime.BPTime(v['value']).get(default=(0,0,0, None, None))
0235         if _dt[-1] is None:
0236             # all day event
0237             dd['allday']=True
0238             _dt=_dt[:3]+(0,0)
0239         return _dt
0240 
0241     def _conv_end_date(self, v, _):
0242         return bptime.BPTime(v['value']).get(default=(0,0,0, 23,59))
0243 
0244     _calendar_keys=[
0245         ('CATEGORIES', 'categories', parentclass._conv_cat),
0246         ('DESCRIPTION', 'notes', parentclass._conv_str),
0247         ('DTSTART', 'start', _conv_start_date),
0248         ('DTEND', 'end', _conv_end_date),
0249         ('DURATION', 'end', _conv_duration),
0250         ('LOCATION', 'location', parentclass._conv_str),
0251         ('PRIORITY', 'priority', parentclass._conv_priority),
0252         ('SUMMARY', 'description', parentclass._conv_str),
0253         ('RRULE', 'repeat', _conv_repeat),
0254         ('EXDATE', 'exceptions', _conv_exceptions),
0255         ('BEGIN-END', 'alarm', _conv_valarm),
0256         ]
0257 
0258 #-------------------------------------------------------------------------------
0259 class iCalImportCalDialog(vcal.VcalImportCalDialog):
0260     _filetype_label='iCalendar File:'
0261     _data_type='iCalendar'
0262     _import_data_class=iCalendarImportData
0263 
0264 #------------------------------------------------------------------------------
0265 ExportDialogParent=common_calendar.ExportCalendarDialog
0266 out_line=vcard.out_line
0267 
0268 class ExportDialog(ExportDialogParent):
0269     _default_file_name="calendar.ics"
0270     _wildcards="ICS files|*.ics"
0271 
0272     def __init__(self, parent, title):
0273         super(ExportDialog, self).__init__(parent, title)
0274 
0275     def _write_header(self, f):
0276         f.write(out_line('BEGIN', None, 'VCALENDAR', None))
0277         f.write(out_line('PRODID', None, '-//BitPim//EN', None))
0278         f.write(out_line('VERSION', None, '2.0', None))
0279         f.write(out_line('METHOD', None, 'PUBLISH', None))
0280     def _write_end(self, f):
0281         f.write(out_line('END', None, 'VCALENDAR', None))
0282 
0283     def _write_timezone(self, f):
0284         # write out the timezone info, return a timezone ID
0285         f.write(out_line('BEGIN', None, 'VTIMEZONE', None))
0286         _tzid=time.tzname[0].split(' ')[0]
0287         f.write(out_line('TZID', None, _tzid, None))
0288         _offset_standard=-((time.timezone/3600)*100+time.timezone%3600)
0289         _offset_daylight=_offset_standard+100
0290         # standard part
0291         f.write(out_line('BEGIN', None, 'STANDARD', None))
0292         f.write(out_line('DTSTART', None, '20051030T020000', None))
0293         f.write(out_line('RRULE', None,
0294                          'FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11', None))
0295         f.write(out_line('TZOFFSETFROM', None,
0296                          '%05d'%_offset_daylight, None))
0297         f.write(out_line('TZOFFSETTO', None,
0298                          '%05d'%_offset_standard, None))
0299         f.write(out_line('END', None, 'STANDARD', None))
0300         # daylight part
0301         f.write(out_line('BEGIN', None, 'DAYLIGHT', None))
0302         f.write(out_line('DTSTART', None, '20060402T020000', None))
0303         f.write(out_line('RRULE', None,
0304                          'FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3', None))
0305         f.write(out_line('TZOFFSETFROM', None,
0306                          '%05d'%_offset_standard, None))
0307         f.write(out_line('TZOFFSETTO', None,
0308                          '%05d'%_offset_daylight, None))
0309         f.write(out_line('END', None, 'DAYLIGHT', None))
0310         # all done
0311         f.write(out_line('END', None, 'VTIMEZONE', None))
0312         return _tzid
0313 
0314     # support writing to ICS file routines
0315     def _write_categories(self, keyword, v, *args):
0316         _cats=[x['category'] for x in v]
0317         if _cats:
0318             return out_line(keyword, None, ','.join(_cats), None)
0319     def _write_string(self, keyword, v, *args):
0320         if v:
0321             return out_line(keyword, None, v, None)
0322     def _write_priority(self, keyword, v, *args):
0323         if v<1:
0324             return
0325         return out_line(keyword, None, '%d'%min(v, 9), None)
0326     def _write_alarm(self, keyword, v, *args):
0327         if v<0:
0328             # No Alarm
0329             return
0330         _res=out_line('BEGIN', None, 'VALARM', None)
0331         _res+=out_line('TRIGGER', None,
0332                        '-P%dDT%dH%dM'%(v/1440, (v%1440)/60, v%60), None)
0333         _res+=out_line('ACTION', None, 'AUDIO', None)
0334         _res+=out_line('END', None, 'VALARM', None)
0335         return _res
0336     def _write_times_single(self, keyword, v, event, tzid):
0337         # write the DTSTART/DTEND property for a single
0338         # (non-recurrent) event
0339         _start=bptime.BPTime(event.start)
0340         _end=bptime.BPTime(event.end)
0341         if event.allday:
0342             # all day event
0343             _params=('VALUE=DATE',)
0344             _res=out_line('DTSTART', _params,
0345                           _start.iso_str(no_time=True), None)
0346             _res+=out_line('DTEND', _params,
0347                            _end.iso_str(no_time=True), None)
0348         else:
0349             _params=('TZID=%s'%tzid,)
0350             _res=out_line('DTSTART', _params, _start.iso_str(no_seconds=False), None)
0351             _res+=out_line('DTEND', _params, _end.iso_str(no_seconds=False), None)
0352         return _res
0353     def _write_start(self, event, tzid):
0354         # write the DTSTART/DURATION property for a recurrent event
0355         _start=bptime.BPTime(event.start)
0356         _end=bptime.BPTime(event.end)
0357         if event.allday:
0358             # all day event, can only handle sameday allday event (for now)
0359             _date_str=_start.iso_str(no_time=True)
0360             _params=('VALUE=DATE',)
0361             _res=out_line('DTSTART', _params, _date_str, None)
0362             _res+=out_line('DTEND', _params, _date_str, None)
0363         else:
0364             # can only handle 24hr-long event (for now)
0365             _new_end=_start+(_end-_start).seconds
0366             _params=('TZID=%s'%tzid,)
0367             _res=out_line('DTSTART', _params, _start.iso_str(no_seconds=False), None)
0368             _res+=out_line('DTEND',  _params, _new_end.iso_str(no_seconds=False), None)
0369         return _res
0370     def _write_repeat_daily(self, event, rpt):
0371         _value=['FREQ=DAILY']
0372         if not event.open_ended():
0373             _value.append('UNTIL=%04d%02d%02dT000000Z'%event.end[:3])
0374         if rpt.interval:
0375             # every nth day
0376             _value.append('INTERVAL=%d'%rpt.interval)
0377         else:
0378             # weekday
0379             _value.append('BYDAY=MO,TU,WE,TH,FR')
0380         return out_line('RRULE', None, ';'.join(_value), None)
0381     _dow_list=(
0382         (1, 'SU'), (2, 'MO'), (4, 'TU'), (8, 'WE'), (16, 'TH'),
0383         (32, 'FR'), (64, 'SA'))
0384     _dow_wkst={
0385         1: 'MO', 2: 'TU', 3: 'WE', 4: 'TH', 5: 'FR', 6: 'SA', 7: 'SU' }
0386     def _write_repeat_weekly(self, event, rpt):
0387         _dow=rpt.dow
0388         _byday=','.join([x[1] for x in self._dow_list \
0389                          if _dow&x[0] ])
0390         _value=['FREQ=WEEKLY',
0391                 'INTERVAL=%d'%rpt.interval,
0392                 'BYDAY=%s'%_byday,
0393                 'WKST=%s'%self._dow_wkst.get(rpt.weekstart, 'MO')]
0394         if not event.open_ended():
0395             _value.append('UNTIL=%04d%02d%02d'%event.end[:3])
0396         return out_line('RRULE', None, ';'.join(_value), None)
0397     def _write_repeat_monthly(self, event, rpt):
0398         _value=['FREQ=MONTHLY',
0399                 'INTERVAL=%d'%rpt.interval2,
0400                 ]
0401         if not event.open_ended():
0402             _value.append('UNTIL=%04d%02d%02dT000000Z'%event.end[:3])
0403         _dow=rpt.dow
0404         if _dow==0:
0405             # every n-day of the month
0406             _value.append('BYMONTHDAY=%d'%event.start[2])
0407         else:
0408             # every n-th day-of-week (ie 1st Monday)
0409             for _entry in self._dow_list:
0410                 if _dow & _entry[0]:
0411                     _dow_name=_entry[1]
0412                     break
0413             if rpt.interval<5:
0414                 _nth=rpt.interval
0415             else:
0416                 _nth=-1
0417             _value.append('BYDAY=%d%s'%(_nth, _dow_name))
0418         return out_line('RRULE', None, ';'.join(_value), None)
0419     def _write_repeat_yearly(self, event, rpt):
0420         _value=['FREQ=YEARLY',
0421                 'INTERVAL=1',
0422                 'BYMONTH=%d'%event.start[1],
0423                 'BYMONTHDAY=%d'%event.start[2],
0424                 ]
0425         if not event.open_ended():
0426             _value.append('UNTIL=%04d%02d%02dT000000Z'%event.end[:3])
0427         return out_line('RRULE', None, ';'.join(_value), None)
0428     def _write_repeat_exceptions(self, event, rpt):
0429         # write out the exception dates
0430         return out_line('EXDATE', ('VALUE=DATE',),
0431                         ','.join([x.iso_str(no_time=True) for x in rpt.suppressed]),
0432                         None)
0433     def _write_repeat(self, event):
0434         _repeat=event.repeat
0435         _type=_repeat.repeat_type
0436         if _type:
0437             _res=getattr(self, '_write_repeat_'+_type, lambda *_: None)(event, _repeat)
0438             if _res and _repeat.suppressed:
0439                 _res+=self._write_repeat_exceptions(event, _repeat)
0440             return _res
0441     def _write_times_repeat(self, keyword, v, event, tzid):
0442         return self._write_start(event, tzid)+self._write_repeat(event)
0443     def _write_times(self, keyword, v, event, tzid):
0444         # write the START and DURATION property
0445         if event.repeat:
0446             return self._write_times_repeat(keyword, v, event, tzid)
0447         else:
0448             return self._write_times_single(keyword, v, event, tzid)
0449 
0450     _field_list=(
0451         ('SUMMARY', 'description', _write_string),
0452         ('DESCRIPTION', 'notes', _write_string),
0453         ('DTSTART', 'start', _write_times),
0454         ('LOCATION', 'location', _write_string),
0455         ('PRIORITY', 'priority', _write_priority),
0456         ('CATEGORIES', 'categories', _write_categories),
0457         ('TRIGGER', 'alarm', _write_alarm),
0458         )
0459 
0460     def _write_event(self, f, event, tzid):
0461         # write out an BitPim Calendar event
0462         f.write(out_line('COMMENT', None, '//----------', None))
0463         f.write(out_line('BEGIN', None, 'VEVENT', None))
0464         for _entry in self._field_list:
0465             _v=getattr(event, _entry[1], None)
0466             if _v is not None:
0467                 _line=_entry[2](self, _entry[0], _v, event, tzid)
0468                 if _line:
0469                     f.write(_line)
0470         f.write(out_line('DTSTAMP', None,
0471                          '%04d%02d%02dT%02d%02d%02dZ'%time.gmtime()[:6],
0472                          None))
0473         f.write(out_line('UID', None, event.id, None))
0474         f.write(out_line('END', None, 'VEVENT', None))
0475     def _export(self):
0476         filename=self.filenamectrl.GetValue()
0477         try:
0478             f=file(filename, 'wt')
0479         except:
0480             f=None
0481         if f is None:
0482             guihelper.MessageDialog(self, 'Failed to open file ['+filename+']',
0483                                     'Export Error')
0484             return
0485         all_items=self._selection.GetSelection()==0
0486         dt=self._start_date.GetValue()
0487         range_start=(dt.GetYear(), dt.GetMonth()+1, dt.GetDay())
0488         dt=self._end_date.GetValue()
0489         range_end=(dt.GetYear(), dt.GetMonth()+1, dt.GetDay())
0490         cal_dict=self.GetParent().GetCalendarData()
0491         self._write_header(f)
0492         _tzid=self._write_timezone(f)
0493         # now go through the data and export each event
0494         for k,e in cal_dict.items():
0495             if not all_items and \
0496                (e.end < range_start or e.start>range_end):
0497                 continue
0498             self._write_event(f, e, _tzid)
0499         self._write_end(f)
0500         f.close()
0501 

Generated by PyXR 0.9.4