PyXR

c:\projects\bitpim\src \ bpcalendar.py



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