0001 #!/usr/bin/env python 0002 ### BITPIM 0003 ### 0004 ### Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com> 0005 ### 0006 ### This program is free software; you can redistribute it and/or modify 0007 ### it under the terms of the BitPim license as detailed in the LICENSE file. 0008 ### 0009 ### $Id: bpcalendar.py 4431 2007-10-18 01:41:28Z djpham $ 0010 0011 """Calendar user interface and data for bitpim. 0012 0013 This module has a bp prefix so it doesn't clash with the system calendar module 0014 0015 Version 3: 0016 0017 The format for the calendar is standardised. It is a dict with the following 0018 fields: 0019 (Note: hour fields are in 24 hour format) 0020 'string id': CalendarEntry object. 0021 0022 CalendarEntry properties: 0023 description - 'string description' 0024 location - 'string location' 0025 desc_loc - combination of description & location in the form of 'description[location]' 0026 priority - None=no priority, int from 1-10, 1=highest priority 0027 alarm - how many minutes beforehand to set the alarm (use 0 for on-time, None or -1 for no alarm) 0028 allday - True for an allday event, False otherwise 0029 start - (year, month, day, hour, minute) as integers 0030 end - (year, month, day, hour, minute) as integers 0031 serials - list of dicts of serials. 0032 repeat - None, or RepeatEntry object 0033 id - string id of this object. Created the same way as bpserials IDs for phonebook entries. 0034 notes - string notes 0035 categories - [ { 'category': string category }, ... ] 0036 ringtone - string ringtone assignment 0037 wallpaper - string wallpaper assignment. 0038 vibrate - True if the alarm is set to vibrate, False otherwise 0039 voice - ID of voice alarm 0040 0041 CalendarEntry methods: 0042 get() - return a copy of the internal dict 0043 get_db_dict()- return a copy of a database.basedataobject dict. 0044 set(dict) - set the internal dict with the supplied dict 0045 set_db_dict(dict) - set internal data with the database.basedataobject dict 0046 is_active(y, m, d) - True if this event is active on (y,m,d) 0047 suppress_repeat_entry(y,m,d) - exclude (y,m,d) from this repeat event. 0048 0049 RepeatEntry properties: 0050 repeat_type - one of daily, weekly, monthly, or yearly. 0051 interval - for daily: repeat every nth day. For weekly, for every nth week. 0052 interval2 - for monhtly: repeat every nth month. 0053 dow - bitmap of which day of week are being repeated. 0054 weekstart - the start of the work week ('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU') 0055 suppressed - list of (y,m,d) being excluded from this series. 0056 0057 -------------------------------------------------------------------------------- 0058 Version 2: 0059 0060 The format for the calendar is standardised. It is a dict with the following 0061 fields: 0062 0063 (Note: hour fields are in 24 hour format) 0064 0065 start: 0066 0067 - (year, month, day, hour, minute) as integers 0068 end: 0069 0070 - (year, month, day, hour, minute) as integers # if you want no end, set to the same value as start, or to the year 4000 0071 0072 repeat: 0073 0074 - one of None, "daily", "monfri", "weekly", "monthly", "yearly" 0075 0076 description: 0077 0078 - "String description" 0079 0080 changeserial: 0081 0082 - Set to integer 1 0083 0084 snoozedelay: 0085 0086 - Set to an integer number of minutes (default 0) 0087 0088 alarm: 0089 0090 - how many minutes beforehand to set the alarm (use 0 for on-time, None for no alarm) 0091 0092 daybitmap: 0093 0094 - default 0, it will become which days of the week weekly events happen on (eg every monday and friday) 0095 0096 ringtone: 0097 0098 - index number of the ringtone for the alarm (use 0 for none - will become a string) 0099 0100 pos: 0101 0102 - integer that should be the same as the dictionary key for this entry 0103 0104 exceptions: 0105 0106 - (optional) A list of (year,month,day) tuples that repeats are suppressed 0107 """ 0108 0109 # Standard modules 0110 from __future__ import with_statement 0111 import os 0112 import copy 0113 import calendar 0114 import datetime 0115 import random 0116 import sha 0117 import time 0118 0119 # wx stuff 0120 import wx 0121 import wx.lib 0122 import wx.lib.masked.textctrl 0123 import wx.lib.intctrl 0124 import wx.grid as gridlib 0125 0126 # my modules 0127 import bphtml 0128 import bptime 0129 import calendarcontrol 0130 import calendarentryeditor 0131 import common 0132 import database 0133 import guihelper 0134 import guiwidgets 0135 import helpids 0136 import pubsub 0137 import today 0138 import xyaptu 0139 0140 #------------------------------------------------------------------------------- 0141 class CalendarDataObject(database.basedataobject): 0142 """ 0143 This class is a wrapper class to enable CalendarEntry object data to be 0144 stored in the database stuff. Once the database module is updated, this 0145 class will also be updated and eventually replace CalendarEntry. 0146 """ 0147 _knownproperties=['description', 'location', 'priority', 'alarm', 0148 'notes', 'ringtone', 'wallpaper', 0149 'start', 'end', 'vibrate', 'voice' ] 0150 _knownlistproperties=database.basedataobject._knownlistproperties.copy() 0151 _knownlistproperties.update( { 0152 'repeat': ['type', 'interval', 0153 'interval2', 'dow', 'weekstart'], 0154 'suppressed': ['date'], 0155 'categories': ['category'] }) 0156 def __init__(self, data=None): 0157 if data is None or not isinstance(data, CalendarEntry): 0158 # empty data, do nothing 0159 return 0160 self.update(data.get_db_dict()) 0161 0162 calendarobjectfactory=database.dataobjectfactory(CalendarDataObject) 0163 #------------------------------------------------------------------------------- 0164 class RepeatEntry(object): 0165 # class constants 0166 daily='daily' 0167 weekly='weekly' 0168 monthly='monthly' 0169 yearly='yearly' 0170 _interval=0 0171 _dow=1 0172 _dom=0 0173 _moy=1 0174 _interval2=2 0175 _dow_names=( 0176 {1: 'Sun'}, {2: 'Mon'}, {4: 'Tue'}, {8: 'Wed'}, 0177 {16: 'Thu'}, {32: 'Fri'}, {64: 'Sat'}) 0178 # this faster than log2(x) 0179 _dow_num={ 1: wx.DateTime.Sun, 0180 2: wx.DateTime.Mon, 0181 4: wx.DateTime.Tue, 0182 8: wx.DateTime.Wed, 0183 16: wx.DateTime.Thu, 0184 32: wx.DateTime.Fri, 0185 64: wx.DateTime.Sat } 0186 dow_names={ 'Sun': 1, 'Mon': 2, 'Tue': 4, 'Wed': 8, 0187 'Thu': 16, 'Fri': 32, 'Sat': 64 } 0188 dow_weekday=0x3E 0189 dow_weekend=0x41 0190 dow_weekstart={ 0191 'SU': 7, 'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6 } 0192 0193 def __init__(self, repeat_type=daily): 0194 self._type=repeat_type 0195 self._data=[0,0,0] 0196 self._suppressed=[] 0197 self._wkstart=7 # default to Sun 0198 0199 def __eq__(self, rhs): 0200 # return T if equal 0201 if not isinstance(rhs, RepeatEntry): 0202 return False 0203 if self.repeat_type!=rhs.repeat_type: 0204 return False 0205 if self.repeat_type==RepeatEntry.daily: 0206 if self.interval!=rhs.interval: 0207 return False 0208 elif self.repeat_type==RepeatEntry.weekly: 0209 if self.interval!=rhs.interval or \ 0210 self.dow!=rhs.dow: 0211 return False 0212 elif self.repeat_type==RepeatEntry.monthly: 0213 if self.interval!=rhs.interval or \ 0214 self.interval2!=rhs.interval2 or \ 0215 self.dow!=rhs.dow: 0216 return False 0217 return True 0218 def __ne__(self, rhs): 0219 return not self.__eq__(rhs) 0220 0221 def get(self): 0222 # return a dict representing internal data 0223 # mainly used for populatefs 0224 r={} 0225 if self._type==self.daily: 0226 r[self.daily]= { 'interval': self._data[self._interval] } 0227 elif self._type==self.weekly: 0228 r[self.weekly]= { 'interval': self._data[self._interval], 0229 'dow': self._data[self._dow] } 0230 elif self._type==self.monthly: 0231 r[self.monthly]={ 'interval': self._data[self._interval], 0232 'interval2': self._data[self._interval2], 0233 'dow': self._data[self._dow] } 0234 else: 0235 r[self.yearly]=None 0236 s=[] 0237 for n in self._suppressed: 0238 s.append(n.get()) 0239 r['suppressed']=s 0240 return r 0241 0242 def get_db_dict(self): 0243 # return a copy of the dict compatible with the database stuff 0244 db_r={} 0245 r={} 0246 r['type']=self._type 0247 r['weekstart']=self.weekstart 0248 if self._type==self.daily: 0249 r['interval']=self._data[self._interval] 0250 elif self._type==self.weekly or self._type==self.monthly: 0251 r['interval']=self._data[self._interval] 0252 r['dow']=self._data[self._dow] 0253 if self._type==self.monthly: 0254 r['interval2']=self._data[self._interval2] 0255 # and the suppressed stuff 0256 s=[] 0257 for n in self._suppressed: 0258 s.append({ 'date': n.iso_str(True) }) 0259 db_r['repeat']=[r] 0260 if len(s): 0261 db_r['suppressed']=s 0262 return db_r 0263 0264 def set(self, data): 0265 # setting data from a dict, mainly used for getfromfs 0266 if data.has_key(self.daily): 0267 # daily type 0268 self.repeat_type=self.daily 0269 self.interval=data[self.daily]['interval'] 0270 elif data.has_key(self.weekly): 0271 # weekly type 0272 self.repeat_type=self.weekly 0273 self.interval=data[self.weekly]['interval'] 0274 self.dow=data[self.weekly]['dow'] 0275 elif data.has_key(self.monthly): 0276 self.repeat_type=self.monthly 0277 self.dow=data[self.monthly].get('dow', 0) 0278 self.interval=data[self.monthly].get('interval', 0) 0279 self.interval2=data[self.monthly].get('interval2', 1) 0280 else: 0281 self.repeat_type=self.yearly 0282 s=[] 0283 for n in data.get('suppressed', []): 0284 s.append(bptime.BPTime(n)) 0285 self.suppressed=s 0286 0287 def set_db_dict(self, data): 0288 r=data.get('repeat', [{}])[0] 0289 self.repeat_type=r['type'] 0290 _dow=r.get('dow', 0) 0291 _interval=r.get('interval', 0) 0292 self.weekstart=r.get('weekstart', 'SU') 0293 if self.repeat_type==self.daily: 0294 self.interval=_interval 0295 elif self.repeat_type==self.weekly or self.repeat_type==self.monthly: 0296 self.interval=_interval 0297 self.dow=_dow 0298 if self.repeat_type==self.monthly: 0299 self.interval2=r.get('interval2', 1) 0300 # now the suppressed stuff 0301 s=[] 0302 for n in data.get('suppressed', []): 0303 s.append(bptime.BPTime(n['date'])) 0304 self.suppressed=s 0305 0306 def get_nthweekday(self, date): 0307 """Utility routine: return the nth weekday of the specified date""" 0308 _wxmonth=date[1]-1 0309 _year=date[0] 0310 _day=date[2] 0311 _dt=wx.DateTimeFromDMY(_day, _wxmonth, _year) 0312 _dt.SetToWeekDay(_dt.GetWeekDay(), 1, _wxmonth, _year) 0313 return (_day-_dt.GetDay())/7+1 0314 0315 def _check_daily(self, s, d): 0316 if self.interval: 0317 # every nth day 0318 return (int((d-s).days)%self.interval)==0 0319 else: 0320 # every weekday 0321 return d.weekday()<5 0322 def _next_daily(self, ymd): 0323 """Return the date (y,m,d) of the next occurrence of this event""" 0324 _d0=datetime.date(*ymd) 0325 if self.interval: 0326 # every nth day: 0327 _delta=self.interval 0328 else: 0329 # every weekday 0330 if _d0.isoweekday()<5: 0331 # next weekday 0332 _delta=1 0333 else: 0334 # the following Monday 0335 _delta=3 0336 _d1=_d0+datetime.timedelta(days=_delta) 0337 return (_d1.year, _d1.month, _d1.day) 0338 0339 def _weekof(self, d): 0340 # return the date of the start of the week into which that d falls. 0341 _workweek=self.weekstart 0342 _dow=d.isoweekday() 0343 return d-datetime.timedelta((_dow-_workweek) if _dow>=_workweek \ 0344 else (_dow+7-_workweek)) 0345 0346 def _check_weekly(self, s, d): 0347 # check if at least one day-of-week is specified, if not default to the 0348 # start date 0349 if self.dow==0: 0350 self.dow=1<<(s.isoweekday()%7) 0351 # check to see if this is the nth week 0352 day_of_week=d.isoweekday()%7 # Sun=0, ..., Sat=6 0353 if ((self._weekof(d)-self._weekof(s)).days/7)%self.interval: 0354 # wrong week 0355 return False 0356 # check for the right weekday 0357 return ((1<<day_of_week)&self.dow) != 0 0358 def _next_weekly(self, ymd): 0359 """Return the next occurrence of this event from ymd date""" 0360 _oneday=datetime.timedelta(days=1) 0361 _d0=datetime.date(*ymd)+_oneday 0362 _dowbit=1<<(_d0.isoweekday()%7) 0363 while _dowbit!=1: 0364 if self.dow&_dowbit: 0365 return (_d0.year, _d0.month, _d0.day) 0366 _dowbit<<=1 0367 if _dowbit==128: 0368 _dowbit=1 0369 _d0+=_oneday 0370 _delta=(self.interval-1)*7 0371 _d0+=datetime.timedelta(days=_delta) 0372 while _dowbit!=128: 0373 if self.dow&_dowbit: 0374 return (_d0.year, _d0.month, _d0.day) 0375 _dowbit<<=1 0376 _d0+=_oneday 0377 0378 def _check_monthly(self, s, d): 0379 if not self.interval2: 0380 # default to every month 0381 self.interval2=1 0382 if d.month>=s.month: 0383 if (d.month-s.month)%self.interval2: 0384 # wrong month 0385 return False 0386 elif (12+d.month-s.month)%self.interval2: 0387 return False 0388 if self.dow==0: 0389 # no weekday specified, implied nth day of the month 0390 return d.day==s.day 0391 else: 0392 # every interval-th dow-day (ie 1st Mon) of the month 0393 _dow=(1<<(d.isoweekday()%7))&self.dow 0394 if not _dow: 0395 # not even the right day-of-week 0396 return False 0397 dt=wx.DateTime.Now() 0398 if self.interval<5: 0399 # nth *day of the month 0400 _nth=self.interval 0401 else: 0402 # last *day of the month 0403 _nth=-1 0404 return dt.SetToWeekDay(self._dow_num[_dow], 0405 _nth, month=d.month-1, year=d.year) and \ 0406 dt.GetDay()==d.day 0407 def _next_monthly(self, ymd): 0408 """Return the date of the next occurrence of this event""" 0409 _day=ymd[2] 0410 _month=ymd[1]+self.interval2 0411 if _month%12: 0412 _year=ymd[0]+_month/12 0413 _month=_month%12 0414 else: 0415 _year=ymd[0]+_month/12-1 0416 _month=12 0417 _d1=datetime.date(_year, _month, _day) 0418 if self.dow==0: 0419 # nth day of the month 0420 return (_d1.year, _d1.month, _d1.day) 0421 else: 0422 # every interval-th dow-day (ie 1st Mon) of the month 0423 if self.interval<5: 0424 # nth *day of the month 0425 _nth=self.interval 0426 else: 0427 # last *day of the month 0428 _nth=-1 0429 _dt=wx.DateTime() 0430 _dt.SetToWeekDay(self._dow_num[self.dow], _nth, month=_d1.month-1, 0431 year=_d1.year) 0432 return (_dt.GetYear(), _dt.GetMonth()+1, _dt.GetDay()) 0433 0434 def _check_yearly(self, s, d): 0435 return d.month==s.month and d.day==s.day 0436 def _next_yearly(self, ymd): 0437 """Return the date of the next occurrence of this event""" 0438 return (ymd[0]+1, ymd[1], ymd[2]) 0439 0440 def is_active(self, s, d): 0441 # check in the suppressed list 0442 if bptime.BPTime(d) in self._suppressed: 0443 # in the list, not part of this repeat 0444 return False 0445 # determine if the date is active 0446 if self.repeat_type==self.daily: 0447 return self._check_daily(s, d) 0448 elif self.repeat_type==self.weekly: 0449 return self._check_weekly(s, d) 0450 elif self.repeat_type==self.monthly: 0451 return self._check_monthly(s, d) 0452 elif self.repeat_type==self.yearly: 0453 return self._check_yearly(s, d) 0454 else: 0455 return False 0456 0457 def next_date(self, ymd): 0458 """Return the date of the next occurrence of this event""" 0459 if self.repeat_type==self.daily: 0460 return self._next_daily(ymd) 0461 elif self.repeat_type==self.weekly: 0462 return self._next_weekly(ymd) 0463 elif self.repeat_type==self.monthly: 0464 return self._next_monthly(ymd) 0465 else: 0466 return self._next_yearly(ymd) 0467 0468 def _get_type(self): 0469 return self._type 0470 def _set_type(self, repeat_type): 0471 if repeat_type in (self.daily, self.weekly, 0472 self.monthly, self.yearly): 0473 self._type = repeat_type 0474 else: 0475 raise AttributeError, 'type' 0476 repeat_type=property(fget=_get_type, fset=_set_type) 0477 0478 def _get_interval(self): 0479 if self._type==self.yearly: 0480 raise AttributeError 0481 return self._data[self._interval] 0482 def _set_interval(self, interval): 0483 if self._type==self.yearly: 0484 raise AttributeError 0485 self._data[self._interval]=interval 0486 interval=property(fget=_get_interval, fset=_set_interval) 0487 0488 def _get_interval2(self): 0489 if self._type==self.yearly: 0490 raise AttributeError 0491 return self._data[self._interval2] 0492 def _set_interval2(self, interval): 0493 if self._type==self.yearly: 0494 raise AttributeError 0495 self._data[self._interval2]=interval 0496 interval2=property(fget=_get_interval2, fset=_set_interval2) 0497 0498 def _get_dow(self): 0499 if self._type==self.yearly: 0500 raise AttributeError 0501 return self._data[self._dow] 0502 def _set_dow(self, dow): 0503 if self._type==self.yearly: 0504 raise AttributeError 0505 if isinstance(dow, int): 0506 self._data[self._dow]=dow 0507 elif isinstance(dow, (list, tuple)): 0508 self._data[self._dow]=1<<(datetime.date(*dow[:3]).isoweekday()%7) 0509 else: 0510 raise TypeError,"Must be an int or a list/tuple" 0511 dow=property(fget=_get_dow, fset=_set_dow) 0512 def _get_dow_str(self): 0513 try: 0514 _dow=self.dow 0515 except AttributeError: 0516 return '' 0517 names=[] 0518 for l in self._dow_names: 0519 for k,e in l.items(): 0520 if k&_dow: 0521 names.append(e) 0522 return ';'.join(names) 0523 dow_str=property(fget=_get_dow_str) 0524 0525 def _get_wkstart(self): 0526 return self._wkstart 0527 def _set_wkstart(self, wkstart): 0528 if isinstance(wkstart, int): 0529 if wkstart in range(1, 8): 0530 self._wkstart=wkstart 0531 else: 0532 raise ValueError('Must be between 1-7') 0533 elif isinstance(wkstart, (str, unicode)): 0534 self._wkstart=self.dow_weekstart.get(str(wkstart.upper()), 7) 0535 else: 0536 raise TypeError("Must be either a string or int") 0537 weekstart=property(fget=_get_wkstart, fset=_set_wkstart) 0538 0539 def _get_suppressed(self): 0540 return self._suppressed 0541 def _set_suppressed(self, d): 0542 if not isinstance(d, list): 0543 raise TypeError, 'must be a list of string or BPTime' 0544 if not len(d) or isinstance(d[0], bptime.BPTime): 0545 # empty list or already a list of BPTime 0546 self._suppressed=d 0547 elif isinstance(d[0], str): 0548 # list of 'yyyy-mm-dd' 0549 self._suppressed=[] 0550 for n in d: 0551 self._suppressed.append(bptime.BPTime(n.replace('-', ''))) 0552 def add_suppressed(self, y, m, d): 0553 self._suppressed.append(bptime.BPTime((y, m, d))) 0554 def get_suppressed_list(self): 0555 return [x.date_str() for x in self._suppressed] 0556 suppressed=property(fget=_get_suppressed, fset=_set_suppressed) 0557 def _get_suppressed_str(self): 0558 return ';'.join(self.get_suppressed_list()) 0559 suppressed_str=property(fget=_get_suppressed_str) 0560 0561 #------------------------------------------------------------------------------- 0562 class CalendarEntry(object): 0563 # priority const 0564 priority_high=1 0565 priority_normal=5 0566 priority_low=10 0567 # no end date 0568 no_end_date=(4000, 1, 1) 0569 # required and optional attributes, mainly used for comparison 0570 _required_attrs=('description', 'start','end') 0571 _required_attr_names=('Description', 'Start', 'End') 0572 _optional_attrs=('location', 'priority', 'alarm', 'allday', 'vibrate', 0573 'voice', 'repeat', 'notes', 'categories', 0574 'ringtone', 'wallpaper') 0575 _optional_attr_names=('Location', 'Priority', 'Alarm', 'All-Day', 0576 'Vibrate', '', 'Repeat', 'Notes', 'Categories', 0577 'Ringtone', 'Wallpaper') 0578 def __init__(self, year=None, month=None, day=None): 0579 self._data={} 0580 # setting default values 0581 if day is not None: 0582 self._data['start']=bptime.BPTime((year, month, day)) 0583 self._data['end']=bptime.BPTime((year, month, day)) 0584 else: 0585 self._data['start']=bptime.BPTime() 0586 self._data['end']=bptime.BPTime() 0587 self._data['serials']=[] 0588 self._create_id() 0589 0590 def matches(self, rhs): 0591 # Match self against this entry, which may not have all the 0592 # optional attributes 0593 if not isinstance(rhs, CalendarEntry): 0594 return False 0595 for _attr in CalendarEntry._required_attrs: 0596 if getattr(self, _attr) != getattr(rhs, _attr): 0597 return False 0598 for _attr in CalendarEntry._optional_attrs: 0599 _rhs_attr=getattr(rhs, _attr) 0600 if _rhs_attr is not None and getattr(self, _attr)!=_rhs_attr: 0601 return False 0602 return True 0603 def get_changed_fields(self, rhs): 0604 # Return a CSV string of all the fields having different values 0605 if not isinstance(rhs, CalendarEntry): 0606 return '' 0607 _res=[] 0608 for _idx,_attr in enumerate(CalendarEntry._required_attrs): 0609 if getattr(self, _attr) != getattr(rhs, _attr): 0610 _res.append(CalendarEntry._required_attr_names[_idx]) 0611 for _idx,_attr in enumerate(CalendarEntry._optional_attrs): 0612 _rhs_attr=getattr(rhs, _attr) 0613 if _rhs_attr is not None and getattr(self, _attr)!=_rhs_attr: 0614 _res.append(CalendarEntry._optional_attr_names[_idx]) 0615 return ','.join(_res) 0616 0617 def similar(self, rhs): 0618 # return T if rhs is similar to self 0619 # for now, they're similar if they have the same start time 0620 return self.start==rhs.start 0621 0622 def replace(self, rhs): 0623 # replace the contents of this entry with the new one 0624 for _attr in CalendarEntry._required_attrs+\ 0625 CalendarEntry._optional_attrs: 0626 _rhs_attr=getattr(rhs, _attr) 0627 if _rhs_attr is not None: 0628 setattr(self, _attr, _rhs_attr) 0629 0630 def __eq__(self, rhs): 0631 if not isinstance(rhs, CalendarEntry): 0632 return False 0633 for _attr in CalendarEntry._required_attrs+CalendarEntry._optional_attrs: 0634 if getattr(self, _attr)!=getattr(rhs, _attr): 0635 return False 0636 return True 0637 def __ne__(self, rhs): 0638 return not self.__eq__(rhs) 0639 0640 def get(self): 0641 r=copy.deepcopy(self._data, _nil={}) 0642 if self.repeat is not None: 0643 r['repeat']=self.repeat.get() 0644 r['start']=self._data['start'].iso_str() 0645 r['end']=self._data['end'].iso_str() 0646 return r 0647 0648 def get_db_dict(self): 0649 # return a dict compatible with the database stuff 0650 r=copy.deepcopy(self._data, _nil={}) 0651 # adjust for start & end 0652 r['start']=self._data['start'].iso_str(self.allday) 0653 r['end']=self._data['end'].iso_str(self.allday) 0654 # adjust for repeat & suppressed 0655 if self.repeat is not None: 0656 r.update(self.repeat.get_db_dict()) 0657 # take out uneeded keys 0658 if r.has_key('allday'): 0659 del r['allday'] 0660 return r 0661 0662 def set(self, data): 0663 self._data={} 0664 self._data.update(data) 0665 self._data['start']=bptime.BPTime(data['start']) 0666 self._data['end']=bptime.BPTime(data['end']) 0667 if self.repeat is not None: 0668 r=RepeatEntry() 0669 r.set(self.repeat) 0670 self.repeat=r 0671 # try to clean up the dict 0672 for k, e in self._data.items(): 0673 if e is None or e=='' or e==[]: 0674 del self._data[k] 0675 0676 def set_db_dict(self, data): 0677 # update our data with dict return from database 0678 self._data={} 0679 self._data.update(data) 0680 # adjust for allday 0681 self.allday=len(data['start'])==8 0682 # adjust for start and end 0683 self._data['start']=bptime.BPTime(data['start']) 0684 self._data['end']=bptime.BPTime(data['end']) 0685 # adjust for repeat 0686 if data.has_key('repeat'): 0687 rp=RepeatEntry() 0688 rp.set_db_dict(data) 0689 self.repeat=rp 0690 0691 def is_active(self, y, m ,d): 0692 # return true if if this event is active on this date, 0693 # mainly used for repeating events. 0694 s=self._data['start'].date 0695 e=self._data['end'].date 0696 d=datetime.date(y, m, d) 0697 if d<s or d>e: 0698 # before start date, after end date 0699 return False 0700 if self.repeat is None: 0701 # not a repeat event, within range so it's good 0702 return True 0703 # repeat event: check if it's in range. 0704 return self.repeat.is_active(s, d) 0705 0706 def suppress_repeat_entry(self, y, m, d): 0707 if self.repeat is None: 0708 # not a repeat entry, do nothing 0709 return 0710 self.repeat.add_suppressed(y, m, d) 0711 0712 def _set_or_del(self, key, v, v_list=()): 0713 if v is None or v in v_list: 0714 if self._data.has_key(key): 0715 del self._data[key] 0716 else: 0717 self._data[key]=v 0718 0719 def _get_description(self): 0720 return self._data.get('description', '') 0721 def _set_description(self, desc): 0722 self._set_or_del('description', desc, ('',)) 0723 description=property(fget=_get_description, fset=_set_description) 0724 0725 def _get_location(self): 0726 return self._data.get('location', '') 0727 def _set_location(self, location): 0728 self._set_or_del('location', location, ('',)) 0729 location=property(fget=_get_location, fset=_set_location) 0730 0731 def _get_desc_loc(self): 0732 # return 'description[location]' 0733 if self.location: 0734 return self.description+'['+self.location+']' 0735 return self.description 0736 def _set_desc_loc(self, v): 0737 # parse and set for 'description[location]' 0738 _idx1=v.find('[') 0739 _idx2=v.find(']') 0740 if _idx1!=-1 and _idx2!=-1 and _idx2>_idx1: 0741 # location specified 0742 self.location=v[_idx1+1:_idx2] 0743 self.description=v[:_idx1] 0744 else: 0745 self.description=v 0746 desc_loc=property(fget=_get_desc_loc, fset=_set_desc_loc) 0747 0748 def _get_priority(self): 0749 return self._data.get('priority', None) 0750 def _set_priority(self, priority): 0751 self._set_or_del('priority', priority) 0752 priority=property(fget=_get_priority, fset=_set_priority) 0753 0754 def _get_alarm(self): 0755 return self._data.get('alarm', -1) 0756 def _set_alarm(self, alarm): 0757 self._set_or_del('alarm', alarm) 0758 alarm=property(fget=_get_alarm, fset=_set_alarm) 0759 0760 def _get_allday(self): 0761 return self._data.get('allday', False) 0762 def _set_allday(self, allday): 0763 self._data['allday']=allday 0764 allday=property(fget=_get_allday, fset=_set_allday) 0765 0766 def _get_start(self): 0767 return self._data['start'].get() 0768 def _set_start(self, datetime): 0769 self._data['start'].set(datetime) 0770 start=property(fget=_get_start, fset=_set_start) 0771 def _get_start_str(self): 0772 return self._data['start'].date_str()+' '+\ 0773 self._data['start'].time_str(False, '00:00') 0774 start_str=property(fget=_get_start_str) 0775 0776 def _get_end(self): 0777 return self._data['end'].get() 0778 def _set_end(self, datetime): 0779 self._data['end'].set(datetime) 0780 end=property(fget=_get_end, fset=_set_end) 0781 def _get_end_str(self): 0782 return self._data['end'].date_str()+' '+\ 0783 self._data['end'].time_str(False, '00:00') 0784 end_str=property(fget=_get_end_str) 0785 def open_ended(self): 0786 # True if this is an open-ended event 0787 return self.end[:3]==self.no_end_date 0788 0789 def _get_vibrate(self): 0790 return self._data.get('vibrate', 0) 0791 def _set_vibrate(self, v): 0792 self._set_or_del('vibrate', v, (None, 0, False)) 0793 vibrate=property(fget=_get_vibrate, fset=_set_vibrate) 0794 0795 def _get_voice(self): 0796 return self._data.get('voice', None) 0797 def _set_voice(self, v): 0798 self._set_or_del('voice', v, (None,)) 0799 voice=property(fget=_get_voice, fset=_set_voice) 0800 0801 def _get_serials(self): 0802 return self._data.get('serials', None) 0803 def _set_serials(self, serials): 0804 self._data['serials']=serials 0805 serials=property(fget=_get_serials, fset=_set_serials) 0806 0807 def _get_repeat(self): 0808 return self._data.get('repeat', None) 0809 def _set_repeat(self, repeat): 0810 self._set_or_del('repeat', repeat) 0811 repeat=property(fget=_get_repeat, fset=_set_repeat) 0812 0813 def _get_id(self): 0814 s=self._data.get('serials', []) 0815 for n in s: 0816 if n.get('sourcetype', None)=='bitpim': 0817 return n.get('id', None) 0818 return None 0819 def _set_id(self, id): 0820 s=self._data.get('serials', []) 0821 for n in s: 0822 if n.get('sourcetype', None)=='bitpim': 0823 n['id']=id 0824 return 0825 self._data['serials'].append({'sourcetype': 'bitpim', 'id': id } ) 0826 id=property(fget=_get_id, fset=_set_id) 0827 0828 def _get_notes(self): 0829 return self._data.get('notes', '') 0830 def _set_notes(self, s): 0831 self._set_or_del('notes', s, ('',)) 0832 notes=property(fget=_get_notes, fset=_set_notes) 0833 0834 def _get_categories(self): 0835 return self._data.get('categories', []) 0836 def _set_categories(self, s): 0837 self._set_or_del('categories', s,([],)) 0838 if s==[] and self._data.has_key('categories'): 0839 del self._data['categories'] 0840 categories=property(fget=_get_categories, fset=_set_categories) 0841 def _get_categories_str(self): 0842 c=self.categories 0843 if len(c): 0844 return ';'.join([x['category'] for x in c]) 0845 else: 0846 return '' 0847 categories_str=property(fget=_get_categories_str) 0848 0849 def _get_ringtone(self): 0850 return self._data.get('ringtone', '') 0851 def _set_ringtone(self, rt): 0852 self._set_or_del('ringtone', rt, ('',)) 0853 ringtone=property(fget=_get_ringtone, fset=_set_ringtone) 0854 0855 def _get_wallpaper(self): 0856 return self._data.get('wallpaper', '',) 0857 def _set_wallpaper(self, wp): 0858 self._set_or_del('wallpaper', wp, ('',)) 0859 wallpaper=property(fget=_get_wallpaper, fset=_set_wallpaper) 0860 0861 # we use two random numbers to generate the serials. _persistrandom 0862 # is seeded at startup 0863 _persistrandom=random.Random() 0864 def _create_id(self): 0865 "Create a BitPim serial for this entry" 0866 rand2=random.Random() # this random is seeded when this function is called 0867 num=sha.new() 0868 num.update(`self._persistrandom.random()`) 0869 num.update(`rand2.random()`) 0870 self._data["serials"].append({"sourcetype": "bitpim", "id": num.hexdigest()}) 0871 0872 def _get_print_data(self): 0873 """ return a list of strings used for printing this event: 0874 [0]: start time, [1]: '', [2]: end time, [3]: Description 0875 [4]: Repeat Type, [5]: Alarm 0876 """ 0877 if self.allday: 0878 t0='All Day' 0879 t1='' 0880 else: 0881 t0=self._data['start'].time_str() 0882 t1=self._data['end'].time_str() 0883 rp=self.repeat 0884 if rp is None: 0885 rp_str='' 0886 else: 0887 rp_str=rp.repeat_type[0].upper() 0888 if self.alarm==-1: 0889 alarm_str='' 0890 else: 0891 alarm_str='%d:%02d'%(self.alarm/60, self.alarm%60) 0892 return [t0, '', t1, self.description, rp_str, alarm_str] 0893 print_data=property(fget=_get_print_data) 0894 @classmethod 0895 def cmp_by_time(cls, a, b): 0896 """ compare 2 objects by start times. 0897 -1 if a<b, 0 if a==b, and 1 if a>b 0898 allday is always less than having start times. 0899 Mainly used for sorting list of events 0900 """ 0901 if not isinstance(a, cls) or \ 0902 not isinstance(b, cls): 0903 raise TypeError, 'must be a CalendarEntry object' 0904 if a.allday and b.allday: 0905 return 0 0906 if a.allday and not b.allday: 0907 return -1 0908 if not a.allday and b.allday: 0909 return 1 0910 t0=a.start[3:] 0911 t1=b.start[3:] 0912 if t0<t1: 0913 return -1 0914 if t0==t1: 0915 return 0 0916 if t0>t1: 0917 return 1 0918 0919 def _summary(self): 0920 # provide a one-liner summary string for this event 0921 if self.allday: 0922 str=self.description 0923 else: 0924 hr=self.start[3] 0925 ap="am" 0926 if hr>=12: 0927 ap="pm" 0928 hr-=12 0929 if hr==0: hr=12 0930 str="%2d:%02d %s" % (hr, self.start[4], ap) 0931 str+=" "+self.description 0932 return str 0933 summary=property(fget=_summary) 0934 0935 0936 #------------------------------------------------------------------------------- 0937 class Calendar(calendarcontrol.Calendar): 0938 """A class encapsulating the GUI and data of the calendar (all days). A seperate dialog is 0939 used to edit the content of one particular day.""" 0940 0941 CURRENTFILEVERSION=3 0942 0943 def __init__(self, mainwindow, parent, id=-1): 0944 """constructor 0945 0946 @type mainwindow: gui.MainWindow 0947 @param mainwindow: Used to get configuration data (such as directory to save/load data. 0948 @param parent: Widget acting as parent for this one 0949 @param id: id 0950 """ 0951 self.mainwindow=mainwindow 0952 self.entrycache={} 0953 self.entries={} 0954 self.repeating=[] # nb this is stored unsorted 0955 self._data={} # the underlying data 0956 calendarcontrol.Calendar.__init__(self, parent, rows=5, id=id) 0957 self.dialog=calendarentryeditor.Editor(self) 0958 pubsub.subscribe(self.OnMediaNameChanged, pubsub.MEDIA_NAME_CHANGED) 0959 today.bind_notification_event(self.OnTodayItem, 0960 today.Today_Group_Calendar) 0961 today.bind_request_event(self.OnTodayRequest) 0962 pubsub.subscribe(self.OnTodayButton, pubsub.MIDNIGHT) 0963 0964 def OnPrintDialog(self, mainwindow, config): 0965 with guihelper.WXDialogWrapper(CalendarPrintDialog(self, mainwindow, config), 0966 True): 0967 pass 0968 def CanPrint(self): 0969 return True 0970 0971 def OnMediaNameChanged(self, msg): 0972 d=msg.data 0973 _type=d.get(pubsub.media_change_type, None) 0974 _old_name=d.get(pubsub.media_old_name, None) 0975 _new_name=d.get(pubsub.media_new_name, None) 0976 if _type is None or _old_name is None or _new_name is None: 0977 # invalid/incomplete data 0978 return 0979 if _type!=pubsub.wallpaper_type and \ 0980 _type!=pubsub.ringtone_type: 0981 # neither wallpaper nor ringtone 0982 return 0983 _old_name=common.basename(_old_name) 0984 _new_name=common.basename(_new_name) 0985 if _type==pubsub.wallpaper_type: 0986 attr_name='wallpaper' 0987 else: 0988 attr_name='ringtone' 0989 modified=False 0990 for k,e in self._data.items(): 0991 if getattr(e, attr_name, None)==_old_name: 0992 setattr(e, attr_name, _new_name) 0993 modified=True 0994 if modified: 0995 # changes were made, update everything 0996 self.updateonchange() 0997 0998 def getdata(self, dict): 0999 """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, _): 1181 self._publish_today_events() 1182 self._publish_thisweek_events() 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
Generated by PyXR 0.9.4