Package camelot :: Package camelot :: Package view :: Package controls :: Module tableview
[hide private]
[frames] | no frames]

Source Code for Module camelot.camelot.view.controls.tableview

  1  #  ============================================================================
 
  2  #
 
  3  #  Copyright (C) 2007-2008 Conceptive Engineering bvba. All rights reserved.
 
  4  #  www.conceptive.be / project-camelot@conceptive.be
 
  5  #
 
  6  #  This file is part of the Camelot Library.
 
  7  #
 
  8  #  This file may be used under the terms of the GNU General Public
 
  9  #  License version 2.0 as published by the Free Software Foundation
 
 10  #  and appearing in the file LICENSE.GPL included in the packaging of
 
 11  #  this file.  Please review the following information to ensure GNU
 
 12  #  General Public Licensing requirements will be met:
 
 13  #  http://www.trolltech.com/products/qt/opensource.html
 
 14  #
 
 15  #  If you are unsure which license is appropriate for your use, please
 
 16  #  review the following information:
 
 17  #  http://www.trolltech.com/products/qt/licensing.html or contact
 
 18  #  project-camelot@conceptive.be.
 
 19  #
 
 20  #  This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 
 21  #  WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 
 22  #
 
 23  #  For use of this library in commercial applications, please contact
 
 24  #  project-camelot@conceptive.be
 
 25  #
 
 26  #  ============================================================================
 
 27  
 
 28  """Tableview""" 
 29  
 
 30  import os 
 31  import logging 
 32  logger = logging.getLogger('camelot.view.controls.tableview') 
 33  
 
 34  from PyQt4 import QtCore, QtGui 
 35  from PyQt4.QtGui import QSizePolicy 
 36  from PyQt4.QtCore import SIGNAL 
 37  
 
 38  from camelot.view.proxy.queryproxy import QueryTableProxy 
 39  import settings 
 40  
 
 41  verbose = False 
 42  
 
 43  
 
44 -class QueryTable(QtGui.QTableView):
45 """the actual displayed table""" 46
47 - def __init__(self, parent=None):
48 QtGui.QTableView.__init__(self, parent) 49 logger.debug('create querytable') 50 self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) 51 self.setEditTriggers(QtGui.QAbstractItemView.AllEditTriggers) 52 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 53 self.horizontalHeader().setClickable(False)
54 55
56 -class TableView(QtGui.QWidget):
57 """emits the row_selected signal when a row has been selected""" 58
59 - def __init__(self, admin, search_text=None, parent=None):
60 from search import SimpleSearchControl 61 from inheritance import SubclassTree 62 QtGui.QWidget.__init__(self, parent) 63 self.setWindowTitle(admin.getName()) 64 self.widget_layout = QtGui.QHBoxLayout() 65 self.widget_layout.setSpacing(0) 66 self.widget_layout.setMargin(0) 67 self.table_layout = QtGui.QVBoxLayout() 68 self.table_layout.setSpacing(0) 69 self.table_layout.setMargin(0) 70 self.search_control = SimpleSearchControl() 71 self.table = None 72 self.filters = None 73 self.admin = admin 74 self.table_model = None 75 self.table_layout.insertWidget(0, self.search_control) 76 self.setSubclass(admin) 77 self.class_tree = SubclassTree(admin, self) 78 self.widget_layout.insertWidget(0, self.class_tree) 79 self.widget_layout.insertLayout(1, self.table_layout) 80 self.closeAfterValidation = QtCore.SIGNAL('closeAfterValidation()') 81 self.connect(self.search_control, SIGNAL('search'), self.startSearch) 82 self.connect(self.search_control, SIGNAL('cancel'), self.cancelSearch) 83 self.connect(self.class_tree, SIGNAL('subclasssClicked'), self.setSubclass) 84 self.search_filter = lambda q: q 85 self.setLayout(self.widget_layout) 86 self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 87 if search_text: 88 self.search_control.search(search_text)
89
90 - def sectionClicked(self, section):
91 """emits a row_selected signal""" 92 self.emit(SIGNAL('row_selected'), section)
93
94 - def setSubclass(self, admin):
95 """Switch to a different subclass, where admin is the admin object of the 96 subclass""" 97 self.admin = admin 98 if self.table: 99 self.table.deleteLater() 100 self.table_model.deleteLater() 101 self.table = QueryTable() 102 # We create the table first with only 10 rows, to be able resize 103 # the columns to the contents without much processing 104 self.table_model = QueryTableProxy(admin, 105 lambda:admin.entity.query.limit(10), 106 admin.getColumns) 107 self.table.setModel(self.table_model) 108 self.connect(self.table.verticalHeader(), 109 SIGNAL('sectionClicked(int)'), 110 self.sectionClicked) 111 self.table_layout.insertWidget(1, self.table) 112 113 def update_delegates(*args): 114 """update item delegate""" 115 self.table.setItemDelegate(self.table_model.getItemDelegate())
116 117 admin.mt.post(lambda: None, update_delegates) 118 # Once those are loaded, rebuild the query to get the actual number of rows 119 admin.mt.post(lambda: self.table_model._extend_cache(0, 10), 120 lambda x: self.resizeColumnsAndRebuildQuery()) 121 admin.mt.post(lambda: admin.getFilters(), 122 lambda items: self.setFilters(items)) 123 admin.mt.post(lambda: admin.getListCharts(), 124 lambda charts: self.setCharts(charts))
125
126 - def setCharts(self, charts):
127 """creates and display charts""" 128 if charts: 129 130 from matplotlib.figure import Figure 131 from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as \ 132 FigureCanvas 133 134 chart = charts[0] 135 136 def getData(): 137 """fetches data for chart""" 138 from sqlalchemy.sql import select, func 139 from elixir import session 140 xcol = getattr(self.admin.entity, chart['x']) 141 ycol = getattr(self.admin.entity, chart['y']) 142 session.bind = self.admin.entity.table.metadata.bind 143 result = session.execute(select([xcol, func.sum(ycol)]).group_by(xcol)) 144 summary = result.fetchall() 145 return [s[0] for s in summary], [s[1] for s in summary]
146 147 class MyMplCanvas(FigureCanvas): 148 """Ultimately, this is a QWidget (as well as a FigureCanvasAgg)""" 149 150 def __init__(self, parent=None, width=5, height=4, dpi=100): 151 fig = Figure(figsize=(width, height), dpi=dpi, facecolor='w') 152 self.axes = fig.add_subplot(111, axisbg='w') 153 # We want the axes cleared every time plot() is called 154 self.axes.hold(False) 155 self.compute_initial_figure() 156 FigureCanvas.__init__(self, fig) 157 self.setParent(parent) 158 FigureCanvas.setSizePolicy(self, 159 QSizePolicy.Expanding, 160 QSizePolicy.Expanding) 161 FigureCanvas.updateGeometry(self) 162 163 164 def compute_initial_figure(self): 165 pass 166 167 def setData(data): 168 """set chart data""" 169 170 class MyStaticMplCanvas(MyMplCanvas): 171 """simple canvas with a sine plot""" 172 173 def compute_initial_figure(self): 174 """computes initial figure""" 175 x, y = data 176 bar_positions = [i-0.25 for i in range(1, len(x)+1)] 177 width = 0.5 178 self.axes.bar(bar_positions, y, width, color='b') 179 self.axes.set_xlabel('Year') 180 self.axes.set_ylabel('Sales') 181 self.axes.set_xticks(range(len(x)+1)) 182 self.axes.set_xticklabels(['']+[str(d) for d in x]) 183 184 sc = MyStaticMplCanvas(self, width=5, height=4, dpi=100) 185 self.table_layout.addWidget(sc) 186 187 self.admin.mt.post(getData, setData) 188
189 - def resizeColumnsAndRebuildQuery(self):
190 """resizes table of columns""" 191 logger.debug('resizeColumnsAndRebuildQuery') 192 # only if there is data in the model, we can resize the columns and 193 # a query rebuild is needed 194 if self.table_model.rowCount() > 1: 195 self.table.resizeColumnsToContents() 196 self.rebuildQuery()
197 198 #logger.debug('Selecting first row in table') 199 #@todo: select first row is not appropriate because 200 # the custom editors don't scale well 201 #self.table.selectRow(0) 202
203 - def deleteSelectedRows(self):
204 """delete the selected rows in this tableview""" 205 logger.debug('delete selected rows called') 206 for row in set(map(lambda x: x.row(), self.table.selectedIndexes())): 207 self.table_model.removeRow(row)
208
209 - def newRow(self):
210 """Create a new row in the tableview""" 211 from camelot.view.workspace import get_workspace 212 workspace = get_workspace() 213 form = self.admin.createNewView(workspace, 214 oncreate=lambda o:self.table_model.insertEntityInstance(0,o), 215 onexpunge=lambda o:self.table_model.removeEntityInstance(o)) 216 workspace.addSubWindow(form) 217 form.show()
218
219 - def selectTableRow(self, row):
220 """selects the specified row""" 221 self.table.selectRow(row)
222
223 - def selectedTableIndexes(self):
224 """returns a list of selected rows indexes""" 225 return self.table.selectedIndexes()
226
227 - def getColumns(self):
228 """return the columns to be displayed in the table view""" 229 return self.admin.getColumns()
230
231 - def getData(self):
232 """generator for data queried by table model""" 233 for d in self.table_model.getData(): 234 yield d
235
236 - def getTitle(self):
237 """return the name of the entity managed by the admin attribute""" 238 return self.admin.getName()
239
240 - def viewFirst(self):
241 """selects first row""" 242 self.selectTableRow(0)
243
244 - def viewLast(self):
245 """selects last row""" 246 self.selectTableRow(self.table_model.rowCount()-1)
247
248 - def viewNext(self):
249 """selects next row""" 250 first = self.selectedTableIndexes()[0] 251 next = (first.row()+1) % self.table_model.rowCount() 252 self.selectTableRow(next)
253
254 - def viewPrevious(self):
255 """selects previous row""" 256 first = self.selectedTableIndexes()[0] 257 prev = (first.row()-1) % self.table_model.rowCount() 258 self.selectTableRow(prev)
259
260 - def rebuildQuery(self):
261 """resets the table model query""" 262 263 def rebuild_query(): 264 query = self.admin.entity.query 265 if self.filters: 266 query = self.filters.decorate_query(query) 267 if self.search_filter: 268 query = self.search_filter(query) 269 self.table_model.setQuery(lambda:query)
270 271 self.admin.mt.post(rebuild_query) 272
273 - def startSearch(self, text):
274 """rebuilds query based on filtering text""" 275 from camelot.view.search import create_entity_search_query_decorator 276 logger.debug('search %s' % text) 277 self.search_filter = create_entity_search_query_decorator(self.admin, text) 278 self.rebuildQuery()
279
280 - def cancelSearch(self):
281 """resets search filtering to default""" 282 logger.debug('cancel search') 283 self.search_filter = lambda q: q 284 self.rebuildQuery()
285
286 - def setFilters(self, items):
287 """sets filters for the tableview""" 288 from filter import FilterList 289 if verbose: 290 logger.debug('setting filters with items : %s' % str(items)) 291 else: 292 logger.debug('setting filters for tableview') 293 if self.filters: 294 self.filters.deleteLater() 295 self.filters = None 296 if items: 297 self.filters = FilterList(items, self) 298 self.widget_layout.insertWidget(2, self.filters) 299 self.connect(self.filters, SIGNAL('filters_changed'), self.rebuildQuery)
300
301 - def toHtml(self):
302 """generates html of the table""" 303 table = [[getattr(row, col[0]) for col in self.admin.getColumns()] 304 for row in self.admin.entity.query.all()] 305 context = { 306 'title': self.admin.getName(), 307 'table': table, 308 'columns': [c[0] for c in self.admin.getColumns()], 309 } 310 from jinja import Environment, FileSystemLoader 311 ld = FileSystemLoader(settings.CAMELOT_TEMPLATES_DIRECTORY) 312 env = Environment(loader=ld) 313 tp = env.get_template('table_view.html') 314 return tp.render(context)
315
316 - def closeEvent(self, event):
317 """reimplements close event""" 318 logger.debug('tableview closed') 319 # remove all references we hold, to enable proper garbage collection 320 del self.widget_layout 321 del self.table_layout 322 del self.search_control 323 del self.table 324 del self.filters 325 del self.class_tree 326 del self.table_model 327 event.accept()
328
329 - def __del__(self):
330 """deletes the tableview object""" 331 logger.debug('%s deleted' % self.__class__.__name__)
332