1
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
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):
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
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
61 self.createEmptyModel()
62
63 if not set(self.reclist) == set(self.data.keys()):
64 print 'reclist does not match data keys'
65
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
73 self.default_display = {'text' : 'showstring',
74 'number' : 'numtostring'}
75
76 if len(self.columnNames)>0:
77 self.sortkey = self.columnNames[0]
78 else:
79 self.sortkey = None
80
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
90 """Create base fields, some of which are not saved"""
91 self.data = None
92 self.colors = {}
93 self.colors['fg']={}
94 self.colors['bg']={}
95
96 self.defaulttypes = ['text', 'number']
97
98 self.editable={}
99 self.nodisplay = []
100 self.columnwidths={}
101 return
102
104 """Create the basic empty model dict"""
105 self.data = {}
106
107 self.columnNames = []
108 self.columntypes = {}
109 self.columnOrder = None
110
111 self.columnlabels={}
112 for colname in self.columnNames:
113 self.columnlabels[colname]=colname
114 self.reclist = self.data.keys()
115 return
116
118 """Try to create a table model from a dict of the form
119 {{'rec1': {'col1': 3, 'col2': 2}, ..}"""
120
121
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
131 self.data.update(newdata)
132 self.reclist = self.data.keys()
133 return
134
136 """Get possible field types for this table model"""
137 return self.defaulttypes
138
140 """Return the current data for saving"""
141 data = copy.deepcopy(self.data)
142 data['colors'] = self.colors
143 data['columnnames'] = self.columnNames
144
145 data['reclist'] = self.reclist
146
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
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
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
190 return maxw
191
193 """Get the entire record at the specifed row."""
194 name = self.getRecName(rowIndex)
195 record = self.data[name]
196 return record
197
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
205 if self.data[name].has_key(colname):
206 celldata=self.data[name][colname]
207 else:
208 celldata=None
209 return celldata
210
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
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
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
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
248
249 return
250
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
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
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
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
309 try:
310 recdata = self.toFloats(recdata)
311 except:
312 pass
313 smap = zip(names, recdata)
314
315 smap = sorted(smap, key=operator.itemgetter(1), reverse=reverse)
316
317 sortmap = map(operator.itemgetter(0), smap)
318 return sortmap
319
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
342 moved = self.oldnames[oldcolumnIndex]
343 del self.oldnames[oldcolumnIndex]
344
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
352 if moved not in self.columnNames:
353 self.columnNames.append(moved)
354 return
355
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
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
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
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
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
427
428 return
429
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
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
449 keys = range(start,start+numrows)
450
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
460 """Automatically add x number of cols"""
461
462 alphabet = string.lowercase[:26]
463 currcols=self.getColumnCount()
464
465 start = currcols + 1
466 end = currcols + numcols + 1
467 new = []
468 for n in range(start, end):
469 new.append(str(n))
470
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
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
485 """Get the column type"""
486 colname = self.getColumnName(columnIndex)
487 coltype = self.columntypes[colname]
488 return coltype
489
491 """Returns the number of columns in the data model."""
492 return len(self.columnNames)
493
495 """Returns the name of the given column by columnIndex."""
496 return self.columnNames[columnIndex]
497
499 """Returns the label for this column"""
500 colname = self.getColumnName(columnIndex)
501 return self.columnlabels[colname]
502
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
561 names=[]
562 for rec in self.reclist:
563 if data[rec].has_key(filtercol):
564
565 if op in floatops:
566 try:
567
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
584 """Returns the number of rows in the table model."""
585 return len(self.reclist)
586
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 == '':
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
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
639 """Remove all color formatting"""
640 self.colors={}
641 self.colors['fg']={}
642 self.colors['bg']={}
643 return
644
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
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
674
679
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
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
745
747 return 'Table Model with %s rows' %len(self.reclist)
748