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

Source Code for Module bpcalendar

   1  #!/usr/bin/env python 
   2  ### BITPIM 
   3  ### 
   4  ### Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com> 
   5  ### 
   6  ### This program is free software; you can redistribute it and/or modify 
   7  ### it under the terms of the BitPim license as detailed in the LICENSE file. 
   8  ### 
   9  ### $Id: bpcalendar.py 4706 2008-09-03 21:40:23Z djpham $ 
  10   
  11  """Calendar user interface and data for bitpim. 
  12   
  13  This module has a bp prefix so it doesn't clash with the system calendar module 
  14   
  15  Version 3: 
  16   
  17  The format for the calendar is standardised.  It is a dict with the following 
  18  fields: 
  19  (Note: hour fields are in 24 hour format) 
  20  'string id': CalendarEntry object. 
  21   
  22  CalendarEntry properties: 
  23  description - 'string description' 
  24  location - 'string location' 
  25  desc_loc - combination of description & location in the form of 'description[location]' 
  26  priority - None=no priority, int from 1-10, 1=highest priority 
  27  alarm - how many minutes beforehand to set the alarm (use 0 for on-time, None or -1 for no alarm) 
  28  allday - True for an allday event, False otherwise 
  29  start - (year, month, day, hour, minute) as integers 
  30  end - (year, month, day, hour, minute) as integers 
  31  serials - list of dicts of serials. 
  32  repeat - None, or RepeatEntry object 
  33  id - string id of this object.  Created the same way as bpserials IDs for phonebook entries. 
  34  notes - string notes 
  35  categories - [ { 'category': string category }, ... ] 
  36  ringtone - string ringtone assignment 
  37  wallpaper - string wallpaper assignment. 
  38  vibrate - True if the alarm is set to vibrate, False otherwise 
  39  voice - ID of voice alarm 
  40   
  41  CalendarEntry methods: 
  42  get() - return a copy of the internal dict 
  43  get_db_dict()- return a copy of a database.basedataobject dict. 
  44  set(dict) - set the internal dict with the supplied dict 
  45  set_db_dict(dict) - set internal data with the database.basedataobject dict 
  46  is_active(y, m, d) - True if this event is active on (y,m,d) 
  47  suppress_repeat_entry(y,m,d) - exclude (y,m,d) from this repeat event. 
  48   
  49  RepeatEntry properties: 
  50  repeat_type - one of daily, weekly, monthly, or yearly. 
  51  interval - for daily: repeat every nth day.  For weekly, for every nth week. 
  52  interval2 - for monhtly: repeat every nth month. 
  53  dow - bitmap of which day of week are being repeated. 
  54  weekstart - the start of the work week ('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU') 
  55  suppressed - list of (y,m,d) being excluded from this series. 
  56   
  57  -------------------------------------------------------------------------------- 
  58  Version 2: 
  59   
  60  The format for the calendar is standardised.  It is a dict with the following 
  61  fields: 
  62   
  63  (Note: hour fields are in 24 hour format) 
  64   
  65  start: 
  66   
  67     - (year, month, day, hour, minute) as integers 
  68  end: 
  69   
  70     - (year, month, day, hour, minute) as integers  # if you want no end, set to the same value as start, or to the year 4000 
  71   
  72  repeat: 
  73   
  74     - one of None, "daily", "monfri", "weekly", "monthly", "yearly" 
  75   
  76  description: 
  77   
  78     - "String description" 
  79      
  80  changeserial: 
  81   
  82     - Set to integer 1 
  83      
  84  snoozedelay: 
  85   
  86     - Set to an integer number of minutes (default 0) 
  87      
  88  alarm: 
  89   
  90     - how many minutes beforehand to set the alarm (use 0 for on-time, None for no alarm) 
  91      
  92  daybitmap: 
  93   
  94     - default 0, it will become which days of the week weekly events happen on (eg every monday and friday) 
  95      
  96  ringtone: 
  97   
  98     - index number of the ringtone for the alarm (use 0 for none - will become a string) 
  99      
 100  pos: 
 101   
 102     - integer that should be the same as the dictionary key for this entry 
 103      
 104  exceptions: 
 105   
 106     - (optional) A list of (year,month,day) tuples that repeats are suppressed 
 107  """ 
 108   
 109  # Standard modules 
 110  from __future__ import with_statement 
 111  import os 
 112  import copy 
 113  import calendar 
 114  import datetime 
 115  import random 
 116  import sha 
 117  import time 
 118   
 119  # wx stuff 
 120  import wx 
 121  import wx.lib 
 122  import wx.lib.masked.textctrl 
 123  import wx.lib.intctrl 
 124  import wx.grid as gridlib 
 125   
 126  # my modules 
 127  import bphtml 
 128  import bptime 
 129  import calendarcontrol 
 130  import calendarentryeditor 
 131  import common 
 132  import database 
 133  import guihelper 
 134  import guiwidgets 
 135  import helpids 
 136  import pubsub 
 137  import today 
 138  import xyaptu 
139 140 #------------------------------------------------------------------------------- 141 -class CalendarDataObject(database.basedataobject):
142 """ 143 This class is a wrapper class to enable CalendarEntry object data to be 144 stored in the database stuff. Once the database module is updated, this 145 class will also be updated and eventually replace CalendarEntry. 146 """ 147 _knownproperties=['description', 'location', 'priority', 'alarm', 148 'notes', 'ringtone', 'wallpaper', 149 'start', 'end', 'vibrate', 'voice' ] 150 _knownlistproperties=database.basedataobject._knownlistproperties.copy() 151 _knownlistproperties.update( { 152 'repeat': ['type', 'interval', 153 'interval2', 'dow', 'weekstart'], 154 'suppressed': ['date'], 155 'categories': ['category'] })
156 - def __init__(self, data=None):
157 if data is None or not isinstance(data, CalendarEntry): 158 # empty data, do nothing 159 return 160 self.update(data.get_db_dict())
161 162 calendarobjectfactory=database.dataobjectfactory(CalendarDataObject)
163 #------------------------------------------------------------------------------- 164 -class RepeatEntry(object):
165 # class constants 166 daily='daily' 167 weekly='weekly' 168 monthly='monthly' 169 yearly='yearly' 170 _interval=0 171 _dow=1 172 _dom=0 173 _moy=1 174 _interval2=2 175 _dow_names=( 176 {1: 'Sun'}, {2: 'Mon'}, {4: 'Tue'}, {8: 'Wed'}, 177 {16: 'Thu'}, {32: 'Fri'}, {64: 'Sat'}) 178 # this faster than log2(x) 179 _dow_num={ 1: wx.DateTime.Sun, 180 2: wx.DateTime.Mon, 181 4: wx.DateTime.Tue, 182 8: wx.DateTime.Wed, 183 16: wx.DateTime.Thu, 184 32: wx.DateTime.Fri, 185 64: wx.DateTime.Sat } 186 dow_names={ 'Sun': 1, 'Mon': 2, 'Tue': 4, 'Wed': 8, 187 'Thu': 16, 'Fri': 32, 'Sat': 64 } 188 dow_weekday=0x3E 189 dow_weekend=0x41 190 dow_weekstart={ 191 'SU': 7, 'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6 } 192
193 - def __init__(self, repeat_type=daily):
194 self._type=repeat_type 195 self._data=[0,0,0] 196 self._suppressed=[] 197 self._wkstart=7 # default to Sun
198
199 - def __eq__(self, rhs):
200 # return T if equal 201 if not isinstance(rhs, RepeatEntry): 202 return False 203 if self.repeat_type!=rhs.repeat_type: 204 return False 205 if self.repeat_type==RepeatEntry.daily: 206 if self.interval!=rhs.interval: 207 return False 208 elif self.repeat_type==RepeatEntry.weekly: 209 if self.interval!=rhs.interval or \ 210 self.dow!=rhs.dow: 211 return False 212 elif self.repeat_type==RepeatEntry.monthly: 213 if self.interval!=rhs.interval or \ 214 self.interval2!=rhs.interval2 or \ 215 self.dow!=rhs.dow: 216 return False 217 return True
218 - def __ne__(self, rhs):
219 return not self.__eq__(rhs)
220
221 - def get(self):
222 # return a dict representing internal data 223 # mainly used for populatefs 224 r={} 225 if self._type==self.daily: 226 r[self.daily]= { 'interval': self._data[self._interval] } 227 elif self._type==self.weekly: 228 r[self.weekly]= { 'interval': self._data[self._interval], 229 'dow': self._data[self._dow] } 230 elif self._type==self.monthly: 231 r[self.monthly]={ 'interval': self._data[self._interval], 232 'interval2': self._data[self._interval2], 233 'dow': self._data[self._dow] } 234 else: 235 r[self.yearly]=None 236 s=[] 237 for n in self._suppressed: 238 s.append(n.get()) 239 r['suppressed']=s 240 return r
241
242 - def get_db_dict(self):
243 # return a copy of the dict compatible with the database stuff 244 db_r={} 245 r={} 246 r['type']=self._type 247 r['weekstart']=self.weekstart 248 if self._type==self.daily: 249 r['interval']=self._data[self._interval] 250 elif self._type==self.weekly or self._type==self.monthly: 251 r['interval']=self._data[self._interval] 252 r['dow']=self._data[self._dow] 253 if self._type==self.monthly: 254 r['interval2']=self._data[self._interval2] 255 # and the suppressed stuff 256 s=[] 257 for n in self._suppressed: 258 s.append({ 'date': n.iso_str(True) }) 259 db_r['repeat']=[r] 260 if len(s): 261 db_r['suppressed']=s 262 return db_r
263
264 - def set(self, data):
265 # setting data from a dict, mainly used for getfromfs 266 if data.has_key(self.daily): 267 # daily type 268 self.repeat_type=self.daily 269 self.interval=data[self.daily]['interval'] 270 elif data.has_key(self.weekly): 271 # weekly type 272 self.repeat_type=self.weekly 273 self.interval=data[self.weekly]['interval'] 274 self.dow=data[self.weekly]['dow'] 275 elif data.has_key(self.monthly): 276 self.repeat_type=self.monthly 277 self.dow=data[self.monthly].get('dow', 0) 278 self.interval=data[self.monthly].get('interval', 0) 279 self.interval2=data[self.monthly].get('interval2', 1) 280 else: 281 self.repeat_type=self.yearly 282 s=[] 283 for n in data.get('suppressed', []): 284 s.append(bptime.BPTime(n)) 285 self.suppressed=s
286
287 - def set_db_dict(self, data):
288 r=data.get('repeat', [{}])[0] 289 self.repeat_type=r['type'] 290 _dow=r.get('dow', 0) 291 _interval=r.get('interval', 0) 292 self.weekstart=r.get('weekstart', 'SU') 293 if self.repeat_type==self.daily: 294 self.interval=_interval 295 elif self.repeat_type==self.weekly or self.repeat_type==self.monthly: 296 self.interval=_interval 297 self.dow=_dow 298 if self.repeat_type==self.monthly: 299 self.interval2=r.get('interval2', 1) 300 # now the suppressed stuff 301 s=[] 302 for n in data.get('suppressed', []): 303 s.append(bptime.BPTime(n['date'])) 304 self.suppressed=s
305
306 - def get_nthweekday(self, date):
307 """Utility routine: return the nth weekday of the specified date""" 308 _wxmonth=date[1]-1 309 _year=date[0] 310 _day=date[2] 311 _dt=wx.DateTimeFromDMY(_day, _wxmonth, _year) 312 _dt.SetToWeekDay(_dt.GetWeekDay(), 1, _wxmonth, _year) 313 return (_day-_dt.GetDay())/7+1
314
315 - def _check_daily(self, s, d):
316 if self.interval: 317 # every nth day 318 return (int((d-s).days)%self.interval)==0 319 else: 320 # every weekday 321 return d.weekday()<5
322 - def _next_daily(self, ymd):
323 """Return the date (y,m,d) of the next occurrence of this event""" 324 _d0=datetime.date(*ymd) 325 if self.interval: 326 # every nth day: 327 _delta=self.interval 328 else: 329 # every weekday 330 if _d0.isoweekday()<5: 331 # next weekday 332 _delta=1 333 else: 334 # the following Monday 335 _delta=3 336 _d1=_d0+datetime.timedelta(days=_delta) 337 return (_d1.year, _d1.month, _d1.day)
338
339 - def _weekof(self, d):
340 # return the date of the start of the week into which that d falls. 341 _workweek=self.weekstart 342 _dow=d.isoweekday() 343 return d-datetime.timedelta((_dow-_workweek) if _dow>=_workweek \ 344 else (_dow+7-_workweek))
345
346 - def _check_weekly(self, s, d):
347 # check if at least one day-of-week is specified, if not default to the 348 # start date 349 if self.dow==0: 350 self.dow=1<<(s.isoweekday()%7) 351 # check to see if this is the nth week 352 day_of_week=d.isoweekday()%7 # Sun=0, ..., Sat=6 353 if ((self._weekof(d)-self._weekof(s)).days/7)%self.interval: 354 # wrong week 355 return False 356 # check for the right weekday 357 return ((1<<day_of_week)&self.dow) != 0
358 - def _next_weekly(self, ymd):
359 """Return the next occurrence of this event from ymd date""" 360 _oneday=datetime.timedelta(days=1) 361 _d0=datetime.date(*ymd)+_oneday 362 _dowbit=1<<(_d0.isoweekday()%7) 363 while _dowbit!=1: 364 if self.dow&_dowbit: 365 return (_d0.year, _d0.month, _d0.day) 366 _dowbit<<=1 367 if _dowbit==128: 368 _dowbit=1 369 _d0+=_oneday 370 _delta=(self.interval-1)*7 371 _d0+=datetime.timedelta(days=_delta) 372 while _dowbit!=128: 373 if self.dow&_dowbit: 374 return (_d0.year, _d0.month, _d0.day) 375 _dowbit<<=1 376 _d0+=_oneday
377
378 - def _check_monthly(self, s, d):
379 if not self.interval2: 380 # default to every month 381 self.interval2=1 382 if d.month>=s.month: 383 if (d.month-s.month)%self.interval2: 384 # wrong month 385 return False 386 elif (12+d.month-s.month)%self.interval2: 387 return False 388 if self.dow==0: 389 # no weekday specified, implied nth day of the month 390 return d.day==s.day 391 else: 392 # every interval-th dow-day (ie 1st Mon) of the month 393 _dow=(1<<(d.isoweekday()%7))&self.dow 394 if not _dow: 395 # not even the right day-of-week 396 return False 397 dt=wx.DateTime.Now() 398 if self.interval<5: 399 # nth *day of the month 400 _nth=self.interval 401 else: 402 # last *day of the month 403 _nth=-1 404 return dt.SetToWeekDay(self._dow_num[_dow], 405 _nth, month=d.month-1, year=d.year) and \ 406 dt.GetDay()==d.day
407 - def _next_monthly(self, ymd):
408 """Return the date of the next occurrence of this event""" 409 _day=ymd[2] 410 _month=ymd[1]+self.interval2 411 if _month%12: 412 _year=ymd[0]+_month/12 413 _month=_month%12 414 else: 415 _year=ymd[0]+_month/12-1 416 _month=12 417 _d1=datetime.date(_year, _month, _day) 418 if self.dow==0: 419 # nth day of the month 420 return (_d1.year, _d1.month, _d1.day) 421 else: 422 # every interval-th dow-day (ie 1st Mon) of the month 423 if self.interval<5: 424 # nth *day of the month 425 _nth=self.interval 426 else: 427 # last *day of the month 428 _nth=-1 429 _dt=wx.DateTime() 430 _dt.SetToWeekDay(self._dow_num[self.dow], _nth, month=_d1.month-1, 431 year=_d1.year) 432 return (_dt.GetYear(), _dt.GetMonth()+1, _dt.GetDay())
433
434 - def _check_yearly(self, s, d):
435 return d.month==s.month and d.day==s.day
436 - def _next_yearly(self, ymd):
437 """Return the date of the next occurrence of this event""" 438 return (ymd[0]+1, ymd[1], ymd[2])
439
440 - def is_active(self, s, d):
441 # check in the suppressed list 442 if bptime.BPTime(d) in self._suppressed: 443 # in the list, not part of this repeat 444 return False 445 # determine if the date is active 446 if self.repeat_type==self.daily: 447 return self._check_daily(s, d) 448 elif self.repeat_type==self.weekly: 449 return self._check_weekly(s, d) 450 elif self.repeat_type==self.monthly: 451 return self._check_monthly(s, d) 452 elif self.repeat_type==self.yearly: 453 return self._check_yearly(s, d) 454 else: 455 return False
456
457 - def next_date(self, ymd):
458 """Return the date of the next occurrence of this event""" 459 if self.repeat_type==self.daily: 460 return self._next_daily(ymd) 461 elif self.repeat_type==self.weekly: 462 return self._next_weekly(ymd) 463 elif self.repeat_type==self.monthly: 464 return self._next_monthly(ymd) 465 else: 466 return self._next_yearly(ymd)
467
468 - def _get_type(self):
469 return self._type
470 - def _set_type(self, repeat_type):
471 if repeat_type in (self.daily, self.weekly, 472 self.monthly, self.yearly): 473 self._type = repeat_type 474 else: 475 raise AttributeError, 'type'
476 repeat_type=property(fget=_get_type, fset=_set_type) 477
478 - def _get_interval(self):
479 if self._type==self.yearly: 480 raise AttributeError 481 return self._data[self._interval]
482 - def _set_interval(self, interval):
483 if self._type==self.yearly: 484 raise AttributeError 485 self._data[self._interval]=interval
486 interval=property(fget=_get_interval, fset=_set_interval) 487
488 - def _get_interval2(self):
489 if self._type==self.yearly: 490 raise AttributeError 491 return self._data[self._interval2]
492 - def _set_interval2(self, interval):
493 if self._type==self.yearly: 494 raise AttributeError 495 self._data[self._interval2]=interval
496 interval2=property(fget=_get_interval2, fset=_set_interval2) 497
498 - def _get_dow(self):
499 if self._type==self.yearly: 500 raise AttributeError 501 return self._data[self._dow]
502 - def _set_dow(self, dow):
503 if self._type==self.yearly: 504 raise AttributeError 505 if isinstance(dow, (int, long)): 506 self._data[self._dow]=int(dow) 507 elif isinstance(dow, (list, tuple)): 508 self._data[self._dow]=1<<(datetime.date(*dow[:3]).isoweekday()%7) 509 else: 510 raise TypeError,"Must be an int or a list/tuple"
511 dow=property(fget=_get_dow, fset=_set_dow)
512 - def _get_dow_str(self):
513 try: 514 _dow=self.dow 515 except AttributeError: 516 return '' 517 names=[] 518 for l in self._dow_names: 519 for k,e in l.items(): 520 if k&_dow: 521 names.append(e) 522 return ';'.join(names)
523 dow_str=property(fget=_get_dow_str) 524
525 - def _get_wkstart(self):
526 return self._wkstart
527 - def _set_wkstart(self, wkstart):
528 if isinstance(wkstart, (int, long)): 529 if wkstart in range(1, 8): 530 self._wkstart=int(wkstart) 531 else: 532 raise ValueError('Must be between 1-7') 533 elif isinstance(wkstart, (str, unicode)): 534 self._wkstart=self.dow_weekstart.get(str(wkstart.upper()), 7) 535 else: 536 raise TypeError("Must be either a string or int")
537 weekstart=property(fget=_get_wkstart, fset=_set_wkstart) 538
539 - def _get_suppressed(self):
540 return self._suppressed
541 - def _set_suppressed(self, d):
542 if not isinstance(d, list): 543 raise TypeError, 'must be a list of string or BPTime' 544 if not len(d) or isinstance(d[0], bptime.BPTime): 545 # empty list or already a list of BPTime 546 self._suppressed=d 547 elif isinstance(d[0], str): 548 # list of 'yyyy-mm-dd' 549 self._suppressed=[] 550 for n in d: 551 self._suppressed.append(bptime.BPTime(n.replace('-', '')))
552 - def add_suppressed(self, y, m, d):
553 self._suppressed.append(bptime.BPTime((y, m, d)))
554 - def get_suppressed_list(self):
555 return [x.date_str() for x in self._suppressed]
556 suppressed=property(fget=_get_suppressed, fset=_set_suppressed)
557 - def _get_suppressed_str(self):
558 return ';'.join(self.get_suppressed_list())
559 suppressed_str=property(fget=_get_suppressed_str)
560
561 #------------------------------------------------------------------------------- 562 -class CalendarEntry(object):
563 # priority const 564 priority_high=1 565 priority_normal=5 566 priority_low=10 567 # no end date 568 no_end_date=(4000, 1, 1) 569 # required and optional attributes, mainly used for comparison 570 _required_attrs=('description', 'start','end') 571 _required_attr_names=('Description', 'Start', 'End') 572 _optional_attrs=('location', 'priority', 'alarm', 'allday', 'vibrate', 573 'voice', 'repeat', 'notes', 'categories', 574 'ringtone', 'wallpaper') 575 _optional_attr_names=('Location', 'Priority', 'Alarm', 'All-Day', 576 'Vibrate', '', 'Repeat', 'Notes', 'Categories', 577 'Ringtone', 'Wallpaper')
578 - def __init__(self, year=None, month=None, day=None):
579 self._data={} 580 # setting default values 581 if day is not None: 582 self._data['start']=bptime.BPTime((year, month, day)) 583 self._data['end']=bptime.BPTime((year, month, day)) 584 else: 585 self._data['start']=bptime.BPTime() 586 self._data['end']=bptime.BPTime() 587 self._data['serials']=[] 588 self._create_id()
589
590 - def matches(self, rhs):
591 # Match self against this entry, which may not have all the 592 # optional attributes 593 if not isinstance(rhs, CalendarEntry): 594 return False 595 for _attr in CalendarEntry._required_attrs: 596 if getattr(self, _attr) != getattr(rhs, _attr): 597 return False 598 for _attr in CalendarEntry._optional_attrs: 599 _rhs_attr=getattr(rhs, _attr) 600 if _rhs_attr is not None and getattr(self, _attr)!=_rhs_attr: 601 return False 602 return True
603 - def get_changed_fields(self, rhs):
604 # Return a CSV string of all the fields having different values 605 if not isinstance(rhs, CalendarEntry): 606 return '' 607 _res=[] 608 for _idx,_attr in enumerate(CalendarEntry._required_attrs): 609 if getattr(self, _attr) != getattr(rhs, _attr): 610 _res.append(CalendarEntry._required_attr_names[_idx]) 611 for _idx,_attr in enumerate(CalendarEntry._optional_attrs): 612 _rhs_attr=getattr(rhs, _attr) 613 if _rhs_attr is not None and getattr(self, _attr)!=_rhs_attr: 614 _res.append(CalendarEntry._optional_attr_names[_idx]) 615 return ','.join(_res)
616
617 - def similar(self, rhs):
618 # return T if rhs is similar to self 619 # for now, they're similar if they have the same start time 620 return self.start==rhs.start
621
622 - def replace(self, rhs):
623 # replace the contents of this entry with the new one 624 for _attr in CalendarEntry._required_attrs+\ 625 CalendarEntry._optional_attrs: 626 _rhs_attr=getattr(rhs, _attr) 627 if _rhs_attr is not None: 628 setattr(self, _attr, _rhs_attr)
629
630 - def __eq__(self, rhs):
631 if not isinstance(rhs, CalendarEntry): 632 return False 633 for _attr in CalendarEntry._required_attrs+CalendarEntry._optional_attrs: 634 if getattr(self, _attr)!=getattr(rhs, _attr): 635 return False 636 return True
637 - def __ne__(self, rhs):
638 return not self.__eq__(rhs)
639
640 - def get(self):
641 r=copy.deepcopy(self._data, _nil={}) 642 if self.repeat is not None: 643 r['repeat']=self.repeat.get() 644 r['start']=self._data['start'].iso_str() 645 r['end']=self._data['end'].iso_str() 646 return r
647
648 - def get_db_dict(self):
649 # return a dict compatible with the database stuff 650 r=copy.deepcopy(self._data, _nil={}) 651 # adjust for start & end 652 r['start']=self._data['start'].iso_str(self.allday) 653 r['end']=self._data['end'].iso_str(self.allday) 654 # adjust for repeat & suppressed 655 if self.repeat is not None: 656 r.update(self.repeat.get_db_dict()) 657 # take out uneeded keys 658 if r.has_key('allday'): 659 del r['allday'] 660 return r
661
662 - def set(self, data):
663 self._data={} 664 self._data.update(data) 665 self._data['start']=bptime.BPTime(data['start']) 666 self._data['end']=bptime.BPTime(data['end']) 667 if self.repeat is not None: 668 r=RepeatEntry() 669 r.set(self.repeat) 670 self.repeat=r 671 # try to clean up the dict 672 for k, e in self._data.items(): 673 if e is None or e=='' or e==[]: 674 del self._data[k]
675
676 - def set_db_dict(self, data):
677 # update our data with dict return from database 678 self._data={} 679 self._data.update(data) 680 # adjust for allday 681 self.allday=len(data['start'])==8 682 # adjust for start and end 683 self._data['start']=bptime.BPTime(data['start']) 684 self._data['end']=bptime.BPTime(data['end']) 685 # adjust for repeat 686 if data.has_key('repeat'): 687 rp=RepeatEntry() 688 rp.set_db_dict(data) 689 self.repeat=rp
690
691 - def is_active(self, y, m ,d):
692 # return true if if this event is active on this date, 693 # mainly used for repeating events. 694 s=self._data['start'].date 695 e=self._data['end'].date 696 d=datetime.date(y, m, d) 697 if d<s or d>e: 698 # before start date, after end date 699 return False 700 if self.repeat is None: 701 # not a repeat event, within range so it's good 702 return True 703 # repeat event: check if it's in range. 704 return self.repeat.is_active(s, d)
705
706 - def suppress_repeat_entry(self, y, m, d):
707 if self.repeat is None: 708 # not a repeat entry, do nothing 709 return 710 self.repeat.add_suppressed(y, m, d)
711
712 - def _set_or_del(self, key, v, v_list=()):
713 if v is None or v in v_list: 714 if self._data.has_key(key): 715 del self._data[key] 716 else: 717 self._data[key]=v
718
719 - def _get_description(self):
720 return self._data.get('description', '')
721 - def _set_description(self, desc):
722 self._set_or_del('description', desc, ('',))
723 description=property(fget=_get_description, fset=_set_description) 724
725 - def _get_location(self):
726 return self._data.get('location', '')
727 - def _set_location(self, location):
728 self._set_or_del('location', location, ('',))
729 location=property(fget=_get_location, fset=_set_location) 730
731 - def _get_desc_loc(self):
732 # return 'description[location]' 733 if self.location: 734 return self.description+'['+self.location+']' 735 return self.description
736 - def _set_desc_loc(self, v):
737 # parse and set for 'description[location]' 738 _idx1=v.find('[') 739 _idx2=v.find(']') 740 if _idx1!=-1 and _idx2!=-1 and _idx2>_idx1: 741 # location specified 742 self.location=v[_idx1+1:_idx2] 743 self.description=v[:_idx1] 744 else: 745 self.description=v
746 desc_loc=property(fget=_get_desc_loc, fset=_set_desc_loc) 747
748 - def _get_priority(self):
749 return self._data.get('priority', None)
750 - def _set_priority(self, priority):
751 self._set_or_del('priority', priority)
752 priority=property(fget=_get_priority, fset=_set_priority) 753
754 - def _get_alarm(self):
755 return self._data.get('alarm', -1)
756 - def _set_alarm(self, alarm):
757 self._set_or_del('alarm', alarm)
758 alarm=property(fget=_get_alarm, fset=_set_alarm) 759
760 - def _get_allday(self):
761 return self._data.get('allday', False)
762 - def _set_allday(self, allday):
763 self._data['allday']=allday
764 allday=property(fget=_get_allday, fset=_set_allday) 765
766 - def _get_start(self):
767 return self._data['start'].get()
768 - def _set_start(self, datetime):
769 self._data['start'].set(datetime)
770 start=property(fget=_get_start, fset=_set_start)
771 - def _get_start_str(self):
772 return self._data['start'].date_str()+' '+\ 773 self._data['start'].time_str(False, '00:00')
774 start_str=property(fget=_get_start_str) 775
776 - def _get_end(self):
777 return self._data['end'].get()
778 - def _set_end(self, datetime):
779 self._data['end'].set(datetime)
780 end=property(fget=_get_end, fset=_set_end)
781 - def _get_end_str(self):
782 return self._data['end'].date_str()+' '+\ 783 self._data['end'].time_str(False, '00:00')
784 end_str=property(fget=_get_end_str)
785 - def open_ended(self):
786 # True if this is an open-ended event 787 return self.end[:3]==self.no_end_date
788
789 - def _get_vibrate(self):
790 return self._data.get('vibrate', 0)
791 - def _set_vibrate(self, v):
792 self._set_or_del('vibrate', v, (None, 0, False))
793 vibrate=property(fget=_get_vibrate, fset=_set_vibrate) 794
795 - def _get_voice(self):
796 return self._data.get('voice', None)
797 - def _set_voice(self, v):
798 self._set_or_del('voice', v, (None,))
799 voice=property(fget=_get_voice, fset=_set_voice) 800
801 - def _get_serials(self):
802 return self._data.get('serials', None)
803 - def _set_serials(self, serials):
804 self._data['serials']=serials
805 serials=property(fget=_get_serials, fset=_set_serials) 806
807 - def _get_repeat(self):
808 return self._data.get('repeat', None)
809 - def _set_repeat(self, repeat):
810 self._set_or_del('repeat', repeat)
811 repeat=property(fget=_get_repeat, fset=_set_repeat) 812
813 - def _get_id(self):
814 s=self._data.get('serials', []) 815 for n in s: 816 if n.get('sourcetype', None)=='bitpim': 817 return n.get('id', None) 818 return None
819 - def _set_id(self, id):
820 s=self._data.get('serials', []) 821 for n in s: 822 if n.get('sourcetype', None)=='bitpim': 823 n['id']=id 824 return 825 self._data['serials'].append({'sourcetype': 'bitpim', 'id': id } )
826 id=property(fget=_get_id, fset=_set_id) 827
828 - def _get_notes(self):
829 return self._data.get('notes', '')
830 - def _set_notes(self, s):
831 self._set_or_del('notes', s, ('',))
832 notes=property(fget=_get_notes, fset=_set_notes) 833
834 - def _get_categories(self):
835 return self._data.get('categories', [])
836 - def _set_categories(self, s):
837 self._set_or_del('categories', s,([],)) 838 if s==[] and self._data.has_key('categories'): 839 del self._data['categories']
840 categories=property(fget=_get_categories, fset=_set_categories)
841 - def _get_categories_str(self):
842 c=self.categories 843 if len(c): 844 return ';'.join([x['category'] for x in c]) 845 else: 846 return ''
847 categories_str=property(fget=_get_categories_str) 848
849 - def _get_ringtone(self):
850 return self._data.get('ringtone', '')
851 - def _set_ringtone(self, rt):
852 self._set_or_del('ringtone', rt, ('',))
853 ringtone=property(fget=_get_ringtone, fset=_set_ringtone) 854
855 - def _get_wallpaper(self):
856 return self._data.get('wallpaper', '',)
857 - def _set_wallpaper(self, wp):
858 self._set_or_del('wallpaper', wp, ('',))
859 wallpaper=property(fget=_get_wallpaper, fset=_set_wallpaper) 860 861 # we use two random numbers to generate the serials. _persistrandom 862 # is seeded at startup 863 _persistrandom=random.Random()
864 - def _create_id(self):
865 "Create a BitPim serial for this entry" 866 rand2=random.Random() # this random is seeded when this function is called 867 num=sha.new() 868 num.update(`self._persistrandom.random()`) 869 num.update(`rand2.random()`) 870 self._data["serials"].append({"sourcetype": "bitpim", "id": num.hexdigest()})
871
872 - def _get_print_data(self):
873 """ return a list of strings used for printing this event: 874 [0]: start time, [1]: '', [2]: end time, [3]: Description 875 [4]: Repeat Type, [5]: Alarm 876 """ 877 if self.allday: 878 t0='All Day' 879 t1='' 880 else: 881 t0=self._data['start'].time_str() 882 t1=self._data['end'].time_str() 883 rp=self.repeat 884 if rp is None: 885 rp_str='' 886 else: 887 rp_str=rp.repeat_type[0].upper() 888 if self.alarm==-1: 889 alarm_str='' 890 else: 891 alarm_str='%d:%02d'%(self.alarm/60, self.alarm%60) 892 return [t0, '', t1, self.description, rp_str, alarm_str]
893 print_data=property(fget=_get_print_data) 894 @classmethod
895 - def cmp_by_time(cls, a, b):
896 """ compare 2 objects by start times. 897 -1 if a<b, 0 if a==b, and 1 if a>b 898 allday is always less than having start times. 899 Mainly used for sorting list of events 900 """ 901 if not isinstance(a, cls) or \ 902 not isinstance(b, cls): 903 raise TypeError, 'must be a CalendarEntry object' 904 if a.allday and b.allday: 905 return 0 906 if a.allday and not b.allday: 907 return -1 908 if not a.allday and b.allday: 909 return 1 910 t0=a.start[3:] 911 t1=b.start[3:] 912 if t0<t1: 913 return -1 914 if t0==t1: 915 return 0 916 if t0>t1: 917 return 1
918
919 - def _summary(self):
920 # provide a one-liner summary string for this event 921 if self.allday: 922 str=self.description 923 else: 924 hr=self.start[3] 925 ap="am" 926 if hr>=12: 927 ap="pm" 928 hr-=12 929 if hr==0: hr=12 930 str="%2d:%02d %s" % (hr, self.start[4], ap) 931 str+=" "+self.description 932 return str
933 summary=property(fget=_summary)
934
935 936 #------------------------------------------------------------------------------- 937 -class Calendar(calendarcontrol.Calendar):
938 """A class encapsulating the GUI and data of the calendar (all days). A seperate dialog is 939 used to edit the content of one particular day.""" 940 941 CURRENTFILEVERSION=3 942
943 - def __init__(self, mainwindow, parent, id=-1):
944 """constructor 945 946 @type mainwindow: gui.MainWindow 947 @param mainwindow: Used to get configuration data (such as directory to save/load data. 948 @param parent: Widget acting as parent for this one 949 @param id: id 950 """ 951 self.mainwindow=mainwindow 952 self.entrycache={} 953 self.entries={} 954 self.repeating=[] # nb this is stored unsorted 955 self._data={} # the underlying data 956 calendarcontrol.Calendar.__init__(self, parent, rows=5, id=id) 957 self.dialog=calendarentryeditor.Editor(self) 958 pubsub.subscribe(self.OnMediaNameChanged, pubsub.MEDIA_NAME_CHANGED) 959 today.bind_notification_event(self.OnTodayItem, 960 today.Today_Group_Calendar) 961 today.bind_request_event(self.OnTodayRequest) 962 pubsub.subscribe(self.OnTodayButton, pubsub.MIDNIGHT)
963
964 - def OnPrintDialog(self, mainwindow, config):
965 with guihelper.WXDialogWrapper(CalendarPrintDialog(self, mainwindow, config), 966 True): 967 pass
968 - def CanPrint(self):
969 return True
970
971 - def OnMediaNameChanged(self, msg):
972 d=msg.data 973 _type=d.get(pubsub.media_change_type, None) 974 _old_name=d.get(pubsub.media_old_name, None) 975 _new_name=d.get(pubsub.media_new_name, None) 976 if _type is None or _old_name is None or _new_name is None: 977 # invalid/incomplete data 978 return 979 if _type!=pubsub.wallpaper_type and \ 980 _type!=pubsub.ringtone_type: 981 # neither wallpaper nor ringtone 982 return 983 _old_name=common.basename(_old_name) 984 _new_name=common.basename(_new_name) 985 if _type==pubsub.wallpaper_type: 986 attr_name='wallpaper' 987 else: 988 attr_name='ringtone' 989 modified=False 990 for k,e in self._data.items(): 991 if getattr(e, attr_name, None)==_old_name: 992 setattr(e, attr_name, _new_name) 993 modified=True 994 if modified: 995 # changes were made, update everything 996 self.updateonchange()
997
998 - def getdata(self, dict):
999 """Return underlying calendar data in bitpim format 1000 1001 @return: The modified dict updated with at least C{dict['calendar']}""" 1002 if dict.get('calendar_version', None)==2: 1003 # return a version 2 dict 1004 dict['calendar']=self._convert3to2(self._data, 1005 dict.get('ringtone-index', None)) 1006 else: 1007 dict['calendar']=copy.deepcopy(self._data, _nil={}) 1008 return dict
1009
1010 - def updateonchange(self):
1011 """Called when our data has changed 1012 1013 The disk, widget and display are all updated with the new data""" 1014 d={} 1015 d=self.getdata(d) 1016 self.populatefs(d) 1017 self.populate(d) 1018 # Brute force - assume all entries have changed 1019 self.RefreshAllEntries()
1020
1021 - def AddEntry(self, entry):
1022 """Adds and entry into the calendar data. 1023 1024 The entries on disk are updated by this function. 1025 1026 @type entry: a dict containing all the fields. 1027 @param entry: an entry. It must contain a C{pos} field. You 1028 should call L{newentryfactory} to make 1029 an entry that you then modify 1030 """ 1031 self._data[entry.id]=entry 1032 self.updateonchange()
1033
1034 - def DeleteEntry(self, entry):
1035 """Deletes an entry from the calendar data. 1036 1037 The entries on disk are updated by this function. 1038 1039 @type entry: a dict containing all the fields. 1040 @param entry: an entry. It must contain a C{pos} field 1041 corresponding to an existing entry 1042 """ 1043 del self._data[entry.id] 1044 self.updateonchange()
1045
1046 - def DeleteEntryRepeat(self, entry, year, month, day):
1047 """Deletes a specific repeat of an entry 1048 See L{DeleteEntry}""" 1049 self._data[entry.id].suppress_repeat_entry(year, month, day) 1050 self.updateonchange()
1051
1052 - def ChangeEntry(self, oldentry, newentry):
1053 """Changes an entry in the calendar data. 1054 1055 The entries on disk are updated by this function. 1056 """ 1057 assert oldentry.id==newentry.id 1058 self._data[newentry.id]=newentry 1059 self.updateonchange()
1060
1061 - def getentrydata(self, year, month, day):
1062 """return the entry objects for corresponding date 1063 1064 @rtype: list""" 1065 # return data from cache if we have it 1066 res=self.entrycache.get( (year,month,day), None) 1067 if res is not None: 1068 return res 1069 # find non-repeating entries 1070 res=self.entries.get((year,month,day), []) 1071 for i in self.repeating: 1072 if i.is_active(year, month, day): 1073 res.append(i) 1074 self.entrycache[(year,month,day)] = res 1075 return res
1076
1077 - def newentryfactory(self, year, month, day):
1078 """Returns a new 'blank' entry with default fields 1079 1080 @rtype: CalendarEntry 1081 """ 1082 # create a new entry 1083 res=CalendarEntry(year, month, day) 1084 # fill in default start & end data 1085 now=time.localtime() 1086 event_start=(year, month, day, now.tm_hour, now.tm_min) 1087 event_end=[year, month, day, now.tm_hour, now.tm_min] 1088 # we make end be the next hour, unless it has gone 11pm 1089 # in which case it is 11:59pm 1090 if event_end[3]<23: 1091 event_end[3]+=1 1092 event_end[4]=0 1093 else: 1094 event_end[3]=23 1095 event_end[4]=59 1096 res.start=event_start 1097 res.end=event_end 1098 res.description='New Event' 1099 return res
1100
1101 - def getdaybitmap(self, start, repeat):
1102 if repeat!="weekly": 1103 return 0 1104 dayofweek=calendar.weekday(*(start[:3])) 1105 dayofweek=(dayofweek+1)%7 # normalize to sunday == 0 1106 return [2048,1024,512,256,128,64,32][dayofweek]
1107
1108 - def OnGetEntries(self, year, month, day):
1109 """return pretty printed sorted entries for date 1110 as required by the parent L{calendarcontrol.Calendar} for 1111 display in a cell""" 1112 entry_list=self.getentrydata(year, month, day) 1113 res=[] 1114 for _entry in entry_list: 1115 (_y,_m,_d,_h,_min, _desc)=_entry.start+(_entry.description,) 1116 if _entry.allday: 1117 res.append((None, None, _desc)) 1118 elif _entry.repeat or (_y,_m,_d)==(year, month, day): 1119 res.append((_h, _min, _desc)) 1120 else: 1121 res.append((None, None, '...'+_desc)) 1122 res.sort() 1123 return res
1124
1125 - def OnEdit(self, year, month, day, entry=None):
1126 """Called when the user wants to edit entries for a particular day""" 1127 if self.dialog.dirty: 1128 # user is editing a field so we don't allow edit 1129 wx.Bell() 1130 else: 1131 self.dialog.setdate(year, month, day, entry) 1132 self.dialog.Show(True)
1133
1134 - def OnTodayItem(self, evt):
1135 self.ActivateSelf() 1136 if evt.data: 1137 args=evt.data['datetime']+(evt.data['entry'],) 1138 self.OnEdit(*args)
1139
1140 - def OnTodayButton(self, evt):
1141 """ Called when the user goes to today cell""" 1142 super(Calendar, self).OnTodayButton(evt) 1143 if self.dialog.IsShown(): 1144 # editor dialog is up, update it 1145 self.OnEdit(*self.selecteddate)
1146
1147 - def _publish_today_events(self):
1148 now=datetime.datetime.now() 1149 l=self.getentrydata(now.year, now.month, now.day) 1150 l.sort(CalendarEntry.cmp_by_time) 1151 today_event=today.TodayCalendarEvent() 1152 for e in l: 1153 today_event.append(e.summary, { 'datetime': (now.year, now.month, now.day), 1154 'entry': e }) 1155 today_event.broadcast()
1156
1157 - def _publish_thisweek_events(self):
1158 now=datetime.datetime.now() 1159 one_day=datetime.timedelta(1) 1160 d1=now 1161 _days=6-(now.isoweekday()%7) 1162 res=[] 1163 today_event=today.ThisWeekCalendarEvent() 1164 for i in range(_days): 1165 d1+=one_day 1166 l=self.getentrydata(d1.year, d1.month, d1.day) 1167 if l: 1168 _dow=today.dow_initials[d1.isoweekday()%7] 1169 l.sort(CalendarEntry.cmp_by_time) 1170 for i,x in enumerate(l): 1171 if i: 1172 _name=today.dow_initials[-1]+' ' 1173 else: 1174 _name=_dow+' - ' 1175 _name+=x.summary 1176 today_event.append(_name, { 'datetime': (d1.year, d1.month, d1.day), 1177 'entry': x }) 1178 today_event.broadcast()
1179
1180 - def OnTodayRequest(self, _):
1183
1184 - def _add_entries(self, entry):
1185 # Add this entry, which may span several days, to the entries list 1186 _t0=datetime.datetime(*entry.start[:3]) 1187 _t1=datetime.datetime(*entry.end[:3]) 1188 _oneday=datetime.timedelta(days=1) 1189 for _ in range((_t1-_t0).days+1): 1190 self.entries.setdefault((_t0.year, _t0.month, _t0.day), []).append(entry) 1191 _t0+=_oneday
1192
1193 - def populate(self, dict):
1194 """Updates the internal data with the contents of C{dict['calendar']}""" 1195 if dict.get('calendar_version', None)==2: 1196 # Cal dict version 2, need to convert to current ver(3) 1197 self._data=self._convert2to3(dict.get('calendar', {}), 1198 dict.get('ringtone-index', {})) 1199 else: 1200 self._data=dict.get('calendar', {}) 1201 self.entrycache={} 1202 self.entries={} 1203 self.repeating=[] 1204 1205 for entry in self._data: 1206 entry=self._data[entry] 1207 y,m,d,h,min=entry.start 1208 if entry.repeat is None: 1209 self._add_entries(entry) 1210 else: 1211 self.repeating.append(entry) 1212 # tell everyone that i've changed 1213 1214 self._publish_today_events() 1215 self._publish_thisweek_events() 1216 self.RefreshAllEntries()
1217
1218 - def populatefs(self, dict):
1219 """Saves the dict to disk""" 1220 1221 if dict.get('calendar_version', None)==2: 1222 # Cal dict version 2, need to convert to current ver(3) 1223 cal_dict=self._convert2to3(dict.get('calendar', {}), 1224 dict.get('ringtone-index', {})) 1225 else: 1226 cal_dict=dict.get('calendar', {}) 1227 1228 db_rr={} 1229 for k, e in cal_dict.items(): 1230 db_rr[k]=CalendarDataObject(e) 1231 database.ensurerecordtype(db_rr, calendarobjectfactory) 1232 db_rr=database.extractbitpimserials(db_rr) 1233 self.mainwindow.database.savemajordict('calendar', db_rr) 1234 return dict
1235
1236 - def getfromfs(self, dict):
1237 """Updates dict with info from disk 1238 1239 @Note: The dictionary passed in is modified, as well 1240 as returned 1241 @rtype: dict 1242 @param dict: the dictionary to update 1243 @return: the updated dictionary""" 1244 self.thedir=self.mainwindow.calendarpath 1245 if os.path.exists(os.path.join(self.thedir, "index.idx")): 1246 # old index file exists: read, convert, and discard file 1247 dct={'result': {}} 1248 common.readversionedindexfile(os.path.join(self.thedir, "index.idx"), 1249 dct, self.versionupgrade, 1250 self.CURRENTFILEVERSION) 1251 converted=dct['result'].has_key('converted') 1252 db_r={} 1253 for k,e in dct['result'].get('calendar', {}).items(): 1254 if converted: 1255 db_r[k]=CalendarDataObject(e) 1256 else: 1257 ce=CalendarEntry() 1258 ce.set(e) 1259 db_r[k]=CalendarDataObject(ce) 1260 # save it in the new database 1261 database.ensurerecordtype(db_r, calendarobjectfactory) 1262 db_r=database.extractbitpimserials(db_r) 1263 self.mainwindow.database.savemajordict('calendar', db_r) 1264 # now that save is succesful, move file out of the way 1265 os.rename(os.path.join(self.thedir, "index.idx"), os.path.join(self.thedir, "index-is-now-in-database.bak")) 1266 # read data from the database 1267 cal_dict=self.mainwindow.database.getmajordictvalues('calendar', 1268 calendarobjectfactory) 1269 #if __debug__: 1270 # print 'Calendar.getfromfs: dicts returned from Database:' 1271 r={} 1272 for k,e in cal_dict.items(): 1273 #if __debug__: 1274 # print e 1275 ce=CalendarEntry() 1276 ce.set_db_dict(e) 1277 r[ce.id]=ce 1278 dict.update({ 'calendar': r }) 1279 1280 return dict
1281
1282 - def mergedata(self, result):
1283 """ Merge the newdata (from the phone) into current data 1284 """ 1285 with guihelper.WXDialogWrapper(MergeDialog(self, self._data, result.get('calendar', {})), 1286 True) as (dlg, retcode): 1287 if retcode==wx.ID_OK: 1288 self._data=dlg.get() 1289 self.updateonchange()
1290
1291 - def versionupgrade(self, dict, version):
1292 """Upgrade old data format read from disk 1293 1294 @param dict: The dict that was read in 1295 @param version: version number of the data on disk 1296 """ 1297 1298 # version 0 to 1 upgrade 1299 if version==0: 1300 version=1 # they are the same 1301 1302 # 1 to 2 1303 if version==1: 1304 # ?d field renamed daybitmap 1305 version=2 1306 for k in dict['result']['calendar']: 1307 entry=dict['result']['calendar'][k] 1308 entry['daybitmap']=self.getdaybitmap(entry['start'], entry['repeat']) 1309 del entry['?d'] 1310 1311 # 2 to 3 etc 1312 if version==2: 1313 version=3 1314 dict['result']['calendar']=self.convert_dict(dict['result'].get('calendar', {}), 2, 3) 1315 dict['result']['converted']=True # already converted
1316 1317 # 3 to 4 etc 1318
1319 - def convert_dict(self, dict, from_version, to_version, ringtone_index={}):
1320 """ 1321 Convert the calendatr dict from one version to another. 1322 Currently only support conversion between version 2 and 3. 1323 """ 1324 if dict is None: 1325 return None 1326 if from_version==2 and to_version==3: 1327 return self._convert2to3(dict, ringtone_index) 1328 elif from_version==3 and to_version==2: 1329 return self._convert3to2(dict, ringtone_index) 1330 else: 1331 raise 'Invalid conversion'
1332
1333 - def _convert2to3(self, dict, ringtone_index):
1334 """ 1335 Convert calendar dict from version 2 to 3. 1336 """ 1337 r={} 1338 for k,e in dict.items(): 1339 ce=CalendarEntry() 1340 ce.start=e['start'] 1341 ce.end=e['end'] 1342 ce.description=e['description'] 1343 ce.alarm=e['alarm'] 1344 ce.ringtone=ringtone_index.get(e['ringtone'], {}).get('name', '') 1345 repeat=e['repeat'] 1346 if repeat is None: 1347 ce.repeat=None 1348 else: 1349 repeat_entry=RepeatEntry() 1350 if repeat=='daily': 1351 repeat_entry.repeat_type=repeat_entry.daily 1352 repeat_entry.interval=1 1353 elif repeat=='monfri': 1354 repeat_entry.repeat_type=repeat_entry.daily 1355 repeat_entry.interval=0 1356 elif repeat=='weekly': 1357 repeat_entry.repeat_type=repeat_entry.weekly 1358 repeat_entry.interval=1 1359 dow=datetime.date(*e['start'][:3]).isoweekday()%7 1360 repeat_entry.dow=1<<dow 1361 elif repeat=='monthly': 1362 repeat_entry.repeat_type=repeat_entry.monthly 1363 else: 1364 repeat_entry.repeat_type=repeat_entry.yearly 1365 s=[] 1366 for n in e.get('exceptions',[]): 1367 s.append(bptime.BPTime(n)) 1368 repeat_entry.suppressed=s 1369 ce.repeat=repeat_entry 1370 r[ce.id]=ce 1371 return r
1372
1373 - def _convert_daily_events(self, e, d):
1374 """ Conver a daily event from v3 to v2 """ 1375 rp=e.repeat 1376 if rp.interval==1: 1377 # repeat everyday 1378 d['repeat']='daily' 1379 elif rp.interval==0: 1380 # repeat every weekday 1381 d['repeat']='monfri' 1382 else: 1383 # the interval is every nth day, with n>1 1384 # generate exceptions for those dates that are N/A 1385 d['repeat']='daily' 1386 t0=datetime.date(*e.start[:3]) 1387 t1=datetime.date(*e.end[:3]) 1388 delta_t=datetime.timedelta(1) 1389 while t0<=t1: 1390 if not e.is_active(t0.year, t0.month, t0.day): 1391 d['exceptions'].append((t0.year, t0.month, t0.day)) 1392 t0+=delta_t
1393
1394 - def _convert_weekly_events(self, e, d, idx):
1395 """ 1396 Convert a weekly event from v3 to v2 1397 """ 1398 rp=e.repeat 1399 dow=rp.dow 1400 t0=datetime.date(*e.start[:3]) 1401 t1=t3=datetime.date(*e.end[:3]) 1402 delta_t=datetime.timedelta(1) 1403 delta_t7=datetime.timedelta(7) 1404 if (t1-t0).days>6: 1405 # end time is more than a week away 1406 t1=t0+datetime.timedelta(6) 1407 d['repeat']='weekly' 1408 res={} 1409 while t0<=t1: 1410 dow_0=t0.isoweekday()%7 1411 if (1<<dow_0)&dow: 1412 # we have a hit, generate a weekly repeat event here 1413 dd=copy.deepcopy(d) 1414 dd['start']=(t0.year, t0.month, t0.day, e.start[3], e.start[4]) 1415 dd['daybitmap']=self.getdaybitmap(dd['start'], dd['repeat']) 1416 # generate exceptions for every nth week case 1417 t2=t0 1418 while t2<=t3: 1419 if not e.is_active(t2.year, t2.month, t2.day): 1420 dd['exceptions'].append((t2.year, t2.month, t2.day)) 1421 t2+=delta_t7 1422 # done, add it to the dict 1423 dd['pos']=idx 1424 res[idx]=dd 1425 idx+=1 1426 t0+=delta_t 1427 return idx, res
1428
1429 - def _convert3to2(self, dict, ringtone_index):
1430 """Convert calendar dict from version 3 to 2.""" 1431 r={} 1432 idx=0 1433 for k,e in dict.items(): 1434 d={} 1435 d['start']=e.start 1436 d['end']=e.end 1437 d['description']=e.description 1438 d['alarm']=e.alarm 1439 d['changeserial']=1 1440 d['snoozedelay']=0 1441 d['ringtone']=0 # by default 1442 try: 1443 d['ringtone']=[i for i,r in ringtone_index.items() \ 1444 if r.get('name', '')==e.ringtone][0] 1445 except: 1446 pass 1447 rp=e.repeat 1448 if rp is None: 1449 d['repeat']=None 1450 d['exceptions']=[] 1451 d['daybitmap']=0 1452 else: 1453 s=[] 1454 for n in rp.suppressed: 1455 s.append(n.get()[:3]) 1456 d['exceptions']=s 1457 if rp.repeat_type==rp.daily: 1458 self._convert_daily_events(e, d) 1459 elif rp.repeat_type==rp.weekly: 1460 idx, rr=self._convert_weekly_events(e, d, idx) 1461 r.update(rr) 1462 continue 1463 elif rp.repeat_type==rp.monthly: 1464 d['repeat']='monthly' 1465 elif rp.repeat_type==rp.yearly: 1466 d['repeat']='yearly' 1467 d['daybitmap']=self.getdaybitmap(d['start'], d['repeat']) 1468 d['pos']=idx 1469 r[idx]=d 1470 idx+=1 1471 if __debug__: 1472 print 'Calendar._convert3to2: V2 dict:' 1473 print r 1474 return r
1475
1476 #------------------------------------------------------------------------------- 1477 -class CalendarPrintDialog(guiwidgets.PrintDialog):
1478 1479 _regular_template='cal_regular.xy' 1480 _regular_style='cal_regular_style.xy' 1481 _monthly_template='cal_monthly.xy' 1482 _monthly_style='cal_monthly_style.xy' 1483
1484 - def __init__(self, calwidget, mainwindow, config):
1485 super(CalendarPrintDialog, self).__init__(calwidget, mainwindow, 1486 config, 'Print Calendar') 1487 self._dt_index=self._dt_start=self._dt_end=None 1488 self._date_changed=self._style_changed=False
1489
1490 - def _create_contents(self, vbs):
1491 hbs=wx.BoxSizer(wx.HORIZONTAL) 1492 # the print range box 1493 sbs=wx.StaticBoxSizer(wx.StaticBox(self, -1, 'Print Range'), 1494 wx.VERTICAL) 1495 gs=wx.FlexGridSizer(-1, 2, 5, 5) 1496 gs.AddGrowableCol(1) 1497 gs.Add(wx.StaticText(self, -1, 'Start:'), 0, wx.ALL, 0) 1498 self._start_date=wx.DatePickerCtrl(self, style=wx.DP_DROPDOWN | wx.DP_SHOWCENTURY) 1499 wx.EVT_DATE_CHANGED(self, self._start_date.GetId(), 1500 self.OnDateChanged) 1501 gs.Add(self._start_date, 0, wx.ALL, 0) 1502 gs.Add(wx.StaticText(self, -1, 'End:'), 0, wx.ALL, 0) 1503 self._end_date=wx.DatePickerCtrl(self, style=wx.DP_DROPDOWN | wx.DP_SHOWCENTURY) 1504 wx.EVT_DATE_CHANGED(self, self._end_date.GetId(), 1505 self.OnDateChanged) 1506 gs.Add(self._end_date, 0, wx.ALL, 0) 1507 sbs.Add(gs, 1, wx.EXPAND|wx.ALL, 5) 1508 hbs.Add(sbs, 0, wx.ALL, 5) 1509 # thye print style box 1510 self._print_style=wx.RadioBox(self, -1, 'Print Style', 1511 choices=['List View', 'Month View'], 1512 style=wx.RA_SPECIFY_ROWS) 1513 wx.EVT_RADIOBOX(self, self._print_style.GetId(), self.OnStyleChanged) 1514 hbs.Add(self._print_style, 0, wx.ALL, 5) 1515 vbs.Add(hbs, 0, wx.ALL, 5)
1516 1517 # constant class variables 1518 _one_day=wx.DateSpan(days=1) 1519 _empty_day=['', []]
1520 - def _one_day_data(self):
1521 # generate data for 1 day 1522 r=[str(self._dt_index.GetDay())] 1523 events=[] 1524 if self._dt_start<=self._dt_index<=self._dt_end: 1525 entries=self._widget.getentrydata(self._dt_index.GetYear(), 1526 self._dt_index.GetMonth()+1, 1527 self._dt_index.GetDay()) 1528 else: 1529 entries=[] 1530 self._dt_index+=self._one_day 1531 if len(entries): 1532 entries.sort(CalendarEntry.cmp_by_time) 1533 for e in entries: 1534 print_data=e.print_data 1535 events.append('%s: %s'%(print_data[0], print_data[3])) 1536 r.append(events) 1537 return r
1538 - def _one_week_data(self):
1539 # generate data for 1 week 1540 dow=self._dt_index.GetWeekDay() 1541 if dow: 1542 r=[self._empty_day]*dow 1543 else: 1544 r=[] 1545 for d in range(dow, 7): 1546 r.append(self._one_day_data()) 1547 if self._dt_index.GetDay()==1: 1548 # new month 1549 break 1550 return r
1551 - def _one_month_data(self):
1552 # generate data for a month 1553 m=self._dt_index.GetMonth() 1554 y=self._dt_index.GetYear() 1555 r=['%s %d'%(self._dt_index.GetMonthName(m), y)] 1556 while self._dt_index.GetMonth()==m: 1557 r.append(self._one_week_data()) 1558 return r
1559 - def _get_monthly_data(self):
1560 """ generate a dict suitable to print monthly events 1561 """ 1562 res=[] 1563 self._dt_index=wx.DateTimeFromDMY(1, self._dt_start.GetMonth(), 1564 self._dt_start.GetYear()) 1565 while self._dt_index<=self._dt_end: 1566 res.append(self._one_month_data()) 1567 return res
1568
1569 - def _get_list_data(self):
1570 """ generate a dict suitable for printing""" 1571 self._dt_index=wx.DateTimeFromDMY(self._dt_start.GetDay(), 1572 self._dt_start.GetMonth(), 1573 self._dt_start.GetYear()) 1574 current_month=None 1575 res=a_month=month_events=[] 1576 while self._dt_index<=self._dt_end: 1577 y=self._dt_index.GetYear() 1578 m=self._dt_index.GetMonth() 1579 d=self._dt_index.GetDay() 1580 entries=self._widget.getentrydata(y, m+1, d) 1581 self._dt_index+=self._one_day 1582 if not len(entries): 1583 # no events on this day 1584 continue 1585 entries.sort(CalendarEntry.cmp_by_time) 1586 if m!=current_month: 1587 # save the current month 1588 if len(month_events): 1589 a_month.append(month_events) 1590 res.append(a_month) 1591 # start a new month 1592 current_month=m 1593 a_month=['%s %d'%(self._dt_index.GetMonthName(m), y)] 1594 month_events=[] 1595 # go through the entries and build a list of print data 1596 for i,e in enumerate(entries): 1597 if i: 1598 date_str=day_str='' 1599 else: 1600 date_str=str(d) 1601 day_str=self._dt_index.GetWeekDayName( 1602 self._dt_index.GetWeekDay()-1, wx.DateTime.Name_Abbr) 1603 month_events.append([date_str, day_str]+e.print_data) 1604 if len(month_events): 1605 # data left in the list 1606 a_month.append(month_events) 1607 res.append(a_month) 1608 return res
1609
1610 - def _init_print_data(self):
1611 # Initialize the dns dict with empty data 1612 super(CalendarPrintDialog, self)._init_print_data() 1613 self._dns['events']=[]
1614
1615 - def _gen_print_data(self):
1616 if not self._date_changed and \ 1617 not self._style_changed and \ 1618 self._html is not None: 1619 # already generate the print data, no changes needed 1620 return 1621 self._dt_start=self._start_date.GetValue() 1622 self._dt_end=self._end_date.GetValue() 1623 if not self._dt_start.IsValid() or not self._dt_end.IsValid(): 1624 # invalid print range 1625 return 1626 print_data=( 1627 (self._regular_template, self._regular_style, self._get_list_data), 1628 (self._monthly_template, self._monthly_style, self._get_monthly_data)) 1629 print_style=self._print_style.GetSelection() 1630 # tell the calendar widget to give me the dict I need 1631 print_dict=print_data[print_style][2]() 1632 # generate the html data 1633 if self._xcp is None: 1634 # build the whole document template 1635 self._xcp=xyaptu.xcopier(None) 1636 tmpl=file(guihelper.getresourcefile(print_data[print_style][0]), 1637 'rt').read() 1638 self._xcp.setupxcopy(tmpl) 1639 elif self._style_changed: 1640 # just update the template 1641 tmpl=file(guihelper.getresourcefile(print_data[print_style][0]), 1642 'rt').read() 1643 self._xcp.setupxcopy(tmpl) 1644 if self._dns is None: 1645 self._init_print_data() 1646 self._dns['events']=print_dict 1647 self._dns['date_range']='%s - %s'%\ 1648 (self._dt_start.FormatDate(), 1649 self._dt_end.FormatDate()) 1650 html=self._xcp.xcopywithdns(self._dns.copy()) 1651 # apply styles 1652 sd={'styles': {}, '__builtins__': __builtins__ } 1653 try: 1654 execfile(guihelper.getresourcefile(print_data[print_style][1]), sd, sd) 1655 except UnicodeError: 1656 common.unicode_execfile(guihelper.getresourcefile(print_data[print_style][1]), sd, sd) 1657 try: 1658 self._html=bphtml.applyhtmlstyles(html, sd['styles']) 1659 except: 1660 if __debug__: 1661 file('debug.html', 'wt').write(html) 1662 raise 1663 self._date_changed=self._style_change=False
1664
1665 - def OnDateChanged(self, _):
1666 self._date_changed=True
1667 - def OnStyleChanged(self, _):
1668 self._style_changed=True
1669
1670 #------------------------------------------------------------------------------- 1671 1672 -class MergeDataTable(gridlib.PyGridTableBase):
1673 # colums attributes 1674 _cols_attrs=( 1675 { 'label': 'Description', 1676 'readonly': True, 1677 'alignment': (wx.ALIGN_LEFT, wx.ALIGN_CENTRE), 1678 'type': gridlib.GRID_VALUE_STRING }, 1679 { 'label': 'Start', 1680 'readonly': True, 1681 'alignment': (wx.ALIGN_LEFT, wx.ALIGN_CENTRE), 1682 'type': gridlib.GRID_VALUE_STRING }, 1683 { 'label': 'Changed', 1684 'readonly': True, 1685 'alignment': (wx.ALIGN_CENTRE, wx.ALIGN_CENTRE), 1686 'type': gridlib.GRID_VALUE_BOOL }, 1687 { 'label': 'New', 1688 'readonly': False, 1689 'alignment': (wx.ALIGN_CENTRE, wx.ALIGN_CENTRE), 1690 'type': gridlib.GRID_VALUE_BOOL }, 1691 { 'label': 'Ignore', 1692 'readonly': False, 1693 'alignment': (wx.ALIGN_CENTRE, wx.ALIGN_CENTRE), 1694 'type': gridlib.GRID_VALUE_BOOL }, 1695 { 'label': 'Changed Details', 1696 'readonly': True, 1697 'alignment': (wx.ALIGN_LEFT, wx.ALIGN_CENTRE), 1698 'type': gridlib.GRID_VALUE_STRING }, 1699 ) 1700 # index into each row 1701 _desc_index=0 1702 _start_index=1 1703 _changed_index=2 1704 _new_index=3 1705 _ignore_index=4 1706 _details_index=5 1707 _key_index=6 1708 _similar_key_index=7 1709
1710 - def __init__(self, olddata, newdata):
1711 super(MergeDataTable, self).__init__() 1712 self._old=olddata 1713 self._new=newdata 1714 self.data=[] 1715 self._bins={} 1716 self._similarpairs={} 1717 self._generate_table()
1718
1719 - def _generate_table(self):
1720 # Generate table data from the given data 1721 # first, separate old events into bins for efficient comparison 1722 self._bins={} 1723 for _key,_entry in self._old.items(): 1724 self._bins.setdefault(_entry.start[:3], []).append(_key) 1725 self._similarpairs={} 1726 for _key,_entry in self._new.items(): 1727 # default to a new event being added 1728 _row=[_entry.description, _entry.start_str, 0, 1, 0, 'New event', _key] 1729 _bin_key=_entry.start[:3] 1730 for _item_key in self._bins.get(_bin_key, []): 1731 _old_event=self._old[_item_key] 1732 if _old_event.matches(_entry): 1733 # same event, no action 1734 _row[self._new_index]=0 1735 _row[self._details_index]='No changes' 1736 break 1737 elif _old_event.similar(_entry): 1738 # changed event, being merged 1739 _row[self._changed_index]=1 1740 _row[self._new_index]=0 1741 _row[self._details_index]=_old_event.get_changed_fields(_entry) 1742 _row.append(_item_key) 1743 break 1744 self.data.append(_row)
1745 - def _merge(self):
1746 # merge the new data into the old one, and return the result 1747 for _row in self.data: 1748 if _row[self._ignore_index]: 1749 # ignore this new entry 1750 continue 1751 elif _row[self._new_index]: 1752 # add this new entry 1753 _key=_row[self._key_index] 1754 self._old[_key]=self._new[_key] 1755 elif _row[self._changed_index]: 1756 # replace the old entry with this new one 1757 _new_key=_row[self._key_index] 1758 _old_key=_row[self._similar_key_index] 1759 self._old[_old_key].replace(self._new[_new_key]) 1760 return self._old
1761 - def _replace(self):
1762 # return non-ignore events 1763 _res={} 1764 for _row in self.data: 1765 if not _row[self._ignore_index]: 1766 _key=_row[self._key_index] 1767 _res[_key]=self._new[_key] 1768 return _res
1769 - def get(self, merge=False):
1770 # return the result data 1771 if not merge: 1772 # replace all with new data 1773 return self._replace() 1774 else: 1775 # return merged data 1776 return self._merge()
1777 1778 #-------------------------------------------------- 1779 # required methods for the wxPyGridTableBase interface
1780 - def GetNumberRows(self):
1781 return len(self.data)
1782 - def GetNumberCols(self):
1783 return len(self._cols_attrs)
1784 - def IsEmptyCell(self, row, col):
1785 if row>len(self.data) or col>len(self._cols_attrs): 1786 return True 1787 return False
1788 # Get/Set values in the table. The Python version of these 1789 # methods can handle any data-type, (as long as the Editor and 1790 # Renderer understands the type too,) not just strings as in the 1791 # C++ version.
1792 - def GetValue(self, row, col):
1793 try: 1794 return self.data[row][col] 1795 except IndexError: 1796 return ''
1797 - def SetValue(self, row, col, value):
1798 try: 1799 self.data[row][col] = value 1800 except IndexError: 1801 pass
1802 1803 #-------------------------------------------------- 1804 # Some optional methods 1805 # Called when the grid needs to display labels
1806 - def GetColLabelValue(self, col):
1807 try: 1808 return self._cols_attrs[col]['label'] 1809 except IndexError: 1810 return ''
1811 - def IsReadOnlyCell(self, row, col):
1812 try: 1813 return self._cols_attrs[col]['readonly'] 1814 except IndexError: 1815 return False
1816 - def GetAlignments(self, row, col):
1817 try: 1818 return self._cols_attrs[col]['alignment'] 1819 except IndexError: 1820 return None
1821 # Called to determine the kind of editor/renderer to use by 1822 # default, doesn't necessarily have to be the same type used 1823 # natively by the editor/renderer if they know how to convert.
1824 - def GetTypeName(self, row, col):
1825 return self._cols_attrs[col]['type']
1826 # Called to determine how the data can be fetched and stored by the 1827 # editor and renderer. This allows you to enforce some type-safety 1828 # in the grid.
1829 - def CanGetValueAs(self, row, col, typeName):
1830 return self._cols_attrs[col]['type']==typeName
1831 - def CanSetValueAs(self, row, col, typeName):
1832 return self.CanGetValueAs(row, col, typeName)
1833
1834 -class MergeDataGrid(gridlib.Grid):
1835 - def __init__(self, parent, table):
1836 super(MergeDataGrid, self).__init__(parent, -1) 1837 self.SetTable(table, True) 1838 # set col attributes 1839 for _col in range(table.GetNumberCols()): 1840 _ro=table.IsReadOnlyCell(0, _col) 1841 _alignments=table.GetAlignments(0, _col) 1842 if _ro or _alignments: 1843 _attr=gridlib.GridCellAttr() 1844 if _ro: 1845 _attr.SetReadOnly(True) 1846 if _alignments: 1847 _attr.SetAlignment(*_alignments) 1848 self.SetColAttr(_col, _attr) 1849 self.SetRowLabelSize(0) 1850 self.SetMargins(0,0) 1851 self.AutoSize() 1852 self.Refresh()
1853
1854 -class MergeDialog(wx.Dialog):
1855 - def __init__(self, parent, olddata, newdata):
1856 super(MergeDialog, self).__init__(parent, -1, 1857 'Calendar Data Merge', 1858 style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) 1859 self._merge=False 1860 vbs=wx.BoxSizer(wx.VERTICAL) 1861 self._grid=MergeDataGrid(self, MergeDataTable(olddata, newdata)) 1862 vbs.Add(self._grid, 1, wx.EXPAND|wx.ALL, 5) 1863 vbs.Add(wx.StaticLine(self), 0, wx.EXPAND|wx.ALL, 5) 1864 hbs=wx.BoxSizer(wx.HORIZONTAL) 1865 _btn=wx.Button(self, -1, 'Replace All') 1866 wx.EVT_BUTTON(self, _btn.GetId(), self.OnReplaceAll) 1867 hbs.Add(_btn, 0, wx.EXPAND|wx.ALL, 5) 1868 _btn=wx.Button(self, -1, 'Merge') 1869 wx.EVT_BUTTON(self, _btn.GetId(), self.OnMerge) 1870 hbs.Add(_btn, 0, wx.EXPAND|wx.ALL, 5) 1871 _btn=wx.Button(self, wx.ID_CANCEL, 'Cancel') 1872 hbs.Add(_btn, 0, wx.EXPAND|wx.ALL, 5) 1873 _btn=wx.Button(self, wx.ID_HELP, 'Help') 1874 wx.EVT_BUTTON(self, wx.ID_HELP, 1875 lambda _: wx.GetApp().displayhelpid(helpids.ID_DLG_CALENDAR_MERGE)) 1876 hbs.Add(_btn, 0, wx.EXPAND|wx.ALL, 5) 1877 vbs.Add(hbs, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 1878 self.SetSizer(vbs) 1879 self.SetAutoLayout(True) 1880 guiwidgets.set_size("CalendarMergeEditor", self, 52, 1.0)
1881 - def OnOK(self, _=None):
1882 guiwidgets.save_size("CalendarMergeEditor", self.GetRect()) 1883 if self.IsModal(): 1884 self.EndModal(wx.ID_OK) 1885 else: 1886 self.SetReturnCode(wx.ID_OK) 1887 self.Show(False)
1888 - def OnReplaceAll(self, evt):
1889 self._merge=False 1890 self.OnOK()
1891 - def OnMerge(self, _):
1892 self._merge=True 1893 self.OnOK()
1894 - def get(self):
1895 # return the merge data 1896 return self._grid.GetTable().get(self._merge)
1897