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