Module ical_calendar
[hide private]
[frames] | no frames]

Source Code for Module ical_calendar

  1  ### BITPIM 
  2  ### 
  3  ### Copyright (C) 2006 Joe Pham <djpham@bitpim.org> 
  4  ### 
  5  ### This program is free software; you can redistribute it and/or modify 
  6  ### it under the terms of the BitPim license as detailed in the LICENSE file. 
  7  ### 
  8  ### $Id: ical_calendar.py 4708 2008-09-06 04:10:44Z djpham $ 
  9   
 10  "Deals with iCalendar calendar import stuff" 
 11   
 12  # system modules 
 13  from __future__ import with_statement 
 14  import datetime 
 15  import time 
 16   
 17  # site modules 
 18   
 19  # local modules 
 20  import bpcalendar 
 21  import bptime 
 22  import common_calendar 
 23  import guihelper 
 24  import vcal_calendar as vcal 
 25  import vcard 
 26   
 27  module_debug=False 
 28   
 29  #------------------------------------------------------------------------------- 
30 -class ImportDataSource(common_calendar.ImportDataSource):
31 # how to define, and retrieve calendar import data source 32 message_str="Pick an iCal Calendar File" 33 wildcard='*.ics'
34 35 #-------------------------------------------------------------------------------
36 -class Duration(object):
37 - def __init__(self, data):
38 # Got a dict, compute the time duration in seconds 39 self._duration=0 40 self._neg=False 41 self._extract_data(data)
42 _funcs={ 43 'W': lambda x: x*604800, # 7*24*60*60 44 'H': lambda x: x*3600, # 60*60 45 'M': lambda x: x*60, 46 'S': lambda x: x, 47 'D': lambda x: x*86400, # 24*60*60 48 'T': lambda x: 0, 49 'P': lambda x: 0, 50 }
51 - def _extract_data(self, data):
52 _i=0 53 for _ch in data.get('value', ''): 54 if _ch=='+': 55 self._neg=False 56 elif _ch=='-': 57 self._neg=True 58 elif _ch.isdigit(): 59 _i=_i*10+int(_ch) 60 else: 61 self._duration+=self._funcs.get(_ch, lambda _: 0)(_i) 62 _i=0
63 - def get(self):
64 if self._neg: 65 return -self._duration 66 return self._duration
67 68 #------------------------------------------------------------------------------- 69 parentclass=vcal.VCalendarImportData
70 -class iCalendarImportData(parentclass):
71
72 - def __init__(self, file_name=None):
73 super(iCalendarImportData, self).__init__(file_name)
74
75 - def _conv_alarm(self, v, dd):
76 # return True if there's valid alarm and set dd['alarm_value'] 77 # False otherwise 78 # Only supports negative alarm duration value. 79 try: 80 _params=v.get('params', {}) 81 if _params.get('RELATED', None)=='END': 82 return False 83 if _params.get('VALUE', 'DURATION')!='DURATION': 84 return False 85 _d=Duration(v) 86 if _d.get()>0: 87 return False 88 dd['alarm_value']=abs(_d.get()/60) 89 return True 90 except: 91 if __debug__: 92 raise 93 return False
94
95 - def _conv_valarm(self, v, dd):
96 # convert a VALARM block to alarm value, if available/applicable 97 if v.get('value', None)!='VALARM': 98 return False 99 _trigger=v.get('params', {}).get('TRIGGER', None) 100 if _trigger: 101 return self._conv_alarm(_trigger, dd) 102 return False
103
104 - def _conv_duration(self, v, dd):
105 # compute the 'end' date based on the duration 106 return (datetime.datetime(*dd['start'])+\ 107 datetime.timedelta(seconds=Duration(v).get())).timetuple()[:5]
108
109 - def _conv_date(self, v, dd):
110 if v.get('params', {}).get('VALUE', None)=='DATE': 111 # allday event 112 dd['allday']=True 113 return bptime.BPTime(v['value']).get()
114 115 # building repeat data
116 - def _build_value_dict(self, data):
117 _value={} 118 for _item in data.get('value', '').split(';'): 119 _l=_item.split('=') 120 if len(_l)>1: 121 _value[_l[0]]=_l[1].split(',') 122 else: 123 _value[_l[0]]=[] 124 return _value
125 126 _sorted_weekdays=['FR', 'MO', 'TH', 'TU', 'WE'] 127 _dow_bitmap={ 128 'SU': 1, 129 'MO': 2, 130 'TU': 4, 131 'WE': 8, 132 'TH': 0x10, 133 'FR': 0x20, 134 'SA': 0x40 135 } 136
137 - def _build_daily(self, value, dd):
138 # build a daily repeat event 139 dd['repeat_type']='daily' 140 # only support either every nth day or every weekday 141 # is this every weekday? 142 _days=value.get('BYDAY', []) 143 _days.sort() 144 if _days==self._sorted_weekdays: 145 _interval=0 146 else: 147 try: 148 _interval=int(value.get('INTERVAL', [1])[0]) 149 except ValueError: 150 _interval=1 151 dd['repeat_interval']=_interval 152 return True
153
154 - def _build_weekly(self, value, dd):
155 # build a weekly repeat event 156 dd['repeat_type']='weekly' 157 try: 158 _interval=int(value.get('INTERVAL', [1])[0]) 159 except ValueError: 160 _interval=1 161 dd['repeat_interval']=_interval 162 _dow=0 163 for _day in value.get('BYDAY', []): 164 _dow|=self._dow_bitmap.get(_day, 0) 165 dd['repeat_dow']=_dow 166 return True
167
168 - def _build_monthly(self, value, dd):
169 dd['repeat_type']='monthly' 170 try: 171 _interval2=int(value.get('INTERVAL', [1])[0]) 172 except ValueError: 173 _interval2=1 174 dd['repeat_interval2']=_interval2 175 # nth day of the month by default 176 _nth=0 177 _dow=0 178 _daystr=value.get('BYDAY', [None])[0] 179 if _daystr: 180 # every nth day-of-week ie 1st Monday 181 _dow=self._dow_bitmap.get(_daystr[-2:], 0) 182 _nth=1 183 try: 184 if len(_daystr)>2: 185 _nth=int(_daystr[:-2]) 186 elif value.get('BYSETPOS', [None])[0]: 187 _nth=int(value['BYSETPOS'][0]) 188 except ValueError: 189 pass 190 if _nth==-1: 191 _nth=5 192 if _nth<1 or _nth>5: 193 _nth=1 194 dd['repeat_dow']=_dow 195 dd['repeat_interval']=_nth 196 return True
197
198 - def _build_yearly(self, value, dd):
199 dd['repeat_type']='yearly' 200 return True
201 202 _funcs={ 203 'DAILY': _build_daily, 204 'WEEKLY': _build_weekly, 205 'MONTHLY': _build_monthly, 206 'YEARLY': _build_yearly, 207 }
208 - def _conv_repeat(self, v, dd):
209 _params=v.get('params', {}) 210 _value=self._build_value_dict(v) 211 _rep=self._funcs.get( 212 _value.get('FREQ', [None])[0], lambda *_: False)(self, _value, dd) 213 if _rep: 214 if _value.get('COUNT', [None])[0]: 215 dd['repeat_num']=int(_value['COUNT'][0]) 216 elif _value.get('UNTIL', [None])[0]: 217 dd['repeat_end']=bptime.BPTime(_value['UNTIL'][0]).get() 218 dd['repeat_wkst']=_value.get('WKST', ['MO'])[0] 219 return _rep
220
221 - def _conv_exceptions(self, v, _):
222 r=[] 223 try: 224 _val=v if isinstance(v, (list, tuple)) else [v] 225 for _item in _val: 226 for n in _item['value'].split(','): 227 r.append(bptime.BPTime(n).get()) 228 return r 229 except: 230 if __debug__: 231 raise 232 return []
233
234 - def _conv_start_date(self, v, dd):
235 _dt=bptime.BPTime(v['value']).get(default=(0,0,0, None, None)) 236 if _dt[-1] is None: 237 # all day event 238 dd['allday']=True 239 _dt=_dt[:3]+(0,0) 240 return _dt
241
242 - def _conv_end_date(self, v, _):
243 return bptime.BPTime(v['value']).get(default=(0,0,0, 23,59))
244 245 _calendar_keys=[ 246 ('CATEGORIES', 'categories', parentclass._conv_cat), 247 ('DESCRIPTION', 'notes', parentclass._conv_str), 248 ('DTSTART', 'start', _conv_start_date), 249 ('DTEND', 'end', _conv_end_date), 250 ('DURATION', 'end', _conv_duration), 251 ('LOCATION', 'location', parentclass._conv_str), 252 ('PRIORITY', 'priority', parentclass._conv_priority), 253 ('SUMMARY', 'description', parentclass._conv_str), 254 ('RRULE', 'repeat', _conv_repeat), 255 ('EXDATE', 'exceptions', _conv_exceptions), 256 ('BEGIN-END', 'alarm', _conv_valarm), 257 ]
258 259 #-------------------------------------------------------------------------------
260 -class iCalImportCalDialog(vcal.VcalImportCalDialog):
261 _filetype_label='iCalendar File:' 262 _data_type='iCalendar' 263 _import_data_class=iCalendarImportData
264 265 #------------------------------------------------------------------------------ 266 ExportDialogParent=common_calendar.ExportCalendarDialog 267 out_line=vcard.out_line 268
269 -class ExportDialog(ExportDialogParent):
270 _default_file_name="calendar.ics" 271 _wildcards="ICS files|*.ics" 272
273 - def __init__(self, parent, title):
274 super(ExportDialog, self).__init__(parent, title)
275
276 - def _write_header(self, f):
277 f.write(out_line('BEGIN', None, 'VCALENDAR', None)) 278 f.write(out_line('PRODID', None, '-//BitPim//EN', None)) 279 f.write(out_line('VERSION', None, '2.0', None)) 280 f.write(out_line('METHOD', None, 'PUBLISH', None))
281 - def _write_end(self, f):
282 f.write(out_line('END', None, 'VCALENDAR', None))
283
284 - def _write_timezone(self, f):
285 # write out the timezone info, return a timezone ID 286 f.write(out_line('BEGIN', None, 'VTIMEZONE', None)) 287 _tzid=time.tzname[0].split(' ')[0] 288 f.write(out_line('TZID', None, _tzid, None)) 289 _offset_standard=-((time.timezone/3600)*100+time.timezone%3600) 290 _offset_daylight=_offset_standard+100 291 # standard part 292 f.write(out_line('BEGIN', None, 'STANDARD', None)) 293 f.write(out_line('DTSTART', None, '20051030T020000', None)) 294 f.write(out_line('RRULE', None, 295 'FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=11', None)) 296 f.write(out_line('TZOFFSETFROM', None, 297 '%05d'%_offset_daylight, None)) 298 f.write(out_line('TZOFFSETTO', None, 299 '%05d'%_offset_standard, None)) 300 f.write(out_line('END', None, 'STANDARD', None)) 301 # daylight part 302 f.write(out_line('BEGIN', None, 'DAYLIGHT', None)) 303 f.write(out_line('DTSTART', None, '20060402T020000', None)) 304 f.write(out_line('RRULE', None, 305 'FREQ=YEARLY;INTERVAL=1;BYDAY=2SU;BYMONTH=3', None)) 306 f.write(out_line('TZOFFSETFROM', None, 307 '%05d'%_offset_standard, None)) 308 f.write(out_line('TZOFFSETTO', None, 309 '%05d'%_offset_daylight, None)) 310 f.write(out_line('END', None, 'DAYLIGHT', None)) 311 # all done 312 f.write(out_line('END', None, 'VTIMEZONE', None)) 313 return _tzid
314 315 # support writing to ICS file routines
316 - def _write_categories(self, keyword, v, *args):
317 _cats=[x['category'] for x in v] 318 if _cats: 319 return out_line(keyword, None, ','.join(_cats), None)
320 - def _write_string(self, keyword, v, *args):
321 if v: 322 return out_line(keyword, None, v, None)
323 - def _write_priority(self, keyword, v, *args):
324 if v<1: 325 return 326 return out_line(keyword, None, '%d'%min(v, 9), None)
327 - def _write_alarm(self, keyword, v, *args):
328 if v<0: 329 # No Alarm 330 return 331 _res=out_line('BEGIN', None, 'VALARM', None) 332 _res+=out_line('TRIGGER', None, 333 '-P%dDT%dH%dM'%(v/1440, (v%1440)/60, v%60), None) 334 _res+=out_line('ACTION', None, 'AUDIO', None) 335 _res+=out_line('END', None, 'VALARM', None) 336 return _res
337 - def _write_times_single(self, keyword, v, event, tzid):
338 # write the DTSTART/DTEND property for a single 339 # (non-recurrent) event 340 _start=bptime.BPTime(event.start) 341 _end=bptime.BPTime(event.end) 342 if event.allday: 343 # all day event 344 _params=('VALUE=DATE',) 345 _res=out_line('DTSTART', _params, 346 _start.iso_str(no_time=True), None) 347 _end+=bptime.timedelta(days=1) 348 _res+=out_line('DTEND', _params, 349 _end.iso_str(no_time=True), None) 350 else: 351 _params=('TZID=%s'%tzid,) 352 _res=out_line('DTSTART', _params, _start.iso_str(no_seconds=False), None) 353 _res+=out_line('DTEND', _params, _end.iso_str(no_seconds=False), None) 354 return _res
355 - def _write_start(self, event, tzid):
356 # write the DTSTART/DURATION property for a recurrent event 357 _start=bptime.BPTime(event.start) 358 _end=bptime.BPTime(event.end) 359 if event.allday: 360 # all day event, can only handle sameday allday event (for now) 361 _params=('VALUE=DATE',) 362 _res=out_line('DTSTART', _params, 363 _start.iso_str(no_time=True), None) 364 _end+=bptime.timedelta(days=1) 365 _res+=out_line('DTEND', _params, 366 _end.iso_str(no_time=True), None) 367 else: 368 # can only handle 24hr-long event (for now) 369 _new_end=_start+(_end-_start).seconds 370 _params=('TZID=%s'%tzid,) 371 _res=out_line('DTSTART', _params, _start.iso_str(no_seconds=False), None) 372 _res+=out_line('DTEND', _params, _new_end.iso_str(no_seconds=False), None) 373 return _res
374 - def _write_repeat_daily(self, event, rpt):
375 _value=['FREQ=DAILY'] 376 if not event.open_ended(): 377 _value.append('UNTIL=%04d%02d%02dT000000Z'%event.end[:3]) 378 if rpt.interval: 379 # every nth day 380 _value.append('INTERVAL=%d'%rpt.interval) 381 else: 382 # weekday 383 _value.append('BYDAY=MO,TU,WE,TH,FR') 384 return out_line('RRULE', None, ';'.join(_value), None)
385 _dow_list=( 386 (1, 'SU'), (2, 'MO'), (4, 'TU'), (8, 'WE'), (16, 'TH'), 387 (32, 'FR'), (64, 'SA')) 388 _dow_wkst={ 389 1: 'MO', 2: 'TU', 3: 'WE', 4: 'TH', 5: 'FR', 6: 'SA', 7: 'SU' }
390 - def _write_repeat_weekly(self, event, rpt):
391 _dow=rpt.dow 392 _byday=','.join([x[1] for x in self._dow_list \ 393 if _dow&x[0] ]) 394 _value=['FREQ=WEEKLY', 395 'INTERVAL=%d'%rpt.interval, 396 'BYDAY=%s'%_byday, 397 'WKST=%s'%self._dow_wkst.get(rpt.weekstart, 'MO')] 398 if not event.open_ended(): 399 _value.append('UNTIL=%04d%02d%02d'%event.end[:3]) 400 return out_line('RRULE', None, ';'.join(_value), None)
401 - def _write_repeat_monthly(self, event, rpt):
402 _value=['FREQ=MONTHLY', 403 'INTERVAL=%d'%rpt.interval2, 404 ] 405 if not event.open_ended(): 406 _value.append('UNTIL=%04d%02d%02dT000000Z'%event.end[:3]) 407 _dow=rpt.dow 408 if _dow==0: 409 # every n-day of the month 410 _value.append('BYMONTHDAY=%d'%event.start[2]) 411 else: 412 # every n-th day-of-week (ie 1st Monday) 413 for _entry in self._dow_list: 414 if _dow & _entry[0]: 415 _dow_name=_entry[1] 416 break 417 if rpt.interval<5: 418 _nth=rpt.interval 419 else: 420 _nth=-1 421 _value.append('BYDAY=%d%s'%(_nth, _dow_name)) 422 return out_line('RRULE', None, ';'.join(_value), None)
423 - def _write_repeat_yearly(self, event, rpt):
424 _value=['FREQ=YEARLY', 425 'INTERVAL=1', 426 'BYMONTH=%d'%event.start[1], 427 'BYMONTHDAY=%d'%event.start[2], 428 ] 429 if not event.open_ended(): 430 _value.append('UNTIL=%04d%02d%02dT000000Z'%event.end[:3]) 431 return out_line('RRULE', None, ';'.join(_value), None)
432 - def _write_repeat_exceptions(self, event, rpt):
433 # write out the exception dates 434 return out_line('EXDATE', ('VALUE=DATE',), 435 ','.join([x.iso_str(no_time=True) for x in rpt.suppressed]), 436 None)
437 - def _write_repeat(self, event):
438 _repeat=event.repeat 439 _type=_repeat.repeat_type 440 if _type: 441 _res=getattr(self, '_write_repeat_'+_type, lambda *_: None)(event, _repeat) 442 if _res and _repeat.suppressed: 443 _res+=self._write_repeat_exceptions(event, _repeat) 444 return _res
445 - def _write_times_repeat(self, keyword, v, event, tzid):
446 return self._write_start(event, tzid)+self._write_repeat(event)
447 - def _write_times(self, keyword, v, event, tzid):
448 # write the START and DURATION property 449 if event.repeat: 450 return self._write_times_repeat(keyword, v, event, tzid) 451 else: 452 return self._write_times_single(keyword, v, event, tzid)
453 454 _field_list=( 455 ('SUMMARY', 'description', _write_string), 456 ('DESCRIPTION', 'notes', _write_string), 457 ('DTSTART', 'start', _write_times), 458 ('LOCATION', 'location', _write_string), 459 ('PRIORITY', 'priority', _write_priority), 460 ('CATEGORIES', 'categories', _write_categories), 461 ('TRIGGER', 'alarm', _write_alarm), 462 ) 463
464 - def _write_event(self, f, event, tzid):
465 # write out an BitPim Calendar event 466 f.write(out_line('COMMENT', None, '//----------', None)) 467 f.write(out_line('BEGIN', None, 'VEVENT', None)) 468 for _entry in self._field_list: 469 _v=getattr(event, _entry[1], None) 470 if _v is not None: 471 _line=_entry[2](self, _entry[0], _v, event, tzid) 472 if _line: 473 f.write(_line) 474 f.write(out_line('DTSTAMP', None, 475 '%04d%02d%02dT%02d%02d%02dZ'%time.gmtime()[:6], 476 None)) 477 f.write(out_line('UID', None, event.id, None)) 478 f.write(out_line('END', None, 'VEVENT', None))
479 - def _export(self):
480 filename=self.filenamectrl.GetValue() 481 try: 482 f=file(filename, 'wt') 483 except: 484 f=None 485 if f is None: 486 guihelper.MessageDialog(self, 'Failed to open file ['+filename+']', 487 'Export Error') 488 return 489 all_items=self._selection.GetSelection()==0 490 dt=self._start_date.GetValue() 491 range_start=(dt.GetYear(), dt.GetMonth()+1, dt.GetDay()) 492 dt=self._end_date.GetValue() 493 range_end=(dt.GetYear(), dt.GetMonth()+1, dt.GetDay()) 494 cal_dict=self.GetParent().GetCalendarData() 495 self._write_header(f) 496 _tzid=self._write_timezone(f) 497 # now go through the data and export each event 498 for k,e in cal_dict.items(): 499 if not all_items and \ 500 (e.end < range_start or e.start>range_end): 501 continue 502 self._write_event(f, e, _tzid) 503 self._write_end(f) 504 f.close()
505