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'], item.finisheddate.Format('%m/%d/%y'))
32  
33  
34  
<< draw item >>

code:

 1  if item.finisheddate:
 
2      #It appears that SetTextColour resets font weight to Normal but this makes no sense
 
3      #This means that all finished items have Normal weight whether they are priority 3,2 or 1
 
4      #May actually be that GetItem() and then SetItem() sets the weight to Normal no matter what it was originally
 
5      LC_Item.SetTextColour(wxLIGHT_GREY)
 
6      
 
7  elif item.priority==1:
 
8      #see note above about SetTextColour apparently resetting weight
 
9      LC_Item.SetTextColour(wxBLACK)
10      
11  elif item.priority==2:
12      #LC_Item.SetTextColour(wxBLACK) -- this line should be necessary but it does not appear to be
13      # ? font is black so ? if have to reset it
14      f = self.LC_font
15      f.SetWeight(wxBOLD)
16      LC_Item.SetFont(f)
17      f.SetWeight(wxNORMAL) # resetting font weight
18  
19  else:
20      LC_Item.SetTextColour(wxRED) #appears to be the only way to set color - can't through font
21      f = self.LC_font #LCtrl.font
22      f.SetWeight(wxBOLD)
23      LC_Item.SetFont(f)
24      f.SetWeight(wxNORMAL) # resetting font weight
25      

OnPriority

code:

 1  def OnPriority(self, event=None, input=None):
 
2      L = self.L
 
3      idx = self.curIdx
 
4      LCtrl = self.ListCtrls[L]
 
5      Properties = self.PropertyDicts[L]
 
6      item = self.ItemLists[L][idx]
 
7      
 
8      if input:
 
9          item.priority=input
10  
11      else:
12          if item.priority < 3:
13              item.priority+= 1
14          else:
15              item.priority=1
16  
17      LC_Item = LCtrl.GetItem(idx)
18  
19      << draw item >>
20  
21      text = str(item.priority)        
22      LCtrl.SetStringItem(idx, 0, text)
23  
24      host = Properties['host']
25      cursor = self.Cursors[host]
26      table = Properties['table']
27      
28      cursor.execute("UPDATE "+table+" SET priority = %s WHERE id = %s", (item.priority,item.id))
29      item.timestamp = self.TimeStamper(host, cursor, table, item.id)
30      
31      if Properties['LCdate'] == 'timestamp':
32          LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.timestamp.Format('%m/%d %H:%M:%S'))
33          
34      wxCallAfter(LCtrl.SetFocus)
35      
<< draw item >>

code:

 1  if item.finisheddate:
 
2      #It appears that SetTextColour resets font weight to Normal but this makes no sense
 
3      #This means that all finished items have Normal weight whether they are priority 3,2 or 1
 
4      #May actually be that GetItem() and then SetItem() sets the weight to Normal no matter what it was originally
 
5      LC_Item.SetTextColour(wxLIGHT_GREY)
 
6      
 
7  elif item.priority==1:
 
8      #see note above about SetTextColour apparently resetting weight
 
9      LC_Item.SetTextColour(wxBLACK)
10      
11  elif item.priority==2:
12      #LC_Item.SetTextColour(wxBLACK) -- this line should be necessary but it does not appear to be
13      # ? font is black so ? if have to reset it
14      f = self.LC_font
15      f.SetWeight(wxBOLD)
16      LC_Item.SetFont(f)
17      f.SetWeight(wxNORMAL) # resetting font weight
18  
19  else:
20      LC_Item.SetTextColour(wxRED) #appears to be the only way to set color - can't through font
21      f = self.LC_font #LCtrl.font
22      f.SetWeight(wxBOLD)
23      LC_Item.SetFont(f)
24      f.SetWeight(wxNORMAL) # resetting font weight
25      

Inplace Edit Methods

OnDisplayInPlaceEditor

code:

 1  def OnDisplayInPlaceEditor(self,evt=None):
 
2      L = self.L
 
3      LCtrl = self.ListCtrls[L]
 
4      Properties = self.PropertyDicts[L]
 
5      idx = self.curIdx
 
6      item = self.ItemLists[L][idx]
 
7      
 
8      host = Properties['host']
 
9      cursor = self.Cursors[host]
10      table = Properties['table']
11          
12      #if self.Conflict(host, cursor, table, item): return #works -- may be overkill so i've commented it out
13      
14      TCid = wxNewId()
15      y = LCtrl.GetItemPosition(idx)[1] 
16      length = LCtrl.GetColumnWidth(1)
17  
18      editor = wxTextCtrl(self, TCid, pos = (167,y+28), size = (length,23), style=wxTE_PROCESS_ENTER)
19      editor.SetFont(wxFont(9, wxSWISS, wxNORMAL, wxNORMAL))
20      editor.SetBackgroundColour(wxColour(red=255,green=255,blue=175)) #Yellow
21      editor.AppendText(item.name)
22      editor.Show(True)
23      editor.Raise()
24      editor.SetSelection(-1,-1)
25      editor.SetFocus()       
26      
27      EVT_TEXT_ENTER(self, TCid, self.OnCloseInPlaceEditor)           
28  
29      self.in_place_editor = editor
30      self.modified['inplace'] = 1    
31  
32  
33  
34  
35  
OnCloseInPlaceEditor

code:

 1  def OnCloseInPlaceEditor(self,evt=None):
 
2      L = self.L
 
3      LCtrl = self.ListCtrls[L]
 
4      Properties = self.PropertyDicts[L]
 
5      idx = self.curIdx
 
6      item = self.ItemLists[L][idx]
 
7      
 
8      host = Properties['host']
 
9      cursor = self.Cursors[host]
10      table = Properties['table']
11      LCdate = Properties['LCdate']
12      
13      #if self.Conflict(host, cursor, table, item)...
14  
15      text = self.in_place_editor.GetValue().strip()[:150]
16      item.name = text
17      LCtrl.SetStringItem(idx, self.attr2col_num['name'], text)
18      self.in_place_editor.Destroy()
19      
20      cursor.execute("UPDATE "+table+" SET name = %s WHERE id = %s", (text, item.id))
21      item.timestamp = self.TimeStamper(host, cursor, table, item.id)
22  
23      if Properties['LCdate'] == 'timestamp':
24          LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.timestamp.Format('%m/%d %H:%M:%S'))
25  
26      self.name.Clear()
27      self.name.AppendText(text) #this will cause self.modified['name'] = 1, which is dealt with below
28      
29      #using default in case for some reason self.modified does not have the keys
30      self.modified.pop('inplace', None)
31      self.modified.pop('name', None)
32          
33      wxCallAfter(LCtrl.SetFocus) #sets focus on LCtrl and current selection to be highlighted
34  
35  
36  

OnDueDate

code:

 1  def OnDueDate(self, evt=None):
 
2      idx = self.curIdx
 
3      if idx == -1:
 
4          return
 
5      L = self.L
 
6      Properties = self.PropertyDicts[L]
 
7      item = self.ItemLists[L][idx]
 
8      LCtrl = self.ListCtrls[L]
 
9  
10      if item.duedate:
11          date = wxDateTime()
12          date.SetTimeT(item.duedate) #I am surprised it takes a mx.DateTime object; supposed to need ticks
13      else:
14          date = 0
15      dlg = CalendarDialog(parent=self,
16                   title="Select a date",
17                   size=(400,400),
18                   style=wxCAPTION,
19                   date = date)
20      if dlg.ShowModal()==wxID_OK:
21          date = dlg.cal.GetDate() # this is some date object
22          #date = date.GetTicks()
23          item.duedate = mx.DateTime.DateFromTicks(date.GetTicks())
24  
25          host = Properties['host']
26          cursor = self.Cursors[host]
27          table = Properties['table']
28          
29          cursor.execute("UPDATE "+table+" SET duedate = %s WHERE id = %s", (item.duedate,item.id))
30          item.timestamp = self.TimeStamper(host, cursor, table, item.id)
31          if Properties['LCdate'] == 'timestamp':
32              LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.timestamp.Format("%m/%d %H:%M:%S"))
33          elif Properties['LCdate'] == 'duedate':
34              LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.duedate.Format('%m/%d/%y'))
35      dlg.cal.Destroy()
36      dlg.Destroy()
37      

OnEditOwner

code:

 1  def OnEditOwner(self, evt=None): #, new=False) removed Aug. 31 for simplicity
 
2      idx = self.curIdx
 
3      if idx == -1:
 
4          return
 
5      L = self.L
 
6      Properties = self.PropertyDicts[L]
 
7      LCtrl = self.ListCtrls[L]
 
8      item = self.ItemLists[L][idx]
 
9      if not self.ModifierDialog:
10          print "self.ModifierDialog is still being constructed"
11          return
12      #need to clear the current selections or you'll just be making more and more selections
13      self.ModifierDialog.SelectCurrent(item.owners)
14      self.ModifierDialog.tc.Clear()
15      self.ModifierDialog.CenterOnParent()
16      
17      val = self.ModifierDialog.ShowModal()
18  
19      if val == wxID_OK:
20          item.owners, new_names = self.ModifierDialog.GetUserInput()
21          
22          << Common Owner Code >>
23  
24          for owner in item.owners:
25              if self.OwnerLBoxes[L].FindString(owner) == -1:
26                  self.OwnerLBoxes[L].Append(owner)
27  
28          for owner in new_names:
29              self.ModifierDialog.lb.Append(owner)
30          
31          host = Properties['host']
32          cursor = self.Cursors[host]
33          table = Properties['table']
34  
35          cursor.execute("UPDATE "+table+" SET owner1 = %s, owner2 = %s, owner3 = %s WHERE id = %s", (z[0],z[1],z[2],item.id))
36          item.timestamp = self.TimeStamper(host, cursor, table, item.id)
37          if Properties['LCdate'] == 'timestamp':
38              LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.timestamp.Format("%m/%d %H:%M:%S"))
39  
40          if 'owners' in self.modified:
41              del self.modified['owners']
42              
43      wxCallAfter(LCtrl.SetFocus)
44      
<< Common Owner Code >>

code:

1  owner_str = '; '.join(item.owners)
2  LCtrl.SetStringItem(idx, self.attr2col_num['owners'], owner_str)
3  self.owners.Clear()
4  self.owners.AppendText(owner_str)
5          
6  z = item.owners+[None,None,None] #note that + creates a new list

OnUpdate

code:

 1  def OnUpdate(self, evt=None):
 
2      if 'inplace' in self.modified:
 
3          self.OnCloseInPlaceEditor()
 
4          if not self.modified:
 
5              return
 
6  
 
7      L = self.L
 
8      LCtrl = self.ListCtrls[L]
 
9      IList = self.ItemLists[L]
10      Properties = self.PropertyDicts[L]
11      OLBox = self.OwnerLBoxes[L]
12      idx = self.curIdx
13  
14      # there is some chance that it is never true that idx == -1 and then this could be eliminated
15      if idx != -1:
16          item = IList[idx]
17      else:
18          msg = wxMessageDialog(self, "There is no selected item to update", "", wxICON_ERROR|wxOK)
19          msg.ShowModal()
20          msg.Destroy()
21          self.modified = {}
22          return
23          
24      host = Properties['host']
25      cursor = self.Cursors[host]
26      table = Properties['table']
27      
28      if 'name' in self.modified:
29          item.name = self.name.GetValue().strip()[:150]
30          LCtrl.SetStringItem(idx, self.attr2col_num['name'], item.name)
31          cursor.execute("UPDATE "+table+" SET name =%s WHERE id = %s",(item.name,item.id))
32          
33      if 'note' in self.modified:
34          note = self.note.GetValue() #a blank note starts out as None but after this it becomes '' -- ??
35          cursor.execute("UPDATE "+table+" SET note =%s WHERE id = %s",(note,item.id))
36          
37      if 'owners' in self.modified:
38          owner_str = self.owners.GetValue().strip()
39          item.owners = []
40          if owner_str:
41              owner_list = [x.strip() for x in owner_str.split(';')]
42              for owner in owner_list:
43                  owner = ", ".join([x.strip().title() for x in owner.split(',')])
44                  item.owners.append(owner)
45              
46          << Common Owner Code >>
47  
48          cursor.execute("UPDATE "+table+" SET owner1 = %s, owner2 = %s, owner3 = %s WHERE id = %s", (z[0],z[1],z[2],item.id))
49          
50          for owner in item.owners:
51              if self.ModifierDialog.lb.FindString(owner) == -1:
52                  self.ModifierDialog.lb.Append(owner)
53                  OLBox.Append(owner)
54              elif OLBox.FindString(owner) == -1:
55                  OLBox.Append(owner)         
56                  
57      item.timestamp = self.TimeStamper(host, cursor, table, item.id)
58      if Properties['LCdate'] == 'timestamp':
59          LCtrl.SetStringItem(idx, 3, item.timestamp.Format("%m/%d %H:%M:%S"))
60      
61      self.modified = {}
62      
63      
<< Common Owner Code >>

code:

1  owner_str = '; '.join(item.owners)
2  LCtrl.SetStringItem(idx, self.attr2col_num['owners'], owner_str)
3  self.owners.Clear()
4  self.owners.AppendText(owner_str)
5          
6  z = item.owners+[None,None,None] #note that + creates a new list

OnNewItem

code:

 1  def OnNewItem(self, evt=None):
 
2      L=self.L
 
3      LCtrl = self.ListCtrls[L]
 
4      Properties = self.PropertyDicts[L]
 
5      
 
6      if self.curIdx != -1:
 
7          LCtrl.SetItemState(self.curIdx, 0, wxLIST_STATE_SELECTED)
 
8      
 
9      << Clear data fields >>
10      
11      class Item: pass
12      item = Item()
13      item.name = '<New Item>'
14      item.priority = 1
15      item.owners = []
16      item.createdate = mx.DateTime.now() #need this to be a timestamp and not just date for syncing
17      item.duedate = item.finisheddate = None
18  
19      self.ItemLists[L].insert(0,item)
20      
21      host = Properties['host']
22      cursor = self.Cursors[host]
23      table = Properties['table']
24      item.id = self.GetUID()
25      
26      cursor.execute("INSERT INTO "+table+" (priority,name,createdate,finisheddate,duedate,id) VALUES (%s,%s,%s,%s,%s,%s)",
27                  (item.priority,item.name,item.createdate,None,None,item.id))
28          
29      item.timestamp = self.TimeStamper(host, cursor, table, item.id)
30      
31      #tracking new item for syncing will happen in Edit Name
32  
33      LCtrl.InsertImageStringItem(0,"1", LCtrl.idx1)
34      LCtrl.SetStringItem(0,1,item.name)
35  
36      if Properties['LCdate'] == 'timestamp':
37          LCtrl.SetStringItem(0, self.attr2col_num['date'], item.timestamp.Format("%m/%d %H:%M:%S"))
38      elif Properties['LCdate'] == 'createdate':
39          LCtrl.SetStringItem(0, self.attr2col_num['date'], item.createdate.Format('%m/%d/%y'))
40  
41      self.curIdx = 0
42      
43      #if Display is being filtered we assume that is the owner of the new node
44      owner = Properties['owner']     
45      if owner and owner!='*ALL':
46          self.ListCtrls[L].SetStringItem(0, self.attr2col_num['owners'], owner)
47          item.owners = [owner]
48          
49          self.owners.Clear()
50          self.owners.AppendText(owner)
51          
52          cursor.execute("UPDATE "+table+" SET owner1 = %s WHERE id = %s", (owner,item.id))
53          item.timestamp = self.TimeStamper(host, cursor, table, item.id)  #not really necessary since just got a timestamp
54      
55      # decided that it was actually better not to ask for the owner on a new node    
56      #else:
57          #self.OnEditOwner()
58      
59      LCtrl.SetFocus() #needed for the in place editor to look right
60      LCtrl.SetItemState(0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
61      
<< Clear data fields >>

code:

1  self.name.Clear()
2  self.owners.Clear()

Conflict (not in use)

code:

 1  @ Need to decide if we are going to have timestamp checking to be sure something hasn't changed
 2  Note that there would not need to be timestamp checking on a new node
 
3  Also  there is no need to timestamp check on a local DB
 
4  The following code seems to work fine, however, I have just commented out the calls to it in NameEditor methods
 
5  @c
 
6  def Conflict(self, host, cursor, table, item):
 
7      if host is 'sqlite':
 
8          return False
 
9      cursor.execute("Select timestamp from "+table+" WHERE id = %s", (item.id,))
10      db_timestamp = cursor.fetchone()[0]
11      if db_timestamp != item.timestamp:
12          print "There is a conflict and you should refresh display"
13          return True
14      else:
15          return False

OnEditNote

code:

 1  def OnEditNote(self, evt=None):
 
2      if self.modified:
 
3          self.OnUpdate()
 
4      
 
5      idx = self.curIdx
 
6      
 
7      if idx == -1:
 
8          return
 
9          
10      L = self.L
11          
12      #if self.editor:
13          #machine = None
14          #win32pdh.EnumObjects(None, machine, 0, 1) # resets Enum otherwise it seems to hold onto old data
15          #object = "Process"
16          #items, instances = win32pdh.EnumObjectItems(None,None,"Process", -1)
17          #if 'TextPad' in instances:
18              #print "TextPad is running"
19          #else:
20              #self.editor = {}
21      
22      item = self.ItemLists[L][idx]
23      file_name = re.sub('[\\/:*"<>|\?]','-',item.name) #make sure all chars are legal file name characters
24      
25      path = os.path.join(os.environ['TMP'],file_name[:50])+'.%s'%NOTE_EXT
26          
27      f = file(path,'w')
28      f.write(self.GetNote())
29      f.close()
30      
31      os.startfile(path)
32      
33      id = item.id
34      for d in self.editor:
35          if d['id'] == id:
36              return
37  
38      ed = {}
39      ed['time'] = os.path.getmtime(path)
40      ed['host'] = self.PropertyDicts[L]['host']
41      ed['table'] = self.PropertyDicts[L]['table']
42      ed['path'] = path
43      ed['id'] = item.id
44      
45      self.editor.append(ed)
46      
47      time.sleep(.1)

File menu methods

OnNewList

code:

 1  def OnNewList(self, event=None):
 
2      if self.modified:
 
3          self.OnUpdate()
 
4      
 
5      if OFFLINE_ONLY is True or REMOTE_HOST is None:
 
6          hosts = [LOCAL_HOST]
 
7      else:
 
8          hosts = [LOCAL_HOST, REMOTE_HOST]
 
9          
10      dlg = wxSingleChoiceDialog(self, 'Databases', 'Choose a database:', hosts, wxCHOICEDLG_STYLE)
11      val = dlg.ShowModal()
12      dlg.Destroy()
13      if val == wxID_OK:
14          host = dlg.GetStringSelection()
15      else:
16          return
17          
18      cursor = self.GetCursor(host)
19      if cursor is None:
20          return
21          
22      dlg = wxTextEntryDialog(self, 'What is the name of the new table?', 'Create Table')
23      val = dlg.ShowModal()
24      dlg.Destroy()
25      if val == wxID_OK:
26          table = dlg.GetValue()
27      else:
28          return
29      
30      if not table:
31          return
32          
33      location, rdbms = host.split(':')
34      
35      if rdbms == 'sqlite':
36          cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
37      else:
38          cursor.execute("SHOW tables")
39      
40      if (table,) in cursor.fetchall():
41          msg = wxMessageDialog(self,
42                                "Table '%s' already exists"%table,
43                                "Duplicate Table",
44                                wxICON_ERROR|wxOK)
45          msg.ShowModal()
46          msg.Destroy()
47          return
48          
49      dlg = wxMessageDialog(self,
50            "Are you sure you want to create Table '%s'?"%table,
51            "Create Table?",
52            wxICON_QUESTION|wxYES_NO)
53  
54      if dlg.ShowModal() == wxID_YES:
55          self.CreateTable(host,table)
56          self.CreateNewNotebookPage(host,table)
57  
58          #self.AddListControl(tab_title) #add listcontrol displays the list
59          
60          #self.OnNewItem()
61          
62      dlg.Destroy()
63  
64  

OnFileList

code:

 1  def OnFileList(self, evt=None, path=None):
 
2      if self.modified:
 
3          self.OnUpdate()
 
4          
 
5      #if there is no event, we got here through the start up loading of lists
 
6      if evt:
 
7          fileNum = evt.GetId() - wxID_FILE1                  
 
8          path = self.filehistory.GetHistoryFile(fileNum)
 
9          location, rdbms, table = path.split(':')
10          host = '%s:%s'%(location, rdbms)
11          # only need to check if table is open if this is not at startup
12          if table in [p['table'] for p in self.PropertyDicts if p['host'] == host]:
13              dlg = wxMessageDialog(self,"%s (%s) is already open!"%(table,host),"List Open",wxICON_ERROR|wxOK)
14              dlg.ShowModal()
15              dlg.Destroy()
16              return
17          
18      else:
19          location, rdbms, table = path.split(':')
20          host = '%s:%s'%(location, rdbms)
21      
22      cursor = self.GetCursor(host)
23      if cursor is None:
24          return
25          
26      if rdbms == 'sqlite':
27          sql = "SELECT name FROM sqlite_master WHERE name = '%s'"%table
28      else:
29          sql = "SHOW TABLES LIKE '%s'"%table
30      
31      cursor.execute(sql)
32      if not cursor.fetchall():
33          dlg = wxMessageDialog(self,
34                      "Table '%s' at host '%s' does not appear to exist!"%(table,host),
35                      "Table does not exist",
36                      wxICON_ERROR|wxOK)
37          dlg.ShowModal()
38          dlg.Destroy()
39          return
40          
41      self.CreateNewNotebookPage(host,table)
42  

OnOpenList

code:

 1  def OnOpenList(self, evt=None):
 
2      if self.modified:
 
3          self.OnUpdate()
 
4          
 
5      tree = {}
 
6      
 
7      if OFFLINE_ONLY is True or REMOTE_HOST is None:
 
8          hosts = [LOCAL_HOST]
 
9      else:
10          hosts = [LOCAL_HOST, REMOTE_HOST]
11          
12      for host in hosts:
13          cursor = self.GetCursor(host)
14          if cursor:
15              if host.split(':')[1] == 'sqlite':
16                  sql = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
17              else:
18                  sql = "SHOW TABLES" #sorted
19      
20              cursor.execute(sql)
21              results = cursor.fetchall()
22      
23              #excluding already open tables + 'system' tables
24              excluded_tables = [p['table'] for p in self.PropertyDicts if p['host'] == host]
25              excluded_tables.extend(['user_sync','sync','owners'])
26      
27              tables = [t for (t,) in results if t not in excluded_tables]
28      
29              tree[host] = tables
30  
31      dlg = TreeDialog(self, "Open List", tree=tree)
32      val = dlg.ShowModal()
33      dlg.Destroy()
34      if val == wxID_OK:
35          sel = dlg.TreeCtrl.GetSelection()
36          table = dlg.TreeCtrl.GetItemText(sel)
37          sel = dlg.TreeCtrl.GetItemParent(sel)
38          host = dlg.TreeCtrl.GetItemText(sel)
39          
40          if host in hosts: #takes care of highlighting root or hosts
41              self.CreateNewNotebookPage(host,table)

OnDeleteList

code:

 1  def OnDeleteList(self, evt=None):
 
2      #ini controls whether the menu item is enabled
 
3      Properties = self.PropertyDicts[self.L]
 
4      host = Properties['host']
 
5      table = Properties['table']
 
6          
 
7      #if table is in SYNC_TABLES, should we make a point of that?
 
8      dlg = wxMessageDialog(self,
 
9                          "Are you sure that you want to delete table %s (%s)?\n(Please note that you cannot recover it once it is deleted!)"%(table,host),
10                          "Delete Table...",
11                          wxICON_EXCLAMATION|wxYES_NO|wxNO_DEFAULT)
12      
13      val = dlg.ShowModal()
14      dlg.Destroy()
15      if val == wxID_NO:
16          return
17          
18      rdbms = host.split(':')[1]
19      
20      if rdbms == 'mysql':
21          dlg = wxMessageDialog(self,
22                          "Are you sure really really sure you want to delete table %s (%s)?\n(You really really cannot recover it once it is deleted)"%(table,host),
23                          "Delete Table...",
24                          wxICON_EXCLAMATION|wxYES_NO|wxNO_DEFAULT)
25                          
26          val = dlg.ShowModal()
27          dlg.Destroy()
28          if val == wxID_NO:
29              return
30  
31      cursor = self.Cursors[host]
32      cursor.execute("DROP TABLE %s"%table)
33      
34      self.OnCloseList()
35  

OnCloseList

code:

 1  def OnCloseList(self, evt=None):
 
2      if self.modified:
 
3          self.OnUpdate()
 
4          
 
5      L = self.L
 
6              
 
7      del self.ItemLists[L]
 
8      del self.PropertyDicts[L]
 
9      del self.ListCtrls[L]
10      del self.OwnerLBoxes[L]
11  
12      self.nb.DeletePage(L)        
13  
14      ln = len(self.PropertyDicts)
15      if ln:
16          self.nb.SetSelection(0)
17          self.L = 0
18      else:
19          self.L = -1
20  
21  
22  
23  

OnCloseAll

code:

 1  def OnCloseAll(self, evt=None):
 
2      if self.modified:
 
3          self.OnUpdate()
 
4          
 
5      while self.L != -1:
 
6          self.OnCloseList()
 
7          
 
8      self.name.Clear()
 
9      self.owners.Clear()
10      self.note.Clear()
11      #note that Clearing does set self.modified (eg {'name':1})
12      self.modified = {}
13      

OnSaveAsText

code:

 1  def OnSaveAsText(self, evt=None):
 
2      if self.modified:
 
3          self.OnUpdate()
 
4          
 
5      Properties = self.PropertyDicts[self.L]
 
6      wildcard = "txt files (*.txt)|*.txt|All files (*.*)|*.*"
 
7      #dlg = wxFileDialog(self, "Save file", "", Properties['table'], wildcard, wxSAVE|wxOVERWRITE_PROMPT|wxCHANGE_DIR)
 
8          
 
9      body = ""
10      for i,item in enumerate(self.ItemLists[self.L]):
11          body = body+"%d. %s (%d)\n"%(i+1, item.name, item.priority)
12      
13      table = Properties['table']
14      location, rdbms = Properties['host'].split(':')
15      filename = re.sub('[\\/:*"<>|\?]','-','%s-%s-%s'%(location,rdbms,table)) 
16      filename = filename[:50]+'.txt'
17  
18      path = os.path.join(DIRECTORY,filename)
19      
20      f = file(path,'w')
21      f.write(body)
22      f.close()
23  
24      os.startfile(path)
25  
26      self.SetStatusText("Saved file %s"%path)
27      

OnArchive

code:

 1  def OnArchive(self, evt=None):
 
2      if self.modified:
 
3          self.OnUpdate()
 
4          
 
5      Properties = self.PropertyDicts[self.L]
 
6      host = Properties['host']
 
7      cursor = self.Cursors[host]
 
8      table = Properties['table']
 
9      rdbms = host.split(':')[1]
10          
11      table_archive = table+'_archive'
12      
13      #need to test for existence of table_archive
14      if rdbms == 'sqlite':
15          cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
16      else:
17          cursor.execute("SHOW tables")
18  
19      results = cursor.fetchall()
20      
21      if (table_archive,) not in results:
22          dlg = wxMessageDialog(self,
23                      "Do you want to create an archive for table %s (%s)"%(table,rdbms),
24                      "Create an archive...",
25                      wxICON_QUESTION|wxYES_NO)
26          val = dlg.ShowModal()
27          dlg.Destroy()
28          if val==wxID_YES:
29              self.CreateTable(host,table_archive)
30          else:
31              return
32      
33      label1 = "In table %s (%s) \narchive all finished items older than:"%(table,rdbms)
34      label2 = "Archive all finished items"
35      dlg = FinishedDialog(self, "Archive completed items", days=7, spin_label=label1, check_label=label2)
36      
37      val = dlg.ShowModal()
38      dlg.Destroy() #dialogs and frames not destroyed right away to allow processing events, methods
39      if val==wxID_CANCEL:
40          return
41          
42      if dlg.check.GetValue():
43          cursor.execute("SELECT id,priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,note FROM "+table+" WHERE finisheddate IS NOT NULL")
44      else:
45          days = dlg.text.GetValue()
46          date = mx.DateTime.today() - int(days)
47          cursor.execute("SELECT id,priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,note FROM "+table+" WHERE finisheddate < %s",(date,))
48  
49      results = cursor.fetchall()
50      dlg = wxMessageDialog(self,
51                          "Archiving will remove %d records from %s.\nDo you want to proceed?"%(len(results),table),
52                          "Proceed to archive...",
53                          wxICON_QUESTION|wxYES_NO)
54      
55      val = dlg.ShowModal()
56      dlg.Destroy()
57      if val == wxID_NO:
58          return
59  
60      if table in SYNC_TABLES:
61          if rdbms == 'sqlite':
62              def track_deletes():
63                  timestamp = mx.DateTime.now()
64                  cursor.execute("INSERT INTO sync (id,action,table_name,name,timestamp) VALUES (%s,%s,%s,%s,%s)",(id,'d',table,name,timestamp))
65          else:
66              def track_deletes():
67                  cursor.execute("INSERT INTO sync (id,action,table_name,user,name) VALUES (%s,%s,%s,%s,%s)",(id,'d',table,USER,name))
68      else:
69          def track_deletes():
70              pass    
71  
72      for row in results:
73          # the next line is necessary because pysqlite returns a tuple-like object that is not a tuple
74          r = tuple(row)
75          id = r[0]
76          name = r[2]
77          cursor.execute("INSERT INTO "+table_archive+"  (id,priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,note) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",r)
78          timestamp = self.TimeStamper(host, cursor, table_archive, id)
79          cursor.execute("DELETE from "+table+" WHERE id = %s", (id,))
80          track_deletes()
81          
82      self.OnRefresh()
83      dlg = wxMessageDialog(self,
84                          "Table %s had items older than %s days successfully archived"%(table,days),
85                          "Archiving successful...",
86                          wxICON_INFORMATION|wxOK)
87      dlg.ShowModal()

OnWorkOffline

code:

 1  def OnWorkOffline(self, evt=None):
 
2      global OFFLINE_ONLY
 
3      OFFLINE_ONLY = not OFFLINE_ONLY
 
4      if OFFLINE_ONLY:
 
5          del self.Cursors[REMOTE_HOST]
 
6      else:
 
7          server = REMOTE_HOST.split(':')[0]
 
8          try:
 
9              socket.gethostbyname(server)
10          except:
11              dlg = wxMessageDialog(None, "Cannot connect to remote server! Will set to work offline.", "ListManager", style=wxOK|wxICON_EXCLAMATION|wxSTAY_ON_TOP)
12              dlg.ShowModal()
13              dlg.Destroy()
14              OFFLINE_ONLY = True
15  
16      self.filemenu.Check(idOFFLINE,OFFLINE_ONLY)
17      

comments:

This method toggles whether we are working offline only or both on and offline.

Display methods

OnItemSelected

code:

 1  def OnItemSelected(self, evt=None):
 
2      if self.modified:
 
3          self.OnUpdate()
 
4  
 
5      if evt:
 
6          idx = evt.GetIndex()
 
7      elif self.curIdx != -1:
 
8          idx = self.curIdx
 
9      else: # really to catch self.curIdx = -1 (see OnDelete and OnRefresh)
10          self.name.Clear() # could be moved out of if
11          self.owners.Clear() # could be moved out of if
12          self.note.Clear()
13          #note that Clearing does set self.modified (eg {'name':1})
14          self.modified = {}
15          return
16      
17      L = self.L
18      item = self.ItemLists[L][idx]
19  
20      self.name.Clear()
21      self.name.AppendText(item.name) #SetValue(item.name) - if you use setvalue you don't get the font
22          
23      self.owners.Clear()
24      self.owners.AppendText('; '.join(item.owners))
25      
26      note = self.GetNote(L,item)
27      if note.find("<leo_file>") != -1:
28          self.note.SetValue("Leo Outline")
29          self.note.SetEditable(False)
30      else:
31          self.note.SetValue(note)
32          self.note.SetEditable(True)
33          
34      self.ListCtrls[L].EnsureVisible(idx)
35      self.curIdx = idx
36      
37      #writing to text widgets caused wxEVT_COMMAND_TEXT_UPDATED which is caught by EVT_TEXT, which updates self.modified
38      self.modified={}
39  

OnItemActivated

code:

1  def OnItemActivated(self,evt):
2      print "On Activated"
3      

OnShowAll

code:

 1  def OnShowAll(self, evt=None):
 
2      L = self.L
 
3      OLBox = self.OwnerLBoxes[L]
 
4      
 
5      Properties = self.PropertyDicts[L]
 
6      Properties['showfinished'] = -1
 
7      Properties['owner'] = '*ALL'
 
8      
 
9      OLBox.SetStringSelection('*ALL')
10      
11      self.OnRefresh()

OnRefresh

code:

 1  def OnRefresh(self, evt=None):
 
2      #OnItemSelected should be able to handle no items so this could be very short
 
3      L = self.L
 
4      
 
5      results = self.ReadFromDB()
 
6      self.ItemLists[L] = self.CreateAndDisplayList(results)
 
7  
 
8      if self.ItemLists[L]:
 
9          self.ListCtrls[L].SetItemState(0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
10          self.curIdx = 0
11      else:
12          self.curIdx = -1            
13          
14      self.OnItemSelected()

OnFilterOwners

code:

1  def OnFilterOwners(self, evt=None):
2      if self.modified:
3          self.OnUpdate()
4      sel = self.OwnerLBoxes[self.L].GetStringSelection()
5      
6      if sel:
7          self.PropertyDicts[self.L]['owner'] = sel
8          self.OnRefresh()

OnColumnClick (to sort columns)

code:

 1  def OnColumnClick(self, evt):
 
2      col_num = evt.GetColumn()
 
3      L = self.L
 
4      LCtrl = self.ListCtrls[L]
 
5      Sort = self.PropertyDicts[L]['sort']
 
6      attr2col = self.attr2col_num
 
7      
 
8      prev_sort_attr = Sort.get('attribute') #if this is the first sort Properties['sort'] is {}
 
9      
10      #following is a little bit ugly but gets the key from the value, which is col_num
11      Sort['attribute'] = attr2col.keys()[attr2col.values().index(col_num)]
12      
13      if prev_sort_attr == Sort['attribute']:
14          Sort['direction'] = not Sort['direction']
15      else:
16          Sort['direction'] = 0
17      
18      self.OnRefresh()
19  
20      LCtrl.ClearColumnImage(attr2col['priority'])
21      LCtrl.ClearColumnImage(attr2col['date'])
22      img_num = LCtrl.arrows[Sort['direction']]
23      LCtrl.SetColumnImage(col_num, img_num)
24      

OnShowFinished

code:

 1  def OnShowFinished(self,evt):
 
2      Properties = self.PropertyDicts[self.L]
 
3      label1 = "Enter the number of days to retain\ncompleted tasks in the display:"
 
4      label2 = "Show all finished items"
 
5      dlg = FinishedDialog(self, "Display of completed items", days=Properties['showfinished'], spin_label=label1, check_label=label2)
 
6      if dlg.ShowModal()==wxID_OK:
 
7          if dlg.check.GetValue():
 
8              Properties['showfinished'] = -1
 
9          else:
10              days = dlg.text.GetValue()
11              Properties['showfinished'] = int(days)                  
12          self.OnRefresh()
13      dlg.Destroy()
14      

OnColumnRightClick (popup to change date displayed)

code:

 1  def OnColumnRightClick(self, evt=None):
 
2      col = evt.GetColumn()
 
3      if col != self.attr2col_num['date']:
 
4          return
 
5          
 
6      L = self.L
 
7      LCtrl = self.ListCtrls[L]
 
8      Properties = self.PropertyDicts[L]
 
9      
10      #x,y = evt.GetPosition()
11      datemenu = wxMenu()
12      
13      for i,date in enumerate(['Create Date','Last Modified','Due Date','Completion Date']):
14          datemenu.Append(200+i, date)
15          EVT_MENU(self, 200+i, lambda e, i=i: self.ChangeDateDisplayed(e,i))
16  
17      x = LCtrl.GetColumnWidth(1)+ LCtrl.GetColumnWidth(2) + LCtrl.GetColumnWidth(3)
18      self.PopupMenu(datemenu,(x,40))
19      datemenu.Destroy()
20  
21  

OnDisplayDateCategory

code:

 1  def OnDisplayDateCategory(self, evt=None):
 
2      dlg = wxSingleChoiceDialog(self, 'Date Display', 'Choose a date to display:',
 
3                      ['Create Date','Last Modified','Due Date','Completion Date']
 
4                      , wxOK|wxCANCEL)
 
5      val = dlg.ShowModal()
 
6      dlg.Destroy()
 
7      
 
8      if val == wxID_OK:
 
9          idx = dlg.GetSelection()
10          self.ChangeDateDisplayed(i=idx)
11          

ChangeDateDisplayed

code:

 1  def ChangeDateDisplayed(self, evt=None, i=0):
 
2      L = self.L
 
3      LCtrl = self.ListCtrls[L]
 
4      self.PropertyDicts[L]['LCdate'] = displaydate = ('createdate','timestamp','duedate','finisheddate')[i]  
 
5      col_num = self.attr2col_num['date']
 
6      col_info = LCtrl.GetColumn(col_num)
 
7      col_info.SetText(self.date_titles[displaydate])
 
8      LCtrl.SetColumn(col_num,col_info)
 
9      self.DisplayList(self.ItemLists[L])
10      #self.OnRefresh() #have gone back and forth but think that it should be self.DisplayList

DisplayList

code:

 1  def DisplayList(self, List, L=None):
 
2      #OnPasteItems needs to be able to have an L that is not self.L
 
3      if L is None:
 
4          L = self.L
 
5      LCtrl = self.ListCtrls[L]
 
6      LCdate = self.PropertyDicts[L]['LCdate']
 
7      if LCdate == 'timestamp':
 
8          format = '%m/%d %H:%M:%S'
 
9      else:
10          format = '%m/%d/%y'
11      LCtrl.DeleteAllItems()
12      
13      for x,item in enumerate(List):
14          << draw item >>
15          
16  
<< draw item >>

code:

 1  LCtrl.InsertImageStringItem(x, str(item.priority), LCtrl.idx1)
 
2  LCtrl.SetStringItem(x,1,item.name)
 
3  LCtrl.SetStringItem(x,2,'; '.join(item.owners))
 
4  date = item.__dict__[LCdate]
 
5  LCtrl.SetStringItem(x,3,date and date.Format(format) or "")
 
6  
 
7  if item.finisheddate:
 
8      LC_Item = LCtrl.GetItem(x)
 
9      LC_Item.SetImage(LCtrl.idx0) #might just want generic number or greyed one two three
10      LC_Item.SetTextColour(wxLIGHT_GREY)
11      LCtrl.SetItem(LC_Item)
12      
13  elif item.priority==2:
14      LC_Item = LCtrl.GetItem(x)
15      f = self.LC_font
16      f.SetWeight(wxBOLD)
17      LC_Item.SetFont(f)
18      f.SetWeight(wxNORMAL) #resetting weight
19      LCtrl.SetItem(LC_Item)
20  
21  elif item.priority==3:
22      LC_Item = LCtrl.GetItem(x)
23      f = self.LC_font
24      f.SetWeight(wxBOLD)
25      LC_Item.SetFont(f)
26      f.SetWeight(wxNORMAL) #return to normal
27      LC_Item.SetTextColour(wxRED)

Printing methods

OnPageSetup

code:

 1  def OnPageSetup(self, evt):
 
2      #need to pass printdata to tableprint
 
3  
 
4      psdata = wxPageSetupDialogData()
 
5  
 
6      # if want to vary margins will need to save them as ivars and then set
 
7      #psdata.SetMarginTopLeft((self.Left,self.Top))
 
8      psdata.EnableMargins(False)
 
9      psdata.SetPrintData(self.printdata) #gets Paper Orientation and PaperId info from printdata
10      
11      dlg = wxPageSetupDialog(self, psdata)
12      if dlg.ShowModal() == wxID_OK:
13          self.printdata = dlg.GetPageSetupData().GetPrintData()
14          dlg.Destroy()

OnPrint

code:

 1  def OnPrint(self, evt=None, prev=False, showprtdlg=True):           #???self.psdata = psdata
 
2      IList = self.ItemLists[self.L]
 
3      Properties = self.PropertyDicts[self.L]
 
4      
 
5      prt = PrintTable(self.printdata) #self.printdata is the wxPrintData object with Orientation Info
 
6  
 
7      font_name = prt.default_font_name
 
8      prt.text_font = {'Name':font_name, 'Size':11, 'Colour':[0, 0, 0], 'Attr':[0, 0, 0]}
 
9      prt.label_font = {'Name':font_name, 'Size':12, 'Colour':[0, 0, 0], 'Attr':[1, 0, 0]}
10      prt.header_font = {'Name':font_name, 'Size':14, 'Colour':[0, 0, 0], 'Attr':[1, 0, 0]}
11      
12      prt.row_def_line_colour = wxLIGHT_GREY
13      prt.column_def_line_colour = wxLIGHT_GREY
14      
15      prt.left_margin = 0.5
16  
17      data = []
18      for row,item in enumerate(IList):       
19          data.append([str(item.priority),
20                      item.name,
21                      item.duedate and item.duedate.Format('%m/%d/%y') or '',
22                      '; '.join([x.split(',')[0] for x in item.owners])]) #just last names
23                      
24          if item.finisheddate:
25              prt.SetCellText(row, 0, wxLIGHT_GREY)
26              prt.SetCellText(row, 1, wxLIGHT_GREY)
27              prt.SetCellText(row, 2, wxLIGHT_GREY)
28              prt.SetCellText(row, 3, wxLIGHT_GREY)
29  
30      prt.data = data
31      prt.label = ['P','Item','Due','Owner']
32      
33      if self.printdata.GetOrientation() == wxPORTRAIT:
34          prt.set_column = [.2, 5, .65, 1]
35      else:
36          prt.set_column = [.2, 7, .65, 1.5]
37                         
38      title = "Table: %s   Owner: %s    "%(Properties['table'],Properties['owner'])
39      prt.SetHeader(title, type='Date & Time', align=wxALIGN_LEFT, indent = 1.5)
40      prt.SetFooter("Page No ", type ="Num")
41  
42      if prev:
43          prt.Preview()
44      else:
45          prt.Print(prompt=showprtdlg)

Exiting methods

OnWindowExit

code:

1  def OnWindowExit(self, evt):
2      #this is called if you close the ListManager Window with the X
3      if evt.CanVeto():
4          self.OnExit()
5      else:
6          evt.Skip()

OnExit

code:

1  def OnExit(self, event=None):   
2      << save configuration file >>
3      sys.stderr.dlg.Destroy() #destroys the error dialog; need to do this to shut down correctly
4      if self.ModifierDialog: #only reason to check is if closed before ModifierDialog is constructed
5          self.ModifierDialog.Destroy()
6      self.Close(1)
<<save configuration file>>

code:

 1  cp.remove_section('Files')
 
2  cp.add_section("Files")
 
3  
 
4  x,y = self.GetSizeTuple()
 
5  
 
6  cp.set('Configuration','x', str(x))
 
7  cp.set('Configuration','y', str(y))
 
8  
 
9  numfiles = self.filehistory.GetNoHistoryFiles()
10  
11  for n in range(numfiles):
12      cp.set("Files", "path%d"%n, self.filehistory.GetHistoryFile(n))
13  
14  try:
15      #you have to give ConfigParser a writable object
16      cfile = file(config_file, 'w')
17      cp.write(cfile)
18      cfile.close()
19  except IOError:
20      print "The configuration file can't be written!"
21      time.sleep(10) #so you can see that there was a problem

Find methods

OnFind

code:

1  def OnFind(self, evt=None):
2      self.FindDialog.Show(True)
3      self.FindDialog.FindText.SetSelection(-1,-1)
4      self.FindDialog.FindText.SetFocus()
5  
6  

FindString

code:

 1  def FindString(self, evt=None):
 
2      L = self.L
 
3      Properties = self.PropertyDicts[L]
 
4      cursor = self.Cursors[Properties['host']]
 
5      table = Properties['table']
 
6      
 
7      pat = self.FindDialog.FindText.GetValue()
 
8      likepat = r"'%"+pat+r"%'"
 
9      finished = self.FindDialog.SearchFinished.GetValue()
10      notes = self.FindDialog.SearchNotes.GetValue()
11      
12      if finished:
13          WHERE = "WHERE "
14      else:
15          WHERE = "WHERE finisheddate IS NULL AND "
16      
17      if notes:
18          SELECT = "SELECT priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,id,timestamp,note FROM %s "%table
19          WHERE = WHERE + "(name LIKE %s OR note LIKE %s) ORDER BY timestamp DESC"%(likepat,likepat)
20      else:
21          SELECT = "SELECT priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,id,timestamp FROM %s "%table
22          WHERE = WHERE + "name LIKE %s ORDER BY timestamp DESC"%likepat
23  
24      sql = SELECT + WHERE                    
25      try:
26          cursor.execute(sql)
27      except:
28          print "Cannot read %s: %s"%(Properties['host'],table)
29          return
30      else:
31          results = cursor.fetchall()
32      
33      case = self.FindDialog.MatchCase.GetValue()
34      whole = self.FindDialog.MatchWhole.GetValue()
35      
36      if whole:
37          pat = '\\b%s\\b'%pat
38      
39      if case:
40          z = re.compile(pat)
41      else:
42          z =re.compile(pat, re.I)
43  
44      if notes:
45          results = [x for x in results if re.search(z,x[1]) or re.search(z,x[10])]
46      else:
47          results = [x for x in results if re.search(z,x[1])]
48      
49      Properties['LCdate'] = 'timestamp'
50      self.ItemLists[L]= IList = self.CreateAndDisplayList(results)
51      
52      LCtrl = self.ListCtrls[L]
53      col_num = self.attr2col_num['date']
54      col_info = LCtrl.GetColumn(col_num)
55      col_info.SetText(self.date_titles['timestamp'])
56      LCtrl.SetColumn(col_num,col_info)
57      
58      if IList:
59          self.curIdx = 0
60          LCtrl.SetItemState(0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
61      else:           
62          self.curIdx = -1
63          
64      self.OnItemSelected()
65      
66      Properties['sort'] = {'direction':0,'attribute':'date'}
67      Properties['owner'] = '*ALL'
68      
69      owner_idx = self.OwnerLBoxes[L].GetSelection()
70      if owner_idx != -1:
71          self.OwnerLBoxes[L].SetSelection(owner_idx, 0) #get exception if index = -1
72  
73      self.SetStatusText("Found %d items"%len(IList))

FindNode

code:

 1  def FindNode(self, item, showfinished=True):
 
2      L = self.L
 
3      LCtrl = self.ListCtrls[L]
 
4      Properties = self.PropertyDicts[L]
 
5      
 
6      Properties['owner'] = '*ALL'
 
7      Properties['showfinished'] = showfinished
 
8      
 
9      self.ItemLists[L] = IList = self.CreateAndDisplayList(self.ReadFromDB())
10      
11      id = item.id
12      idx = -1
13      for item in IList:
14          idx+=1
15          if id == item.id:
16              break
17      else:
18          idx = -1
19  
20      if idx != -1:   
21          LCtrl.SetItemState(idx, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
22          LCtrl.EnsureVisible(idx)
23      self.curIdx = idx
24