ListManager Documentation

Author: Steven Zatz
Contact: slzatz@hotmail.com
Date: 2003/12/24
Status: This is a "work in progress"
Revision: 1.02
Copyright: Application and documentation use the Python license which is compatible with the GPL.

This is experimental documentation of a program called ListManager, written in Python and wxPython using Leo to create both the application code and the associated reST documentation.

ListManager is an application that allows a group of people working on a joint project to maintain a common list of todos and related items that have owners, due dates and associated notes. The application uses mysql as its database for group use and also uses sqlite for locally resident databases for personal lists. It works in conjunction with Outlook to allow email messages to be sent to ListManager for inclusion in lists and uses Outlook to mail messages to users.

Table of Contents

wxListManager.py

code:

1  @language python

Initial stuff

comments:

Nothing unusual in what follows: we start with the module imports, setting some global constants including Menu Ids and read the ListManager.ini file.

Module Imports

code:

 1  from wxPython.wx import *
 
2  from wxPython.lib.mixins.listctrl import wxListCtrlAutoWidthMixin
 
3  
 
4  import os
 
5  import time
 
6  import pickle
 
7  import socket
 
8  import select
 
9  import random
10  import ConfigParser
11  import threading
12  import re
13  import sys
14  
15  from pywintypes import CreateGuid
16  from win32com.client import Dispatch
17  #import win32pdh
18  import win32api
19  #from win32com.client import constants #--> just needed two constants...
20  
21  import MySQLdb
22  import sqlite
23  import mx.DateTime
24  
25  from LMDialogs import CalendarDialog, ModifierDialog, TicklerDialog, MailDialog,LoggerDialog, FinishedDialog, FindDialog, EvalDialog, TreeDialog, StartupDialog
26  #from wxTreeCtrl import TreeDialog
27  
28  from printout import PrintTable

comments:

os
uses os.getcwd, os.path.split, os.chdir, os.path.join, os.path.getmtime, os.startfile, os.environ
time
uses time.sleep, time.asctime
pickle
used to serialize data that is moved from Outlook to ListManager via sockets.
socket
as noted above, a socket is opened between Outlook and ListManager to move messages back and forth
select
ListManager selects on the socket to see if there is a message that has been queued by Outlook
random
used by the reminder popup to select messages
ConfigParser
not surprisingly, using ConfiParser to parse the ListManager.ini file.
threading
more for fun than absolute necessity, a thread is opened on starting the program that constructs the list of owners for items. In theory, if the datasize and number of Lists were large enough it could delay the appearance of the GUI and its initial responsiveness if we didn't construct the ownerlist in a thread. On the other hand, it really let me play with threads and with creating a custom event that signalled the construction of the owner list to the main thread by posting a custom event.
re
mainly using re.sub('[\\/:*"<>|\?]','-',f) to make sure that files are constructed only with legal characters. Also searching the body text of nodes using re because it allows case insensitive searches through re.compile(pat, re.I).
pywintypes.CreateGuid
probably should use pure python GUID that is in ASPN cookbook but it was easiest to just use the Windows GUID function. Thank you Mark Hammond for win32all.
win32com.client.Dispatch
used when launching Outlook to send email messages
win32api
using win32api.GetUserName() in case there is no user name in the ini file or no ini file
MySQLdb
using Andy Dustman's python extension module to connect to mysql back-end.
sqlite
using D. Richard Hipp's python extension to connect to local sqlite databases
import mx.DateTime
using Marc-André Lemburg's mx.DateTime for dealing with datetime stuff in the databases
CalendarDialog, ModifierDialog, TicklerDialog, MailDialog,LoggerDialog, FinishedDialog, FindDialog, EvalDialog, TreeDialog, StartupDialog
should just import LMDialogs and then access each dialog class by LM.WhateverDialog
printout.PrintTable
There was an existing wxPython print module for printing from tables that I have modified to print Lists.
#from win32com.client import constants
probably not wise but since the app only needs two constants from this module, just set the directly. If MSFT decides to change the api, this is not good.

Constants

code:

 1  cwd = os.getcwd()
 
2  DIRECTORY = os.path.split(cwd)[0]
 
3  os.chdir(DIRECTORY)
 
4  del cwd
 
5  
 
6  #Outlook Constants
 
7  olMailItem = 0x0
 
8  olFlagMarked = 0x2
 
9  
10  OFFLINE_ONLY = False #False-> Online only  ; True-> Online and Offline possible; REMOTE_HOST = None -> Offline only
11  
12  VERSION = '1.02'

comments:

The following two global constants are needed to create emails through Outlook via COM:

olMailItem = 0x0
olFlagMarked = 0x2

For some reason, it seemed easier to just include them explicitly rather than worrying about generating all the Outlook constants in order to use early binding. I supppose if MSFT changes the api, that would be a problem.

Read Config File

code:

 1  config_file = os.path.join(DIRECTORY, "List Manager.ini")
 
2  defaults = dict(pw='python', db='listmanager', ext='txt', local='wxLMDB:sqlite', x='700', y='400')
 
3  cp = ConfigParser.ConfigParser(defaults=defaults)
 
4  cp.read(config_file) #ConfigParser closes the file
 
5  
 
6  USER = cp.has_option('User','user') and cp.get('User','user') or win32api.GetUserName()
 
7  
 
8  # the following all have default values provided in the constructor
 
9  PW = cp.get('User','pw')
10  DB = cp.get('Database','db')
11  NOTE_EXT = cp.get('Note','ext')
12  LOCAL_HOST = cp.get('Hosts','local')
13  X = cp.getint('Configuration','x')
14  Y = cp.getint('Configuration','y')
15  
16  # the folloowing default to None
17  MAIL_LIST_PATH = cp.has_option('Mail','path') and cp.get('Mail','path') or None
18  QUICK_LIST = cp.has_option('User','quicklist') and cp.get('User','quicklist') or None
19  
20  # the following default to False
21  STARTUP_DIALOG = cp.has_option('User','startup_dialog') and cp.getboolean('User','startup_dialog')
22  DELETE_LIST = cp.has_option('User','delete_list') and cp.getboolean('User','delete_list')
23  OUTLOOK = cp.has_option('Mail','outlook') and cp.getboolean('Mail','outlook')
24  
25  if cp.has_option('Hosts','remote'):
26      REMOTE_HOST = cp.get('Hosts','remote')
27  else:
28      REMOTE_HOST = None
29      OFFLINE_ONLY = True
30      
31  # reading it again because of the way defaults are handled
32  cp = ConfigParser.ConfigParser()
33  cp.read(config_file) #ConfigParser closes the file
34  
35  if cp.has_section('Synchronization'):
36      SYNC_TABLES = [t[1] for t in cp.items('Synchronization')]
37  else:
38      SYNC_TABLES = ['follow_ups']
39  

comments:

Application uses the ConfigParser module ito parse the ini file. Unfortunately, ConfigParser doesn't work exactly like I think it should although it has been improved in 2.3. My main issue is in the handling of default options. The default options specified through the constructor show up in every section. For example, if you use the items(section) method then in addition to returning a list of tuples with whatever option/value pairs exist in the section, the list will include all the default option/value pairs, which does not make a whole lot of sense to me. At the least, there should be a 'nodefaults' argument whose default was False but which could be set to True. The following methods should have this option:

  • items
  • options
  • has_option

In any event, because a nodefaults option does not exist, I create the ConfigParser object twice -- once with default options and once without them.

The application will work fine if there is no ini file. In an effort to save some typing but not be too obscure, many of the options are read such that they default to the correct value either through explicit defaults in the constructor or statements that evaluate to None or False.

QUICK_LIST = cp.has_option('User','quicklist') and cp.get('User','quicklist') or None

OUTLOOK = cp.has_option('Mail','outlook') and cp.getboolean('Mail,'outlook')

class ListManager

code:

1  class ListManager(wxFrame):

comments:

ListManager is the main class in the application and is a sublass of wxFrame, which is typical for a wxPython application. From a GUI standpoint, the main child window of the ListManager object is a wxNoteBook object that holds one wxListCtrl per notebook page and one wxListBox. The wxListCtrls display item information (e.g., name of the item, owners of the item, etc.) for a particular List and the wxListBoxes displays a list of owners that is used to filter the items displayed by the wxListCtrl object.

Each wxListCtrl object has its own set of events that it is hooked to (see CreateNewNotebookPage`<< ListControl Events >>`_.

Instantiation

comments:

def __init__

code:

 1  def __init__(self, parent, id, title, size):
 
2      wxFrame.__init__(self, parent, id, title, size = size)
 
3  
 
4      self.SetIcon(wxIcon('bitmaps//wxpdemo.ico', wxBITMAP_TYPE_ICO))
 
5      self.CreateStatusBar()
 
6    
 
7      << ListManager Attributes >>
 
8      << Menu Setup >>
 
9      << Toolbar Setup >>
10      << Menu/Toolbar Events >>
11      << Create Controls>>
12      << Layout Stuff >>
13      << Other Events >>
14      << GUI Instance Objects >>
15      << Create Socket >>
16      << Load Recent Files >>
17      << Idle Timer >>
18      
19      ownerthread = threading.Thread(target=self.createownerlist)
20      ownerthread.start()
21      self.ModifierDialog = None
22  

comments:

The ListManager __init__ method is pretty straightforward. The __init__ arguments are the ones that need to be passed to wxFrame __init__ method. The wxFrame class has the following form:

wxFrame(parent, id, title, pos=wxDefaultPosition, size=wxDefaultSize, style=wxDEFAULT_FRAME_STYLE, name="frame")

The default style (wxDEFAULT_FRAME_STYLE) includes wxMINIMIZE_BOX, wxMAXIMIZE_BOX, wxRESIZE_BORDER, wxSYSTEM_MENU, wxCAPTION (the latter is the text that appears in the title bar).

SetIcon is a method of wxFrame that sets the icon in the upper left of the title bar of the frame. The wxIcon class has the following form:

wxIcon(filename, type, desiredWidth=-1, desiredHeight=-1)

CreateStatusBar is a method of wxFrame. The wxPython form is:

CreateStatusBar(number=1, style=0, id=-1)
number -->
number of fields to create. Specify a value greater than 1 to create a multi-field status bar.

CreateStatusBar needs to be called before << Load Recent Files >>.

The various sections of __init__ are explained in their corresponding section:

<< ListManager Attributes >>
<< Menu Setup >>
<< Toolbar Setup >>
<< Menu/Toolbar Events >>
<< Create Controls>>
<< Layout Stuff >>
<< Other Events >>
<< GUI Instance Objects >>
<< Create Socket >>
<< Load Recent Files >>
<< List Manager Attributes >>

code:

 1  self.PropertyDicts = []
 
2  self.ItemLists = []
 
3  self.ListCtrls = []
 
4  self.OwnerLBoxes = []
 
5  
 
6  self.L = -1
 
7  self.curIdx = -1
 
8  
 
9  self.printdata = wxPrintData()
10  self.printdata.SetPaperId(wxPAPER_LETTER)
11  self.printdata.SetOrientation(wxPORTRAIT)
12  
13  #self._options = {} #would be used in loadconfig
14  
15  self.copyitems = []    
16  self.modified = {}
17  self.tickler_active = False
18  
19  #there is a wxPanel in the AddListControl method so each wxListCtrl has a different panel as parent
20  #there is a nb_sizer = wxNotebookSizer(nb) class but doesn't seem to make any difference
21  
22  self.editor = []
23  
24  self.Cursors = {}
25  self.sqlite_connections = []
26  self.popupvisible = False
27  self.in_place_editor = None
28  self.showrecentcompleted = 0
29  
30  self.LC_font = wxFont(9, wxSWISS, wxNORMAL, wxNORMAL)
31  
32  self.date_titles = {'createdate':"Create Date",'duedate':"Due Date",'timestamp':"Last Modified",'finisheddate':"Completion Date"}
33  self.attr2col_num = {'priority':0, 'name':1,'owners':2, 'date':3}
34  
35  self.FindDialog = FindDialog(self, "Find...", "")
36  self.EvalDialog = EvalDialog(self, "Evaluate...", "")

comments:

self.PropertyDicts
list of dictionaries that describe properties of each ListManager List (note that when referring to a collection of ListManager items a capital L List and table are used interchangeably).
self.ItemLists
list of lists that consist of instance objects of class Item. Each of the lists contained in self.ItemLists correspond to the items that are being displayed in the ListCtrl. So self.Itemlist[2] corresponds to the 2nd tab of the notebook and to the items in self.ListCtrls[2].

The class Item is just an empty class being used as a convenience to hold item attributes:

class Item:
    pass

The purpose of the class is just to create an object that can have various attributes as follows:

item.id GUID
item.name string that describes the item
item.priority integer ranging from 1 (high) to 3 (low)
item.owners list of the form ["Zatz, Steve", "Hoffman, Steve"]
item.note string that provides additional info on item
item.timestamp timestamp indicating when an item was last modified
item.duedate default is None; mx.DateTime date
item.createdate mx.DateTime.now() mx.DateTime timestamp
item.finisheddate efaut is None; mx.DateTime date
self.ListCtrls
list of of instance objects of class ListCtrls, which are a subclass of wxPython class wxListCtrl.
self.OwnerLBoxes
list of of instance objects of wxPython class wxListBox, which is a simple one column List Control.

The wxPython constructor for a wxListBox is:

wxListBox(parent, id, pos=wxDefaultPosition, size=wxDefaultSize, choices=[], style=0)
self.L
index of the currently active notebook tab. If there are any tabs in the notebook then one of them is always selected. If there are no tabs then this is indicated by setting self.L = -1.
self.curIdx
currently selected row in the active ListCtrl. There are times like after a row is deleted in which there may be rows visible but no row is selected.

The following lines set the default printer data:

self.printdata = wxPrintData()
self.printdata.SetPaperId(wxPAPER_LETTER)
self.printdata.SetOrientation(wxPORTRAIT)

The wxPython class wxPrintData holds a variety of information related to printers and printer device contexts. This class is used to create a wxPrinterDC and a wxPostScriptDC. It is also used as a data member of wxPrintDialogData and wxPageSetupDialogData, as part of the mechanism for transferring data between the print dialogs and the application.

self.copyitems
list that contains item instance objects that have been copied from one list to be moved to another list.
self.modified

dictionary that contains the information concerning whether any of several elements have been changed. Chose a dictionary more to test the idea that I could create a simple method that would update the dictionary and here is an example:

EVT_TEXT(self, self.name.GetId(), lambda e: self.modified.update({'name':1}))

So this lambda function means that if an EVT_TEXT event occurs then update the dictionary by adding the key to the dictionary (the value is not used and arbitrarily set to 1). The wxPython form for the macro EVT_TEXT is:

EVT_TEXT(window, id, func)

A wxEVT_COMMAND_TEXT_UPDATED event is generated when the text in a wxTextCtrl changes and that is what EVT_TEXT catches. Note that this event will always be sent when the text control’s content changes - whether this is due to user input or comes programmatically (for example, if SetValue() is called)

self.Cursors
dictionary that holds the database cursor objects. For example, it will look like: {'sqlite':<sqlite cursor object>,'nycpsltszatz':<mysql cursor object>}
self.tickler_active
booean determines whether the tickler capabililty is active; can be shut off by unchecking Tickler menu item
self.editor

list that holds the dictionaries that describe the notes that are edited by the external text editor:

[
{
'table': 'mine',
'host': 'wxLMDB:sqlite',
'path': 'C:\\DOCUME~1\\STEVEN~1\\LOCALS~1\\Temp\\Journal Scan schedule.txt',
'id': '1AB34FB9-9EE6-4AFC-8AF0-FFCA50103BF3',
'time': 1070850894
}, 
{
'table': 'factoids',
'host': 'wxLMDB:sqlite',
'path': 'C:\\DOCUME~1\\STEVEN~1\\LOCALS~1\\Temp\\How many cme programs are sponsored- - 91%.txt', 
'id': '9CAC4D18-DE1C-4535-B9A5-4CDB1AD3F304', 
'time': 1070850908
}
]

The method that uses self.editor is << Check if Edited File has Changed >>.

There is a wxPanel in the AddListControl method so each wxListCtrl has a different panel as parent.

There is a nb_sizer = wxNotebookSizer(nb) class but doesn't seem to make any difference.

self.sqlite_connections
Here because the sqlite connection has a weakreference that deletes it when you want it around
self.popupvisible
boolean that is used to ensure that two reminder popups aren't visible at the same time.
self.in_place_editor
boolean that indicates whether the inplace item name text editor is active or not.
self.showrecentcompleted
integer that determines the number of days in the past to retain completed items in the display.
self.LC_font
default font for all of the ListCtrls: self.LC_font = wxFont(9, wxSWISS, wxNORMAL, wxNORMAL)

The wxPython wxFont constructor is:

wxFont(pointSize, family, style, weight, underline=False, faceName="", wencoding=wxFONTENCODING_DEFAULT)
self.date_titles
dictionary that holds the various dates that are associated with each item and which can be displayed in the date column. The dictionary is not modified. We use one column of each ListCtrl to display any one of the four dates that that the application tracks. This dictionary associates the item attribute with the text that will be displayed in both the column header for the date and in the dropdown that allows you to change the date: self.date_titles = {'createdate':"Create Date",'duedate':"Due Date",'timestamp':"Last Modified",'finisheddate':"Completion Date"}
self.attr2col_num
dictionary that associates the item attribute with the column that attribute is displayed in in the ListCtrl: self.attr2col_num = {'priority':0, 'name':1,'owners':2, 'date':3}

The following lines construct the Find Dialog and the Dialog that catches errors and shows expressions for debugging:

self.FindDialog = FindDialog(self, "Find...", "")
self.EvalDialog = EvalDialog(self, "Evaluate...", "")
<< Toolbar Setup >>

code:

 1  tb = self.CreateToolBar(wxTB_HORIZONTAL|wxTB_FLAT)
 
2  
 
3  tb.AddLabelTool(idNEWLIST, "New (local) List", wxBitmap('bitmaps\\new.bmp'), shortHelp="Create New List")
 
4  tb.AddLabelTool(idOPENLIST, "Open", wxBitmap('bitmaps\\open.bmp'), shortHelp="Open List")
 
5  tb.AddSeparator()
 
6  tb.AddLabelTool(idTOOLPRINT, "Print", wxBitmap('bitmaps\\print.bmp'), shortHelp="Print List")
 
7  tb.AddLabelTool(idPRINTPREV, "Preview", wxBitmap('bitmaps\\preview.bmp'), shortHelp="Print Preview")
 
8  tb.AddLabelTool(idPAGESETUP, "Setup", wxBitmap('bitmaps\\setup.bmp'), shortHelp="Page Setup")
 
9  tb.AddSeparator()
10  tb.AddLabelTool(idNEWITEM, "New Item", wxBitmap('bitmaps\\new_item.bmp'), shortHelp="Create New Item")
11  tb.AddSeparator()
12  tb.AddLabelTool(idREFRESH, "Refresh", wxBitmap('bitmaps\\refresh.bmp'), shortHelp="Refresh Display")     
13  tb.AddSeparator()
14  tb.AddLabelTool(idEDITNOTE, "Edit Note", wxBitmap('bitmaps\\edit_doc.bmp'), shortHelp="Edit Note")
15  tb.AddSeparator()
16  tb.AddLabelTool(idFIND, "Find", wxBitmap('bitmaps\\find.bmp'), shortHelp = "Find Item")        
17  tb.AddSeparator()
18  tb.AddLabelTool(idCUT, "Cut", wxBitmap('bitmaps\\editcut.bmp'), shortHelp ="Cut Item")        
19  tb.AddLabelTool(idCOPY, "Copy", wxBitmap('bitmaps\\copy.bmp'), shortHelp ="Copy Item")
20  tb.AddLabelTool(idPASTE, "Paste", wxBitmap('bitmaps\\paste.bmp'), shortHelp="Paste Item")
21  tb.AddSeparator()
22  tb.AddLabelTool(idTOGGLEFINISHED, "Toggle Date", wxBitmap('bitmaps\\filledbox.bmp'), shortHelp="Toggle Finished Date")
23  tb.AddLabelTool(idDELETEITEMS, "Delete", wxBitmap('bitmaps\\delete.bmp'), shortHelp="Delete Item")
24  tb.AddLabelTool(idDUEDATE, "Due Date", wxBitmap('bitmaps\\calendar.bmp'), shortHelp="Enter Due Date")
25  tb.AddLabelTool(idEDITOWNER,"Owner", wxBitmap('bitmaps\\owners.bmp'), shortHelp="Select Owner(s)")
26  tb.AddSeparator()
27  tb.AddLabelTool(idMAILITEM, "Mail", wxBitmap('bitmaps\\mail.bmp'), shortHelp="Mail Item")
28  
29  if QUICK_LIST:
30      tb.AddSeparator()
31      tb.AddLabelTool(idSENDTO, "Send to", wxBitmap('bitmaps\\sendto.bmp'), shortHelp="Send to %s"%QUICK_LIST)
32      
33  tb.Realize()

comments:

images\toolbar.gif

nl Creates a new List self.OnNewList
ol Open an existing List self.OnOpenList
pt Print lambda e: self.OnPrint(e,showprtdlg=False))
pp Print Preview lambda e: self.OnPrint(e, prev=True))
ps Page Setup self.OnPageSetup
ni New Item self.OnNewItem
re Refresh self.OnRefresh
en Edit Note self.OnEditNote
fi Find self.OnFind
ec Cut lambda e: self.OnCopyItems(e, cut=True))
ey Copy self.OnCopyItems
ep Paste self.OnPasteItems
co Toggle Finished self.OnToggleFinished
de Delete Item self.OnDeleteItems
dd Set Item Due Date self.OnDueDate
ow Set Item Owners self.OnEditOwner
mi Mail Item self.OnMailItem
<< Create Controls>>

code:

 1  upper_panel = wxPanel(self, -1)   #size = (900,400)
 
2  bottom_panel = wxPanel(self, -1, size = (900,150)) #900 note that 000 seems to work???
 
3  
 
4  nb = wxNotebook(upper_panel, -1, size=(900,500), style=wxNB_BOTTOM)
 
5  
 
6  f = wxFont(10, wxSWISS, wxNORMAL, wxNORMAL)
 
7  self.name = wxTextCtrl(bottom_panel, -1, size = (285,42), style = wxTE_MULTILINE|wxTE_RICH2)#34 #wxTE_PROCESS_ENTER
 
8  self.name.SetDefaultStyle(wxTextAttr("BLACK", font = f))
 
9       
10  self.owners = wxTextCtrl(bottom_panel, -1, size = (250,42),style = wxTE_MULTILINE|wxTE_RICH2)
11  self.owners.SetDefaultStyle(wxTextAttr("BLACK", font = f))
12  
13  self.note = wxTextCtrl(bottom_panel, -1, size = (400,50), style=wxTE_MULTILINE)

comments:

The wxFrame has two wxPanels: upper_panel will contain the notebook. The bottom_panel will contain the various item textctrls including name, owners and note.

<< Other Events >>

code:

1  EVT_TEXT(self, self.name.GetId(), lambda e: self.modified.update({'name':1}))
2  EVT_TEXT(self, self.note.GetId(), lambda e: self.modified.update({'note':1}))
3  EVT_TEXT(self, self.owners.GetId(), lambda e: self.modified.update({'owners':1}))
4  
5  EVT_CLOSE(self, self.OnWindowExit)
6  
7  EVT_IDLE(self, self.OnIdle)
8  

comments:

The EVT_TEXT event macros indicate whether a particular textctrl has changed.

EVT_CLOSE(self, self.OnWindowExit) is used to record settings and cleanup on exiting

EVT_IDLE(self, self.OnIdle) --> Idle events used for checking text files and transfers from Outlook

There are also a number of events related to the individual ListCtrls that are placed on Notebook pages << ListControl Events >>.

<< Layout Stuff >>

code:

 1  #Appears necessary to really get the listcontrol to size with the overall window  
 
2  #upper_panel sizer
 
3  sizer = wxBoxSizer(wxHORIZONTAL)
 
4  sizer.Add(nb,1,wxALIGN_LEFT|wxEXPAND)
 
5  upper_panel.SetSizer(sizer)        
 
6  
 
7  #sizer for the row of data items
 
8  box = wxBoxSizer(wxHORIZONTAL)
 
9  box.Add(self.name,1,wxEXPAND)
10  box.Add(self.owners,0)
11  
12  #bottom_panel sizer  
13  sizer = wxBoxSizer(wxVERTICAL)        
14  sizer.AddSizer(box, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxALL, 5)
15  sizer.Add(self.note,1,wxALIGN_LEFT|wxEXPAND)
16  bottom_panel.SetSizer(sizer)
17  
18  sizer = wxBoxSizer(wxVERTICAL)
19  sizer.Add(upper_panel,1,wxALIGN_TOP|wxEXPAND)
20  sizer.Add(bottom_panel,0,wxALIGN_TOP|wxEXPAND)
21  
22  self.SetAutoLayout(1)
23  self.SetSizer(sizer)
24  #sizer.Fit(self) #actively does bad things to the dimensions on startup

comments:

The parent of the wxPanel object upper_panel is the ListManager, which is a subclass of wxFrame. The parent of wxNotebook object nb is upper_panel. Since the only child of upper_panel is the nb it wasn't obvious to me that a sizer was needed but apparently without it the wxListCtrl that will be a child of the wxPanel of nb won't size right if we don't do it this was.

Frame ---> upper_panel ---> notebook ---> panel (for each page) ---> listctrl
  |                                                             |            } one on each page
  ---> lower_panel ---> variety of textctrls                    ---> listbox
<< GUI Instance Objects >>

code:

1  self.toolmenu = toolmenu
2  self.filemenu = filemenu
3  self.nb = nb
4  self.tb = tb

comments:

No comments yet.

<< Create Socket >>

code:

1  if OUTLOOK:
2      s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Create a TCP socket
3      s.bind(('localhost',8888)) # Bind to port 8888
4      s.listen(5) # Listen, but allow no more than
5      self.sock = s

comments:

No comment

<< Load Recent Files >>

code:

 1  try:
 
2      pathlist = [f[1] for f in cp.items('Files')]
 
3  except:
 
4      pathlist = []
 
5      
 
6  if pathlist:
 
7      pathlist.sort()
 
8      pathlist.reverse()
 
9      for path in pathlist[1:]:
10          self.OnFileList(path=path)
11  
12      #don't want to trigger the page change event until n-1 of n files are loaded
13      EVT_NOTEBOOK_PAGE_CHANGED(self,nb.GetId(),self.OnPageChange)
14  
15      self.OnFileList(path=pathlist[0])
16  else:
17      EVT_NOTEBOOK_PAGE_CHANGED(self,nb.GetId(),self.OnPageChange)
18  
19  
20  

comments:

pathlist = [f[1] for f in cp.items('Files')]

This uses the new in 2.3 ConfigParser method items. This will not work unless ConfigParser has been constructed without any defaults and so a ConfigParser object is created twice.

The only remotely subtle thing here is that we don't want to execute the EVT_NOTEBOOK_PAGE_CHANGED statement while we're doing the initial loading of files since it does unnecessary processing. The statement is executed before the last file is loaded.

<< Idle Timer >>

code:

1  ID_TIMER = wxNewId()
2  self.timer = wxTimer(self, ID_TIMER) 
3  EVT_TIMER(self,  ID_TIMER, self.OnIdle)
4  self.timer.Start(3000)

comments:

Need to figure out exactly what this timer is doing.

Ownerlist creation methods (used by thread)

comments:

No comment.

def createownerlist

code:

 1  def createownerlist(self):
 
2      
 
3      if REMOTE_HOST and OFFLINE_ONLY is False:
 
4          cursor = self.GetCursor(REMOTE_HOST)
 
5          sql = "SHOW TABLES" #sorted
 
6      else:
 
7          cursor = self.GetCursor(LOCAL_HOST)
 
8          sql = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
 
9          
10      cursor.execute(sql)
11      results = cursor.fetchall()
12  
13      #excluding 'system' tables and archive tables
14      excluded_tables = ['user_sync','sync','owners']
15      tables = [t for (t,) in results if t.find('_archive')== -1 and t not in excluded_tables]
16  
17      sql_list = []
18      for table in tables:
19          sql_list.append("""SELECT owner1 FROM %s UNION SELECT owner2 FROM %s UNION SELECT owner3 FROM %s"""%((table,)*3))
20                  
21      sql = " UNION ".join(sql_list)
22      cursor.execute(sql)
23      results = cursor.fetchall()
24      
25      _list = [x[0] for x in results]
26      if '' in _list:
27          _list.remove('')
28      if None in _list:
29          _list.remove(None)
30          
31      self._list = _list
32      
33      #posting custom event to signal that this thread is done
34      evt = wxPyEvent()
35      evt_id = wxNewEventType()
36      evt.SetEventType(evt_id)
37      self.Connect(-1, -1, evt_id, self.createownerdialog)
38      wxPostEvent(self, evt)
39  

comments:

This method grabs the owners from many of the tables to create a list of possile owners for each item. The alternative is actually to create a separate owner table but it seemed to make sense to just construct the owners on the fly from the various List databases. This is done in a thread so no matter how long it takes to construct the owners it doesn't slow the appearance of the GUI. The result of this method is the contruction of the instance variable self._list.

The most interesting thing here is creating a custom event (without needing to create an event macro) to signal that this thread is done:

evt = wxPyEvent()
evt_id = wxNewEventType()
evt.SetEventType(evt_id)
self.Connect(-1, -1, evt_id, self.createownerdialog)
wxPostEvent(self, evt)

The code above is adapted from the more general wxCallAfter, which I could have used but just wanted to explicitly show the steps involved in creating a custom event, associating it with a callback and posting it.

The general point is that if you want to notify the main GUI thread of something going on in a non-GUI thread, posting events is an easy way to do it whether you use the code above, the more complete wxCallAfter (see below) from which it was derived or actually create your own custom event macro (farther below).

The code for wxCallAfter is:

def wxCallAfter(callable, *args, **kw):
     """
     Call the specified function after the current and pending event
     handlers have been completed.  This is also good for making GUI
     method calls from non-GUI threads.
     """
     app = wxGetApp()
     assert app, 'No wxApp created yet'

     global _wxCallAfterId
     if _wxCallAfterId is None:
         _wxCallAfterId = wxNewEventType()
         app.Connect(-1, -1, _wxCallAfterId,
               lambda event: event.callable(*event.args, **event.kw) )
     evt = wxPyEvent()
     evt.SetEventType(_wxCallAfterId)
     evt.callable = callable
     evt.args = args
     evt.kw = kw
     wxPostEvent(app, evt)

Unless you want multiple handlers to be able to respond to a custom event (by using evt.Skip()) or just want custom event macros that are like native event macros there doesn't seem to be much need to create full-blown custom events. If you do need to, here is how it is done:

wxEVT_THREAD_DONE = wxNewEventType()

def EVT_THREAD_DONE(win, func):
    win.Connect(-1, -1, wxEVT_THREAD_DONE, func)
    
class ThreadDoneEvent(wxPyEvent):
    def __init__(self):
        wxPyEvent.__init__(self)
        self.SetEventType(wxEVT_THREAD_DONE)

When you want to post the custom event, you do the following:

evt = ThreadDoneEvent() 
wxPostEvent(win, evt)

def createownerdialog

code:

1  def createownerdialog(self, evt=None):
2      self.ModifierDialog = ModifierDialog(parent=self, title="Select owner(s)", size=(180,300), style=wxCAPTION, modifierlist = self._list)
3      del self._list
4  

comments:

When the thread is done that creates the owner list it posts an event whose callback is this method. This method uses self._list that was generated by the createownerlist method. As an alternative, we could probably pass the list as an attribute of the event that is generated in the thread.

Notebook methods

comments:

The main method here is the one that constructs a new Notebook page by creating a new ListCtrl and new OwnerListBox and populating them. The second method does what is needed when an existing notebook page is selected.

def CreateNewNotebookPage

code:

 1  def CreateNewNotebookPage(self, host, table):
 
2      
 
3      Properties = {'owner':'*ALL',
 
4                  'LCdate':'duedate',
 
5                  'sort':{'attribute':'priority','direction':0}, #these could be set in Config
 
6                  'showfinished':0} #-1 show them all; 0 show none; integer show for that many days
 
7      
 
8      Properties['table'] = table
 
9      Properties['host'] = host
10                  
11      self.PropertyDicts.append(Properties)
12  
13      self.L = len(self.ItemLists)#could use self.ListCtrls, self.OwnerLBoxes, etc. with a -1
14      
15      results = self.ReadFromDB()
16      if results is None:
17          self.PropertyDicts = self.PropertyDicts[:-1]
18          self.L = self.L - 1
19          return
20          
21      panel = wxPanel(self.nb, -1, size = (900,400))
22      LCtrl = ListCtrl(panel, -1, style=wxLC_REPORT|wxSUNKEN_BORDER|wxLC_VRULES|wxLC_HRULES)
23      LCtrl.SetFont(self.LC_font)
24      self.ListCtrls.append(LCtrl)
25      
26      OLBox = wxListBox(panel, -1, size = (126,550), choices = [""], style=wxLB_SORT|wxSUNKEN_BORDER)
27      self.OwnerLBoxes.append(OLBox)
28      
29      sizer = wxBoxSizer(wxHORIZONTAL)
30      sizer.Add(OLBox,0,wxALIGN_LEFT|wxEXPAND)
31      sizer.Add(LCtrl,1,wxALIGN_LEFT|wxEXPAND)
32      panel.SetSizer(sizer)
33          
34      self.ItemLists.append(self.CreateAndDisplayList(results)) 
35  
36      << Fill OwnerListBox >>
37      << ListControl Events >>
38      
39      #img_num = LCtrl.arrows[Properties['sort']['direction']]
40      #LCtrl.SetColumnImage(self.attr2col_num[Properties['sort']['attribute']], img_num)
41      
42      rdbms = host.split(':')[1]
43      if rdbms == 'mysql':
44          tab_title = '%s (remote)'%table
45      else:
46          tab_title = table
47      
48      if table in SYNC_TABLES:
49          tab_title = '*'+tab_title
50                               
51      self.nb.AddPage(panel,tab_title)
52      self.nb.SetSelection(self.L)
53      
54      self.filehistory.AddFileToHistory('%s:%s'%(host,table))
55  
56      self.SetStatusText("Successfully loaded %s"%tab_title)
57      

comments:

This method creates the ListCtrl and ListBox that appears on every notebook page.

Each list has a properties dictionary associated with it.

'owner' The owner that is filtering the display or '*ALL'
'LCdate' Date that is displayed by the ListCtrl; values: 'duedate', 'createdate', 'timestamp', 'finisheddate'
'sort' Value is a dictionary of the form {'attribute':'priority','direction':0}
'showfinished' Values: -1 show them all; 0 show none; integer show for that many days
'table' The table that holds the List
'host' The form for this is 'nycpsszatzsql:mysql' or 'wxLMDB:sqlite'
<< Fill OwnerListBox >>

code:

 1  cursor = self.GetCursor(host)
 
2  if cursor is None:
 
3      print "Couldn't get cursor to fill OwnerListBox"
 
4      return
 
5      
 
6  cursor.execute("SELECT owner1 FROM %s UNION SELECT owner2 FROM %s UNION SELECT owner3 FROM %s"%((table,)*3))
 
7  
 
8  owners = [x for (x,) in cursor.fetchall()]
 
9  
10  if None in owners:
11      owners.remove(None)
12  if '' in owners:
13      owners.remove('')
14  
15  OLBox.Clear()
16  for name in owners: 
17      OLBox.Append(name)
18  OLBox.Append('*ALL')
19  OLBox.SetSelection(0)
20  

comments:

The list is sorted by the ListBox control.

mysql doesn't like '%s' while sqlite is fine with '%s' for table names.

If you don't do OLBox.Clear() then you get a blank line in the list that must be from initiating it with "".

Relying on the fact that '*All' should be first alphabetically, which is dumb so should change it.

<< ListControl Events >>

code:

 1  LCId = LCtrl.GetId()
 
2  EVT_LIST_ITEM_SELECTED(self, LCId, self.OnItemSelected)
 
3  EVT_LIST_ITEM_ACTIVATED(self, LCId, self.OnDisplayInPlaceEditor)
 
4  EVT_LEFT_DOWN(LCtrl, self.OnLeftDown) 
 
5  EVT_LEFT_DCLICK(LCtrl, self.OnLeftDown)
 
6  EVT_RIGHT_DOWN(LCtrl, self.OnRightDown)
 
7  EVT_LIST_COL_CLICK(self, LCId, self.OnColumnClick)
 
8  EVT_LIST_COL_RIGHT_CLICK(self, LCId, self.OnColumnRightClick)
 
9  
10  # the following is a ListBox event
11  EVT_LISTBOX(self, OLBox.GetId(), self.OnFilterOwners)
12  

comments:

It would seem that the two mouse events: EVT_LEFT_DOWN(LCtrl, self.OnLeftDown) and EVT_LEFT_DCLICK(LCtrl, self.OnLeftDown) could be owned by the ListManager object and not each ListCtrl object, but when I tried this, the mouse events were not detected. I did not investigate this for long so maybe I was just screwing things up or perhaps a wxFrame cannot detect a mousedown event (does that make sense?). In any event (no pun intended), it is certainly not a big deal to create these mousedown events for each ListCtrl.

Each ListBox object has one event associated with it that occurs, not surprisingly, when a name in the control is selected. The callback, self.OnFilterOwners, causes the ListCtrl to display only the items of the selected owner.

def OnPageChange

code:

 1  def OnPageChange(self, evt=None):
 
2      if self.modified:
 
3          self.OnUpdate()
 
4          
 
5      self.L = L = self.nb.GetSelection()
 
6  
 
7      << Find Highlighted Row >>
 
8      << Update Title >>
 
9      
10      evt.Skip() #051403
11      

comments:

Call-back for the EVT_NOTEBOOK_PAGE_CHANGED(self,nb.GetId(),self.OnPageChange) event.

self.modified is the dictionary that indicates which textctrls have data that has changed.

Note

Not sure whether event.Skip() is needed or not.
<< Find Highlighted Row >>

code:

 1  idx = self.ListCtrls[L].GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)
 
2  if idx != -1:
 
3      self.curIdx = idx
 
4      #LCtrl.EnsureVisible(idx)
 
5      self.OnItemSelected()
 
6  elif self.ItemLists[L]:
 
7      self.curIdx = 0
 
8      self.ListCtrls[L].SetItemState(0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
 
9      #the line above triggers an OnItemSelected EVT so don't need self.OnItemSelected() 092803
10  else:
11      self.curIdx = -1
12  

comments:

<< Update Title >>

code:

1  location,rdbms = self.PropertyDicts[L]['host'].split(':')
2  table = self.PropertyDicts[L]['table']
3  self.SetTitle("List Manager:  %s:  %s:  %s"%(location,rdbms,table))
4  

comments:

Tickler methods

def OnShowTickler

code:

 1  def OnShowTickler(self, evt=None):
 
2      if self.popupvisible:
 
3          return
 
4      
 
5      self.popupvisible = True
 
6      
 
7      host = 'wxLMDB:sqlite'
 
8      cursor = self.Cursors[host]
 
9      table = 'follow_ups'
10  
11      sql = "SELECT COUNT() FROM "+table+" WHERE finisheddate IS NULL AND priority > 1"
12      cursor.execute(sql)
13      results = cursor.fetchone()
14  
15      num_items = int(results[0])
16      
17      if not num_items:
18          return
19  
20      if self.modified: #Should decide if this should be put back or not
21          self.OnUpdate()
22          
23      n = random.randint(0,num_items-1)
24  
25      sql = "SELECT priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,id,timestamp,note FROM "+table+" WHERE finisheddate IS NULL AND priority > 1 LIMIT 1 OFFSET %d"%n
26      
27      try:
28          cursor.execute(sql)
29      except:
30          print "In OnShowTickler and attempt to Select an item failed"
31          return
32          
33      row = cursor.fetchone()
34      
35      class Item: pass
36      item = Item()
37  
38      item.priority = int(row[0]) #int(row[0]) needs int because it seems to come back as a long from MySQL
39      item.name = row[1]
40      item.createdate = row[2]
41      item.finisheddate = row[3]
42      item.duedate = row[4]
43      item.owners = [z for z in row[5:7] if z is not None] #if you carry around ['tom',None,None] you have an issue when you go write it
44      item.id = row[8]
45      item.timestamp = row[9]
46      item.note = row[10]
47  
48      dlg = TicklerDialog(self, "", "Do something about this!!!", size=(550,350))
49      TC = dlg.TC
50      
51      f = wxFont(14, wxSWISS, wxITALIC, wxBOLD, False)
52      TC.SetDefaultStyle(wxTextAttr("BLUE",wxNullColour, f))
53      TC.AppendText("%s..."%item.name)
54  
55      if item.priority == 3:
56          TC.SetDefaultStyle(wxTextAttr("RED","YELLOW",f))
57      TC.AppendText("%d\n\n"%item.priority)
58      
59      f = wxFont(8, wxSWISS, wxNORMAL, wxNORMAL)
60      TC.SetDefaultStyle(wxTextAttr("BLACK","WHITE", f))
61      TC.AppendText("owners: %s\n"%", ".join(item.owners))
62      TC.AppendText("created on: %s\n"%item.createdate.Format('%m/%d/%y'))
63      if item.duedate:
64          ddate = item.duedate.Format('%m/%d/%y')
65      else:
66          ddate = "<no due date>"
67      TC.AppendText("due on: %s\n\n"%ddate)
68  
69      note = item.note
70      if not note:
71          note = "<no note>"
72      TC.AppendText("%s\n\n"%note)
73      f = wxFont(10, wxSWISS, wxITALIC, wxBOLD)
74      TC.SetDefaultStyle(wxTextAttr("BLACK",wxNullColour, f))
75      TC.AppendText('follow_ups')
76      TC.ShowPosition(0)   #did not do anything
77      TC.SetInsertionPoint(0)
78      result = dlg.ShowModal()
79      dlg.Destroy()
80      self.popupvisible = False     
81  
82      if result in (wxID_OK, wxID_APPLY):
83  
84          for L,Properties in enumerate(self.PropertyDicts):
85              if Properties['table'] == table:
86                  break
87          else:
88              print "Can't find %s"%table
89              return
90                      
91          self.nb.SetSelection(L) #if the page changes it sends a EVT_NOTEBOOK_PAGE_CHANGED, which calls OnPageChange
92          self.L = L
93          self.FindNode(item)
94          if result==wxID_APPLY:
95              self.OnMailItem(item)
96  
97      elif result==wxID_FORWARD:
98          self.OnShowTickler()
99  

def OnActivateTickler

code:

1  def OnActivateTickler(self, evt):
2      self.tickler_active = not self.tickler_active
3      self.toolmenu.Enable(idSHOWNEXT,self.tickler_active)
4  
5      

Email methods

OnMailItem

code:

 1  def OnMailItem(self, evt=None, item=None):
 
2      if item is None:
 
3          if self.curIdx == -1:
 
4              return
 
5          else:
 
6              item = self.ItemLists[self.L][self.curIdx]
 
7          
 
8      dlg = MailDialog(self,"Mail a reminder", size=(450,500),
 
9                 recipients=item.owners,    
10                 subject=item.name,
11                 body=self.GetNote())          
12      result = dlg.ShowModal()
13      if result==wxID_OK:
14          outlook= Dispatch("Outlook.Application")
15          newMsg = outlook.CreateItem(olMailItem) #outlook.CreateItem(constants.olMailItem)
16          newMsg.To = to = dlg.RTC.GetValue()
17          newMsg.Subject = subject = dlg.STC.GetValue()
18          newMsg.Body = body = dlg.BTC.GetValue()
19  
20          #newMsg.FlagStatus = constants.olFlagMarked
21          
22          newMsg.Display()
23  
24          dlg.Destroy()            
25          #del outlook
26  
27          self.note.SetSelection(0,0)
28          self.note.WriteText("**************************************************\n")
29          self.note.WriteText("Email sent on %s\n"%mx.DateTime.today().Format("%m/%d/%y"))
30          self.note.WriteText("To: %s\n"%to)
31          self.note.WriteText("Subject: %s\n"%subject)
32          self.note.WriteText("%s\n"%body)
33          self.note.WriteText("**************************************************\n")
34  

OnMailView

code:

 1  def OnMailView(self, evt=None):
 
2      recipients = [self.PropertyDicts[self.L]['owner']]
 
3      
 
4      body = ""
 
5      for i,item in enumerate(self.ItemLists[self.L]):
 
6          body = body+"%d. %s (%d)\n"%(i+1, item.name, item.priority)
 
7      
 
8      subject = "Follow-ups " + mx.DateTime.today().Format("%m/%d/%y")
 
9              
10      dlg = MailDialog(self,"Follow-up List", size=(450,500),
11                 recipients=recipients,
12                 subject=subject,
13                 body=body)
14                 
15      val = dlg.ShowModal()
16      dlg.Destroy()
17      if val==wxID_OK:
18          outlook= Dispatch("Outlook.Application")
19          newMsg = outlook.CreateItem(olMailItem) #outlook.CreateItem(constants.olMailItem)
20          newMsg.To = dlg.RTC.GetValue()
21          newMsg.Subject = dlg.STC.GetValue()
22          newMsg.Body = dlg.BTC.GetValue()
23  
24          newMsg.FlagStatus = olFlagMarked #constants.olFlagMarked
25          newMsg.Categories = "Follow-up"
26          
27          newMsg.Display()
28      
29          #del outlook
30  

Cut/Copy/Paste methods

OnCopyItems

code:

 1  def OnCopyItems(self, event=None, cut=False):
 
2      if self.curIdx == -1:
 
3          return
 
4          
 
5      L = self.L
 
6      IList = self.ItemLists[L]
 
7      LCtrl = self.ListCtrls[L]
 
8      
 
9      << Find Highlighted Items >>
10      
11      self.SetStatusText("%d items copied"%len(copyitems))
12      if cut:
13          self.OnDeleteItems()
14  
<< Find Highlighted Items >>

code:

 1  copyitems = []
 
2  i = -1
 
3  while 1:
 
4      i = LCtrl.GetNextItem(i, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)
 
5      if i==-1:
 
6          break
 
7      item = IList[i]
 
8      item.notes = self.GetNote(L,item) #handles the database situation
 
9      copyitems.append(item)
10  

OnPasteItems

code:

 1  def OnPasteItems(self, evt=None, L=None): #noselect 051603
 
2      #used by OnMoveToList, OnMoveToSpecificList and called directly
 
3      if not self.copyitems:
 
4          print "Nothing was selected to be copied"
 
5          return
 
6          
 
7      if L is None: #this is not needed by OnMoveTo or OnDragToTab but is for a straight call
 
8          L = self.L
 
9          
10      Properties = self.PropertyDicts[L]
11      LCtrl = self.ListCtrls[L]
12      IList = self.ItemLists[L]
13      
14      items = self.copyitems
15      numitems = len(items)
16      
17      for item in items:
18  
19          z = item.owners+[None,None,None]
20  
21          id = self.GetUID() #we do give it a new id
22          host = Properties['host']
23          cursor = self.Cursors[host]
24          table = Properties['table']
25          
26          createdate = mx.DateTime.now() #need this or else it won't be seen as a new item when synching; would be seen as updated
27          command = "INSERT INTO "+table+" (priority,name,createdate,finisheddate,duedate,note,owner1,owner2,owner3,id) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
28          cursor.execute(command,(item.priority,item.name,createdate,item.finisheddate,item.duedate,item.notes,z[0],z[1],z[2],id))
29          
30          timestamp = self.TimeStamper(host, cursor, table, id)
31          
32          #creating a new item breaks the connection between item.x and new_item.x
33          class Item: pass
34          new_item = Item()
35          new_item.id = id
36          new_item.priority = item.priority
37          new_item.owners = item.owners
38          new_item.name = item.name
39          new_item.timestamp = timestamp
40          new_item.duedate =item.duedate
41          new_item.finisheddate = item.finisheddate
42          new_item.createdate = createdate
43          IList.insert(0,new_item)
44          
45      self.DisplayList(IList,L)
46      
47      #If we didn't come from OnMoveToList or OnMoveToSpecificList where L != self.L
48      if L==self.L:
49          for i in range(numitems):
50              LCtrl.SetItemState(i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
51          self.curIdx = numitems-1
52  
53  
54  

OnDeleteItems

code:

 1  def OnDeleteItems(self, event=None):
 
2      """Called directly and by OnCopyItems (cut = true)
 3      """

 
4      if self.curIdx == -1: #not absolutely necessary but gets you out quickly
 
5          return
 
6          
 
7      L = self.L
 
8      LCtrl = self.ListCtrls[L]
 
9      IList = self.ItemLists[L]
10      Properties = self.PropertyDicts[L]
11      
12      i = -1
13      while 1:
14          i = LCtrl.GetNextItem(i, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)
15          if i==-1:
16              break
17          item = IList.pop(i)
18          LCtrl.DeleteItem(i)
19  
20          host = Properties['host']
21          cursor = self.Cursors[host]
22          table = Properties['table']
23          
24          cursor.execute("DELETE from "+table+" WHERE id = %s", (item.id,))
25              
26          #Track Deletes for Syncing ############################################
27          if table in SYNC_TABLES:
28              if host.split(':')[1] == 'sqlite':
29                  timestamp = mx.DateTime.now()
30                  cursor.execute("INSERT INTO sync (id,action,table_name,name,timestamp) VALUES (%s,%s,%s,%s,%s)",(item.id,'d',table,item.name,timestamp))
31              else:
32                  cursor.execute("INSERT INTO sync (id,action,table_name,user,name) VALUES (%s,%s,%s,%s,%s)",(item.id,'d',table,USER,item.name))
33          #########################################################################
34          i-=1
35  
36      self.name.Clear()
37      self.owners.Clear()
38      self.note.Clear()
39      #note that Clearing does cause self.modified -->{'name':1}
40      self.modified = {}
41      self.curIdx = -1
42  

MouseDown methods

OnLeftDown (Action depends on x coordinate)

code:

 1  def OnLeftDown(self, evt):
 
2      print "Here"
 
3      if self.modified:
 
4          #if inplace editor is open and you click anywhere (same or different row from current row) but in the editor itself then just to close editor
 
5          flag = self.modified.has_key('inplace')
 
6          self.OnUpdate()
 
7          if flag:
 
8              evt.Skip() #without Skip, EVT_LIST_ITEM_SELECTED is not generated if you click in a new row
 
9              return
10      
11      x,y = evt.GetPosition()
12      LCtrl = self.ListCtrls[self.L]
13      
14      #Using HitTest to obtain row clicked on because there was a noticable delay in the generation of an
15      #EVT_LIST_ITEM_SELECTED event when you click on the already selected row
16      idx,flags = LCtrl.HitTest((x,y))
17      
18      #if you are below rows of items then idx = -1 which could match self.curIdx = -1
19      if idx == -1:
20          return
21      
22      # only if you click on the currently selected row do the following events occur
23      if idx == self.curIdx:
24          if x < 18:
25              self.OnToggleFinished()
26          elif x < 33:
27              self.OnPriority()
28          elif x < 33 + LCtrl.GetColumnWidth(1):
29              self.OnDisplayInPlaceEditor()
30          elif x < 33 + LCtrl.GetColumnWidth(1) + LCtrl.GetColumnWidth(2): 
31              self.OnEditOwner()
32          else:
33              self.OnDueDate
34      else:
35          evt.Skip() #without Skip, EVT_LIST_ITEM_SELECTED is not generated if you click in a new row
36  
37  
38  

OnRightDown (Display popup sendto menu)

code:

 1  def OnRightDown(self, evt):
 
2      x,y = evt.GetPosition()
 
3      
 
4      sendtomenu = wxMenu()
 
5      
 
6      open_tables = []
 
7      for page,Properties in enumerate(self.PropertyDicts):
 
8          host,table = Properties['host'],Properties['table']
 
9          open_tables.append((host,table))
10          sendtomenu.Append(1+page,"%s (%s)"%(table,host))
11          EVT_MENU(self, 1+page, lambda e,p=page: self.OnMoveToList(e,p))
12          
13      sendtomenu.Delete(self.L+1) # don't send it to the page you're already on
14      sendtomenu.AppendSeparator()
15      
16      self.closed_tables = []
17      for host,cursor in self.Cursors.items():
18  
19          location, rdbms = host.split(':')
20  
21          if rdbms == 'sqlite':
22              cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
23          elif rdbms == 'mysql':
24              cursor.execute("SHOW tables")
25  
26          results = cursor.fetchall()
27          
28          page+=1
29          for (table,) in results:
30              if not ((host,table) in open_tables or table in ['user_sync','owners','sync']):
31                  self.closed_tables.append((host,table))
32                  sendtomenu.Append(1+page,"%s (%s)"%('*'+table,host))
33                  EVT_MENU(self, 1+page, lambda e,p=page: self.OnMoveToList(e,p))
34                  page+=1
35  
36      self.PopupMenu(sendtomenu,(x+125,y+40))
37      sendtomenu.Destroy()
38  

Move/Combine items methods

OnCombineItems

code:

 1  def OnCombineItems(self, evt):
 
2      L = self.L
 
3      idx = self.curIdx
 
4      IList = self.ItemLists[L]
 
5      LCtrl = self.ListCtrls[L]
 
6      
 
7      combine_list = []
 
8      i = -1
 
9      while 1:
10          i = LCtrl.GetNextItem(i, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)
11          if i==-1:
12              break
13          combine_list.append((IList[i].createdate,IList[i]))
14  
15      
16      if len(combine_list) < 2:
17          print "Fewer than two items highlighted"
18          return
19      
20      combine_list.sort()
21      combine_list.reverse()
22      
23      dlg = wxMessageDialog(self,
24                          "Combine the %d selected items?"%len(combine_list),
25                          "Combine Items?",
26                          wxICON_QUESTION|wxYES_NO)
27                          
28      if dlg.ShowModal() == wxID_YES:
29          Properties = self.PropertyDicts[L]
30          host = Properties['host']
31          cursor = self.Cursors[host]
32          table = Properties['table']
33          
34          t_item = combine_list[0][1]
35          merge_list = combine_list[1:]
36          new_note = ""
37          
38          for date,item in merge_list:
39              note = self.GetNote(item=item)
40              date = date.Format("%m/%d/%y")
41              new_note = "%s\n%s %s\n\n%s"%(new_note, date, item.name, note)
42              
43              cursor.execute("DELETE from "+table+" WHERE id = %s", (item.id,))
44              #Track Deletes for Syncing ############################################
45              if table in SYNC_TABLES:
46                  if host.split(':')[1] == 'sqlite':
47                      timestamp = mx.DateTime.now()
48                      cursor.execute("INSERT INTO sync (id,action,table_name,name,timestamp) VALUES (%s,%s,%s,%s,%s)",(item.id,'d',table,item.name,timestamp))
49                  else:
50                      cursor.execute("INSERT INTO sync (id,action,table_name,user,name) VALUES (%s,%s,%s,%s,%s)",(item.id,'d',table,USER,item.name))
51              #########################################################################
52                  
53          t_note = self.GetNote(item=t_item)
54          t_note = "%s\n%s"%(t_note,new_note)
55          
56          #What about combining owners?######################################
57          
58          cursor.execute("UPDATE "+table+" SET name = %s, note = %s WHERE id = %s", (t_item.name+"*",t_note,t_item.id))
59          t_item.timestamp = self.TimeStamper(host, cursor, table, t_item.id)
60          
61          self.OnRefresh()
62          LCtrl.SetItemState(0, 0, wxLIST_STATE_SELECTED)
63          IList = self.ItemLists[L]
64          id = t_item.id
65          idx = -1
66          for item in IList:
67              idx+=1
68              if id == item.id:
69                  break
70          else:
71              idx = -1 
72      
73          #should never be -1
74          if idx != -1:       
75              LCtrl.SetItemState(idx, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
76              LCtrl.EnsureVisible(idx)
77          self.curIdx = idx
78          
79      dlg.Destroy()
80  

OnMoveToList

code:

 1  def OnMoveToList(self, evt=None, page=0):
 
2      self.OnCopyItems(cut=True)
 
3      pc = self.nb.GetPageCount()
 
4      if page < pc:           
 
5          self.OnPasteItems(L=page)
 
6      else:
 
7          host,table = self.closed_tables[page-pc]
 
8          cursor = self.Cursors[host]# in ini self.Cursors[host]
 
9      
10          for item in self.copyitems:
11              z = item.owners+[None,None,None]
12              id = self.GetUID() #give it a new id
13              
14              #need this or else it won't be seen as a new item when syncing; would be seen as updated
15              createdate = mx.DateTime.now() 
16              command = "INSERT INTO "+table+" (priority,name,createdate,finisheddate,duedate,note,owner1,owner2,owner3,id) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
17              cursor.execute(command,(item.priority,item.name,createdate,item.finisheddate,item.duedate,item.notes,z[0],z[1],z[2],id))
18              timestamp = self.TimeStamper(host, cursor, table, id)
19              
20      self.copyitems = []
21      

OnMoveToSpecificList

code:

 1  def OnMoveToSpecificList(self, evt=None, table='follow_ups'):
 
2      matches = {}
 
3      for page,Properties in enumerate(self.PropertyDicts):
 
4          host,tble = Properties['host'],Properties['table']
 
5          if tble == table:
 
6              rdbms = host.split(':')[1]
 
7              matches[rdbms] = page
 
8          
 
9      self.OnCopyItems(cut=True)
10      
11      if matches:
12          if matches.get('mysql'):    
13              self.OnPasteItems(L=matches['mysql'])
14          else:
15              self.OnPasteItems(L=matches['sqlite'])
16      else:
17          cursor = self.Cursors[LOCAL_HOST]
18      
19          for item in self.copyitems:
20              z = item.owners+[None,None,None]
21              id = self.GetUID() #give it a new id
22              
23              #need this or else it won't be seen as a new item when syncing; would be seen as updated
24              createdate = mx.DateTime.now() 
25              command = "INSERT INTO "+table+" (priority,name,createdate,finisheddate,duedate,note,owner1,owner2,owner3,id) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
26              cursor.execute(command,(item.priority,item.name,createdate,item.finisheddate,item.duedate,item.notes,z[0],z[1],z[2],id))
27              timestamp = self.TimeStamper(host, cursor, table, id)
28              
29      self.copyitems = []
30  
31              
32  

Change/update items methods

OnToggleFinished

code:

 1  def OnToggleFinished(self, evt=None):
 
2      L = self.L
 
3      LCtrl = self.ListCtrls[L]
 
4      Properties = self.PropertyDicts[L]
 
5      idx = self.curIdx
 
6  
 
7      item = self.ItemLists[L][idx]
 
8      LC_Item = LCtrl.GetItem(idx)
 
9      
10      if not item.finisheddate:
11          item.finisheddate = mx.DateTime.today()
12          LC_Item.SetImage(LCtrl.idx0)
13      else:
14          item.finisheddate = None
15          LC_Item.SetImage(LCtrl.idx1)
16      
17      << draw item >>
18  
19      self.tb.EnableTool(30, True)
20      
21      host = Properties['host']       
22      cursor = self.Cursors[host]
23      table = Properties['table']
24      
25      cursor.execute("UPDATE "+table+" SET finisheddate = %s WHERE id = %s", (item.finisheddate, item.id))
26      item.timestamp = self.TimeStamper(host, cursor, table, item.id)
27      
28      if Properties['LCdate'] == 'timestamp':
29          LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.timestamp.Format("%m/%d %H:%M:%S"))
30      elif Properties['LCdate'] == 'finisheddate':
31          LCtrl.SetStringItem(idx, self.attr2col_num['date'],