Package tkintertable :: Module TableModels
[hide private]
[frames] | no frames]

Source Code for Module tkintertable.TableModels

  1  #!/usr/bin/env python 
  2  """ 
  3      Module implementing the TableModel class that manages data for 
  4      it's associated TableCanvas. 
  5   
  6      Created Oct 2008 
  7      Copyright (C) Damien Farrell 
  8   
  9      This program is free software; you can redistribute it and/or 
 10      modify it under the terms of the GNU General Public License 
 11      as published by the Free Software Foundation; either version 2 
 12      of the License, or (at your option) any later version. 
 13   
 14      This program is distributed in the hope that it will be useful, 
 15      but WITHOUT ANY WARRANTY; without even the implied warranty of 
 16      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 17      GNU General Public License for more details. 
 18   
 19      You should have received a copy of the GNU General Public License 
 20      along with this program; if not, write to the Free Software 
 21      Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 22  """ 
 23   
 24  from TableFormula import Formula 
 25  import Filtering 
 26  from types import * 
 27  import operator 
 28  import string, types, copy 
 29  import pickle 
 30   
31 -class TableModel(object):
32 """A base model for managing the data in a TableCanvas class""" 33 34 keywords = {'columnnames':'columnNames', 'columntypes':'columntypes', 35 'columnlabels':'columnlabels', 'columnorder':'columnOrder', 36 'colors':'colors'} 37
38 - def __init__(self, newdict=None, rows=None, columns=None):
39 """Constructor""" 40 self.initialiseFields() 41 self.setupModel(newdict, rows, columns) 42 return
43
44 - def setupModel(self, newdict, rows=None, columns=None):
45 """Create table model""" 46 if newdict != None: 47 self.data = copy.deepcopy(newdict) 48 for k in self.keywords: 49 if self.data.has_key(k): 50 self.__dict__[self.keywords[k]] = self.data[k] 51 del self.data[k] 52 #read in the record list order 53 if self.data.has_key('reclist'): 54 temp = self.data['reclist'] 55 del self.data['reclist'] 56 self.reclist = temp 57 else: 58 self.reclist = self.data.keys() 59 else: 60 #just make a new empty model 61 self.createEmptyModel() 62 63 if not set(self.reclist) == set(self.data.keys()): 64 print 'reclist does not match data keys' 65 #restore last column order 66 if hasattr(self, 'columnOrder') and self.columnOrder != None: 67 self.columnNames=[] 68 for i in self.columnOrder.keys(): 69 self.columnNames.append(self.columnOrder[i]) 70 i=i+1 71 self.defaulttypes = ['text', 'number'] 72 #setup default display for column types 73 self.default_display = {'text' : 'showstring', 74 'number' : 'numtostring'} 75 #set default sort order as first col 76 if len(self.columnNames)>0: 77 self.sortkey = self.columnNames[0] 78 else: 79 self.sortkey = None 80 #add rows and cols if they are given in the constructor 81 if newdict == None: 82 if rows != None: 83 self.autoAddRows(rows) 84 if columns != None: 85 self.autoAddColumns(columns) 86 self.filteredrecs = None 87 return
88
89 - def initialiseFields(self):
90 """Create base fields, some of which are not saved""" 91 self.data = None # holds the table dict 92 self.colors = {} # holds cell colors 93 self.colors['fg']={} 94 self.colors['bg']={} 95 #default types 96 self.defaulttypes = ['text', 'number'] 97 #list of editable column types 98 self.editable={} 99 self.nodisplay = [] 100 self.columnwidths={} #used to store col widths, not held in saved data 101 return
102
103 - def createEmptyModel(self):
104 """Create the basic empty model dict""" 105 self.data = {} 106 # Define the starting column names and locations in the table. 107 self.columnNames = [] 108 self.columntypes = {} 109 self.columnOrder = None 110 #record column labels for use in a table header 111 self.columnlabels={} 112 for colname in self.columnNames: 113 self.columnlabels[colname]=colname 114 self.reclist = self.data.keys() 115 return
116
117 - def importDict(self, newdata):
118 """Try to create a table model from a dict of the form 119 {{'rec1': {'col1': 3, 'col2': 2}, ..}""" 120 121 #get cols from sub data keys 122 colnames = [] 123 for k in newdata: 124 fields = newdata[k].keys() 125 for f in fields: 126 if not f in colnames: 127 colnames.append(f) 128 for c in colnames: 129 self.addColumn(c) 130 #add the data 131 self.data.update(newdata) 132 self.reclist = self.data.keys() 133 return
134
135 - def getDefaultTypes(self):
136 """Get possible field types for this table model""" 137 return self.defaulttypes
138
139 - def getData(self):
140 """Return the current data for saving""" 141 data = copy.deepcopy(self.data) 142 data['colors'] = self.colors 143 data['columnnames'] = self.columnNames 144 #we keep original record order 145 data['reclist'] = self.reclist 146 #record current col order 147 data['columnorder']={} 148 i=0 149 for name in self.columnNames: 150 data['columnorder'][i] = name 151 i=i+1 152 data['columntypes'] = self.columntypes 153 data['columnlabels'] = self.columnlabels 154 return data
155
156 - def getAllCells(self):
157 """Return a dict of the form rowname: list of cell contents 158 Useful for a simple table export for example""" 159 records={} 160 for row in range(len(self.reclist)): 161 recdata=[] 162 for col in range(len(self.columnNames)): 163 recdata.append(self.getValueAt(row,col)) 164 records[row]=recdata 165 return records
166
167 - def getColCells(self, colIndex):
168 """Get the viewable contents of a col into a list""" 169 collist = [] 170 if self.getColumnType(colIndex) == 'Link': 171 return ['xxxxxx'] 172 else: 173 for row in range(len(self.reclist)): 174 v = self.getValueAt(row, colIndex) 175 collist.append(v) 176 return collist
177
178 - def getlongestEntry(self, columnIndex):
179 """Get the longest cell entry in the col""" 180 collist = self.getColCells(columnIndex) 181 maxw=5 182 for c in collist: 183 try: 184 w = len(str(c)) 185 except UnicodeEncodeError: 186 pass 187 if w > maxw: 188 maxw = w 189 #print 'longest width', maxw 190 return maxw
191
192 - def getRecordAtRow(self, rowIndex):
193 """Get the entire record at the specifed row.""" 194 name = self.getRecName(rowIndex) 195 record = self.data[name] 196 return record
197
198 - def getCellRecord(self, rowIndex, columnIndex):
199 """Get the data held in this row and column""" 200 value = None 201 colname = self.getColumnName(columnIndex) 202 coltype = self.columntypes[colname] 203 name = self.getRecName(rowIndex) 204 #print self.data[name] 205 if self.data[name].has_key(colname): 206 celldata=self.data[name][colname] 207 else: 208 celldata=None 209 return celldata
210
211 - def deleteCellRecord(self, rowIndex, columnIndex):
212 """Remove the cell data at this row/column""" 213 colname = self.getColumnName(columnIndex) 214 coltype = self.columntypes[colname] 215 name = self.getRecName(rowIndex) 216 if self.data[name].has_key(colname): 217 del self.data[name][colname] 218 return
219
220 - def getRecName(self, rowIndex):
221 """Get record name from row number""" 222 if len(self.reclist)==0: 223 return None 224 if self.filteredrecs != None: 225 name = self.filteredrecs[rowIndex] 226 else: 227 name = self.reclist[rowIndex] 228 return name
229
230 - def setRecName(self, newname, rowIndex):
231 """Set the record name to another value - requires re-setting in all 232 dicts that this rec is referenced""" 233 if len(self.reclist)==0: 234 return None 235 currname = self.getRecName(rowIndex) 236 self.reclist[rowIndex] = newname 237 temp = copy.deepcopy(self.data[currname]) 238 self.data[newname] = temp 239 #self.data[newname]['Name'] = newname 240 del self.data[currname] 241 for key in ['bg', 'fg']: 242 if self.colors[key].has_key(currname): 243 temp = copy.deepcopy(self.colors[key][currname]) 244 self.colors[key][newname] = temp 245 del self.colors[key][currname] 246 print 'renamed' 247 #would also need to resolve all refs to this rec in formulas here! 248 249 return
250
251 - def getRecordAttributeAtColumn(self, rowIndex=None, columnIndex=None, 252 recName=None, columnName=None):
253 """Get the attribute of the record at the specified column index. 254 This determines what will be displayed in the cell""" 255 256 value = None 257 if columnName != None and recName != None: 258 if not self.data[recName].has_key(columnName): 259 return '' 260 cell = self.data[recName][columnName] 261 else: 262 cell = self.getCellRecord(rowIndex, columnIndex) 263 columnName = self.getColumnName(columnIndex) 264 if cell == None: 265 cell='' 266 # Set the value based on the data record field 267 coltype = self.columntypes[columnName] 268 if Formula.isFormula(cell) == True: 269 value = self.doFormula(cell) 270 return value 271 272 if not type(cell) is DictType: 273 if coltype == 'text' or coltype == 'Text': 274 value = cell 275 elif coltype == 'number': 276 value = str(cell) 277 else: 278 value = 'other' 279 if value==None: 280 value='' 281 return value
282
283 - def getRecordIndex(self, recname):
284 rowIndex = self.reclist.index(recname) 285 return rowIndex
286
287 - def setSortOrder(self, columnIndex=None, columnName=None, reverse=0):
288 """Changes the order that records are sorted in, which will 289 be reflected in the table upon redrawing""" 290 291 if columnName != None and columnName in self.columnNames: 292 self.sortkey = columnName 293 elif columnIndex != None: 294 self.sortkey = self.getColumnName(columnIndex) 295 else: 296 return 297 self.reclist = self.createSortMap(self.reclist, self.sortkey, reverse) 298 if self.filteredrecs != None: 299 self.filteredrecs = self.createSortMap(self.filteredrecs, self.sortkey, reverse) 300 return
301
302 - def createSortMap(self, names, sortkey, reverse=0):
303 """Create a sort mapping for given list""" 304 305 recdata = [] 306 for rec in names: 307 recdata.append(self.getRecordAttributeAtColumn(recName=rec, columnName=sortkey)) 308 #try create list of floats if col has numbers only 309 try: 310 recdata = self.toFloats(recdata) 311 except: 312 pass 313 smap = zip(names, recdata) 314 #sort the mapping by the second key 315 smap = sorted(smap, key=operator.itemgetter(1), reverse=reverse) 316 #now sort the main reclist by the mapping order 317 sortmap = map(operator.itemgetter(0), smap) 318 return sortmap
319
320 - def toFloats(self, l):
321 x=[] 322 for i in l: 323 if i == '': 324 x.append(0.0) 325 else: 326 x.append(float(i)) 327 return x
328 329 '''def getSortIndex(self): 330 """Return the current sort order index""" 331 if self.sortcolumnIndex: 332 return self.sortcolumnIndex 333 else: 334 return 0''' 335
336 - def moveColumn(self, oldcolumnIndex, newcolumnIndex):
337 """Changes the order of columns""" 338 self.oldnames = self.columnNames 339 self.columnNames=[] 340 341 #write out a new column names list - tedious 342 moved = self.oldnames[oldcolumnIndex] 343 del self.oldnames[oldcolumnIndex] 344 #print self.oldnames 345 i=0 346 for c in self.oldnames: 347 if i==newcolumnIndex: 348 self.columnNames.append(moved) 349 self.columnNames.append(c) 350 i=i+1 351 #if new col is at end just append 352 if moved not in self.columnNames: 353 self.columnNames.append(moved) 354 return
355
356 - def getNextKey(self):
357 """Return the next numeric key in the dict""" 358 num = len(self.reclist)+1 359 return num
360
361 - def addRow(self, key=None, **kwargs):
362 """Add a row""" 363 if key == '': 364 return 365 if key==None: 366 key = self.getNextKey() 367 if self.data.has_key(key) or key in self.reclist: 368 print 'name already present!!' 369 return 370 self.data[key]={} 371 for k in kwargs: 372 if not k in self.columnNames: 373 self.addColumn(k) 374 self.data[key][k] = str(kwargs[k]) 375 self.reclist.append(key) 376 return key
377
378 - def deleteRow(self, rowIndex=None, key=None, update=True):
379 """Delete a row""" 380 if key == None or not key in self.reclist: 381 key = self.getRecName(rowIndex) 382 del self.data[key] 383 if update==True: 384 self.reclist.remove(key) 385 return
386
387 - def deleteRows(self, rowlist=None):
388 """Delete multiple or all rows""" 389 if rowlist == None: 390 rowlist = range(len(self.reclist)) 391 names = [self.getRecName(i) for i in rowlist] 392 for name in names: 393 self.deleteRow(key=name, update=True) 394 return
395
396 - def addColumn(self, colname=None, coltype=None):
397 """Add a column""" 398 index = self.getColumnCount()+ 1 399 if colname == None: 400 colname=str(index) 401 if colname in self.columnNames: 402 #print 'name is present!' 403 return 404 self.columnNames.append(colname) 405 self.columnlabels[colname] = colname 406 if coltype == None: 407 self.columntypes[colname]='text' 408 else: 409 self.columntypes[colname]=coltype 410 return
411
412 - def deleteColumn(self, columnIndex):
413 """delete a column""" 414 colname = self.getColumnName(columnIndex) 415 self.columnNames.remove(colname) 416 del self.columnlabels[colname] 417 del self.columntypes[colname] 418 #remove this field from every record 419 for recname in self.reclist: 420 if self.data[recname].has_key(colname): 421 del self.data[recname][colname] 422 if self.sortkey != None: 423 currIndex = self.getColumnIndex(self.sortkey) 424 if columnIndex == currIndex: 425 self.setSortOrder(0) 426 #print 'column deleted' 427 #print 'new cols:', self.columnNames 428 return
429
430 - def deleteColumns(self, cols=None):
431 """Remove all cols or list provided""" 432 if cols == None: 433 cols = self.columnNames 434 if self.getColumnCount() == 0: 435 return 436 for col in cols: 437 self.deleteColumn(col) 438 return
439
440 - def autoAddRows(self, numrows=None):
441 """Automatically add x number of records""" 442 rows = self.getRowCount() 443 ints = [i for i in self.reclist if isinstance(i, int)] 444 if len(ints)>0: 445 start = max(ints)+1 446 else: 447 start = 0 448 #we don't use addRow as it's too slow 449 keys = range(start,start+numrows) 450 #make sure no keys are present already 451 keys = list(set(keys)-set(self.reclist)) 452 newdata = {} 453 for k in keys: 454 newdata[k] = {} 455 self.data.update(newdata) 456 self.reclist.extend(newdata.keys()) 457 return keys
458
459 - def autoAddColumns(self, numcols=None):
460 """Automatically add x number of cols""" 461 462 alphabet = string.lowercase[:26] 463 currcols=self.getColumnCount() 464 #find where to start 465 start = currcols + 1 466 end = currcols + numcols + 1 467 new = [] 468 for n in range(start, end): 469 new.append(str(n)) 470 #check if any of these colnames present 471 common = set(new) & set(self.columnNames) 472 extra = len(common) 473 end = end + extra 474 for x in range(start, end): 475 self.addColumn(str(x)) 476 return
477
478 - def relabel_Column(self, columnIndex, newname):
479 """Change the column label - can be used in a table header""" 480 colname = self.getColumnName(columnIndex) 481 self.columnlabels[colname]=newname 482 return
483
484 - def getColumnType(self, columnIndex):
485 """Get the column type""" 486 colname = self.getColumnName(columnIndex) 487 coltype = self.columntypes[colname] 488 return coltype
489
490 - def getColumnCount(self):
491 """Returns the number of columns in the data model.""" 492 return len(self.columnNames)
493
494 - def getColumnName(self, columnIndex):
495 """Returns the name of the given column by columnIndex.""" 496 return self.columnNames[columnIndex]
497
498 - def getColumnLabel(self, columnIndex):
499 """Returns the label for this column""" 500 colname = self.getColumnName(columnIndex) 501 return self.columnlabels[colname]
502
503 - def getColumnIndex(self, columnName):
504 """Returns the column index for this column""" 505 colindex = self.columnNames.index(columnName) 506 return colindex
507
508 - def getColumnData(self, columnIndex=None, columnName=None, 509 filters=None):
510 """Return the data in a list for this col, 511 filters is a tuple of the form (key,value,operator,bool)""" 512 if columnIndex != None and columnIndex < len(self.columnNames): 513 columnName = self.getColumnName(columnIndex) 514 names = Filtering.doFiltering(searchfunc=self.filterBy, 515 filters=filters) 516 coldata = [self.data[n][columnName] for n in names] 517 return coldata
518
519 - def getColumns(self, colnames, filters=None, allowempty=True):
520 """Get column data for multiple cols, with given filter options, 521 filterby: list of tuples of the form (key,value,operator,bool) 522 allowempty: boolean if false means rows with empty vals for any 523 required fields are not returned 524 returns: lists of column data""" 525 526 def evaluate(l): 527 for i in l: 528 if i == '' or i == None: 529 return False 530 return True
531 coldata=[] 532 for c in colnames: 533 vals = self.getColumnData(columnName=c, filters=filters) 534 coldata.append(vals) 535 if allowempty == False: 536 result = [i for i in zip(*coldata) if evaluate(i) == True] 537 coldata = zip(*result) 538 return coldata
539
540 - def getDict(self, colnames, filters=None):
541 """Get the model data as a dict for given columns with filter options""" 542 data={} 543 names = self.reclist 544 cols = self.getColumns(colnames, filters) 545 coldata = zip(*cols) 546 for name,cdata in zip(names, coldata): 547 data[name] = dict(zip(colnames,cdata)) 548 return data
549
550 - def filterBy(self, filtercol, value, op='contains', userecnames=False, 551 progresscallback=None):
552 """The searching function that we apply to the model data. 553 This is used in Filtering.doFiltering to find the required recs 554 according to column, value and an operator""" 555 556 funcs = Filtering.operatornames 557 floatops = ['=','>','<'] 558 func = funcs[op] 559 data = self.data 560 #coltype = self.columntypes[filtercol] 561 names=[] 562 for rec in self.reclist: 563 if data[rec].has_key(filtercol): 564 #try to do float comparisons if required 565 if op in floatops: 566 try: 567 #print float(data[rec][filtercol]) 568 item = float(data[rec][filtercol]) 569 v = float(value) 570 if func(v, item) == True: 571 names.append(rec) 572 continue 573 except: 574 pass 575 if filtercol == 'name' and userecnames == True: 576 item = rec 577 else: 578 item = str(data[rec][filtercol]) 579 if func(value, item): 580 names.append(rec) 581 return names
582
583 - def getRowCount(self):
584 """Returns the number of rows in the table model.""" 585 return len(self.reclist)
586
587 - def getValueAt(self, rowIndex, columnIndex):
588 """Returns the cell value at location specified 589 by columnIndex and rowIndex.""" 590 value = self.getRecordAttributeAtColumn(rowIndex, columnIndex) 591 return value
592
593 - def setValueAt(self, value, rowIndex, columnIndex):
594 """Changed the dictionary when cell is updated by user""" 595 name = self.getRecName(rowIndex) 596 colname = self.getColumnName(columnIndex) 597 coltype = self.columntypes[colname] 598 if coltype == 'number': 599 try: 600 if value == '': #need this to allow deletion of values 601 self.data[name][colname] = '' 602 else: 603 self.data[name][colname] = float(value) 604 except: 605 pass 606 else: 607 self.data[name][colname] = value 608 return
609
610 - def setFormulaAt(self, f, rowIndex, columnIndex):
611 """Set a formula at cell given""" 612 name = self.getRecName(rowIndex) 613 colname = self.getColumnName(columnIndex) 614 coltype = self.columntypes[colname] 615 rec = {} 616 rec['formula'] = f 617 self.data[name][colname] = rec 618 return
619
620 - def getColorAt(self, rowIndex, columnIndex, key='bg'):
621 """Return color of that record field for the table""" 622 name = self.getRecName(rowIndex) 623 colname = self.getColumnName(columnIndex) 624 if self.colors[key].has_key(name) and self.colors[key][name].has_key(colname): 625 return self.colors[key][name][colname] 626 else: 627 return None
628
629 - def setColorAt(self, rowIndex, columnIndex, color, key='bg'):
630 """Set color""" 631 name = self.getRecName(rowIndex) 632 colname = self.getColumnName(columnIndex) 633 if not self.colors[key].has_key(name): 634 self.colors[key][name] = {} 635 self.colors[key][name][colname] = str(color) 636 return
637
638 - def resetcolors(self):
639 """Remove all color formatting""" 640 self.colors={} 641 self.colors['fg']={} 642 self.colors['bg']={} 643 return
644
645 - def getRecColNames(self, rowIndex, ColIndex):
646 """Returns the rec and col name as a tuple""" 647 recname = self.getRecName(rowIndex) 648 colname = self.getColumnName(ColIndex) 649 return (recname, colname)
650
651 - def getRecAtRow(self, recname, colname, offset=1, dim='y'):
652 """Get the record name at a specified offset in the current 653 table from the record given, by using the current sort order""" 654 thisrow = self.getRecordIndex(recname) 655 thiscol = self.getColumnIndex(colname) 656 #table goto next row 657 if dim == 'y': 658 nrow = thisrow + offset 659 ncol = thiscol 660 else: 661 nrow = thisrow 662 ncol = thiscol + offset 663 664 newrecname, newcolname = self.getRecColNames(nrow, ncol) 665 print 'recname, colname', recname, colname 666 print 'thisrow, col', thisrow, thiscol 667 return newrecname, newcolname
668
669 - def appendtoFormula(self, formula, rowIndex, colIndex):
670 """Add the input cell to the formula""" 671 cellRec = getRecColNames(rowIndex, colIndex) 672 formula.append(cellRec) 673 return
674
675 - def doFormula(self, cellformula):
676 """Evaluate the formula for a cell and return the result""" 677 value = Formula.doFormula(cellformula, self.data) 678 return value
679
680 - def copyFormula(self, cellval, row, col, offset=1, dim='y'):
681 """Copy a formula down or across, using the provided offset""" 682 import re 683 frmla = Formula.getFormula(cellval) 684 #print 'formula', frmla 685 686 newcells=[] 687 cells, ops = Formula.readExpression(frmla) 688 689 for c in cells: 690 print c 691 if type(c) is not ListType: 692 nc = c 693 else: 694 recname = c[0] 695 colname = c[1] 696 nc = list(self.getRecAtRow(recname, colname, offset, dim=dim)) 697 newcells.append(nc) 698 newformula = Formula.doExpression(newcells, ops, getvalues=False) 699 return newformula
700
701 - def merge(self, model, key='name', fields=None):
702 """Merge another table model with this one based on a key field, 703 we only add records from the new model where the key is present 704 in both models""" 705 if fields == None: fields = model.columnNames 706 for rec in self.reclist: 707 if not self.data[rec].has_key(key): 708 continue 709 for new in model.reclist: 710 if not model.data[new].has_key(key): 711 continue 712 if self.data[rec][key] == model.data[new][key]: 713 #if new == rec: 714 for f in fields: 715 if not model.data[rec].has_key(f): 716 continue 717 if not f in self.columnNames: 718 self.addColumn(f) 719 self.data[rec][f] = model.data[rec][f] 720 return
721
722 - def save(self, filename=None):
723 """Save model to file""" 724 if filename == None: 725 return 726 data = self.getData() 727 fd = open(filename,'w') 728 pickle.dump(data,fd) 729 fd.close() 730 return
731
732 - def load(self, filename):
733 """Load model from pickle file""" 734 fd=open(filename,'r') 735 data = pickle.load(fd) 736 self.setupModel(data) 737 return
738
739 - def copy(self):
740 """Return a copy of this model""" 741 M = TableModel() 742 data = self.getData() 743 M.setupModel(data) 744 return M
745
746 - def __repr__(self):
747 return 'Table Model with %s rows' %len(self.reclist)
748