camelot.camelot.view.elixir_admin

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

# Source Code for Module camelot.camelot.view.elixir_admin

      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  """

     29  @TODO: rewrite docstring

     30

     31  Admin classes, specify how objects should be rendered in the gui

     32

     33  An admin class has class attributes like 'list_display' which contains the

     34  columns that should be displayed in a list view (again, see Django)

     35

     36  So this 'list_display' attribute can be overwritten in the Admin class for each

     37  model.

     38

     39  But for the gui generation itself, we don't use the class attributes, but we

     40  use methods, like 'getColumns', that way, we can make the gui very specific, on

     41  the context

     42  """
     43

     44  import os
     45  import sys
     46  import logging
     47  logger = logging.getLogger('camelot.view.elixir_admin')
     48

     49  import datetime
     50

     51  import sqlalchemy.types
     52  import camelot.types
     53  from model_thread import gui_function
     54  from model_thread import model_function
     55  import settings
     56

     57  _ = lambda x: x
     58

     59 -class EntityAdmin(object):
     60    name = None
     61    list_display = []
     62    fields = []
     63    form = [] #DEPRECATED
     64    form_display = []
     65    # list of field_names to filter on, if the field name is a one2many,

     66    # many2one or many2many field, the field name should be followed by a

     67    # field name of the related entity, eg : 'organization.name'

     68    list_filter = []
     69    list_charts = []
     70    list_actions = []
     71    list_size = (700,500)
     72    form_size = (700,500)
     73    form_actions = []
     74    form_title_column = None
     75    field_attributes = {}
     76

     77 -  def __init__(self, app_admin, entity):
     78      """

     79      @param app_admin: the application admin object for this application

     80      @param entity: the entity class for which this admin instance is to be used

     81      """
     82      from camelot.view.remote_signals import get_signal_handler
     83      self.app_admin = app_admin
     84      self.rsh = get_signal_handler()
     85      if entity:
     86        from model_thread import get_model_thread
     87        self.entity = entity
     88        self.mt = get_model_thread()
     89      #

     90      # caches to prevent recalculation of things

     91      #

     92      self.__field_attributes = dict()
     93

     94 -  def __str__(self):
     95      return 'Admin %s' % str(self.entity.__name__)
     96

     97 -  def getName(self):
     98      return (self.name or self.entity.__name__)
     99

    100 -  def getModelThread(self):
    101      return self.mt
    102

    103    @model_function
    104 -  def getFormActions(self, entity):
    105      return self.form_actions
    106

    107 -  def getRelatedEntityAdmin(self, entity):
    108      """

    109      Get the related admin class for an entity, optionally specify for which

    110      field of this admin's entity

    111      """
    112      related_admin = self.app_admin.getEntityAdmin(entity)
    113      if not related_admin:
    114        logger.warn('no related admin found for %s'%(entity.__name__))
    115      return related_admin
    116

    117    @model_function
    118 -  def getSubclasses(self):
    119      """

    120      Return admin objects for the subclasses of the Entity represented by this

    121      admin object

    122      """
    123      from elixir import entities
    124      return [e.Admin(self.app_admin, e)

    125              for e in entities

    126              if (issubclass(e, (self.entity, )) and

    127                  hasattr(e, 'Admin') and

    128                  e!=self.entity)]
    129

    130    @model_function
    131 -  def getFieldAttributes(self, field_name):
    132      """

    133      Get the attributes needed to visualize the field field_name

    134      @param field_name : the name of the field

    135      @return: a dictionary of attributes needed to visualize the field, those

    136      attributes can be:

    137       * python_type : the corresponding python type of the object

    138       * editable : bool specifying wether the user can edit this field

    139       * widget : which widget to be used to render the field

    140       * ...

    141      """
    142      try:
    143        return self.__field_attributes[field_name]
    144      except KeyError:
    145        from camelot.model.i18n import tr
    146        from sqlalchemy import orm
    147        from sqlalchemy.exceptions import InvalidRequestError
    148        from field_attributes import _sqlalchemy_to_python_type_
    149        default = lambda x: dict(python_type=str,

    150                                 length=None,

    151                                 editable=False,

    152                                 nullable=True,

    153                                 widget='str')
    154        attributes = default(field_name)
    155        mapper = orm.class_mapper(self.entity)
    156

    157        def get_entity_admin(target):
    158          try:
    159            admin_class = self.field_attributes[field_name]['admin']
    160            return admin_class(self.app_admin, target)
    161          except KeyError:
    162            return self.getRelatedEntityAdmin(target)
    163

    164        try:
    165          property = mapper.get_property(field_name, resolve_synonyms=True)
    166          if isinstance(property, orm.properties.ColumnProperty):
    167            type = property.columns[0].type
    168            python_type = _sqlalchemy_to_python_type_.get(type.__class__, default)
    169            attributes = python_type(type)
    170            attributes['nullable'] = property.columns[0].nullable
    171            attributes['default'] = property.columns[0].default
    172          elif isinstance(property, orm.properties.PropertyLoader):
    173            target = property._get_target_class()
    174            foreign_keys = property.foreign_keys
    175            if property.direction == orm.sync.ONETOMANY:
    176              attributes = dict(python_type=list,

    177                                length=None,

    178                                editable=True,

    179                                nullable=True,

    180                                widget='one2many',

    181                                create_inline=False,

    182                                backref=property.backref.key,

    183                                admin=get_entity_admin(target))
    184            elif property.direction == orm.sync.MANYTOONE:
    185              attributes = dict(python_type=str,

    186                                length=None,

    187                                editable=True,

    188                                #@todo: take into account all foreign keys instead of only the first one

    189                                nullable=foreign_keys[0].nullable,

    190                                widget='many2one',

    191                                admin=get_entity_admin(target))
    192            elif property.direction == orm.sync.MANYTOMANY:
    193              attributes = dict(python_type=list,

    194                                length=None,

    195                                editable=True,

    196                                nullable=True,

    197                                widget='one2many',

    198                                admin=get_entity_admin(target))
    199            else:
    200              raise Exception('PropertyLoader has unknown direction')
    201        except InvalidRequestError:
    202          """

    203          If the field name is not a property of the mapper, then use the default

    204          stuff

    205          """
    206          pass
    207        attributes.update(dict(blank=True,

    208                               validator_list=[],

    209                               name=field_name.replace('_', ' ').capitalize()))
    210        try:
    211          for k, v in self.field_attributes[field_name].items():
    212            if k!='admin':
    213              attributes[k] = v
    214        except KeyError:
    215          pass
    216        attributes['name'] = tr(attributes['name'])
    217        self.__field_attributes[field_name] = attributes
    218        return attributes
    219

    220    @model_function
    221 -  def getColumns(self):
    222      """

    223      The columns to be displayed in the list view, returns a list of pairs of

    224      the name of the field and its attributes needed to display it properly

    225

    226      @return: [(field_name,

    227                {'widget': widget_type,

    228                 'editable': True or False,

    229                 'blank': True or False,

    230                 'validator_list':[...],

    231                 'name':'Field name'}),

    232               ...]

    233      """
    234      return [(field, self.getFieldAttributes(field))

    235              for field in self.list_display]
    236

    237    @model_function
    238 -  def getFields(self):
    239      if self.form or self.form_display:
    240        fields = self.getForm().get_fields()
    241      elif self.fields:
    242        fields = self.fields
    243      else:
    244        fields = self.list_display
    245      fields_and_attributes =  [(field, self.getFieldAttributes(field)) for field in fields]
    246      return fields_and_attributes
    247

    248 -  def getForm(self):
    249      from forms import Form, structure_to_form
    250      if self.form or self.form_display:
    251        return structure_to_form(self.form or self.form_display)
    252      return Form([f for f, a in self.getFields()])
    253

    254    @model_function
    255 -  def getListCharts(self):
    256      return self.list_charts
    257

    258    @model_function
    259 -  def getFilters(self):
    260      """Return the filters applicable for these entities each filter is

    261

    262      @return: [(filter_name, [(option_name, query_decorator), ...), ... ]

    263      """
    264      from filters import structure_to_filter
    265

    266      def filter_generator():
    267        from filters import GroupBoxFilter
    268        for structure in self.list_filter:
    269          filter = structure_to_filter(structure)
    270          yield (filter, filter.get_name_and_options(self))
    271

    272      return list(filter_generator())
    273

    274 -  def createValidator(self, model):
    275      from validator import Validator
    276      return Validator(self, model)
    277

    278    @model_function
    279 -  def setDefaults(self, entity_instance):
    280      """Set the defaults of an object"""
    281      from sqlalchemy.schema import ColumnDefault
    282      for field,attributes in self.getFields():
    283        try:
    284          default = attributes['default']
    285          if isinstance(default, ColumnDefault):
    286            default_value = default.execute()
    287          elif callable(default):
    288            import inspect
    289            args, varargs, kwargs, defs = inspect.getargspec(default)
    290            if len(args):
    291              default_value = default(entity_instance)
    292            else:
    293              default_value = default()
    294          else:
    295            default_value = default
    296          logger.debug('set default for %s to %s' % \

    297                      (field, unicode(default_value)))
    298          setattr(entity_instance, field, default_value)
    299        except KeyError,e:
    300          pass
    301

    302    @gui_function
    303 -  def createNewView(admin, parent=None, oncreate=None, onexpunge=None):
    304      """Create a QT widget containing a form to create a new instance of the

    305      entity related to this admin class

    306

    307      The returned class has an 'entity_created_signal' that will be fired when a

    308      a valid new entity was created by the form

    309      """
    310

    311      from PyQt4 import QtCore
    312      from PyQt4 import QtGui
    313      from PyQt4.QtCore import SIGNAL
    314      from proxy.collection_proxy import CollectionProxy
    315      new_object = []
    316

    317      @model_function
    318      def collection_getter():
    319        if not new_object:
    320          entity_instance = admin.entity()
    321          if oncreate:
    322            oncreate(entity_instance)
    323          # Give the default fields their value

    324          admin.setDefaults(entity_instance)
    325          new_object.append(entity_instance)
    326        return new_object
    327

    328      model = CollectionProxy(admin, collection_getter, admin.getFields,

    329                              max_number_of_rows=1)
    330      validator = admin.createValidator(model)
    331

    332      class NewForm(QtGui.QWidget):
    333

    334        def __init__(self, parent):
    335          super(NewForm, self).__init__(parent)
    336          self.setWindowTitle('New %s'%(admin.getName()))
    337          self.widget_layout = QtGui.QVBoxLayout()
    338          self.form_view = admin.createFormView('New', model, 0, parent)
    339          self.widget_layout.insertWidget(0, self.form_view)
    340          self.setLayout(self.widget_layout)
    341          self.validate_before_close = True
    342          self.entity_created_signal = SIGNAL("entity_created")
    343

    344        def validateClose(self):
    345          if self.validate_before_close:
    346            self.form_view.widget_mapper.submit()
    347            if model.hasUnflushedRows():
    348

    349              def validate():
    350                return validator.isValid(0)
    351

    352              def showMessage(valid):
    353                if not valid:
    354                  messages = u'\n'.join(validator.validityMessages(0))
    355                  reply = QtGui.QMessageBox.question(self, u'Could not create new %s'%admin.getName(),

    356                  u"\n%s\n Do you want to lose your changes ?"%messages, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
    357                  if reply == QtGui.QMessageBox.Yes:
    358                    # clear mapping to prevent data being written again to the model, after we

    359                    # reverted the row

    360                    self.form_view.widget_mapper.clearMapping()
    361

    362                    def onexpunge_on_all():
    363                      if onexpunge:
    364                        for o in new_object:
    365                          onexpunge(o)
    366

    367                    admin.mt.post(onexpunge_on_all)
    368                    self.validate_before_close = False
    369                    from camelot.view.workspace import get_workspace
    370                    for window in get_workspace().subWindowList():
    371                      if window.widget() == self:
    372                        window.close()
    373                else:
    374                  def create_instance_getter(new_object):
    375                    return lambda:new_object[0]
    376

    377                  for o in new_object:
    378                    self.emit(self.entity_created_signal,

    379                              create_instance_getter(new_object))
    380                  self.validate_before_close = False
    381                  from camelot.view.workspace import get_workspace
    382                  for window in get_workspace().subWindowList():
    383                    if window.widget() == self:
    384                      window.close()
    385

    386              admin.mt.post(validate, showMessage)
    387              return False
    388            else:
    389              return True
    390          return True
    391

    392        def closeEvent(self, event):
    393          if self.validateClose():
    394            event.accept()
    395          else:
    396            event.ignore()
    397

    398      form = NewForm(parent)
    399      form.setMinimumSize(admin.form_size[0], admin.form_size[1])
    400      return form
    401

    402    @gui_function
    403 -  def createFormView(admin, title, model, index, parent):
    404      """Creates a Qt widget containing a form view, for a specific row of the

    405      passed query; uses the Admin class

    406      """
    407      logger.debug('creating form view for index %s' % index)
    408      from controls.formview import FormView
    409      form = FormView(title, admin, model, index)
    410      return form
    411

    412    @gui_function
    413 -  def createSelectView(admin, query, search_text=None, parent=None):
    414      """Returns a QT widget that can be used to select an element from a query,

    415

    416      @param query: sqlalchemy query object

    417

    418      @param parent: the widget that will contain this select view, the returned

    419      widget has an entity_selected_signal signal that will be fired when a

    420      entity has been selected.

    421      """
    422      from controls.tableview import TableView
    423      from PyQt4 import QtCore
    424      from PyQt4.QtCore import SIGNAL
    425

    426      class SelectView(TableView):
    427

    428        def __init__(self, admin, parent):
    429          TableView.__init__(self, admin, search_text=search_text, parent=parent)
    430          self.entity_selected_signal = SIGNAL("entity_selected")
    431          self.connect(self, SIGNAL('row_selected'), self.sectionClicked)
    432

    433        def sectionClicked(self, index):
    434          # table model will be set by the model thread, we can't decently select

    435          # if it has not been set yet

    436          if self.table_model:
    437

    438            def create_constant_getter(cst):
    439              return lambda:cst
    440

    441            def create_instance_getter():
    442              entity = self.table_model._get_object(index)
    443              return create_constant_getter(entity)
    444

    445            def create_emit_and_close(selectview):
    446

    447              def emit_and_close(instance_getter):
    448                selectview.emit(self.entity_selected_signal, instance_getter)
    449                from camelot.view.workspace import get_workspace
    450                for window in get_workspace().subWindowList():
    451                  if window.widget() == selectview:
    452                    window.close()
    453

    454              return emit_and_close
    455

    456            self.admin.mt.post(create_instance_getter, create_emit_and_close(self))
    457

    458      widget = SelectView(admin, parent)
    459      widget.resize(admin.list_size[0], admin.list_size[1])
    460      return widget
    461

    462    @gui_function
    463 -  def createTableView(self, query, parent=None):
    464      """Returns a QT widget containing a table view, for a certain query, using

    465      this Admin class; the table widget contains a model QueryTableModel

    466

    467      @param query: sqlalchemy query object

    468

    469      @param parent: the workspace widget that will contain the table view

    470      """
    471

    472      from PyQt4 import QtCore
    473      from controls.tableview import TableView
    474      from proxy.queryproxy import QueryTableProxy
    475      tableview = TableView(self)
    476      admin = self
    477

    478      def createOpenForm(self, tableview):
    479

    480        def openForm(index):
    481          from workspace import get_workspace
    482          model = QueryTableProxy(tableview.admin,

    483                                  tableview.table_model._query_getter,

    484                                  tableview.admin.getFields,

    485                                  max_number_of_rows=1)
    486          title = u'%s' % (self.getName())
    487

    488          formview = tableview.admin.createFormView(title, model, index, parent)
    489          get_workspace().addSubWindow(formview)
    490          formview.show()
    491

    492        return openForm
    493

    494      tableview.connect(tableview,

    495                        QtCore.SIGNAL('row_selected'),

    496                        createOpenForm(self, tableview))
    497

    498      return tableview
    499