1
2
3
4
5
6
7
8
9
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
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
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
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
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'] })
161
162 calendarobjectfactory=database.dataobjectfactory(CalendarDataObject)
163
164 -class RepeatEntry(object):
165
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
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
198
199 - def __eq__(self, rhs):
200
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
222
223
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
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
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
266 if data.has_key(self.daily):
267
268 self.repeat_type=self.daily
269 self.interval=data[self.daily]['interval']
270 elif data.has_key(self.weekly):
271
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
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
318 return (int((d-s).days)%self.interval)==0
319 else:
320
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
327 _delta=self.interval
328 else:
329
330 if _d0.isoweekday()<5:
331
332 _delta=1
333 else:
334
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
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
348
349 if self.dow==0:
350 self.dow=1<<(s.isoweekday()%7)
351
352 day_of_week=d.isoweekday()%7
353 if ((self._weekof(d)-self._weekof(s)).days/7)%self.interval:
354
355 return False
356
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
381 self.interval2=1
382 if d.month>=s.month:
383 if (d.month-s.month)%self.interval2:
384
385 return False
386 elif (12+d.month-s.month)%self.interval2:
387 return False
388 if self.dow==0:
389
390 return d.day==s.day
391 else:
392
393 _dow=(1<<(d.isoweekday()%7))&self.dow
394 if not _dow:
395
396 return False
397 dt=wx.DateTime.Now()
398 if self.interval<5:
399
400 _nth=self.interval
401 else:
402
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
420 return (_d1.year, _d1.month, _d1.day)
421 else:
422
423 if self.interval<5:
424
425 _nth=self.interval
426 else:
427
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
442 if bptime.BPTime(d) in self._suppressed:
443
444 return False
445
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):
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):
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
546 self._suppressed=d
547 elif isinstance(d[0], str):
548
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)))
555 return [x.date_str() for x in self._suppressed]
556 suppressed=property(fget=_get_suppressed, fset=_set_suppressed)
558 return ';'.join(self.get_suppressed_list())
559 suppressed_str=property(fget=_get_suppressed_str)
560
561
562 -class CalendarEntry(object):
563
564 priority_high=1
565 priority_normal=5
566 priority_low=10
567
568 no_end_date=(4000, 1, 1)
569
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
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
592
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
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
619
620 return self.start==rhs.start
621
622 - def replace(self, rhs):
623
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
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
650 r=copy.deepcopy(self._data, _nil={})
651
652 r['start']=self._data['start'].iso_str(self.allday)
653 r['end']=self._data['end'].iso_str(self.allday)
654
655 if self.repeat is not None:
656 r.update(self.repeat.get_db_dict())
657
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
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
678 self._data={}
679 self._data.update(data)
680
681 self.allday=len(data['start'])==8
682
683 self._data['start']=bptime.BPTime(data['start'])
684 self._data['end']=bptime.BPTime(data['end'])
685
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
693
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
699 return False
700 if self.repeat is None:
701
702 return True
703
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
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
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
733 if self.location:
734 return self.description+'['+self.location+']'
735 return self.description
736 - def _set_desc_loc(self, v):
737
738 _idx1=v.find('[')
739 _idx2=v.find(']')
740 if _idx1!=-1 and _idx2!=-1 and _idx2>_idx1:
741
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
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
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)
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
862
863 _persistrandom=random.Random()
864 - def _create_id(self):
865 "Create a BitPim serial for this entry"
866 rand2=random.Random()
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
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):
963
970
997
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
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
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
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
1066 res=self.entrycache.get( (year,month,day), None)
1067 if res is not None:
1068 return res
1069
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
1083 res=CalendarEntry(year, month, day)
1084
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
1089
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
1102 if repeat!="weekly":
1103 return 0
1104 dayofweek=calendar.weekday(*(start[:3]))
1105 dayofweek=(dayofweek+1)%7
1106 return [2048,1024,512,256,128,64,32][dayofweek]
1107
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
1129 wx.Bell()
1130 else:
1131 self.dialog.setdate(year, month, day, entry)
1132 self.dialog.Show(True)
1133
1139
1146
1156
1179
1183
1192
1194 """Updates the internal data with the contents of C{dict['calendar']}"""
1195 if dict.get('calendar_version', None)==2:
1196
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
1213
1214 self._publish_today_events()
1215 self._publish_thisweek_events()
1216 self.RefreshAllEntries()
1217
1235
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
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
1261 database.ensurerecordtype(db_r, calendarobjectfactory)
1262 db_r=database.extractbitpimserials(db_r)
1263 self.mainwindow.database.savemajordict('calendar', db_r)
1264
1265 os.rename(os.path.join(self.thedir, "index.idx"), os.path.join(self.thedir, "index-is-now-in-database.bak"))
1266
1267 cal_dict=self.mainwindow.database.getmajordictvalues('calendar',
1268 calendarobjectfactory)
1269
1270
1271 r={}
1272 for k,e in cal_dict.items():
1273
1274
1275 ce=CalendarEntry()
1276 ce.set_db_dict(e)
1277 r[ce.id]=ce
1278 dict.update({ 'calendar': r })
1279
1280 return dict
1281
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
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
1299 if version==0:
1300 version=1
1301
1302
1303 if version==1:
1304
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
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
1316
1317
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
1372
1374 """ Conver a daily event from v3 to v2 """
1375 rp=e.repeat
1376 if rp.interval==1:
1377
1378 d['repeat']='daily'
1379 elif rp.interval==0:
1380
1381 d['repeat']='monfri'
1382 else:
1383
1384
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
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
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
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
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
1423 dd['pos']=idx
1424 res[idx]=dd
1425 idx+=1
1426 t0+=delta_t
1427 return idx, res
1428
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
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
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
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
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
1518 _one_day=wx.DateSpan(days=1)
1519 _empty_day=['', []]
1521
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
1539
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
1549 break
1550 return r
1552
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
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
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
1584 continue
1585 entries.sort(CalendarEntry.cmp_by_time)
1586 if m!=current_month:
1587
1588 if len(month_events):
1589 a_month.append(month_events)
1590 res.append(a_month)
1591
1592 current_month=m
1593 a_month=['%s %d'%(self._dt_index.GetMonthName(m), y)]
1594 month_events=[]
1595
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
1606 a_month.append(month_events)
1607 res.append(a_month)
1608 return res
1609
1614
1616 if not self._date_changed and \
1617 not self._style_changed and \
1618 self._html is not None:
1619
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
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
1631 print_dict=print_data[print_style][2]()
1632
1633 if self._xcp is None:
1634
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
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
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
1666 self._date_changed=True
1668 self._style_changed=True
1669
1673
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
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
1718
1720
1721
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
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
1734 _row[self._new_index]=0
1735 _row[self._details_index]='No changes'
1736 break
1737 elif _old_event.similar(_entry):
1738
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)
1762
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
1771 if not merge:
1772
1773 return self._replace()
1774 else:
1775
1776 return self._merge()
1777
1778
1779
1788
1789
1790
1791
1793 try:
1794 return self.data[row][col]
1795 except IndexError:
1796 return ''
1798 try:
1799 self.data[row][col] = value
1800 except IndexError:
1801 pass
1802
1803
1804
1805
1807 try:
1808 return self._cols_attrs[col]['label']
1809 except IndexError:
1810 return ''
1812 try:
1813 return self._cols_attrs[col]['readonly']
1814 except IndexError:
1815 return False
1817 try:
1818 return self._cols_attrs[col]['alignment']
1819 except IndexError:
1820 return None
1821
1822
1823
1826
1827
1828
1833
1836 super(MergeDataGrid, self).__init__(parent, -1)
1837 self.SetTable(table, True)
1838
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
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)
1895
1896 return self._grid.GetTable().get(self._merge)
1897