| Trees | Indices | Help |
|
|---|
|
|
1 ### BITPIM
2 ###
3 ### Copyright (C) 2005 Joe Pham <djpham@netzero.net>
4 ###
5 ### This program is free software; you can redistribute it and/or modify
6 ### it under the terms of the BitPim license as detailed in the LICENSE file.
7 ###
8 ### $Id: todo.py 4459 2007-11-24 07:55:33Z djpham $
9
10 """
11 Code to handle Todo Items
12
13 The format for the Todo items is standardized. It is a dict with the following
14 fields:
15
16 TodoEntry properties:
17 summary - 'string subject'
18 note - 'string note'
19 due_date - 'YYYYMMDD'
20 status - (None, NotStarted, InProgress, NeedActions, Completed, Cancelled)
21 percent_complete - None, range(101)
22 completion_date - 'YYYYMMDD'
23 categories - [{ 'category': string }]
24 private - True/<False|None>
25 priority - range(1, 11) 1=Highest, 5=Normal, 10=Lowest
26
27 TodoEntry Methods:
28 get() - return a copy of the internal dict
29 set(dict) - set the internal dict
30 check_completion() - check the task for completion and if so set appropriate
31 values
32 completion() - set the task as completed and set appropriate values
33
34 To implement Todo read/write for a phone module:
35 Add 2 entries into Profile._supportedsyncs:
36 ...
37 ('todo', 'read', None), # all todo reading
38 ('todo', 'write', 'OVERWRITE') # all todo writing
39
40 implement the following 2 methods in your Phone class:
41 def gettodo(self, result):
42 ...
43 return result
44
45 def savetodo(self, result, merge):
46 ...
47 return result
48
49 The result dict key is 'todo'.
50
51 """
52
53 # standard modules
54 from __future__ import with_statement
55 import copy
56 import datetime
57 import time
58
59 # wx modules
60 import wx
61 import wx.lib.calendar
62 import wx.calendar as cal
63 import wx.lib.scrolledpanel as scrolled
64
65 # BitPim modules
66 import calendarentryeditor as cal_editor
67 import database
68 import guihelper
69 import helpids
70 import field_color
71 import phonebookentryeditor as pb_editor
72 import pubsub
73 import today
74 import guihelper
75 import widgets
76
77 widgets_list=[]
78
79 #-------------------------------------------------------------------------------
81 _knownproperties=['summary', 'note', 'due_date', 'status',
82 'percent_complete', 'completion_date', 'priority' ]
83 _knownlistproperties=database.basedataobject._knownlistproperties.copy()
84 _knownlistproperties.update( {'categories': ['category'],
85 'flags': ['secret'] })
86
91 todoobjectfactory=database.dataobjectfactory(TodoDataObject)
92
93 #-------------------------------------------------------------------------------
95 ST_NotStarted=1
96 ST_InProgress=2
97 ST_NeedActions=3
98 ST_Completed=4
99 ST_Cancelled=5
100 ST_Last=6
101 ST_Range=xrange(ST_NotStarted, ST_Last)
102 ST_Names=(
103 '<None>', 'Not Started', 'In Progess', 'Need Actions',
104 'Completed', 'Cancelled', 'LAST')
105 PC_Range=xrange(101) # % Complete: 0-100%
106 PR_Range=xrange(1, 11)
107 _id_index=0
108 _max_id_index=999
109
113
115 return copy.deepcopy(self._data, {})
119
121 return self.get()
123 self.set(d)
124
126 # complete this task: set relevant values to indicate so
127 if self.status != self.ST_Completed:
128 self.status=self.ST_Completed
129 if self.percent_complete != 100:
130 self.percent_complete=100
131 if not len(self.completion_date):
132 self.completion_date=datetime.date.today().strftime('%Y%m%d')
133
135 if self.status==self.ST_Completed or self.percent_complete==100 or \
136 len(self.completion_date):
137 self.complete()
138
140 if v is None or v in v_list:
141 if self._data.has_key(key):
142 del self._data[key]
143 else:
144 self._data[key]=v
145
147 "Create a BitPim serial for this entry"
148 self._data.setdefault("serials", []).append(\
149 {"sourcetype": "bitpim",
150 "id": '%.3f%03d'%(time.time(), TodoEntry._id_index) })
151 if TodoEntry._id_index<TodoEntry._max_id_index:
152 TodoEntry._id_index+=1
153 else:
154 TodoEntry._id_index=0
156 s=self._data.get('serials', [])
157 for n in s:
158 if n.get('sourcetype', None)=='bitpim':
159 return n.get('id', None)
160 return None
161 id=property(fget=_get_id)
162
164 return self._data.get('summary', '')
166 self._set_or_del('summary', v, [''])
167 summary=property(fget=_get_summary, fset=_set_summary)
168
170 return self._data.get('note', '')
172 self._set_or_del('note', v, [''])
173 note=property(fget=_get_note, fset=_set_note)
174
176 return self._data.get('due_date', '')
178 self._set_or_del('due_date', v, [''])
179 due_date=property(fget=_get_due_date, fset=_set_due_date)
180
182 return self._data.get('status', None)
184 if v is not None and v not in self.ST_Range:
185 raise ValueError, 'Illegal Status Value'
186 self._set_or_del('status', v, [])
187 if v==self.ST_Completed:
188 self.complete()
189 status=property(fget=_get_status, fset=_set_status)
193
195 return self._data.get('percent_complete', None)
197 if v is not None and v not in self.PC_Range:
198 raise ValueError, 'Illegal Percent Complete Value'
199 self._set_or_del('percent_complete', v, [])
200 if v==100:
201 self.complete()
202 percent_complete=property(fget=_get_percent_complete,
203 fset=_set_percent_complete)
204
206 return self._data.get('completion_date', '')
208 self._set_or_del('completion_date', v, [''])
209 if v is not None and len(v):
210 self.complete()
211 completion_date=property(fget=_get_completion_date,
212 fset=_set_completion_date)
213
215 return self._data.get('priority', None)
217 if v is not None and v not in self.PR_Range:
218 raise ValueError, 'Illegal priority value'
219 self._set_or_del('priority', v, [])
220 priority=property(fget=_get_priority, fset=_set_priority)
221
223 return self._data.get('categories', [])
225 self._set_or_del('categories', s,[])
226 if not s and self._data.has_key('categories'):
227 del self._data['categories']
228 categories=property(fget=_get_categories, fset=_set_categories)
229
231 f=self._data.get('flags', [])
232 for n in f:
233 if n.has_key('secret'):
234 return n['secret']
235 return False
237 f=self._data.get('flags', [])
238 for i, n in enumerate(f):
239 if n.has_key('secret'):
240 if v is None or not v:
241 del f[i]
242 if not self._data['flags']:
243 del self._data['flags']
244 else:
245 n['secret']=v
246 return
247 if v is not None and v:
248 self._data.setdefault('flags', []).append({'secret': v})
249 private=property(fget=_get_secret, fset=_set_secret)
250
251 #-------------------------------------------------------------------------------
254 self._choices=[TodoEntry.ST_Names[x] for x in range(TodoEntry.ST_Last)]
255 super(StatusComboBox, self).__init__(parent, -1,
256 self._choices[0],
257 (-1, -1), (-1, -1),
258 self._choices, wx.CB_READONLY)
259 wx.EVT_COMBOBOX(self, self.GetId(), parent.OnMakeDirty)
261 s=super(StatusComboBox, self).GetValue()
262 for v,n in enumerate(self._choices):
263 if n==s:
264 break;
265 if v:
266 return v
267 else:
268 return None
273
274 #-------------------------------------------------------------------------------
277 self. _choices=['<None>', '0%', '10%', '20%', '30%', '40%',
278 '50%', '60%', '70%', '80%', '90%', '100%']
279 super(PercentCompleteBox, self).__init__(parent, -1, self._choices[0],
280 (-1,-1), (-1,-1),
281 self._choices, wx.CB_READONLY)
282 wx.EVT_COMBOBOX(self, self.GetId(), parent.OnMakeDirty)
284 s=super(PercentCompleteBox, self).GetValue()
285 for v,n in enumerate(self._choices):
286 if n==s:
287 break
288 if v:
289 return (v-1)*10
290 else:
291 return None
293 if v is None:
294 v=0
295 else:
296 v=(v/10)+1
297 super(PercentCompleteBox, self).SetValue(self._choices[v])
298
299 #-------------------------------------------------------------------------------
302 self._choices=['<None>', '1 - Highest', '2', '3', '4', '5 - Normal',
303 '6', '7', '8', '9', '10 - Lowest']
304 super(PriorityBox, self).__init__(parent, -1, self._choices[0],
305 (-1, -1), (-1, -1),
306 self._choices, wx.CB_READONLY)
307 wx.EVT_COMBOBOX(self, self.GetId(), parent.OnMakeDirty)
309 s=super(PriorityBox, self).GetValue()
310 for v,n in enumerate(self._choices):
311 if n==s:
312 break
313 if v:
314 return v
315 else:
316 return None
321
322 #-------------------------------------------------------------------------------
325 super(DateControl, self).__init__(parent, -1)
326 self._dt=None
327 # main box sizer, a label, and a button
328 self._hs=wx.BoxSizer(wx.HORIZONTAL)
329 self._date_str=wx.StaticText(self, -1, '<None>')
330 self._date_btn=wx.Button(self, -1, 'Set Date')
331 self._hs.Add(self._date_str, 0, wx.EXPAND|wx.LEFT|wx.RIGHT, 5)
332 self._hs.Add(self._date_btn, 0, wx.EXPAND|wx.LEFT|wx.RIGHT, 5)
333 # events
334 wx.EVT_BUTTON(self, self._date_btn.GetId(), self._OnSetDate)
335 # all done
336 self.SetSizer(self._hs)
337 self.SetAutoLayout(True)
339 if self._dt is None:
340 s='<None>'
341 else :
342 s=self._dt.strftime('%Y-%m-%d')
343 self._date_str.SetLabel(s)
344 self._hs.Layout()
345 self.GetParent().OnMakeDirty(None)
347 # bring up a calendar dlg
348 if self._dt is None:
349 dt=datetime.date.today()
350 else:
351 dt=self._dt
352 with guihelper.WXDialogWrapper(wx.lib.calendar.CalenDlg(self,
353 month=dt.month,
354 day=dt.day,
355 year=dt.year)) as dlg:
356 dlg.Centre()
357 if dlg.ShowModal() == wx.ID_OK:
358 self._dt=datetime.date(dlg.calend.GetYear(),
359 dlg.calend.GetMonth(),
360 dlg.calend.GetDay())
361 self._refresh()
363 # set a date string from the dict
364 if v is None or not len(v):
365 self._dt=None
366 else:
367 self._dt=datetime.date(int(v[:4]), int(v[4:6]), int(v[6:]))
368 self._refresh()
374
375 #-------------------------------------------------------------------------------
378 super(DirtyCheckBox, self).__init__(parent, -1)
379 wx.EVT_CHECKBOX(self, self.GetId(), parent.OnMakeDirty)
380
381 #-------------------------------------------------------------------------------
383 _dict_key_index=0
384 _label_index=1
385 _class_index=2
386 _get_index=3
387 _set_index=4
388 _w_index=5
389 _flg_index=6
390
392 global widgets_list
393
394 super(GeneralEditor, self).__init__(parent)
395 self._fields=[
396 ['summary', 'Summary:', cal_editor.DVTextControl, None, None, None, wx.EXPAND],
397 ['status', 'Status:', StatusComboBox, None, None, None, 0],
398 ['due_date', 'Due Date:', DateControl, None, None, None, wx.EXPAND],
399 ['percent_complete', '% Complete:', PercentCompleteBox, None, None, None, 0],
400 ['completion_date', 'Completion Date:', DateControl, None, None, None, wx.EXPAND],
401 ['private', 'Private:', DirtyCheckBox, None, None, None, 0],
402 ['priority', 'Priority:', PriorityBox, None, None, None, 0]
403 ]
404 gs=wx.FlexGridSizer(-1, 2, 5, 5)
405 gs.AddGrowableCol(1)
406 for n in self._fields:
407 _txt=wx.StaticText(self, -1, n[self._label_index],
408 style=wx.ALIGN_LEFT)
409 widgets_list.append((_txt, n[self._dict_key_index]))
410 gs.Add(_txt, 0, wx.EXPAND|wx.BOTTOM, 0)
411 w=n[self._class_index](self, -1)
412 gs.Add(w, 0, n[self._flg_index]|wx.BOTTOM, 5)
413 n[self._w_index]=w
414 # event handlers
415 # all done
416 self.SetSizer(gs)
417 self.SetAutoLayout(True)
418 gs.Fit(self)
419
421 self.OnDirtyUI(evt)
422
424 self.ignore_dirty=True
425 if data is None:
426 for n in self._fields:
427 n[self._w_index].Enable(False)
428 else:
429 for n in self._fields:
430 w=n[self._w_index]
431 w.Enable(True)
432 w.SetValue(getattr(data, n[self._dict_key_index]))
433 self.ignore_dirty=self.dirty=False
434
444
445 #-------------------------------------------------------------------------------
447 color_field_name='todo'
448
450 global widgets_list
451
452 super(TodoWidget, self).__init__(parent, -1)
453 self._main_window=mainwindow
454 self._data=self._data_map={}
455 # main box sizer
456 vbs=wx.BoxSizer(wx.VERTICAL)
457 # horizontal sizer for the listbox and tabs
458 hbs=wx.BoxSizer(wx.HORIZONTAL)
459 # the list box
460 self._item_list=wx.ListBox(self, wx.NewId(),
461 style=wx.LB_SINGLE|wx.LB_HSCROLL|wx.LB_NEEDED_SB)
462 hbs.Add(self._item_list, 1, wx.EXPAND|wx.BOTTOM, border=5)
463 # the detailed info pane as a scrolled panel
464 scrolled_panel=scrolled.ScrolledPanel(self, -1)
465 vbs1=wx.BoxSizer(wx.VERTICAL)
466 self._items=(
467 (GeneralEditor, 0, None),
468 (cal_editor.CategoryEditor, 1, 'category'),
469 (pb_editor.MemoEditor, 1, 'memo')
470 )
471 self._w=[]
472 for n in self._items:
473 w=n[0](scrolled_panel, -1)
474 vbs1.Add(w, n[1], wx.EXPAND|wx.ALL, 5)
475 self._w.append(w)
476 if n[2]:
477 widgets_list.append((w.static_box, n[2]))
478 scrolled_panel.SetSizer(vbs1)
479 scrolled_panel.SetAutoLayout(True)
480 vbs1.Fit(scrolled_panel)
481 scrolled_panel.SetupScrolling()
482 hbs.Add(scrolled_panel, 3, wx.EXPAND|wx.ALL, border=5)
483 # save references to the widgets
484 self._general_editor_w=self._w[0]
485 self._cat_editor_w=self._w[1]
486 self._memo_editor_w=self._w[2]
487 # the bottom buttons
488 hbs1=wx.BoxSizer(wx.HORIZONTAL)
489 self._save_btn=wx.Button(self, wx.ID_SAVE)
490 self._revert_btn=wx.Button(self, wx.ID_REVERT_TO_SAVED)
491 help_btn=wx.Button(self, wx.ID_HELP)
492 hbs1.Add(self._save_btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
493 hbs1.Add(help_btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
494 hbs1.Add(self._revert_btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
495 # all done
496 vbs.Add(hbs, 1, wx.EXPAND|wx.ALL, 5)
497 vbs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5)
498 vbs.Add(hbs1, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
499 self.SetSizer(vbs)
500 self.SetAutoLayout(True)
501 vbs.Fit(self)
502 # event handlers
503 wx.EVT_LISTBOX(self, self._item_list.GetId(), self._OnListBoxItem)
504 wx.EVT_BUTTON(self, self._save_btn.GetId(), self._OnSave)
505 wx.EVT_BUTTON(self, self._revert_btn.GetId(), self._OnRevert)
506 wx.EVT_BUTTON(self, wx.ID_HELP,
507 lambda _: wx.GetApp().displayhelpid(helpids.ID_TAB_TODO))
508 # DIRTY UI Event handlers
509 for w in self._w:
510 pb_editor.EVT_DIRTY_UI(self, w.GetId(), self.OnMakeDirty)
511 # populate data
512 self._populate()
513 # turn on dirty flag
514 self.ignoredirty=False
515 self.setdirty(False)
516 # register for Today selection
517 today.bind_notification_event(self.OnTodaySelection,
518 today.Today_Group_Todo)
519 today.bind_request_event(self.OnTodayRequest)
520 # color coded labels
521 field_color.reload_color_info(self, widgets_list)
522 pubsub.subscribe(self.OnPhoneChanged, pubsub.PHONE_MODEL_CHANGED)
523
525 # just reload the color info based on the new phone
526 field_color.reload_color_info(self, widgets_list)
527 self.Refresh()
528
532
538
540 now=datetime.datetime.now()
541 _today='%04d%02d%02d'%(now.year, now.month, now.day)
542 keys=self._data.keys()
543 keys.sort()
544 today_event=today.TodayTodoEvent()
545 for k in keys:
546 if self._data[k].is_active() and \
547 (not self._data[k].due_date or \
548 self._data[k].due_date<=_today):
549 today_event.append(self._data[k].summary,
550 { 'key': k,
551 'index': self._data_map[k] })
552 today_event.broadcast()
553
555 now=datetime.datetime.now()
556 _today='%04d%02d%02d'%(now.year, now.month, now.day)
557 s=now+datetime.timedelta(7-now.isoweekday()%7)
558 _sun='%04d%02d%02d'%(s.year, s.month, s.day)
559 keys=self._data.keys()
560 keys.sort()
561 today_event=today.ThisWeekTodoEvent()
562 dow_flg=[False]*7
563 for k in keys:
564 due_date=self._data[k].due_date
565 if due_date>_today and due_date<_sun:
566 dt=datetime.datetime(int(due_date[:4]), int(due_date[4:6]),
567 int(due_date[6:8]))
568 _dow=dt.isoweekday()%7
569 if dow_flg[_dow]:
570 _name=today.dow_initials[-1]+' '+self._data[k].summary
571 else:
572 dow_flg[_dow]=True
573 _name=today.dow_initials[_dow]+' - '+self._data[k].summary
574 today_event.append(_name, { 'key': k,
575 'index': self._data_map[k] })
576 today_event.broadcast()
577
581
583 self.ActivateSelf()
584 if evt.data:
585 self._item_list.SetSelection(evt.data.get('index', wx.NOT_FOUND))
586 self._populate_each(evt.data.get('key', None))
587
589 # populate new data
590 self._clear()
591 self._data_map={}
592 # populate the list with data
593 keys=self._data.keys()
594 keys.sort()
595 for k in keys:
596 n=self._data[k]
597 i=self._item_list.Append(n.summary)
598 self._item_list.SetClientData(i, k)
599 self._data_map[k]=i
600 self._publish_today_events()
601 self._publish_thisweek_events()
602
604 # populate the detailed info of the item keyed k
605 if k is None:
606 # clear out all the subfields
607 self._clear_each()
608 return
609 # there're data, first enable the widgets
610 self.ignoredirty=True
611 for w in self._w:
612 w.Enable(True)
613 entry=self._data[k]
614 # set the general detail
615 self._general_editor_w.Set(entry)
616 self._cat_editor_w.Set(entry.categories)
617 self._memo_editor_w.Set({ 'memo': entry.note })
618 self.ignoredirty=False
619 self.setdirty(False)
620
621 # called from various widget update callbacks
623 """A public function you can call that will set the dirty flag"""
624 if self.dirty or self.ignoredirty or not self.IsShown():
625 # already dirty, no need to make it worse
626 return
627 self.setdirty(True)
628
630 """Set the dirty flag"""
631 if self.ignoredirty:
632 return
633 self.dirty=val
634 self._item_list.Enable(not self.dirty)
635 self._save_btn.Enable(self.dirty)
636 self._revert_btn.Enable(self.dirty)
637
640
643
648
650 # add a new memo item
651 if self.dirty:
652 # busy editing, cannot add now, just return
653 return
654 m=TodoEntry()
655 m.summary='New Task'
656 self._data[m.id]=m
657 self._populate()
658 self._save_to_db(self._data)
659 self._item_list.Select(self._data_map[m.id])
660 self._populate_each(m.id)
661
663 sel_idx=self._item_list.GetSelection()
664 if sel_idx is None or sel_idx==-1:
665 # none selected
666 return False
667 return True
668
670 # delete the current selected item
671 sel_idx=self._item_list.GetSelection()
672 if sel_idx is None or sel_idx==-1:
673 # none selected
674 return
675 self.ignoredirty=True
676 k=self._item_list.GetClientData(sel_idx)
677 self._item_list.Delete(sel_idx)
678 self._clear_each()
679 del self._data[k]
680 del self._data_map[k]
681 self._save_to_db(self._data)
682 self.ignoredirty=False
683 self.setdirty(False)
684
688
692
694 db_rr={}
695 for k, e in todo_dict.items():
696 db_rr[k]=TodoDataObject(e)
697 database.ensurerecordtype(db_rr, todoobjectfactory)
698 self._main_window.database.savemajordict('todo', db_rr)
699 self._publish_today_events()
700 self._publish_thisweek_events()
701
705
707 # read data from the database
708 todo_dict=self._main_window.database.\
709 getmajordictvalues('todo',todoobjectfactory)
710 r={}
711 for k,e in todo_dict.items():
712 ce=TodoEntry()
713 ce.set_db_dict(e)
714 r[ce.id]=ce
715 result.update({ 'todo': r })
716 return result
717
719 # an item was clicked on/selected
720 self._populate_each(self._item_list.GetClientData(evt.GetInt()))
721 self.Refresh()
722
724 # save the current changes
725 self.ignoredirty=True
726 sel_idx=self._item_list.GetSelection()
727 k=self._item_list.GetClientData(sel_idx)
728 entry=self._data[k]
729 self._general_editor_w.Get(entry)
730 entry.note=self._memo_editor_w.Get().get('memo', None)
731 entry.categories=self._cat_editor_w.Get()
732 entry.check_completion()
733 self._general_editor_w.Set(entry)
734 self._item_list.SetString(sel_idx, entry.summary)
735 self._save_to_db(self._data)
736 self.ignoredirty=False
737 self.setdirty(False)
738
740 self.ignoredirty=True
741 self._item_list.Enable()
742 sel_idx=self._item_list.GetSelection()
743 if sel_idx!=wx.NOT_FOUND:
744 k=self._item_list.GetClientData(sel_idx)
745 self._populate_each(k)
746 self.ignoredirty=False
747 self.setdirty(False)
748
749 #-------------------------------------------------------------------------------
750
| Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Sun Jan 24 16:24:09 2010 | http://epydoc.sourceforge.net |