1
2
3
4
5
6
7
8
9
10 """
11 Code to handle Call History data storage and display.
12
13 The format of the Call History is standardized. It is an object with the
14 following attributes:
15
16 folder: string (where this item belongs)
17 datetime: string 'YYYYMMDDThhmmss' or (y,m,d,h,m,s)
18 number: string (the phone number of this call)
19 name: string (optional name associated with this number)
20 duration: int (optional duration of the call in minutes)
21
22 To implement Call History feature for a phone module:
23
24 Add an entry into Profile._supportedsyncs:
25 ('call_history', 'read', None),
26
27 Implement the following method in your Phone class:
28 def getcallhistory(self, result, merge):
29 ...
30 return result
31
32 The result dict key is 'call_history'.
33
34 """
35
36
37 from __future__ import with_statement
38 import copy
39 import sha
40 import time
41
42
43 import wx
44 import wx.lib.scrolledpanel as scrolled
45
46
47 import database
48 import guiwidgets
49 import guihelper
50 import helpids
51 import phonenumber
52 import pubsub
53 import today
54 import widgets
55
56
57 -class CallHistoryDataobject(database.basedataobject):
58 _knownproperties=['folder', 'datetime', 'number', 'name', 'duration' ]
59 _knownlistproperties=database.basedataobject._knownlistproperties.copy()
60 - def __init__(self, data=None):
61 if data is None or not isinstance(data, CallHistoryEntry):
62 return;
63 self.update(data.get_db_dict())
64 callhistoryobjectfactory=database.dataobjectfactory(CallHistoryDataobject)
65
66
68 """convert duration int into an h:mm:ss formatted string"""
69 if duration is None:
70 return ''
71 else:
72 sec=duration%60
73 min=duration/60
74 hr=min/60
75 min=min%60
76 return "%d:%02d:%02d" % (hr, min, sec)
77
78
79 -class CallHistoryEntry(object):
80 Folder_Incoming='Incoming'
81 Folder_Outgoing='Outgoing'
82 Folder_Missed='Missed'
83 Folder_Data='Data'
84 Valid_Folders=(Folder_Incoming, Folder_Outgoing, Folder_Missed, Folder_Data)
85 _folder_key='folder'
86 _datetime_key='datetime'
87 _number_key='number'
88 _name_key='name'
89 _duration_key='duration'
90 _unknown_datetime='YYYY-MM-DD hh:mm:ss'
91 _id_index=0
92 _max_id_index=999
94 self._data={ 'serials': [] }
95 self._create_id()
96
97 - def __eq__(self, rhs):
98 return self.folder==rhs.folder and self.datetime==rhs.datetime and\
99 self.number==rhs.number
100 - def __ne__(self, rhs):
101 return self.folder!=rhs.folder or self.datetime!=rhs.datetime or\
102 self.number!=rhs.number
104 return copy.deepcopy(self._data, {})
106 self._data={}
107 self._data.update(d)
108
109 - def get_db_dict(self):
111 - def set_db_dict(self, d):
113
114 - def _create_id(self):
115 "Create a BitPim serial for this entry"
116 self._data.setdefault("serials", []).append(\
117 {"sourcetype": "bitpim",
118 "id": '%.3f%03d'%(time.time(), CallHistoryEntry._id_index) })
119 if CallHistoryEntry._id_index<CallHistoryEntry._max_id_index:
120 CallHistoryEntry._id_index+=1
121 else:
122 CallHistoryEntry._id_index=0
124 s=self._data.get('serials', [])
125 for n in s:
126 if n.get('sourcetype', None)=='bitpim':
127 return n.get('id', None)
128 return None
129 - def _set_id(self, id):
130 s=self._data.get('serials', [])
131 for n in s:
132 if n.get('sourcetype', None)=='bitpim':
133 n['id']=id
134 return
135 self._data['serials'].append({'sourcetype': 'bitpim', 'id': id } )
136 id=property(fget=_get_id, fset=_set_id)
137
138 - def _set_or_del(self, key, v, v_list=[]):
139 if v is None or v in v_list:
140 if self._data.has_key(key):
141 del self._data[key]
142 else:
143 self._data[key]=v
144
145 - def _get_folder(self):
146 return self._data.get(self._folder_key, '')
147 - def _set_folder(self, v):
148 if v is None:
149 if self._data.has_key(self._folder_key):
150 del self._data[self._folder_key]
151 return
152 if not isinstance(v, (str, unicode)):
153 raise TypeError,'not a string or unicode type'
154 if v not in self.Valid_Folders:
155 raise ValueError,'not a valid folder'
156 self._data[self._folder_key]=v
157 folder=property(fget=_get_folder, fset=_set_folder)
158
159 - def _get_number(self):
160 return self._data.get(self._number_key, '')
161 - def _set_number(self, v):
162 self._set_or_del(self._number_key, v, [''])
163 number=property(fget=_get_number, fset=_set_number)
164
165 - def _get_name(self):
166 return self._data.get(self._name_key, '')
167 - def _set_name(self, v):
168 self._set_or_del(self._name_key, v, ('',))
169 name=property(fget=_get_name, fset=_set_name)
170
171 - def _get_duration(self):
172 return self._data.get(self._duration_key, None)
173 - def _set_duration(self, v):
174 if v is not None and not isinstance(v, int):
175 raise TypeError('duration property is an int arg')
176 self._set_or_del(self._duration_key, v)
179 duration=property(fget=_get_duration, fset=_set_duration)
180 durationstr=property(fget=_get_durationstr)
181
182
183 - def _get_datetime(self):
184 return self._data.get(self._datetime_key, '')
185 - def _set_datetime(self, v):
186
187
188
189 if v is None:
190 if self._data.has_key(self._datetime_key):
191 del self._data[self._datetime_key]
192 return
193 if isinstance(v, (tuple, list)):
194 if len(v)!=6:
195 raise ValueError,'(y, m, d, h, m, s)'
196 s='%04d%02d%02dT%02d%02d%02d'%tuple(v)
197 elif isinstance(v, (str, unicode)):
198
199 if len(v)!=15 or v[8]!='T':
200 raise ValueError,'value must be in format YYYYMMDDThhmmss'
201 s=v
202 else:
203 raise TypeError
204 self._data[self._datetime_key]=s
205 datetime=property(fget=_get_datetime, fset=_set_datetime)
207
208
209 s=self.datetime
210 if not len(s):
211 s=self._unknown_datetime
212 else:
213 s=s[:4]+'-'+s[4:6]+'-'+s[6:8]+' '+s[9:11]+':'+s[11:13]+':'+s[13:]
214 return s
215 - def summary(self, name=None):
216
217
218 s=self.datetime
219 if s:
220 s=s[4:6]+'/'+s[6:8]+' '+s[9:11]+':'+s[11:13]+' '
221 else:
222 s='**/** **:** '
223 if name:
224 s+=name
225 elif self.name:
226 s+=self.name
227 else:
228 s+=phonenumber.format(self.number)
229 return s
230
231
233 _data_key='call_history'
234 stat_list=("Data", "Missed", "Incoming", "Outgoing", "All")
235 - def __init__(self, mainwindow, parent):
236 super(CallHistoryWidget, self).__init__(parent, -1)
237 self._main_window=mainwindow
238 self.call_history_tree_nodes={}
239 self._parent=parent
240 self.read_only=False
241 self.historical_date=None
242 self._data={}
243 self._name_map={}
244 pubsub.subscribe(self._OnPBLookup, pubsub.RESPONSE_PB_LOOKUP)
245 self.list_widget=CallHistoryList(self._main_window, self._parent, self)
246
247 vbs=wx.BoxSizer(wx.VERTICAL)
248
249 hbs=wx.BoxSizer(wx.HORIZONTAL)
250 static_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1,
251 'Historical Data Status:'),
252 wx.VERTICAL)
253 self.historical_data_label=wx.StaticText(self, -1, 'Current Data')
254 static_bs.Add(self.historical_data_label, 1, wx.EXPAND|wx.ALL, 5)
255 hbs.Add(static_bs, 1, wx.EXPAND|wx.ALL, 5)
256 vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
257
258 self.total_calls=today.HyperLinkCtrl(self, -1, ' Total Calls: 0')
259 today.EVT_HYPERLINK_LEFT(self, self.total_calls.GetId(),
260 self.OnNodeSelection)
261 self.total_in=today.HyperLinkCtrl(self, -1, ' Incoming Calls: 0')
262 today.EVT_HYPERLINK_LEFT(self, self.total_in.GetId(),
263 self.OnNodeSelection)
264 self.total_out=today.HyperLinkCtrl(self, -1, ' Outgoing Calls: 0')
265 today.EVT_HYPERLINK_LEFT(self, self.total_out.GetId(),
266 self.OnNodeSelection)
267 self.total_missed=today.HyperLinkCtrl(self, -1, ' Missed Calls: 0')
268 today.EVT_HYPERLINK_LEFT(self, self.total_missed.GetId(),
269 self.OnNodeSelection)
270 self.total_data=today.HyperLinkCtrl(self, -1, ' Data Calls: 0')
271 today.EVT_HYPERLINK_LEFT(self, self.total_data.GetId(),
272 self.OnNodeSelection)
273 self.duration_all=wx.StaticText(self, -1, ' Total Duration(h:m:s): 0')
274 self.duration_in=wx.StaticText(self, -1, ' Incoming Duration(h:m:s): 0')
275 self.duration_out=wx.StaticText(self, -1, ' Outgoing Duration(h:m:s): 0')
276 self.duration_data=wx.StaticText(self, -1, ' Data Duration(h:m:s): 0')
277 self._id_dict={
278 self.total_calls.GetId(): self.stat_list[4],
279 self.total_in.GetId(): self.stat_list[2],
280 self.total_out.GetId(): self.stat_list[3],
281 self.total_missed.GetId(): self.stat_list[1],
282 self.total_data.GetId(): self.stat_list[0],
283 }
284 vbs.Add(wx.StaticText(self, -1, ''), 0, wx.ALIGN_LEFT|wx.ALL, 2)
285 vbs.Add(self.total_calls, 0, wx.ALIGN_LEFT|wx.ALL, 2)
286 vbs.Add(self.total_in, 0, wx.ALIGN_LEFT|wx.ALL, 2)
287 vbs.Add(self.total_out, 0, wx.ALIGN_LEFT|wx.ALL, 2)
288 vbs.Add(self.total_missed, 0, wx.ALIGN_LEFT|wx.ALL, 2)
289 vbs.Add(self.total_data, 0, wx.ALIGN_LEFT|wx.ALL, 2)
290 vbs.Add(wx.StaticText(self, -1, ''), 0, wx.ALIGN_LEFT|wx.ALL, 2)
291 vbs.Add(self.duration_all, 0, wx.ALIGN_LEFT|wx.ALL, 2)
292 vbs.Add(self.duration_in, 0, wx.ALIGN_LEFT|wx.ALL, 2)
293 vbs.Add(self.duration_out, 0, wx.ALIGN_LEFT|wx.ALL, 2)
294 vbs.Add(self.duration_data, 0, wx.ALIGN_LEFT|wx.ALL, 2)
295
296 self.SetSizer(vbs)
297 self.SetAutoLayout(True)
298 vbs.Fit(self)
299 self.SetupScrolling()
300 self.SetBackgroundColour(wx.WHITE)
301
302 self._populate()
303
305
306 _node=self._id_dict.get(evt.GetId(), None)
307 if _node and self.call_history_tree_nodes.get(_node, None):
308 self.ActivateSelf(self.call_history_tree_nodes[_node])
309
311 if self.read_only and not force:
312
313 return
314 self._data=dict.get(self._data_key, {})
315 self._populate()
316
320
326
329
358
372
388
419
421 d=msg.data
422 k=d.get('item', None)
423 name=d.get('name', None)
424 if k is None:
425 return
426 self._name_map[k]=name
427
428 - def getdata(self, dict, want=None):
430
439
441 if self.read_only:
442 wx.MessageBox('You are viewing historical data which cannot be changed or saved',
443 'Cannot Save Call History Data',
444 style=wx.OK|wx.ICON_ERROR)
445 else:
446 self._save_to_db(dict.get(self._data_key, {}))
447 return dict
448
449 - def getfromfs(self, result, timestamp=None):
461
463 if self.read_only:
464 wx.MessageBox('You are viewing historical data which cannot be changed or saved',
465 'Cannot Save Call History Data',
466 style=wx.OK|wx.ICON_ERROR)
467 return
468 d=dict.get(self._data_key, {})
469 l=[e for k,e in self._data.items()]
470 for k,e in d.items():
471 if e not in l:
472 self._data[e.id]=e
473 self._save_to_db(self._data)
474 self._populate()
475
477
478 res={}
479 for sel_idx in self.list_widget._item_list.GetSelections():
480 k=self.list_widget._item_list.GetItemData(sel_idx)
481 if k:
482 res[k]=self._data[k]
483 return res
484
487
488
489 -class CallHistoryList(wx.Panel, widgets.BitPimWidget):
490 _by_type=0
491 _by_date=1
492 _by_number=2
493 - def __init__(self, mainwindow, parent, stats):
494 super(CallHistoryList, self).__init__(parent, -1)
495 self._main_window=mainwindow
496 self._stats=stats
497 self.nodes={}
498 self.nodes_keys={}
499 self._display_filter="All"
500
501 vbs=wx.BoxSizer(wx.VERTICAL)
502
503 hbs=wx.BoxSizer(wx.HORIZONTAL)
504 static_bs=wx.StaticBoxSizer(wx.StaticBox(self, -1,
505 'Historical Data Status:'),
506 wx.VERTICAL)
507 self.historical_data_label=wx.StaticText(self, -1, 'Current Data')
508 static_bs.Add(self.historical_data_label, 1, wx.EXPAND|wx.ALL, 5)
509 hbs.Add(static_bs, 1, wx.EXPAND|wx.ALL, 5)
510 vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
511
512 column_info=[]
513 column_info.append(("Call Type", 80, False))
514 column_info.append(("Date", 120, False))
515 column_info.append(("Number", 100, False))
516 column_info.append(("Duration", 80, False))
517 column_info.append(("Name", 130, False))
518 self._item_list=guiwidgets.BitPimListCtrl(self, column_info)
519 self._item_list.ResetView(self.nodes, self.nodes_keys)
520 vbs.Add(self._item_list, 1, wx.EXPAND|wx.ALL, 5)
521 vbs.Add(wx.StaticText(self, -1, ' Note: Click column headings to sort data'), 0, wx.ALIGN_CENTRE|wx.BOTTOM, 10)
522
523 today.bind_notification_event(self.OnTodaySelectionIncoming,
524 today.Today_Group_IncomingCalls)
525 today.bind_notification_event(self.OnTodaySelectionMissed,
526 today.Today_Group_MissedCalls)
527 self.today_data=None
528 self.SetSizer(vbs)
529 self.SetAutoLayout(True)
530 vbs.Fit(self)
531
532 - def OnSelected(self, node):
533 for stat in self._stats.stat_list:
534 if self._stats.call_history_tree_nodes[stat]==node:
535 if self._display_filter!=stat:
536 self._display_filter=stat
537
538
539 if self._item_list.GetItemCount():
540 item=self._item_list.GetTopItem()
541
542 while item!=-1:
543 self._item_list.Select(item, 0)
544 item=self._item_list.GetNextItem(item)
545 self.populate()
546 self._on_today_selection()
547 return
548
550 node=self._stats.call_history_tree_nodes["Incoming"]
551 self.today_data=evt.data
552 self.ActivateSelf(node)
553
555 node=self._stats.call_history_tree_nodes["Missed"]
556 self.today_data=evt.data
557 self.ActivateSelf(node)
558
560 if self.today_data and self._item_list.GetItemCount():
561 item=self._item_list.GetTopItem()
562 while item!=-1:
563 if self.today_data['id']==self._item_list.GetItemData(item):
564 self._item_list.Select(item, 1)
565 self._item_list.EnsureVisible(item)
566 else:
567 self._item_list.Select(item, 0)
568 item=self._item_list.GetNextItem(item)
569 self.today_data=None
570
572 result=[]
573 result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EDITSELECTALL, "Select All", "Select All Items"))
574 result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EDITDELETEENTRY, "Delete Selected", "Delete Selected Items"))
575 result.append((widgets.BitPimWidget.MENU_SPACER, 0, "", ""))
576 result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_EXPORT_CSV_CALL_HISTORY, "Export to CSV ...", "Export the call history to a csv file"))
577 result.append((widgets.BitPimWidget.MENU_NORMAL, guihelper.ID_DATAHISTORICAL, "Historical Data ...", "Display Historical Data"))
578 return result
579
580 - def CanSelectAll(self):
581 if self._item_list.GetItemCount():
582 return True
583 return False
584
585 - def OnSelectAll(self, _):
586 item=self._item_list.GetTopItem()
587 while item!=-1:
588 self._item_list.Select(item)
589 item=self._item_list.GetNextItem(item)
590
592 return self._stats.HasHistoricalData()
593
595 return self._stats.OnHistoricalData()
596
597 - def populate(self):
598 self.nodes={}
599 self.nodes_keys={}
600 index=0
601 for k,e in self._stats._data.items():
602 if self._display_filter=="All" or e.folder==self._display_filter:
603 name=e.name
604 if name==None or name=="":
605 temp=self._stats._name_map.get(e.number, None)
606 if temp !=None:
607 name=temp
608 else:
609 name=""
610 self.nodes[index]=(e.folder, e.get_date_time_str(),
611 phonenumber.format(e.number),
612 e.durationstr, name)
613 self.nodes_keys[index]=k
614 index+=1
615 self._item_list.ResetView(self.nodes, self.nodes_keys)
616
617 - def CanDelete(self):
618 if self._stats.read_only:
619 return False
620 sels_idx=self._item_list.GetFirstSelected()
621 if sels_idx==-1:
622 return False
623 return True
624
625 - def GetDeleteInfo(self):
626 return wx.ART_DEL_BOOKMARK, "Delete Call Record"
627
628 - def OnDelete(self, _):
629 if self._stats.read_only:
630 return
631 sels_idx=self._item_list.GetSelections()
632 if len(sels_idx):
633
634 for i,item in sels_idx.items():
635 del self._stats._data[self._item_list.GetItemData(item)]
636 self._item_list.Select(item, 0)
637 self.populate()
638 self._stats._save_to_db(self._stats._data)
639 self._stats.CalculateStats()
640 - def GetHelpID(self):
642