~launchpad-p-s/sofastatistics/main

« back to all changes in this revision

Viewing changes to table_edit.py

  • Committer: Grant Paton-Simpson
  • Date: 2009-05-19 04:21:43 UTC
  • Revision ID: g@ubuntu-20090519042143-p561mbokz3inefvd
Initial import

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from decimal import Decimal
 
2
import pprint
 
3
import util
 
4
import wx
 
5
import wx.grid
 
6
 
 
7
import getdata
 
8
 
 
9
MISSING_VAL_INDICATOR = "."
 
10
 
 
11
"""
 
12
DbTbl is the link between the grid and the underlying data.
 
13
TextEditor is the custom grid cell editor - currently only used for the cells
 
14
    in the new row.  Needed so that the edited value can be captured when
 
15
    navigating away from a cell in editing mode (needed for validation).
 
16
TblEditor is the grid (the Dialog containing the grid).
 
17
Cell values are taken from the database in batches and cached for performance
 
18
    reasons.
 
19
Navigation around inside the grid triggers data saving (cells updated or 
 
20
    a new row added).  Validation occurs first to ensure that values will be
 
21
    acceptable to the underlying database.  If not, the cursor stays at the 
 
22
    original location.
 
23
"""
 
24
 
 
25
 
 
26
class DbTbl(wx.grid.PyGridTableBase):
 
27
    def __init__(self, grid, dbe, conn, cur, tbl, flds, var_labels, idxs, 
 
28
                 read_only):
 
29
        wx.grid.PyGridTableBase.__init__(self)
 
30
        self.debug = False
 
31
        self.tbl = tbl
 
32
        self.grid = grid
 
33
        self.dbe = dbe
 
34
        self.quote = getdata.DBE_MODULES[self.dbe].quote_identifier
 
35
        self.conn = conn
 
36
        self.cur = cur
 
37
        self.read_only = read_only
 
38
        self.SetNumberRows()
 
39
        self.flds = flds # dict with key = fld name and vals = dict of characteristics
 
40
        self.fld_names = getdata.FldsDic2FldNamesLst(flds_dic=self.flds)
 
41
        self.fld_labels = [var_labels.get(x, x.title()) for x in self.fld_names]
 
42
        self.idxs = idxs
 
43
        
 
44
        self.idx_id, self.must_quote = self.GetIndexCol()
 
45
        self.id_col_name = self.fld_names[self.idx_id]
 
46
        self.SetRowIdDic()
 
47
        self.row_vals_dic = {} # key = row, val = tuple of values
 
48
        if self.debug:
 
49
            pprint.pprint(self.fld_names)
 
50
            pprint.pprint(self.fld_labels)
 
51
            pprint.pprint(self.flds)
 
52
            pprint.pprint(self.row_id_dic)
 
53
        self.new_buffer = {} # where new values are stored until ready to be saved
 
54
        self.new_is_dirty = False # TextEditor can set to True.  Is reset to 
 
55
            # False when adding a new record
 
56
    
 
57
    def SetRowIdDic(self):
 
58
        """
 
59
        Row number and the value of the primary key will not always be 
 
60
            the same.  Need quick way of translating from row e.g. 0
 
61
            to value of the id field e.g. "ABC123" or 128797 or even 0 ;-).
 
62
        """
 
63
        SQL_get_id_vals = "SELECT %s FROM %s ORDER BY %s" % \
 
64
            (self.quote(self.id_col_name), self.quote(self.tbl), 
 
65
             self.quote(self.id_col_name))
 
66
        self.cur.execute(SQL_get_id_vals)
 
67
        # NB could easily be 10s or 100s of thousands of records
 
68
        ids_lst = [x[0] for x in self.cur.fetchall()]
 
69
        n_lst = range(len(ids_lst)) # 0-based to match row_n
 
70
        self.row_id_dic = dict(zip(n_lst, ids_lst))        
 
71
    
 
72
    def GetFldDic(self, col):
 
73
        fld_name = self.fld_names[col]
 
74
        return self.flds[fld_name]
 
75
    
 
76
    def GetIndexCol(self):
 
77
        """
 
78
        Pick first unique indexed column and return 
 
79
            col position, and must_quote (e.g. for string or date fields).
 
80
        TODO - cater to composite indexes properly esp when getting values
 
81
        idxs = [idx0, idx1, ...]
 
82
        each idx = (name, is_unique, flds)
 
83
        """
 
84
        idx_is_unique = 1
 
85
        idx_flds = 2
 
86
        for idx in self.idxs:
 
87
            name = idx[getdata.IDX_NAME] 
 
88
            is_unique = idx[getdata.IDX_IS_UNIQUE]
 
89
            fld_names = idx[getdata.IDX_FLDS]
 
90
            if is_unique:
 
91
                # pretend only ever one field (TODO see above)
 
92
                fld_to_use = fld_names[0]
 
93
                must_quote = not self.flds[fld_to_use][getdata.FLD_BOLNUMERIC]
 
94
                col_idx = self.fld_names.index(fld_to_use)
 
95
                if self.debug:
 
96
                    print "Col idx: %s" % col_idx
 
97
                    print "Must quote:" + str(must_quote)
 
98
                return col_idx, must_quote
 
99
    
 
100
    def GetNumberCols(self):
 
101
        num_cols = len(self.flds)
 
102
        if self.debug:
 
103
            print "N cols: %s" % num_cols
 
104
        return num_cols
 
105
 
 
106
    def SetNumberRows(self):
 
107
        SQL_num_rows = "SELECT COUNT(*) FROM %s" % self.quote(self.tbl)
 
108
        self.cur.execute(SQL_num_rows)
 
109
        self.num_rows = self.cur.fetchone()[0]
 
110
        if not self.read_only:
 
111
            self.num_rows += 1
 
112
        if self.debug:
 
113
            print "N rows: %s" % self.num_rows
 
114
        self.rows_to_fill = self.num_rows - 1 if self.read_only \
 
115
                else self.num_rows - 2
 
116
    
 
117
    def GetNumberRows(self):
 
118
        return self.num_rows
 
119
    
 
120
    def NewRow(self, row):
 
121
        new_row = row > self.rows_to_fill
 
122
        return new_row
 
123
    
 
124
    def GetRowLabelValue(self, row):
 
125
        new_row = row > self.rows_to_fill
 
126
        if new_row:
 
127
            if self.new_is_dirty:
 
128
                return "..."
 
129
            else:
 
130
                return "*"
 
131
        else:
 
132
            return row + 1
 
133
    
 
134
    def GetColLabelValue(self, col):
 
135
        return self.fld_labels[col]
 
136
    
 
137
    def NoneToMissingVal(self, val):
 
138
        if val == None:
 
139
            val = MISSING_VAL_INDICATOR
 
140
        return val
 
141
    
 
142
    def GetValue(self, row, col):
 
143
        """
 
144
        NB row and col are 0-based.
 
145
        The performance of this method is critical to the performance
 
146
            of the grid as a whole - displaying, scrolling, updating etc.
 
147
        Very IMPORTANT to have a unique field we can use to identify rows
 
148
            if at all possible.
 
149
        Much, much faster to do one database call per row than once per cell 
 
150
            (esp with lots of columns).
 
151
        On larger datasets (> 10,000) performance is hideous
 
152
            using order by and limit or similar.
 
153
        Need to be able to filter to individual, unique, indexed row.
 
154
        Use unique index where possible - if < 1000 recs and no unique
 
155
            index, use the method below (while telling the user the lack of 
 
156
            an index significantly harms performance esp while scrolling). 
 
157
        SQL_get_value = "SELECT %s " % col_name + \
 
158
            " FROM %s " % self.tbl + \
 
159
            " ORDER BY %s " % id_col_name + \
 
160
            " LIMIT %s, 1" % row
 
161
        NB if not read only will be an empty row at the end.
 
162
        Turn None (Null) into . as missing value identifier.
 
163
        """
 
164
        # try cache first
 
165
        try:
 
166
            return self.row_vals_dic[row][col]
 
167
        except KeyError:
 
168
            extra = 10
 
169
            """
 
170
            If new row, just return value from new_buffer (or missing value).
 
171
            Otherwise, fill cache (up to extra (e.g. 10) rows above and below) 
 
172
                 and then grab this col value.
 
173
            More expensive for first cell but heaps 
 
174
                less expensive for rest.
 
175
            Set cell editor while at it.  Very expensive for large table 
 
176
                so do it as needed.
 
177
            """
 
178
            if self.NewRow(row):
 
179
                return self.new_buffer.get((row, col), MISSING_VAL_INDICATOR)
 
180
            # identify row range            
 
181
            row_min = row - extra if row - extra > 0 else 0
 
182
            row_max = row + extra if row + extra < self.rows_to_fill \
 
183
                else self.rows_to_fill
 
184
            # create IN clause listing id values
 
185
            IN_clause_lst = []
 
186
            if self.must_quote:
 
187
                val_part = "\"%s\"" 
 
188
            else:
 
189
                val_part = "%s"
 
190
            for row_n in range(row_min, row_max + 1):
 
191
                IN_clause_lst.append(val_part % self.row_id_dic[row_n])
 
192
            IN_clause = ", ".join(IN_clause_lst)
 
193
            SQL_get_values = "SELECT * " + \
 
194
                " FROM %s " % self.quote(self.tbl) + \
 
195
                " WHERE %s IN(%s)" % (self.quote(self.id_col_name), 
 
196
                                      IN_clause) + \
 
197
                " ORDER BY %s" % self.quote(self.id_col_name)
 
198
            if self.debug:
 
199
                print SQL_get_values
 
200
            self.cur.execute(SQL_get_values)
 
201
            row_idx = row_min
 
202
            for data_tup in self.cur.fetchall(): # tuple of values
 
203
                self.AddDataToRowValsDic(self.row_vals_dic, row_idx, data_tup)
 
204
                row_idx += 1
 
205
            return self.row_vals_dic[row][col] # the bit we're interested in now
 
206
    
 
207
    def AddDataToRowValsDic(self, row_vals_dic, row_idx, data_tup):
 
208
        """
 
209
        row_vals_dic - key = row, val = tuple of values
 
210
        Add new row to row_vals_dic.
 
211
        """
 
212
        proc_data_tup = tuple([self.NoneToMissingVal(x) for x \
 
213
                              in data_tup])
 
214
        row_vals_dic[row_idx] = proc_data_tup
 
215
    
 
216
    def IsEmptyCell(self, row, col):
 
217
        value = self.GetValue(row, col)
 
218
        return value == MISSING_VAL_INDICATOR
 
219
    
 
220
    def SetValue(self, row, col, value):
 
221
        # not called if enter edit mode and then directly jump out ...
 
222
        if self.NewRow(row):
 
223
            self.new_buffer[(row, col)] = value
 
224
        else:
 
225
            existing_row_data_list = list(self.row_vals_dic.get(row))
 
226
            if existing_row_data_list:
 
227
                existing_row_data_list[col] = value
 
228
            row_id = self.GetValue(row, self.idx_id)
 
229
            col_name = self.fld_names[col]
 
230
            if self.must_quote:
 
231
                val_part = "\"%s\"" % self.row_id_dic[row]
 
232
            else:
 
233
                val_part = "%s" % self.row_id_dic[row]
 
234
            SQL_update_value = "UPDATE %s " % self.tbl + \
 
235
                " SET %s = \"%s\" " % (self.quote(col_name), value) + \
 
236
                " WHERE %s = " % self.id_col_name + \
 
237
                val_part
 
238
            if self.debug: print SQL_update_value
 
239
            self.cur.execute(SQL_update_value)
 
240
            self.conn.commit()
 
241
 
 
242
    def DisplayNewRow(self):
 
243
        """
 
244
        http://wiki.wxpython.org/wxGrid
 
245
        The example uses getGrid() instead of wxPyGridTableBase::GetGrid()
 
246
          can be issues depending on version of wxPython.
 
247
        Safest to pass in the grid.
 
248
        """
 
249
        self.grid.BeginBatch()
 
250
        msg = wx.grid.GridTableMessage(self, 
 
251
                            wx.grid.GRIDTABLE_NOTIFY_ROWS_APPENDED, 1)
 
252
        self.grid.ProcessTableMessage(msg)
 
253
        msg = wx.grid.GridTableMessage(self, 
 
254
                            wx.grid.GRIDTABLE_REQUEST_VIEW_GET_VALUES)
 
255
        self.grid.ProcessTableMessage(msg)
 
256
        self.grid.EndBatch()
 
257
        self.grid.ForceRefresh()
 
258
 
 
259
 
 
260
class TextEditor(wx.grid.PyGridCellEditor):
 
261
    
 
262
    def __init__(self, tbl_editor, row, col, new_row, new_buffer):
 
263
        wx.grid.PyGridCellEditor.__init__(self)
 
264
        self.debug = False
 
265
        self.tbl_editor = tbl_editor
 
266
        self.row = row
 
267
        self.col = col
 
268
        self.new_row = new_row
 
269
        self.new_buffer = new_buffer
 
270
    
 
271
    def BeginEdit(self, row, col, grid):
 
272
        if self.debug: print "Editing started"
 
273
        val = grid.GetTable().GetValue(row, col)
 
274
        self.start_val = val
 
275
        self.txt.SetValue(val)
 
276
        self.txt.SetFocus()
 
277
 
 
278
    def Clone(self):
 
279
        return TextEditor(self.tbl_editor, self.row, self.col, self.new_row, 
 
280
                          self.new_buffer)
 
281
    
 
282
    def Create(self, parent, id, evt_handler):
 
283
        # created when clicked
 
284
        self.txt = wx.TextCtrl(parent, -1, "")
 
285
        self.SetControl(self.txt)
 
286
        if evt_handler:
 
287
            # so the control itself doesn't handle events but passes to handler
 
288
            self.txt.PushEventHandler(evt_handler)
 
289
            evt_handler.Bind(wx.EVT_KEY_DOWN, self.OnTxtEdKeyDown)
 
290
    
 
291
    def EndEdit(self, row, col, grid):
 
292
        if self.debug: print "Editing ending"
 
293
        changed = False
 
294
        val = self.txt.GetValue()
 
295
        if val != self.start_val:
 
296
            changed = True
 
297
            grid.GetTable().SetValue(row, col, val)
 
298
        if self.debug:
 
299
            print "Value entered was \"%s\"" % val
 
300
            print "Editing ended"
 
301
        if changed:
 
302
            if self.debug: print "Some data in new record has changed"
 
303
            self.tbl_editor.dbtbl.new_is_dirty = True
 
304
        return changed
 
305
        
 
306
    def StartingKey(self, event):
 
307
        key_code = event.GetKeyCode()
 
308
        if self.debug: print "Starting key was \"%s\"" % chr(key_code)
 
309
        if key_code <= 255 :
 
310
            self.txt.SetValue(chr(key_code))
 
311
            self.txt.SetInsertionPoint(1)
 
312
        else:
 
313
            event.Skip()
 
314
 
 
315
    def Reset(self):
 
316
        pass # N/A
 
317
    
 
318
    def OnTxtEdKeyDown(self, event):
 
319
        """
 
320
        Very tricky code re: impact of event.Skip().  Small changes can 
 
321
            have big impacts. Test thoroughly.
 
322
        """
 
323
        if event.GetKeyCode() in [wx.WXK_TAB]:
 
324
            raw_val = self.txt.GetValue()
 
325
            if self.debug:
 
326
                print "[OnTxtEdKeyDown] Tabbing away from field with " + \
 
327
                    "value \"%s\"" % raw_val
 
328
            if self.new_row:
 
329
                if self.debug: print "Tabbing within new row"
 
330
                self.new_buffer[(self.row, self.col)] = raw_val
 
331
                final_col = (self.col == len(self.tbl_editor.flds) - 1)
 
332
                if final_col:
 
333
                    # only attempt to save if value is OK to save
 
334
                    if not self.tbl_editor.CellOKToSave(self.row, self.col):
 
335
                        self.tbl_editor.grid.SetFocus()
 
336
                        return
 
337
                    if self.debug: print "OnTxtEdKeyDown - Trying to leave " + \
 
338
                        "new record"
 
339
                    saved_ok = self.tbl_editor.SaveRow(self.row)
 
340
                    if saved_ok:
 
341
                        if self.debug: print "OnTxtEdKeyDown - Was able " + \
 
342
                            "to save record after tabbing away"
 
343
                    else:
 
344
                        # CellOkToSave obviously failed to give correct answer
 
345
                        if self.debug: print "OnTxtEdKeyDown - Unable " + \
 
346
                            "to save record after tabbing away"
 
347
                        wx.MessageBox("Unable to save record - please " + \
 
348
                                      "check values")
 
349
                    return
 
350
        event.Skip()
 
351
        
 
352
        
 
353
class TblEditor(wx.Dialog):
 
354
    def __init__(self, parent, dbe, conn, cur, db, tbl_name, flds, var_labels,
 
355
                 idxs, read_only=True):
 
356
        self.debug = False
 
357
        wx.Dialog.__init__(self, None, 
 
358
                           title="Data from %s.%s" % (db, tbl_name),
 
359
                           size=(500, 500),
 
360
                           style=wx.MINIMIZE_BOX | wx.MAXIMIZE_BOX | \
 
361
                           wx.RESIZE_BORDER | wx.SYSTEM_MENU | \
 
362
                           wx.CAPTION | wx.CLOSE_BOX | \
 
363
                           wx.CLIP_CHILDREN)
 
364
        self.parent = parent
 
365
        self.dbe = dbe
 
366
        self.conn = conn
 
367
        self.cur = cur
 
368
        self.tbl_name = tbl_name
 
369
        self.flds = flds
 
370
        self.panel = wx.Panel(self, -1)
 
371
        self.szrMain = wx.BoxSizer(wx.VERTICAL)
 
372
        self.grid = wx.grid.Grid(self.panel, size=(500, 600))
 
373
        self.grid.EnableEditing(not read_only)
 
374
        self.dbtbl = DbTbl(self.grid, self.dbe, self.conn, self.cur, tbl_name, 
 
375
                           self.flds, var_labels, idxs, read_only)
 
376
        self.grid.SetTable(self.dbtbl, takeOwnership=True)
 
377
        if read_only:
 
378
            self.grid.SetGridCursor(0, 0)
 
379
            self.current_row_idx = 0
 
380
            self.current_col_idx = 0
 
381
        else:
 
382
            # start at new line
 
383
            new_row_idx = self.dbtbl.GetNumberRows() - 1
 
384
            self.FocusOnNewRow(new_row_idx)
 
385
            self.SetNewRowEd(new_row_idx)
 
386
        self.SetColWidths()
 
387
        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.OnCellChange)
 
388
        self.grid.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnSelectCell)
 
389
        self.grid.Bind(wx.EVT_KEY_DOWN, self.OnGridKeyDown)
 
390
        self.szrMain.Add(self.grid, 1, wx.GROW)
 
391
        self.panel.SetSizer(self.szrMain)
 
392
        self.szrMain.SetSizeHints(self)
 
393
        self.panel.Layout()
 
394
        self.grid.SetFocus()
 
395
 
 
396
    def OnGridKeyDown(self, event):
 
397
        if event.GetKeyCode() in [wx.WXK_TAB]:
 
398
            row = self.current_row_idx
 
399
            col = self.current_col_idx
 
400
            if self.dbtbl.NewRow(row):
 
401
                if self.debug: print "New buffer is " + str(self.dbtbl.new_buffer)
 
402
                raw_val = self.dbtbl.new_buffer.get((row, col), 
 
403
                                                    MISSING_VAL_INDICATOR)
 
404
            else:
 
405
                raw_val = self.grid.GetCellValue(row, col)
 
406
            if self.debug:
 
407
                print "[OnGridKeyDown] Tabbing away from field with value \"%s\"" % raw_val
 
408
            if self.NewRow(row):
 
409
                if self.debug: print "Tabbing within new row"
 
410
                self.dbtbl.new_buffer[(row, col)] = raw_val
 
411
                final_col = (col == len(self.flds) - 1)
 
412
                if final_col:
 
413
                    # only attempt to save if value is OK to save
 
414
                    if not self.CellOKToSave(row, col):
 
415
                        self.grid.SetFocus()
 
416
                        return
 
417
                    if self.debug: print "OnGridKeyDown - Trying to leave new record"
 
418
                    saved_ok = self.SaveRow(row)
 
419
                    if saved_ok:
 
420
                        if self.debug: print "OnGridKeyDown - Was able " + \
 
421
                            "to save record after tabbing away"
 
422
                    else:
 
423
                        # CellOkToSave obviously failed to give correct answer
 
424
                        if self.debug: print "OnGridKeyDown - Unable to " + \
 
425
                            "save record after tabbing away"
 
426
                        wx.MessageBox("Unable to save record - please " + \
 
427
                                      "check values")
 
428
                    return
 
429
        event.Skip()
 
430
 
 
431
    def SetNewRowEd(self, new_row_idx):
 
432
        "Set new line custom cell editor for new row"
 
433
        for col_idx in range(len(self.flds)):
 
434
            text_ed = TextEditor(self, new_row_idx, col_idx, new_row=True, 
 
435
                                 new_buffer=self.dbtbl.new_buffer)
 
436
            self.grid.SetCellEditor(new_row_idx, col_idx, text_ed)
 
437
    
 
438
    def FocusOnNewRow(self, new_row_idx):
 
439
        "Focus on cell in new row - set current to refer to that cell etc"
 
440
        self.grid.SetGridCursor(new_row_idx, 0)
 
441
        self.grid.MakeCellVisible(new_row_idx, 0)
 
442
        self.current_row_idx = new_row_idx
 
443
        self.current_col_idx = 0
 
444
    
 
445
    def SetColWidths(self):
 
446
        "Set column widths based on display widths of fields"
 
447
        self.parent.AddFeedback("Setting column widths " + \
 
448
            "(%s columns for %s rows)..." % (self.dbtbl.GetNumberCols(), 
 
449
                                             self.dbtbl.GetNumberRows()))
 
450
        pix_per_char = 8
 
451
        sorted_fld_names = getdata.FldsDic2FldNamesLst(self.flds)
 
452
        for col_idx, fld_name in enumerate(sorted_fld_names):
 
453
            fld_dic = self.flds[fld_name]
 
454
            col_width = None
 
455
            if fld_dic[getdata.FLD_BOLTEXT]:
 
456
                txt_len = fld_dic[getdata.FLD_TEXT_LENGTH]
 
457
                col_width = txt_len*pix_per_char if txt_len != None \
 
458
                    and txt_len < 25 else None # leave for auto
 
459
            elif fld_dic[getdata.FLD_BOLNUMERIC]:
 
460
                num_len = fld_dic[getdata.FLD_NUM_WIDTH]
 
461
                col_width = num_len*pix_per_char if num_len != None else None
 
462
            elif fld_dic[getdata.FLD_BOLDATETIME]:
 
463
                col_width = 170
 
464
            if col_width:
 
465
                if self.debug: print "Width of %s set to %s" % (fld_name, 
 
466
                                                                col_width)
 
467
                self.grid.SetColSize(col_idx, col_width)
 
468
            else:
 
469
                if self.debug: print "Autosizing %s" % fld_name
 
470
                self.grid.AutoSizeColumn(col_idx, setAsMin=False)            
 
471
            fld_name_width = len(fld_name)*pix_per_char
 
472
            # if actual column width is small and the label width is larger,
 
473
            # use label width.
 
474
            self.grid.ForceRefresh()
 
475
            actual_width = self.grid.GetColSize(col_idx)
 
476
            if actual_width < 15*pix_per_char \
 
477
                    and actual_width < fld_name_width:
 
478
                self.grid.SetColSize(col_idx, fld_name_width)
 
479
            if self.debug: print "%s %s" % (fld_name, 
 
480
                                            self.grid.GetColSize(col_idx))
 
481
        
 
482
        self.parent.AddFeedback("")
 
483
    
 
484
    def NewRow(self, row):
 
485
        new_row = self.dbtbl.NewRow(row)
 
486
        return new_row
 
487
    
 
488
    def OnSelectCell(self, event):
 
489
        """
 
490
        Prevent selection away from a record still in process of being saved,
 
491
            whether by mouse or keyboard, unless saved OK.
 
492
        Don't allow to leave cell in invalid state.
 
493
        Check the following:
 
494
            If jumping around within new row, cell cannot be invalid.
 
495
            If not in a new row (i.e. in existing), cell must be ok to save.
 
496
            If leaving new row, must be ready to save whole row.
 
497
        If any rules are broken, abort the jump.
 
498
        """
 
499
        row = event.GetRow()
 
500
        col = event.GetCol()
 
501
        was_new_row = self.NewRow(self.current_row_idx)
 
502
        jump_row_new = self.NewRow(row)
 
503
        if was_new_row and jump_row_new: # jumping within new
 
504
            if self.debug: print "Jumping within new row"
 
505
            ok_to_move = not self.CellInvalid(self.current_row_idx, 
 
506
                                              self.current_col_idx)
 
507
        elif not was_new_row:
 
508
            if self.debug: print "Was in existing row"
 
509
            ok_to_move = self.CellOKToSave(self.current_row_idx, 
 
510
                                           self.current_col_idx)
 
511
        elif was_new_row and not jump_row_new: # leaving new row
 
512
            if self.debug: print "Leaving new row"
 
513
            # only attempt to save if value is OK to save
 
514
            if not self.CellOKToSave(self.current_row_idx, 
 
515
                                     self.current_col_idx):
 
516
                ok_to_move = False
 
517
            else:
 
518
                ok_to_move = self.SaveRow(self.current_row_idx)
 
519
        if ok_to_move:
 
520
            self.current_row_idx = row
 
521
            self.current_col_idx = col
 
522
            event.Skip() # will allow us to move to the new cell
 
523
    
 
524
    def ValueInRange(self, raw_val, fld_dic):
 
525
        "NB may be None if N/A e.g. SQLite"
 
526
        min = fld_dic[getdata.FLD_NUM_MIN_VAL]
 
527
        max = fld_dic[getdata.FLD_NUM_MAX_VAL]        
 
528
        if min != None:
 
529
            if Decimal(raw_val) < Decimal(str(min)):
 
530
                if self.debug: print "%s is < the min of %s" % (raw_val, min)
 
531
                return False
 
532
        if max != None:
 
533
            if Decimal(raw_val) > Decimal(str(max)):
 
534
                if self.debug: print "%s is > the max of %s" % (raw_val, max)
 
535
                return False
 
536
        if self.debug: print "%s was accepted" % raw_val
 
537
        return True
 
538
    
 
539
    def CellInvalid(self, row, col):
 
540
        """
 
541
        Does a cell contain a value which shouldn't be allowed (even
 
542
            temporarily)?
 
543
        Values which are OK to allow temporarily are missing values 
 
544
            where data is required (for saving).
 
545
        If field numeric, value must be numeric ;-)
 
546
            Cannot be negative if unsigned.
 
547
            Must not be too big (turn 1.00 into 1 first etc).
 
548
            And if a not decimal, cannot have decimal places.
 
549
        If field is datetime, value must be valid date (or datetime).
 
550
        If field is text, cannot be longer than maximum length.
 
551
        """
 
552
        cell_invalid = False # innocent until proven guilty
 
553
        if self.dbtbl.NewRow(row):
 
554
            if self.debug: print "New buffer is " + str(self.dbtbl.new_buffer)
 
555
            raw_val = self.dbtbl.new_buffer.get((row, col), 
 
556
                                                MISSING_VAL_INDICATOR)
 
557
        else:
 
558
            raw_val = self.grid.GetCellValue(row, col)
 
559
            existing_row_data_tup = self.dbtbl.row_vals_dic.get(row)
 
560
            if existing_row_data_tup:
 
561
                prev_val = str(existing_row_data_tup[col])
 
562
            if self.debug: print "prev_val: %s raw_val: %s" % (prev_val, 
 
563
                                                               raw_val)
 
564
            if raw_val == prev_val:
 
565
                if self.debug: print "Unchanged"
 
566
                return False
 
567
        fld_dic = self.dbtbl.GetFldDic(col)
 
568
        if self.debug: print "\"%s\"" % raw_val
 
569
        if raw_val == MISSING_VAL_INDICATOR:
 
570
            return False
 
571
        elif not fld_dic[getdata.FLD_DATA_ENTRY_OK]: # and raw_val != MISSING_VAL_INDICATOR
 
572
            wx.MessageBox("This field does not accept user data entry.  " + \
 
573
                          "Leave as missing value (.)")
 
574
            return True
 
575
        elif fld_dic[getdata.FLD_BOLNUMERIC]:
 
576
            if not util.isNumeric(raw_val):
 
577
                wx.MessageBox("\"%s\" is not a valid number.\n\n" % raw_val + \
 
578
                              "Either enter a valid number or " + \
 
579
                              "the missing value character (.)")
 
580
                return True
 
581
            if not self.ValueInRange(raw_val, fld_dic):
 
582
                if self.debug: print "\"%s\" is invalid for data type" % raw_val
 
583
                return True
 
584
            return False
 
585
        elif fld_dic[getdata.FLD_BOLDATETIME]:
 
586
            valid_datetime, _ = util.datetime_str_valid(raw_val)
 
587
            if not valid_datetime:
 
588
                wx.MessageBox("\"%s\" is not a valid datetime.\n\n" % raw_val + \
 
589
                              "Either enter a valid date/ datetime\n" + \
 
590
                              "e.g. 31/3/2009 or 2:30pm 31/3/2009 or " + \
 
591
                              "the missing value character (.)")
 
592
                return True
 
593
            return False
 
594
        elif fld_dic[getdata.FLD_BOLTEXT]:
 
595
            max_len = fld_dic[getdata.FLD_TEXT_LENGTH]
 
596
            if len(raw_val) > max_len:
 
597
                wx.MessageBox("\"%s\" is longer than the maximum of %s" % \
 
598
                              (raw_val, max_len) + "Either enter a shorter" + \
 
599
                              "value or the missing value character (.)")
 
600
                return True
 
601
            return False
 
602
        else:
 
603
            raise Exception, "Field supposedly not numeric, datetime, or text"
 
604
    
 
605
    def CellOKToSave(self, row, col):
 
606
        """
 
607
        Cannot be an invalid value (must be valid or missing value).
 
608
        And if missing value, must be nullable field.
 
609
        """
 
610
        raw_val = self.grid.GetCellValue(row, col)
 
611
        fld_dic = self.dbtbl.GetFldDic(col)
 
612
        missing_not_nullable_prob = (raw_val == MISSING_VAL_INDICATOR and \
 
613
                                    not fld_dic[getdata.FLD_BOLNULLABLE] and \
 
614
                                    fld_dic[getdata.FLD_DATA_ENTRY_OK])
 
615
        if missing_not_nullable_prob:
 
616
            wx.MessageBox("This field will not allow missing values to " + \
 
617
                          "be stored")
 
618
        ok_to_save = not self.CellInvalid(row, col) and \
 
619
            not missing_not_nullable_prob
 
620
        return ok_to_save
 
621
 
 
622
    def InitNewRowBuffer(self):
 
623
        "Initialise new row buffer"
 
624
        self.dbtbl.new_is_dirty = False
 
625
        self.dbtbl.new_buffer = {}
 
626
 
 
627
    def ResetPrevRowEd(self, prev_row_idx):
 
628
        "Set new line custom cell editor for new row"
 
629
        for col_idx in range(len(self.flds)):
 
630
            self.grid.SetCellEditor(prev_row_idx, col_idx, 
 
631
                                    wx.grid.GridCellTextEditor())
 
632
 
 
633
    def SetupNewRow(self, data):
 
634
        """
 
635
        Setup new row ready to receive new data.
 
636
        data = [(value as string, fld_name, fld_dets), ...]
 
637
        """
 
638
        self.dbtbl.SetRowIdDic()
 
639
        self.dbtbl.SetNumberRows() # need to refresh        
 
640
        new_row_idx = self.dbtbl.GetNumberRows() - 1
 
641
        data_tup = tuple([x[0] for x in data])
 
642
        # do not add to row_vals_dic - force it to look it up from the db
 
643
        # will thus show autocreated values e.g. timestamp, autoincrement etc
 
644
        self.DisplayNewRow()
 
645
        self.ResetRowLabels(new_row_idx)
 
646
        self.InitNewRowBuffer()
 
647
        self.FocusOnNewRow(new_row_idx)
 
648
        self.ResetPrevRowEd(new_row_idx - 1)
 
649
        self.SetNewRowEd(new_row_idx)
 
650
    
 
651
    def DisplayNewRow(self):
 
652
        "Display a new entry row on end of grid"
 
653
        self.dbtbl.DisplayNewRow()
 
654
    
 
655
    def ResetRowLabels(self, row):
 
656
        "Reset new row label and restore previous new row label to default"
 
657
        prev_row = row - 1
 
658
        self.grid.SetRowLabelValue(prev_row, str(prev_row))
 
659
        self.grid.SetRowLabelValue(row, "*")
 
660
    
 
661
    def SaveRow(self, row):
 
662
        data = []
 
663
        fld_names = getdata.FldsDic2FldNamesLst(self.flds) # sorted list
 
664
        for col in range(len(self.flds)):
 
665
            raw_val = self.dbtbl.new_buffer.get((row, col), None)
 
666
            if raw_val == MISSING_VAL_INDICATOR:
 
667
                raw_val = None
 
668
            fld_name = fld_names[col]
 
669
            fld_dic = self.flds[fld_name]
 
670
            data.append((raw_val, fld_name, fld_dic))
 
671
        row_inserted = getdata.InsertRow(self.dbe, self.conn, self.cur, 
 
672
                                         self.tbl_name, data)
 
673
        if row_inserted:
 
674
            if self.debug: print "Just inserted row in SaveRow()"
 
675
        else:
 
676
            if self.debug: print "Unable to insert row in SaveRow()"
 
677
            return False
 
678
        try:
 
679
            self.SetupNewRow(data)
 
680
            return True
 
681
        except:
 
682
            if self.debug: print "Unable to setup new row"
 
683
            return False
 
684
        
 
685
    def OnCellChange(self, event):
 
686
        self.grid.ForceRefresh()
 
687
        event.Skip()