camelot.camelot.view.controls.tableview

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